代码随想录 字符串模块小结

字符串操作其实和数组的操作是差不多的,本篇文章也是以做笔记为主,主要记录一下遇到字符串题型的几种做法。

1、双指针

2、反转系列

3、kmp

目录

一、双指针法

二、翻转系列

2.1、间隔反转

2.2、全局反转 + 局部反转

2.2.1、反转字符串的单词

2.2.2、左旋字符串

三、KMP算法

3.1、最长公共前后缀

3.2、计算前缀表

3.3、用next数组进行匹配


一、双指针法

字符串本质也是序列,可以用数组的常规做法去做,或者将字符串转换成数组来处理。

比如经典的字符串翻转。344. 反转字符串 - 力扣(LeetCode)

用到的就是双指针法,通过首尾两个指针指向的字符相互交换,使首尾指针不断靠近,最终翻转整个字符串。

需要注意的点在进入while()的判断条件,是选择左闭右开还是左闭右闭。

左闭右开,则为while(left <= right),因为left和right可以相等;

左闭右闭,则为while(left < right),因为left和right不可以相等。

代码如下

class Solution {
    public void reverseString(char[] s) {
        // 双指针 左闭右闭
        int left = 0;
        int right = s.length - 1;
        while (left <= right) {
            char t = s[left];
            s[left] = s[right];
            s[right] = t;
            left++;
            right--;
        }
    }
}

二、翻转系列

翻转系列可以说是字符串的常考类型,比较常见的类型是隔几个翻转一次,以及整体反转 + 局部反转。

2.1、间隔反转

题目参考LC_541.

541. 反转字符串 II - 力扣(LeetCode)

这道题读懂题目很重要,题目描述是隔2k个字符就反转一次前k个字符,同时每次要判断剩余字符的个数,并作出不同的处理。

隔2k个字符就反转一次前k个字符,其实就是每隔k个字符,反转k个字符。

剩余的不足k个则全部反转 大于k个则继续反转前k个

那么这道题可以分为几部去解决:

1、既然每隔2k个反转一次,那么可以考虑在循环中将步长调整为2k,这样i就是反转区间的左边界,这样左边界是一定能确定下来的。

2、反转右边界会根据字符串剩余长度发生改变,因此每次循环时都要判断右边界是否超过了字符串长度,如果超过了,说明剩余字符已经不足k个了,则全部反转,然后下次直接退出循环;如果没超过,说明剩余字符大于等于k个,那么就反转前k个。

3、用双指针法反转指定区间的字符串。

这样可以保证长度不为0的字符串最少可以反转一次。

具体代码如下

class Solution {
    public String reverseStr(String s, int k) {
        char[] c = s.toCharArray();
        int right = 0;
        // 每隔k个反转k个 剩余的不足k个则全部反转 大于k个则反转前k个
        for (int i = 0; i < s.length(); i += 2 * k) { // 步长为2k,也是左边界
            right = i + k - 1;   // 反转区间的右边界
            if (i + k <= s.length()) {  // 至少有k个 直接反转前k个
                reverse(c, i, right);
            }else reverse(c, i, s.length() - 1);  // 否则反转剩余全部
            
        }
        return new String(c);
    }
}

也可以换一种思路,直接看right与字符串长度的大小关系,选小的那个作为新的右边界。 

class Solution {
    public String reverseStr(String s, int k) {
        char[] c = s.toCharArray();
        int right = 0;
        // 每隔k个反转k个 剩余的不足k个则全部反转 大于k个则反转前k个
        for (int i = 0; i < s.length(); i += 2 * k) { // 步长为2k,也是左边界
            right = i + k - 1;
       
            // 换种思路 剩余字符如果小于2k 则下次循环会直接退出
            // 此次循环 当前右边界如果在长度范围内,说明至少有k个,右边界为right,反转前k个
            // 当前右边界如果不在长度范围内 则剩余字符全部反转 也就是右边界为s.length() - 1;
            // 因此 可以比较right和s.length() - 1 选小的那个为新的右边界
            int newRight = Math.min(right, s.length() - 1);
            reverse(c, i, newRight);
        }
        return new String(c);
    }
}

2.2、全局反转 + 局部反转

这里以LC_151以及剑指offer_58为例。

151. 反转字符串中的单词 - 力扣(LeetCode)

剑指 Offer 58 - II. 左旋转字符串 - 力扣(LeetCode)

2.2.1、反转字符串的单词

这道题最直接的做法就是,去除首位空格,再用spilt + 反向写的方式,需要使用库函数。

