代码随想录——字符串篇

1、反转字符串

344.反转字符串

力扣题目链接

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。

不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。

示例 1:
输入:[“h”,“e”,“l”,“l”,“o”]
输出:[“o”,“l”,“l”,“e”,“h”]

示例 2:
输入:[“H”,“a”,“n”,“n”,“a”,“h”]
输出:[“h”,“a”,“n”,“n”,“a”,“H”]

思路

可使用左右双指针法,left = 0,right = 数组长度 - 1,两两交换位置,交换完成,左右指针同时往里收缩。

如下例子:

代码随想录——字符串篇_第1张图片

代码实现

public void reverseString(char[] s) {
       //相向双指针
       int left = 0, right = s.length - 1;
       //中间元素不需要动,所以临界不需要等于
       while(left < right){
         swap(s, left, right);
         left++;
         right--;
       } 
    }

    public void swap(char[] s, int i, int j){
        char temp = s[i];
        s[i] = s[j];
        s[j] = temp;
    }

2、反转字符串Ⅱ

541. 反转字符串II

力扣题目链接

给定一个字符串 s 和一个整数 k,从字符串开头算起, 每计数至 2k 个字符,就反转这 2k 个字符中的前 k 个字符。

  • 如果剩余字符少于 k 个,则将剩余字符全部反转。

  • 如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。

示例:

输入: s = “abcdefg”, k = 2
输出: “bacdfeg”

思路

依旧是反转字符串,在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,每次确定反转区间即可。

因为要找的也就是每2 * k 区间的起点,这样写,程序会高效很多。

所以当需要固定规律一段一段去处理字符串的时候,要想想在for循环的表达式上做做文章。

代码实现

    public String reverseStr(String s, int k) {
        char[] arr = s.toCharArray();
        // 左右双指针 i 左指针 j 右指针
        int j;
        for (int i = 0; i < s.length(); i += 2 * k) {
            j = i + k - 1;
            if (s.length() - 1 - i < k) {
                //兼容第一种情况,也是最后一次反转字符串
                swap(arr, i, s.length() - 1);
                break;
            } else {
                swap(arr, i, j);
            }
        }
        return new String(arr);
    }

	//字符串反转,左右双指针法
    public void swap(char[] chars, int i, int j) {
        while (i < j) {
            char temp = chars[i];
            chars[i] = chars[j];
            chars[j] = temp;
            i++;
            j--;
        }
    }

3、替换空格

题目:剑指Offer 05.替换空格

力扣题目链接

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

示例 1: 输入:s = “We are happy.”
输出:“We%20are%20happy.”

思路

这道题要是调用Java apis.replace(" ", "%20")将变得毫无意义。想把这道题目做到极致,首先扩充数组到每个空格替换成"%20"之后的大小。然后从后向前替换空格,也就是双指针法,过程如下:

i指向新长度的末尾,j指向旧长度的末尾。

其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。

这么做有两个好处:

  1. 不用申请新数组。
  2. 从后向前填充元素,避免了从前先后填充元素时 每次添加元素都要将添加元素之后的所有元素向后移动

代码实现

    public String replaceSpace(String s) {
        //最简洁,调Java api 毫无意义
        //return s.replace(" ", "%20");

        char[] ch = s.toCharArray();
        //记录空格个数
        int count = 0;
        for (char c : ch) {
            if (c == ' ') {
                count++;
            }
        }
        //将原数组扩容
        char[] newCh = Arrays.copyOf(ch, ch.length + count * 2);
        //双指针从后往前遍历,两个指针分别指向新旧数组末尾!!! 后面的指针追上了前面的指针说明前面已经没有空格了,直接结束循环
        // i -> new ; j -> old
        for (int i = newCh.length - 1, j = ch.length - 1; j < i; i--, j--) {
            if (ch[j] != ' ') {
                newCh[i] = ch[j];
            } else {
                newCh[i] = '0';
                newCh[i - 1] = '2';
                newCh[i - 2] = '%';
                i -= 2;
            }
        }
        return new String(newCh);
        
        -----------------------------------------
       //另一种解法
       char[] ch = s.toCharArray();
       //使用StringBuilder拼接
       StringBuilder sb = new StringBuilder();
       for(char c : ch){
           if(c == ' '){
               sb.append("%20");
           }else{
               sb.append(c);
           }
       }

       return sb.toString();
    }

4、反转字符串里的单词

151.翻转字符串里的单词

力扣题目链接

给定一个字符串,逐个翻转字符串中的每个单词。

示例 1:
输入: “the sky is blue”
输出: “blue is sky the”

示例 2:
输入: " hello world! "
输出: “world! hello”
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。

示例 3:
输入: “a good example”
输出: “example good a”
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

思路

