-
空格串:是只包含空格的串,空格串有内容和长度,而且可以不止一个空格。
-
空串:零个字符的串。
-
子串:串中任意个数的连续字符组成的子序列。
-
串的比较:
- 取决于它们挨个字母的前后顺序
- 通过组成串的字符之间的编码来进行(字符的编码指的是字符在对应字符集中的序号)
- ASCII编码:8位二进制表示一个字符,总共可表示256个字符。( 2 8 2^8 28)
- Unicode编码:16位二进制表示一个字符,总共6.5万个字符( 2 16 2^{16} 216)
- 线性表更关注的是单个元素的操作,比如查找一个元素,插入或删除一个元素。
- 串中更多的是查找子串位置、得到指定位置子串、替换子串等操作。
5.1朴素的模式匹配算法
子串的定位操作通常称做串的模式匹配
假设现在我们面临这样一个问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置,怎么查找呢?
如果用暴力匹配的思路,并假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置,则有:
- 如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符;
- 如果失配(即S[i]! = P[j]),令i = i - (j - 1)+1,j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。
- 最坏时间复杂度为 O ( ( n − m + 1 ) ∗ m ) O((n-m+1)*m) O((n−m+1)∗m)
//假设文本串S和模式串P的长度存在S[0]与P[0]中
//下标从[1]开始,这点要注意
int Index(char* s, char* p, int pos)
{
int i = pos; //i用于文本串S中当前位置的下标
int j = 1; //j用于模式串P中当前位置下标值
while (i <= s[0] && j <= p[0]) //若i小于s长度且j小于p的长度时,循环
{
if (s[i] == p[j])
{
//①如果当前字符匹配成功(即S[i] == P[j]),则i++,j++
i++;
j++;
}
else //指针后退,重新开始匹配
{
//②如果失配(即S[i]! = P[j]),令i = i - (j - 1)+1,j = 0
i = i - j + 2; //i退回到上次匹配首位的下一位
j = 1; //j退回到子串T的首位
}
}
//匹配成功,返回模式串p在文本串s中的位置,否则返回-1
if (j > p[0]) //遍历完模式串,匹配
return i - p[0];
else
return -1;
}
5.2KMP算法
假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置
- 如果j = 0,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符;
- 如果j != 0,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了j - next [j] 位。
- 换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值,即移动的实际位数为:j - next[j],且此值大于等于1。
- 让i值不回溯,j值回溯多少取决于模式串p当前字符之前的串的前后缀的相似度
int KmpSearch(char* s, char* p, int pos)
{
int i = pos;
int j = 1;
int next[255]
get_next(p, next) //对p进行分析,得到next数组
while (i <= s[0] && j <= p[0])
{
//①如果j = 0,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
if (j == 0 || s[i] == p[j])
{
i++;
j++;
}
else //指针后退重新开始匹配
{
//②如果j != 0,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]
//next[j]即为j所对应的next值
j = next[j]; //j退回到合适的位置,i值不变
}
}
if (j > p[0])
return i - p[0];
else
return -1;
}
5.3求next数组
- 字符串本身不是自己的前缀、后缀
- PMT前缀集合、后缀集合的交集中最长元素的长度
- 下标从1开始的next值:如果前后缀一个字符相等,k值是2,两个字符k值是3,n个相等k值就是n+1
j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
模式串p | a | b | a | b | a | a | a | b | a |
最大长度值(包含自己) | 0 | 0 | 1 | 2 | 3 | 1 | 1 | 2 | 3 |
next(不包含自己)(下标从0开始) | -1 | 0 | 0 | 1 | 2 | 3 | 1 | 1 | 2 |
next(不包含自己)(下标从1开始) | 0 | 1 | 1 | 2 | 3 | 4 | 2 | 2 | 3 |
void GetNext(char* p,int next[])
{
next[1] = 0;
int k = 1;
int j = 0;
while (j < p[0])
{
//p[k]表示前缀,p[j]表示后缀
if (k == 0 || p[j] == p[k]) //k==0,失配后,给失配处的字符赋值next值
{
++k;
++j;
next[j] = k;
}
else
{
k = next[k]; //若字符不相同,则k值回溯,寻找次大的匹配,使得k值回溯程度最小
}
}
}
5.4next数组的优化
当中的2、3、4、5步骤,其实都是多余的判断。由于p串的第二、三、四、五位置的字符都与首位的"a"相等,那么可以用首位next[1]的值去取代与它相等的字符后续next[j]的值,这是个很好的办法。
//优化过后的next 数组求法
void GetNextval(char* p, int next[])
{
next[1] = 0;
int i = 1;
int j = 0;
while (i < p[0])
{
//p[j]表示前缀,p[i]表示后缀
if (j == 0 || p[i] == p[j])
{
++i;
++j;
//较之前next数组求法,改动在下面4行
if (p[i] != p[j])
next[i] = j; //若当前字符与前缀字符不同,则当前的j为next在i位置的值
else
//因为不能出现p[i] = p[ next[i]],所以当出现时需要继续递归,k = next[k] = next[next[k]]
next[i] = next[j]; //如果与前缀字符相同,则将前缀字符的next值赋值给next在i位置的值
}
else
{
j = next[j];
}
}
}
j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
模式串p | a | b | a | b | a | a | a | b | a |
最大长度值(包含自己) | 0 | 0 | 1 | 2 | 3 | 1 | 1 | 2 | 3 |
next(不包含自己)(下标从0开始) | -1 | 0 | 0 | 1 | 2 | 3 | 1 | 1 | 2 |
next(不包含自己)(下标从1开始) | 0 | 1 | 1 | 2 | 3 | 4 | 2 | 2 | 3 |
改进的next值 | 0 | 1 | 0 | 1 | 0 | 4 | 2 | 1 | 0 |
如果某字符A的next值指向的字符B=这个字符A,则将B的next赋值给A的next值
如果a位字符与它next值指向的b位字符相等,则该a位的nextval就指向b位的nextval值,如果不等,则该a位的nextval值就是它自己a位的next值。