class Solution {
    public String reverseWords(String s) {
        int start = 0;
        int end = s.length() - 1;
        // 记录首尾第一次不为空格的字符的下标
        while (start < s.length() && s.charAt(start) == ' ') start++;
        while (end >= 0 && s.charAt(end) == ' ') end--;
        String s1 = s.substring(start, end + 1);   // 切割
        if (s1.length() == 0) return "";    // 空串 直接返回

        String[] spl = s1.split(" +");  // split分割 参数为正则 匹配多个空格
        StringBuilder sb = new StringBuilder();
        for (int i = spl.length - 1; i > 0; i--) {  // 反着加
            sb.append(spl[i]);   // 每加一个单词就加一个空格
            sb.append(" ");
        }
        sb.append(spl[0]);   // 第一个单词加进去
        return sb.toString();
    }
}

但如果要在原字符串上进行操作,并且不使用库函数,难度会稍微大一些。

去除空格的思路还是一样,先去除首尾冗余空格,再去除中间一些多余的空格,保留每个单词之间只有一个空格间隙。

   public void deleteSpace(StringBuilder str, String s) {
        int end = s.length() - 1;
        int start = 0;
        String temp = "";
        
        while (start < s.length() && s.charAt(start) == ' ') {
            start++;
        }
        while (end >= 0 && s.charAt(end) == ' ') {
            end--;
        }
        // 处理中间空格
        while (start <= end) { // <= 取到所有字符
            char ch = s.charAt(start);
            // 若获取的字符不为空或者新的字符串最后一个不为空格 则插入字符
            if (ch != ' ' || str.charAt(str.length() - 1) != ' ') {
                str.append(ch);
            }
            start++;
        }
    }

此时就只剩反转问题了,如何将单词的顺序反转?

这里就可以用全局反转 + 局部反转的思路

比如字符串" the sky is blue "

  • 移除多余空格 : "the sky is blue"
  • 字符串反转:"eulb si yks eht"
  • 单词反转:"blue is sky the"

全局反转可以用首尾双指针,轻车熟路了。

局部反转,即反转每一个单词,可以使用快慢指针,当发现空格时,就确定下来一个单词,然后再整体反转这个单词就行。

   public void reverseEachWord(StringBuilder s) {
        // 快慢指针反转
        int low = 0;
        int fast = 1;
        while (fast < s.length()) {
            // fast指向空格就停止
            while (fast < s.length() && s.charAt(fast) != ' ') {
                fast++;
            }
            reverse(s, low, fast - 1);  // 反转该单词
            low = fast + 1;             // 更新指针指向 保持与循环前的位置一致
            fast = low + 1;
        }
    }

整体代码如下

class Solution {
    public String reverseWords(String s) {
        
        // 副本字符串
        StringBuilder str = new StringBuilder();
        // 去除多余空格 即首尾多余空格
        deleteSpace(str,s);
        // 字符串反转 按照指定区间
        reverse(str,0,str.length() - 1);
        // 字符串局部翻转 每个单词反转
        reverseEachWord(str);
        return str.toString();
    }

    public void deleteSpace(StringBuilder str, String s) {
        int end = s.length() - 1;
        int start = 0;
        String temp = "";
        boolean flag = true;
        while (start < s.length() && s.charAt(start) == ' ') {
            start++;
        }
        while (end >= 0 && s.charAt(end) == ' ') {
            end--;
        }
        // 处理中间空格
        while (start <= end) { // <= 取到所有字符
            char ch = s.charAt(start);
            // 若获取的字符不为空或者新的字符串最后一个不为空格 则插入字符
            if (ch != ' ' || str.charAt(str.length() - 1) != ' ') {
                str.append(ch);
            }
            start++;
        }
    }

    public void reverse(StringBuilder s, int start, int end) {
        // 双指针反转
        int len = s.length();
        while (start < end) {
            char temp = s.charAt(start);
            s.setCharAt(start, s.charAt(end));
            s.setCharAt(end, temp);
            end--;
            start++;
        }
    }

    public void reverseEachWord(StringBuilder s) {
        // 快慢指针反转
        int low = 0;
        int fast = 1;
        while (fast < s.length()) {
            while (fast < s.length() && s.charAt(fast) != ' ') {
                fast++;
            }
            reverse(s, low, fast - 1);
            low = fast + 1;
            fast = low + 1;
        }
    }
}

2.2.2、左旋字符串

观察发现,也可以用全局反转 + 局部反转

代码如下

class Solution {
    public String reverseLeftWords(String s, int n) {
        // 整体反转 + 局部反转
        char[] c = s.toCharArray();
        reverseString(c, 0, c.length - 1);  // 全局反转 
        reverseString(c, 0, c.length - n - 1);  // 反转前length - n个
        reverseString(c, c.length - n, c.length - 1);   // 反转倒数n个
        return new String(c);
    }

