1. 程式人生 > >數據結構與算法之美專欄學習筆記-二叉樹基礎(下)

數據結構與算法之美專欄學習筆記-二叉樹基礎(下)

binary 特性 child 數據大小 del delet 動態擴容 eve 怎麽

二叉查找樹 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;}} //查找方法 public
Node 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) 快。

加上哈希函數的耗時,也不一定就比平衡二叉查找樹的效率高。

散列表裝載因子不能太大

為了避免過多的散列沖突,散列表裝載因子不能太大,特別是基於開放尋址法解決沖突的散列表,不然會浪費一定的存儲空間。

綜合這幾點,平衡二叉查找樹在某些方面還是優於散列表的,所以,這兩者的存在並不沖突。


思考

如何通過編程,求出一棵給定二叉樹的確切高度呢?

數據結構與算法之美專欄學習筆記-二叉樹基礎(下)