最近剛學了平衡樹,然后突發奇想寫幾篇博客紀念一下,可能由於是剛學的緣故,還有點兒生疏,望大家海涵
說到平衡樹,就不得不從基礎說起,而基礎,正是二叉查找樹
什么是二叉查找樹??
大家觀察一下下面的這棵二叉樹
相信大家一眼就能發現,這棵樹從左往右是遞增的(也就是右兒子大於左兒子)
那么這樣的一棵樹有什么用呢?
就比如說圖上的這個數列 12 10 15 6 13 19 2 8 14 22
如果你想找第n大的數,你明顯需要冒泡排序或快速排序,最壞復雜度分別為:O(n^2/2),O(nlogn);
如果數列再改大一下,而你每次都要跑這么高的復雜度,電腦累死了
但如果你用上圖這棵樹,根據他(右兒子>自己>左兒子)的特性,可以在期望復雜度O(logn)的時間內查出一個數據
同時修改也變得簡單,O(logn)的時間查找位置,O(1)的時間連邊
首先先看看插入
以7為例,我們從根節點開始
很明顯,根節點大於7,那么我把7下放到當前的左兒子
還是大,怎么辦?繼續下放到當前的左兒子
這次是小了,放當前的右兒子
右兒子又大了,我們打算把他往當前的左兒子上放
但是,8這里並沒有左兒子
可以恭喜了,7終於找到了自己的位置,我們現在新建一個結點,連一條邊,使他變為8的左兒子
當然,有重復時也很好辦,在該結點加一個數量標記,告訴他有幾個即可
插入的代碼:
void add(int wz,int v) { x[wz].size++; if(x[wz].val==v) { x[wz].cnt++; return; } if(x[wz].val>v) { if(x[wz].ls!=0) { add(x[wz].ls,v); } else { bnt++; x[bnt].val=v; x[bnt].size=1; x[bnt].cnt=1; x[wz].ls=bnt; } } else { if(x[wz].rs!=0) { add(x[wz].rs,v); } else { bnt++; x[bnt].val=v; x[bnt].size=1; x[bnt].cnt=1; x[wz].rs=bnt; } } }
然后是刪除
刪除其實可以通過改變該數字連的邊來做到,但這我放到講treap時再說,這個圖的刪除可以利用cnt標記,找到這個數后cnt--即可,如果為cnt=0,就表示此時已經沒有這個數;
同時,cnt的改變不會影響大小關系,所以這棵樹還是一定滿足bst的性質的
真正的重頭戲————查詢
不得不說bst的功能還是很強大,常規的也可以支持找前驅,找后繼,按值查排名和按排名查值,下面我一個一個來說
1.找前驅和后繼:
先說前驅
根據定義,x的前驅是小於x的最大的數,所以前驅一定比x小,前驅就有可能在x的左子樹或x的左父親里
這里以9為例
我們知道前驅一定比9小,那么從根開始
我們發現根比9大,不滿足前驅小於本身的條件,那么根據二叉查找樹的性質,我們找他的左子樹
發現還是大,找他的左子樹
這回終於碰上一個比9小的了,我們把他記下來,為了確保6是最小的,我們要看他的右子樹
這回是8,還是比9小,我們更新一下
在我們想向右跑時,發現當前已經沒有右子樹了,說明不可能有別的滿足條件的,所以答案是8
總結一下,其實找前驅,就是從根節點開始,遞歸子樹,如果當前節點大於你要找的數,就找他的左子樹,反之找他的右子樹,直到沒有可以找的為止;
至於后繼,其實道理一樣,不過是反過來,這里我就不多說了,大家自己手碼
代碼:
找前驅:
int GetPre(int wz,int val,int ans) { if(x[wz].val>=val) { if(x[wz].ls==0) { return ans; } else { GetPre(x[wz].ls,val,ans); } } else { if(x[wz].rs==0) { if(x[wz].val<val) { return x[wz].val; } else { return ans; } } if(x[wz].cnt!=0) { return GetPre(x[wz].rs,val,x[wz].val); } else { return GetPre(x[wz].rs,val,ans); } } }
找后繼:
int GetNext(int wz,int val,int ans) { if(x[wz].val<=val) { if(x[wz].rs==0) { return ans; } else { GetNext(x[wz].rs,val,ans); } } else { if(x[wz].ls==0) { if(x[wz].val>val) { return x[wz].val; } else { return ans; } } if(x[wz].cnt!=0) { return GetNext(x[wz].ls,val,x[wz].val); } else { return GetNext(x[wz].ls,val,ans); } } }
2.按排名找值和按值找排名
按排名找值:
相信大家在剛才的代碼中發現了不和諧的東西——size,現在他就要派上用場了
還是剛才的圖,我要找排名第7的數,怎么辦呢?
看圖(綠色的是子樹及本身的大小)
根據bst的性質,右面的元素嚴格大於左面的元素,所以右面的排名也大於左面的,自然,排名為n的數就是第n靠左的節點
在這里我以n=7為例
我還是從根開始
因為root的左子樹的大小為5,說明root本節點及他的左子樹大小為6,而我要找的排名為7,顯然不足,於是我可以假裝把樹切了一半(如圖),然后查詢新樹中排名第1的數
因為我們知道5的左子樹大小為2,而2+1=3>1,所以我們找他的左子樹(如圖)
13沒有左子樹,所以他在該子樹中的排名為1,滿足,所以答案為13。
由上面的例子可以得出,我們在按排名查值時,當前位置的排名為他左子樹的大小加上自己cnt(該節點有幾個)的大小,如果當前排名小於要找的排名,就去右子樹找,並更新要找的排名,反之先自查,不行再去左子樹找
看代碼:
int GetValByRank(int wz,int rank) { if(wz==0) { return INF; } if(x[x[wz].ls].size>=rank) { return GetValByRank(x[wz].ls,rank); } if(x[x[wz].ls].size+x[wz].cnt>=rank) { return x[wz].val; } return GetValByRank(x[wz].rs,rank-x[x[wz].ls].size-x[wz].cnt); }
按值找排名:
這個其實和按排名查值是一樣的,只不過變量從排名成了值,但其實也很簡單,我們以13為例:
首先們從根開始(藍色為當前數的總數)
12顯然是小於13的,所以我們找12的右子樹,同時我們已知的小於13的數有了6個
15比13大,所以我們找他的左子樹,比他小的還是6個
我們發現15的左兒子的值為13,到達終點。這時我們知道他的排名是所有比他小的數+1,比他小的有路上的6個,而他的左子樹大小為0,所以排名為6+0+1=7
總結一下,按值找排名時,從根開始,如果該位置的值小於要查詢的值,就找他的右子樹,同時記住他左子樹的大小,如果小於,就查詢他的左子樹,直到大小相等,他的排名就是該點左子樹的大小加上一路上比他小的節點個數再加上1
看代碼:
int GetRankByVal(int wz,int val) { if(wz==0) { return 0; } if(val==x[wz].val) { return x[x[wz].ls].size+1; } if(val<x[wz].val) { return GetRankByVal(x[wz].ls,val); } return GetRankByVal(x[wz].rs,val)+x[x[wz].ls].size+x[wz].cnt; }
有關二叉查找樹的內容暫時告一段落,這個數據結構相對較優,但在遇到下圖時會被卡成O(n)。所以一些優化還是必不可少的,過一段時間我會繼續寫下去,給大家帶來關於treap,splay和替罪羊樹的一些事兒