本篇博文将详细总结算法里面关于字符串部分知识,包括:
- 字符串循环左移
- LCS最长递增子序列
- KMP
- Huffman编码
这里面一些算法比如LCS,KMP,Huffman是非常难以理解的,也是一些笔试面试经常遇见的问题,务必要全部弄清楚。
字符串循环左移
给定一个字符串S[0…N-1],要求把S的前k个字符移动到S的尾部,如把字符串“abcdef”前面的2个字符‘a’、‘b’移动到字符串的尾部,得到新字符串“cdefab”:即字符串循环左移k。
算法要求:
- 时间复杂度为 O(n),空间复杂度为 O(1)。
算法分析
$ (X’Y’)’=YX$
如:
a
b
c
d
e
f
abcdef
abcdef
X
=
a
b
X
’
=
b
a
X=ab X’=ba
X=abX’=ba
Y
=
c
d
e
f
Y
’
=
f
e
d
c
Y=cdef Y’=fedc
Y=cdefY’=fedc
(
X
’
Y
’
)
’
=
(
b
a
f
e
d
c
)
’
=
c
d
e
f
a
b
(X’Y’)’=(bafedc)’=cdefab
(X’Y’)’=(bafedc)’=cdefab
时间复杂度O(N),空间复杂度O(1)
代码实现
C++
#include<stdio.h>
#include <stdlib.h>
#include<stack>
#include<string>
using namespace std;
void Print(char p[], int size)
{
printf("%c", p[0]);
for (int i = 1; i < size; i++)
{
printf(",%c", p[i]);
}
printf("\n");
}
void reverse(char p[], int from, int to)
{
while (from<to)
{
char t = p[from];
p[from++] = p[to];
p[to--] = t;
}
//Print(p, 5);
}
void leftMove(char p[], int k, int size)
{
k %= size;
reverse(p, 0, k-1);
reverse(p, k, size-1);
reverse(p, 0, size-1);
Print(p,size);
}
int main()
{
char p[] = "abcdef";
int size = strlen(p);
leftMove(p, 2, size);
}
JAVA
public class LeftMove{
public void reverse(char[] str, int i, int j){
while(i < j){
char tmp = str[i];
str[i++] = str[j];
str[j--] = tmp;
}
}
public void func(String str, int m){
if(m > str.length()-1)
m = str.length();
if(m < 0)
m = 0;
char[] arr = str.toCharArray();
reverse(arr, 0, m-1);
reverse(arr, m, arr.length-1);
reverse(arr, 0, arr.length-1);
for(char e: arr)
System.out.print(e+",");
}
public static void main(String[] args){
String str = "abcdefg";
LeftMove obj = new LeftMove();
obj.func(str, 2);
}
}
LCS(最长公共子序列)
最长公共子序列,即Longest Common Subsequence,LCS。
一个序列S任意删除若干个字符得到新序列T,则T叫做S的子序列。
两个序列X和Y的公共子序列中,长度最长的那个,定义为X和Y的最长公共子序列。
- 字符串13455与245576的最长公共子序列为455。
- 字符串acdfg与adfc的最长公共子序列为adf。
注:子序列在原序列中不需要连续,而子串必须是在原串中连续存在的。
LCS的记号
字符串X,长度为m,从1开始数;
字符串Y,长度为n ,从1开始数;
X
i
=
﹤
x
1
,
⋯
,
x
i
﹥
即
X
序
列
的
前
i
个
字
符
(
1
≤
i
≤
m
)
(
X
i
不
妨
读
作
“
字
符
串
X
的
i
前
缀
”
)
Xi=﹤x1,⋯,xi﹥ 即X序列的前i个字符(1≤i≤m) (Xi不妨读作“字符串X的i前缀”)
Xi=﹤x1,⋯,xi﹥即X序列的前i个字符(1≤i≤m)(Xi不妨读作“字符串X的i前缀”)
Y
j
=
﹤
y
1
,
⋯
,
y
j
﹥
即
Y
序
列
的
前
j
个
字
符
(
1
≤
j
≤
n
)
(
字
符
串
Y
的
j
前
缀
)
;
Yj=﹤y1,⋯,yj﹥ 即Y序列的前j个字符 (1≤j≤n) (字符串Y的j前缀);
Yj=﹤y1,⋯,yj﹥即Y序列的前j个字符(1≤j≤n)(字符串Y的j前缀);
L
C
S
(
X
,
Y
)
为
字
符
串
X
和
Y
的
最
长
公
共
子
序
列
,
即
为
Z
=
﹤
z
1
,
⋯
,
z
k
﹥
LCS(X , Y) 为字符串X和Y的最长公共子序列,即为Z=﹤z1,⋯,zk﹥
LCS(X,Y)为字符串X和Y的最长公共子序列,即为Z=﹤z1,⋯,zk﹥。
注:不严格的表述。事实上,X和Y的可能存在多个子串,长度相同并且最大,因此,LCS(X,Y)严格的说,是个字符串集合。即:Z∈ LCS(X , Y) 。
LCS解法的探索
结尾字符相等: x m = y n {x}_{m}={y}_{n} xm=yn
若 x m = y n {x}_{m}={y}_{n} xm=yn (最后一个字符相同),则: X m {X}_{m} Xm 与 Y n {Y}_{n} Yn的最长公共子序列 Z k {Z}_{k} Zk 的最后一个字符必定为 x m {x}_{m} xm或 y n {y}_{n} yn。
即:
z
k
=
x
m
=
y
n
{z}_{k}={x}_{m}={y}_{n}
zk=xm=yn;
L
C
S
(
X
m
,
Y
n
)
=
L
C
S
(
X
m
−
1
,
Y
n
−
1
)
+
x
m
LCS({X}_{m} , {Y}_{n} ) = LCS({X}_{m-1} , {Y}_{n-1} ) + {x}_{m}
LCS(Xm,Yn)=LCS(Xm−1,Yn−1)+xm
举例: x m = y n {x}_{m}={y}_{n} xm=yn
对于上面的字符串X和Y:
x
3
=
y
3
=
‘
C
,
则
:
L
C
S
(
B
D
C
,
A
B
C
)
=
L
C
S
(
B
D
,
A
B
)
+
‘
C
’
{x}_{3}={y}_{3} =‘C,则:LCS(BDC,ABC)=LCS(BD,AB)+‘C’
x3=y3=‘C,则:LCS(BDC,ABC)=LCS(BD,AB)+‘C’
x
5
=
y
4
=
‘
B
,
则
:
L
C
S
(
B
D
C
A
B
,
A
B
C
B
)
=
L
C
S
(
B
D
C
A
,
A
B
C
)
+
‘
B
’
{x}_{5}={y}_{4} =‘B,则:LCS(BDCAB,ABCB)=LCS(BDCA,ABC)+‘B’
x5=y4=‘B,则:LCS(BDCAB,ABCB)=LCS(BDCA,ABC)+‘B’
结尾字符不相等: x m ≠ y n {x}_{m}\neq {y}_{n} xm=yn
若 x m ≠ y n {x}_{m}\neq {y}_{n} xm=yn ,则:
- 要么: L C S ( X m , Y n ) = L C S ( X m − 1 , Y n ) LCS({X}_{m} ,{Y}_{n} )=LCS({X}_{m-1} , {Y}_{n} ) LCS(Xm,Yn)=LCS(Xm−1,Yn)
- 要么: L C S ( X m , Y n ) = L C S ( X m , Y n − 1 ) LCS({X}_{m} ,{Y}_{n})=LCS({X}_{m} , {Y}_{n-1} ) LCS(Xm,Yn)=LCS(Xm,Yn−1)
即,若 x m ≠ y n {x}_{m}\neq {y}_{n} xm=yn ,则: L C S ( X m , Y n ) = m a x { L C S ( X m − 1 , Y n ) , L C S ( X m , Y n − 1 ) } LCS({X}_{m} ,{Y}_{n} )= max\begin{Bmatrix} LCS({X}_{m-1} , {Y}_{n} ),LCS({X}_{m} , {Y}_{n-1} )\end{Bmatrix} LCS(Xm,Yn)=max{LCS(Xm−1,Yn),LCS(Xm,Yn−1)}
举例: x m ≠ y n {x}_{m}\neq{y}_{n} xm=yn
对于字符串X和Y:
x
2
≠
y
2
,
则
:
L
C
S
(
B
D
,
A
B
)
=
m
a
x
{
L
C
S
(
B
D
,
A
)
,
L
C
S
(
B
,
A
B
)
}
{x}_{2}\neq{y}_{2},则:LCS(BD,AB)=max\begin{Bmatrix} LCS(BD,A), LCS(B, AB) \end{Bmatrix}
x2=y2,则:LCS(BD,AB)=max{LCS(BD,A),LCS(B,AB)}
x 4 ≠ y 5 , 则 : L C S ( B D C A , A B C B D ) = m a x { L C S ( B D C A , A B C B ) , L C S ( B D C , A B C B D ) } {x}_{4}\neq{y}_{5} ,则:LCS(BDCA,ABCBD)=max\begin{Bmatrix} LCS(BDCA,ABCB), LCS(BDC,ABCBD) \end{Bmatrix} x4=y5,则:LCS(BDCA,ABCBD)=max{LCS(BDCA,ABCB),LCS(BDC,ABCBD)}
LCS分析总结
算法中的数据结构:长度数组
使用二维数组 C [ m , n ] C\begin{bmatrix}m,n\end{bmatrix} C[m,n]。
C [ i , j ] C\begin{bmatrix}i,j\end{bmatrix} C[i,j] 记录序列Xi和Yj的最长公共子序列的长度。
当i=0或j=0时,空序列是 X i 和 Y j 的 最 长 公 共 子 序 列 , 故 C [ i , j ] = 0 {X}_{i}和{Y}_{j} 的最长公共子序列,故 C\begin{bmatrix}i,j\end{bmatrix}=0 Xi和Yj的最长公共子序列,故C[i,j]=0 。
实例:
X
=
<
A
,
B
,
C
,
B
,
D
,
A
,
B
>
X=< A,B,C,B,D,A,B >
X=<A,B,C,B,D,A,B>
Y
=
<
B
,
D
,
C
,
A
,
B
,
A
>
Y=< B,D,C,A,B,A >
Y=<B,D,C,A,B,A>
我们利用上面得出的结论进行打表:
注:这里两个序列下标都是从1开始计数。表cheese的shape为(X.size()+1,Y.size()+1),cheese[i][j]表示序列X的i前缀(i从1计,并且包括第i个字符)和序列Y的j前缀的最长公共子系列长度。
代码实现
C++
#include<stdio.h>
#include <stdlib.h>
#include<stack>
#include<string>
#include<vector>
#include<iostream>
using namespace std;
void reverse(string &str,int from,int to)
{
while (from<to)
{
char t = str[from];
str[from++] = str[to];
str[to--] = t;
}
}
void LCS(char *str1, char *str2, string &str)
{
int len1 = strlen(str1);
int len2 = strlen(str2);
char * s1 = str1 - 1;//使数值下标从1开始,方便下面编程,即数组下标依次后移加一后移一位
//这一步非常重要,从上面的打表可以看出,两序列下标都是从1开始计算,在编程中如果不从1开始会很麻烦。
char * s2 = str2 - 1;
vector<vector<int>> cheese(len1 + 1, vector<int>(len2 + 1));
for (int i = 0; i < len2 + 1; i++)
{
cheese[0][i] = 0;
}
for (int i = 0; i < len1 + 1; i++)
{
cheese[i][0] = 0;
}
for (int i = 1; i < =len1; i++)
{
for (int j = 1; j < =len2; j++)
{
if (s1[i] == s2[j])
cheese[i][j] = cheese[i-1][j-1] + 1;
else
cheese[i][j] = cheese[i][j-1] > cheese[i-1][j] ? cheese[i][j-1] : cheese[i-1][j];
}
}
printf("最长公共子序列长度为%d\n", cheese[len1][len2]);
int i = len1;
int j = len2;
while ((i!=0)&&(j!=0))
{
if (s1[i] == s2[j])
{
str.push_back(s1[i]);//只有两序列相等的元素才存储到str。
i--;
j--;
}
else
{
if (cheese[i][j - 1] > cheese[i - 1][j])
j--;
else
{
i--;
}
}
}
reverse(str, 0, str.length()-1);
}
void Print(string str)
{
printf("%c", str[0]);
for (int i = 1; i < str.length(); i++)
{
printf(",%c", str[i]);
}
printf("\n");
}
int main()
{
char *str1 = "ABCBDAB";
char *str2 = "BDCABA";
string str;
LCS(str1, str2, str);
Print(str);
return 0;
}
JAVA
import java.util.Stack;
public class LCS{
public String func(String str1, String str2){
int len1 = str1.length();
int len2 = str2.length();
int[][] lcs = new int[len1+1][len2+1];
for(int i=0; i<=len1; i++)
lcs[i][0] = 0;
for(int i=0; i<=len2; i++)
lcs[0][i] = 0;
for(int i = 1; i <= len1; i++){
for(int j = 1; j <= len2; j++){
if(str1.charAt(i-1) == str2.charAt(j-1)){
lcs[i][j] = lcs[i-1][j-1] + 1;
}else{
lcs[i][j] = lcs[i-1][j] > lcs[i][j-1]? lcs[i-1][j]: lcs[i][j-1];
}
}
}
System.out.println(lcs[len1][len2]);
int i = len1;
int j = len2;
Stack<Character> stack = new Stack<>();
while(i > 0 && j > 0){
if(str1.charAt(i-1) == str2.charAt(j-1)){
stack.push(str1.charAt(i-1));
i--;
j--;
}else{
if(lcs[i-1][j] > lcs[i][j-1]){
i--;
}else
j--;
}
}
String res = "";
while(!stack.isEmpty()){
res += stack.pop();
}
return res;
}
public static void main(String[] args){
String str1 = "13455";
String str2 = "245576";
LCS obj = new LCS();
String res = obj.func(str1, str2);
System.out.println(res);
}
}
若只计算 L C S LCS LCS的长度,则空间复杂度为 O ( m i n ( m , n ) ) O(min(m, n)) O(min(m,n))。在计算 c [ i , j ] c[i,j] c[i,j]时,只用到数组c的第i行和第i-1行。因此,只要用2行的数组空间就可以计算出最长公共子序列的长度。
最大公共子序列的多解性:求所有的LCS
当 x m ≠ y n {x}_{m}\neq {y}_{n} xm=yn 时:若 L C S ( x m − 1 , Y n ) = L C S ( x m , Y n − 1 ) LCS({x}_{m-1} ,{Y}_{n})=LCS({x}_{m} ,{Y}_{n-1} ) LCS(xm−1,Yn)=LCS(xm,Yn−1),会导致多解:有多个最长公共子序 列,并且它们的长度相等。
LCS的应用:最长递增子序列LIS
Longest Increasing Subsequence;给定一个长度为N的数组,找出一个最长的单调递增子序列。
原数组为A {5, 6, 7, 1, 2, 8},排序后:A’{1, 2, 5, 6, 7, 8}。 因为,原数组A的子序列顺序保持不变,而且排序后A’本身就是递增的,这样,就保证了两序列的最长公共子序列的递增特性。如此,若想求数组A的最长递增子序列,其实就是求数组A与它的排序数组A’的最长公共子序列。
代码实现:
#include<stdio.h>
#include <stdlib.h>
#include<stack>
#include<string>
#include<vector>
#include<iostream>
using namespace std;
void reverse(string &str,int from,int to)
{
while (from<to)
{
char t = str[from];
str[from++] = str[to];
str[to--] = t;
}
}
void LCS(char *str1, char *str2, string &str)
{
int len1 = strlen(str1);
int len2 = strlen(str2);
char * s1 = str1 - 1;//使数值下标从1开始,方便下面编程,即数组下标依次后移加一后移一位
char * s2 = str2 - 1;
vector<vector<int>> cheese(len1 + 1, vector<int>(len2 + 1));
for (int i = 0; i < len2 + 1; i++)
{
cheese[0][i] = 0;
}
for (int i = 0; i < len1 + 1; i++)
{
cheese[i][0] = 0;
}
for (int i = 1; i <= len1; i++)
{
for (int j = 1; j <=len2; j++)
{
if (s1[i] == s2[j])
{
cheese[i][j] = cheese[i - 1][j - 1] + 1;
}
else
cheese[i][j] = cheese[i][j-1] > cheese[i-1][j] ? cheese[i][j-1] : cheese[i-1][j];
}
}
printf("最长公共子序列长度为%d\n", cheese[len1][len2]);
int i = len1;
int j = len2;
while ((i!=0)&&(j!=0))
{
if (s1[i] == s2[j])
{
str.push_back(s1[i]);
i--;
j--;
}
else
{
if (cheese[i][j - 1] > cheese[i - 1][j])
j--;
else
{
i--;
}
}
}
reverse(str, 0, str.length()-1);
}
void Print(string str)
{
printf("%c", str[0]);
for (int i = 1; i < str.length(); i++)
{
printf(",%c", str[i]);
}
printf("\n");
}
void Print2(char* str)
{
printf("%c", str[0]);
for (int i = 1; i < strlen(str); i++)
{
printf(",%c", str[i]);
}
printf("\n");
}
void bubbleSort(char str[],int n)
{
for (int i = 0; i < n; i++)
{
for (int j = n-1; j > i ; j--)
{
if (str[j]<str[j - 1])
{
char t = str[j];
str[j] = str[j - 1];
str[j - 1] = t;
}
}
}
}
int main()
{
char str1[] = "ABCBDAB";
int n = strlen(str1);
vector<char> temp;
for (int i = 0; i < n; i++)
{
temp.push_back(str1[i]);
}
temp.erase(8, 10);
char* str2 = temp.data();
bubbleSort(str1, n);
Print2(str1);
Print2(str2);
string str;
LCS(str1, str2, str);
Print(str);
return 0;
}
KMP算法
字符串查找问题
给定文本串text和模式串pattern,从文本串text中找出模式串pattern第一次出现的位置。
最基本的字符串匹配算法:暴力求解(Brute Force) :时间复杂度
O
(
m
∗
n
)
O(m*n)
O(m∗n)
KMP算法是一种线性时间复杂度的字符串匹配算法,它是对BF算法改进。
记:文本串长度为N,模式串长度为M
- BF算法的时间复杂度 O ( M ∗ N ) O(M*N) O(M∗N),空间复杂度 O ( 1 ) O(1) O(1)。
- KMP算法的时间复杂度 O ( M + N ) O(M+N) O(M+N),空间复杂度 O ( M ) O(M) O(M)。
BF算法思想
由上图,较长的为字符串 s s s,下面较短的为模式串 p p p,当前模式串 p p p 从 s s s 的第 i i i 个位置开始匹配, s s s 和 p p p 同步的往后比较每个元素是否相同,如果 p p p 能成功匹配完毕则 i i i 即为所求结果。若当匹配到某一个位置时,发现这个位置的元素不同,当前匹配不成功, i i i 后移一位, j j j 回溯到 p p p 首位重新尝试匹配。
BF算法代码实现
C++
#include<stdio.h>
#include <stdlib.h>
#include<stack>
#include<string>
using namespace std;
//查找s中首次出现p的位置
int BruteForceSearch(char *s, char *p)
{
int i = 0;//当前匹配到p首位位于s的位置,也即是i是在S中开始匹配的首位置
int j = 0;//当前匹配到p的位置
int size = strlen(p);
int nlast = strlen(s) - size;
while ((j<size)&&(i<=nlast))//注意边界情况
{
if (s[i+j] == p[j])//若相等,模式p匹配位置后移
{
j++;
}
else//若发现不匹配,s位置后移一位,模式p回溯到首位
{
i++;
j = 0;
}
}
if (j > size)
return i;
return -1;
}
JAVA
class BruteForceSearch{
public int func(String str, String patt){
int i = 0;
int j = 0;
int s_len = str.length();
int p_len = patt.length();
int end = s_len - p_len;
while(i < end && j < p_len){
if(str.charAt(i+j) == patt.charAt(j)){
j++;
}else{
i++;
j=0;
}
}
if(j == p_len)
return i;
return -1;
}
public static void main(String[] args){
BruteForceSearch obj = new BruteForceSearch();
int res = obj.func("122134", "13");
System.out.println(res);
}
}
看上面整个BF的匹配过程,每次匹配失败后模式串 p p p 都是沿着 s s s 后移一位一位的匹配,其匹配速度太慢,而每次匹配失败后,模式串 p p p 都得回溯到首位重新匹配,时间复杂度较高。
KMP算法思想
我们以上图为例,长一些的为文本串 s s s,短一些的为模式串 p p p,当模式串 p p p 匹配到绿色那一小块时,对应的文本串 s s s 黄色那一小块,假设这一小块并不匹配,那么按照上面的暴力解法,模式串 p p p 整体沿着文本串 s s s 后移一位,p再回溯到首位进行重新匹配。那么有没有更好更快的办法呢?使 得当前匹配失败后 p p p 后移更多位数,同时不用每次失败后回溯到首位重新匹配? 这样其时间复杂度就会变为线性的。
看上图,如果在模式串 p p p 在位置 d d d 处的内容与对应的文本串 s s s 的位置 d ′ d' d′ 内容不一致 ,则匹配失败,不过可以肯定的是模式串 p p p 在 位置 d d d 之前的内容和文本串 s s s 在位置 d ′ d' d′ 之前的内容完全匹配。这里我们不将模式串 p p p 整体只是后移一位而是后移若干位,然后比较模式串 p p p 在 位置 c c c 处内容 与文本串 s s s 在位置 d ′ d' d′ 内容是否一致。由上图可以看出,如果这样移动合理的话,那么模式串 p p p 在 A A A 处内容必须和模式串在 B B B (这里需要注意 B B B的后面一块必须是之前不匹配的位置。)处内容一致,也可以这样说,如果我们知道模式串 p p p 在 A A A处内容和在 B B B 处内容一致,那么我们就敢于将模式串直接拉到文本串 s s s 的 B B B 位置处,然后再比较模式串 p p p 的 c c c 位置处内容与文本串 s s s 的 d ′ d' d′位置处内容是否一致。这样一来,模式串每次就不仅仅后移一位了,每次失败后模式串不用都回溯到首位重新匹配。并且上次文本串 s s s 比较到第 i i i 个位置,下次比较还是从第 i i i 位置开始比较。
那么问题来了,上面所讲的模式串 p p p 在 A A A 处和在 B B B 处内容要一致,并且 B B B 处后面的位置即为上次匹配失败的位置。那么在模式串 p p p 如何找到这样的 A A A 和 B B B 呢?这是KMP算法要解决的核心问题。
对于模式串的位置 j j j(注意这里是 j j j,上一次匹配失败的位置),考察 P a t t e r n j − 1 = p 0 p 1 p 2 . . . p j − 1 {Pattern}_{j-1}={p}_{0}{p}_{1}{p}_{2}...{p}_{j-1} Patternj−1=p0p1p2...pj−1 ,查找字符串 P a t t e r n j − 1 {Pattern}_{j-1} Patternj−1 的最大相等 k k k 前缀(就是上面说的 A A A 处内容,长度为 k k k)和 k k k 后缀(就是上面说的 B B B 内容)。
即:查找满足条件的最大的 k k k,使得 p 0 p 1 p 2 . . . p k − 1 = p j − k p j − k + 1 . . . p j − 2 p j − 1 {p}_{0}{p}_{1}{p}_{2}...{p}_{k-1} ={p}_{j-k}{p}_{j-k+1}...{p}_{j-2}{p}_{j-1} p0p1p2...pk−1=pj−kpj−k+1...pj−2pj−1
这里我们定义一个 n e x t next next 数组,计算 n e x t [ j ] next[j] next[j] ( j j j 为上次匹配失败的位置)时,考察的字符串是模式串的前 j − 1 j-1 j−1 个字符,与 p a t t e r n [ j ] pattern[j] pattern[j] 无关。
求模式串的next
如:j=5时,考察字符串 “ a b a a b ” “abaab” “abaab” 的最大相等 k k k 前缀和 k k k 后缀。
故k=2。
next的递推关系
对于模式串的位置j,有 n e x t [ j ] = k next[j]=k next[j]=k ,即: p 0 p 1 p 2 . . . p k − 1 = p j − k p j − k + 1 . . . p j − 2 p j − 1 {p}_{0}{p}_{1}{p}_{2}...{p}_{k-1} ={p}_{j-k}{p}_{j-k+1}...{p}_{j-2}{p}_{j-1} p0p1p2...pk−1=pj−kpj−k+1...pj−2pj−1
则,对于模式串的位置 j + 1 j+1 j+1,考察 p j {p}_{j} pj :
-
若 p [ k ] = = p [ j ] p[k]==p[j] p[k]==p[j]
n e x t [ j + 1 ] = n e x t [ j ] + 1 next[j+1]=next[j]+1 next[j+1]=next[j]+1 -
若 p [ k ] ≠ p [ j ] p[k]≠p[j] p[k]=p[j](这里需要注意 j , k j,k j,k 都是从0开始计,那么 p [ h ] p[h] p[h] 表示模式串从开始计长度为 h h h 后面一个位置内容)
记 h = n e x t [ k ] h=next[k] h=next[k], h h h表示 p k {p}_{k} pk 的最大前缀后缀长度 ;如果 p [ h ] = = p [ j ] p[h]==p[j] p[h]==p[j],则 n e x t [ j + 1 ] = h + 1 next[j+1]=h+1 next[j+1]=h+1,否则重复此过程。
next数组代码实现:
C++
void getNext(char* p, int next[])
{
int j = 0;
int k = -1;
int n = strlen(p);
next[j] = k;
while (j<n-1)//next数组长度即为模式串长度,下面有个++j。
{
//由上面的算法分析,知道next每次第j+1个k的求解都是在上一次第j个k的基础上得出。所以下面的循环是依次求next[1],next[2]...。
//只有在p[k] == p[j]情况下,才能计算出next[j+1]
if (k == -1 || p[k] == p[j])//k=-1有两种情况:首次比较;一直匹配不成功需要不断的回溯,
//可能会回溯到模式串首位,这个时候k=next[k]=-1
{
++j;
++k;//没加1之前的k为next[j]
next[j] = k;
}
else //p[j]与p[k]失配,这个时候不需要回溯到首位,而是p[next[k]]。
//则连续递归计算前缀p[next[k]],这里始终是在求next[j]对应的k。注意在next中k从0开始,
//而最长前缀后缀长度k是从1开始,故p[k]表示是在模式串长度k后的位置的内容。
{
k = next[k];
}
}
}
JAVA
public class Kmp_GetNext{
public void getNext(String str, int[] next){
int k = -1;
int j = 0;
next[j] = k;
int len = next.length;
while(j < len-1){
if(k == -1 || str.charAt(k) == str.charAt(j)){
k++;
j++;
next[j] = k;
}else{
k = next[k];
}
}
}
public static void main(String[] args){
String str = "abaabcaba";
int len = str.length();
int[] next = new int[len];
Kmp_GetNext obj = new Kmp_GetNext();
obj.getNext(str, next);
for(int i = 0; i < next.length; i++)
System.out.print(next[i]);
System.out.println();
}
}
有了next数组,Kmp算法代码就很好实现了。
KMP算法代码实现
C++
#include<stdio.h>
#include <stdlib.h>
#include<stack>
#include<string>
using namespace std;
void getNext(char p[], int next[])
{
int j = 0;
int k = -1;
int n = strlen(p);
next[j] = k;
while (j<n-1)
{
if (k == -1 || p[k] == p[j])
{
++j;
++k;
next[j] = k;
}
else
{
k = next[k];
}
}
}
int KMP(char s[], char p[], int n, int patt_len,int next[])
{
int ans = -1;
int i = 0;
int j = 0;
while (i<n)
{
if (j==-1||p[j] == s[i])//依次比较每个元素,如果相等匹配的上则同步往后比较。
//但是如果一直匹配不上会一直回溯回溯,可能回溯到首位这个时候j=next[j]=-1。
{
i++;
j++;
}
else //匹配不上时,模式串不需要回溯到首位进行匹配,只需要根据next数组回溯到p[j]再进行比较。
{
j = next[j];
}
if (j == patt_len)
{
ans = i - patt_len;
break;
}
}
return ans;
}
int main()
{
char s[] = "abaabcaba";
char p[] = "abca";
int n = strlen(p);
int *next = new int(n);
getNext(p, next);
int m = strlen(s);
int ans = KMP(s, p, m,n, next);
printf("%d ", ans);
}
JAVA
public class KMP{
public void getNext(String str, int[] next){
int k = -1;
int j = 0;
next[j] = k;
while(j < str.length()-1){
if(k == -1 || str.charAt(j) == str.charAt(k)){
j++;
k++;
next[j] = k;
}
else{
k = next[k];
}
}
}
public int kmp(String str, String patt){
int len = str.length();
int[] next = new int[len];
getNext(patt, next);
int i = 0;
int j = 0;
int ans_pos = -1;
while(i < len){
if(j == -1 || str.charAt(i) == patt.charAt(j)){
i++;
j++;
}else{
j = next[j];
}
if(j == patt.length()){
ans_pos = i - patt.length();
break;
}
}
return ans_pos;
}
public static void main(String[] args){
KMP obj = new KMP();
int res = obj.kmp("abdbcsa", "dbcs");
System.out.println(res);
}
}
KMP算法改进版本
文本串匹配到i,模式串匹配到j,此刻,若 t e x t [ i ] ≠ p a t t e r n [ j ] text[i]≠pattern[j] text[i]=pattern[j] ,即失配的情况:若 n e x t [ j ] = k next[j]=k next[j]=k ,说明模式串应该从j滑动到k位置;
若此时满足 p a t t e r n [ j ] = = p a t t e r n [ k ] pattern[j]==pattern[k] pattern[j]==pattern[k],因为 t e x t [ i ] ≠ p a t t e r n [ j ] text[i]≠pattern[j] text[i]=pattern[j],所以, t e x t [ i ] ≠ p a t t e r n [ k ] text[i] ≠pattern[k] text[i]=pattern[k], 即i和k没有匹配,应该继续滑动到 n e x t [ k ] next[k] next[k] 。 换句话说:在原始的next数组中,若 n e x t [ j ] = k next[j]=k next[j]=k 并且 p a t t e r n [ j ] = = p a t t e r n [ k ] pattern[j]==pattern[k] pattern[j]==pattern[k], n e x t [ j ] next[j] next[j] 可以直接等于 n e x t [ k ] next[k] next[k] 。
实现代码:
void getNext(char p[], int next[])
{
int j = 0;
int k = -1;
int n = strlen(p);
next[j] = k;
while (j<n-1)
{
if (k == -1 || p[k] == p[j])
{
++j;
++k;
if (p[j] == p[k])
next[j] = next[k];
else
next[j] = k;
}
else
{
k = next[k];
}
}
}
KMP的时间复杂度
最好情况:当模式串的首字符和其他字符都不相等时,模式串不存在相等的k前缀和k后缀,next数组全为-1;一旦匹配失效,模式串直接跳过文本串 s s s 已经比较的字符。比较次数为N。
最差情况:当模式串的首字符和其他字符全都相等时,模式串存在***最长的k前缀和k后缀***,next数组呈现递增样式:-1,0,1,2…
KMP应用:PowerString问题
给定一个长度为n的字符串S,如果存在一个字符串T,重复若干次T能够得到S,那么,S叫做周期串,T叫做S的一个周期。
如:字符串abababab是周期串,abab、ab都是它的周期,其中,ab是它的最小周期。
设计一个算法,计算S的最小周期。如果S不存在周期,返回空串。
使用next,线性时间解决问题
计算S的next数组;
- 记k=next[len],p=len-k;
- 若len%p==0,则p为最小周期长度,前p个字符就是最小周期。
说明:
- 使用的是经典KMP的next算法,非改进KMP的next算法;
- 要“多”计算到len,即next[len]。
证明:
实现代码:
C++
#include<stdio.h>
#include <stdlib.h>
#include<stack>
#include<string>
using namespace std;
int getNext(char p[], int next[])
{
int ans = -1;
int j = 0;
int k = -1;
int n = strlen(p);
next[j] = k;
while (j<n)
{
if (k == -1 || p[k] == p[j])
{
++j;
++k;
next[j] = k;
/*if (j == n)
break;*/
}
else
{
k = next[k];
}
}
int nlast = n - next[n];
if (n%nlast == 0)
{
ans = nlast;
}
return ans;
}
void Print(int * next)
{
int n = 13;
for (int i = 0; i < n; i++)
{
printf("%d ", next[i]);
}
printf("\n");
}
int main()
{
char p[] = "abcabcabcabc";
int n = strlen(p);
int *next = new int(n+1);
int ans=getNext(p,next);
Print(next);
printf("%d \n", ans);
}
JAVA
public class PowerString{
public void getNext(String str, int[] next){
int i = 0;
int k = -1;
next[0] = k;
while(i < str.length()){
if(k == -1 || str.charAt(i)==str.charAt(k)){
i++;
k++;
next[i] = k;
}else{
k = next[k];
}
}
}
public String poweString2(String str){
int[] next = new int[str.length()+1];
getNext(str, next);
int p = str.length() - next[str.length()];
if(str.length() % p == 0){
return str.substring(0, p);
}else{
return "-1";
}
}
public static void main(String[] args){
PowerString obj = new PowerString();
String str = "abcabab";
String res = obj.poweString2(str);
System.out.println(res);
}
}
Huffman
二叉树的结点
令有2个孩子、1个孩子和0个孩子的结点个数分别为n2、n1、n0,则有:
- 所有结点的出度为2n2+1n1+0*n0;
- 除了根结点,其他所有结点的入度都是1,从而所有结点的入度为(n2+n1+n0)-1;
- 总入度等于总出度,2n2+1n1+0*n0=n2+n1+n0-1,化简得n0-n2=1;
- 二叉树叶子节点数目比两个孩子的结点数目多1。
Huffman编码
Huffman编码是一种***无损压缩编码*** 方案。
思想:根据源字符出现的(估算)概率对字符编码,概率高的字符使用较短的编码,概率低的使用较长的编码,从而使得编码后的字符串长度期望最小。
Huffman编码是一种贪心算法:每次总选择两个最小概率的字符结点合并。
称字符出现的次数为***频数***,则概率约等于频数除以字符总长;因此,概率可以用频数代替。
代码实现:
#include<stdio.h>
#include <stdlib.h>
#include<stack>
#include<string>
#include<vector>
#include<iostream>
using namespace std;
typedef struct HuffmanNode
{
int nWeight;
int nParent=0, nLchild=0, nRchild=0; //用于保存节点在数组中的位置
}HuffmanNode;
void CalcFrequency(const char* str, int* pWeight)
{
while (*str)
{
pWeight[*str]++;
str++;
}
}
void CalcExistChar(int* pWeight, int N, vector<int>& pChar)
{
int j = 0;
for (int i = 0; i < N; i++)
{
if (pWeight[i] != 0)
{
pChar.push_back(i);
if (j != i)
{
pWeight[j] = pWeight[i];
}
j++;
}
}
}
void SelectNode(const HuffmanNode* pHuffmanTree, int n, int &s1, int &s2)
{
s1 = -1;
s2 = -1;
int nMin1 = -1;
int nMin2 = -1;
for (int i = 0; i < n; i++)
{
if ((pHuffmanTree[i].nParent == 0) && (pHuffmanTree[i].nWeight > 0))
{
if ((s1<0) || (nMin1>pHuffmanTree[i].nWeight))
{
s2 = s1;
nMin2 = nMin1;
s1 = i;
nMin1 = pHuffmanTree[s1].nWeight;
}
else if ((s2<0) || (nMin2>pHuffmanTree[i].nWeight))
{
s2 = i;
nMin2 = pHuffmanTree[s2].nWeight;
}
}
}
}
void reverse(vector<char> vec)
{
for (vector<char>::reverse_iterator it = vec.rbegin(); it != vec.rend(); ++it)
{
cout << *it;
}
cout << endl;
}
void HuffmanCoding(int pWeight[], int N, vector<vector<char>>& code)
{
if (N <= 0)
return;
int m = 2 * N - 1;//N个叶子结点的Huffman树共有2*N-1个结点
HuffmanNode* pHuffmanTree = new HuffmanNode[m];
int s1, s2;
int i;
//建立叶子结点
for (i = 0; i < N; i++)
{
pHuffmanTree[i].nWeight = pWeight[i];
}
//每次选择权值最小的两个结点,建树
for (i = N; i < m; i++)
{
SelectNode(pHuffmanTree, i, s1, s2);
pHuffmanTree[s1].nParent = pHuffmanTree[s2].nParent = i;
pHuffmanTree[i].nLchild = s1;
pHuffmanTree[i].nRchild = s2;
pHuffmanTree[i].nWeight = pHuffmanTree[s1].nWeight + pHuffmanTree[s2].nWeight;
}
//根据建好的Huffman树从叶子到根计算每个叶节点的编码
int node, nParent;
for (i = 0; i < N; i++)
{
vector<char>& cur = code[i];
node = i;
nParent = pHuffmanTree[node].nParent;
while (nParent!=0)
{
if (pHuffmanTree[nParent].nLchild == node)
{
cur.push_back('0');
}
else
{
cur.push_back('1');
}
node = nParent;
nParent = pHuffmanTree[node].nParent;
}
reverse(cur);
}
}
int main()
{
const int N = 256;
char str[] = "When I Was YoungThe rooms were so much colder then\
My father was a soldier then\
And times were very hard\
When I was young\
When I was young\
I smoked my first cigarette at ten\
And for girls I had a that yen\
And I had quite a ball\
When I was young\
When I was young it was more important\
Pain more painful and laughter much louder, yeah\
When I was young\
When I was young\
I met my first love at thirteen\
She was brown, and I was pretty green\
And I learned quite a lot\
When I was young\
When I was young\
When I was young it was more important\
Pain more painful and laughter much louder, yeah\
When I was young\
When I was young\
My faith was so much stronger then\
I believed in fellow men\
And I was so much older then\
When I was young\
When I was young\
When I was young...";
int pWeight[N] = {0};
CalcFrequency(str, pWeight);
pWeight['\t'] = 0;
vector<int> pChar;
CalcExistChar(pWeight, N, pChar);
int N2 = (int)pChar.size();
vector<vector<char>> code(N2);
HuffmanCoding(pWeight, N2, code);
}
##Manacher算法
回文子串的定义:
给定字符串str,若s同时满足以下条件:
- s是str的子串
- s是回文串
则,s是str的回文子串。
该算法的要求,是求str中最长的那个回文子串。
$manacher$ 算法(民间称马拉车算法)是用来找字符串中的最长回文子串的,先来说一下什么是回文串,像这样 $“abcba”$ 这样一个字符串找到一个中间位置,然后分别向他的左边和右边相等的距离位置的字符是相同的,那么这个字符串就称为回文串,$“abcba”$ 这个字符串的 $len$ 为5是奇数,我们可以找到一个中间字符,然后进行搜索也可以找出来(当然时间复杂度是比较高的),但是当我们遇到一个长度为偶数的字符串时该怎么找中间字符呢,像这样 $“abccba”$,下面我们引入$Manacher$ 算法,这是一个可以将长度为奇数或偶数的字符串一起考虑的神奇算法
$Manacher$ 算法可以将长度为奇数和偶数的回文串一起考虑:在原字符串的相邻字符串之间插入一个分隔符,字符串的首尾也要分别添加,注意分隔符必须是原字符串中没有出现过的
原字符串s | a | b | a | b | c |
转换后字符串str | # | a | # | b | # | a | # | b | # | c | # |
一、Len数组的简单介绍
$Manacher$ 算法中用到一个非常重要的辅助数组$Len$, $Len[i]$ 表示以 $str[i]$ 为中心的最长回文子串的最右端到 $str[i]$ 位置的长度(包括 $str[i]$ ),比如以 $str[i]$ 为中心的最长回文串是 $str[l,r]$ ,那么$Len[i]=r-i+1$
转换后的字符串str | # | a | # | b | # | a | # | b | # | c | # |
Len | 1 | 2 | 1 | 4 | 1 | 4 | 1 | 2 | 1 | 2 | 1 |
$Len[i]$ 数组有一个性质,$Len[i]-1$ 就等于该回文串(以 $str[i]$ 为中心)在原串 $s$ 中的长度(这里的长度表示整个回文的长度,从回文左边界到中心再到右边界)
证明:在转换后的字符串 $str$ 中,所有的回文串的长度都是奇数,那么对于以 $str[i]$ 为中心的最长回文串的长度为 $2*Len[i]-1$ ,长度为 $2*Len[i]-1$ 每相邻间隔内又有‘#’分隔符,故共有 $Len[i]$ 个分隔符,所以在原字符串中的回文长度就是 $Len[i]-1$,那么剩下的工作就是求 $Len$ 数组。
二、$Len$ 数组的计算
从左往右开始计算,假设 $0\leq j\leq i$ ,那么在计算$Len[i]$ 时, $Len[j]$ 已经计算过了,记 $mx$ 为之前计算过的最长回文串的右端点,我们记 $id$ 为取得这个最远右端点的中心位置(那么 $Len[id]=mx-id+1$),这是从前向后计算的过程,每次计算 $Len[i]$ 都是在之前的最大mx的基础上进行。
第一种情况:$i\leq mx$ .
找到 $i$ 相对于$id$ 的对称位置,设为 $j$,再次分为两种情况:
1、$Len[j]$ < $mx-i$
$mx$ 的对称点为 $2*id-mx$,$i$和$j$ 所包含的范围是$2*Len[j]-1$
那么说明以 $j$ 为中心的回文串一定在以 $id$ 为中心的回文串内部,且$i$和 $j$ 关于 $id$ 对称,由回文串的定义可知,一个回文串反过来仍是回文串,所以以 $j$ 为中心的回文串长度至少和以$i$ 为中心的回文串长度相等,即 $len[i]\geq len[j] $ 。因为$len[j]\prec mx-i$ 所以$i+len[j]\prec mx$,于是可以令 $len[i]=len[j]$。但是以$i$为对称轴的回文串可能实际上更长,因此我们试着以$i$ 为对称轴,继续往左右两边扩展,直到左右两边字符不同,或者到达边界。
2、$len[j]\geq mx-i $
由对称性说明以 $i$ 为中心的回文串可能延伸到 $mx$ 之外,而大于 $mx$ 的部分我们还没有进行匹配,所以要从 $mx+1$ 位置开始一个一个匹配直到失配,从而更新 $mx$ 和对应的 $id$ 以及 $Len[i]$
第二种情况,$i\geq mx $
如果 $i$ 比 $mx$ 还大,说明对于中点为i的回文串一点都没匹配,这个时候只能一个个匹配,匹配完成后更新 $mx$ 的位置和对应的 $id$ 及 $Len[i]$ .
代码实现:
#include<cstdio>
#include<cstring>
#include<iostream>
#include<vector>
using namespace std;
void getstr(char*s, char str[],int& len)
{
//数据预处理,中间两边均加上'#'
int k = 0;
str[k++] = '#';
for (int i = 0; i < len; i++)
{
str[k++] = s[i];
str[k++] = '#';
}
len = k;
}
int Manacher(char* s, int len, char* str)
{
int mx = 0, id=0;
int maxLen = 0;
vector<int> Len(len);
for (int i = 0; i < len; i++){
Len[i] = 0;
}
for (int i = 0; i<len; i++)
{
if (mx > i)
{
//其中2*id-i是i关于id对称的j,j<i,这是从前向后计算过程,len[j]已经计算出来了。根据对称性可以计算出Len[i]最长的,
//可以确定取得的长度。
Len[i] = Len[2 * id - i] < mx - i ? Len[2 * id - i] : mx - i;
}
else Len[i] = 1;
//如果i<mx,则Len[i]>=Len[j]以i为对称轴的回文串可能实际上更长,因此我们试着以i为中心,继续往左右两边扩展,
//直到左右两边字符不同,或者到达边界。
//如果i>=mx,则以i为中心尝试两边扩展,注意处理边界
//故不论i在不在mx内,都需要以i为中心尝试两边扩展,注意处理边界
while ((i - Len[i] >= 0) && (i + Len[i]<len) && (str[i - Len[i]] == str[i + Len[i]]))
Len[i]++;
//更新最右mx与其轴id
if (Len[i] + i > mx)
{
mx = Len[i] + i;
id = i;
}
//更新最长回文串的长度
maxLen = maxLen>Len[i] ? maxLen : Len[i];
}
return maxLen-1;
}
int main()
{
char *s = "12321kukgh13";
int len = strlen(s);
char * str = new char[2 * len + 1];
getstr(s, str, len);
int maxlen=Manacher(s,len,str);
printf("%d\n", maxlen);
return 0;
}