我的解法:使用String#split函数分割原字符串,得到一个包含""" "或者单词的字符串数组,将所有的单词移至该数组前端记录单词数量,然后类似字符串反转使用双指针法对前端单词进行反转。


我们将整个字符串都反转过来,那么单词的顺序指定是倒序了,只不过单词本身也倒序了,那么再把单词反转一下,单词不就正过来了

最后将多余空格去掉就得到目标字符串。

所以解题思路如下:

  • 移除多余空格
  • 将整个字符串反转
  • 将每个单词反转

上述三个步骤,顺序可变。

举个例子,源字符串为:"the sky is blue "

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

代码实现

我的做法

	 /***
     * 我的做法(应该也还好吧)
     */
    public String reverseWords(String s) {
        //先将字符串分割 分割后可能包含 "" 或者 " "
        String[] strings = s.split(" ");
        int index = 0;
        //将单词移到数组前端
        for (String string : strings) {
            if ("".equals(string) || " ".equals(string)) {
                continue;
            }
            strings[index++] = string;
        }

        //双指针法反转单词
        int l = 0, r = index - 1;
        String temp;
        while (l < r) {
            temp = strings[l];
            strings[l] = strings[r];
            strings[r] = temp;
            l++;
            r--;
        }
        //拼接出最后结果
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < index; i++) {
            if (i == index - 1) {
                sb.append(strings[i]);
                break;
            }
            sb.append(strings[i]).append(" ");

        }
        return sb.toString();
    }

大神做法

  /**
     * 大神们的做法三部曲
     * 1、删除多余空格
     * 2、反转字符串
     * 3、将字符串中每个单词再次反转
     * 得到最后结果
     * 三个步骤顺序可变
     */
    public String reverseWords(String s) {
        String newStr = removeExtraSpace(s);
        char[] chars = newStr.toCharArray();

        //双指针寻找字符串中单词
        int j;
        for (int i = 0; i < chars.length; i++) {
            //不以 ' ' 开头,' '结束 刚好是一个单词
            if (chars[i] != ' ') {
                j = i;
                while (j < chars.length) {
                    char ch = chars[j];
                    if (ch == ' ') {
                        break;
                    }
                    j++;
                }
                //此时j指针已经指向' '
                swap(chars, i, j - 1);
                i = j;
            }
        }

        //反转完字符串里的单词后,最后将整个字符串反转就得到想要的结果
        swap(chars, 0 , chars.length - 1);
        return new String(chars);
    }

    /**
     * 移除多余空格
     */
    public String removeExtraSpace(String s) {
        StringBuilder sb = new StringBuilder();
        char[] chars = s.toCharArray();

        //双指针寻找字符串中单词
        int j;
        for (int i = 0; i < chars.length; i++) {
            //不以 ' ' 开头,' '结束 刚好是一个单词
            if (chars[i] != ' ') {
                j = i;
                //最后一个单词后面可能没有空格
                while (j < chars.length) {
                    char ch = chars[j];
                    if (ch == ' ') {
                        break;
                    }
                    j++;
                    sb.append(ch);
                }
                sb.append(" ");
                i = j;
            }
        }

        //去掉最后一个空格
        return sb.substring(0, sb.length() - 1);
    }

    /**
     * 字符串反转
     */
    public void swap(char[] arr, int i, int j) {
        char temp;
        while (i < j) {
            temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
            i++;
            j--;
        }
    }

5、左旋转字符串

题目:剑指Offer58-II.左旋转字符串

力扣题目链接

字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。

示例 1:
输入: s = “abcdefg”, k = 2
输出: “cdefgab”

示例 2:
输入: s = “lrloseumgh”, k = 6
输出: “umghlrlose”

限制:
1 <= k < s.length <= 10000

思路

最简单的做法。调用string类库函数s.substring(k) + s.substring(0, k)这种做法容易忽略其中细节,不推荐。


我们实际可以通过局部反转+整体反转 达到左旋转的目的。【与反转字符串里的单词类似】

具体步骤为:

  1. 反转区间为前n的子串
  2. 反转区间为n到末尾的子串
  3. 反转整个字符串

举例说明:输入:字符串abcdefg,n=2

代码随想录——字符串篇_第2张图片

最终得到左旋2个单元的字符串:cdefgab。【字符串反转得到充分使用,双指针法

代码实现

    public String reverseLeftWords(String s, int n) {
        //前n个元素反转,后面所有元素反转,最后再将整个字符串反转即得到答案
        char[] ch = s.toCharArray();
        swap(ch, 0, n -1);
        swap(ch, n, ch.length - 1);
        swap(ch, 0, ch.length - 1);
        return new String(ch);
    }

    public void swap(char[] ch, int i, int j){
        char temp;
        while(i < j){
            temp = ch[i];
            ch[i] = ch[j];
            ch[j] = temp;
            i++;
            j--;
        }
    }

6、实现 strStr()

