算法分析之字符串

算法相关数据结构总结:

序号 数据结构 文章
1 动态规划 动态规划之背包问题——01背包
动态规划之背包问题——完全背包
动态规划之打家劫舍系列问题
动态规划之股票买卖系列问题
动态规划之子序列问题
算法(Java)——动态规划
2 数组 算法分析之数组问题
3 链表 算法分析之链表问题
算法(Java)——链表
4 二叉树 算法分析之二叉树
算法分析之二叉树遍历
算法分析之二叉树常见问题
算法(Java)——二叉树
5 哈希表 算法分析之哈希表
算法(Java)——HashMap、HashSet、ArrayList
6 字符串 算法分析之字符串
算法(Java)——字符串String
7 栈和队列 算法分析之栈和队列
算法(Java)——栈、队列、堆
8 贪心算法 算法分析之贪心算法
9 回溯 Java实现回溯算法入门(排列+组合+子集)
Java实现回溯算法进阶(搜索)
10 二分查找 算法(Java)——二分法查找
11 双指针、滑动窗口 算法(Java)——双指针
算法分析之滑动窗口类问题

文章目录

      • 一、字符串基础知识
        • 1. String 类
        • 2. String 函数
        • 3. String常用方法
        • 4.Character函数常用方法
        • 5. StringBuilder常用方法
      • 二、leetcode例题讲解字符串问题
        • 1. 常规字符串问题
          • 709. 转换成小写字母
          • 748. 最短补全词
          • 1816. 截断句子
          • 1446. 连续字符
        • 2. 双指针法
          • 344. 反转字符串
          • 541. 反转字符串 II
          • 剑指 Offer 05. 替换空格
          • 151. 翻转字符串里的单词
          • 剑指 Offer 58 - II. 左旋转字符串
        • 3. KMP算法
          • 28. 实现 strStr()
          • 459. 重复的子字符串
          • 686. 重复叠加字符串匹配
        • 4. 字符串哈希
          • 187. 重复的DNA序列
          • 1044. 最长重复子串
      • 三、其它算法分析
        • 1. 动态规划之背包问题——01背包
        • 2. 动态规划之背包问题——完全背包
        • 3. 动态规划之子序列问题
        • 4. 算法分析之数组问题
        • 5. 算法分析之链表问题
        • 6. 算法分析之哈希表
        • 7. 算法分析之字符串

一、字符串基础知识

1. String 类

在 Java 中字符串属于对象,Java 提供了 String 类来创建和操作字符串。

String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。

字符串创建方法:

String str = "Runoob";
String str2=new String("Runoob");

StingBuffer:

String num = Integer.toString(x);
StringBuffer s = new StringBuffer(num);
2. String 函数

在使用 String 时,需要熟练掌握相关函数:

char charAt(int index)  //返回指定索引处的 char 值。
String concat(String str) //将指定字符串连接到此字符串的结尾。
boolean contentEquals(StringBuffer sb) //当且仅当字符串与指定的StringBuffer有相同顺序的字符时候返回真。
int indexOf(int ch) //返回指定字符在此字符串中第一次出现处的索引。
int indexOf(String str) //返回指定子字符串在此字符串中第一次出现处的索引。
int lastIndexOf(int ch) //返回指定字符在此字符串中最后一次出现处的索引。
int lastIndexOf(String str) //返回指定子字符串在此字符串中最右边出现处的索引。
int length() //返回此字符串的长度。
String replace(char oldChar, char newChar) //返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。
String substring(int beginIndex) //返回一个新的字符串,它是此字符串的一个子字符串。 
char[] toCharArray() //将此字符串转换为一个新的字符数组。
String toLowerCase() //使用默认语言环境的规则将此 String 中的所有字符都转换为小写。
String toUpperCase() //使用默认语言环境的规则将此 String 中的所有字符都转换为大写。
String toString() //返回此对象本身(它已经是一个字符串!)。
String trim() //返回字符串的副本,忽略前导空白和尾部空白。
contains(CharSequence chars) //判断是否包含指定的字符系列。
isEmpty() //判断字符串是否为空。
String[] split(String regex) //根据给定正则表达式的匹配拆分此字符串。
3. String常用方法

