二叉樹不只結構簡單、節省內存,更重要是是這種結構有利於對數據的二分處理。以前咱們提過,在二叉樹的基礎上可以派生不少經典的數據結構,也是下面咱們將進行討論的知識點:ios
(1) 提升數據查找效率的二叉排序樹。算法
(2) 優先隊列的最佳存儲結構的二叉堆。編程
(3) 兼具二叉排序樹和二叉堆性質的樹堆。數組
(4) 用於算法分析的數據編碼的哈夫曼樹。緩存
一. 二叉排序樹數據結構
二叉排序樹主要用於高效率查找。查找方法通常有三種:順序查找、二分查找和二叉排序樹查找。二叉排序樹又能夠分紅多種類型。這裏不一樣的查找方法,衡量他們的關鍵就是查找效率,每每越是複雜的構造二叉排序樹,在查找數據的效率方面越優良。函數
具備如下性質的非空二叉樹,稱爲二叉排序樹:編碼
1) 若根節點的左子樹不空,則左子樹的全部節點值均小於根節點值。spa
2) 若根節點的右子樹不空,則右子樹的全部節點值均不小於根節點值。操作系統
3) 根節點的左右子樹也分別是二叉排序樹。
容易看到,根據二叉排序樹的構造機制,查找某個元素的效率就取決於所構造的二叉排序樹的深度。深度越小,效率越高。二叉排序樹有以下三種類型:
i) 普通二叉排序樹:邊輸入邊構造的二叉排序樹,樹的深度取決於輸入的序列。
ii) 靜態二叉排序樹:按照二分查找的方法構造出的二叉排序樹,近似豐滿,深度約爲logn。可是這種樹須要離線構建,即輸入數據後一次性建樹,不方便動態維護。
iii) 平衡樹:再插入和刪除過程當中一直保持左右子樹的高度至多相差1的平衡條件,且可以保證樹的深度是logn.
Ex1(poj2309):
給出一種無窮滿二叉排序樹的機制,其葉子節點是無窮的奇數序列:一、三、5……而後一、3的父節點是右兒子序號減左兒子序號.這樣可以造成倒數第二層的節點,而後按照一樣的機制造成上面那層節點。而後給出一個節點序號x,編程計算以該節點爲根的二叉排序樹的最小編號以及最大編號。
既然題目給出的是無窮滿二叉排序樹,咱們就可以充分利用二叉排序樹和滿二叉樹的性質。對於以x爲根的子樹,若是咱們知道這棵樹的層數k(層數從0開始計數),根據滿二叉樹的性質,該子樹有2^(k+1) – 1個節點。x的左子樹的節點編號都是小於x的,x的左子樹也是一個滿二叉樹,節點數是2^k – 1,所以咱們能看到,x的左子樹的編號區間是[min , x - 1],這個區間含有的整數就是x的左子樹的節點數,即有x-1-min+1=2^k – 1成立。即有min = x – 2^k +1.對稱的,對於最大標號的節點,是徹底同樣的分析思路。則有max = x + 2^k – 1.
下面咱們須要解決的問題就剩下如何計算根節點爲x的樹的層數。根據這棵無限二叉排序樹的機制,咱們容易看到規律,x除以2^k是奇數的時候,k即是層數。咱們將整數x視爲二進制形式a[n]a[n-1]…a[0],再從按權展開的形式去考察這個整數x,咱們反覆除以2,發現當遇到二進制形式右側第一個1的時候,整數x變成了奇數。即咱們有這樣的結論,二進制數x右側第一個1所在位置的權值2^k,k即是樹的層數。
而這樣的2^k咱們能夠經過位運算x&(-x)快速獲得。
參考代碼以下:
#include<iostream> using namespace std; long long lowbit(long long x){ return x & (-x); } int main(){ long long n , x; cin >> n; for(int i = 0;i < n;i++){ cin >> x; cout << x - lowbit(x) + 1 <<' ' << x + lowbit(x) - 1<<endl; } }
二.二叉堆.
二叉堆是一棵知足下列性質的徹底二叉樹:若是某節點有孩子,則根節點的值都小於孩子節點的值,這樣的二叉堆咱們稱之位小根堆。反之,若是根節點的值都大於孩子節點的值,咱們稱之爲大根堆。
基於二叉堆的性質,咱們很是好得到整個堆的最大元素和最小元素,所以二叉堆常常用於優先隊列的存儲結構。由於優先隊列的刪除操做正是刪除一個線性序列中優先級最大或者最小的元素。
下面咱們便開始討論這樣一個數據結構的各類操做。
1) 小根堆的插入操做:
假設咱們本來有一個知足二叉堆性質的結構(在程序中咱們用線性結構存儲這樣一個特殊的徹底徹底二叉樹),當須要加入一個新的節點進入小根堆的時候,咱們將其加入到小根堆的最後面,而後根據其節點序號(注意和節點權值不同)來獲得其父節點的序號,進而訪問其權值,進行比較並進行交換,而後進行迭代操做一直向上走。
Int k = ++top; Heap[k] = 被插入元素的權值. While(k > 0){//k=0到達根節點,是迭代的終結點. int t = (k - 1)/2; if(heap(k )< heap(t)){ swap(heap[k] , heap[t]) k = t; } else break; }
2) 小根堆的刪除操做
在堆中刪除最小元素,即刪除heap[0],而後將二叉堆中節點序號最大的節點移動到根節點,而後從根節點往下進行維護小根堆性質的操做。
if(top){ int temp = heap[0]; int k = 0; heap[k] = heap[top--]; while((2 * k + 1) <= top){ int t = 2*k + 1; if(t < top && heap[t + 1] < heap[t]) t++; if(heap[k] > heap[t]){//當前節點的最小孩子比當前節點的權值小,須要交換 swap(heap[k] , heap[t]); k = t; } else break; } } else output "empty heap"
ex1(zoj2724):
消息隊列是操做系統的基礎,如今給出兩種類型的指令:
1)GET指令:獲取消息隊列中優先級最高的指令信息(包括指令名稱和指令參數)。
2)PUT指令:往消息隊列中添加指令,包括指令名稱,指令參數和優先值。注意這裏優先值越小優先級越大,兩者相同時,較早進入消息隊列的優先級高。
典型的優先隊列的題目,這裏咱們在用二叉堆實現的時候,須要手寫一個函數用於二叉堆元素權值的比較。輸入的時候咱們爲每一個節點順序標號,節點信息咱們存儲在一個緩存區,而二叉堆用於存儲節點序號。在插入刪除的時候,咱們利用存儲的節點序號這一索引,在緩衝區找到對應節點的信息,進行比較。
參考代碼以下:
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn = 60000 + 10;//60000條指令 const int maxs = 100; struct info{ char name[maxs]; int para; int pri , t; }p[maxn]; int heap[maxn]; int top , used; int compare(int a , int b){//ad額優先級高發返回-1 ,不然返回1 if(p[a].pri < p[b].pri) return -1; if(p[a].pri > p[b].pri) return 1; if(p[a].t < p[b].t) return -1; //優先級相等,看時間戳 if(p[a].t > p[b].t) return 1; } int main(){ used = 0; top = 0;//堆尾指針,指向堆數組最後一個元素的後面。 int cnt = 0; char s[maxs]; while(scanf("%s" , s) != EOF){ getchar(); if(!strcmp(s , "GET")){//輸入GET,進行刪除操做 if(top){ printf("%s %d\n" , p[heap[0]].name , p[heap[0]].para); int k = 0; heap[k] = heap[--top]; while(2 * k + 1 < top){//這裏注意取值範圍,top是指堆的尾部指針的後面那個一個,所以這裏就不取等號 int t = 2 * k + 1; if(t < top && compare(heap[t + 1] , heap[t] < 0)) t++; if(t < top && compare(heap[t] , heap[k]) < 0){ swap(heap[t] , heap[k]); k = t; } } } else printf("EMPTY QUEUE!\n"); } else{ scanf("%s %d %d" , p[cnt].name , &p[cnt].para , &p[cnt].pri); p[cnt].t = cnt; int k = top++; heap[k] = cnt++;//堆尾放入當前節點序號 while(k > 0){//第一個點不走這個 int t = (k - 1)/2; if(compare(heap[k] , heap[t]) < 0){ swap(heap[k] , heap[t]); k = t; } else break; } } } return 0; }