算法基础-->字符串(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)。

算法分析

(XY)=YX
如: abcdef
X=abX=ba
Y=cdefY=fedc
(XY)=(bafedc)=cdefab
时间复杂度O(N),空间复杂度O(1)

代码实现

#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);
}

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开始数
Xi=x1xiXi(1im)(XiXi)
Yj=y1yjYj(1jn)(Yj)
LCS(X,Y)XYZ=z1zk

注:不严格的表述。事实上,X和Y的可能存在多个子串,长度相同并且最大,因此,LCS(X,Y)严格的说,是个字符串集合。即:Z∈ LCS(X , Y) 。

LCS解法的探索

结尾字符相等: xm=yn

xm=yn (最后一个字符相同),则: Xm Yn 的最长公共子序列 Zk 的最后一个字符必定为 xm yn

即:
zk=xm=yn
LCS(Xm,Yn)=LCS(Xm1,Yn1)+xm

举例: xm=yn

《算法基础-->字符串(LCS,KMP,Huffman,Manacher)》” /></p><p>对于上面的字符串X和Y: <br /> <span></span><span style= x3=y3=CLCS(BDC,ABC)=LCS(BD,AB)+C
x5=y4=BLCS(BDCAB,ABCB)=LCS(BDCA,ABC)+B

结尾字符不相等: xmyn

xmyn ,则:

  • 要么: LCS(Xm,Yn)=LCS(Xm1,Yn)
  • 要么: LCS(Xm,Yn)=LCS(Xm,Yn1)

即,若 xmyn ,则: LCS(Xm,Yn)=max{LCS(Xm1,Yn),LCS(Xm,Yn1)}

举例: xmyn

《算法基础-->字符串(LCS,KMP,Huffman,Manacher)》” /></p><p>对于字符串X和Y: <br /> <span></span><span style= x2y2LCS(BD,AB)=max{LCS(BD,A),LCS(B,AB)}

x4y5LCS(BDCA,ABCBD)=max{LCS(BDCA,ABCB),LCS(BDC,ABCBD)}

LCS分析总结

《算法基础-->字符串(LCS,KMP,Huffman,Manacher)》” /></p><h3><strong>算法中的数据结构:长度数组</strong></h3><p>使用二维数组 <span></span><span style= C[m,n]

C[ij] 记录序列Xi和Yj的最长公共子序列的长度。

当i=0或j=0时,空序列是 XiYjC[ij]=0

《算法基础-->字符串(LCS,KMP,Huffman,Manacher)》” /></p><h4><strong>实例:</strong></h4><p><span></span><span style= X=<ABCBDAB>
Y=<BDCABA>

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

《算法基础-->字符串(LCS,KMP,Huffman,Manacher)》” /></p><p><strong><em>注:这里两个序列下标都是从1开始计数。表cheese的shape为(X.size()+1,Y.size()+1),cheese[i][j]表示序列X的i前缀(i从1计,并且包括第i个字符)和序列Y的j前缀的最长公共子系列长度。</em></strong></p><h3><strong>代码实现</strong></h3><pre><code><span>#include<stdio.h></span>
<span>#include <stdlib.h></span>
<span>#include<stack></span>
<span>#include<string></span>
<span>#include<vector></span>
<span>#include<iostream></span>
<span>using</span> <span>namespace</span> <span>std</span>;


<span>void</span> reverse(<span>string</span> &str,<span>int</span> from,<span>int</span> to)
{
    <span>while</span> (from<to)
    {
        <span>char</span> t = str[from];
        str[from++] = str[to];
        str[to--] = t;
    }
}