(1)将字符串转化为数组

String s = "abcdef";
char[] str = s.toCharArray();

// 遍历字符串的每一个字符
for(int i = 0; i < s.length(); i++) {
    s.charAt(i);
}
for (char c : s.toCharArray()) {
}

(2)返回字符串的子串

String substring(i, j); // 返回从i到j-1的字符串

(3)整数转化为字符串

String num = Integer.toString(x);
String.valueOf(int a);
String date = "2021-12-01";
int year = Integer.parseInt(date.substring(0, 4));

(4)得到字符对应的数字
如:'1' - '0' = 1; 'b' - 'a' = 1;

数字:ch - '0'
小写字母:ch - 'a'
大写字母:ch - 'A'

String magazine = "abc";
int[] cnt = new int[26];
for (char c : magazine.toCharArray()) {
    cnt[c - 'a']++;
}

(5)返回指定索引处的字符,用于遍历

char charAt(int index)

if (s.charAt(i) == t.charAt(j))

// 遍历字符串的每一个字符
for(int i = 0; i < s.length(); i++) {
    s.charAt(i);
}

(6)返回索引

indexOf(c) 返回第一次出现的指定子字符串在此字符串中的索引
indexOf(c, index) 从指定的索引处开始,返回第一次出现的指定子字符串在此字符串中的索引,若找不到,返回-1
4.Character函数常用方法
1. isLetter() 判断是否是一个字母
System.out.println(Character.isLetter('4')); 输出 false
2. isDigit() 判断是否是一个数字字符
System.out.println(Character.isDigit('8')); 输出 true
3. isLowerCase() 判断是否是小写字母
System.out.println(Character.isLowerCase('y')); 输出true
4. isUpperCase() 判断是否是大写字母
System.out.println(Character.isUpperCase('M')); 输出true
5. toUpperCase() 将一个小写字符转为大写字母
System.out.println(Character.toUpperCase('a')); 输出 'A'
6. toLowerCase() 将一个大写字符转为小写字母
System.out.println(Character.toLowerCase('T')); 输出 't'
7. toString() 将单个字符转为字符串形式
System.out.println(Character.toString('r')); 输出的是"r"
5. StringBuilder常用方法
int length()  返回长度(字符数)
StringBuilder reverse() 导致此字符序列被序列的反向替换。
substring(int start, int end) 返回一个新的String,包含此序列中当前包含的字符的子序列。
toString()   返回表示此序列中数据的字符串。
int indexOf(String str) 返回指定子字符串第一次出现的字符串中的索引。
StringBuilder append(char c)char 参数的字符串表示形式追加到此序列。
StringBuilder append(String str) 将指定的字符串追加到此字符序列。

二、leetcode例题讲解字符串问题

双指针法是字符串处理的常客。我们使用双指针法实现了反转字符串的操作,双指针法在数组,链表和字符串中很常用。

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

那么使用KMP可以解决两类经典问题:

  1. 匹配问题:28. 实现 strStr()
  2. 重复子串问题:459. 重复的子字符串
1. 常规字符串问题
709. 转换成小写字母

leetcode题目链接:709. 转换成小写字母

给你一个字符串 s ,将该字符串中的大写字母转换成相同的小写字母,返回新的字符串。

示例一:

输入:s = "Hello"
输出:"hello"

Java代码实现:

class Solution {
    public String toLowerCase(String s) {
        // 调用函数
        // return s.toLowerCase();

        StringBuilder sb = new StringBuilder();
        for(char ch : s.toCharArray()) {
            if(ch >= 'A' && ch <= 'Z') {
                sb.append((char)(ch + 32));
            }else {
                sb.append(ch);
            }
        }
        return sb.toString();
    }
}
748. 最短补全词

leetcode题目链接:748. 最短补全词

给你一个字符串 licensePlate 和一个字符串数组 words ,请你找出并返回 words 中的 最短补全词 。

补全词 是一个包含 licensePlate 中所有字母的单词。