28. 实现 strStr()

力扣题目链接

实现 strStr() 函数。

给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。

示例 1: 输入: haystack = “hello”, needle = “ll” 输出: 2

示例 2: 输入: haystack = “aaaaa”, needle = “bba” 输出: -1

说明: 当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。 对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。

思路

本题是KMP 经典题目。

先看暴力解法,双重for循环,第一层for循环遍历文本串haystack ,第二层for循环遍历模式串needle 。指针同时向后移动,当碰到元素不相等时,第一层for循环回到上一次开始匹配元素的下一个位置,模式串从头开始继续匹配。时间复杂度O(m * n)

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

实际上就是遍历过程中,如何发现元素不相等,去前缀表中找到前面字串最长公共前后缀,使得文本串继续匹配(不必回到文本串上次匹配位置的后一个位置),从子串最长公共前后缀后一个元素继续匹配(不必回到模式串的头部)。实际上时间复杂度为O(m + n)

什么是前缀表

前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。

例如:aabaabaafa 中查找是否出现过一个模式串:aabaaf。

如动画所示:
代码随想录——字符串篇_第3张图片

当两字符串匹配过程中,b和f匹配不相等时,从前面字串公共前后缀后面元素继续匹配即从b元素位置继续匹配。

在某个字符失败时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。

那么什么是前缀表:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

最长公共前后缀?

字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。例如在字符串abcd中,就有{a,ab,abc}

后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。例如在字符串abcd中,就有{d,cd,bcd}
那么前缀表中记录的最长相等前后缀,就是要求前后缀相等且具有最大长度,例如字符串abcab的最长相等前后缀为2,字符串aaa的最长相等前后缀为2。

因为前缀表要求的就是最大相同前后缀的长度。

例如:字符串a的最长相等前后缀为0。 字符串aa的最长相等前后缀为1。 字符串aaa的最长相等前后缀为2。

如何计算前缀表next

代码随想录——字符串篇_第4张图片

长度为前1个字符的子串a,最长相同前后缀的长度为0。

代码随想录——字符串篇_第5张图片

长度为前2个字符的子串aa,最长相同前后缀的长度为1。

代码随想录——字符串篇_第6张图片

长度为前3个字符的子串aab,最长相同前后缀的长度为0。

以此类推: 长度为前4个字符的子串aaba,最长相同前后缀的长度为1。 长度为前5个字符的子串aabaa,最长相同前后缀的长度为2。 长度为前6个字符的子串aabaaf,最长相同前后缀的长度为0。

那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图:

代码随想录——字符串篇_第7张图片

可以看出模式串与前缀表对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示:

构造next数组其实就是计算模式串s,前缀表的过程。 主要有如下三步:

  1. 初始化
  2. 处理前后缀不相同的情况
  3. 处理前后缀相同的情况

  • 初始化:

定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置。【j有两重含义,一是遍历下标位置,另一个就是公共前后缀长度】

然后还要对next数组进行初始化赋值,如下:

int j = 0;
//直接初始化前缀表第一个元素,公共前后缀为0,直接初始化为0
next[0] = 0;
  • 处理前后缀不相同的情况

i从1开始,进行s[i] 与 s[j]的比较。

所以遍历模式串s的循环下标i 要从 1开始,代码如下:

for (int i = 1; i < s.length(); i++) {

如果 s[i] 与 s[j]不相同,也就是遇到 前后缀末尾不相同的情况,就要向前回退。

怎么回退呢?

next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。

那么 s[i] 与 s[j] 不相同,就要找 j前一个元素在next数组里的值(就是next[j - 1])。

所以,处理前后缀不相同的情况代码如下:

while (j > 0 && s[i] != s[j]) { // 前后缀不相同了
    j = next[j - 1]; // 向前回退
}
  • 处理前后缀相同的情况

如果 s[i] 与 s[j] 相同,共同前后缀长度j+1同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。

代码如下:

if (s[i] == s[j]) { // 找到相同的前后缀
    j++;
}
next[i] = j;

计算前缀表也是KMP算法基本应用

最后整体构建next数组的函数代码如下:

    public int[] getNext(String s) {
        int[] next = new int[s.length()];
        next[0] = 0;

        int j = 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[i] = j;
        }

        return next;
    }

使用next数组来做匹配

在文本串s里 找是否出现过模式串t。

定义两个下标j 指向模式串起始位置 j = 0,i指向文本串起始位置。

i就从0开始,遍历文本串,代码如下:

for (int i = 0; i < s.length; i++) 

接下来就是 s[i] 与 t[j] 进行比较。

如果 s[i] 与 t[j] 不相同,j就要从next数组里寻找下一个匹配的位置。

代码如下:

while(j > 0 && s[i] != t[j]) {
    j = next[j - 1];
}

