关于字符串:一些常见的字符串匹配算法

作者:京东批发 李文涛 一、简介1.1 Background字符串匹配在文本处理的宽泛畛域中是一个十分重要的主题。字符串匹配包含在文本中找到一个,或者更一般地说,所有字符串(通常来讲称其为模式)的呈现。该模式示意为p=p[0..m-1];它的长度等于m。文本示意为t=t[0..n-1],它的长度等于n。两个字符串都建设在一个无限的字符集上。 一个比拟常见的字符串匹配办法工作原理如下。在一个大小通常等于m的窗口帮忙下扫描文本。首先将窗口和文本的左端对齐,而后将窗口的字符与文本中的字符进行比拟,这一特定的工作被称为尝试,在齐全匹配或不匹配之后,将窗口移到右侧。持续反复同样的过程,直到窗口的右端超过文本的右端,个别称为滑动窗口机制。 1.2 Brute forceBF算法查看文本中0到n-m之间的所有地位,是否有模式从那里开始呈现。而后,在每次尝试之后,它将模式串向右挪动一个地位。 BF算法不须要预处理阶段,除了模式和文本之外,还须要一个恒定的额定空间。在搜寻阶段,文本字符比拟能够以任何程序进行。该搜寻阶段的工夫复杂度为O(mn)。 public static int strMatch(String s, String p){ int i = 0, j = 0; while(i < s.length() && j < p.length()){ if(s.charAt(i) == p.charAt(j)){ i++; j++; }else{ i = i - j + 1; j = 0; } if (j == p.length()){ return i - j; } } return -1;}二、KMP先回顾下brute force中匹配的状况。咱们在文本串BBC#ABCDAB$ABCDABCDABDE中查找模式串ABCDABD,文本串中第1个字符“B”与模式串中第1个字符“A”不匹配,所以咱们将模式传后移一位。 文本串中的第2个字符“B”和模式串中的第一个字符“A”不匹配,持续后移。 基于这种形式一直比拟并且挪动,咱们发现文本串中的第5个字符“A”和模式串中的第1个字符“A”是匹配的,那么持续比拟文本串和模式串的下一个字符。 一直比拟之后咱们发现,文本串中的字符“$”和模式串中的最初一个字符“D”不匹配。 ...

April 25, 2023 · 3 min · jiezi

关于字符串:KMP算法

在利用中常常会遇到字符串比拟的算法,判断一个字符串pp是否是另外一个字符串ss的子串。注明的算法是KMP算法,当初整顿如下,参考 宫水三叶 的代码实现。 // 作者 宫水三叶// 链接 https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/solution/shua-chuan-lc-shuang-bai-po-su-jie-fa-km-tb86/// KMP 算法// ss: 原串(string) pp: 匹配串(pattern)// 工夫复杂度O(m + n)public int kmp(String ss, String pp) { if (pp.isEmpty()){ return 0; } // 别离读取原串和匹配串的长度 int n = ss.length(), m = pp.length(); // 原串和匹配串后面都加空格,使其下标从 1 开始 ss = " " + ss; pp = " " + pp; char[] s = ss.toCharArray(); char[] p = pp.toCharArray(); // 构建 next 数组,数组长度为匹配串的长度(next 数组是和匹配串相干的) int[] next = new int[m + 1]; // 结构过程 i = 2,j = 0 开始,i 小于等于匹配串长度 【结构 i 从 2 开始】 for (int i = 2, j = 0; i <= m; i++) { // 匹配不胜利的话,j = next(j) while (j > 0 && p[i] != p[j + 1]){ j = next[j]; } // 匹配胜利的话,先让 j++ if (p[i] == p[j + 1]){ j++; } // 更新 next[i],完结本次循环,i++ next[i] = j; } // 匹配过程,i = 1,j = 0 开始,i 小于等于原串长度 【匹配 i 从 1 开始】 for (int i = 1, j = 0; i <= n; i++) { // 匹配不胜利 j = next(j) while (j > 0 && s[i] != p[j + 1]){ j = next[j]; } // 匹配胜利的话,先让 j++,完结本次循环后 i++ if (s[i] == p[j + 1]){ j++; } // 整一段匹配胜利,间接返回下标 if (j == m){ return i - m; } } return -1;}如果是java的开发者,能够应用jdk自带的 ...

September 28, 2022 · 1 min · jiezi

关于字符串:算法字符串

