LeetCode 之 KMP算法 ——另一个树的子树( •̀ ω •́ )y

vector的size()返回值是unsigned int !!!
vector的size()返回值是unsigned int !!!
vector的size()返回值是unsigned int !!!

重要的事情说三遍!!!(改了一下午的bug)

这次我是根据 LeetCode 中 572. 另一个树的子树一题来学习KMP算法的,所以KMP 算法只是其中一部分,请按需食用。

正文开始φ(* ̄0 ̄)

这只是一个目录

    • 题目描述
    • 初次思路
    • KMP算法
      • 核心思想
      • next数组
    • 查找子树的KMP解法
    • 完结

题目描述

LeetCode 之 KMP算法 ——另一个树的子树( •̀ ω •́ )y_第1张图片

初次思路

在我看到这一题的时候,出于小白的自我修养,肯定是暴力破解,什么?KMP,Rabin-Karp,哈希树?那是啥,听都没听过。
所以我开始就想出来根据DFS深度优先搜索,暴力匹配,当一个节点相同时,便遍历这个节点下的所有子节点与目标树对比,直到全部遍历完毕,或者出现不同数字,中间两个简单的递归就可以搞定。然后我就开始写了,然后… …就出现下面这个了,这个我就不说了,我知道各位大佬肯定都会。

class Solution {
public:
    bool check(TreeNode* m, TreeNode* n){
        if(!m && !n)
            return true;
        else if(!m || !n)
            return false;
        else if(m->val != n->val)
            return false;
        
        return check(m->left,n->left) && check(m->right,n->right);
        
    }

    bool isSubtree(TreeNode* s, TreeNode* t) {
        bool tmp = false;
        if(s != nullptr && t != nullptr){
            if(s->val == t->val)
                tmp = check(s,t);
            if(!tmp)
                tmp = isSubtree(s->left, t);
            if(!tmp)
                tmp = isSubtree(s->right, t);
        }
        return tmp;
    }
};

