leetcode DAY 8-9 字符串 KMP算法

DAY 8 字符串1

344 反转字符串

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

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

    public void reverseString(char[] s) {
        int l=0, r=s.length-1;
        while(l<r){
            // swap的第一种实现:利用临时变量记录
            char temp = s[l];
            s[l] = s[r];
            s[r] = temp;
            
            l++;
            r--;
        }
    }

swap的第二种实现:通过位运算

s[l] ^= s[r];  //构造 a ^ b 的结果,并放在 a 中
s[r] ^= s[l];  //将 a ^ b 这一结果再 ^ b ,存入b中,此时 b = a, a = a ^ b  (b^b==0同一个数字做与运算结果为1,任何数与1还是任何数)
s[l] ^= s[r];  //a ^ b 的结果再 ^ a ,存入 a 中,此时 b = a, a = b 完成交换

541 反转字符串 II

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

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

方法一:

题目意思就是:每k个反转,再k个不反转,交替出现。最终剩余的字符若不满k个也反转

    public String reverseStr(String s, int k) {
        StringBuilder sb = new StringBuilder();
        Deque<Character> stack = new LinkedList<>();
        for(int i=0;i<s.length();i++){
            if((i/k)%2==1){
                sb.append(s.charAt(i));
                continue;
            }
            stack.push(s.charAt(i));
            if((i+1)%k==0 || i==s.length()-1){
                while(!stack.isEmpty()){
                    sb.append(stack.pop());
                }
            }
        }
        return sb.toString();
    }

方法二:直接模拟

先考虑通用情况,再考虑特殊的边界

能不用库函数就先不用

    public String reverseStr(String s, int k) {
        char[] charArr = s.toCharArray();
        for(int i=0;i<s.length();i+=2*k){
            if(i+k<=s.length()){
                reverseSubstr(charArr, i, i+k);
            }else{
                reverseSubstr(charArr, i, s.length());
            }
        }
        return new String(charArr);
    }

    public void reverseSubstr(char[] arr,int start, int end){
        while(start<end-1){
            char temp = arr[start];
            arr[start] = arr[end-1];
            arr[end-1] = temp;
            start++;
            end--;
        }
    }

剑指 Offer 05. 替换空格

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

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

双指针

    public String replaceSpace(String s) {
        // 统计空格数
        int count = 0;
        for(char c : s.toCharArray()){
            if(c==' ') count++;
        }
        // 无空格,直接返回
        if(count==0) return s;
        // 有空格,构建一个比原字符串长2倍的空格数的空间,双指针填充
        char[] arr = new char[s.length()+count*2];
        int j = arr.length-1; // 指向新字符串末尾
        int i = s.length()-1; // 指向旧字符串末尾
        while(i>=0){
            char c = s.charAt(i--);
            if(c==' '){
                arr[j--] = '0';
                arr[j--] = '2';
                arr[j--] = '%';
            }else arr[j--] = c;
        }
        return new String(arr);
    }

151 反转字符串中的单词

给你一个字符串 s ,请你反转字符串中 单词 的顺序。

单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。

返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。

**注意:**输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。

**解题思路:**局部反转+整体反转

  • 移除多余空格
  • 将整个字符串反转
  • 将每个单词反转
    public String reverseWords(String s) {
        // 1. 去多余空格
        s = removeSpce(s);
        // 2. 整句反转
        char[] arr = s.toCharArray();
        parReverse(arr,0,arr.length-1);
        // 3. 局部单词反转
        // 终止条件也可以设置为:start
        for(int start=0,end=0;end<=arr.length;end++){
            if(end==arr.length || arr[end]==' '){
                parReverse(arr,start,end-1);
                start = ++end;
            }
        }
        return new String(arr);
    }

    public String removeSpce(String s){
        int j=0;
        StringBuilder res = new StringBuilder();
        while(s.charAt(j)==' '){
            j++;
        }
        for(;j<s.length();j++){     
            if(s.charAt(j)!=' '){
                if(res.length()>0 && s.charAt(j-1)==' '){
                    res.append(' ');
                }
                res.append(s.charAt(j));
            }
        }
        return res.toString();
    }

    public void parReverse(char[] arr, int start, int end){   
        int l = start, r = end;
        while(l<r){
            char temp = arr[l];
            arr[l] = arr[r];
            arr[r] = temp;
            l++;
            r--;
        }
    }

