數據結構與算法之美-二叉查找樹

前言:本篇文章只是記錄王爭的數據結構與算法之美的學習筆記,寫下來能強迫自己系統的再過一遍,加深理解。這門課以實際開發中遇到的問題為例,引入解決問題涉及到的的數據結構和算法,但不會講的太細,最好結合一本實體書進行學習。

二叉查找樹是一種特殊的二叉樹,支持動態數據集合的快速插入、刪除、查找操作。

1. 二叉查找(搜索)樹

特點:

  • 任意節點的左子樹中節點的值,都要小于這個節點的值
  • 任意節點的右子樹中節點的值,都要大于這個節點的值

如下圖所示:


image.jpeg

2. 二叉查找樹的操作

2.1 查找操作

具體操作:

  • 先取根節點
  • 如果它等于要查找的數據,就返回
  • 如果它比要查找的數據大,就從左子樹中繼續查找
  • 如果它比要查找的數據小,就從右子樹中繼續查找

如下圖所示:


image.jpeg

代碼也很好理解,如下圖所示:


public class BinarySearchTree {
  private Node tree;

  public Node find(int data) {
    Node p = tree;
    while (p != null) {
      if (data < p.data) p = p.left;
      else if (data > p.data) p = p.right;
      else return p;
    }
    return null;
  }

  public static class Node {
    private int data;
    private Node left;
    private Node right;

    public Node(int data) {
      this.data = data;
    }
  }
}
2.2 插入操作

新插入的數據是在葉子節點上,所以要從根節點開始,依次比較要插入的數據和節點的大小關系。

  • 如果要插入的數據比節點的數據大,并且節點的右子樹為空,就插入到右子節點的位置;如果不為空,則遍歷右子樹,查找插入的位置
  • 如果插入的數據比節點數據小,并且節點的左子樹為空,就將數據插入到左子節點的位置;如果不為空,則遍歷左子樹,查找插入位置
image.jpeg

代碼如下:


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 { // data < p.data
      if (p.left == null) {
        p.left = new Node(data);
        return;
      }
      p = p.left;
    }
  }
}
2.3 刪除操作

需要分三種情況去處理:

  • 如果要刪除的節點沒有子節點,只需要將其父節點中指向要刪除節點的指針置為 null
  • 如果要刪除的節點只有一個子節點,只需要將父節點中指向要刪除節點的指針,指向要刪除節點的唯一的子節點
  • 如果要刪除的節點有兩個子節點,需要找到這個節點的右子樹中的最小節點,把它替換到要刪除的節點上,然后再刪除掉這個最小節點,因為最小節點肯定沒有左子節點

如下圖:


image.jpeg

代碼如下:


public void delete(int data) {
  Node p = tree; // p指向要刪除的節點,初始化指向根節點
  Node pp = null; // pp記錄的是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; // minPP表示minP的父節點
    while (minP.left != null) {
      minPP = minP;
      minP = minP.left;
    }
    p.data = minP.data; // 將minP的數據替換到p中
    p = minP; // 下面就變成了刪除minP了
    pp = minPP;
  }

  // 刪除節點是葉子節點或者僅有一個子節點
  Node child; // p的子節點
  if (p.left != null) child = p.left;
  else if (p.right != null) child = p.right;
  else child = null;

  if (pp == null) tree = child; 
  else if (pp.left == p) pp.left = child;
  else pp.right = child;
}
2.4 其他操作

比如:

  • 快速查找最大節點和最小節點
  • 查找前驅節點和后繼節點

中序遍歷二叉查找樹,可以輸出有序的數據序列,時間復雜度是 O(n),所以又稱為二叉排序樹

3. 支持重復數據的二叉查找樹

實際應用中,二叉查找樹中存儲的,是一個包含很多字段的對象。可以利用對象的某個字段值(key)來構建二叉查找樹,對象中其他字段叫作衛星數據

對于包含相同鍵值的數據的二叉查找樹,有兩種解決方法:

  • 可以通過鏈表或者支持動態擴容的數組等結構,將值相同的數據都存儲在同一個節點
  • 每個節點仍然只存儲一個節點,在插入時,將要插入的數據,放到相同數據節點的右子樹,把這個新插入的數據當作大于這個節點的值來處理。
image.jpeg

在查找數據的時候,遇到值相同的節點之后,繼續在其右子樹中查找,直到遇到葉子節點,才停止。這樣就可以把鍵值等于要查找值的所有節點都找出來。

image.png

對于刪除操作,我們也需要先查找到每個要刪除的節點,然后再按前面的刪除操作的方法,依次刪除。


image.jpeg
image.png

4. 時間復雜度分析

對于不同類型的二叉查找樹,執行效率是不同的,比如下圖:

image.jpeg

比如第一種,已經退化成鏈表了,查找的時間復雜度就是 O(n)了。

最理想的情況就是二叉查找樹是一棵完全二叉樹或者滿二叉樹,插入、刪除、查找的時間復雜度都是跟樹的高度成正比,也就是 O(height)

樹的高度 height = 最大層數 - 1,完全二叉樹的層數小于等于 log2n + 1,也就是完全二叉樹的高度小于等于 log2n。平衡二叉查找樹的高度接近logn,所以插入、刪除、查找操作的時間復雜度也比較穩定,是O(logn)

5. 有了散列表之后為啥還需要二叉查找樹?

  • 散列表中的數據是無序的,輸出時需要先排序,對于二叉查找樹,只需要中序遍歷即可
  • 散列表擴容耗時,遇到散列沖突時,性能不穩定,平衡二叉查找樹的性能非常穩定,時間復雜度穩定在 O(logn)。
  • 散列表涉及因素較多
  • 散列表裝載因子不能太大,會浪費內存

6. 練習

  • 二叉查找樹的插入、刪除、查找操作(包含相同數據)
  • 獲取二叉樹的高度
  • 快速查找最大節點和最小節點
  • 查找前驅節點和后繼節點
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容