字符串双指针用两个指针来定位一个子数组,其中一个指针指向数组的第1个数字,另一个指针指向数组的最初一个数字,那么两个指针之间所蕴含的就是一个子数组。 能够在挪动这两个指针的同时,统计两个指针之间的字符串中字符呈现的次数,这样能够解决很多常见的面试题,如在一个字符串中定位另一个字符串的变位词等。 因为这种类型的面试题都与统计字母呈现的次数无关,咱们常常应用哈希表来存储每个元素呈现的次数,因而解决这种类型的面试题通常须要同时应用双指针和哈希表。 字符串中的变位词题目:输出字符串s1和s2,如何判断字符串s2中是否蕴含字符串s1的某个变位词?如果字符串s2中蕴含字符串s1的某个变位词,则字符串s1至多有一个变位词是字符串s2的子字符串。假如两个字符串中只蕴含英文小写字母。例如,字符串s1为"ac",字符串s2为"dgcaf",因为字符串s2中蕴含字符串s1的变位词"ca",因而输入为true。如果字符串s1为"ab",字符串s2为"dgcaf",则输入为false。 由变位词的定义可知,变位词具备以下几个特点。首先,一组变位词的长度肯定雷同;其次,组成变位词的字母汇合肯定雷同,并且每个字母呈现的次数也雷同。 /** * @param {string} s1 * @param {string} s2 * @return {boolean} */var checkInclusion = function(s1, s2) { if(s2.length < s1.length) { return false; } var areAllZero = function(counts) { for(let i of counts) { if(i !== 0) { return false } } return true; } const counts = new Array(26).fill(0) for(let i = 0; i < s1.length; i++) { counts[s1.charCodeAt(i) - 97]++; counts[s2.charCodeAt(i) - 97]--; } if(areAllZero(counts)) { return true; } for (let i = s1.length; i < s2.length; ++i) { counts[s2.charCodeAt(i) - 97]-- counts[s2.charCodeAt(i - s1.length) - 97]++ if(areAllZero(counts)) { return true } } return false;};字符串中的所有变位词题目:输出字符串s1和s2,如何找出字符串s2的所有变位词在字符串s1中的起始下标?假如两个字符串中只蕴含英文小写字母。例如,字符串s1为"cbadabacg",字符串s2为"abc",字符串s2的两个变位词"cba"和"bac"是字符串s1中的子字符串,输入它们在字符串s1中的起始下标0和5。/** * @param {string} s * @param {string} p * @return {number[]} */var findAnagrams = function(s1, s2) { if(s2.length > s1.length) { return []; } let result = []; const counts = new Array(26).fill(0); var areAllZero = function(counts) { for(let i of counts) { if(i !== 0) { return false } } return true; } for(let i = 0; i < s2.length; i++){ counts[s2.charCodeAt(i)-97]++ counts[s1.charCodeAt(i)-97]-- } if(areAllZero(counts)) { result.push(0) } for(let i = s2.length; i < s1.length; i++) { counts[s1.charCodeAt(i)-97]-- counts[s1.charCodeAt(i-s2.length)-97]++ if(areAllZero(counts)) { result.push(i-s2.length+1) } } return result;};不含反复字符的最长子字符串题目:输出一个字符串,求该字符串中不含反复字符的最长子字符串的长度。例如,输出字符串"babcca",其最长的不含反复字符的子字符串是"abc",长度为3。/** * @param {string} s * @return {number} */var lengthOfLongestSubstring = function(s) { if(s.length == 0) return 0 if(s.length == 1) return 1 const map = new Map(); let max = 0 // 双指针 let left = 0 let right = 0 var areAllZero = function(counts) { for(let i of counts) { if(i[1] === 2) { return false } } return true; } while(right < s.length) { if(map.get(s.charAt(right)) !== 1) { map.set(s.charAt(right), (map.get(s.charAt(right)) || 0) + 1) right++ if(map.get(s.charAt(right)) === 1) { max = Math.max(right - left, max) } } else { map.set(s.charAt(left), (map.get(s.charAt(left)) || 0) - 1) left++ } } max = Math.max(right - left, max) return max;};蕴含所有字符的最短字符串输出两个字符串s和t,请找出字符串s中蕴含字符串t的所有字符的最短子字符串。例如,输出的字符串s为"ADDBANCAD",字符串t为"ABC",则字符串s中蕴含字符'A'、'B'和'C'的最短子字符串是"BANC"。如果不存在符合条件的子字符串,则返回空字符串""。如果存在多个符合条件的子字符串,则返回任意一个。var minWindow = (s, t) => { const charToCount = new Map(); for(let ch of t) { charToCount.set(ch, (charToCount.get(ch) || 0) + 1) } let count = charToCount.size; let start = 0, end = 0, minStart = 0, minEnd = 0; let minLength = Infinity; while(end < s.length || (count == 0) && end == s.length) { if(count > 0) { let endCh = s.charAt(end); if(charToCount.get(endCh)!== undefined) { charToCount.set(endCh, charToCount.get(endCh)-1) if (charToCount.get(endCh) === 0) { count-- } } } else { if (end - start < minLength) { minLength = end - start minStart = start minEnd = end } let startCh = s.charAt(start); if(charToCount.get(startCh)!==undefined) { charToCount.set(startCh, charToCount.get(startCh) + 1) if(charToCount.get(startCh) == 1) { count++ } } start++ } } return minLength < Infinity ? s.substring(minStrat, minEnd) : "";}上述代码中只有一个while循环,用来把两个变量从0减少到字符串s的长度。如果字符串的长度是n,那么工夫复杂度就是O(n)。能够应用一个哈希表来统计每个字符呈现的次数。哈希表的键为字符,假如字符串中只有英文字母,那么哈希表的大小不会超过256,辅助空间的大小不会随着字符串长度的减少而减少,因而空间复杂度是O(1)。 ...

March 28, 2022 · 4 min · jiezi

关于字符串:golangleetcode中级二叉树的序列化与反序列化常数时间插入删除和获取随机元素

第一题 二叉树的序列化与反序列化题目 解题思路本题有两个子问题 第一个为将二叉树序列化为一个字符串,即为二叉树的遍历对于树的遍历,有深搜和广搜两种深搜又按根节点的地位分为先序遍历,中序遍历,后序遍历三种状况这里咱们规定 应用先序遍历将其转化为字符串序列 这里咱们应用包strconv实现对根本数据类型的字符串示意的转换Atoi(string to int)和 Itoa(int to string) func (Codec) serialize(root *TreeNode) string { sb := &strings.Builder{} var dfs func(*TreeNode) dfs = func(node *TreeNode) { if node == nil { sb.WriteString("null,") return } sb.WriteString(strconv.Itoa(node.Val)) sb.WriteByte(',') dfs(node.Left) dfs(node.Right) } dfs(root) return sb.String()}第二个为将序列化的字符串从新转化为树依据先序遍历的规定字符的第一个元素为根节点 这里应用函数strings.Split函数按指定的分隔符切割字符串,并返回切割后的字符串切片。不便咱们对字符串进行操作 func (Codec) deserialize(data string) *TreeNode { sp := strings.Split(data, ",") var build func() *TreeNode build = func() *TreeNode { if sp[0] == "null" { sp = sp[1:] return nil } val, _ := strconv.Atoi(sp[0]) sp = sp[1:] return &TreeNode{val, build(), build()} } return build()}具体代码为package mainimport ( "strconv" "strings")type TreeNode struct { Val int Left *TreeNode Right *TreeNode}type Codec struct{}func Constructor() (_ Codec) { return}func (Codec) serialize(root *TreeNode) string { sb := &strings.Builder{} var dfs func(*TreeNode) dfs = func(node *TreeNode) { if node == nil { sb.WriteString("null,") return } sb.WriteString(strconv.Itoa(node.Val)) sb.WriteByte(',') dfs(node.Left) dfs(node.Right) } dfs(root) return sb.String()}func (Codec) deserialize(data string) *TreeNode { sp := strings.Split(data, ",") var build func() *TreeNode build = func() *TreeNode { if sp[0] == "null" { sp = sp[1:] return nil } val, _ := strconv.Atoi(sp[0]) sp = sp[1:] return &TreeNode{val, build(), build()} } return build()}复杂度剖析工夫复杂度:O(n);咱们只拜访每个节点一次,因而工夫复杂度为 O(n),其中 n 是节点数,即树的大小。空间复杂度:O(n);在序列化和反序列化函数中,咱们递归会应用栈空间,故渐进空间复杂度为 O(n)。 ...

February 23, 2022 · 2 min · jiezi

关于字符串:Java-字符串常见的操作

在 Java 当中,为字符串类提供了丰盛的操作方法,对于字符串,咱们常见的操作就是:字符串的比拟、查找、替换、拆分、截取以及其余的一些操作。 在 Java 中,有 String,StringBuffer 和 StringBuilder 字符串类,他们的区别是 String 类是不可批改的,而 StringBuffer 和 StringBuilder 类是能够批改的。要留神的是,这里的批改不是字面意思上的批改。简略来说,比方,要实现两个字符串的拼接,对于前者来说,假如有 str1 = "hello" , 要给他拼接一个"world",那么是这样的,在这个过程中,"hello"自身没有变,它还在池中。然而对于后两者来说,假如有 str2 = "世界",要拼接''你好'',拼接之后,池中就不存在"世界"了。StringBuffer 和 StringBuilder 的区别就是一个是线程平安的,一个不是线程平安的。 上面,咱们围绕字符串的一些操作来进行阐明。 一,字符串的比拟 1,equal()办法 官网文档形容: public boolean equals(Object anObject) 将此字符串与指定对象进行比拟。 其后果是 true 当且仅当该参数不是 null 并且是 String 对象,示意雷同的字符序列作为该对象。 参数 anObject - 比照这个 String 的对象 后果 true 如果给定的对象代表一个 String 等效于这个字符串, 否则 false String 类当中的 equal()办法用来比拟两个字符串是否相等。这一种比拟是辨别大小写的。当有一个是字符串常量的时候,倡议的写法是将字符串常量放在里面,这样的理由是:如果里面的变量是 null 的时候,就会抛出空指针异样。 String str1 = new String("Hello"); String str2 = new String("Hello"); ...

February 8, 2022 · 4 min · jiezi

关于字符串:不使用strcat函数进行字符串拼接

include"stdio.h"void function(char p,char q){ int n=0;int m=0;for(;p[n]!='\0';) n++;for(;q[m]!='\0';) p[++n]=q[m++];p[n]='\0';printf("%s",p);}int main(void){ char n[100]="asdfgh";char m[100]="zxcvbnm";function(n,m);return 0;}

October 18, 2021 · 1 min · jiezi