当我提交之后,果然不出我所料,官方答案第一个就是我这样写的,是最差的答案ψ(`∇´)ψ。然后观摩了大佬们的解法,我发现了传说中的KMP算法。
然后开始面向必应编程,经过了几个小时的折磨之后,我终于找到一个能够看懂的KMP算法的知乎大佬的回答了(海纳的回答),说实话,这是我唯一能够很轻松看懂的KMP算法的原理,好了废话不多说,开始算法。

KMP算法

核心思想

首先,我们要知道什么是KMP,KMP算法是一种改进的字符串匹配算法,简单来说就是利用比较失败后的信息来减少暴力匹配中的信息的浪费。首先,根据原始的两字符串匹配的方法,我们就是一个一个的比较,当出现两字符串不同的地方就将头指针下移,再从头一个一个比,在这个过程中,你是不是总感觉有的地方我已经比过了,但是又不得不再比一遍。所以,KMP就是用来处理这个问题的。KMP算法的动机了解这么多就完全够了。

KMP算法的核心,是一个被称为部分匹配表(Partial Match Table)的数组,后面简称PMT,也就是我们常说的next数组。当我在看其他的文章时总是花里胡哨的给我搞一大堆式子,然后又扯一些啥有限状态自动机的,看的我一脸懵,所以我这篇文章,你什么都不要问,记住有三个数组就行:被搜索字符串s,搜索字符串t,辅助数组next。还有两个概念,前缀和后缀。

vector <char> s = "ABABABABCA" //被搜索字符串
vector <char> t = "ABABABCA"	//搜索字符串

在正式开始之前,我们先来了解一下什么叫前缀和后缀。
前缀(后缀),就是一串字符串从前面(后面)开始数的连续字符串,但是不包括这个字符串本身。比如:Hello的前缀有H,He,Hel,Hell,后缀有o,lo,llo,ello,这些字符串的集合就是前缀(后缀)的集合。

明白这个定义后,就可以说一下PMT中的值的意义了。PMT中的值是字符串的前缀集合与后缀集合的交集中最长元素的长度。比如:abab的前缀有a,ab,aba,后缀有b,ab,bab 那么abab在PMT中的长度就是前后缀集合的交集中最长元素ab的长度 2。

准备工作完毕,正式开始!ヾ(•ω•)o
我们现在的目的就是从字符串s "ABABABABCA" 中找到字符串t "ABABABCA"。但是这次我们不按照原始的方法,找错一个就从头开始找,我们用的是高大上的KMP算法,找错一个不从头开始找!从现在开始我们先假设我们已经有了一个next数组,至于next数组怎么来的后面再说。
现在我们开始比较(黑底表示指针位置,黄底为已匹配字符)
在这里插入图片描述在这里插入图片描述
第一次比较到 s[4] 和 t[4] 时出现不同,按照原始方法我们应该
在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述
… …

原始算法中我们一旦找错,就直接回到开头重新来,KMP中当出错时,上面的s数组的指针不动,下面的t数组的指针向前回溯,而且这个回溯的位数是由PMT决定的也就是next数组。以下为KMP匹配的过程。
在这里插入图片描述第一次匹配
在这里插入图片描述第二次匹配 在这里插入图片描述

在这里插入图片描述在这里插入图片描述 在这里插入图片描述
我们可以看到s数组的指针是一直向前进的,t的指针向前回溯了。当s数组第i位与t数组第j位的字符不同时,t数组的指针会根据第j位的next数组值来进行回溯。而next数组中存放的是第0位到第i-1位字符串的最大前后缀交集长度(PMT)。

在下图中,已匹配的字符串为“ABAB”,这时我们发现,ABAB的最大相同前后缀为AB,长度为2,也就是说,PMT为2,由于两个红底的相同,所以我们不需要回去重新匹配,只需要将红底字符串对齐,即向前回溯到第三个元素(由next数组决定),然后继续向后匹配即可。这样就做到将之前匹配过的字符串不用重新匹配,可以继续向下进行。直到匹配到最后一位,跳出循环的条件为s或t的指针超出数组大小,跳出后如果是t指针超出,则匹配成功!
在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述
最后附上代码

	int t = 0,s = 0; 
    while(s < int(sOrder.size()) && t < int(tOrder.size()))
        {
        	//next[0]为了方便编程设为-1,无意义
            if(t == -1 || tOrder[t] == sOrder[s]){//匹配成功
                s++; t++;
            }else{
                t = next[t];//t数组回溯
            }
        }

next数组

好了,到这里,KMP的核心思想基本就结束了,就这么多,现在我们所缺的就是一个记录在每一位匹配失败时,应该回溯位数的next数组了。下面我们开始求这个数组。求next数组的过程,其实也就是字符串匹配的过程,所以说,开始套娃Ψ( ̄∀ ̄)Ψ。其实,根据t字符串求next数组也就是字符串匹配的过程,即以模式字符串为主字符串,以模式字符串的前缀为目标字符串。一旦字符串匹配成功,那么当前的next值就是匹配成功的字符串的长度。

下面开始求next,首先让第一位等于-1,即next[0] = -1,无意义,仅编程方便,令next[1] = 0。(这里要特别注意:由于next数组是根据第i位,找0到i-1位的PMT,所以,next数组的值会滞后一位)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
本质上就是拿上一列数组指针前字符串的后缀和下一列数组的前缀进行比较,得出相同前后缀PMT,即为next数组的值。

最后上代码(~ ̄▽ ̄)~

		int i = 0,j = -1;
		vector <int> next(tOrder.size(), -1);
        while(i < tOrder.size()-1){
            if(j == -1 || tOrder[i] == tOrder[j]){
                ++i;
                ++j;
                next[i] = j;
            }else{
            	//这里需要注意,当匹配失败时,不能直接将j = 0,
            	//道理相同,需要再次套娃,因为你需要回溯到你已经匹配
            	//的字符串中的最大前后缀相同的地方,再次使用KMP。
            	//由于例子选择问题,这里不能很好体现。
                j = next[j];
            }
        }

d=====( ̄▽ ̄*)b,KMP算法结束,希望你看到这已经理解了这个算法,如有不懂或者错误,欢迎留言。
参考资料:(海纳的回答)

查找子树的KMP解法

首先,我们已经了解了KMP算法是啥,所以,接下来所做的事情就很简单了,将两棵树按先序遍历成字符串,然后查找就完事了,不过,我们要在这个过程中做点“手脚”,需要在树叶下面加上两个标识符,识别树枝的终点。
下面,上代码!

class Solution {
public:

    vector<int> sOrder,tOrder;
    int maxElement,lNULL,rNULL;

    void getmaxElement(TreeNode* o){
        if(!o) return;
        maxElement = max(maxElement,o->val);
        getmaxElement(o->left);
        getmaxElement(o->right);
    }

    void zip(TreeNode *o,vector<int>& tar){
        if(!o) return;
        tar.push_back(o->val);
        if(!o->left)
            tar.push_back(lNULL);
        else
            zip(o->left,tar);
        if(!o->right)
            tar.push_back(rNULL);
        else
            zip(o->right,tar);
    }

    bool kmp(){
        vector <int> next(tOrder.size(), -1);
        int i = 0,j = -1;
        while(i < tOrder.size()-1){
            if(j == -1 || tOrder[i] == tOrder[j]){
                ++i;
                ++j;
                next[i] = j;
            }else{
                j = next[j];
            }
        }

        int s = 0,t = 0;

        while(s < int(sOrder.size()) && t < int(tOrder.size()))
        {
            if(t == -1 || tOrder[t] == sOrder[s]){
                s++; t++;
            }else{
                t = next[t];
            }
        }
        if(t == tOrder.size())
            return true;
        else
            return false;
    }

    bool isSubtree(TreeNode* s, TreeNode* t) {
        maxElement = INT_MIN;
        getmaxElement(s);
        getmaxElement(t);

        lNULL = maxElement + 1;
        rNULL = maxElement + 2;

        zip(s,sOrder);
        zip(t,tOrder);

        return kmp();
    }
};

完结

完结撒花!( •̀ ω •́ )✧

如有错误请各位大佬轻锤,我会一一改正!

你可能感兴趣的:(算法)