<span>void</span> LCS(<span>char</span> *str1, <span>char</span> *str2, <span>string</span> &str)
{
    <span>int</span> len1 = <span>strlen</span>(str1);
    <span>int</span> len2 = <span>strlen</span>(str2);
    <span>char</span> * s1 = str1 - <span>1</span>;<span>//使数值下标从1开始,方便下面编程,即数组下标依次后移加一后移一位</span>
    <span>//这一步非常重要,从上面的打表可以看出,两序列下标都是从1开始计算,在编程中如果不从1开始会很麻烦。</span>
    <span>char</span> * s2 = str2 - <span>1</span>;
    <span><span>vector</span><<span><span>vector</span><<span>int</span>></span>></span> cheese(len1 + <span>1</span>, <span><span>vector</span><<span>int</span>></span>(len2 + <span>1</span>));
    <span>for</span> (<span>int</span> i = <span>0</span>; i < len2 + <span>1</span>; i++)
    {
        cheese[<span>0</span>][i] = <span>0</span>;
    }
    <span>for</span> (<span>int</span> i = <span>0</span>; i < len1 + <span>1</span>; i++)
    {
        cheese[i][<span>0</span>] = <span>0</span>;
    }

    <span>for</span> (<span>int</span> i = <span>1</span>; i < =len1; i++)
    {
        <span>for</span> (<span>int</span> j = <span>1</span>; j < =len2; j++)
        {
            <span>if</span> (s1[i] == s2[j])
                cheese[i][j] = cheese[i-<span>1</span>][j-<span>1</span>] + <span>1</span>;
            <span>else</span>
                cheese[i][j] = cheese[i][j-<span>1</span>] > cheese[i-<span>1</span>][j] ? cheese[i][j-<span>1</span>] : cheese[i-<span>1</span>][j];
        }
    }
    <span>printf</span>(<span>, 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; }

若只计算 LCS 的长度,则空间复杂度为 O(min(m,n)) 。在计算 c[i,j] 时,只用到数组c的第i行和第i-1行。因此,只要用2行的数组空间就可以计算出最长公共子序列的长度。

最大公共子序列的多解性:求所有的LCS

xmyn 时:若 LCS(xm1,Yn)=LCS(xm,Yn1) ,会导致多解:有多个最长公共子序 列,并且它们的长度相等。

《算法基础-->字符串(LCS,KMP,Huffman,Manacher)》” /></p><h3><strong>LCS的应用:最长递增子序列LIS</strong></h3><p>Longest Increasing Subsequence;给定一个长度为N的数组,找出一个最长的单调递增子序列。</p><p>原数组为A {5, 6, 7, 1, 2, 8},排序后:A’{1, 2, 5, 6, 7, 8}。 因为,原数组A的子序列顺序保持不变,而且排序后A’本身就是递增的,这样,就保证了两序列的最长公共子序列的递增特性。如此,若想求数组A的最长递增子序列,其实就是求数组A与它的排序数组A’的最长公共子序列。</p><p><strong>代码实现:</strong></p><pre><code><span>#include<stdio.h></span>
<span>#include <stdlib.h></span>
<span>#include<stack></span>
<span>#include<string></span>
<span>#include<vector></span>
<span>#include<iostream></span>
<span>using</span> <span>namespace</span> <span>std</span>;


<span>void</span> reverse(<span>string</span> &str,<span>int</span> from,<span>int</span> to)
{
    <span>while</span> (from<to)
    {
        <span>char</span> t = str[from];
        str[from++] = str[to];
        str[to--] = t;
    }
}


<span>void</span> LCS(<span>char</span> *str1, <span>char</span> *str2, <span>string</span> &str)
{
    <span>int</span> len1 = <span>strlen</span>(str1);
    <span>int</span> len2 = <span>strlen</span>(str2);
    <span>char</span> * s1 = str1 - <span>1</span>;<span>//使数值下标从1开始,方便下面编程,即数组下标依次后移加一后移一位</span>
    <span>char</span> * s2 = str2 - <span>1</span>;
    <span><span>vector</span><<span><span>vector</span><<span>int</span>></span>></span> cheese(len1 + <span>1</span>, <span><span>vector</span><<span>int</span>></span>(len2 + <span>1</span>));
    <span>for</span> (<span>int</span> i = <span>0</span>; i < len2 + <span>1</span>; i++)
    {
        cheese[<span>0</span>][i] = <span>0</span>;
    }
    <span>for</span> (<span>int</span> i = <span>0</span>; i < len1 + <span>1</span>; i++)
    {
        cheese[i][<span>0</span>] = <span>0</span>;
    }

    <span>for</span> (<span>int</span> i = <span>1</span>; i <= len1; i++)
    {
        <span>for</span> (<span>int</span> j = <span>1</span>; j <=len2; j++)
        {
            <span>if</span> (s1[i] == s2[j])
            {
                cheese[i][j] = cheese[i - <span>1</span>][j - <span>1</span>] + <span>1</span>;
            }
            <span>else</span>
                cheese[i][j] = cheese[i][j-<span>1</span>] > cheese[i-<span>1</span>][j] ? cheese[i][j-<span>1</span>] : cheese[i-<span>1</span>][j];
        }
    }
    <span>printf</span>(<span>, 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(mn)
KMP算法是一种线性时间复杂度的字符串匹配算法,它是对BF算法改进。
记:文本串长度为N,模式串长度为M

  • BF算法的时间复杂度 O(MN) ,空间复杂度 O(1)
  • KMP算法的时间复杂度 O(M+N) ,空间复杂度 O(M)

BF算法思想

《算法基础-->字符串(LCS,KMP,Huffman,Manacher)》” /></p><p>由上图,较长的为字符串 <span></span><span style= s ,下面较短的为模式串 p ,当前模式串 p s 的第 i 个位置开始匹配, s p 同步的往后比较每个元素是否相同,如果 p 能成功匹配完毕则 i 即为所求结果。若当匹配到某一个位置时,发现这个位置的元素不同,当前匹配不成功, i 后移一位, j 回溯到 p 首位重新尝试匹配。

BF算法代码实现

#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;
}

看上面整个BF的匹配过程,每次匹配失败后模式串 p 都是沿着 s 后移一位一位的匹配,其匹配速度太慢,而每次匹配失败后,模式串 p 都得回溯到首位重新匹配,时间复杂度较高。

KMP算法思想

《算法基础-->字符串(LCS,KMP,Huffman,Manacher)》” /></p><p>我们以上图为例,长一些的为文本串 <span></span><span style= s ,短一些的为模式串 p ,当模式串 p 匹配到绿色那一小块时,对应的文本串 s 黄色那一小块,假设这一小块并不匹配,那么按照上面的暴力解法,模式串 p 整体沿着文本串 s 后移一位,p再回溯到首位进行重新匹配。那么有没有更好更快的办法呢?使得当前匹配失败后 p 后移更多位数,同时不用每次失败后回溯到首位重新匹配?这样其时间复杂度就会变为线性的。

《算法基础-->字符串(LCS,KMP,Huffman,Manacher)》” /> <br /> <img layer-src= p 在位置 d 处的内容与对应的文本串 s 的位置 d 内容不一致 ,则匹配失败,不过可以肯定的是模式串 p 在 位置 d 之前的内容和文本串 s 在位置 d 之前的内容完全匹配。这里我们不将模式串 p 整体只是后移一位而是后移若干位,然后比较模式串 p 在 位置 c 处内容 与文本串 s 在位置 d 内容是否一致。由上图可以看出,如果这样移动合理的话,那么模式串 p A 处内容必须和模式串在 B (这里需要注意 B 的后面一块必须是之前不匹配的位置。)处内容一致,也可以这样说,如果我们知道模式串 p A 处内容和在 B 处内容一致,那么我们就敢于将模式串直接拉到文本串 s B 位置处,然后再比较模式串 p c 位置处内容与文本串 s d 位置处内容是否一致。这样一来,模式串每次就不仅仅后移一位了,每次失败后模式串不用都回溯到首位重新匹配。并且上次文本串 s 比较到第 i 个位置,下次比较还是从第 i 位置开始比较。

那么问题来了,上面所讲的模式串 p A 处和在 B 处内容要一致,并且 B 处后面的位置即为上次匹配失败的位置。那么在模式串 p 如何找到这样的 A B 呢?这是KMP算法要解决的核心问题。

对于模式串的位置 j (注意这里是 j ,上一次匹配失败的位置),考察 Patternj1=p0p1p2...pj1 ,查找字符串 Patternj1 的最大相等 k 前缀(就是上面说的 A 处内容,长度为 k )和 k 后缀(就是上面说的 B 内容)。

即:查找满足条件的最大的 k ,使得 p0p1p2...pk1=pjkpjk+1...pj2pj1

这里我们定义一个 next 数组,计算 next[j] j 为上次匹配失败的位置)时,考察的字符串是模式串的前 j1 个字符,与 pattern[j] 无关。

求模式串的next

《算法基础-->字符串(LCS,KMP,Huffman,Manacher)》” /></p><p>如:j=5时,考察字符串 <span></span><span style= abaab 的最大相等 k 前缀和 k 后缀。

《算法基础-->字符串(LCS,KMP,Huffman,Manacher)》” /></p><p>故k=2。</p><h4><strong>next的递推关系</strong></h4><p>对于模式串的位置j,有 <span></span><span style= next[j]=k ,即: p0p1p2...pk1=pjkpjk+1...pj2pj1

《算法基础-->字符串(LCS,KMP,Huffman,Manacher)》” /></p><p>则,对于模式串的位置 <span></span><span style= j+1 ,考察 pj

  • p[k]==p[j]
    next[j+1]=next[j]+1

  • p[k]p[j] (这里需要注意 j,k 都是从0开始计,那么 p[h] 表示模式串从开始计长度为 h 后面一个位置内容)
    h=next[k] h 表示 pk 的最大前缀后缀长度 ;如果 p[h]==p[j] ,则 next[j+1]=h+1 ,否则重复此过程。

《算法基础-->字符串(LCS,KMP,Huffman,Manacher)》” /></p><p><strong>next数组代码实现:</strong></p><pre><code>void getNext(char* p, int <span>next</span>[])
{
    int j = <span>0</span>;
    int k = -<span>1</span>;
    int n = strlen(p);
    <span>next</span>[j] = k;
    <span>while</span> (j<n-<span>1</span>)//<span>next</span>数组长度即为模式串长度,下面有个++j。
    {
        //由上面的算法分析,知道<span>next</span>每次第j+<span>1</span>个k的求解都是在上一次第j个k的基础上得出。所以下面的循环是依次求<span>next</span>[<span>1</span>],<span>next</span>[<span>2</span>]<span>...</span>。
        //只有在p[k] == p[j]情况下,才能计算出<span>next</span>[j+<span>1</span>]
        <span>if</span> (k == -<span>1</span> || p[k] == p[j])//k=-<span>1</span>有两种情况:首次比较;一直匹配不成功需要不断的回溯,
        //可能会回溯到模式串首位,这个时候k=<span>next</span>[k]=-<span>1</span>
        {
            ++j;
            ++k;//没加<span>1</span>之前的k为<span>next</span>[j]
            <span>next</span>[j] = k;
        }
        <span>else</span> //p[j]与p[k]失配,这个时候不需要回溯到首位,而是p[<span>next</span>[k]]。
        //则连续递归计算前缀p[<span>next</span>[k]],这里始终是在求<span>next</span>[j]对应的k。注意在<span>next</span>中k从<span>0</span>开始,
        //而最长前缀后缀长度k是从<span>1</span>开始,故p[k]表示是在模式串长度k后的位置的内容。
        {
            k = <span>next</span>[k];
        }
    }
}
</code></pre><p>有了next数组,Kmp算法代码就很好实现了。</p><h3><strong>KMP算法代码实现</strong></h3><pre><code><span>#include<stdio.h></span>
<span>#include <stdlib.h></span>
<span>#include<stack></span>
<span>#include<string></span>
<span>using</span> <span>namespace</span> <span>std</span>;


<span>void</span> getNext(<span>char</span> p[], <span>int</span> next[])
{
    <span>int</span> j = <span>0</span>;
    <span>int</span> k = -<span>1</span>;
    <span>int</span> n = <span>strlen</span>(p);
    next[j] = k;
    <span>while</span> (j<n-<span>1</span>)
    {
        <span>if</span> (k == -<span>1</span> || p[k] == p[j])
        {
            ++j;
            ++k;
            next[j] = k;
        }
        <span>else</span>
        {
            k = next[k];
        }
    }
}


<span>int</span> KMP(<span>char</span> s[], <span>char</span> p[], <span>int</span> n, <span>int</span> patt_len,<span>int</span> next[])
{
    <span>int</span> ans = -<span>1</span>;
    <span>int</span> i = <span>0</span>;
    <span>int</span> j = <span>0</span>;
    <span>while</span> (i<n)
    {
        <span>if</span> (j==-<span>1</span>||p[j] == s[i])<span>//依次比较每个元素,如果相等匹配的上则同步往后比较。</span>
        <span>//但是如果一直匹配不上会一直回溯回溯,可能回溯到首位这个时候j=next[j]=-1。</span>
        {
            i++;
            j++;
        }
        <span>else</span> <span>//匹配不上时,模式串不需要回溯到首位进行匹配,只需要根据next数组回溯到p[j]再进行比较。</span>
        {
            j = next[j];
        }
        <span>if</span> (j == patt_len)
        {
            ans = i - patt_len;
            <span>break</span>;  
        }
    }
    <span>return</span> ans;
}

<span>int</span> main()
{
    <span>char</span> s[] = <span>; 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); }

