前言
本系列的文章为笔者学习《剑指offer第二版》后的笔记整理以及Java实现,解法不保证与书上一致。
另外,我最近在系统整理一些 Java 后台方面的面试题和参考答案,有找工作需求的童鞋,欢迎关注我的 Github 仓库,如果觉得不错可以点个 star 关注 :
题目描述
请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为“We Are Happy.”则经过替换之后的字符串为“We%20Are%20Happy.”。
解题思路
通常,按照习惯性思维,我们总习惯从左到右处理字符串。当遇到第一个空格时,将其换成“%20”并将其后的所有字符都往后移动两个位置;当遇到第二个空格时,将其替换成“%20”并将其后的所有字符都往后移动两个位置…正因为这样,导致很多字符移动了很多次。这种情况下,假设字符串长度为 n,对于每个空格而言都需要移动其后面 O(n) 个字符,因此对于含有 O(n) 个空格的字符串而言,总的时间复杂度为 O(n^2)。
这种从前往后遍历的方式需要那么高的时间复杂度,那么能不能减少一些元素移动的次数呢?我们不妨反过来从后往前试试。
- 先遍历一遍字符串,统计空格字符的数目;
- 每替换一个空格,长度增加 2(相当于将一个空格字符变成了
%20
的三个字符),因此替换后字符串的总长度为原来的长度加上空格的数目乘以 2; - 设置两个指针:
indexOfOld
和indexOfNew
,分别指向原始字符串的末尾和替换之后的字符串的末尾。接下来,不断地将indexOfOld
处的字符移动到indexOfNew
,同时向前(左)移动两个指针;如果indexOfOld
处为空格字符,则indexOfOld
向前移动一格,而indexOfNew
指针前插入字符串“%20”,并向前移动 3 格。
从后往前的解法中,由于所有的字符都只复制(移动)一次,因此这个算法的时间复杂度为 O(n),比从前往后遍历的思路要块很多。
/**
* 请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为
* “We Are Happy.”则经过替换之后的字符串为“We%20Are%20Happy.”。
*
* @author Rotor
* @since 2019/9/29 22:53
*/
public class ReplaceBlank {
public void replaceBlank(StringBuffer str) {
if (str == null) {
return;
}
// 统计空格数目
int oldStrLength = 0;
int numOfBlank = 0;
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) == ' ') {
numOfBlank++;
}
}
// 原字符串的尾指针
int indexOfOld = str.length() - 1;
// 设置新的长度
str.setLength(str.length() + 2 * numOfBlank) ;
// 新的字符串的末尾指针
int indexOfNew = str.length() - 1;
// 当新的字符串的末尾指针和原字符串的末尾指针指向同一位置时,表明所有空格都已替换完毕
while (indexOfOld >= 0 && indexOfNew > indexOfOld) {
if (str.charAt(indexOfOld) == ' ') {
str.setCharAt(indexOfNew--, '0');
str.setCharAt(indexOfNew--, '2');
str.setCharAt(indexOfNew--, '%');
} else {
str.setCharAt(indexOfNew--, str.charAt(indexOfOld));
}
indexOfOld--;
}
}
}
相关题目
题目描述
有两个排序的数组 A1 和 A2,内存在 A1 的末尾有足够多的空余空间容纳 A2。请实现一个函数,把 A2 中的所有数字插入 A1 中,并且所有的数字是排序的。
解题思路
首先很容易想到对 A1 从前往后复制数字,将 A1 和 A2 中的元素从前往后比较,并插入到 A1 中,然后往后移动剩余数字,但这种方式会重复多次移动一个数字,时间效率很低。
我们可以换个方式,从后往前遍历。因为从题目中可知,A1、A2 是有序的,并且 A1 中有足够的空余空间容纳 A2 中的元素。所以可以这么做:
- 设置两个指针分别指向 A1 和 A2 的最后一个元素,往后从后往前依次遍历并比较,将值较大的元素移动到 A1 的末尾;
- 继续向前移动值较大的那个元素的指针,并不断重复上述过程;
- 当某个数组中的值取完了,则直接从另一个数组继续取元素。
package com.offers.chapter2.question05;
import java.util.Arrays;
/**
* 相关题目:有两个排序的数组 A1 和 A2,内存在 A1 的末尾有足够多的空余空间容纳 A2。
* 请实现一个函数,把 A2 中的所有数字插入 A1 中,并且所有的数字是排序的。
*
* @author Rotor
* @since 2019/9/30 9:46
*/
public class Merge2SortedArray {
public static void merge(Integer[] arrayA, Integer[] arrayB) {
// 统计A数组中有值元素的个数
int lengthOfA = 0;
for (int i = 0; i < arrayA.length; i++) {
if (arrayA[i] != null) {
lengthOfA++;
}
}
// 指向数组A中不为空的最后一个元素的尾指针
int pA = lengthOfA - 1;
// 指向数组B不为空的最后一个元素的尾指针
int pB = arrayB.length - 1;
// 指向合并后数组不为空的最后一个元素的尾指针
int pMerge = lengthOfA + arrayB.length - 1;
while (pMerge >= 0) {
// A 数组中元素取完了,接下来直接取 B 数组中的
if (pA < 0) {
arrayA[pMerge--] = arrayB[pB--];
// B 数组中元素取完了,接下来直接取 A 数组中的
} else if (pB < 0) {
arrayA[pMerge--] = arrayA[pA--];
// A 中元素更大,取A 中的
} else if (arrayA[pA] > arrayB[pB]) {
arrayA[pMerge--] = arrayA[pA--];
// B 中元素更大,取 B 中的
} else {
arrayA[pMerge--] = arrayB[pB--];
}
}
}
public static void main(String[] args) {
Integer[] arrayA = new Integer[10];
Integer[] arrayB = {1, 3, 5, 7, 9};
// 初始化 A 为 {2, 4, 6, 8, 10}
for (int i = 0; i < 5; i++) {
arrayA[i] = i * 2 + 2;
}
merge(arrayA, arrayB);
System.out.println("合并后的数组为:" + Arrays.toString(arrayA));
}
}
总结
- 在合并两个数组(包括字符串)时,如果从前往后复制每个数字(或字符串)则需要重复移动数字(或字符)多次,那么我们可以考虑从后往前复制,这样就能减少移动的次数,从而提高效率;
- 从前往后遍历的解法会将某些字符移动多次,而从后往前遍历的解法则只会每个字符都只移动一次;
- 替换空格之后,空格由一个字符变成了三个字符,因此字符串会变长。如果在原来字符串的基础上进行替换,就有可能覆盖修改在该字符串后面的内存。如果是创建新的字符串并在新的字符串上进行替换,则可以自己分配足够多的内存。
后记
如果你同我一样想要努力学好数据结构与算法、想要刷 LeetCode 和剑指 offer,欢迎关注我 GitHub 上的 LeetCode 题解:awesome-java-notes