前言
记录 LeetCode 刷题时遇到的字符串相关题目,第一篇
344.反转字符串-头尾双指针
简单的头尾双指针,初始化一个头指针指向0号元素,尾指针指向最后一个元素,每次循环交换两个指针指向的位置然后头指针向右移尾指针向左移重复循环,直到两个指针相遇,就反转结束
public void reverseString(char[] s) {
int length = s.length;
int left = 0;
int right = length - 1;
while (left < right){
char temp = s[left];
s[left] = s[right];
s[right] = temp;
left++;
right--;
}
}
541.反转字符串2
顺着344题的思路然后进行模拟
public String reverseStr(String s, int k) {
if(s == null) return "";
char[] chars = s.toCharArray();
int length = s.length();
int kk = k << 1;
int i = 0;
int j;
while (i < length){
j = i + kk - 1;
//根据题意设计分支条件
if(j < length){
reverseString(chars,i,i + k - 1);
}else{
int rest = length - i;
if(rest >= k){
reverseString(chars,i,i + k - 1);
}else{
reverseString(chars,i,length - 1);
}
break;
}
i = j + 1;
}
//由于toString()方法不是单纯把字符数组变成字符串,还会携带中括号以及逗号,空格,所以要替换掉
return Arrays.toString(chars).replace("[","")
.replace("]","")
.replace(" ","")
.replace(",","");
}
public void reverseString(char[] s,int begin,int end) {
while (begin < end){
char temp = s[begin];
s[begin] = s[end];
s[end] = temp;
begin++;
end--;
}
}
然而,根据字符数组得到对应的字符串有更为方便和直接的方法就是在String构造函数中直接传入字符数组,所以最后的return语句应该改为:
return new String(chars);
151.翻转字符串里面的单词
直接想到的做法,就是遍历字符串,找出所有单词,然后逆序重组成题目要的新字符串
public String reverseWords(String s) {
int length = s.length();
List<String> list = new ArrayList<>();
for (int i = 0; i < length; i++) {
if(s.charAt(i) != ' '){
int j = i;
//找到以i为开头的单词的结尾
while (j + 1 < length && s.charAt(j + 1) != ' '){
j++;
}
list.add(s.substring(i,j + 1));
i = j + 1;
}
}
StringBuilder res = new StringBuilder();
int size = list.size();
//因为需要加上空格,字符串最后又不能有空格,所以加空格的操作不能放在循环中加单词后进行,
//这里先把最后一个单词放进去,然后再循环放单词前先加空格
if(size > 0){
res.append(list.get(size - 1));
}
for (int i = size - 2; i >= 0; i--) {
res.append(" ").append(list.get(i));
}
return res.toString();
}
557.反转字符串中单词
将字符串转化为字符数组,然后在字符数组上原地反转,找到每个单词,然后使用头尾双指针进行反转
public String reverseWords(String s) {
char[] c = s.toCharArray();
int len = s.length();
int i = 0;
int start;
char tmp;
while(i < len){
//记录单词的开始位置
start = i;
//找到单词的结束位置(空格的前一个字符)
while(i < len && c[i] != ' '){
i++;
}
int r = i - 1;
//头尾交换字符
while(start < r){
tmp = c[start];
c[start] = c[r];
c[r] = tmp;
start++;
r--;
}
//i指向下个单词的起始字符
i += 1;
}
return new String(c);
}
还了解到了另一个思路:先移除所有空格,再翻转整个字符串,再翻转每个单词。多利用翻转的思想,实现常数级空间复杂度
28.实现strStr()-KMP
其实return haystack.indexOf(needle)
就可以过题了,但意义肯定不是这样。所以就学了KMP来解这道题。
学习这个算法首先要知道这个算法好在哪里:我们知道匹配文本串haystack跟模式串needle,肯定需要去遍历两个字符串,使用暴力法去匹配的话做法可能就是在文本串中从i=0开始判断[i,i + needle.length() - 1]与needle是否相同,不相同的话i+1,但其实[i,i + needle.length() - 1]中可能会存在某部分的后缀是有效的,并没有被利用起来,而是从新的i开始一个个字符判断。所以KMP处理了这个问题,把每次匹配失败的字符串的有效部分以某种形式保存下来(就是代码中的next数组)。而且文本串的所有元素只可能会被遍历一次。
整个算法的难点应该集中在next数组如何生成。关于next数组的生成以及使用next数组完成整个匹配过程的解析在下面代码中的注释:
public int strStr(String haystack, String needle) {
//如果模式串是空串直接返回0
if(needle.length() == 0){
return 0;
}
int j = -1;
int[] next = getNext(needle);
int i;
int needleLength = needle.length();
for (i = 0; i < haystack.length() && j != needleLength - 1;) {
//next保存的是前一个前缀的下标,在匹配文本串和模式串时出现不相符的情况时要回退到前一个相符的字符串的前缀然后重新匹配,
//此时跳回去的next[j]是符合条件的有效的字段,所以要从next[j]+1开始与文本串匹配,所以为了统一成匹配j + 1的格式,j从-1开始
if(haystack.charAt(i) == needle.charAt(j + 1)){
i++;
j++;
}else if(haystack.charAt(i) != needle.charAt(j + 1) && j != -1){
//如果当前文本串的字符与模式串的下一个字符不相同,模式串就要回退到上次记录到的前缀,
//也就是KMP的算法思想,将之前检索过的有效部分保留下来
j = next[j];
}else {
//如果既不匹配,也没有前缀可以回退(j == -1),那就只能移动i,在文本串中找下一个字符进行匹配
i++;
}
}
if(j == needleLength - 1){
//由for循环中的第一个if可知,最后一个字符匹配成功后i也会++,所以要减一
return i - 1 - needleLength + 1;
}
return -1;
}
//生成模式串的next数组。前缀表指的是最长相等前缀和后缀的长度,如果长度减一那么得到的就是前缀最后一个字符的下标,
//更适合在代码中利用,因此以前缀表的数值减一作为next数组的元素值
//next[i]表示[0,i]子串的最长前缀的最后一个字符的下标
//例如"abcab",next[0]=-1,next[1]=-1,next[2]=-1,next[3]=0,next[4]=1
public int[] getNext(String needle){
int length = needle.length();
int[] next = new int[length];
//初始化next[0]值为-1,因为0下标在前缀表对应的值为0
//而且值为-1也表示[0,i]没有最长前缀
next[0] = -1;
//next[0]已经赋值所以从next[1]开始计算
for (int i = 1; i < length; i++) {
//如果i对应字符与[0,i-1]子串的最长前缀的下一个字符相同,那么[0,i-1]子串的最长前缀加上其下一个字符就是[0.i]子串的最长前缀
if(needle.charAt(i) == needle.charAt(next[i - 1] + 1)){
next[i] = next[i - 1] + 1;
}else{
//如果i对应字符与[0,i-1]子串的最长前缀的下一个字符不相同,我们就要往前找更短的最长相等前缀,而且同时还要保证我们找的前缀在[0,i-1]子串中有对应的相等后缀,
//为了这一点,k的取值是k = next[k],而不是k = next[k-1]。也就是说,对于[0,i]这个子串,我们要找到和 [0.i-1]这个子串的某个后缀和i指向的字符构成的新的后缀
//相等的前缀,那么这个前缀的最后一个字符就和i指向的字符相同,剩下的部分和那个新的后缀中的[0.i-1]这个子串的后缀相同,为了这个条件,我们找的[0,i-1]的前缀必须是
//之前出现的最长相等前缀,所以k = next[k]
int k = next[i - 1];
//如果找到某个之前出现过的前缀的下一个字符与i字符相同,那么这个前缀就和[0,i-1]的某个后缀相同,这个前缀的下一个字符就和i字符相同,那么这个前缀和他的下一个字符
//构成的就符合[o,i]子串的最长相等前缀,根据next数组存放的是最长相等前缀的最后一个字符的下标,next[i]存放的就应该是k + 1。因为k==-1时,不一定是回到next[0]了,可能是中间的时候某个子串也没有最长前缀,对应的next值也为0
while (k != -1 && needle.charAt(k + 1) != needle.charAt(i)){
k = next[k];
}
//如果找不到那样的前缀,说明没有最长相等前缀存在,next[i]自然就为-1;不过要注意,当k回退到-1时,要看看0字符跟i字符是否相等,相等那next[i]应该等于0,也就还是k + 1。如"ababcaabc"
if(k == -1 && needle.charAt(0) != needle.charAt(i)){
next[i] = -1;
}else {
next[i] = k + 1;
}
}
}
return next;
}
459.重复的子字符串-KMP
尝试计算一下存在重复子字符串的字符串的前缀表(或 next 数组),会发现有一定规律。下面代码中的 next 数组是前缀表不是前缀表的值减一。也就是 next 数组的值是最长相等前缀的长度而不是前缀中最后一个字符的下标
如果字符串存在最长相等前缀且存在重复的子字符串,那么这个前缀肯定是由这个子字符串重复构成,那么整个字符串减去这个前缀后剩下的部分也会是这个子字符串,只要判断整个字符串的长度和剩下的部分的长度是否是倍数关系就可以了。
用数学语言来说,假设这个重复的子串长为 len,那么最长相等前缀的长度肯定为 n*len,那么总的字符串长度就是 (n+1)*len,判断取余 len 的结果是否为 0 即可
public boolean repeatedSubstringPattern(String s) {
int length = s.length();
int[] next = new int[length];
next[0] = 0;
for (int i = 1; i < length; i++) {
if(s.charAt(i) == s.charAt(next[i - 1])){
next[i] = next[i - 1] + 1;
}else{
int k = next[i - 1];
while (k != 0 && s.charAt(k) != s.charAt(i)){
k = next[k - 1];
}
//额外判断一下第一个字符是否与s[i]相同
if(k == 0 && s.charAt(0) != s.charAt(i)){
next[i] = 0;
}else {
next[i] = k + 1;
}
}
}
//如果最后一个字符没有最长相等前缀那肯定不存在重复的子字符串
if(next[length - 1] != 0 && length % (length - next[length - 1]) == 0){
return true;
}
return false;
}
面试题 01.06. 字符串压缩
public String compressString(String S) {
int len = S.length();
if(len == 0 || len == 1) return S;
char[] c = S.toCharArray();
int fast = 0;
int slow = 0;
int count = 0;
StringBuilder sb = new StringBuilder();
while(fast < len){
if(c[fast] == c[slow]){
fast++;
count++;
}
else{
sb.append(c[slow])
.append(count);
slow = fast;
count = 0;
}
}
//最后的一段,跳出循环后没有被append到
String res = sb.append(c[slow])
.append(count)
.toString();
//长度没有变短就返回原字符串
return res.length() >= len ? S : res;
}
415. 字符串相加
从右往左遍历两个字符串每一位进行相加即可,注意可能有进位
public String addStrings(String num1, String num2) {
char[] c1 = num1.toCharArray();
int index1 = c1.length - 1; //转换为字符数组的话下标从大到小遍历
char[] c2 = num2.toCharArray();
int index2 = c2.length - 1;
int more = 0,cur = 0;
StringBuilder res = new StringBuilder(""); //字符串拼接操作较多应使用StringBuilder,使用String的话在编译时也是会转换为StringBuilder
/*****节约代码量的写法
while(index1 >= 0 || index2 >= 0){
if(index1 < 0) cur = c2[index2--] - '0' + more;
else if(index2 < 0) cur = c1[index1--] - '0' + more;
else cur = c1[index1--] - '0' + c2[index2--] - '0' + more;
more = 0;
if(cur > 9){
more = 1;
cur -= 10;
}
res.append((char)(cur + '0'));
}
*/
while(index1 >= 0 && index2 >= 0){
cur = c1[index1--] - '0' + c2[index2--] - '0' + more;
more = 0;
if(cur > 9){
more = 1;
cur -= 10;
}
res.append((char)(cur + '0'));
}
while(index1 >= 0){
cur = c1[index1--] - '0' + more;
more = 0;
if(cur > 9) {
more = 1;
cur -= 10;
}
res.append((char)(cur + '0'));
}
while(index2 >= 0){
cur = c2[index2--] - '0' + more;
more = 0;
if(cur > 9) {
more = 1;
cur -= 10;
}
res.append((char)(cur + '0'));
}
if(more > 0) res.append(more);
return res.reverse().toString();
}