    public void reverseString(char[] s, int left, int right) {
        // 双指针 左闭右闭
        while (left <= right) {
            char t = s[left];
            s[left] = s[right];
            s[right] = t;
            left++;
            right--;
        }
    }
}

三、KMP算法

KMP的经典思想就是:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。

以LC_28为例

28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)

这道题可以用两层循环直接去匹配,每次匹配上模式串的首字符时,就继续往下匹配,如果能匹配到尾,则就算匹配完成,否则就退出继续迭代文本串进行匹配。

代码如下

class Solution {
    public int strStr(String haystack, String needle) {
        int len1 = haystack.length();
        int len2 = needle.length();
        if (len1 < len2) return -1;
        for (int i = 0; i < len1; i++) {
            if (haystack.charAt(i) == needle.charAt(0)) {  // 第一个匹配上了
                int index = 1;
                while (index < len2 && index + i < len1) {  // 看看后边还能不能匹配上
                    if (haystack.charAt(index + i) != needle.charAt(index)) break;
                    index++;
                }
                if (index == len2) return i;   // 说明遍历完了needle 找到了第一次出现的位置
            }
        }
        return -1;
    }
}

这种做法比较直接,一旦匹配不上,之后再想匹配就要从头开始遍历模式串needle。

而kmp的核心,就是避免从头再去做匹配。

用卡哥的动画来描述kmp算法非常清晰。

那么现在要解决的问题就是,当匹配不上的时候,如何知道该回退到哪个地方,才不至于从头开始?

kmp的做法是:使用前缀表记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

3.1、最长公共前后缀

最长公共前后缀的概念:

前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串

后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串

比如aaba,前缀包括a、aa、aab;后缀包括aba、ba、a。

那么最长公共前后缀为a,长度为1。

那么当匹配失败时,怎么进行回退?前缀表记录的是最长公共前后缀,匹配失败的位置是后缀子串的后面,那么只要找到与其相同的前缀的后面重新匹配就可以了。

通俗点说,最长相等后缀在前缀中已经匹配过了,所以直接在最长前缀的后面再往后匹配就行,因此避免了从头开始匹配。

 

3.2、计算前缀表

前缀表如果不减去1,那么就next数组与前缀表就是相同的。

以下就是求模式串的next数组,之后通过next数组去进行匹配。

next[i]表示:下标i之前(包括i)的字符串中,最长的公共前后缀长度。

直接贴出代码实现

   public void getNext(String s, int[] next) {
        int j = 0;
        next[0] = j;
        for (int i = 1; i < next.length; i++) {
            // 注意是while 匹配不上就一直回退
            while (j > 0 && s.charAt(i) != s.charAt(j)) {  
                // 回退
                j = next[j - 1];
            }
            if (s.charAt(i) == s.charAt(j)) {
                j++;
            }
            next[i] = j;
        }
    }

得到了next数组后,就用next数组来进行匹配。

3.3、用next数组进行匹配

匹配的逻辑大致分为三步:

1、若文本串与字符串的字符匹配不上,那么j要一直回退。

2、若文本串与字符串的字符匹配上了,则 i 和 j 一起往后走。

3、如果 j == needle.length(),则说明模式串已经匹配完成了,直接返回模式串首次出现位置。

完整代码如下

class Solution {
    public int strStr(String haystack, String needle) {
        if (needle.length() == 0) {
            return 0;
        }
        int[] next = new int[needle.length()];
        getNext(needle, next);
        // 用获取到的next进行匹配
        int j = 0;
        for (int i = 0; i < haystack.length(); i++) {
            while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
                j = next[j - 1];
            }
            if (haystack.charAt(i) == needle.charAt(j)) {
                j++;
            }
            if (j == needle.length()) {
                return i - needle.length() + 1;   // 注意这里要 +1
            }
        }
        return -1;
    }

    public void getNext(String needle, int[] next) {
        // 定义两个下标 i指向后缀末尾 j指向前缀末尾 且next数组整体要减一
        int j = 0;
        next[0] = j;
        for (int i = 1; i < next.length; i++) { // 需要j的位置与i比较 则i要从1位置开始 指向后缀末尾
            // 处理前后缀末尾不相同的情况
            // 要求最长公共缀,所以遇到相同的就停下
            while (j > 0 && needle.charAt(i) != needle.charAt(j)) {
                // 让j回退
                j = next[j - 1];
            }
            // 处理前后缀相同的情况
            if (needle.charAt(i) == needle.charAt(j)) {
                j++;
            }
            // next数组记录前后缀的公共长度
            next[i] = j;
        }
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值