KMP算法改进版本

文本串匹配到i,模式串匹配到j,此刻,若 text[i]pattern[j] ,即失配的情况:若 next[j]=k ,说明模式串应该从j滑动到k位置;

若此时满足 pattern[j]==pattern[k] ,因为 text[i]pattern[j] ,所以, text[i]pattern[k] , 即i和k没有匹配,应该继续滑动到 next[k] 。 换句话说:在原始的next数组中,若 next[j]=k 并且 pattern[j]==pattern[k] next[j] 可以直接等于 next[k]

《算法基础-->字符串(LCS,KMP,Huffman,Manacher)》” /></p><p><strong>实现代码:</strong></p><pre><code>void getNext(char p[], <span>int</span> <span>next</span>[])
{
    <span>int</span> j = <span>0</span>;
    <span>int</span> k = -<span>1</span>;
    <span>int</span> n = strlen(p);
    <span>next</span>[j] = k;
    <span>while</span> (j<n-<span>1</span>)
    {
        <span>if</span> (k == -<span>1</span> || p[k] == p[j])
        {
            ++j;
            ++k;
            <span>if</span> (p[j] == p[k])
                <span>next</span>[j] = <span>next</span>[k];
            <span>else</span>
                <span>next</span>[j] = k;

        }
        <span>else</span>
        {
            k = <span>next</span>[k];
        }
    }
}</code></pre><h3><strong>KMP的时间复杂度</strong></h3><p>最好情况:当模式串的首字符和其他字符都不相等时,模式串不存在相等的k前缀和k后缀,next数组全为-1;<strong><em>一旦匹配失效,模式串直接跳过文本串 <span></span><span style= 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]。