在匹配 licensePlate 中的字母时:

  • 忽略 licensePlate 中的 数字和空格 。
  • 不区分大小写。
  • 如果某个字母在 licensePlate 中出现不止一次,那么该字母在补全词中的出现次数应当一致或者更多。

例如:licensePlate = “aBc 12c”,那么它的补全词应当包含字母 ‘a’、‘b’ (忽略大写)和两个 ‘c’ 。可能的 补全词 有 “abccdef”、“caaacab” 以及 “cbca” 。

请你找出并返回 words 中的 最短补全词 。题目数据保证一定存在一个最短补全词。当有多个单词都符合最短补全词的匹配条件时取 words 中 第一个 出现的那个。

示例一:

输入:licensePlate = "1s3 PSt", words = ["step", "steps", "stripe", "stepple"]
输出:"steps"
解释:最短补全词应该包括 "s""p""s"(忽略大小写) 以及 "t""step" 包含 "t""p",但只包含一个 "s",所以它不符合条件。
"steps" 包含 "t""p" 和两个 "s""stripe" 缺一个 "s""stepple" 缺一个 "s"。
因此,"steps" 是唯一一个包含所有字母的单词,也是本例的答案。

Java代码实现:

class Solution {
    public String shortestCompletingWord(String licensePlate, String[] words) {
        int[] res = getwords(licensePlate);
        String ans = null;  // 初始化字符串
        for(String s : words) {
            int[] cur = getwords(s);
            boolean mark = true;
            for(int i = 0; i < 26 && mark; i++) {
                if(res[i] > cur[i]) mark = false;
            }
            if(mark && (ans == null || ans.length() > s.length())) {
                ans = s;
            }
        }
         return ans;        
    }
    public int[] getwords(String s) {
        int[] arr = new int[26];
        // 把licensePlate的大写字母转为小写字母,并把小写字母存入到数组里,并记录小写字母的个数
        for(char ch : s.toLowerCase().toCharArray()) {
            if(Character.isLowerCase(ch)) {
                arr[ch - 'a'] ++;
            }
        }
        return arr;
    }
}
1816. 截断句子

leetcode题目链接:1816. 截断句子

句子 是一个单词列表,列表中的单词之间用单个空格隔开,且不存在前导或尾随空格。每个单词仅由大小写英文字母组成(不含标点符号)。

  • 例如,“Hello World”、“HELLO” 和 “hello world hello world” 都是句子。

给你一个句子 s​​​​​​ 和一个整数 k​​​​​​ ,请你将 s​​ 截断 ​,​​​使截断后的句子仅含 前 k​​​​​​ 个单词。返回 截断 s​​​​​​ 后得到的句子。

示例一:

输入:s = "Hello how are you Contestant", k = 4
输出:"Hello how are you"
解释:
s 中的单词为 ["Hello", "how" "are", "you", "Contestant"]4 个单词为 ["Hello", "how", "are", "you"]
因此,应当返回 "Hello how are you"

Java代码实现:

class Solution {
    public String truncateSentence(String s, int k) {
        for(int i = 0; i < s.length(); i++) {
            // 当遇到空格且单词个数为0时
            if(s.charAt(i) == ' ' && --k == 0) {
                return s.substring(0, i);
            }
        }
        return s;
    }
}
1446. 连续字符

leetcode题目链接:1446. 连续字符

给你一个字符串 s ,字符串的「能量」定义为:只包含一种字符的最长非空子字符串的长度。

请你返回字符串的能量。

示例一:

输入:s = "leetcode"
输出:2
解释:子字符串 "ee" 长度为 2 ,只包含字符 'e'

示例二:

输入:s = "abbcccddddeeeeedcba"
输出:5
解释:子字符串 "eeeee" 长度为 5 ,只包含字符 'e'

Java代码实现:

class Solution {
    public int maxPower(String s) {
        // 遍历计算重复字符的长度,取最大值
        char[] str = s.toCharArray();
        int res = 1, max = 1;
        for(int i = 1; i < str.length; i++) {
            if(str[i] == str[i-1]) {
                res++;
                max = Math.max(max, res);
            }else {
                res = 1;
            }
        }
        return max;       
    }
}
2. 双指针法
344. 反转字符串

