数据结构与算法–字符串:字符串替换
字符串的优化
- 由于字符串在编程时候使用的评率非常高,为了优化,很多语言都对字符串做了特殊的规定。下面我们讨论java中字符串的特性
- java中的字符数组以’\0’ 结尾,我们可以利用这个特性来找到字符数组的末尾,而且为了节省内存,java将常量字符串存储在单独的一个区域,我们新申请内存的字符串存储在另外一个地方,总结就是:基本类型的的变量数据和对象的引用都是在栈里面,对象本身放再堆里面,显示的String常量放再常量池,String对象放再堆中。
常量池说明
- 常量池之前是放在方法区里面,也就是永久代,从JDK1.7开始移动到堆内存中,这个改变我们可以从oracle的 release version里面的 Important RFEs Addressed in JDK 7 看到
Area: HotSpotSynopsis: In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String.intern() method will see more significant differences.
String内存位置说明
- 显示的声明String常量
String str1 = new String("hello world");
String str2 = "hello world";
- 以上第一句执行后会在堆内存中创建了一个值为hello world的String对象,同时会在产量池中创建一个值为hello world的String对象
- 第二句执行的时候因为产量池中存在hello world对象,就不会再常量池中创建了hello world的String对象,直接栈中创建str2,并指向产量池中的hello world 地址。
- 验证以上:
public class StrSaveAddress {private static final String str3 = "hello world";public static void strMain() {String str1 = new String("hello world");String str2 = "hello world";String str4 = str3;String str5 = str3;System.out.println(str1 == str2);System.out.println(str1.equals(str2));System.out.println(str4 == str5);}public static void main(String[] args) {strMain();}
}
//输出:
false
true
true
- Java中的String类型比较特殊,修饰如下 public final class String,显然是不可改变的,所有我们每一次对String内容的我修改都会产生一个新的实例,比如toUpperCase转大写操作,这个操作的结果是生成一个新的String实例并且返回,原String内容并不会修改,因此原来的Hello world还是愿意的值。由此可见试图对String多次修改,每次都会有一个临时对象。这样开销大,内存浪费太多。java中定义了一个新的与字符串相关的StringBuilder,能容纳修改后的结果英雌如果多次修改,建议用StringBuilder。
算法题:替换空格
- 题目:请事先一个函数,吧字符串中的每个空格替换成%20,例如输出“this is my way”,则输出“this%20is%20my%20way ”
- 网络编程中,URL参数含义特殊字符,如空格,‘#’等,可能导致服务器无法获取正确的参数。我们将这些特殊符号换成服务器可识别的字符。转换后的规则就是在‘%’后面加上ASCII码的两位十六进制标识。比如空格的ASCII码是32,那么转16进制是0x20,因此我们得到的被替换的空格就是‘%20’ 这个是浏览器URL中%20的由来,再比如’#'的ASCII是35,十六进制是0x23,那么URL中就是%23
- 分析:题中需要将空格替换成‘%20’ 一个字符变三个字符,长度改变,如果在原字符上修改,必定会覆盖改字符串之后的数据。如果创建新的字符串,并在新字符串上修改替换,那么我们可以自己分配足够的内存,有这两种解决方法一下我们两种方法都来尝试解答。
原字符串修改,时间复杂度O(n^2) ,空间复杂度O(1)
-
最简单的做法,循环遍历字符串,每次碰到空格先移动之后字符,在替换空格。如下图流程:
-
第一步找到第一个空格
-
第二步,移动空格之后的字符2个位置,将空格替换成%20,彩色部分是需要移动的字符
-
第三步之后的流程和第二步一样,一直到遍历到最后一个字符
-
实现方式如下:
/*** @author liaojiamin* @Date:Created in 18:30 2020/11/3*/
public class StrReplace {/*** 时间复杂度O(n^2) 空间复杂度O(1)* @return a* @author: liaojiamin* @Param str 目标字符串 '\0' 结束* @Param replaceChar 需替换字符* @param length 字符数组总长度* @date: 19:10 2020/11/3*/public static char[] replaceFirst1(char[] str, char replaceChar, int length){if(null == str || str.length == 0){return str;}int originStrLength = 0;int replaceStrCount = 0;while(str[originStrLength] != '\0'){originStrLength ++;if(str[originStrLength] == replaceChar){replaceStrCount ++;}}int resultLength = replaceStrCount * 2 + originStrLength;if(resultLength > length){return str;}int i = 0;while(str[i] != '\0'){if(str[i] == ' '){for (int j = originStrLength; j > i; j--) {str[j+2] = str[j];}str[i++] = '%';str[i++] = '2';str[i++] = '0';originStrLength+=2;}else {i++;}}return str;}public static char[] getChar(String str, int length){char[] chars = new char[length];for (int i = 0; i < str.length(); i++) {chars[i] = str.charAt(i);}return chars;}public static void main(String[] args) {int length = 100;char[] chars = getChar("this is my way", length);System.out.println(new String(replaceFirst1(chars, ' ', length)));}
新字符串中修改,时间复杂度O(n) ,空间复杂度O(n)
-
新字符串中修改,也不难,我们先计算好替换之后需要的总内存,新申请合适的内存,接着只需要将老的字符串一个一个复制进去,碰到空格则替换成%20替换即可。如下步骤
-
第一步申请新的字符串计算结果长度,申请新容量内存空间
-
需要的总内存
-
第二步:遍历字符串,将字符按位复制进新的内存位置中,如果遇到空格则复制20%,如下图:
-
第三部:依第二步骤继续复制,得到最终结果:
-
实现方式如下:
public class StrReplace {/*** 时间复杂度O(n) 空间复杂度O(n)* @return a* @author: liaojiamin* @Param str 目标字符串 '\0' 结束* @Param replaceChar 需替换字符* @param length 字符数组总长度* @date: 19:10 2020/11/3*/public static char[] replaceFirst(char[] str, char replaceChar, int length){if(null == str || str.length == 0){return str;}int countStrNum = 0;int i = 0;while(str[i] != '\0'){i++;if(str[i] == replaceChar){countStrNum ++;}}if(countStrNum == 0){return str;}int resultStrLength = i + countStrNum*2;if(resultStrLength > length){return str;}char[] resultStr = new char[resultStrLength];int resultPosition = 0;for (int j = 0; j < i; j++) {char thisChar = str[j];if(thisChar == replaceChar){resultStr[resultPosition++]='%';resultStr[resultPosition++]='2';resultStr[resultPosition++]='0';}else {resultStr[resultPosition++] = thisChar;}}return resultStr;}public static char[] getChar(String str, int length){char[] chars = new char[length];for (int i = 0; i < str.length(); i++) {chars[i] = str.charAt(i);}return chars;}public static void main(String[] args) {int length = 100;char[] chars = getChar("this is my way", length);System.out.println(new String(replaceFirst(chars, ' ', length)));}
}
最优解,原字符串中修改,时间复杂度O(n) ,空间复杂度O(1)
- 第一种思路中每遇到空格都会需要移动之后的字符,导致时间复杂度指数增加,我们需要更快捷的方法
- 依照以上的思路,我们先遍历一次字符串,统计空格数,计算最终大小,每个空格增加2位置,第一种思路是从头开始变量,导致位置的移动频繁,
- 我们从字符串最后开始复制替换,首先追捕我们需要准备两个指针,一个用来指向最终替换之后的位置,一个指向原来位置,如下图,s1指定字符串原来的长度末尾,s2指定替换之后的长度末尾。
- 我们从S1 处开始遍历,每遇到一个字符将S1位置复制到S2 位置,如果遇到空格则S2 位置复制%20三个字符如下图替换一个之后的效果图,遇到空格后,s2同时移动三个位置,s1 永远都移动一个位置:
- 最后持续依据第二步替换,得到最终结果,实现方式如下:
public class StrReplace {/*** 最优解:时间复杂度O(n) 空间复杂度O(1)* @return a* @author: liaojiamin* @Param str 目标字符串 '\0' 结束* @Param replaceChar 需替换字符* @param length 字符数组总长度* @date: 19:10 2020/11/3*/public static char[] replaceFirst2(char[] str, char replaceChar, int length){if(null == str || str.length == 0){return str;}int originStrLength = 0;int replaceStrCount = 0;while(str[originStrLength] != '\0'){originStrLength ++;if(str[originStrLength] == replaceChar){replaceStrCount ++;}}int resultLength = replaceStrCount * 2 + originStrLength;if(resultLength > length){return str;}int i = resultLength;while (originStrLength >= 0){if(str[originStrLength] == ' '){str[i--] = '0';str[i--] = '2';str[i--] = '%';}else {str[i--] = str[originStrLength];}originStrLength --;}return str;}public static char[] getChar(String str, int length){char[] chars = new char[length];for (int i = 0; i < str.length(); i++) {chars[i] = str.charAt(i);}return chars;}public static void main(String[] args) {int length = 100;char[] chars = getChar("this is my way", length);System.out.println(new String(replaceFirst2(chars, ' ', length)));}
}
测试用例
- 输入字符中包含空格
- 输入字符中没有空格
- 输入特殊字符:null
相关算法题型
- 题目:有两个排序的数组A1和A2,内存在A1 的末尾有足够多的空余空间容纳A2.实现一个函数,将A2中所有数组插入到A1 中并且所有数组是排序状态:
- 分析:同之前的思路,我们开始看到题目可能从头遍历A1 ,当这样会出现多次复制数字的情况。更好的方法是计算A1+A2总长度后,逐个比较A1,A2末尾数字,并将末尾数字大的放到指定的位置。算法实现如下:
public class SplicStr {/*** 时间复杂度O(n),空间复杂度O(1)* @author: liaojiamin* @description: 拼接两个排序的数组,target 足够容纳两个数组* @date: 14:54 2020/11/4** @return */public static int[] splicStr(int[] target, int[] origin){if(null == target || null == origin){return null;}int targetLength = 0;while (target[targetLength] != '\0'){targetLength ++;}int resultLength = targetLength + origin.length;if(target.length < resultLength){return null;}int resultPosition = resultLength - 1;int targetPosition = targetLength - 1;int originPosition = origin.length - 1;while (resultPosition >= 0){if(targetPosition >= 0 && originPosition >= 0&& target[targetPosition] > origin[originPosition]){target[resultPosition--] = target[targetPosition--];}else {target[resultPosition--] = origin[originPosition--];}}return target;}public static void main(String[] args) {int[] target = new int[20];int[] origin = new int[10];for (int i = 0; i < 10; i++) {target[i] = i+300;origin[i] = i*10;}int[] targetInt = splicStr(target, origin);for (int i : targetInt) {System.out.print(i + " ");}}
}
上一篇:数据结构与算法–数组:二维数组中查找
下一篇:数据结构与算法–排序算法总结