数据结构算法刷题--字符串

1、反转字符串

  • 题目:https://leetcode.cn/problems/reverse-string/
  • 思路:双指针,左指针从数组首部开始,右指针从数组末尾开始;每两个数的交换可以按常规的创建一个中间变量,也可以通过三次异或
    • a ^= b,此时a里面是 a^b
    • b ^= a,此时b里面是 b^(a^b) = a
    • a ^= b,此时a里面是 (a^b)^a = b

2、反转字符串Ⅱ

  • 题目:https://leetcode.cn/problems/reverse-string-ii/submissions/
  • 思路:整体就是在反转字符串基础上逻辑判断 2k 一个循环,然后最后剩余的一部分要注意右指针的起始位置可能够k个可能不够
    • 2k一循环,通过循环变量 i ,每次循环 i += 2*k,即可
    • 每个大循环内反转的右指针起始位置,可以通过 i + k - 1 和 s.length() - 1 中的小值选择

3、替换空格

  • 题目:https://leetcode.cn/problems/ti-huan-kong-ge-lcof/
  • 思路:先扩容、后移动替换
    • 先统计原字符串里面的空格个数,每一个空格在原字符串长度的基础上追加两个空格(可以使用StringBuilder统计追加的空格,再和原字符串拼接);
    • 进行替换的时候使用双指针法,要从后往前从原字符串选择字符串,在扩容后的字符串末尾从后往前移动或者替换为"%20",如果从前往后双指针操作当替换为"%20"的时候需要移动后面的字符造成不必要的操作。
  • 代码:
class Solution {
    // 双指针

    public String replaceSpace(String s) {
        // 1、特殊条件判断
        if(s == null || s.length() == 0){
            return s;
        }

        // 2、统计s中空格的个数,然后用一个 StringBuilder 来对每个空格多扩展两个槽位--注意StringBuilder只是s基础上增加的
        StringBuilder sb = new StringBuilder();
        for(int i = 0; i < s.length(); ++i){
            if(s.charAt(i) == ' '){
                sb.append("  ");
            }
        }

        // 3、双指针遍历,左指针从s末尾开始查字符,右指针从StringBuilder末尾开始填充
        // 字符直接挪过去,空格补充成%20
        int left = s.length() - 1;
        char[] ch = (s + sb).toCharArray();
        int right = ch.length - 1;
        while(left >= 0){
            // 如果该字符是字符串,通过右指针补成%20,从后往前操作避免了后面字符的移动
            if(s.charAt(left) == ' '){
                ch[right--] = '0';
                ch[right--] = '2';
                ch[right--] = '%';
            }else{
                // 字符,正常挪过来
                ch[right--] = s.charAt(left);
            }
            --left;
        }

        return new String(ch);
    }
}

4、翻转字符串里的单词

  • 题目:https://leetcode.cn/problems/reverse-words-in-a-string/
  • 思路:先整体反转,再局部反转每个单词,反转完每个单词后移动单词从而去除多余的空格,注意最后一个单词的处理
    • 整体反转,双指针封装一个反转字符串的自定义方法
    • 局部反转每个单词,利用一个变量i,遇到空格跳过后遇到的第一个非空格字符记录为一个单词的起点,然后一直找到下一个空格位置记录这个单词的结束位置;调用自定义方法实现这个单词的反转
    • 移动:设置一个全局变量k控制最终的结果中的索引,局部变量j从反转后的单词中取字符放到k控制的位置并同时向后移动,每个单词结束以后,控制k追加一个空格;但最后一个字符要注意,如果字符串中没有删除多余空格就不应该追加多余的空格,避免k索引越界;
    • 返回结果:因为最后可能多追加了一个空格,也可能之前没有删除空格恰好没追加,此时就应该控制返回的截止位置是去除一个空格后的k-1,还是不去除的k?只需要判断k是否恰好处在原字符串最后一个位置并且这个位置不为空格就可以了,满足就返回到k;否则删除一个索引返回到k-1.
  • 代码:
class Solution {

    /**
     * 思路:两次反转+移位
     * 以 "the sky is blue "为例
     * 1、反转整个字符串: " eulb si yks eth"
     * 2、反转单个单词,并有一定移位去除多余空格: " blue si yks eth" --> "blue si yks eth"
     */

