<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
對於符號表,要支援高效的插入操作,就需要一種鏈式結構。但單連結串列無法使用二分查詢,因為二分查詢的高效來自於能夠快速通過索引取得任何子陣列的中間元素,連結串列只能遍歷(詳細描述)。為了將二分查詢的效率和連結串列的靈活性結合,需要更復雜的資料結構:二叉查詢樹。具體來說,就是使用每個結點含有兩個連結的二叉查詢樹來高效地實現符號表。
一棵二叉查詢樹(BST)是一棵二元樹,其中每個結點都含有一個IComparable 型別的鍵以及相關聯的值,且每個結點的鍵都大於其左子樹的任意結點的鍵而小於右子樹的任意結點的鍵。
public class BinarySearchTreesST<Key, Value> : BaseSymbolTables<Key, Value> where Key : IComparable { private Node root;//二元樹根節點 /// <summary> /// 巢狀定義一個私有類表示二叉查詢樹上的一個結點。 /// 每個結點都含有一個鍵,一個值,一條左連線,一條右連線和一個結點計數器。 /// 變數 N 給出了以該結點為根的子樹的結點總數。 /// x.N = Size(x.left) + Size(x.right) + 1; /// </summary> private class Node { public Key key; public Value value; public Node left, right; public int N; public Node(Key key,Value value,int N) { this.key = key; this.value = value; this.N = N; } } public override int Size() { return Size(root); } private int Size(Node x) { if (x == null) return 0; else return x.N; } }
一棵二叉查詢樹代表了一組鍵(及其相應的值)的集合,而一個可以用多棵不同的二叉查詢樹表(起始根結點不同,樹就不同),下面是一種情況。但不管什麼情況的樹,我們將一棵二叉查詢樹的所有鍵投影到一條直線上,一定可以得到一條有序的鍵列。
在符號表中查詢一個鍵可能有兩種結果:命中和未命中。下面的實現演演算法是在二叉查詢樹中查詢一個鍵的遞迴演演算法:如果樹是空的,則查詢未命中,如果被查詢的鍵和根結點的鍵相等,查詢命中,否則就遞迴地在適當地子樹中繼續查詢。如果被查詢的鍵較小就選擇左子樹,較大則選擇右子樹。當找到一個含有被查詢鍵的結點(命中)或者當前子樹變為空(未命中)時這個過程才結束。
public override Value Get(Key key) { return Get(root,key); } /// <summary> /// /// </summary> /// <param name="x">第一個引數是結點(子樹的根結點)</param> /// <param name="key">第二個引數是被查詢的鍵</param> /// <returns></returns> private Value Get(Node x, Key key) { if (x == null) return default(Value); int cmp = key.CompareTo(x.key); if (cmp < 0) return Get(x.left, key); else if (cmp > 0) return Get(x.right, key); else return x.value; }
插入方法和查詢的實現差不多,當查詢一個不存在於樹中的結點並結束於一條空連線時,需要將連線指向一個含有被查詢的鍵的新節點。下面插入方法的邏輯:如果樹是空的,就返回一個含有該鍵值對的新結點賦值給這個空連線;如果被查詢的鍵小於根結點的鍵,繼續在左子樹中插入該鍵,否則就在右子樹中插入。並且需要更新計數器。
public override void Put(Key key, Value value) { root = Put(root,key,value); } private Node Put(Node x, Key key, Value value) { if (x == null) return new Node(key,value,1); int cmp = key.CompareTo(x.key); if (cmp < 0) x.left = Put(x.left, key, value); //注意 x.left = 的意思 else if (cmp > 0) x.right = Put(x.right, key, value);//注意 x.right = else x.value = value; x.N = Size(x.left) + Size(x.right) + 1; return x; }
在查詢和插入的遞迴實現中,可以將遞迴呼叫前的程式碼想象成沿著樹向下走,將遞迴呼叫後的程式碼想象成沿著樹向上爬。對於 Get 方法,這對應著一系列的返回指令。對於 Put 方法,意味著重置搜尋路徑上每個父結點指向子結點的連線,並增加路徑上每個結點中計數器的值。在一棵簡單的二叉查詢樹中,唯一的新連線就是在最底層指向新結點的連線,重置更上層的連線可以通過比較語句來避免。
二叉查詢樹上演演算法的執行時間取決於樹的形狀,而樹的形狀又取決於鍵的插入順序。在最好情況下,一棵含有 N 個結點的樹是完全平衡的,每條空連線和根結點的距離都是 ~lgN 。在最壞情況下,搜尋路徑上有 N 個結點。
對於隨機模型的分析而言,二叉查詢樹和快速排序很相似。根結點就是快速排序中的第一個切分元素,對於其他子樹也同樣使用。
在由 N 個隨機鍵構造的二叉查詢樹,查詢命中的平均所需的比較次數為 ~2lnN (約1.39lgN),插入和查詢未命中平均所需的比較次數也為~2lnN (約1.39lgN)。插入和查詢未命中比查詢命中需要一次額外比較。
由此可知,在二叉查詢樹中查詢比二分查詢的成本高出約 39% ,但是插入操作所需的成本達到了對數界別。
如果根結點的左連線為空,那麼一棵二叉查詢樹中最小的鍵就是根結點;如果左連線非空,那麼樹中的最小鍵就是左子樹中的最小鍵。
public override Key Min() { return Min(root).key; } private Node Min(Node x) { if (x.left == null) return x; return Min(x.left); }
如果給定的鍵 key 小於二叉查詢樹的根結點的鍵,那麼小於等於 key 的最大鍵 Floor( key ) 一定在根結點的左子樹中;如果給定的鍵大於二叉查詢樹的根結點,那麼只有當根節點的右子樹中存在小於等於給定鍵的結點時,小於等於給定鍵的最大鍵才會出現在右子樹中,否則根結點就是小於等於 key 的最大鍵。
/// <summary> /// 向下取整 /// </summary> /// <param name="key"></param> /// <returns></returns> public override Key Floor(Key key) { Node x = Floor(root,key); if (x == null) return default(Key); return x.key; } private Node Floor(Node x, Key key) { if (x == null) return default(Node); int cmp = key.CompareTo(x.key); if (cmp == 0) return x; if (cmp < 0) return Floor(x.left,key); Node t = Floor(x.right,key); if (t != null) return t; else return x; }
我們在二叉查詢樹的每個結點中維護的子樹結點計數器變數 N 就是用來支援此操作的。
如果要找到排名為 k 的鍵(即樹中正好有 k 個小於它的鍵)。如果左子樹中的結點樹 t 大於 k,就繼續(遞迴地)在左子樹中查詢排名為 k 的鍵;如果 t == k,就返回根結點的鍵;如果 t 小於 k,就遞迴地在右子樹中查詢排名為 (k-t-1)的鍵。
public override Key Select(int k) { return Select(root, k).key; } private Node Select(Node x, int k) { if (x == null) return default(Node); int t = Size(x.left); if (t > k) return Select(x.left, k); else if (t < k) return Select(x.right, k - t - 1); else return x; }
排名是選擇操作的逆方法,它會返回給定鍵的排名。它的實現和 Select 類似:如果給定的鍵等於根根結點的鍵,就返回左子樹中的節點數 t ;如果給定的鍵小於根結點,就返回該鍵在左子樹中的排名(遞迴計算);如果給定的鍵大於根結點,就返回 t+1 (根結點)再加上它在右子樹中的排名(遞迴計算)。
public override int Rank(Key key) { return Rank(key,root); } private int Rank(Key key, Node x) { if (x == null) return 0; int cmp = key.CompareTo(x.key); if (cmp < 0) return Rank(key, x.left); else if (cmp > 0) return 1 + Size(x.left) + Rank(key, x.right); else return Size(x.left); }
二叉查詢樹中最難實現的就是刪除操作,我們先實現刪除最小鍵的操作。我們要不斷深入根節點的左子樹直到遇到一個空連線,然後將指向該結點的連線指向該結點的右子樹(只需在 x.left == null 時返回右連結,賦值給上層的左連線)。
/// <summary> /// 注意可考慮刪除根結點 /// </summary> public override void DeleteMin() { root = DeleteMin(root); } private Node DeleteMin(Node x) { if (x.left == null) return x.right; x.left = DeleteMin(x.left); x.N = Size(x.left) + Size(x.right) + 1; return x; }
我們 可以使用上面類似的方法刪除任意只有一個子結點或者沒有子結點的結點,但是無法實現刪除有兩個子結點的結點的方法。我們在刪除 x 結點後用它的右子樹最小結點結點填補它的位置,這樣就可以保證樹的有序性,分四步完成:
public override void Delete(Key key) { root = Delete(root,key); } private Node Delete(Node x, Key key) { if (x == null) return null; int cmp = key.CompareTo(x.key); if (cmp < 0) x.left = Delete(x.left, key); else if (cmp > 0) x.right = Delete(x.right, key); else { if (x.right == null) return x.left; if (x.left == null) return x.right; Node t = x; x = Min(t.right); x.right = DeleteMin(t.right); x.left = t.left; } x.N = Size(x.left) + Size(x.right) + 1; return x; }
該演演算法有個問題,在選擇後繼結點應該是隨機的,應該考慮樹的對成性。
要實現能夠返回給定範圍內鍵的方法 Keys(),需要一個遍歷二叉查詢樹的基本方法,叫做中序遍歷。先找出根結點的左子樹中的符合的所有鍵,然後找出根結點的鍵,最後找出根結點右子樹的符合的所有鍵。
public override IEnumerable<Key> Keys(Key lo, Key hi) { Queue<Key> quene = new Queue<Key>(); Keys(root, quene,lo,hi); return quene; } private void Keys(Node x, Queue<Key> quene, Key lo, Key hi) { if (x == null) return; int cmplo = lo.CompareTo(x.key); int cmphi = hi.CompareTo(x.key); if (cmplo < 0) Keys(x.left,quene,lo,hi); if (cmplo <= 0 && cmphi >= 0) quene.Enqueue(x.key); if (cmphi > 0) Keys(x.right,quene,lo,hi); }
全部程式碼
public class BinarySearchTreesST<Key, Value> : BaseSymbolTables<Key, Value> where Key : IComparable { private Node root;//二元樹根節點 /// <summary> /// 巢狀定義一個私有類表示二叉查詢樹上的一個結點。 /// 每個結點都含有一個鍵,一個值,一條左連線,一條右連線和一個結點計數器。 /// 變數 N 給出了以該結點為根的子樹的結點總數。 /// x.N = Size(x.left) + Size(x.right) + 1; /// </summary> private class Node { public Key key; public Value value; public Node left, right; public int N; public Node(Key key,Value value,int N) { this.key = key; this.value = value; this.N = N; } } public override int Size() { return Size(root); } private int Size(Node x) { if (x == null) return 0; else return x.N; } public override Value Get(Key key) { return Get(root,key); } /// <summary> /// /// </summary> /// <param name="x">第一個引數是結點(子樹的根結點)</param> /// <param name="key">第二個引數是被查詢的鍵</param> /// <returns></returns> private Value Get(Node x, Key key) { if (x == null) return default(Value); int cmp = key.CompareTo(x.key); if (cmp < 0) return Get(x.left, key); else if (cmp > 0) return Get(x.right, key); else return x.value; } public override void Put(Key key, Value value) { root = Put(root,key,value); } private Node Put(Node x, Key key, Value value) { if (x == null) return new Node(key,value,1); int cmp = key.CompareTo(x.key); if (cmp < 0) x.left = Put(x.left, key, value); //注意 x.left = 的意思 else if (cmp > 0) x.right = Put(x.right, key, value);//注意 x.right = else x.value = value; x.N = Size(x.left) + Size(x.right) + 1; return x; } public override Key Min() { return Min(root).key; } private Node Min(Node x) { if (x.left == null) return x; return Min(x.left); } /// <summary> /// 向下取整 /// </summary> /// <param name="key"></param> /// <returns></returns> public override Key Floor(Key key) { Node x = Floor(root,key); if (x == null) return default(Key); return x.key; } private Node Floor(Node x, Key key) { if (x == null) return default(Node); int cmp = key.CompareTo(x.key); if (cmp == 0) return x; if (cmp < 0) return Floor(x.left,key); Node t = Floor(x.right,key); if (t != null) return t; else return x; } public override Key Select(int k) { return Select(root, k).key; } private Node Select(Node x, int k) { if (x == null) return default(Node); int t = Size(x.left); if (t > k) return Select(x.left, k); else if (t < k) return Select(x.right, k - t - 1); else return x; } public override int Rank(Key key) { return Rank(key,root); } private int Rank(Key key, Node x) { if (x == null) return 0; int cmp = key.CompareTo(x.key); if (cmp < 0) return Rank(key, x.left); else if (cmp > 0) return 1 + Size(x.left) + Rank(key, x.right); else return Size(x.left); } /// <summary> /// 注意可考慮刪除根結點 /// </summary> public override void DeleteMin() { root = DeleteMin(root); } private Node DeleteMin(Node x) { if (x.left == null) return x.right; x.left = DeleteMin(x.left); x.N = Size(x.left) + Size(x.right) + 1; return x; } public override void Delete(Key key) { root = Delete(root,key); } private Node Delete(Node x, Key key) { if (x == null) return null; int cmp = key.CompareTo(x.key); if (cmp < 0) x.left = Delete(x.left, key); else if (cmp > 0) x.right = Delete(x.right, key); else { if (x.right == null) return x.left; if (x.left == null) return x.right; Node t = x; x = Min(t.right); x.right = DeleteMin(t.right); x.left = t.left; } x.N = Size(x.left) + Size(x.right) + 1; return x; } public override IEnumerable<Key> Keys(Key lo, Key hi) { Queue<Key> quene = new Queue<Key>(); Keys(root, quene,lo,hi); return quene; } private void Keys(Node x, Queue<Key> quene, Key lo, Key hi) { if (x == null) return; int cmplo = lo.CompareTo(x.key); int cmphi = hi.CompareTo(x.key); if (cmplo < 0) Keys(x.left,quene,lo,hi); if (cmplo <= 0 && cmphi >= 0) quene.Enqueue(x.key); if (cmphi > 0) Keys(x.right,quene,lo,hi); } }
給定一棵樹,樹的高度決定了所有操作在最壞情況下的效能(範圍查詢除外,因為它的額外成本和返回的鍵的數量成正比),成正比。
隨機構造的二叉查詢樹的平均高度為樹中結點數量的對數級別,約為 2.99 lgN 。但如果構造樹的鍵不是隨機的(例如,順序或者倒序),效能會大大降低,後面會講到平衡二叉查詢樹。
演演算法 | 最壞情況下執行時間的增長量級 | 平均情況下的執行時間的增長量級 | 是否支援有序性相關操作 | ||
查詢 | 插入 | 查詢命中 | 插入 | ||
順序查詢(無序連結串列) | N | N | N/2 | N | 否 |
二分查詢(有序陣列) | lgN | N | lgN | N/2 | 是 |
二元樹查詢 | N | N | 1.39lgN | 1.39lgN | 是 |
到此這篇關於C#實現二叉查詢樹的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45