**Tips:**StringBuilder中有方法 sb.setCharAt(位置,字符) 可以直接在字符串上做修改,不用转换成char[] 操作

剑指 Offer 58 - II. 左旋转字符串

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

解题思路:

通过 局部反转+整体反转 达到左旋转的目的

  1. 反转区间为前n的子串
  2. 反转区间为n到末尾的子串
  3. 反转整个字符串
    public String reverseLeftWords(String s, int n) {
        StringBuilder str = new StringBuilder(s);
        parReverse(str,0,n-1);
        parReverse(str,n,s.length()-1);
        parReverse(str,0,s.length()-1);
        return str.toString();
    }

    public StringBuilder parReverse(StringBuilder str, int start, int end){
        while(start<end){
            char temp = str.charAt(start);
            str.setCharAt(start, str.charAt(end));
            str.setCharAt(end,temp);
            // 注意移动指针!!!
            start++;
            end--;
        }
        return str;
    }

DAY 9 字符串2 KMP算法

基本定义

前缀:包含首字母,不包含尾字母的所有连续子串

后缀:包含尾字母,不包含首字母的所有连续子串

如:aabaaf

字符串 前缀 后缀
a null null
aa a a
aab a, aa b, ab
aaba a, aa, aab a, ba, aba
aabaa a, aa, aab, aaba a, aa, baa, abaa,
aabaaf a, aa, aab, aaba, aabaaf f, af, aaf, baaf, abaaf

最长相等前后缀:找到 以字符串首字母为开头,以字符串尾字母为结尾 的 两个相同的子串。取结果最长的一组子串长度作为最长相等前后缀

next:前缀表,保存最长相同前后缀的长度。

如:字符串 aabaaf 的前缀表为:

index 字串 最长相等前后缀长度
0 a 0
1 aa 0
2 aab 0
3 aaba 1
4 aabaa 2
5 aabaaf 0

求next数组

1. 不向后推一位

next 数组此时存储的是**下标i之前(包括i,即str[0]~str[i])的字符串中,有多大长度的相同前缀后缀。**当i位置字符与另一个字符不相等时,指针要跳转去 next[i-1]

原理:

index 0 1 2 3 4 5 6 7 8 9 10 11 12
str a a b a a d x a a b a a f
next[i] 0 1 0 1 2 0 0 1 2 3 4 5 0

next数组中,若有 next[i-1] = k, 则说明在str[0]~str[i-1]的子串中有 str[0]~str[k-1] == str[i-k]~str[i-1]

此时,对处于位置 i 的指针来说:

​ 若str[i] == str[k],则说明str[0]~str[k-1] str[k] == str[i-k]~str[i-1] str[i],即相等前后缀子串的长度+1,即 k+1,故next[i] = k+1

​ 如:已知 next[9] = 3, 此时有 i=10,k=3,则有 str[0]~str[2] == str[7]~str[9] 即 aab == aab

​ 由于str[10] == str[3] == a,故有 aaba == aaba,即str[0]~str[3] == str[7]str[10],所以从010的str子串最长相等前后缀长度为 4,即 next[10] = 3+1 = 4;

​ 若str[i] != str[k],指针回退到 next[k-1] 位置,若还不相等,则继续回退操作,直到指针回退到0位置还不相等(此时,令next[i]==0), 或找到 str[i] == str[k] 为止(此时,令next[i]==k+1)

​ 如: 已知 next[11] = 5,此时有 i=12,k=5,则有 str[0]~str[4] == str[7]~str[11] == aabaa

​ a. 由于 str[12] != str[5],进行回退操作,令 k=next[5-1]=next[4]=2, 比较 str[12] 和 str[2]

​ b. 由于 str[12] != str[2],进行回退操作,令 k=next[2-1]=next[1]=1,比较 str[12] 和 str[1]

​ c. 由于 str[12] != str[1],进行回退操作,令 k=next[1-1]=next[0]=0,比较 str[12] 和 str[0]

​ d. 由于 str[12] != str[0],且无法再退,因此 next[12] = 0;

