代码随想录刷题记录:字符串篇

前言

字符串篇啦!

344. 反转字符串

题目描述:
代码随想录刷题记录:字符串篇_第1张图片
思路描述:
之前其实说过,一般情况下对于数据的线性操作我们都可以采用多指针法来做(一般双指针即可),本题一样,我们使用一个left指向最左边,一个right指向最右边,然后我们从两边往中间进行遍历,每遍历一个就进行一次交换即可。
代码如下:

class Solution {
    public void reverseString(char[] s) {
        //双指针法
        int left = 0;
        int right = s.length - 1;
        while(left < right){
            char temp = s[left];//交换变量temp
            s[left] = s[right];
            s[right] = temp;
            left++;
            right--;
        }
    }
}

541. 反转字符串 ||

题目描述:
代码随想录刷题记录:字符串篇_第2张图片

思路分析:
这题和上面那个题差不多,但是有一个诡异的地方就是题目给的很有歧义(反正我理解出 了问题),总之题意概括一下就为一句话:
每隔2k个反转前k个字符组成的子串,若该子串不够k个的时候则全部反转。
就上面的问题来说,还是蛮简单的:

class Solution {
    public String reverseStr(String s, int k) {
        //把字符转化为字符数组
        char[] str = s.toCharArray();
        //i+=2*k,这是细节呀挖槽
        //这样的设计可以满足i索引每一次循环正好达到2*k的位置
        for(int i=0; i<s.length(); i+=2*k){
            int left = i;//定义左指针
            //判断是str.length-1小还是left+k-1小
            //str.length是字符数组的大小
            //left+k-1是要反转的前k个字符
            int right = Math.min(str.length-1,left+k-1);
            //进行交换
            while(left < right){
                char temp = str[left];
                str[left] = str[right];
                str[right] = temp;
                left++;
                right--;
            }
        }
        //将字符数组转化为字符串
        s = String.valueOf(str);
        return s;
    }
}

05. 替换空格

题目描述:
代码随想录刷题记录:字符串篇_第3张图片
思路描述:
我靠,这个支架用一个String类中的库函数replace就可以了。

class Solution {
    public String replaceSpace(String s) {
        return s.replace(" ","%20");
    }
}

但是一般情况下我们最好还是不要直接用,知道就行了。
emmm另外一种方法就是暴力了呗,遍历数组,遇到空格加%20,没遇到空格那么正常添加该字符即可:

class Solution {
    public String replaceSpace(String s) {
        StringBuilder res = new StringBuilder();
        for(Character c : s.toCharArray())
        {
            if(c == ' ') res.append("%20");
            else res.append(c);
        }
        return res.toString();
    }
}

151. 翻转字符串里的单词

代码随想录刷题记录:字符串篇_第4张图片
思路:
不使用Java内置方法实现会比较有难度,过程如下:
1.去除首尾以及中间多余空格
2.反转整个字符串
3.反转各个单词

代码如下:

class Solution {
    public String reverseWords(String s) {
        //不适用java内置库来实现
        //1、去除首尾以及中间多余空格
        StringBuilder sb = removeSpace(s);
        //2、反转整个字符串
        reverseString(sb,0,sb.length()-1);
        //3、反转各个单词
        reverseEachWord(sb);
        return sb.toString();
    }

    //去除多余空格函数
    private StringBuilder removeSpace(String s){
        //双指针法
        int left = 0;
        int right = s.length() - 1;
        //去除首尾部分的空格
        while(s.charAt(left) == ' ') left++;
        while(s.charAt(right) == ' ') right--;
        //去除中间部分空格
        StringBuilder sb = new StringBuilder();
        while(left <= right){
            char c = s.charAt(left);
            //当该字符不为空格或者sb的最后一个位置字符不为空时
            //这样很微妙的可以使得每个单词之间正好间隔一个空格
            //这个技巧可以记一下,非常微妙
            if(c != ' ' || sb.charAt(sb.length() - 1) != ' '){
                //添加进结果字符串sb中
                sb.append(c);
            }
            left++;
        }
        System.out.println(sb.toString());
        return sb;
    }

    //反转字符串指定区间[left,right]的字符
    private void reverseString(StringBuilder sb,int left,int right){
        //逐个字符进行反转
        while (left < right) {
            char temp = sb.charAt(left);
            //setCharAt(index,newChar);
            //该方法用来将指定索引处的字符替换为新字符
            sb.setCharAt(left, sb.charAt(right));
            sb.setCharAt(right, temp);
            left++;
            right--;
        }
    }