leetcode题目链接:344. 反转字符串

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

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

示例一:

输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]

解题思路:

看情况使用库函数,如果题目就是为了用算法实现库函数的功能,此时就不要用库函数。如果库函数只是其中的一小步,如处理前的排序。

在反转链表中,使用了双指针的方法。

那么反转字符串依然是使用双指针的方法,只不过对于字符串的反转,其实要比链表简单一些。

因为字符串也是一种数组,所以元素在内存中是连续分布,这就决定了反转链表和反转字符串方式上还是有所差异的。

对于字符串,我们定义两个指针(也可以说是索引下表),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。

Java 代码实现:

class Solution {
    public void reverseString(char[] s) {
        // 双指针,同时向中间移动,并交换元素
        int i = 0;
        int j = s.length - 1;
        while(i < j) {
            char tmp = s[i];
            s[i] = s[j];
            s[j] = tmp;
            i++;
            j--;
        }
    }
}

交换数值的方法有两种:

一、临时变量

int tmp = s[i];
s[i] = s[j];
s[j] = tmp;

二、位运算

s[i] ^= s[j];
s[j] ^= s[i];
s[i] ^= s[j];
541. 反转字符串 II

leetcode题目链接:541. 反转字符串 II

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

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

示例一:

输入:s = "abcdefg", k = 2
输出:"bacdfeg"

解题思路:

在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。

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

只要判断处需要反转的起始指针和终止指针的位置,剩下的和上题就一样了。

Java代码实现:

class Solution {
    public String reverseStr(String s, int k) {
        char[] str = s.toCharArray();
        for(int i = 0; i < str.length; i += 2 * k) {
            int start = i;
            // 判断尾数够不够k个来决定end指针的位置
            int end = Math.min(str.length - 1, start + k - 1);
            while(start < end) {
                char tmp = str[start];
                str[start] = str[end];
                str[end] = tmp;
                start++;
                end--;
            }
        }
        return new String(str);
    }
}
剑指 Offer 05. 替换空格

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

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

示例一:

输入:s = "We are happy."
输出:"We%20are%20happy."

解题思路:

使用一个新的对象,复制 str,复制的过程对其判断,是空格则替换,否则直接复制,类似于数组复制。

String不可变,使用StringBuilder 单线程使用,比较快。

StringBuilder sb = new StringBuilder();

Java代码实现:

class Solution {
    public String replaceSpace(String s) {
        // 创建一个新对象,复制字符串,空格则替换为%20,其它不变
        StringBuilder sb = new StringBuilder();
        for(int i = 0; i < s.length(); i++) {
            if(s.charAt(i) == ' ') {
                sb.append("%20");
            }else {
                sb.append(s.charAt(i));
            }
        }
        return sb.toString();
    }
}
151. 翻转字符串里的单词

leetcode题目链接:151. 翻转字符串里的单词

给你一个字符串 s ,逐个翻转字符串中的所有 单词 。

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

请你返回一个翻转 s 中单词顺序并用单个空格相连的字符串。

说明:

  • 输入字符串 s 可以在前面、后面或者单词间包含多余的空格。
  • 翻转后单词间应当仅用一个空格分隔。
  • 翻转后的字符串中不应包含额外的空格。

示例一:

输入:s = "the sky is blue"
输出:"blue is sky the"

示例二:

输入:s = "  hello world  "
输出:"world hello"
解释:输入字符串可以在前面或者后面包含多余的空格,但是翻转后的字符不能包括。

示例三:

输入:s = "a good   example"
输出:"example good a"
解释:如果两个单词间有多余的空格,将翻转后单词间的空格减少到只含一个。

解题思路:

  1. 移除多余空格
  2. 将整个字符串反转
  3. 将每个单词反转

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

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

这样我们就完成了翻转字符串里的单词。

如果使用库函数的话,比较简单。

  1. 使用 split 将字符串按空格分割成字符串数组;
  2. 使用 reverse 将字符串数组进行反转;
  3. 使用 join 方法将字符串数组拼成一个字符串。

