數據結構復習
模式匹配
模式匹配就是給定模式串和主串,在主串中找模式串第一次出現的位置的算法。
BF算法
BF算法就是暴力匹配算法,下面給個簡單代碼就過吧。
char* s1 = "abcaba";//主串
char* s2= "aba";//模式串
int i=0;//主串中的位置
int j=0;//模式串中的位置
while(i<strlen(s1)&&j<strlen(s2)){//防止越界
if(s1[i]==s2[j]){//匹配成功則比較下一位
i++;
j++;
}else{
i=i-j+1;//匹配失敗,i回退
j=0;//j從0開始匹配
}
}
if(j>strlen(s2)){
cout<<i-j<<endl;//找到了輸出位置
}else{
cout<<-1<<endl;//沒找到輸出-1
}
KMP算法
上面的暴力算法回退的太多,很多無用的比較,浪費時間,KMP算法解決了這個問題,提出了next數組,使得在一次比較失敗之后可以快速跳過無用比較,大大簡化了算法。其思路是主串的i每次不回退,j不一定回退到0,而是按照next數組回退到對應的位置,那么關鍵就是next數組的計算了。
next數組
next數組存在的意義是為了簡化j的回退,方便理解而言,提出了最長公共前后綴的概念,但實際代碼中並不體現這種理解。
例如,對於字符串“abcsab”的前綴串集合為{a,ab,abc,abcs,abcsa},后綴串集合為{b,ab,sab,csab,bcsab},可以看出最大公共前后綴字串為ab。長度為2,因此對應next數組值就是2。
求解代碼:
void get_next(char* s,int* next){
next[0] = -1;
next[1] = 0;
int k = 0;//初始位置next值
int i = 2;//初始需要賦值的位置
while(i<strlen(s)){
if(k==-1||s[i-1]==s[k]){//如果相等,next的值就是當前位置next的值+1
next[i++] = ++k;
}else{
k = next[k];//不相等則繼續往前找,直到k=-1或者找到相等的
}
}
}
模式匹配代碼:
int KMP(char* m,char* s,int pos){//pos是指從pos位置開始匹配
int i=pos;
int j=0;
int* next = new int[strlen(s)];
get_next(s,next);
while(i<strlen(m) && j<strlen(s)){
if(j==-1 || m[i]==s[j]){
j++;
i++;
}else{
j = next[j];//直接按照next數組進行回退
}
}
delete next;
if(j>=strlen(s)){
return i-j;
}else{
return -1;
}
}
改進的KMP算法
上面的KMP算法存在的問題是,如果模式串為"aaaaaaaab",那么其next數組的值便是[-1,0,1,2,3,4,5,6,7],也就意味着,如果最后一個a不匹配,那么將一步一步回退j,回退了之后進行重復的比較,繼續回退(因為還是a,還是失配)。所以我們如果能判斷s[i]是否與s[k]相等,如果相等,其next值直接為next[k]就好了。所以改進后的next數組值應該為[-1,-1,-1,-1,-1,-1,-1,-1,0],簡單來說,就是在之前next數組的基礎上呢,看要跳轉到的地方的字符跟當前掃描到的字符是否相等,相等直接就讓當前的next值為跳轉地方的next值,因為就算跳過去也還會進行同樣的比較,多余的比較。
改進的next求解代碼
int* get_next_plus(char* s,int* next){
next[0] = -1;
int i=0;
int k=-1;
while(i<strlen(s)){
if(k==-1 || s[i]==s[k]){
i++;
k++;
if(i>=strlen(s)){
break;
}
if(s[i]==s[k]){
next[i] = next[k];
}else{
next[i] = k;
}
}else{
k = next[k];
}
}
}
后綴表達式求值
過程:
- 遍歷表達式,遇到數字入棧
- 遇到操作數,彈出最上面的兩個元素,執行操作,操作完后結果再入棧
- 一直讀完,將最后的結果輸出
例子:
32 26 - 5 * 28 4 / +
32-26=6
6*5=30
28/4=7
30+7=37
結果就是37
中綴表達式轉后綴表達式
下面規則用來理解轉換過程,理解掃描過程棧變化的情況。
中綴表達式a + b*c + (d * e + f) * g,其轉換成后綴表達式則為a b c * + d e * f + g * +。
轉換過程需要用到棧,具體過程如下:
1)如果遇到操作數,我們就直接將其輸出。
2)如果遇到操作符,則我們將其放入到棧中,遇到左括號時我們也將其放入棧中。
3)如果遇到一個右括號,則將棧元素彈出,將彈出的操作符輸出直到遇到左括號為止。注意,左括號只彈出並不輸出。
4)如果遇到任何其他的操作符,如(“+”, “*”,“(”)等,從棧中彈出元素直到遇到發現更低優先級的元素(或者棧為空)為止。彈出完這些元素后,才將遇到的操作符壓入到棧中。有一點需要注意,只有在遇到" ) "的情況下我們才彈出" ( ",其他情況我們都不會彈出" ( "。
5)如果我們讀到了輸入的末尾,則將棧中所有元素依次彈出。
靜態鏈表
靜態鏈表是用數組來模擬存儲空間實現鏈表的。
分為head鏈和avail鏈,head鏈存儲的是具體數據,而avail鏈存儲的是可利用空間。
讀一下:
'C'、'B'、'A'
如果要添加數據,就要從可以用空間開始添加,刪除的修改avail指針就可以了,不需要把值改掉。
循環隊列
//循環隊列判空條件:
rear==front;
//循環隊列判滿條件:
(rear+1)%maxSize == front;
//求循環隊列的長度:
(rear-front+maxSize)%maxSize;
//遍歷循環隊列
for(i=front;i!=rear;i=(i+1)%maxSize)
棧、隊列和遞歸單元 - 易錯題
判斷題
- 棧是操作受限的線性表,它只允許在表頭插入和刪除元素。(x)- 棧只允許在表尾插入和刪除元素,所以錯。
- 只有使用了局部變量的遞歸過程在轉換成非遞歸過程時,才必須使用棧。 (x) - 是否必須使用棧和局部變量存在與否無關,尾遞歸形式可以不用棧模擬,而遞歸過程中函數的調用取地址等操作必須由棧模擬(除尾遞歸等特殊情況)。
填空題
-
有n個數入棧,則出棧的順序有C(2n,n)-C(2n,n-1)種;卡特蘭數的定義:
令h(0)=1,h(1)=1,Catalan數滿足遞歸式:h(n) = h(0)*h(n-1) + h(1)*h(n-2) + ... + h(n-1)*h(0) (n>=2),
-
在雙端隊列中,元素進入該隊列的順序依次為a,b,c,d,則既不能由輸入受限的雙端隊列得到,又不能由輸出受限的雙端隊列得到的輸出序列是dbca;分析:輸入受限的雙端隊列可以看成是一個隊列和一個棧的組合,因此如果隊列和棧都無法輸出的元素在輸入受限的雙端隊列中同樣無法輸出。而我們知道,棧可以輸出的組合個數為C(8,4)-C(8,3) = 14種,隊列能輸出的組合個數為1種,四個元素排列有A(4,4)=24種,24-14-1=9種,所以兩種結構都不能輸出的組合數共有9種。只考慮棧不能輸出的組合,因為數目小。分別為adbc,bdac,cabd,cadb,dabc,dacb,dbac,dbca,dcab,在這幾個序列里面找輸出受限的隊列不能輸出的序列,就很容易找到dbca了。
數組的存儲結構
二維數組的順序存儲結構分為以行序為主序的存儲方式和以列序為主序的存儲方式。
以行序為主的存儲方式就是常規的先存第0行的每列,再存第一行的每列,以此類推。以列為主的存儲方式同理。
對於三維數組來說,按下標從左到右的順序存儲。例如,設a[0][0][0]的地址為p,則對於數組a[m][n][r],a[i][j][k] = p + (i*n*r + j*r + k)*l;
稀疏矩陣
三元組順序表
轉置函數最簡單的方法就是row和col對應的值交換,並且row和col順序交換,然后按照行從小到大排序。
O(col*num)的轉置算法:
把M矩陣的第0列的所有元素找出來,轉置,放到N矩陣的第0行,把M矩陣的第1列所有元素找出來,轉置,放到N矩陣的第1行,以此類推。每次照完所有第n列元素需要掃描col次,轉置num個元素,時間復雜度O(col*num)。
int i=0;
for(int col=0;col<cols;col++){//找每一列的所有元素
for(int j=0;j<num;j++){
if(triElems[j].col == col){//找到了之后存到N的第j行
N.triElems[i].row = triElems[j].col;
N.triElems[i].col = triElems[j].col;
N.triElems[i].val = triElems[j].val;
i++;
}
}
}
O(num)算法:
空間換時間,申請兩個數組,一個用來記錄每一列的第一個非0元存儲的起始位置,另一個用來存儲這一列有多少個非0元。因為在原矩陣的三元組表示種,存儲是連續的,一旦我們知道了某一列存儲的非0元起始地址和長度(相當於知道了終止地址),我們就能確定整個一列,進而轉置整個一行。下面簡要寫一下。
int *cNum = new int[col+1];//存儲每一列非0元個數
int *cPos = new int[col+1];//存儲每一列第一個非0元起始位置
for(int col=0;col<cols;col++) cNum[col] = 0;
for(int i=0;i<nums;i++) cNum[triElems[i].col]++;
cPos[0] = 0;
for(int col=1;col<cols;col++){//下一列第一個非0元的起始位置=上一列非0元的起始位置 + 上一列非0元個數
cPos[col] = cPos[col-1] + cNum[col-1];
//我覺得這里很巧妙
}
//由於是從小列到大列求的非0元起始位置,所以的時候的存儲順序就是M矩陣按列從小到大存儲的
//上面就都求完了,直接開始轉置
for(int i=0;i<nums;i++){
int j=cPos[triElems[i].col];//得到的是M矩陣存儲的第i個元素的col列的起始非0元位置
N.triElems[j].row = triElems[i].col;
N.triElems[j].col = triElems[i].row;
N.triElems[j].row = triElems[i].val;
cPos[triElems[i].col]++;
//注意這里是重點,非常巧妙的++,這樣下一次循環進來找到的非0元起始位置就是之前位置+1,不用再寫個循環了
}
廣義表
定義相關
LS=(a1,a2,...,an)
LS是表名,表的長度為n,長度為0的廣義表為空表。如果n>=1,則a1是表頭,(a2,a3,...,an)是表尾。(注意表頭沒有加括號表示是原子,而表尾加括號了表示子表)
廣義表種括號的重數代表廣義表的深度。遞歸表的深度為無窮大。
表頭、表尾
一個例子足以
A = ((),(a,b,c),d,e)
head(A) = ()
tail(A) = ((a,b,c),d,e)
取到c的操作是head(tail(tail(head(tail(A)))))
畫存儲結構
廣義表的存儲結構為:
tag- data/slink-link
tag=1,為原子; tag =0, 為子表.
data--為原子數據,slink---為子表地址
link---為本元素的同層下一個元素的地址
A = ((),a,(b,(c,d)),(e,f))
樹的相關概念
節點的度:一個節點的子樹數目成為節點的度。也就是一個節點連着幾個子節點的意思。
葉子節點:沒有子節點的節點。
樹的度:Max{所有節點的度}。
深度:就是樹的高度,很好理解不解釋。
二叉樹的相關概念
- 有n個節點的二叉樹的分支數為n-1
- 若二叉樹高為h,則該二叉樹最少有h個節點,最多有2^h-1個節點
- 若高度為h的二叉樹具有最大數目的節點,則稱其為滿二叉樹
- 若高度為h的二叉樹除第h層外,其他各層的節點數都達到最大個數,並且第h層的節點還是連續分布的,則稱其為完全二叉樹。
- 具有n各節點的完全二叉樹高度為log2 (n+1)取上界。
二叉樹的遍歷
遞歸算法
前序遍歷
void preOrder(Node* r){
if(!r){//遞歸終點
return;
}
cout<<r->data;//先輸出
preOrder(r->left);//再遞歸左子樹
preOrder(r->right);//再遞歸右子樹
}
中序遍歷
void inOrder(Node* r){
if(!r){//遞歸終點
return;
}
inOrder(r->left);//先遞歸左子樹
cout<<r->data;//再輸出
inOrder(r->right);//再遞歸右子樹
}
后序遍歷
void postOrder(Node* r){
if(!r){
return ;
}
postOrder(r->left);//先遞歸左子樹
postOrder(r->right);//再遞歸右子樹
cout<<r->data;//最后輸出
}
層次遍歷
void Level(Node* r){
queue<Node*> q;
if(!r){
return;
}
Node* p;
q.push(r);
while(!q.empty()){
p = q.front();//取出元素
cout<<p->data;//訪問
q.pop();//出隊
if(p->left){
q.push(p->left);
}
if(p->right){
q.push(p->right);
}//由於是按照先左后右的順序入隊的,出隊的順序在同一層里也是先左后右
//因此實現了層次遍歷
}
}
非遞歸算法
棧可以實現非遞歸的二叉樹三種遍歷,實際上棧的作用是存儲訪問的順序,這樣就不需要再回溯找,直接能夠通過取出棧頂元素得到下一個應該被訪問的節點。
前序遍歷
用棧:
void preOrder(Node* r){
stack<Node*> s;
Node* p = r;
while(!s.empty() || p){
while(p){//如果p不為NULL,就一直往左遍歷,邊遍歷邊輸出。
cout<<p->data;
s.push(p);
p=p->left;
}//出循環的時候一條路上的所有偏左的節點都被訪問過了
if(!s.empty()){//依次取出路徑上保存的元素,從他們的右節點繼續上面的循環
p=s.top();
s.pop();
p=p->right;
}//這里如果p->right ==null不會執行while(p)循環,而是再次去取棧頂元素,從下一個右節點開始。
}
}
不用棧(三叉鏈表):
void preOrder(Node* r){
Node* p =r;//三叉鏈表的parent節點方便了回溯,因為parent節點反應了從上到下的遍歷路徑,因此可以通過父節點找到遍歷路徑上的節點
while(p){
cout<<p->data;
if(p->left){//先使勁往左遍歷,邊遍歷邊輸出
p=p->left;
}else if(p->right){//如果節點只有右子樹,就使勁往右遍歷
p=p->right;
}else{//對於葉子節點,需要向上回溯找到第一個未被訪問的第一個右子樹的根節點
Node* q=p->parent;
while(q && (q->right==p || !q->right)){
//q->right==p表明從左往上回溯,不符合
//q->right == NULL完全沒右子樹,當然也不符合找右子樹根節點的規則
p=q;
q = q->parent;
}
if(!q){//全部訪問完的時候 p=r,q=NULL,結束循環
break;
}
}
}
}
后面除了雙棧就不寫注釋了,太麻煩。思想都是一樣的。
中序遍歷
用棧:
void inOrder(Node* r){
stack<Node*> s;
Node* p = r;
while(!s.empty() || p){
while(p){
s.push(p);
p=p->left;
}
if(!s.empty()){
p=s.top();
s.pop();
cout<<p->data;
p=p->right;
}
}
}
不用棧(三叉鏈表):
void inOrder(Node* r){
Node* p =r;
Node* q;
while(p){
if(p->left){
p=p->left;
}else if(p->right){
cout<<p->data;
p=p->right;
}else{
cout<<p->data;
Node* q=p->parent;
while(q){
if(!q->right){
cout<<q->data;
}else if(q->left==p){
cout<<q->data;
break;
}
p=q;
q = q->parent;
}
if(!q){
break;
}
}
}
}
后序遍歷
用棧:
void inOrder(Node* r){
stack<Node*> s1,s2;
//用兩個棧來存儲,第一個棧是從右往左的訪問順序,第二個棧的接收順序是先根再左再右,符合后序遍歷規則
Node* p;
s1.push(p);
while(!s1.empty()){
p = s1.top();
s1.pop();
if(p->left){
s1.push(p->left);
}
if(p->right){
s1.push(p->right);
}
//注意這里先左子樹入棧s1,再右子樹入棧s2,然后根節點入棧s2
//那么在后面的循環里面,必定右子樹的節點先入棧s2,左子樹的節點后入棧s2
//導致的結果就是,根節點被壓在棧底,左子樹次之,右子樹最上
s2.push(p);
}
while(!s2.empty()){
p=s2.top();
s2.pop();
cout<<p->data;
}
}
不用棧(三叉鏈表):
void postOrder(Node* r){
Node* p =r;
Node* q;
while(p){
if(p->left){
p=p->left;
}
if(p->right){
p=p->right;
}else{
cout<<p->data;
q=p->parent;
while(q){
if(!q->right){//右子樹不存在。就輸出它,繼續向上找
cout<<q->val<<" ";
p=q;
q=q->parent;
continue;
}
if(q->left==p){//如果是從左邊過來的,右子樹就是回溯的節點
p=q->right;
break;
}else{//如果是從右邊過來的,說明左右子樹都已經訪問完了,這個節點可以輸出了,繼續向上回溯
p=q;
q=q->parent;
cout<<p->val<<" ";
}
}
if(!q){//所有元素都輸出完的終點是q==NULL,跳出循環
break;
}
}
}
}