    public String reverseWords(String s) {
        // 1、转为字符数组、反转整个字符串
        char[] ch = s.toCharArray();
        reverseString(ch, 0, ch.length - 1);

        // 2、设置变量i控制得到每个单词的起始、结束索引;
        // 设置实现移位的变量j、k,j实现从反转后的单词对应索引读取字符
        // k实现不考虑原字符串中的空格只填写j对应的字符,并适当追加单个空格
        int k = 0, wordEndIndex = 0;
        for(int i = 0; i < s.length(); ++i){
            // 跳开原字符串里面的空格
            if(ch[i] == ' '){
                continue;
            }

            // 记录当前这个单词的起始索引
            int wordStartIndex = i;
            // 移动i到下一个空格,获得当前这个单词的结束索引
            while(i < ch.length && ch[i] != ' '){
                ++i;
            }
            wordEndIndex = i - 1;

            // 反转当前单词
            reverseString(ch, wordStartIndex, wordEndIndex);

            // 移动操作,通过j 读取、k填充控制实现去除原字符串里面的空格(不管他在哪有几个)
            for(int j = wordStartIndex; j <= wordEndIndex; ++j){
                ch[k++] = ch[j];
            }
            // 在每个单词结束的时候追加空格,但要判断如果没有删除多余空格的话,最后不要越界
            if(k < ch.length){
                ch[k++] = ' ';
            }
        }

        // System.out.println(ch);
        // System.out.println(k);

        // 3、结果返回,注意返回的字符数组末尾如果有空格要去除
        // 什么情况下要返回整个修改后的字符数组?最后k和原字符串长度维持一致,并且最后一个字符不为' '
        return new String(ch, 0, (k - 1 == ch.length - 1) && (ch[k - 1] != ' ') ? k : k - 1);
    }

    public void reverseString(char[] ch, int left, int right){
        while(left < right){
            ch[left] ^= ch[right];
            ch[right] ^= ch[left];
            ch[left] ^= ch[right];
            ++left;
            --right;
        }
    }
}

5、左旋转字符串

  • 题目:https://leetcode.cn/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof/
  • 思路:先整体反转整个字符串,再分别局部反转后面n个字符和前面剩下的字符即可
    • 双指针法反转整个字符串,此时前n个到后面n位,后面剩下的到了前面,但前后的顺序都是反的;
    • 双指针局部反转后n个,调回其原来字符顺序
    • 局部反转前面剩下的,回到原来字符顺序
  • 代码实现:
class Solution {
    // 局部反转+整体反转
    public String reverseLeftWords(String s, int n) {
        // 1、转为字符数组
        char[] ch = s.toCharArray();
        int length = ch.length;

        // 2、整体反转
        reverseString(ch, 0, length - 1);

        // 3、反转最后n个
        reverseString(ch, length - n, length - 1);

        // 4、反转前面剩下的
        reverseString(ch, 0, length - n - 1);

        return new String(ch);

    }

    public void reverseString(char[] ch, int left, int right){
        while(left < right){
            ch[left] ^= ch[right];
            ch[right] ^= ch[left];
            ch[left] ^= ch[right];
            left++;
            right--;
        }
    }
}

6、实现strStr()

  • 题目:https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/
  • 思路:KMP模式匹配算法!!!避免重复遍历情况,当模式串与文本串匹配的过程中,当前面匹配了一部分,到某一个字符不匹配了,并不是要回退到两个串本轮匹配起始的下一个位置重新开始匹配。固定文本串不匹配位置索引;前缀表用来记录下一字符匹配不上时,以当前字符结尾的子串如果有最长相等前后缀,这里面的后缀和前缀相等,后缀又和文本串里面匹配上了,那么前缀自然也是匹配的,没必要再重复过来判断,直接判断前缀的下一位和文本串固定着的不匹配位置就ok了;如果匹配上了,各自向后移动一位再判定就是了,注意判断是否完全匹配完了。
    • 先构建前缀表
    • 初始化模式串匹配起始索引
    • 循环文本串字符,判断是否匹配
    • 不匹配:根据前缀表回退
    • 匹配:模式串索引后移一位等待与文本串下一字符的匹配判断
    • 判断模式串是否已经全部匹配上,是返回结果
  • 前缀表next构建:
    • 前缀指的是当前串不包括末尾字符的所有以第一个字符开始的子串,后缀指不包括当前串首个字符在内的所有以最后一个字符结尾的子串;
    • 构建步骤:首先初始化数据,next[0] = 0,只有一个字符的串根据前后缀定义不包含该字符,直接为0,初始化判定相等前后缀中控制前缀的索引 j;
    • 循环模式串的每个字符,统计其最长相等前后缀
    • 如果没匹配上,要回退 j,j 一定要回退到起始位置重新来判断吗?这里个人感觉又是一遍KMP模式匹配算法的体现,以模式串 "abcabdabcabx"为例,当计算x的前缀表的时候,j刚进来应该处在d的位置,这时候相当于在拿前面的"abcabd"去匹配"abcabx"了,同样的道理,d和x没匹配上,但是这个时候d前面的"abcab"和后面的"abcab"是匹配上了,我们这个时候回退 j,也应该考虑他前面的最长相等前后缀,d 前面紧挨的 ab 和 x 前面的 ab 是匹配的,那么前一个 ab 的相等前缀就也和 x 前面的 ab 匹配,直接判定 c 和 x 是否匹配就行了,即 j 回退到 next[j - 1]
    • 如果匹配上了,j 后移以为,i 通过 for 循环控制后移以为,等待下一位匹配判断。
  • 代码实现:
class Solution {
    // KMP模式匹配算法,固定文本串的当前索引,利用前缀表回退模式串要匹配的位置

    public int strStr(String haystack, String needle) {
        //  特殊情况排除
        if(haystack == null){
            return -1;
        }

        // 1、构建前缀表数组 next
        int[] next = new int[needle.length()];
        getNext(needle, next);

        // 2、初始化匹配索引
        // j -- 匹配串当前匹配到的位置;i——文本串当前被匹配的位置(不会回退)
        int j = 0;

        // 3、循环匹配
        for(int i = 0; i < haystack.length(); ++i){
            // System.out.println("i-->" + i);

            // 3.1、不匹配,一直回退到匹配或者模式串回到起点
            while(j > 0 && needle.charAt(j) != haystack.charAt(i)){
                j = next[j - 1];
            }

            // 3.2、匹配,控制模式串匹配索引后移一位
            if(needle.charAt(j) == haystack.charAt(i)){
                ++j;
            }
            // System.out.println("j-->" + j);
            // 3.3、判断是否完成整个的匹配
            if(j == needle.length()){
                return i - needle.length() + 1;
            }
        }

        return -1;
    }

    // 获得前缀表数组next
    public void getNext(String s, int[] next){
        // 1、初始化变量
        next[0] = 0;
        int j = 0;

        // 循环计算每个位置的最长相等前后缀——即不匹配时应该重新从哪里开始匹配
        for(int i = 1; i < s.length(); ++i){
            // 2、不匹配,回退
            while(j > 0 && s.charAt(j) != s.charAt(i)){
                // 回退,重点,这里其实也相当于一种匹配,通过kmp模式匹配思想,回退到j前一位的前缀表指向
                j = next[j - 1];
            }

            // 3、匹配,j向右挪动,代表(i+1)位置到时候不匹配的话应该从(j+1)开始重新匹配就行了
            if(s.charAt(j) == s.charAt(i)){
                j++;
            }

            // 4、记录当前i的前缀表值
            next[i] = j;
        }

    }
}

7、重复的子字符串

  • 题目:https://leetcode.cn/problems/repeated-substring-pattern/
  • 思路:使用的是KMP模式匹配算法思路,重点是分析出来如何将判断的条件结合起来了KMP——如果一个字符串由子串重复构成,那么最后一个元素的最长前缀表起始体现了这个子串,如 "abcabcabcabc",最长相等前后缀中前缀是前三个 abc,后缀是后三个 abc,可以看到整个字符串抛出后缀后,就是重复那个子串!此时,整个字符串的长度是 n*x,x是那个子串的长度,那么最长相等前后缀的长度就是 (n - 1) * x,所以有 n*x % x == 0,x可以通过字符串长度减去最长相等前后缀长度获得,而最长相等前后缀长度其实就是最后一个字符的前缀表值(前缀的下一位)。
    • 构建前缀表
    • 计算字符串和最长相等前后缀长度的差值;
    • 取余判断,注意避免字符串中没有相等前后缀的情况(在构建前缀表以后直接排除掉)。
  • 代码实现:
class Solution {
    // 利用kmp模式匹配找到s的前缀表中最长相等前后缀
    // 如果是由子串重复构成,二者之差就是单个重复子串
    // 所以s长度对(s长度-末尾元素最长相等前后缀长度)取余为0是即返回true

    public boolean repeatedSubstringPattern(String s) {

        // 1、构建前缀表
        int[] next = new int[s.length()];
        getNext(s, next);
        if(next[s.length() - 1] == 0){
            return false;
        }

        // 2、计算前后缀差出来的部分长度
        int diff = s.length() - next[s.length() - 1];
        System.out.println(diff);

        // 3、取余判断结果,注意要避免最后一个元素没有相等前后缀的情况
        if(s.length() % diff == 0){
            return true;
        }

        return false;
    }

    public void getNext(String s, int[] next){
        // 1、初始化
        next[0] = 0;
        int j = 0;

        // 遍历计算每个位置的最长前后缀
        for(int i = 1; i < s.length(); ++i){

            // 2、不匹配,回退
            while(j > 0 && s.charAt(j) != s.charAt(i)){
                j = next[j - 1];
            }

            // 3、匹配,让j多移动一位
            if(s.charAt(j) == s.charAt(i)){
                j++;
            }

            // 设置前缀表值
            next[i] = j;
            // System.out.println(next[i] + ",");
        }
    }
}

8、总结

  • 双指针法:反转字符串,替换空格(注意提前扩容,从后往前操作)
  • 反转字符串:整体反转和局部反转结合
  • KMP:当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。
    • 前缀表:起始位置到下标i之前(包括i)的子串中,有多大长度的相同前缀后缀
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值