但不是题目想要考察的知识。

这里分别实现这几个函数。

Java代码实现:

class Solution {
    public String reverseWords(String s) {
        // // 使用内置函数
        // String[] words = s.trim().split(" +");
        // Collections.reverse(Arrays.asList(words));
        // return String.join(" ", words);

        // 不使用内置函数
        // 1.去除首尾以及中间多余空格
        StringBuilder sb = removeSpace(s);
        // 2. 反转整个字符串
        reverseString(sb, 0, sb.length() - 1);
        // 3.反转各个单词
        reverseEachWord(sb);

        return sb.toString();

    }

    private StringBuilder removeSpace(String s) {
        int start = 0;
        int end = s.length()-1;
        while (s.charAt(start) == ' ') { // 防止开头连着多个空格
            start++;
        }
        while (s.charAt(end) == ' ') { // 防止结尾连着多个空格
            end--;
        }
        StringBuilder sb = new StringBuilder();
        while(start <= end) {
            char c = s.charAt(start);
            if(c != ' ' || sb.charAt(sb.length()-1) != ' ') { // 中间的第一个空格加入,后面再有空格则不加入
                sb.append(c);
            }
            start++;
        }
        return sb;
    }

    public void reverseString(StringBuilder sb, int start, int end) {   
        while (start < end) {
            char temp = sb.charAt(start);
            sb.setCharAt(start, sb.charAt(end));  // 将第start位置的字符改为sb.charAt(end)
            sb.setCharAt(end, temp);
            start++;
            end--;
        }     
    }

    private void reverseEachWord(StringBuilder sb) {
        int start = 0;
        int end = 1;
        int n = sb.length();
        while (start < n) {
            while (end < n && sb.charAt(end) != ' ') {  // 判断每一个单词
                end++;
            }
            reverseString(sb, start, end - 1);  // 反转每一个单词
            start = end + 1;
            end = start + 1;
        }
    }
}
剑指 Offer 58 - II. 左旋转字符串

leetcode题目链接:剑指 Offer 58 - II. 左旋转字符串

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

示例一:

输入: s = "abcdefg", k = 2
输出: "cdefgab"

解题思路:

最简单的方法当然还是使用库函数:

return s.substring(n)+s.substring(0,n);

同样可以复制字符串,拼接

Java实现代码:

class Solution {
    public String reverseLeftWords(String s, int n) {        
        // 列表拼接
        StringBuilder res = new StringBuilder();
        for(int i = n; i < s.length(); i++)
            res.append(s.charAt(i));
        for(int i = 0; i < n; i++)
            res.append(s.charAt(i));
        return res.toString();
    }
}
3. KMP算法
28. 实现 strStr()

leetcode题目链接:28. 实现 strStr()

459. 重复的子字符串

leetcode题目链接:459. 重复的子字符串

686. 重复叠加字符串匹配

leetcode题目链接:686. 重复叠加字符串匹配

给定两个字符串 a 和 b,寻找重复叠加字符串 a 的最小次数,使得字符串 b 成为叠加后的字符串 a 的子串,如果不存在则返回 -1。

注意:字符串 “abc” 重复叠加 0 次是 “”,重复叠加 1 次是 “abc”,重复叠加 2 次是 “abcabc”。

示例一:

输入:a = "abcd", b = "cdabcdab"
输出:3
解释:a 重复叠加三遍后为 "abcdabcdabcd", 此时 b 是其子串。

Java代码实现:

  1. 卡常
class Solution {
    public int repeatedStringMatch(String a, String b) {
        int count = 0;       
        StringBuffer sb = new StringBuffer();
        while(sb.length() < b.length()) {  // 如果长度比b小,则一直加a
            sb.append(a);
            count++;
        }
        if(sb.toString().contains(b)) {  // 包含b则返回重叠次数
            return count;
        }
        sb.append(a);  // 不包含则再加一次,判断是否包含
        if(!sb.toString().contains(b)) {
            return -1;
        }
        return count + 1;
    }
}
  1. KMP算法
4. 字符串哈希
187. 重复的DNA序列

leetcode题目链接:187. 重复的DNA序列