如果 s[i] 与 t[j ] 相同,那么i 和 j 同时向后移动, 代码如下:

if (s[i] == t[j]) {
    j++; // i的增加在for循环里
}

如何判断在文本串s里出现了模式串t呢,如果匹配到模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了。此时j的长度多加了1

代码实现:

if (j == (t.length()) {
    return (i - j + 1);
}

那么使用next数组,用模式串匹配文本串的整体代码如下:

    public int strStr(String haystack, String needle) {
        //前缀表
        int[] next = getNext(needle);
        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++;
            }
            if (j == needle.length()) {
                return i - needle.length() + 1;
            }
        }
        return -1;
    }

最终代码实现

public int strStr(String haystack, String needle) {
        //前缀表
        int[] next = getNext(needle);
        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匹配到末尾,当前长度多加了一个1
            if (j == needle.length()) {
                return i - needle.length() + 1;
            }
        }
        return -1;
    }

    /***
     * KMP算法,匹配元素不相等时,从字串最长公共前后缀长度位置开始继续匹配
     * 获取前缀表 next数组每个元素代表当前字串最长公共前后缀长度
     * aabaaf [0 1 0 1 2 0]
     *
     * aba
     *
     * abcd
     */
    public int[] getNext(String str) {
        int[] next = new int[str.length()];
        //第一位为0
        next[0] = 0;

        // j 代表最长公共前缀的长度以及遍历位置
        int j = 0;
        for (int i = 1; i < str.length(); i++) {
            //前后缀不相等的情况 向前回退
            while (j > 0 && str.charAt(i) != str.charAt(j)) {
                //KMP
                j = next[j - 1];
            }

            //前后缀相等的情况
            if (str.charAt(i) == str.charAt(j)) {
                j++;
            }
            //更新next数组
            next[i] = j;
        }
        return next;
    }

7、重复的子字符串

459.重复的子字符串

力扣题目链接

给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。

示例 1:
输入: “abab”
输出: True
解释: 可由子字符串 “ab” 重复两次构成。

示例 2:
输入: “aba”
输出: False

示例 3:
输入: “abcabcabcabc”
输出: True
解释: 可由子字符串 “abc” 重复四次构成。 (或者子字符串 “abcabc” 重复两次构成。)

思路

移动匹配

当一个字符串s:abcabc,由重复的子串组成,那么这个字符串的结构一定是这样的:

代码随想录——字符串篇_第8张图片

也就是由前后相同的子串组成。

那么既然前面有相同的子串,后面有相同的子串,用 s + s,这样组成的字符串中,后面的子串前串做后串,前面的子串的后串做前串,就一定还能组成一个s,如图:
代码随想录——字符串篇_第9张图片
所以判断字符串s是否有重复子串组成,只要两个s拼接在一起,里面还出现一个s的话,就说明是由重复子串组成。

当然,我们在判断 s + s 拼接的字符串里是否出现一个s的的时候,要刨除 s + s 的首字符和尾字符,这样避免在s+s中搜索出原来的s,我们要搜索的是中间拼接出来的s。

代码实现:

    public boolean repeatedSubstringPattern(String s) {
        //拼接去头去尾包含匹配
   		String str = s + s;
        return str.substring(1, str.length() - 1).contains(s);
    }

KMP

在一个串中查找是否出现过另一个串,这是KMP的看家本领。那么寻找重复子串怎么也涉及到KMP算法了呢

在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串,这里那字符串s:abababab 来举例,ab就是最小重复单位,如图所示:
代码随想录——字符串篇_第10张图片
假如字符串是由重复子串组成,完整字符串的最长公共前后缀应为 (n - 1) * x【x为最小重复字串,n为原字符串由多少个最小重复字串组成】

所以最小重复字串的长度应该就是原字符串长度 - 完整字符串的最长公共前后缀长度,

只需要根据原字符串的长度是否能整除该长度就能判断,该字符串是否由该最小重复子串构成。

数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。

代码实现

public boolean repeatedSubstringPattern(String s) {
        int[] next = getNext(s);
        if (next[next.length - 1] == 0) {
            return false;
        }

        //看原字符串长度是否能除尽子字符串长度
        return s.length() % (s.length() - next[next.length - 1]) == 0;
    }


    /**
     * 前缀表
     */
    public int[] getNext(String s) {
        int[] next = new int[s.length()];
        next[0] = 0;

        int j = 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[i] = j;
        }

        return next;
    }

总结篇

常用解法

双指针法

反转系列

KMP

总结

字符串类类型的题目,往往想法比较简单,但是实现起来并不容易,复杂的字符串题目非常考验对代码的掌控能力。

双指针法是字符串处理的常客。

KMP算法是字符串查找最重要的算法。【KMP字符串匹配算法】

详情见代码随想录

你可能感兴趣的:(数据结构与算法,算法,leetcode,数据结构)