证明:

《算法基础-->字符串(LCS,KMP,Huffman,Manacher)》” /></p><p><img layer-src=, 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); }

Huffman

二叉树的结点

令有2个孩子、1个孩子和0个孩子的结点个数分别为n2、n1、n0,则有:

  • 所有结点的出度为2*n2+1*n1+0*n0;
  • 除了根结点,其他所有结点的入度都是1,从而所有结点的入度为(n2+n1+n0)-1;
  • 总入度等于总出度,2*n2+1*n1+0*n0=n2+n1+n0-1,化简得n0-n2=1;
  • 二叉树叶子节点数目比两个孩子的结点数目多1。

Huffman编码

Huffman编码是一种无损压缩编码 方案。

思想:根据源字符出现的(估算)概率对字符编码,概率高的字符使用较短的编码,概率低的使用较长的编码,从而使得编码后的字符串长度期望最小。

Huffman编码是一种贪心算法:每次总选择两个最小概率的字符结点合并。

称字符出现的次数为频数,则概率约等于频数除以字符总长;因此,概率可以用频数代替。

《算法基础-->字符串(LCS,KMP,Huffman,Manacher)》” /></p><p><strong>代码实现:</strong></p><pre><code><span>#include<stdio.h></span>
<span>#include <stdlib.h></span>
<span>#include<stack></span>
<span>#include<string></span>
<span>#include<vector></span>
<span>#include<iostream></span>
<span>using</span> <span>namespace</span> <span>std</span>;