所有 DNA 都由一系列缩写为 ‘A’,‘C’,‘G’ 和 ‘T’ 的核苷酸组成,例如:“ACGAATTCCG”。在研究 DNA 时,识别 DNA 中的重复序列有时会对研究非常有帮助。

编写一个函数来找出所有目标子串,目标子串的长度为 10,且在 DNA 字符串 s 中出现次数超过一次。

示例一:

输入:s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT"
输出:["AAAAACCCCC","CCCCCAAAAA"]

解题思路:

数据范围只有 105,一个朴素的想法是:从左到右处理字符串 ,使用滑动窗口得到每个以 s[i] 为结尾且长度为 10 的子串,同时使用哈希表记录每个子串的出现次数,如果该子串出现次数超过一次,则加入答案。

1.滑动窗口 + 哈希表 + Set 去重

为了防止相同的子串被重复添加到答案,使用Set进行去重操作。

class Solution {
    public List<String> findRepeatedDnaSequences(String s) {
        // 字符串哈希表 + Set去重
        if (s.length() < 10 || s.length() > 10000)  return new ArrayList<>();  // 先判断不满足条件的,会减少很多时间
        Map<String, Integer> map = new HashMap<>();
        Set<String> res = new HashSet<>();
        for (int i = 0; i + 10 <= s.length(); i++) {
            String cur = s.substring(i, i + 10);
            if (map.getOrDefault(cur, 0) != 0) {
                res.add(cur);
            } else {
                map.put(cur, 1);
            }
        }
        return new ArrayList<>(res);
    }
}

2.滑动窗口 + 哈希表

为了防止相同的子串被重复添加到答案,而又不使用常数较大的 Set 结构。我们可以规定:当且仅当该子串在之前出现过一次(加上本次,当前出现次数为两次)时,将子串加入答案。

class Solution {
    public List<String> findRepeatedDnaSequences(String s) {
        // 1. 滑动窗口 + 哈希表
        if (s.length() < 10 || s.length() > 10000)  return new ArrayList<>();  // 先判断不满足条件的,会减少很多时间
        List<String> res = new ArrayList<>();
        Map<String, Integer> map = new HashMap<>();
        for (int i = 0; i + 10 <= s.length(); i++) {
            String cur = s.substring(i, i + 10);  // 滑动窗口的10个字符
            int cnt = map.getOrDefault(cur, 0);   // 判断字符串是否在哈希表中存在过,返回出现的次数
            if (cnt == 1) res.add(cur);           // 当且仅当出现过一次的加入list
            map.put(cur, cnt + 1);
        }
        return res;
    }
}

3.字符串哈希 + 前缀和

子串长度为 10,因此上述解法的计算量为 106

若题目给定的子串长度大于 100 时,加上生成子串和哈希表本身常数操作,那么计算量将超过 107,会 TLE。

因此一个能够做到严格 O(n) 的做法是使用「字符串哈希 + 前缀和」。

具体做法为,我们使用一个与字符串 s 等长的哈希数组 h[],以及次方数组 p[]。

由字符串预处理得到这样的哈希数组和次方数组复杂度为 O(n)。当我们需要计算子串 s[i…j] 的哈希值,只需要利用前缀和思想 h[j] - h[i-1] * p[j-i+1] 即可在 O(1) 时间内得出哈希值(与子串长度无关)。

到这里,还有一个小小的细节需要注意:如果我们期望做到严格 O(n) ,进行计数的「哈希表」就不能是以 String 作为 key,只能使用 Integer(也就是 hash 结果本身)作为 key。因为 Java 中的 String 的 hashCode 实现是会对字符串进行遍历的,这样哈希计数过程仍与长度有关,而 Integer 的 hashCode 就是该值本身,这是与长度无关的。