解释:

第a步:由于 str[12] != str[5],则说明还未找到相等前后缀,还要继续看str[5]之前是否能找到满足条件的前缀。next数组中,next[4]=2 意味着str[0]~str[1] ==str[3]~str[4] == aa,由于已知 str[0]~str[4] == str[7]~str[11],故一定有

str[10]~str[11] == str[3]~str[4],因此可推出 str[0]~str[1] == str[10]~str[11],故只需比较 str[12] 和 str[2]是否相等,若相等则找到最长相等前后缀,若不相等则还未找到,还要继续缩小范围

第b步:由于 str[12] != str[2],则说明还未找到相等前后缀,还要继续看str[2]之前,由于next[1] = 1, 说明对next[0]~next[1]这个子串中,最长相等前后缀长度为1,即 next[0] == next[1],由a步可知 str[0]~str[1] == str[10]~str[11],则有 str[1]==str[11],可推出 str[11]==str[0], 故只需比较 str[12] 和 str[1]

以此类推直到 str[0] != str[i],说明子串str[0]~str[i] 中没有相等前后缀

或 找到 str[i] == str[k], 则说明 str[0]~str[k] == str[i-k]~str[i],故next[i] = k-0+1 = k+1;

实现:

1)初始化:next[0] = 0

​ k = 0 :指向前缀末尾位置,不断变化

​ i = 1:始终指向后缀末尾位置,即子串中最后一个字符位置

2)处理前后缀最后一个字符相同情况

3)处理前后缀最后一个字符不相同情况

    public static int[] getNext(String str,int[] next){
        next[0] = 0;
        int k = 0;
        int i = 1;
        while(i<next.length){
            if(str.charAt(i) == str.charAt(k)){
                next[i++] = ++k;
            }else {
               //  str.charAt(i) != str.charAt(k)
                if(k==0){
                    next[i++] = 0;
                    continue;
                }
                k = next[k-1];
            }
        }
        return  next;
    }

2. 向后推一位(推荐)

next 数组此时存储的是**下标i之前(不包括i,,即str[0]~str[i-1])的字符串中,有多大长度的相同前缀后缀。**也就是当i位置字符与另一个字符不相等时,指针下一次要跳转去的位置

原理:

index 0 1 2 3 4 5 6 7 8 9 10 11 12
str a a b a a d x a a b a a f
next[i] -1 0 1 0 1 2 0 0 1 2 3 4 5

next数组中,若有 next[i-1] = k, 则说明在str[0]~str[i-2]的子串中有 str[0]~str[k-1] == str[i-k-1]~str[i-2]

此时,对处于位置 i 的指针来说:

​ 若str[i-1] == str[k],则说明str[0]~str[k] == str[i-k-1]~str[i-1],即相等前后缀子串的长度+1,即 k+1,故next[i] = k+1

​ 若str[i-1] != str[k],指针回退到 next[k] 位置,若还不相等,则继续回退操作,直到指针k==-1 或找到 str[i-1] == str[k] 为止,令next[i]==k+1

**特殊情况:**k==-1时,说明没有相等的前后缀 或 i==1,此时令 str[i] = k+1 = 0

实现:

1)初始化:

错误写法:未考虑到str长度为1的情况

					 next[0] = -1    next[1] = 0

​					 i = 2:指向子串最后一个字符的后一位

​					 k = 0 :k=next[i-1] = next[1] = 0

更正:

					next[0] = -1;
					int k = -1; // 前一项的k
					int i = 1; // 提前走了一步

2)处理前后缀最后一个字符相同情况

3)处理前后缀最后一个字符不相同情况

    public static int[] getNext(String str, int[] next){
        next[0] = -1;
        int k = -1; // 前一项的k
        int i = 1; // 提前走了一步
        while(i<next.length){
            if(k==-1 || str.charAt(i-1) == str.charAt(k)){
                next[i++] = ++k;
            }else {
               //  str.charAt(i-1) != str.charAt(k)
                k = next[k];
            }
        }
        return  next;
    }

nextValue数组——优化后的next

index 0 1 2 3 4 5 6 7 8 9
str a a a a a a a a b d
next -1 0 1 2 3 4 5 6 7 0