<span>typedef</span> <span>struct</span> HuffmanNode
{
     <span>int</span> nWeight;
     <span>int</span> nParent=<span>0</span>, nLchild=<span>0</span>, nRchild=<span>0</span>;   <span>//用于保存节点在数组中的位置</span>
}HuffmanNode;


<span>void</span> CalcFrequency(<span>const</span> <span>char</span>* str, <span>int</span>* pWeight)
{
    <span>while</span> (*str)
    {
        pWeight[*str]++;
        str++;
    }
}

<span>void</span> CalcExistChar(<span>int</span>* pWeight, <span>int</span> N, <span><span>vector</span><<span>int</span>></span>& pChar)
{
    <span>int</span> j = <span>0</span>;
    <span>for</span> (<span>int</span> i = <span>0</span>; i < N; i++)
    {
        <span>if</span> (pWeight[i] != <span>0</span>)
        {
            pChar.push_back(i);
            <span>if</span> (j != i)
            {
                pWeight[j] = pWeight[i];
            }
            j++;
        }
    }
}

<span>void</span> SelectNode(<span>const</span> HuffmanNode* pHuffmanTree, <span>int</span> n, <span>int</span> &s1, <span>int</span> &s2)
{
    s1 = -<span>1</span>;
    s2 = -<span>1</span>;
    <span>int</span> nMin1 = -<span>1</span>;
    <span>int</span> nMin2 = -<span>1</span>;
    <span>for</span> (<span>int</span> i = <span>0</span>; i < n; i++)
    {
        <span>if</span> ((pHuffmanTree[i].nParent == <span>0</span>) && (pHuffmanTree[i].nWeight > <span>0</span>))
        {
            <span>if</span> ((s1<<span>0</span>) || (nMin1>pHuffmanTree[i].nWeight))
            {
                s2 = s1;
                nMin2 = nMin1;
                s1 = i;
                nMin1 = pHuffmanTree[s1].nWeight;
            }
            <span>else</span> <span>if</span> ((s2<<span>0</span>) || (nMin2>pHuffmanTree[i].nWeight))
            {
                s2 = i;
                nMin2 = pHuffmanTree[s2].nWeight;
            }
        }
    }
}

