如果觉得有帮助,麻烦动动手指点赞加关注💗💗💗 非常感谢!!!
有想看源码的小伙伴请移步这里👉https://gitee.com/fearless123/demo/tree/master/src/main/java/com/ma
一、串
1. 定义
串(也称字符串)是由n(n>=0)个字符串组成的有限序列。抽象含义的串一般记为s=“s1,s2…sn”,其中s为串名,n 为串长度,双引号括起来的字符序列成为串值。每个字符si(0<i<n)可以是任意的Unicode码字符,一般是字母、数子、标点符号等屏幕可显示的字符。
含有零个元素的串称为空串,串中任意连续的字符组成的字符序列作为该串的子串,包含子串的串称为主串。要注意的是,空格也是串字符集合的一个元素,由一个或多个空格组成的串称为空格串。(空格串不是空串)
2. 存储结构
- 定长顺序存储表示,用字符数组来存储,变量Length来专门存储串的长度
- 变长分配存储表示,用指针变量指向动态分配存储区的首地址,同样变量Length来专门存储串的长度
3. 基本操作
下面是Java中String类常用的api
1、获取长度
public int length(); // 这与数组中的length属性获取长度不同
2、比较相等
public boolean equals(Object anObject);//判断内容是否相同
public boolean equalsIgnoreCase(String anotherString);//忽略大小写的情况下判断内容是否相同
如果想对字符串中的部分内容是否相同进行比较,可以用reagionMatches() 有两种
public boolean regionMatches(int toffset, String other,int ooffset,int len);表示如果String对象的一个子字符串与参数other的一个子字符串是相同的字符序列,则为true.要比较的String对象的字符串从索引toffset开始,other的字符串从索引ooffset开始,长度为len。
public boolean reagionMatches(boolean ignoreCase,int toffset,String other,int ooffset,int len);//用布尔类型的参数指明两个字符串的比较是否对大小写敏感。
3、比较大小
public int compareTo(String anotherString); //判断字符串的大小关系
public int compareToIgnoreCase(String str); //在比较时忽略字母大小写
4、查找字符串中某个位置的字符
public char charAt(int index);//返回指定索引index位置上的字符,索引范围从0开始
5、查找指定字符串在字符串中第一次或最后一词出现的位置
在String类中提供了两种查找指定位置的字符串第一次出现的位置的方法
public int indexOf(String str);//从字符串开始检索str,并返回第一次出现的位置,未出现返回-1
public int indexOf(String str,int fromIndex);//从字符串的第fromIndex个字符开始检索str
查找最后一次出现的位置有两种方法
public int lastIndexOf(String str);
public int lastIndexOf(String str,int fromIndex);
如果不关心字符串的确切位置则可使用public boolean contains(CharSequence s);
6、检查字符串的起始字符和结束字符
开始的字符串两种方法
public boolean starWith(String prefix,int toffset);//如果参数prefix表示的字符串序列是该对象从索引toffset处开始的子字符串,则返回true
public boolean starWith(String prefix);
结束的字符串方法
public boolean endsWith(String suffix);
7、截取子串
public String subString(int beginIndex);// 返回以beginIndex开头直至该字符串结尾的串
public String subString(int beginIndex,int endIndex);//返回的字符串是从beginIndex开始到endIndex-1的串,要返回后4位可以这样写Syetem.out.println(*.subString()(*.length()-4));
8、字符串的替换
public String replace(char oldChar,char newChar);
public String replace(CharSequence target,CharSequence replacement);//把原来的etarget子序列替换为replacement序列,返回新串
public String replaceAll(String regex,String replacement);//用正则表达式实现对字符串的匹配
9、字符串的大小写替转换
public String toLowerCase(Locale locale);
public String toLowerCase();
public String toupperCase(Locale locale);
public String toUpperCase();
10、去除字符串首尾空格
public String trim();
11、字符串转换
字符串转换成字符数组
public char[] toCharArray();
将字符串转换成字符串数组
public String[] split(String regex);//regex 是给定的匹配
将其它数据类型转化为字符串
public static String valueOf(T t);
4. KMP算法及改进
1)简单模式匹配算法
对于一个串中的子串的定位操作称为串的模式匹配,其中待定位的子串称为模式串。简答模式匹配算法的基本思想是:从主串的第一个位置起和模式串的第一个字符开始比较,如果相等,则继续逐一比较后续字符;否则从主串的第二字符开始,再重新用上一步的方法与模式串中的字符做比较,依次类推,直到比较完模式串中的所有字符。
/**
* 简单模式匹配算法,相当于java.lang.String的indexOf
* @param str
* @param substr
* @return
*/
public static int match(String str, String substr){
// 1.从主串的第一个位置起和模式串的第一个字符开始比较,如果相等,则继续逐一比较后续字符;
// 否则从主串的第二字符开始,再重新用上一步的方法与模式串中的字符做比较,依次类推,直到比较完模式串中的所有字符。
int index = -1;
boolean match = true;
for (int i = 0; i < str.length() - substr.length(); i++) {
match = true;
for (int j = 0; j < substr.length(); j++) {
if (str.charAt(i + j) != substr.charAt(j)){
match = false;
}
}
if (match){
index = i;
break;
}
}
return index;
}
2)KMP算法
Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。下面先直接给出KMP的算法流程(如果感到一点点不适,没关系,坚持下,稍后会有具体步骤及解释)
假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置
- 如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符;
- 如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了 j - next [j] 位。
换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值,即移动的实际位数为:j - next[j],且此值大于等于1。
很快,你也会意识到next 数组各值的含义:代表当前字符前半部分子串的最长相同前后缀的长度。例如,如果next [j] = k,代表j 之前的字符串中有最大长度为k 的相同前缀后缀。
此也意味着在某个字符失配时,该字符对应的next 值会告诉你下一步匹配中,模式串应该跳到哪个位置(跳到next [j] 的位置)。如果next [j] 等于0或-1,则跳到模式串的开头字符,若next [j] = k 且 k > 0,代表下次匹配跳到j 之前的某个字符,而不是跳到开头,且具体跳过了k 个字符。
寻找最大前后缀
如果给定的模式串是:“ABCDABD”,从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表格所示:
也就是说,原模式串子串对应的各个前缀后缀的公共元素的最大长度表为(下简称《最大长度表》):
注:求解最大相同前后缀详细流程可以点这里 👉阮一峰 老师的KMP算法的讲解
根据《最大长度表》求next 数组
由上文,我们已经知道,字符串“ABCDABD”各个前缀后缀的最大公共元素长度分别为:
而且,根据这个表可以得出下述结论:失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值。
上文利用这个表和结论进行匹配时,我们发现,当匹配到一个字符失配时,其实没必要考虑当前失配的字符,更何况我们每次失配时,都是看的失配字符的上一位字符对应的最大长度值。如此,便引出了next 数组。可求得它的next 数组如下:
把next 数组跟之前求得的最大长度表对比后,不难发现,next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1。意识到了这一点,你会惊呼原来next 数组的求解竟然如此简单:就是找最大对称长度的前缀后缀,然后整体右移一位,初值赋为-1(当然,你也可以直接计算某个字符对应的next值,就是看这个字符之前的字符串中有多大长度的相同前缀后缀)。
换言之,对于给定的模式串:ABCDABD,它的最大长度表及next 数组分别如下:
根据最大长度表求出了next 数组后,从而有另外一个结论:失配时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值
而后,你会发现,无论是基于《最大长度表》的匹配,还是基于next 数组的匹配,两者得出来的向右移动的位数是一样的。为什么呢?因为:
- 根据《最大长度表》,失配时,模式串向右移动的位数 = 已经匹配的字符数 - 失配字符的上一位字符的最大长度值
- 而根据《next 数组》,失配时,模式串向右移动的位数 = 失配字符的位置 - 失配字符对应的next 值
其中,从0开始计数时,失配字符的位置 = 已经匹配的字符数(失配字符不计数),而失配字符对应的next 值 = 失配字符的上一位字符的最大长度值,两相比较,结果必然完全一致。
所以,你可以把《最大长度表》看做是next 数组的雏形,甚至就把它当做next 数组也是可以的,区别不过是怎么用的问题。(利用最大长度表也可以实现kmp算法)
递推计算next 数组(KMP算法的核心部分)
接下来,咱们来写代码求下next 数组。
基于之前的理解,可知计算next 数组的方法可以采用递推:
- 如果对于值k,已有p0 p1, …, pk-1 = pj-k pj-k+1, …, pj-1,相当于next[j] = k。
此意味着什么呢?究其本质,next[j] = k 代表p[j] 之前的模式串子串中,有长度为k 的相同前缀和后缀。有了这个next 数组,在KMP匹配中,当模式串中j 处的字符失配时,下一步用next[j]处的字符继续跟文本串匹配,相当于模式串向右移动j - next[j] 位。
KMP代码如下:
下面是代码实现:
/**
* 根据 subStr 字符串 来计算出对应的部分匹配值
* @param subStr
* @return
*/
private static int calcMathValue(String subStr){
int length = subStr.length();
String prefixStr = subStr.substring(0, length - 1);
String suffixStr= subStr.substring(1);
while (prefixStr.length() > 0 && suffixStr.length() > 0){
if (prefixStr.equals(suffixStr)){
return prefixStr.length();
}
if (prefixStr.length() == 1 && suffixStr.length() == 1){
break;
}
prefixStr = prefixStr.substring(0, prefixStr.length() - 1);
suffixStr = suffixStr.substring(1, suffixStr.length());
}
return 0;
}
/**
* 根据模式串 pattern 创造出对应的部分匹配表
* @param pattern
* @return
*/
private static int[] createPartialMatchTable(String pattern){
int patternLen = pattern.length();
int[] matchTalbe = new int[patternLen];
int i = 0;
int matchValue = 0;
while(i < patternLen){
if (i == 0){
matchValue = 0;
} else {
matchValue = calcMathValue(pattern.substring(0, i + 1));
}
matchTalbe[i] = matchValue;
i++;
}
return matchTalbe;
}
/**
* 使用KMP算法算出pattern字符串在target字符串当中的位置
* -1 代表不存在,即主串中的所有子串没有与模式串相匹配的
* resultIndex 返回模式串在主串中的初始位置
* @param target
* @param pattern
* @return
*/
private static int KMP(String target, String pattern){
int[] partialMatchTable = createPartialMatchTable(pattern);
char[] targetCharArr = target.toCharArray();
char[] patternCharArr = pattern.toCharArray();
int matchCharCounts = 0; // 记录下已经匹配的字符的个数
int i = 0, j = 0, moveCounts = 0;
int resultIndex = -1;
boolean firstMatchFlg = false;
while(i < targetCharArr.length){
// 如果当前主串和子串的字符匹配上了,那么比较下一字符是否匹配
if (targetCharArr[i] == patternCharArr[j]){
// 记录模式串在主串的初始位置
if (!firstMatchFlg){
firstMatchFlg = true;
resultIndex = i;
}
matchCharCounts++;
i++;
j++;
}
// 如果子串的第一个元素都不和子串的元素相等,那么就拿主串的下一个元素进行比较
else if (j == 0){
i++;
}
// 如果子串不是在第一个元素的位置而是在其他位置进行了匹配,那么进行移位操作
else {
// 移动位数 = 已匹配的字符数 - 对应的部分匹配值
// 对应匹配值 指的是最后一个字符的对应匹配值 j是失配的位置 所以这里是partialMatchTable[j - 1]
moveCounts = matchCharCounts - partialMatchTable[j - 1];
j = j - moveCounts; // 移动模式串 往前移动moveCounts位
matchCharCounts = matchCharCounts - moveCounts;
firstMatchFlg = false; // 使首位下标记录失效
resultIndex = -1;
}
// 如果匹配成功了 直接返回首位下标位置
if (j == patternCharArr.length){
return resultIndex;
}
}
return resultIndex;
}
该算法比较难理解,感兴趣的同学可以点这里👇