算法基础-- >字符串(LCS,KMP,Huffman,Manacher)

本篇博文将详细总结算法里面关于字符串部分知识,包括:

  • 字符串循环左移
  • 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 (XY)=(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=x1xiXi(1im)(XiXi)
Y j = ﹤ y 1 , ⋯ , y j ﹥ 即 Y 序 列 的 前 j 个 字 符 ( 1 ≤ j ≤ n ) ( 字 符 串 Y 的 j 前 缀 ) ; Yj=﹤y1,⋯,yj﹥ 即Y序列的前j个字符 (1≤j≤n) (字符串Y的j前缀); Yj=y1yjYj(1jn)(Yj)
L C S ( X , Y ) 为 字 符 串 X 和 Y 的 最 长 公 共 子 序 列 , 即 为 Z = ﹤ z 1 , ⋯ , z k ﹥ LCS(X , Y) 为字符串X和Y的最长公共子序列,即为Z=﹤z1,⋯,zk﹥ LCS(X,Y)XYZ=z1zk

注:不严格的表述。事实上,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(Xm1,Yn1)+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=CLCS(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=BLCS(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(Xm1,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,Yn1)

即,若 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(Xm1,Yn),LCS(Xm,Yn1)}

举例: 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=y2LCS(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=y5LCS(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[ij] 记录序列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 XiYjC[ij]=0

这里写图片描述

实例:

X = < A , B , C , B , D , A , B > X=< A,B,C,B,D,A,B > X=<ABCBDAB>
Y = < B , D , C , A , B , A > Y=< B,D,C,A,B,A > Y=<BDCABA>

我们利用上面得出的结论进行打表:

这里写图片描述

注:这里两个序列下标都是从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(xm1,Yn)=LCS(xm,Yn1),会导致多解:有多个最长公共子序 列,并且它们的长度相等。

这里写图片描述

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(mn)
KMP算法是一种线性时间复杂度的字符串匹配算法,它是对BF算法改进。
记:文本串长度为N,模式串长度为M

  • BF算法的时间复杂度 O ( M ∗ N ) O(M*N) O(MN),空间复杂度 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} Patternj1=p0p1p2...pj1 ,查找字符串 P a t t e r n j − 1 {Pattern}_{j-1} Patternj1 的最大相等 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...pk1=pjkpjk+1...pj2pj1

这里我们定义一个 n e x t next next 数组,计算 n e x t [ j ] next[j] next[j] j j j 为上次匹配失败的位置)时,考察的字符串是模式串的前 j − 1 j-1 j1 个字符,与 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...pk1=pjkpjk+1...pj2pj1

这里写图片描述

则,对于模式串的位置 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数组;

  1. 记k=next[len],p=len-k;
  2. 若len%p==0,则p为最小周期长度,前p个字符就是最小周期。

说明:

  1. 使用的是经典KMP的next算法,非改进KMP的next算法;
  2. 要“多”计算到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$ 算法可以将长度为奇数和偶数的回文串一起考虑:在原字符串的相邻字符串之间插入一个分隔符,字符串的首尾也要分别添加,注意分隔符必须是原字符串中没有出现过的

原字符串sababc
转换后字符串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#
Len12141412121

 

    $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;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值