当求 next[9] 时,我们会发现 str[i-1] = str[8] 和 str[k] = str[7] 不匹配,即 b != a。此时指针会回退到next[6] = 5,由于 b!=a,指针继续回退到next[5] = 4,由于str中前7个字符都是a,因此指针重复了7遍回退,比较操作直到 str[0]=a 也和 b 不匹配。这样做浪费了时间。

nextValue数组构造原则:

  1. 回退到的位置和当前字符一样,就写回那个位置的nextValue值
  2. 回退到的位置和当前字符不一样,就写当前字符原来的next值
index 0 1 2 3 4 5 6 7 8 9
str a a a a a a a a b d
nextValue -1 -1 -1 -1 -1 -1 -1 -1 7 0

nextValue则可令指针从str[7]位置直接回退到str[0],一步到位,相较于next数组更加省时。

    public static void getNextValue(String str, int[] nextValue){
        nextValue[0] = -1;
        int k = -1; // 前一项的k
        int i = 1; // 提前走了一步
        while(i<next.length){
            if(k==-1 || str.charAt(i-1) == str.charAt(k)){	
                // 此时next[i]=k+1,即要回退到k+1位置
                // 1) 回退到的位置和当前字符一样
                if(str.charAt(k+1) == str.charAt(i)){
                    nextValue[i] = next[k+1];
                }else{
                    // 2)回退到的位置和当前字符不一样
                    nextValue[i] = k+1;
                }
                i++;
                k++;
            }else {
               //  str.charAt(i-1) != str.charAt(k)
                k = next[k];
            }
        }
    }

28 找出字符串中第一个匹配项的下标

给你两个字符串 haystackneedle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1

解题思路:kmp算法

  1. 求出next数组

  2. 将主字符串和目标字符串进行匹配

    向后推一位的next:
    特殊情况,当主串i位置字母和目标串首字母不相同时,t = next[0] = -1,此时应令 t = 0,i++, 继续匹配 i+1位置的主串字母 和 目标串首字母。

    /**
     * 在主串中找到目标字符串
     * @param haystack 主串
     * @param needle 目标字符串
     * @return
     */
	public int strStr(String haystack, String needle) {
        int mLen = haystack.length();
        int tLen = needle.length();
        if(tLen>mLen) return -1;
        
        int[] next = new int[tLen];
        getNext(needle,next);

        int i = 0; // 指向主串的查询起始位置
        int t = 0; // 指向目标串
        while (i<mLen && t<tLen){
            if(t==-1 || haystack.charAt(i) == needle.charAt(t)){
                t++;
                i++;
            }else{
                t = next[t];
            }

        }
        if(t>=tLen){
            return i-t;
        }
        return -1;
    }

    public static void getNext(String str, int[] next){
        next[0] = -1;
        int k = -1; // 前一项的k
        int i = 1; // 提前走了一步
        while(i<next.length){
            if(k==-1 || str.charAt(i-1) == str.charAt(k)){
                next[i++] = ++k;
            }else {
               //  str.charAt(i-1) != str.charAt(k)
                k = next[k];
            }
        }
    }

不向后推的next:

​ 特殊情况,当主串i位置字母和目标串首字母不相同时,若令 t=next[0-1],发生数组下标越界错误,因此不改变 t=0,仅令 i++, 继续匹配 i+1位置的主串字母 和 目标串首字母。

    public int strStr(String haystack, String needle) {
        int mLen = haystack.length();
        int tLen = needle.length();
        
        int[] next = new int[tLen];
        getNext(needle,next);

        int i = 0; // 指向主串的查询起始位置
        int t = 0; // 指向目标串
        while (i<mLen && t<tLen){
            if(haystack.charAt(i) == needle.charAt(t)){
                t++;
            }else if(t!=0){
                t = next[t-1];
                continue;
            }
            i++;
        }
        if(t==tLen){
            return i-t;
        }
        return -1;
    }

    public static void getNext(String str, int[] next){
        next[0] = 0;
        int pre = 0;
        int tail = 1;
        while(tail<next.length){
            if(str.charAt(tail) == str.charAt(pre)){
                next[tail++] = ++pre;
            }else {
               //  str.charAt(tail) != str.charAt(pre)
                if(pre==0){
                    next[tail++] = 0;
                    continue;
                }
                pre = next[pre-1];
            }
        }
    }