<span>void</span> reverse(<span><span>vector</span><<span>char</span>></span> vec)
{
    <span>for</span> (<span><span>vector</span><<span>char</span>></span>::reverse_iterator it = vec.rbegin(); it != vec.rend(); ++it)
    {
        <span>cout</span> << *it;
    }
    <span>cout</span> << endl;
}

<span>void</span> HuffmanCoding(<span>int</span> pWeight[], <span>int</span> N, <span><span>vector</span><<span><span>vector</span><<span>char</span>></span>></span>& code)
{
    <span>if</span> (N <= <span>0</span>)
        <span>return</span>;
    <span>int</span> m = <span>2</span> * N - <span>1</span>;<span>//N个叶子结点的Huffman树共有2*N-1个结点</span>
    HuffmanNode* pHuffmanTree = <span>new</span> HuffmanNode[m];
    <span>int</span> s1, s2;
    <span>int</span> i;
    <span>//建立叶子结点</span>
    <span>for</span> (i = <span>0</span>; i < N; i++)
    {
        pHuffmanTree[i].nWeight = pWeight[i];
    }
    <span>//每次选择权值最小的两个结点,建树</span>
    <span>for</span> (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;
    }
    <span>//根据建好的Huffman树从叶子到根计算每个叶节点的编码</span>
    <span>int</span> node, nParent;
    <span>for</span> (i = <span>0</span>; i < N; i++)
    {
        <span><span>vector</span><<span>char</span>></span>& cur = code[i];
        node = i;
        nParent = pHuffmanTree[node].nParent;
        <span>while</span> (nParent!=<span>0</span>)
        {
            <span>if</span> (pHuffmanTree[nParent].nLchild == node)
            {
                cur.push_back(<span>'0'</span>);
            }
            <span>else</span>
            {
                cur.push_back(<span>'1'</span>);
            }
            node = nParent;
            nParent = pHuffmanTree[node].nParent;
        }
        reverse(cur);
    }
}

