演算法導論第十二(12)章 二叉查詢樹
12.1 二叉查詢樹
定義:設x為二叉查詢樹中的一個結點。如果y是x的左子樹中的一個結點,則key[y]≤key[x]。如果y是x的右子樹中的一個結點,則key[x]≤key[y].
前序遍歷:先遍歷根再遍歷左右子樹,簡稱根-左-右。
中序遍歷:先遍歷左子樹再遍歷根再遍歷右子樹,簡稱左-根-右。
後序遍歷:先遍歷左右子樹再遍歷根,簡稱左-右-根。
書中中序遍歷程式碼:
//中序遍歷 void INORDER-TREE-WALK(struct Tree *p) { if (p->key) { INORDER-TREE-WALK(p->lchild); cout<<p->key<<" "; INORDER-TREE-WALK(p->rchild); } }
定理12.1 如果x是一棵包含n個結點的子樹的根,則呼叫INORDER-TREE-WALK(x)過程時間為θ(n).
12.1-1 略。
12.1-2 二叉查詢樹性質與最小堆(見6.1節)之間有什麼區別?能否利用最小堆性質在O(n)時間內,按序輸出含有n個結點的樹中的所有關鍵字?行的話,解釋該怎麼做,不行的話,說明原因。
二叉查詢樹:根的左子樹小於根,根的右子樹大於根。而最小堆:根的左右子樹均大於根。
不能。原因是含有n個結點的最小堆的結點key大小是根<左<右或者根<右<左,左右子樹是無序的。導致結果就是不能按照樹的前中後序遍歷在O(n)時間內來有序的輸出他們。再看二叉查詢樹,按照中序遍歷,正好是左<根<右,他們是有序的。
12.1-3 給出一個非遞迴的中序樹遍歷演算法。(提示:有兩種方法,在較容易的方法中,可以採用棧作為輔助資料結構;較複雜的方法中,不採用棧結構,但假設可以測試兩個指標是否相等)。
請看二叉樹的遞迴與非遞迴遍歷 這是以前寫得一遍文章。裡面有一段是非遞迴輔助棧遍歷是前序遍歷法。
現在給出非遞迴輔助棧中序遍歷法,這段程式碼直接替換上面這篇文章的那段非遞迴前序遍歷程式碼即可執行,如果寫得有問題,請舉例說明,謝謝!
void InOrderTraversal(struct Tree *root)
{
if (root == NULL) return;
s.top=-1;
s.push(root);
struct Tree *p=root;
while (1) {
if (p->lchild->key != 0)
{
p=p->lchild;
s.push(p);
}
else
{
cout<<p->key<<" ";
s.pop();
if (p->rchild->key)
{
s.push(p->rchild);
}
else
{
p=reinterpret_cast<struct Tree *>(s.Array[s.top]);//將陣列中的資料恢復為編譯器所能識別的指標
if (s.empty()) break;
cout<<p->key<<" ";
}
s.pop();
if (!p->rchild->key)
{
p=reinterpret_cast<struct Tree *>(s.Array[s.top]);
if (s.empty()) break;
cout<<p->key<<" ";
s.pop();
}
p=p->rchild;
s.push(p);
}
}
}
12.1-4 對一棵含有n個結點的樹,給出能在θ(n)時間內,完成前序遍歷和後序遍歷的遞迴演算法。
12.1-5 論證:在比較模型中,最壞情況下排序n個元素的時間為Ω(nlgn),則為從任意的n個元素中構造出一棵二叉查詢樹,任何一個基於比較的演算法在最壞情況下,都要花Ω(nlgn)的時間。
構造二叉查詢樹的同時也是對一組雜亂無章的資料排序的過程,而基於比較的排序的時間為Ω(nlgn),所以構造這棵樹也要Ω(nlgn)。
12.2查詢二叉查詢樹
查詢特定關鍵字程式碼:
//遞迴版本的二叉查詢樹查詢函式
struct Tree*TREE_SEARCH(struct Tree*x,int k)
{
if (0==x->key||k==x->key)return x;
if (k<x->key) return TREE_SEARCH(x->lchild,k);
else return TREE_SEARCH(x->rchild,k);
}
//非遞迴版本的二叉查詢樹查詢函式
struct Tree*ITERATIVE_TREE_SEARCH(struct Tree*x,int k)
{
while (x->key!=NULL&&k!=x->key)
{
if (k<x->key)
{
x=x->lchild;
}
else x=x->rchild;
}
return x;
}
查詢最小關鍵字與最大關鍵字程式碼:
//非遞迴版本的查詢二叉查詢樹的最大值
struct Tree*ITERATIVE_TREE_MAXIMUM(struct Tree*x)
{
while (x->rchild->key!=NULL)
{
x=x->rchild;
}
return x;
}
//非遞迴版本的查詢二叉查詢樹的最小值
struct Tree*ITERATIVE_TREE_MINIMUM(struct Tree*x)
{
while (x->lchild->key!=NULL)
{
x=x->lchild;
}
return x;
}
查詢某個關鍵字的後繼
//查詢二叉查詢樹的後繼
struct Tree*TREE_SUCCESSOR(struct Tree*x)
{
if (x->rchild->key!=NULL)
{
return TREE_MINIMUM(x->rchild);
}
struct Tree*y=x->parent;
while (y->key!=NULL&&x==y->rchild)
{
x=y;
y=y->parent;
}
return y;
}
定理12.2 對一棵高度為h的二叉查詢樹,動態集合操作SEARCH,MINIMUM,MAXIMUM,SUCCESSOR和PREDECESSOR等的執行時間均為O(h).
練習:
12.2-1 假設在某二叉查詢樹中,有1到1000之間的一些數,現要找出363這個數。下列的結點序列中,哪一個不可能是所檢查的序列?
a)2,252, 401,398,330,344,397,363 b)924,220,911,244,898,258,362,363
c)925,202,911,240,912,245,363 d)2,399,387,219,266,382,381,278,363
e)935,278,347,621,299,392,358,363
c. 911與912不符合二叉查詢樹規則。e.347與299不符合二叉查詢樹規則。
12.2-2 寫出TREE-MINIMUM和TREE-MAXIMUM過程的遞迴版本。
//遞迴版本的查詢二叉查詢樹的最小值
struct Tree*TREE_MINIMUM(struct Tree*x)
{
if (x->lchild->key==NULL)return x;
else return TREE_MINIMUM(x->lchild);
}
//遞迴版本的查詢二叉查詢樹的最大值
struct Tree*TREE_MAXIMUM(struct Tree*x)
{
if (x->rchild->key==NULL)return x;
else return TREE_MINIMUM(x->rchild);
}
12.2-3 寫出TREE-PREDECESSOR過程。
寫出二叉查詢樹前驅之前,我有必要給出帶有父結點的二叉樹建立函式。
void create(struct Tree **p)
{
static struct Tree *p1=NULL;//p1儲存當前結點的父親結點
*p=new struct Tree [LEN];
cin>>(*p)->key;
(*p)->parent=p1;
p1=*p;
if ((*p)->key!=0)
{
create((&(*p)->lchild));
p1=*p;
create((&(*p)->rchild));
}
}
//查詢二叉查詢樹的前驅
struct Tree*TREE_PREDECESSOR(struct Tree*x)
{
if (x->lchild->key!=NULL)
{
return TREE_MAXIMUM(x->lchild);
}
struct Tree*y=x->parent;
while (y->key!=NULL&&x==y->lchild)
{
x=y;
y=y->parent;
}
return y;
}
12.2-4 Bun教授認為他發現了二叉查詢樹的一個重要性質。假設在二叉查詢樹中,對某關鍵字k的查詢在一個葉結點結束,考慮三個集合:A,包含查詢路徑左邊的關鍵字;B,包含查詢路徑上的關鍵字;C,包含查詢路徑右邊的關鍵字。Bunyan教授宣稱,任何三個關鍵字a∈A,b∈B,c∈C,必定滿足a≤b≤c.請給出該命題的一個最小可能的反例。
查詢路徑(1→3→4)∈b ,2∈a,所以a>b。
12.2-5 證明:如果二叉查詢樹中的某個結點有兩個子女,則其後繼沒有左子女,其前驅沒有右子女。
設這個結點為x其左孩子x1右孩子x2,則有key[x1]≤key[x]≤key[x2]。若其後繼x2有左子女x3,則key[x3]≤key[x2],而key[x的右子樹]≥key[x],所以key[x2]≥key[x3]≥key[x],那麼x3就為結點x的後繼而非x2了。前驅沒有右子女也有類似的證明。這裡略過。
12.2-6 考慮一棵其關鍵字各不相同的二叉查詢樹T,證明:如果T中某個結點x的右子樹為空,且x有一個後繼y,那麼y就是x的最低祖先,且其左孩子也是x的祖先。(注意每個節點都是它自己的祖先。)
若x是其後繼y的左孩子,那麼key[x]≤key[y],所以y是x的最低祖先,y的左孩子為x的祖先也就是x本身。
若x是其後繼y的右孩子,那麼key[x]≥key[y],這明顯與後繼定義矛盾,y是x的前驅。所以x不可能是後繼y的右子樹。
若x是y的左孩子y1的右孩子,那麼有key[y1]≤key[x],後繼y的左孩子y1是x的祖先,同時y是x的最低祖先。
12.2-7 對於一棵包含n個結點的二叉查詢樹,其中序遍歷可以這樣來實現:先用TREE-MINIMUM找出樹中的最小元素,然後再呼叫n-1次TREE-SUCCESSOR。證明這個演算法的執行時間為θ(n).
這個圖是任意結點樹中的一個小部分,但是他們的情況是類似的,都是從1-6順序遍歷的。本題的關鍵是這個演算法在一個包含n個結點的二叉查詢樹的n-1個邊上,通過這n-1分支中的每條邊至多2次,也就是說總時間是T<h+2(n-1)=lgn+2(n-1)<cn=>T(n)=O(n)
12.2-8證明:在一棵高度為h的二叉查詢樹中,無論從哪一個結點開始,連續k次呼叫TREE-SUCCESSOR所需時間都是O(k+h).
如果簡單的利用定義:每呼叫一次該函式就需要O(h)時間,呼叫k次就需要O(kh)時間。這種想法是沒有深入分析題目中函式具體呼叫過程。如果明白12.2-7題目核心內容。就知道,除了第一次呼叫該函式需要O(h)時間外,其餘的連續k-1次遍歷了連續的k-1個結點,這k-1個結點有k-2個邊,而每條邊最多遍歷2次。所以總時間T=O(h)+2(k-2)=O(h+k).
12.2-9 設T為一棵其關鍵字均不相同的二叉查詢樹,並設x為一個葉子結點,y為其父結點。證明:key[y]或者是T中大於key[x]的最小關鍵字,或者是T中小於key[x]的最大關鍵字
x是y的左孩子,y是在結點t的左子樹上,如圖a表達的關係可知:key[x]≤key[y]≤key[y的右子樹]≤key[t],可見 "key[y]或者是T中大於key[x]的最小關鍵字" .如圖b表達關係可知:key[t]≤key[x]≤key[y]≤key[y的右子樹]。也得到相同的答案,無非如圖ab兩種情況。
x是y的右孩子,y是在結點t的左子樹上,如圖a表達的關係可知:key[y的左子樹]≤key[y]≤key[x]≤key[t],可見 "或者key[y]是T中小於key[x]的最大關鍵字" .如圖b表達關係可知:key[t]≤key[y的左子樹]≤key[y]≤key[x],也可以得到相同的答案。 無非如圖ab兩種情況。
12.3二叉查詢樹的插入與刪除
書上插入函式程式碼(為了使上下節程式碼的一致性,請注意我最後我特別新增的3行):
void TREE_INSERT(struct Tree*root,struct Tree*z)
{
struct Tree*y=NULL;
struct Tree*x=root;
while (x->key!=NULL)
{
y=x;
if (z->key<x->key)
x=x->lchild;
else x=x->rchild;
}
z->parent=y;
if (y->key==NULL)
{
root=z;
}
else if(z->key<y->key)
{
y->lchild=z;
}
else y->rchild=z;
z->lchild=new struct Tree[LEN];//根據此函式,帶插入的空位一定是空結點,也就是0結點。不存在插入兩個已存在的結點之間的情況。
z->rchild=new struct Tree[LEN];//插入後,一定要記得給其左右孩子賦值為空。
z->lchild->key=z->rchild->key=0;//所以要設定待插入的結點的左右孩子為0。
}
書上刪除程式碼(第三版新程式碼,這個版本的刪除程式碼雖然比上個版本複雜些,但是保證了刪除無副作用):
//移植函式 把根為u的子樹移植到根為v的子樹上
void TRANSPLANT(struct Tree*root,struct Tree*u,struct Tree*v)
{
if (u->parent->key==NULL)
{
root=v;
}
else if(u==u->parent->lchild)
{
u->parent->lchild=v;
}
else u->parent->rchild=v;
if (v->key!=NULL)
{
v->parent=u->parent;
}
}
void TREE_DELETE(struct Tree*root,struct Tree*z)
{
if (z->lchild->key==NULL)
{
TRANSPLANT(root,z,z->rchild);
}
else if (z->rchild->key==NULL)
{
TRANSPLANT(root,z,z->lchild);
}
else
{
struct Tree*y=TREE_MINIMUM(z->rchild);
if (y->parent!=z)
{
TRANSPLANT(root,y,y->rchild);
y->rchild=z->rchild;
y->rchild->parent=y;
}
TRANSPLANT(root,z,y);
y->lchild=z->lchild;
y->lchild->parent=y;
}
}
練習:
12.3-1 給出過程TREE-INSERT的一個遞迴版本。
//遞迴的插入函式
void TREE_INSERT(struct Tree*x,struct Tree*z)
{
static struct Tree*y=NULL;
static struct Tree*root=x;
if (x->key!=NULL)
{
y=x;
if (z->key<x->key)
TREE_INSERT(x->lchild,z);
else
TREE_INSERT(x->rchild,z);
}
else
{
z->parent=y;
if (y->key==NULL)
{
root=z;
}
else if(z->key<y->key)
{
y->lchild=z;
}
else y->rchild=z;
z->lchild=new struct Tree[LEN];//根據此函式,帶插入的空位一定是空結點,也就是0結點。不存在插入兩個已存在的結點之間的情況。
z->rchild=new struct Tree[LEN];
z->lchild->key=z->rchild->key=0;//所以要設定待插入的結點的左右孩子為0,
}
}
12.3-2 假設我們通過反覆插入不同的關鍵字的做法來構造一棵二叉查詢樹。論證:為在樹中查詢一個關鍵字,所檢查的結點數等於插入該關鍵字時所檢查的結點數加1.
從插入和查詢函式的while迴圈遍歷結構來看是完全一樣的,區別就是查詢函式遍歷到關鍵字位置後就結束了,而插入函式遍歷到待插入關鍵字的位置前一個位置便停止遍歷轉而進行插入工作,所以比查詢函式少遍歷一個結點。
12.3-3 可以這樣來對n個數進行排序;先構造一棵包含這些數的二叉查詢樹(重複應用TREE-INSERT來逐個地插入這些數),然後按中序遍歷來輸出這些數。這個排序演算法的最壞情況和最好情況執行時間怎麼樣?
最壞情況是樹的高度為θ(n),此時T(n)=θ(1+2+...+n)+θ(n)=θ(n²) 最好情況是樹的高度為θ(h),(h<n),此時T(n)=θ(lg1+lg2+...lgn)+θ(n)=lgn!+θ(n)=θ(nlgn)
12.3-4刪除操作可交換的嗎?(也就是說,先刪除x,再刪除y的二叉查詢樹與先刪除y再刪除x的一樣)說明為什麼是,或者給出一個反例。
不能交換,若交換刪除後,雖然按中序輸出順序一樣,但是樹的內部結構可能不一樣了。以書中圖12-3為例:我們首先插入結點8,有9->left=8;先刪除2後再刪除5,結果為12->left=8,9->left=8,9->right=0,其他結構相同。而先刪5再刪2,12->left=9,8->left=0,8->right=9; 可見內部結構不同中序輸出順序相同。
12.3-5假設為每個節點換一種設計,屬性x.p指向x的雙親,屬性x.succ指向x的後繼。試給出使用這種表示法的二叉搜尋樹T上SEARCH,INSERT和DELETE操作的虛擬碼。這些虛擬碼應在O(h)時間內執行完,其中h為T的高度。(提示:應該設計一個返回某個結點的雙親的子過程。)
似乎感覺,這種表示法和左右子樹表示法類似。二叉查詢樹左子樹相當於它的雙親,而右子樹相當於它的後繼。我的思路就這麼多。如果有更好思路的可以在這裡12.3-5回覆我
12.3-6當TREE-DELETE中的結點z有兩個子結點時,可以將其前驅(而不是後繼)拼接掉。有些人提出一種公平的策略,即為前驅和後繼結點賦予相同的優先順序,從而可以得到更好的經驗效能。那麼,應如何修改TREE-DELETE來實現這樣一種公平策略?
可以等概率隨機選取前驅或後繼結點以達到相同的優先順序。以下是程式碼:
//移植函式 把根為u的子樹移植到根為v的子樹上
void TRANSPLANT(struct Tree*&root,struct Tree*u,struct Tree*v)
{
if (u->parent->key==NULL)
{
root=v;
}
else if(u==u->parent->lchild)
{
u->parent->lchild=v;
}
else u->parent->rchild=v;
if (v->key!=NULL)
{
v->parent=u->parent;
}
}
//將後繼拼接掉的刪除函式
void TREE_DELETE_SUCCESSOR(struct Tree*&root,struct Tree*z)
{
if (z->lchild->key==NULL)
{
TRANSPLANT(root,z,z->rchild);
}
else if (z->rchild->key==NULL)
{
TRANSPLANT(root,z,z->lchild);
}
else
{
struct Tree*y=TREE_MINIMUM(z->rchild);
if (y->parent!=z)
{
TRANSPLANT(root,y,y->rchild);
y->rchild=z->rchild;
y->rchild->parent=y;
}
TRANSPLANT(root,z,y);
y->lchild=z->lchild;
y->lchild->parent=y;
}
}
//將前驅拼接掉的刪除函式
void TREE_DELETE_PREDECESSOR(struct Tree*&root,struct Tree*z)
{
if (z->lchild->key==NULL)
{
TRANSPLANT(root,z,z->rchild);
}
else if (z->rchild->key==NULL)
{
TRANSPLANT(root,z,z->lchild);
}
else
{
struct Tree*y=TREE_MAXIMUM(z->lchild);
if (y->parent!=z)
{
TRANSPLANT(root,y,y->lchild);
y->lchild=z->lchild;
y->lchild->parent=y;
}
TRANSPLANT(root,z,y);
y->rchild=z->rchild;
y->rchild->parent=y;
}
}
//改進的刪除函式
void TREE_DELETE(struct Tree*&root,struct Tree*z)
{
srand( (unsigned)time( NULL ) );
int priority=rand()%2;
if (priority==0)
{
TREE_DELETE_PREDECESSOR(root,z);//若優先順序為0,則把待刪除的z的前驅移出原來的位置進行拼接,替換原來的z。
}
else TREE_DELETE_SUCCESSOR(root,z);//若優先順序為1,則把待刪除的z的後繼移出原來的位置進行拼接,替換原來的z。
}//優先順序priority是等概率出現的,所以有相同的優先順序。
12.4 隨機構建二叉搜尋樹
二叉查詢樹各種操作時間均是O(h),構建二叉查詢樹時,一般只用插入函式,這樣便於分析,如果按嚴格增長順序插入,那麼構造出來的樹就是一個高度為n-1的鏈。另一方面練習B.5-4說明了h≥lgn.這裡我特別證明下。
證明:一個有n個結點的非空二叉樹的高度至少為lgn.
對於一個高度為h的二叉樹總結點數至多為n≤2^h-1(等於的情況就是完全二叉樹),所以給這個不等式適當變型得:h≥lg(n+1)≥lgn,所以對於n個結點的數高度至少為lgn 雖然沒有用歸納法,但是這種方法感覺簡單易懂。
定理12.4 一棵有n個不同關鍵字的隨機構建二叉搜尋樹的期望高度為O(lgn).
練習:
12.4-1 證明等式
12.4-2 請描述這樣的一棵二叉查詢樹:其中每個結點的平均深度為θ(lgn),但樹的深度為ω(lgn).對於一棵含n個結點的二叉查詢樹,如果其中每個結點的平均深度為θ(lgn),給出其高度的一個漸進上界。
問題一:猜想這樣一棵樹有個結點的完全二叉樹,其中某一個葉子結點延伸一條含有個結點的直線鏈。
設完全二叉樹部分高度h,則這棵二叉樹的高度為h=,那麼完全二叉樹部分的所有結點高度O().而另外一條直線鏈
所有結點高度為O().這樣n個結點二叉查詢樹平均高度為h=
,所以這棵特意構造的二叉樹符合題意的上界。故現在證明是否滿足下界條件。
從完全二叉樹葉子結點到直線鏈最底端的所有個結點高度為h≥,
所以這n個結點平均高度至少為其中x是所有完全二叉樹高度總和
其中A是一個正數。由此可見這n個特別構造好的二叉查詢樹平均高度為θ(lgn).
再來看這個特別構造的樹的高度為h=+≥ω(lgn)。問題一得證!
問題二:首先證明:對於一個平均結點高度為θ(lgn)二叉查詢樹來說,樹高的漸近上界為O(√nlgn).
證明:對於某棵含有n個結點的二叉查詢樹,我們定義某個葉子結點x(其深度為h)的路徑上的所有結點深度d依次是0,1,...h,而其它非此路徑上的結點深度為y.那麼所有結點平均深度為h'=1/n(∑(d=0~h)+∑y)≥1/n(∑(d=0~h)=(1/n)θ(h^2),反證法:若h!=O(√nlgn)這個緊確上界,則當h=ω(√nlgn)或h=o(√nlgn)時,即(1/n)θ(h^2)=ω(lgn)或o(lgn)得到的卻是非緊確界,這與樹的平均高度為θ(lgn)這個緊確界不符,所以樹高的漸進上界為O(√nlgn).
12.4-3 說明含有n個關鍵字的隨機選擇二叉搜尋樹的概念,這裡每一棵n個結點的二叉搜尋樹是等可能地被選擇,不同於本節中給出的隨機構建二叉搜尋樹的概念。(提示:當n=3時,列出所有可能)。
如果你認真看這6張圖,可以發現第4和第5張圖是一樣的。我要說明的是構建過程這兩張圖順序是不一樣的,分別是按2,1,3順序與2,3,1的順序插入,所以構建二叉搜尋樹是6種可能。如果選擇這6張圖中第4和第5張圖結構是一樣的,所以選擇方式只有5種。故選擇和構建是不同的概念。其實n>3時,也會出現類似的情況。這裡就不多說了。
12.4-4 證明:f(x)=2^x是凹函式。(注意:原版書中的convex是凸,但是國外的語意和國內相反,所以翻譯成凹才是符合題意。)
證明:根據凹函式定義:對於任意x,y,λ∈(0,1),有λ2^x+(1-λ)2^y≥2^(λx+(1-λ)y)......①這裡有個重要不等式是證明的主要依據。即對於任意a,b,c都有c^a≥c^b+(a-b)(c^b)lnc而這個又根據e^x≥1+x而來。這裡需要簡要的證明下:設x=(a-b)lnc帶入e^((a-b)lnc)≥1+(a-b)lnc 即c^(a-b)≥1+(a-b)lnc 兩邊都乘以c^b即得證!這樣 我用2代替c,a代替x,b代替z,其中z=λx+(1-λ)y 便得:2^x≥2^z+(x-z)(2^z)ln2....②同理,a->y 其他一樣,得2^y≥2^z+(y-z)(2^z)ln2....③ 把②和③式帶入①式得:λ2^x+(1-λ)2^y≥λ(2^z+(x-z)(2^z)ln2)+(1-λ)(2^z+(y-z)(2^z)ln2)=(λ+1-λ)2^z+(λ(x-z)+(1-λ)(y-z))(2^z)ln2 將z用x,y表示式帶入(λ(x-z)+(1-λ)(y-z))=0,所以λ2^x+(1-λ)2^y≥2^z=2^(λx+(1-λ)y),得證!
12.4-5 現對n個輸入數呼叫RANDOMIZED-QUICKSORT.證明:對任何常數k>0,輸入數的所有n!種排列中,除了其中的O(1/n^k)種排列之外,都有O(nlgn)的執行時間。 不懂。 12-1 (帶有相同關鍵字的二叉查詢樹) 相同關鍵字給二叉查詢樹的實現帶來了問題。 a.當用TREE-INSERT將n個其中帶有相同關鍵字的資料插入到一棵初始為空的二叉查詢樹中,其漸進效能是多少? 由於遇到相同關鍵字就向右遍歷樹,所以當樹中關鍵字全一樣或資料有序時,最壞時間發生了,時間為O(n). 建議通過在第5行之前測試z.key=x.key和在第11行之前測試z.key=x.key的方法,來對TREE-INSERT進行改進。如果相等,根據下面的策略之一來實現。對於每個策略,得到將n個其中帶有相同關鍵字的資料插入到一棵初始為空的二叉搜尋樹中的漸近效能。(對第5行描述的策略是比較z和x的關鍵字,用於第11行的策略是用y代替x。) b.在結點x設定一個布林標誌x.b,並根據x.b的值,置x為x.left或x.right。當插入一個與x關鍵字相同的結點時,每次訪問x時交替地置x.b為FALSE或TRUE。 這種方法無論是平均效能還是最壞效能都是O(nlgn),因為將結點交替插入左右子樹使樹變得更加平衡,平均樹高就是lgn。以下是程式碼://思考題12-1 具有相同關鍵字的BST_b,d種處理方法。
#include <iostream>
#include <time.h>
using namespace std;
#define LEN sizeof(struct Tree)
#define m 10//棧的最大容納空間可以調整
struct Tree
{
int key;
struct Tree*lchild;
struct Tree*rchild;
struct Tree*parent;
};
//非遞迴的插入函式處理含有相同關鍵字的BST_b
void ITERATIVE_TREE_INSERT_b(struct Tree*root,struct Tree*z)
{//這種對相同關鍵字處理的結果是O(nlgn),即使n個關鍵字都一樣執行時間也是O(nlgn)。
struct Tree*y=NULL;
struct Tree*x=root;
static bool blag=false,BLAG=false;
while (x)
{
y=x;
if (z->key==x->key)
{
if (!blag)
{
x=x->lchild;
}
else
{
x=x->rchild;
}
blag=!blag;
continue;
}
if (z->key<x->key)
x=x->lchild;
else x=x->rchild;
}
z->parent=y;
if (y==NULL)
{
root=z;
}
else if(z->key==y->key)
{
if (!BLAG)
{
y->lchild=z;
}
else
{
y->rchild=z;
}
BLAG=!BLAG;
}
else if(z->key<y->key)
{
y->lchild=z;
}
else y->rchild=z;
z->lchild=z->rchild=NULL;//所以要設定待插入的結點的左右孩子為0,
}
//非遞迴的插入函式處理含有相同關鍵字的BST_d
void ITERATIVE_TREE_INSERT_d(struct Tree*root,struct Tree*z)
{//這種對相同關鍵字處理的結果是平均情況O(nlgn),既然是隨機選擇左右孩子,那麼可能很不湊巧都選擇了左孩子或者右孩子,最壞為O(n^2)
struct Tree*y=NULL;
struct Tree*x=root;
srand( (unsigned)time( NULL ) );
while (x)
{
y=x;
int flag=rand()%2;
if (z->key==x->key)
{
if (!flag)
{
x=x->lchild;
}
else
{
x=x->rchild;
}
continue;
}
if (z->key<x->key)
x=x->lchild;
else x=x->rchild;
}
z->parent=y;
if (y==NULL)
{
root=z;
}
else if(z->key==y->key)
{
int BLAG=rand()%2;
if (!BLAG)
{
if (!y->lchild->key)
{
y->lchild=z;
}
else y->rchild=z;
}
else
{
if (!y->rchild->key)
{
y->rchild=z;
}
else y->lchild=z;
}
}
else if(z->key<y->key)
{
y->lchild=z;
}
else y->rchild=z;
z->lchild=z->rchild=NULL;//所以要設定待插入的結點的左右孩子為0,
}
//中序遍歷
void InOderTraverse(struct Tree *p)
{
if (p)
{
InOderTraverse(p->lchild);
cout<<p->key<<" ";
InOderTraverse(p->rchild);
}
}
void main()
{
struct Tree*p=NULL;
struct Tree*root=new struct Tree[LEN];
cin>>root->key;
ITERATIVE_TREE_INSERT_b(p,root);
int i=0;
while (i!=3)
{
struct Tree*z=new struct Tree[LEN];
cin>>z->key;
ITERATIVE_TREE_INSERT_b(root,z);
i++;
}
InOderTraverse(root);
}
c.在x處設定一個與x關鍵字相同的結點列表,並將z插入到該列表中。
說白了就是給相同關鍵字處設定一個連結串列。這種方法平均效能O(nlgn),因為遇到相同關鍵字只用O(1)時間就能插入連結串列中,我們用的是頭插法。最佳效能卻是O(n),因為當所有結點關鍵字都一樣時,就相當於鏈成一條連結串列,而每次插入是O(1)時間,n次就是O(n)。最壞效能和有沒有相同關鍵字沒關係,只與資料是否有序有關,其時間為O(n^2).
//思考題12-1 具有相同關鍵字的BST_c種處理方法。
/*#include <iostream>
#include <time.h>
using namespace std;
#define LEN sizeof(struct Tree)
#define m 10//棧的最大容納空間可以調整
struct Tree
{
int key;
struct Tree*lchild;
struct Tree*rchild;
struct Tree*parent;
struct Tree*next;
};
//非遞迴的插入函式處理含有相同關鍵字的BST_c
void ITERATIVE_TREE_INSERT_c(struct Tree*root,struct Tree*z)
{//這種對相同關鍵字處理的結果是O(nlgn),即使n個關鍵字都一樣執行時間也是O(nlgn),因為相同關鍵字插入到列表中只需要O(1)時間。
struct Tree*y=NULL;
struct Tree*x=root;
while (x)//&&x->key!=NULL
{
y=x;
if (z->key==x->key)
{
break;
}
if (z->key<x->key)
x=x->lchild;
else x=x->rchild;
}
z->parent=y;
z->next=NULL;
if (!y)//||y->key==NULL
{
root=z;
}
else if(z->key==y->key)
{
if (y->next)
{
y->next->parent=z;
}
z->next=y->next;//插入到列表用頭插法。
y->next=z;
}
else if(z->key<y->key)
{
y->lchild=z;
}
else y->rchild=z;
z->lchild=z->rchild=NULL;//所以要設定待插入的結點的左右孩子為空,
}
//中序遍歷
void InOderTraverse(struct Tree *p)
{
if (p)//p->key
{
InOderTraverse(p->lchild);
struct Tree *p1=p;
while (p)
{
cout<<p->key<<" ";
p=p->next;
}
p=p1;
InOderTraverse(p->rchild);
}
}
void main()
{
struct Tree*p=NULL;
struct Tree*root=new struct Tree[LEN];
cin>>root->key;
root->next=NULL;
ITERATIVE_TREE_INSERT_c(p,root);
int i=0;
while (i!=13)
{
struct Tree*z=new struct Tree[LEN];
cin>>z->key;
ITERATIVE_TREE_INSERT_c(root,z);
i++;
}
InOderTraverse(root);
}
d.隨機地置x為x.left或x.right.(給出最壞情況效能,並非形式地匯出期望執行時間。)
隨機設定選取左右孩子,我們可以把其設定Rand%2,使其以等概率的選擇左右子樹,當然也可以以其它概率來選擇左右子樹。等概率選擇的話,執行時間類似策略b。而以其他概率的話,比如最壞時,全部選擇右子樹或者全部選擇左子樹,這樣插入n個相同關鍵字,其執行時間就為O(n^2).程式碼在上面的策略b已經給出。
//思考題12-2基數樹
#include <iostream>
#include <string>
#include <time.h>
using namespace std;
#define LEN sizeof(struct Tree)
#define m 10//棧的最大容納空間可以調整
struct Tree
{
string key;
bool flag;//false代表不是使用者輸入結點,true代表是使用者輸入結點。
struct Tree*lchild;
struct Tree*rchild;
struct Tree*parent;//其實給基數樹排序不需要父結點,這個結點適用於刪除以及查詢前驅和後繼。
};
//非遞迴的插入函式
void ITERATIVE_TREE_INSERT(struct Tree*&root,struct Tree*z)
{
struct Tree*x=root;
int i=0;
while (i!=z->key.size()-1)
{//這個迴圈是找到待插入位置的父結點
if (z->key[i++]=='1')
{
if (!x->rchild)//x的右孩子為空,那麼給其增加一個結點
{
x->rchild=new struct Tree[LEN];
x->rchild->key=z->key.substr(0,i);
x->rchild->flag=false;//這個結點是找到待插入結點必須經過的結點,但不是使用者輸入的結點,所以它的哨兵為false
x=x->rchild;
x->lchild=x->rchild=NULL;
}
else x=x->rchild;
}
else
{
if (!x->lchild)
{
x->lchild=new struct Tree[LEN];
x->lchild->key=z->key.substr(0,i);
x->lchild->flag=false;
x=x->lchild;
x->lchild=x->rchild=NULL;
}
else x=x->lchild;
}
}
if (x==NULL)//這組if-else結構是找到位置進行插入的。
{
root=z;
}
else if ((z->key[i]=='1'&&(!x->rchild))||(z->key[i]=='0'&&(!x->lchild)))
{//如果待插入結點位置沒有資料,那麼將
z->parent=x;
z->flag=true;
if(z->key[i]=='1')
{
x->rchild=z;
}
else x->lchild=z;
}
else
{
if (z->key[i]=='1'&&x->rchild)
{
x->rchild->flag=true;
}
if (z->key[i]=='0'&&x->lchild)
{
x->lchild->flag=true;
}
}
z->lchild=z->rchild=NULL;
}//從找到待插入的父結點位置到插入結點,總的執行步驟是該串的長度,進行N次插入其執行時間就是總串長度O(n).
//前序遍歷
void InOderTraverse(struct Tree *p)
{
static struct Tree *p1=p;
if (p)//p->key
{
if (p!=p1&&p->flag)
{
cout<<p->key<<" ";
}
InOderTraverse(p->lchild);
InOderTraverse(p->rchild);
}
}
void main()
{
struct Tree*p=new struct Tree[LEN];
p->key="0";
p->parent=NULL;
struct Tree*root=NULL;
ITERATIVE_TREE_INSERT(root,p);
int i=0;
while (i!=5)
{
struct Tree*z=new struct Tree[LEN];
cin>>z->key;
ITERATIVE_TREE_INSERT(root,z);
i++;
}
InOderTraverse(root);
}
f)我們在插入二叉搜尋樹和快速排序中刻畫出一個演算法。注意到一個元素x被選作樹T的樹根。所有元素在x之後被插入到樹T將與x進行比較。類似地,注意到一個元素y被選作陣列S,所有在S中的其他元素將被與y進行比較。因而,快速排序執行的比較是和插入到二叉查詢樹時進行的比較次序是一樣的。 以下是程式碼:(該程式碼所給定的無序陣列A,與按陣列B順序插入二叉查詢樹的元素比較次序是一樣的。) 其二叉查詢樹插入過程圖為:
//12-3快排與二叉查詢樹比較順序不同 比較元素相同
#include <iostream>
#include <time.h>
using namespace std;
#define LEN sizeof(struct Tree)
#define m 10//棧的最大容納空間可以調整
struct Tree
{
int key;
struct Tree*lchild;
struct Tree*rchild;
struct Tree*parent;
};
//非遞迴的插入函式
void ITERATIVE_TREE_INSERT(struct Tree*&root,struct Tree*z)
{
struct Tree*y=NULL;
struct Tree*x=root;
while (x)
{
y=x;
if (z->key<x->key)
x=x->lchild;
else x=x->rchild;
}
z->parent=y;
if (y==NULL)
{
root=z;
}
else if(z->key<y->key)
{
y->lchild=z;
}
else y->rchild=z;
z->lchild=z->rchild=NULL;
}
int PARTITION(int A[],int p,int r)
{
int x=A[r];
int i=p-1;
for (int j=p;j<=r-1;j++)//O(n)
{
if (A[j]<=x)
{
i++;
swap(A[i],A[j]);
}
}
swap(A[i+1],A[r]);
return i+1;
}
void QUICKSORT(int A[],int p,int r)
{
if (p<r)//T(n)=2
{
int q=PARTITION(A,p,r);
QUICKSORT(A,p,q-1);
QUICKSORT(A,q+1,r);
}
}
//中序遍歷
void InOderTraverse(struct Tree *p)
{
if (p)
{
InOderTraverse(p->lchild);
cout<<p->key<<" ";
InOderTraverse(p->rchild);
}
}
void main()
{
int A[10]={10,5,7,12,3,4,8,11,2,9};
int B[10]={9,2,10,5,11,4,3,7,8,12};
QUICKSORT(A,0,9);
for (int i=0;i<10;i++)
{
cout<<A[i]<<" ";
}
cout<<endl;
i=0;
struct Tree*root=NULL;
while (i!=10)
{
struct Tree*z=new struct Tree[LEN];
z->key=B[i++];
ITERATIVE_TREE_INSERT(root,z);
}
InOderTraverse(root);
}
<img width="1000" height="350" alt="" src="https://img-blog.csdn.net/20140618231216000?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvejg0NjE2OTk1eg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" />