diff --git a/Code/CPP-Code/head/sequence_string.h b/Code/CPP-Code/head/sequence_string.h index 7e52be2..56e5cf7 100644 --- a/Code/CPP-Code/head/sequence_string.h +++ b/Code/CPP-Code/head/sequence_string.h @@ -1,10 +1,10 @@ #include "head.h" // 顺序串 -class SequenceString{ +class SequenceString { private: // 数据 - char* _data{}; + char *_data{}; // 长度 int _length{}; // 最大容量 @@ -20,7 +20,7 @@ public: bool SetData(int index, char character); // 获取数据 - char* GetData(); + char *GetData(); char GetData(int index); @@ -45,6 +45,12 @@ public: SequenceString(); explicit SequenceString(int max_size); + + // 暴力模式匹配 + int LocateSimple(SequenceString string); + + // 销毁 + bool Destroy(); }; bool SequenceString::SetData() { @@ -57,7 +63,7 @@ bool SequenceString::SetData(int max_size) { return true; } -bool SequenceString::SetData(char * character) { +bool SequenceString::SetData(char *character) { this->_data = character; return true; } @@ -115,4 +121,32 @@ SequenceString::SequenceString(int max_size) { this->SetLength(0); } +int SequenceString::LocateSimple(SequenceString string) { + int i = 0, j = 0; + while (i < this->GetLength() && j < string.GetLength()) { + // 匹配就继续后移 + if (this->GetData(i) == string.GetData(j)) { + i++; + j++; + } + // 不匹配就退回 + else { + i = i - j + 1; + j = 0; + } + } + if (j >= string.GetLength()) { + return i - string.GetLength(); + } + return -1; +} + +bool SequenceString::Destroy() { + delete (this->GetData()); + this->SetLength(0); + this->SetMaxSize(0); + this->SetData(nullptr); + return true; +} + diff --git a/Code/Code/head/link_stack.h b/Code/Code/head/link_stack.h index 65b6ec7..44f40d7 100644 --- a/Code/Code/head/link_stack.h +++ b/Code/Code/head/link_stack.h @@ -1,5 +1,6 @@ #include "head.h" +// 链栈 typedef struct LinkStackNode{ // 数据 element_type data; diff --git a/Code/Code/head/link_tree.h b/Code/Code/head/link_tree.h new file mode 100644 index 0000000..ea1ff7e --- /dev/null +++ b/Code/Code/head/link_tree.h @@ -0,0 +1,7 @@ +#include "head.h" + +// 二叉树结点 +typedef struct BinaryTreeNode { + element_type data; + BinaryTreeNode *left_child, *right_child; +} BinaryTreeNode, *BinaryTree; \ No newline at end of file diff --git a/Code/Code/head/sequence_string.h b/Code/Code/head/sequence_string.h index 90a492e..7e08499 100644 --- a/Code/Code/head/sequence_string.h +++ b/Code/Code/head/sequence_string.h @@ -5,9 +5,9 @@ typedef struct { // 数据 char *data; // 长度 - unsigned int length; + int length; // 最大容量 - unsigned int max_size; + int max_size; } SequenceString; bool InitSequenceString(SequenceString &string) { @@ -38,4 +38,110 @@ SequenceString InitSequenceString(int max_size) { string->max_size = max_size; string->length = 0; return (SequenceString &) string; +} + +// 简单字符串匹配 +int LocateSimple(SequenceString string, SequenceString pattern) { + int i = 0, j = 0; + while (i < string.length && j < pattern.length) { + // 匹配就继续后移 + if (string.data[i] == pattern.data[j]) { + i++; + j++; + } + // 不匹配就撤回 + else { + i = i - j + 1; + j = 0; + } + } + if (j >= pattern.length) { + return i - pattern.length; + } + return -1; +} + +// 获取KMP的next数组 +int *GetNext(SequenceString string) { + auto *next = (int *) malloc(sizeof(int) * string.length); + next[0] = 0; + // i为当前主串正在匹配的字符位置,也就是next数组的索引 + int i = 0, j = 0; + while (i < string.length) { + if (j == 0 || string.data[i] == string.data[j]) { + next[++i] = ++j; + } else { + j = next[j]; + } + } + return next; +} + +// 获取KMP的nextval数组 +int *GetNextVal(SequenceString string) { + auto *nextval = (int *) malloc(sizeof(int) * string.length); + nextval[0] = 0; + // i为当前主串正在匹配的字符位置,也就是next数组的索引 + int i = 0, j = 0; + while (i < string.length) { + if (j == 0 || string.data[i] == string.data[j]) { + ++i; + ++j; + if (string.data[i] != string.data[j]) + nextval[i] = j; + else + nextval[i] = nextval[j]; + } else { + j = nextval[j]; + } + } + return nextval; +} + +int LocateKMP(SequenceString string, SequenceString pattern, const int *next) { + int i = 0, j = 0; + while (i < string.length && j < pattern.length) { + if (j == 0 || string.data[i] == pattern.data[j]) { + // 匹配则继续比较 + ++i; + ++j; + } else { + // 模式串右移 + j = next[j]; + } + } + if (j >= pattern.length) { + // 匹配成功 + return i - pattern.length; + } else { + return -1; + } +} + +int LocateKMP(SequenceString string, SequenceString pattern) { + int *next = GetNext(pattern); + int i = 0, j = 0; + while (i < string.length && j < pattern.length) { + if (j == 0 || string.data[i] == pattern.data[j]) { + // 匹配则继续比较 + ++i; + ++j; + } else { + // 模式串右移 + j = next[j]; + } + } + if (j >= pattern.length) { + // 匹配成功 + return i - pattern.length; + } else { + return -1; + } +} + +bool DestroySequenceString(SequenceString &string) { + free(string.data); + string.max_size = 0; + string.length = 0; + return true; } \ No newline at end of file diff --git a/Code/Code/source/test.cpp b/Code/Code/source/test.cpp index 097bd63..dd89f4d 100644 --- a/Code/Code/source/test.cpp +++ b/Code/Code/source/test.cpp @@ -1,4 +1,4 @@ -// ??????? +// 测试 #include #include "../head/sequence_list.h" #include "../head/link_list.h" @@ -11,6 +11,7 @@ #include "../head/link_queue.h" #include "../head/sequence_string.h" #include "../head/link_string.h" +#include "../head/link_tree.h" using namespace std; diff --git a/Data-Structrue/4-string-ex.md b/Data-Structrue/4-string-ex.md new file mode 100644 index 0000000..3626189 --- /dev/null +++ b/Data-Structrue/4-string-ex.md @@ -0,0 +1,70 @@ +# 串习题 + +基本上就是考察$KMP$算法 + +## KMP算法 + +**例题** 设主串$T=$'$abaabaabcabaabc$',模式串$S=$'$abaabc$',采用$KMP$算法进行模式匹配,到匹配成功时为止,在匹配过程中进行的单个字符间的比较次数是()。 + +$A.9$ + +$B.10$ + +$C.12$ + +$D.15$ + +解:$B$。假设位序从$0$开始。 + +编号|0|1|2|3|4|5 +:-:|:-:|:-:|:-:|:-:|:-:|:-: +S|a|b|a|a|b|c +next|-1|0|0|1|1|2 + +第一趟连续比较$6$次,在模式串的$5$号位和主串的$5$号位匹配失败,模式串的下一个比较位置为$next[5]$,即下一次比较从模式串的$2$号位和主串的$5$号位开始,然后直到模式串$5$号位和主串$8$号位匹配,第二趟比较$4$次,模式串匹配成功。单个字符的比较次数为$10$次。 + +## KMP算法优化 + +**例题** 串'$ababaaababaa$'的$nextval$数组为()。 + +$A.0,1,0,1,1,2,0,1,0,1,0,2$ + +$B.0,1,0,1,1,4,1,1,0,1,0,2$ + +$C.0,1,0,1,0,4,2,1,0,1,0,4$ + +$D.0,1,1,1,0,2,1,1,0,1,0,4$ + +解:$C$。$nextval$从$0$开始,可知串的位序从$1$开始(若从$0$开始。则第一个是$-1$)。第一步,令$nextval[1]=next[1]=0$。 + +从$j=2$开始,依次判断$p_j$是否等于$p_{next[j]}$,否则将$next[j]$修正为$next [next[j]]$,直至两者不相等为止。 + +第$2$步: $p_2=b$、$p_{next[2]}=a$,$p_2\neq p_{next [2]}$,$nextval[2]=next[2]=1$。 + +第$3$步: $p_3=a$、$p_{next[3]}=a$,$p_3=p_{next[3]}$,$nextval[3]=nextval [next[3]]=nextval[1]=0$。 + +第$4$步: $p_4=b$、$p_{next[4]}=b$,$p_4=p_{next[4]}$,$nextval[4]=nextval[next[4]]=nextval[2]=1$。 + +第$5$步: $p_5=a$、$p_{next[5]}=a$,$p_5=p_{next[5]}$,$nextval[5]=nextval[next[5]]=nextval[3]=0$。 + +第$6$步: $p_6=a$、$p_{next[6]}=b$,$p_6\neq p_{next [6]}$,$nextval[6]=next[6]=4$。 + +第7步:$p_7=a$、$p_{next [7]}=b$,$p_7\neq p_{next [7]}$,$nextval[7]=next[ 7]=2$。 + +第$8$步: $p_e=b$、$p_{next[8]}=b$,$p_8=p_{next[8]}$,$nextval[8]=nextval[next[8]]=nextval[2]=1$。 + +第$9$步: $p_9=a$、$p_{next[9]}=a$,$p_9=p_{next[9]}$,$nextval[9]=nextval [next[9]]=nextval[3]=0$。 + +第$10$步:$p_{10}=b$、$p_{next[10]}=b$,$p_{10}=p_{next[10]}$,$nextval[10]=nextval[next[10]]=nextval[4]=1$。 + +第$11$步: $p_{1=a}$、$p_{next[11]}=a$,$p_{11}=p_{next(11]}$,$nextval[11]=nextval[next[11]]=nextval[5]=0$。 + +第$12$步:$p_{12}=a$、$p_{next[12]}=a$,$p_{10}=p_{next[12]}$,$nextval[12]=nextval[next[12]]=nextval[6]=4$。 + +在第$5$步的推理中,$p_5=p_{next[5]}=a$,按前面的讲解部分,应该继续让$p_3$和$p_{next[3]}$比较(恰好$p_3=p_{next[3]}=1$),注意到此时$nextval[3]的值已存在,故直接将nextval[5]赋值为nextval[3]$。 + +编号|1|2|3|4|5|6|7|8|9|10|11|12 +:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-: +S|a|b|a|b|a|a|a|b|a|b|a|a +next|0|1|1|2|3|4|2|2|3|4|5|6 +nextval|0|1|0|1|0|4|2|1|0|1|0|4 diff --git a/Data-Structrue/4-string.md b/Data-Structrue/4-string.md index be3fbfb..6cec413 100644 --- a/Data-Structrue/4-string.md +++ b/Data-Structrue/4-string.md @@ -44,23 +44,75 @@ ### KMP算法 +#### 原理 + $KMP$算法是对朴素模式匹配算法的优化。 -朴素模式匹配算法的缺点就是当某些子串与模式串能部分匹配时,主串的扫描指针i经常回溯,从而导致时间开销。 +朴素模式匹配算法的缺点就是当某些子串与模式串能部分匹配时,主串的扫描指针$i$经常回溯,从而导致时间开销。 主要思想是失配时,只有模式串指针回溯,主串指针不变,找到失配前模式串的最长公共前后缀并跳转到最大公共后缀开始匹配,且最大公共前后缀要小于左端子串长度。 -+ 前缀:对于字符串A,B,A=B+S,且S非空,则B是A的前缀。 -+ 后缀:对于字符串A,B,A=S+B,且S非空,则B是A的后缀。 -+ PMT值:前缀集合和后缀集合的交集中,最长元素的长度。 -+ 部分匹配表:PMT值集合,字符串所有前后缀的PMT值。 +#### 公共前后缀 -当一个位置失配时,那么子串前面的所有字符串都是配对的,所以对于子串前面的部分都是已知的了,需要从模式串的最开始开始对比,而一般的模式匹配要从主串的下一个重新开始匹配,但是如果我们找到了主串当前失配位置的前缀和后缀最大重合的地方,即公共前后缀,PMT值,就代表从这里开始就可以匹配了,前面的地方没必要匹配,可以直接多跳几步移动到公共后缀去开始重新匹配。 +模式后滑动位数只与模式串本身的最大公共后缀有关,于主串无关。 -当第j个字符匹配失败,由P前1到j-1个字符组成的串记为S,则next[j]=S的最长相等前后缀长度+1(即最大PMT值+1)。特别的next[1]=0,且next[2]肯定为1。 ++ 前缀:对于字符串$A$,$B$,$A=B+S$,且$S$非空,则$B$是$A$的前缀。 ++ 后缀:对于字符串$A$,$B$,$A=S+B$,且$S$非空,则$B$是$A$的后缀。 ++ $PMT$值:前缀集合和后缀集合的交集中,最长元素的长度。 ++ 部分匹配表:$PMT$值集合,字符串所有前后缀的$PMT$值。 -使用KMP算法时需要先计算不同模式串P的next数组,时间复杂度为$O(m)$,然后使用KMP算法计算,时间复杂度为$O(n)$,从而平均时间复杂度为$O(m+n)$。 +当一个位置失配时,那么子串前面的所有字符串都是配对的,所以对于子串前面的部分都是已知的了,需要从模式串的最开始开始对比,而一般的模式匹配要从主串的下一个重新开始匹配,但是如果我们找到了主串当前失配位置的前缀和后缀最大重合的地方,即公共前后缀,$PMT$值,就代表从这里开始就可以匹配了,前面的地方没必要匹配,可以直接多跳几步移动到公共后缀去开始重新匹配。 -KMP算法对于重复部分比较多的模式串匹配效果更好。 +字符串|前缀|后缀|交集|PMT +:----:|:--:|:--:|:--:|:--: +'a'|∅|∅|∅|0 +'ab'|'a'|'b'|∅|0 +'aba'|'a','ab'|'ba','a'|'a'|1 +'abab'|'a','ab','aba'|'b','ab','bab'|'ab'|2 +'ababab'|'a','ab','aba','abab'|'a','ba','aba','baba'|'a','aba'|3 -KMP算法的next数组存在一定问题,当当前索引的值匹配失败,那么模式串的其他同样值的地方也一定会匹配失败。所以可以直接将模式串所有相同值的部分的next值全部取为其next值对应索引的next值。 +所以字符串'$ababa$'的部分匹配值为$00123$,即可以得到部分匹配值表。 + +#### next数组 + +失配移动位数$move$=已匹配字符数$j-1$-对应的部分匹配值$PM[j-1]$。(从而跳到开始有重复公共前缀的地方) + +部分匹配值表就是子串应该跳转的索引值。当这个位失配,则子串应该跳转的索引值是失配位置前一位的$PM$值。 + +因为纯匹配值表要看前一位的值,所以可以把匹配表数据全部右移一位,这就可以直接看失配位置的表值了,定义为$next$。最开始的一位用$-1$表示,最后一位丢弃。 + +所以$move=(j-1)-next[j]$。即移动位数=匹配位数-本位的跳转值。 + +所以相当于子串的比较指针$j$回到$j=j-move=j-((j-1)-next[j])=next[j]+1$。 + +所以$next$也可以全部加$1$,即得到子串变化表达式$j=next[j]$。$next$此时就是$j$失配时应该跳转到的索引值。 + +所以当$j=0$时,恒定$next[0]=0$(加一)$next[1]=1$。因为只有一个字母没有前一位所以是$-1$,只有两个字母前一位只有一个字母没有前后缀。(这里默认$next$数组从$0$开始,如果从$1$开始则索引全部加一) + +1. 求$next[j+1]$,则已知前面的所有$next$表值$next[1],next[2]\cdots next[j]$。 +2. 假设数组值$next[j]=k_1$(跳转索引),则有$P_1\cdots P_{k_1-1}=P_{j-k_1+1}\cdots P_{j-1}$(前$k_1-1$位字符与后$k_1-1$位字符重合)。 +3. 如果$P_{k_1}=P_j$(即最后一位也一样,则得到在之前匹配基础上的更长的公共前后缀),则$P_1\cdots P_{k_1-1}P_{k_1}=P_{j-k_1+1}\cdots P_{j-1}P_j$,则$next[j+1]=k_1+1$,否则进入下一步。 +4. 假设$next[k_1]=k_2$、则有$P_1\cdots P_{k_2-1}=P_{k_1-k_2+1}\cdots P_{k1-1}$。 +5. 第二第三步联合得到$P_1\cdots P_{k_2-1}=P_{k_1-k_2+1}\cdots P_{k1-1} =P_{j-k_1+1}\cdots P_{k_2-k_1+j-1}=P_{j-k_2+1}\cdots P_{j-1}$,即四段重合。 +6. 这时候.再判断如果$P_{k_2}=P_j$,则$P_1\cdots P_{k_2-1}P_{k_2}=P_{j-k_2+1}\cdots P_{j-1}P_j$,则$next[j+1]=k_2+1$,否则再取$next[k_2]=k_3$回到四。 +7. 如果遇到$0$还没有结果,则表示前面的全部不重合,赋值为$0+1=1$。 + +即要计算当前位置的$next$值,就看前一位的$next$值所代表的索引指向的字符是否与前一位的字符相等,若相等,则是前一位的$next$值加一,若不等,则继续看前一位的$next$值指向的字符的$next$指向的字符与前一位字符是否相等,若相等则结果就是这个$next$值加一,否则继续按照$next$索引向前寻找。 + +#### KMP匹配 + +$KMP$算法在形式上跟简单的模式匹配算法类似,唯一不同的是当失配时指针$i$不动(主串不动)指针$j$回到$next[j]$的位置重新比较,当$j=0$时$ij$同时加一,即主串第$i$个位置与模式串第一个字符不等时应该从主串$i+1$个位置开始匹配。 + +#### 算法性能 + +使用$KMP$算法时需要先计算不同模式串$P$的$next$数组,时间复杂度为$O(m)$,然后使用$KMP$算法计算,时间复杂度为$O(n)$,从而平均时间复杂度为$O(m+n)$。 + +虽然普通模式匹配算法复杂度$O(mn)$,但是一般情况下接近于$O(m+n)$。 + +$KMP$算法对于重复部分比较多的模式串匹配效果更好。 + +### KMP算法优化 + +$KMP$算法的$next$数组存在一定问题,当当前索引的值匹配失败,那么模式串的其他同样值的地方也一定会匹配失败。所以可以直接将模式串所有相同值的部分的$next$值全部取为其$next$值对应索引的$next$值。 + +所以需要再次递归,将$next[j]$变为$next[next[j]]$直到两者不相等,令更新后数组为$nextval$。 diff --git a/Data-Structrue/7-tree-ex.md b/Data-Structrue/7-tree-ex.md new file mode 100644 index 0000000..309fcd6 --- /dev/null +++ b/Data-Structrue/7-tree-ex.md @@ -0,0 +1,15 @@ +# 树 + +## 基本概念 + +**例题** 在一棵度为$4$的树$T$中,若有$20$个度为$4$的结点,$10$个度为$3$的结点,$1$个度为$2$的结点,$10$个度为$1$的结点,则树$T$的叶结点个数是()。 + +$A.41$ + +$B.82$ + +$C.113$ + +$D.122$ + +解:$B$。设树中度为$i$($i=0,1,2,3,4$)的结点数分别为$n_i$,树中结点总数为$n$,则$n$=分支数$+1$,而分支数又等于树中各结点的度之和,即$n=1+0n_0+n_1+2n_2+3n_3+4n_4=n_0+n_1+n_2+n_3+n_4$。依题意,$n_1+2n_2+3n_3+4n_4=10+2+30+80= 122$,$n_1+n_2+n_3+n_4=10+1+10+20=41$,可得出$n_0=82$,即树$T$的叶结点的个数是$82$。 diff --git a/Data-Structrue/7-tree.md b/Data-Structrue/7-tree.md index 170fec7..093da46 100644 --- a/Data-Structrue/7-tree.md +++ b/Data-Structrue/7-tree.md @@ -4,12 +4,16 @@ ### 树的基本概念 -+ 树:n个结点的有限集(树是一种递归的数据结构,适合于表示具有层次的数据结构)。是递归定义的。 ++ 树:$n$个结点的有限集(树是一种递归的数据结构,适合于表示具有层次的数据结构)。是递归定义的。 + 根结点:只有子结点没有父结点的结点。除了根结点外,树任何结点都有且仅有一个前驱。 + 分支结点:有子结点也有父结点的结点。 + 叶子结点:没有子结点只有父结点的结点。 -+ 空树:结点数为0的数。 -+ 子树:当n>1时,其余结点可分为m个互不相交的有限集合,每个集合本身又是一棵树,其就是根结点的子树。 ++ 祖先:根结点到结点的路径上的任意结点都是该结点的祖先。 ++ 双亲:靠近根结点且最靠近该结点的结点。 ++ 兄弟:有共同双亲结点的结点。 ++ 堂兄弟:双亲结点在同一层的结点。 ++ 空树:结点数为$0$的数。 ++ 子树:当$n>1$时,其余结点可分为$m$个互不相交的有限集合,每个集合本身又是一棵树,其就是根结点的子树。 + 结点的度:一个结点的孩子(分支)个数。 + 树的度:树中结点的最大度数。 + 结点的层次(深度):从上往下数。 @@ -23,36 +27,36 @@ ### 森林的基本概念 -+ 森林时m棵互不相交的树的集合。 ++ 森林时$m$棵互不相交的树的集合。 + 一颗树可以被分为森林。 ### 树的性质 -+ 结点数=总度数+1。 -+ 树的度m代表至少一个结点度是为m,且一定是非空树,至少有m+1个结点;而m叉树指所有结点的度都小于等于m,可以是空树。 -+ 度为m的树以及m叉树第i层至多有$m^{i-1}$个结点。 -+ 高度为h的m叉树至多有$\dfrac{m^h-1}{m-1}$个结点。 -+ 高度为h的m叉树至少有$h$个结点,度为m的树至少有$h+m-1$个结点。 -+ 具有n个结点的m叉树最小高度为$\lceil\log_m(n(m-1)+1)\rceil$。已知高度最小时所有结点都有m个孩子,所以$\dfrac{m^{h-1}-1}{m-1}\lfloor\dfrac{n}{2}\rfloor$为叶子结点。 ++ 满二叉树:一棵高度为$h$,含有$2^h-1$个结点的二叉树;只有最后一层有叶子结点,不存在度为$1$的结点;按层序从$1$开始编号,结点$i$的左孩子为$2i$,右孩子为$2i+1$,父结点如果有为$\lfloor\dfrac{i}{2}\rfloor$。 ++ 完全二叉树:当且仅当其每个结点都与高度$h$满二叉树编号$1$到$n$的结点一一对应时该二叉树就是完全二叉树;只有最后两层有叶子结点,最多只有一个度为$1$的结点,且一定为左孩子;$i\leqslant\lfloor\dfrac{n}{2}\rfloor$为分支结点,$i>\lfloor\dfrac{n}{2}\rfloor$为叶子结点。 + 二叉排序树:左子树上所有结点的关键字均小于根结点的关键字;右子树上所有结点的关键字均大于根结点的关键字;左右子树又各是一棵二叉排序树。 -+ 平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1。 ++ 平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过$1$。 ### 二叉树的性质 -+ 设非空二叉树中度为0、1和2的结点个数分别为$n_0$,$n_1$、$n_2$,则$n_0=n_2+1$(叶子结点比二分结点多一个)。假设树中结点的总数为$n$,则$n=n_0+n_1+n_2$,又根据树的结点等于总度数+1得到$n=n_1+2n_2+n_0$,所以相减就得到结论。 ++ 设非空二叉树中度为$0$、$1$和$2$的结点个数分别为$n_0$,$n_1$、$n_2$,则$n_0=n_2+1$(叶子结点比二分结点多一个)。假设树中结点的总数为$n$,则$n=n_0+n_1+n_2$,又根据树的结点等于总度数$+1$得到$n=n_1+2n_2+0n_0+1$,所以相减就得到结论。 + 二叉树的第$i$层至多有$2^{i-1}$个结点。 + 高度为$h$的二叉树至多有$\dfrac{m^h-1}{m-1}$个结点。 + 具有$n$个结点的完全二叉树的高度$h=\lceil\log_2(n+1)\rceil$或$\lfloor\log_2n\rfloor+1$。$2^{h-1}-1