<span>int</span> main()
{
    <span>const</span> <span>int</span> N = <span>256</span>;
    <span>char</span> str[] = <span>; 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]=ri+1

转换后的字符串str#a#b#a#b#c#
Len12141412121

 

    Len[i] 数组有一个性质, Len[i]1 就等于该回文串(以 str[i] 为中心)在原串 s 中的长度(这里的长度表示整个回文的长度,从回文左边界到中心再到右边界)

    证明:在转换后的字符串 str 中,所有的回文串的长度都是奇数,那么对于以 str[i] 为中心的最长回文串的长度为 2Len[i]1 长度为 2Len[i]1 每相邻间隔内又有‘#’分隔符,故共有 Len[i] 个分隔符,所以在原字符串中的回文长度就是 Len[i]1 ,那么剩下的工作就是求 Len 数组。

二、 Len 数组的计算

    从左往右开始计算,假设 0ji ,那么在计算 Len[i] 时, Len[j] 已经计算过了, mx 为之前计算过的最长回文串的右端点,我们记 id 为取得这个最远右端点的中心位置(那么 Len[id]=mxid+1 ),这是从前向后计算的过程,每次计算 Len[i] 都是在之前的最大mx的基础上进行。

第一种情况: imx .

    找到 i 相对于 id 的对称位置,设为 j ,再次分为两种情况:

        1、 Len[j] < mxi

     《算法基础-->字符串(LCS,KMP,Huffman,Manacher)》” />   </p><p>       <span></span><span style= mx 的对称点为 2idmx , i j 所包含的范围是 2Len[j]1

        那么说明以 j 为中心的回文串一定在以 id 为中心的回文串内部,且 i j 关于 id 对称,由回文串的定义可知,一个回文串反过来仍是回文串,所以以 j 为中心的回文串长度至少和以 i 为中心的回文串长度相等,即 len[i]len[j] 。因为 len[j]mxi 所以 i+len[j]mx ,于是可以令 len[i]=len[j] 。但是以 i 为对称轴的回文串可能实际上更长,因此我们试着以 i 为对称轴,继续往左右两边扩展,直到左右两边字符不同,或者到达边界。

        2、 len[j]mxi

《算法基础-->字符串(LCS,KMP,Huffman,Manacher)》” /></p><p>        由对称性说明以 <span></span><span style= i 为中心的回文串可能延伸到 mx 之外,而大于 mx 的部分我们还没有进行匹配,所以要从 mx+1 位置开始一个一个匹配直到失配,从而更新 mx 和对应的 id 以及 Len[i]

第二种情况, imx

        如果 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;
}
    原文作者:KMP算法
    原文地址: https://blog.csdn.net/Mr_tyting/article/details/77488696
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