數據結構與算法之美專欄學習筆記-二叉樹基礎(下)
二叉查找樹 Binary Search Tree
二叉查找樹的定義
二叉查找樹又稱二叉搜索樹。其要求在二叉樹中的任意一個節點,其左子樹中的每個節點的值,都要小於這個節點的值,而右子樹的節點的值都大於這個節點的值。
二叉查找樹的查找操作
二叉樹類、節點類以及查找方法的代碼實現
先取根節點,如果它等於我們要查找的數據,那就返回。
如果要查找的數據比根節點的值小,那就在左子樹中遞歸查找;
如果要查找的數據比根節點的值大,那就在右子樹中遞歸查找。
public class BinarySearchTree{ //二叉樹節點類 public class Node{//自動屬性:整型數據,左節點、右節點引用域 public int Data { get; set; } public Node Left { get; set; } public Node Right { get; set; } public Node(int data){ Data = data; } } //根結點 private Node tree; public Node Tree{get{return tree;}} //查找方法 publicNode Find(int data){ //從根節點遍歷 Node p = tree; while (p != null){ if (data > p.Data) p = p.Right; else if (data < p.Data) p = p.Left; else return p; } return null; } }
二叉查找樹的插入操作
新插入的數據一般都是在葉子節點上,所以我們只需要從根節點開始,依次比較要插入的數據和節點的大小關系。
如果要插入的數據比節點的數據大,並且節點的右子樹為空,就將新數據直接插到右子節點的位置;如果不為空,就再遞歸遍歷右子樹,查找插入位置。
同理,如果要插入的數據比節點數值小,並且節點的左子樹為空,就將新數據插入到左子節點的位置;如果不為空,就再遞歸遍歷左子樹,查找插入位置。
public void Insert(int data){ //沒有根節點則插入根節點 if (tree == null) { tree = new Node(data); return; } //遍歷根節點 Node p = tree; while (p!=null){ //根據數據大小找到左右子樹對應的葉節點,將數據插入 if (data >= p.Data){ if (p.Right == null){ p.Right = new Node(data); return; } p = p.Right; } else if (data < p.Data){ if (p.Left == null){ p.Left = new Node(data); return; } p = p.Left; } } }
二叉查找樹的刪除操作
第一種情況,刪除的節點沒有子節點直接將其父節點指向置為null。
第二種情況,刪除的節點只有一個子節點,將其父節點指向其子節點。
第三種情況,刪除的節點有兩個子節點,首先找到該節點右子樹中最小的的節點把他替換掉要刪除的節點 然後再刪除這個最小的節點,該節點必定沒有子節點,否則就不是最小的節點了
public void Delete(int data){ Node p = tree;//p指向要刪除的節點 Node pp = null;//記錄p的父節點 while (p != null && p.Data != data){ pp = p; if (data > p.Data) p = p.Right; else p = p.Left; } if (p == null) return; //要刪除的節點有兩個子節點 if (p.Left != null && p.Right != null){ Node minP = p.Right; Node minPP = p; while (minP.Left != null){ minPP = minP; minP = minP.Left; } p.Data = minP.Data; p = minP;//p節點的值更新為最小節點的值,使p指向最小節點,下面就變成了刪除p pp = minPP; } //要刪除的節點有一個子節點,就獲取它的子節點 Node child; if (p.Left != null) child = p.Left; else if (p.Right != null) child = p.Right; 或者沒有節點,子節點設為null else child = null; //要刪除的節點是根節點 if (pp == null) tree = child; //刪去節點,其父節點直接指向其子節點 else if (pp.Left == p) pp.Left = child; else pp.Right = child; }
關於二叉查找樹的刪除操作,還有個非常簡單、取巧的方法,就是單純將要刪除的節點標記為“已刪除”,但是並不真正從樹中將這個節點去掉。
這樣原本刪除的節點還需要存儲在內存中,比較浪費內存空間,但是刪除操作就變得簡單了很多。而且,這種處理方法也並沒有增加插入、查找操作代碼實現的難度。
二叉查找樹的其他操作
二叉查找樹中還可以支持快速地查找最大節點和最小節點、前驅節點和後繼節點。
二叉查找樹除了支持上面幾個操作之外,還有一個重要的特性,就是中序遍歷二叉查找樹,可以輸出有序的數據序列,時間復雜度是 O(n),非常高效。因此,二叉查找樹也叫作二叉排序樹。
下面使用一組測試數據,其數據結構如下所示
測試數據Program類
class Program{ static void Main(string[] args){ BinarySearchTree b = new BinarySearchTree(); //插入數據 b.Insert(12); b.Insert(15); b.Insert(5); b.Insert(6); b.Insert(2); b.Insert(22); b.Insert(32); b.Insert(10); b.Insert(16); b.Insert(13); b.Insert(1); //層序遍歷 b.LevelOrder(); Console.WriteLine(); //刪除節點 b.Delete(15); //再層序遍歷 b.LevelOrder(); Console.WriteLine(); //中序遍歷 b.InOrder(b.Tree); Console.WriteLine("Over"); Console.ReadKey(); } }
層序遍歷和中序遍歷二叉查找樹
//層序遍歷,廣度優先搜索 public void LevelOrder(){ Queue<Node> q = new Queue<Node>(); //根節點入棧,循環遍歷棧,直到棧空 q.Enqueue(tree); while (q.Count != 0){ //打印出棧的數據 Node node = q.Dequeue(); Console.Write(node.Data + ","); //將出棧的節點的子節點入棧 if (node.Left != null) q.Enqueue(node.Left); if (node.Right != null) q.Enqueue(node.Right); } } //中序遍歷,相當於排序 public void InOrder(Node node){ if (node == null) return; InOrder(node.Left); Console.Write(node.Data + ","); InOrder(node.Right); }
輸出結果
12,5,15,2,6,13,22,1,10,16,32, 12,5,16,2,6,13,22,1,10,32, 1,2,5,6,10,12,13,16,22,32,Over
支持重復數據的二叉查找樹
前面的二叉查找樹的操作,我們默認樹中節點存儲的都是數字,針對的都是不存在鍵值相同的情況。
我們可以通過兩種辦法來構建支持重復數據的二叉查找樹。
第一種方法
二叉查找樹中每一個節點不僅會存儲一個數據,因此我們通過鏈表和支持動態擴容的數組等數據結構,把值相同的數據都存儲在同一個節點上。
第二種方法
每個節點仍然只存儲一個數據。在查找插入位置的過程中,如果碰到一個節點的值,與要插入數據的值相同,我們就將這個要插入的數據放到這個節點的右子樹,也就是說,把這個新插入的數據當作大於這個節點的值來處理。
當要查找數據的時候,遇到值相同的節點,我們並不停止查找操作,而是繼續在右子樹中查找,直到遇到葉子節點,才停止。這樣就可以把鍵值等於要查找值的所有節點都找出來。
對於刪除操作,我們也需要先查找到每個要刪除的節點,然後再按前面講的刪除操作的方法,依次刪除。
二叉查找樹的時間復雜度分析
最壞、最好情況
如果根節點的左右子樹極度不平衡,已經退化成了鏈表,所以查找的時間復雜度就變成了 O(n)。
最理想的情況,二叉查找樹是一棵完全二叉樹(或滿二叉樹)。不管操作是插入、刪除還是查找,時間復雜度其實都跟樹的高度成正比,也就是 O(height)。而完全二叉樹的高度小於等於 log2n。
平衡二叉查找樹
我們需要構建一種不管怎麽刪除、插入數據,在任何時候都能保持任意節點左右子樹都比較平衡的二叉查找樹,這就是一種特殊的二叉查找樹,平衡二叉查找樹。
平衡二叉查找樹的高度接近 logn,所以插入、刪除、查找操作的時間復雜度也比較穩定,是O(logn)。
二叉查找樹相比散列表的優勢
散列表中的數據是無序存儲的
如果要輸出有序的數據,需要先進行排序。而對於二叉查找樹來說,我們只需要中序遍歷就可以在 O(n) 的時間復雜度內,輸出有序的數據序列。
散列表擴容耗時很多
而且當遇到散列沖突時,性能不穩定,盡管二叉查找樹的性能不穩定,但是在工程中,我們最常用的平衡二叉查找樹的性能非常穩定,時間復雜度穩定在 O(logn)。
散列表存在哈希沖突
盡管散列表的查找等操作的時間復雜度是常量級的,但因為哈希沖突的存在,這個常量不一定比 logn 小,所以實際的查找速度可能不一定比 O(logn) 快。
加上哈希函數的耗時,也不一定就比平衡二叉查找樹的效率高。
散列表裝載因子不能太大
為了避免過多的散列沖突,散列表裝載因子不能太大,特別是基於開放尋址法解決沖突的散列表,不然會浪費一定的存儲空間。
綜合這幾點,平衡二叉查找樹在某些方面還是優於散列表的,所以,這兩者的存在並不沖突。
思考
如何通過編程,求出一棵給定二叉樹的確切高度呢?
數據結構與算法之美專欄學習筆記-二叉樹基礎(下)