    //反转每个单词
    private void reverseEachWord(StringBuilder sb) {
        int left = 0;
        int right = 1;
        int n = sb.length();
        while (left < n) {
            //right指针取到每一个单词的最后一个非空格位置
            while (right < n && sb.charAt(right) != ' ') {
                right++;
            }
            //进行反转字符串(就是反转每个单词了,left索引和right索引正好包含一个单词)
            reverseString(sb, left, right - 1);
            //左指针移到下一个单词开头
            left = right + 1;
            //右指针移到下一个单词开头的第二个位置
            right = left + 1;
        }
    }

}

58. ||. 左旋转字符串

题目描述:
代码随想录刷题记录:字符串篇_第5张图片
思路:
暴力模拟。
代码如下:

class Solution {
    public String reverseLeftWords(String s, int n) {
        //暴力模拟就完事儿
        StringBuilder sb = new StringBuilder();
        //第一次for循环取出n位以后的字符
        for(int i=n; i<s.length(); i++){
            sb.append(s.charAt(i));
        }
        //第二次for循环取出n位以前的字符
        for(int i=0;i<n;i++){
            sb.append(s.charAt(i));
        }
        //将结果转化为字符串返回
        return sb.toString();
    }
}

28. 实现strStr()(使用KMP算法)

题目描述:
代码随想录刷题记录:字符串篇_第6张图片
思路:
KMP算法的典型应用,顺便好好的把KMP算法给理解一波。

假如现在有两个字符串,一个是文本串aabaabaaf,另一个是模式串aabaaf。
我们现在要做的是找到该模式串第一次在文本串中出现的位置或者该模式串是否被包含在该文本串中。
使用KMP算法的第一步就是要先得到一个前缀表,即我们熟知的next数组。
这个表怎么得到?
得到前缀表之前我们首先要明确什么是前缀什么又是后缀:

前缀是包含首字母且不包含尾字母的所有子串都称为前缀串,简称前缀。
以上面的模式串aabaaf为例,它的前缀串为a、aa、aab、aaba、aabaa;但是aabaaf则不是前缀串。
同理,后缀即只包含尾字母不包含首字母的所有子串。
还是以aabaaf为例,它的后缀串为:f、af、aaf、baaf、abaaf;
但是aabaaf则不是前缀串。

现在我们要找的就是最长相等前后缀(就是普遍认知上的最长公共前后缀,一个意思),我们对模式串进行逐个分析:

对子串a来说,既无前缀也无后缀(参考前后缀定义),所以为最长相等前后缀长度为0;
对aa,前缀为a,后缀为a,所以最长相等前后缀为1;
同理对于aab,最长前缀为aa,最长后缀为ab,但不相等,所以最长相等前后缀为0;
依次类推,对于aaba,最长相等前后缀为1;
对于aabaa,最长相等前后缀为2;
对于aabaaf,最长相等前后缀为0;

综上,我们就能够得到一个序列,0、1、0、1、2、0;
这个序列实际上就是我们针对于模式串aabaaf的前缀表:
代码随想录刷题记录:字符串篇_第7张图片
现在我们使用这个表来进行KMP匹配:
代码随想录刷题记录:字符串篇_第8张图片
从上图可以看到当匹配到b与f时,是出现矛盾即不匹配的时候,那么现在我们就要得到模式串的最大相等前后缀的长度,然后让回溯指针跳到与其相等的前缀的后面:

以上面为例,我们可以知道该模式串aabaaf的最长相等前后缀长度为2(前缀表中是有记录的,查看发生冲突位置处的前一个位置即最大相等前后缀,这里为2),那么我们现在就让指针跳到前缀后面一位即可,以指前缀表数组下标对应关系如下:
代码随想录刷题记录:字符串篇_第9张图片
那么让指针到前缀后面的位置即下标为2的位置即可,从此处又开始遍历查找。

循环上述查找过程即可。
有些kmp算法的具体实现中会有将next数组整体右移(-1、0、1、0、1、2)或者整体-1(-1、0、-1、0、1、-1)的操作,都是为了方便匹配不需要去找下一个罢了,但思想都是一样的,具体问题具体分析:

整体右移之后不难发现,右移之后那么发生冲突位置处的next数组元素值就是所求的最长相等前后缀的值。
整体-1这个感觉和原始的方式没什么区别…因为-1之后再次查找指针回溯位置还是要+1的,所以就不管了。

如何得到next数组呢?
伪代码总共四步为:
1、初始化数组
2、找出前后缀不相同情况
3、找出前后缀相同情况
4、更新next前缀数组的元素值

得到next数组代码如下:

private void getNext(int[] next, String s) {
        int j = 0;
        next[0] = 0;
        for (int i = 1; i < s.length(); i++) {
            while (j > 0 && s.charAt(j) != s.charAt(i)) 
                j = next[j - 1];
            if (s.charAt(j) == s.charAt(i)) 
                j++;
            next[i] = j; 
        }
    }

上面这一段可以再有如下解释:
关于指针回溯求next的理解
每次求next[i],可看作前缀与后缀的一次匹配,在该过程中就可以用上之前所求的next,若匹配失败,则像模式串与父串匹配一样,将指针移到next[j-1]上。
求next过程实际上是dp(动态规划),只与前一个状态有关:
若不匹配,一直往前退到0或匹配为止
若匹配,则将之前的结果传递:
因为之前的结果不为0时,前后缀有相等的部分,所以j所指的实际是与当前值相等的前缀,可视为将前缀从前面拖了过来,就不必将指针从前缀开始匹配了,所以之前的结果是可以传递的

完整代码带注释如下:

class Solution {
    public int strStr(String haystack, String needle) {
        //特判
        if(needle.length() == 0) return 0;
        //创建前缀表数组
        int[] next = new int[needle.length()];
        //得到这个前缀表
        getNext(next,needle);

        //用KMP算法循环求解
        int j=0;
        for(int i=0; i<haystack.length(); i++){
            while(j>0 && needle.charAt(j) != haystack.charAt(i)){
                j = next[j-1];//回到前缀表的前一个位置
            }
            if(needle.charAt(j) == haystack.charAt(i)){
                j++;
            }
            //j与needle.length()相等时说明找到了
            if(j == needle.length()) return i-needle.length()+1;
        }
        //退出循环则说明没有找到
        return -1;       
    }
    //获得前缀表方法
        //该方法需要一个前缀空表,以及模式串
        private void getNext(int[] next,String s){
            //1、初始化
            //j指向前缀末尾位置
            //j同时也表示i之前包括i子串在内的最长相等前后缀的长度
            int j=0;
            next[0] = 0;//前缀表第一个子串为单个字符,无前后缀,即最长前后缀长度为0
            //for循环查找所有子串的最长相等(公共)前后缀
            //针对这一段解释如下:
            //关于指针回溯求next的理解
            //每次求next【i】,可看作前缀与后缀的一次匹配,在该过程中就可以用上之前所求的next,若匹配失败,则像模式串与父串匹配一样,将指针移到next【j-1】上。
            //求next过程实际上是dp(动态规划),只与前一个状态有关:
            //若不匹配,一直往前退到0或匹配为止
            //若匹配,则将之前的结果传递:
            //因为之前的结果不为0时,前后缀有相等的部分,所以j所指的实际是与当前值相等的前缀,可视为将前缀从前面拖了过来,就不必将指针从前缀开始匹配了,所以之前的结果是可以传递的
            for(int i=1; i<s.length();i++){//i指向后缀末尾位置
                //2、找出前后缀不相同情况
                while(j>0 && s.charAt(i) != s.charAt(j)) j=next[j-1];
                //3、找出前后缀相同的情况
                if(s.charAt(i) == s.charAt(j)) j++;
                //更新next数组
                next[i] = j;
            }
        } 
}

459. 重复的子字符串

代码随想录刷题记录:字符串篇_第10张图片
思路分析:
也是典型的KMP算法求解,只是判断时稍有差别,具体看代码中的注释:

class Solution {
    public boolean repeatedSubstringPattern(String s) {
        //特判
        if(s.length() == 0) return false;
        //创建next前缀表
        int[] next = new int[s.length()];
        //得到next前缀表
        getNext(next,s);

        int len = s.length();
        //最长相等前后缀的长度为:next[len - 1] 
        //如果该值不为0,则说明该字符串有重复的子字符串
        //如果len % (len - (next[len - 1])) == 0 ,则说明 (数组长度-最长相等前后缀的长度) 正好可以被数组的长度整除,说明该字符串肯定是由若干个该子字符串组成的
        if(next[len - 1] != 0 && len % (len - (next[len - 1])) == 0){
            return true;
        }
        return false;   
    }
    //求前缀表方法
    public void getNext(int[] next, String s){
        int j =0;
        next[0] = 0;
        for(int i=1 ;i<s.length();i++){
            //先找不相同前后缀
            while(j > 0 && s.charAt(i) != s.charAt(j)){
                j = next[j-1];
            }
            //找相同前后缀
            if(s.charAt(i) == s.charAt(j)) j++;
            //刷新next数组
            next[i] = j;
        }
    }
}

你可能感兴趣的:(动态规划,算法,java,字符串)