题目描述

✅ 面试题 01.03. URL化

image-20260929105634304

题意分析

将字符串前 length 个字符中的每个空格替换为 %20,其他字符保持原样。length 是真实内容长度,之后的字符只是预留空间,不属于需要转换的内容。

题目保证尾部有足够空间容纳扩张后的结果,并明确要求 Java 使用字符数组直接操作。下面先说明顺序构建的思路,再给出利用预留空间、从后向前写入的字符数组解法。

解法:线性扫描 + 构建结果字符串

核心思路

[!blue]

每个字符的输出只取决于它是否为空格,与相邻字符无关。建立输出缓冲,从左到右扫描真实内容:空格追加三个字符 %20,非空格追加自身。处理完前 i 个字符时,缓冲里恰好就是这个前缀转换后的结果,因此扫描结束便得到完整答案。

扫描上界必须使用 length,不能使用整个字符串的物理长度。真实内容里的首尾空格和连续空格都是有效内容,需要逐个替换;只有 length 之后的预留字符应被忽略,不能用裁剪首尾空格来区分两者。

解题步骤

  1. 建立空输出缓冲区。
  2. 只扫描前 length 个字符。
  3. 空格追加 %20,其他字符追加自身。
  4. 返回缓冲区内容。

length == 0 时没有有效字符,结果为空串。Go 的缓冲初始化为长度零、容量 length,之后通过 append 增长;容量只是预分配空间,不能把它误设为已经存在的输出长度。

代码实现

class Solution {
    public String replaceSpaces(String S, int length) {
        StringBuilder sb = new StringBuilder();

        // 只读取真实内容长度,忽略后面的预留字符。
        for (int i = 0; i < length; i++) {
            char c = S.charAt(i);

            if (c == ' ') {
                sb.append("%20");
            } else {
                sb.append(c);
            }
        }

        return sb.toString();
    }
}
func replaceSpaces(S string, length int) string {
    buf := make([]byte, 0, length)
    // 只读取真实内容长度,忽略后面的预留字符。
    for i := 0; i < length; i++ {
        if S[i] == ' ' {
            buf = append(buf, '%', '2', '0')
        } else {
            buf = append(buf, S[i])
        }
    }
    return string(buf)
}

复杂度分析

  • 时间复杂度:$O(length+1)$,每个有效字符产生至多三个字符。
  • 空间复杂度:$O(length+1)$,保存结果缓冲。

关键点总结

[!green]

  • 真实内容长度决定读取边界。
  • 每个空格单独展开,连续空格不能合并。
  • 顺序追加避免在字符串中间反复插入。

解法二:字符数组从后向前替换

核心思路

[!blue]

先把字符串转换为可修改的字符数组或字节切片,只统计前 length 个位置中的空格数 spaces。每个空格由一个字符变成三个字符,多占两个位置,所以结果长度是 newLength = length + 2 * spaces,题目保证原数组能容纳它。

如果从前往后直接扩张,写入的 %20 可能覆盖尚未读取的内容。因此用 read 从真实内容末尾向前读,用 write 从结果末尾向前写。普通字符写入一个位置;空格在末尾三个位置写入 %20,然后把写指针向前移动三格。

处理当前位置前,write - read 等于尚未处理部分中空格数的两倍,所以写指针不会落在读指针左侧。遇到空格时,这个差值至少为二,写入的三个位置也都不早于当前读位置,不会覆盖未读取的前缀。两指针向左推进后,已经写好的后缀正是原内容对应后缀的转换结果,直到全部读完。

最后只取数组前 newLength 个字符作为结果,不能把仍未使用的尾部预留空间一起返回。这里是在转换得到的可变数组内部完成替换;字符串参数本身不可修改。

解题步骤

  1. 将 S 转换为可变数组,统计真实内容中的空格数。
  2. 算出结果长度,初始化读指针和写指针。
  3. 从后向前读取,普通字符写一次,空格写成 %20。
  4. 返回前 newLength 个字符组成的字符串。

代码实现

class Solution {
    public String replaceSpaces(String S, int length) {
        char[] chars = S.toCharArray();
        int spaces = 0;

        for (int i = 0; i < length; i++) {
            if (chars[i] == ' ') {
                spaces++;
            }
        }

        int newLength = length + 2 * spaces;
        int write = newLength - 1;

        for (int read = length - 1; read >= 0; read--) {
            if (chars[read] == ' ') {
                chars[write--] = '0';
                chars[write--] = '2';
                chars[write--] = '%';
            } else {
                chars[write--] = chars[read];
            }
        }

        return new String(chars, 0, newLength);
    }
}
func replaceSpaces(S string, length int) string {
    buf := []byte(S)
    spaces := 0

    for i := 0; i < length; i++ {
        if buf[i] == ' ' {
            spaces++
        }
    }

    newLength := length + 2*spaces
    write := newLength - 1

    for read := length - 1; read >= 0; read-- {
        if buf[read] == ' ' {
            buf[write] = '0'
            buf[write-1] = '2'
            buf[write-2] = '%'
            write -= 3
        } else {
            buf[write] = buf[read]
            write--
        }
    }

    return string(buf[:newLength])
}

复杂度分析

  • 时间复杂度:$O( S + 1)$,包含把整个字符串转换为数组;实际扫描和替换只处理真实内容及其扩张结果。
  • 空间复杂度:$O( S + 1)$,当前字符串接口需要可变数组及返回字符串。若输入本来就是具有足够预留空间的字符数组,替换过程本身只需 $O(1)$ 额外空间。

关键点总结

[!green]

  • 先统计空格,才能确定扩张后写入的终点。
  • 从后向前写入,利用 write >= read 保护未读取的前缀。
  • 只返回扩张后的有效长度,不返回多余预留字符。

易错点总结

[!yellow]

  • 处理整个 S:把预留空白也替换进结果。
  • 先 trim:真实内容的首尾空格被丢掉。
  • 空格替换后又追加原空格:输出多出字符。
  • Go 缓冲初始化为非零长度再 append:结果前面会多出默认零字节。

相似题目

题目 难度 关联与区别
剑指 Offer 05. 替换空格 简单 空格替换规则相同,本题只处理真实长度前的内容,尾部预留区不能当成待替换字符。
1089. 复写零 简单 同样涉及输出扩张,但原题直接修改固定数组并截断,本题结果必须覆盖全部真实内容。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2022/38563642
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!