459 重复的子字符串

给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。

字符串 预期结果
abcabc true
ababa false

暴力解法:

遍历以首字母开始的所有连续子串,看是否能组成整个字符串

当子串长度超过字符串的一半时,字符串必定无法由一个子串重复多次构成

    public boolean repeatedSubstringPattern(String s) {
        for(int i=0;i<s.length()/2;i++){ // i指向子串的最后一个字符处
            String str = s.substring(0,i+1); // 获取子串
            int j = i+1;
            for (; j < s.length(); j=j+i+1) {
                // 剩余部分长度小于子串长度:字符串无法由当前子串重复构成
                if(j+i+1>s.length()) break;
                
                // 比较子串和字符串剩余部分是否相等
                String tem = s.substring(j,j+i+1);
                if(!str.equals(tem)) break;
            }
            if(j==s.length()){
                return true;
            }
        }
        return false;
    }

移动匹配:

若一个字符串S是由重复的子串构成的,则两个该字符串S拼接在一起产生一个新字符串SS,在除去新字符串的首字母和尾字母后,必定能找到原字符串S

如:S = abcabc SS = S+S = abcabcabcabc

​ 除去首尾字母 SS = bcabcabcab

原因:

若一个字符串内部由重复的子串组成,则该字符串前半部分和后半部分必定由相同的子串组成

既然前面有相同的子串,后面有相同的子串,用 s + s,这样组成的字符串中,后面的子串做前串,前后的子串做后串,就一定还能组成一个s

所以判断字符串s是否有重复子串组成,只要两个s拼接在一起,里面还出现一个s的话,就说明是又重复子串组成。

注意:要刨除 ss 的首字符和尾字符,这样避免在ss中搜索出原来的s,我们要搜索的是中间拼接出来的s。

如:SS = S+S = abcabcabcabc 或 SS = S+S = abcabcabcabc 避免搜索出原S,导致函数恒真

​ 除去首尾字母 SS = bcabcabcab 搜索出的是新组成的S

特殊情况:

​ 字符串s中仅含一个字符(s.length() == 1),false

​ ss除去首尾字母后,ss.length() = 0,但 ss!=null。因此 ss.contains(s) 为 false

    public boolean repeatedSubstringPattern(String s) {
        String ss = s + s;
        ss = ss.substring(1,ss.length()-1);
        if(ss.contains(s)) return true;
        return false;
    }

String类型空字符串和Null的区别:

null表示的是一个对象的值,而不是一个字符串。如声明一个对象的引用,String a=null。
“”表示的是一个空字符串,也就是说它的长度为0。如声明一个字符串String s=”“。
String a=null;表示声明一个字符串对象的引用,但指向为null,也就是说还没有指向任何的内存空间
String s=”“;表示声明一个字符串类型的引用,其值为“”,也就是空字符串,这个s引用指向的是空字符串的内存空间

null字符串没有内存空间,因此没有长度,无法调用方法。a.length() 出现空指针异常

​ 空字符串长度为0,可以调用方法。s.length() 结果为 0

KMP算法:

当一个字符串由重复子串组成的,以最长相等前后缀的前缀或后缀为基准,不包含的子串(整个字符串 - 前缀/后缀)就是最小重复子串。

若有最小重复子串,则 字符串长度 一定可以整除 最小重复子串。

原理:
详见参考文章

    public boolean repeatedSubstringPattern(String s) {
        if(s.length()<=1){
            return false;
        }
        
        // getNext(s,next)
        int[] next = new int[s.length()];
        int len = s.length();
        next[0] = 0;
        int i = 1;
        int k = 0;
        while(i<len){
            if(s.charAt(i)==s.charAt(k)){
                next[i++] = ++k;
            }else{
                if(k==0) next[i++] = 0;
                else k = next[k-1];
            }

        }

        // 判断
        if(next[len-1]!=0 && len%(len-next[len-1])==0){
            return true;
        }
        return false;
    }

总结

双指针法:双指针法是字符串处理的常客。双指针法在数组,链表和字符串中很常用

反转系列:先整体反转再局部反转

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

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