class Solution {
    int N = (int)1e5+10, P = 131313;  // 通过测试用例解决哈希冲突使用
    int[] h = new int[N], p = new int[N];  // 构造h数组和p数组
    public List<String> findRepeatedDnaSequences(String s) {
        // 2.字符串哈希 + 前缀和
        // 利用前缀和思想计算哈希值,可以做到严格的O(n)
        if (s.length() < 10 || s.length() > 10000)  return new ArrayList<>();  // 先判断不满足条件的,会减少很多时间
        List<String> res = new ArrayList<>();
        p[0] = 1;
        for (int i = 1; i <= s.length(); i++) {
            h[i] = h[i - 1] * P + s.charAt(i - 1);  // 计算哈希数组h
            p[i] = p[i - 1] * P;   // 计算次方数组p
        }
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 1; i + 10 - 1 <= s.length(); i++) {
            int j = i + 10 - 1;
            int hash = h[j] - h[i - 1] * p[j - i + 1];  // 计算子串s[i..j]的哈希值
            int cnt = map.getOrDefault(hash, 0);
            if (cnt == 1) res.add(s.substring(i - 1, i + 10 - 1));
            map.put(hash, cnt + 1);
        }
        return res;
    }
}
1044. 最长重复子串

leetcode题目链接:1044. 最长重复子串

给你一个字符串 s ,考虑其所有 重复子串 :即,s 的连续子串,在 s 中出现 2 次或更多次。这些出现之间可能存在重叠。

返回 任意一个 可能具有最长长度的重复子串。如果 s 不含重复子串,那么答案为 “” 。

示例一:

输入:s = "banana"
输出:"ana"

解题思路:

题目要求得「能取得最大长度的任一方案」,首先以「最大长度」为分割点的数轴具有「二段性」:

  • 小于等于最大长度方案均存在(考虑在最大长度方案上做删减);
  • 大于最大长度的方案不存在。

二分范围为 [0,n],关键在于如何 check 函数,即实现「检查某个长度 len 作为最大长度,是否存在合法方案」。

对于常规做法而言,可枚举每个位置作为起点,得到长度为 len 的子串,同时使用 Set 容器记录已被处理过子串有哪些,当容器中出现过当前子串,说明存在合法方案。

但是该做法实现的 check 并非线性,子串的生成和存入容器的时执行的哈希函数执行均和子串长度相关,复杂度是 O(n∗len)。

我们可以通过「字符串哈希」进行优化,具体的,在二分前先通过 O(n) 的复杂度预处理出哈希数组,从而确保能够在 check 时能够 O(1) 得到某个子串的哈希值,最终将 check 的复杂度降为 O(n)。

Java代码实现:

class Solution {
    long[] h, p;
    public String longestDupSubstring(String s) {
        int P = 1313131, n = s.length();
        h = new long[n + 10]; p = new long[n + 10];
        p[0] = 1;
        for (int i = 0; i < n; i++) {  // 计算哈希值
            p[i + 1] = p[i] * P;
            h[i + 1] = h[i] * P + s.charAt(i);
        }
        String ans = "";
        int l = 0, r = n;
        while (l < r) {  // 二分查找
            int mid = l + r + 1 >> 1;
            String t = check(s, mid);
            if (t.length() != 0) l = mid;
            else r = mid - 1;
            ans = t.length() > ans.length() ? t : ans;
        }
        return ans;
    }
    String check(String s, int len) {  // 检查某个长度为len作为最大长度是否存在合法方案
        int n = s.length();
        Set<Long> set = new HashSet<>();
        for (int i = 1; i + len - 1 <= n; i++) {
            int j = i + len - 1;
            long cur = h[j] - h[i - 1] * p[j - i + 1];
            if (set.contains(cur)) return s.substring(i - 1, j);
            set.add(cur);
        }
        return "";
    }
}

三、其它算法分析

1. 动态规划之背包问题——01背包

动态规划之背包问题——01背包

2. 动态规划之背包问题——完全背包

动态规划之背包问题——完全背包

3. 动态规划之子序列问题

动态规划之子序列问题

4. 算法分析之数组问题

算法分析之数组问题

5. 算法分析之链表问题

算法分析之链表问题

6. 算法分析之哈希表

算法分析之哈希表

7. 算法分析之字符串

算法分析之字符串

参考:

算法(Java)——字符串String

代码随想录:字符串

你可能感兴趣的:(算法分析,算法,String,Java,字符串)