數據結構與算法筆記-數據結構-二叉查找樹

[TOC]

數據結構與算法筆記-數據結構-二叉查找樹

二叉查找樹(Binary Search Tree)

二叉查找樹(也叫二叉搜索樹,二叉排序樹), 是二叉樹中最常用的.

二叉查找樹除了支持快速插入,刪除,查找, 還可以支持快速查找最大節點和最小節點,以及前驅節點和後繼節點.

二叉查找樹的中序遍歷,可以輸出有序的數據序列,時間複雜度是O(n),非常高效.所以二叉查找樹也叫作二叉排序樹.

二叉查找樹的樹中任意一個節點,其左子樹中的每個節點的值都要小於這個節點的值,而右子樹節點的值都大於這個節點的值.

如下圖:

img

二叉查找樹的查找操作

二叉查找樹中查找一個節點

  1. 先取根節點,如果它等於我們要查找的數據就返回.
  2. 如果要查找的數據比根節點的值小,就在左子樹中遞歸查找
  3. 如果要查找的數據比根節點的值大,就在右子樹中遞歸查找

如下圖:

img

查找代碼:

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;
    }
  }
}

二叉查找樹的插入操作

插入過程和查找操作類似

  1. 新增數據一般都在葉子節點上,所以只需要從根節點開始逐個對比要插入的數據和節點的大小關係即可.
  2. 如果要插入的數據比節點的數據大
    1. 如果節點的右子樹爲空,則將新數據直接插到右子節點的位置
    2. 如果節點的柚子樹不爲空,則再遞歸遍歷右子樹,繼續查找插入位置
  3. 如果要插入的數據比節點數值小
    1. 如果節點的左子樹爲空,則將新數據插入到左子節點的位置
    2. 如果節點的左子樹不爲空,則再遞歸遍歷左子樹,繼續查找插入位置

img

插入代碼:

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;
    }
  }
}

二叉查找樹的刪除操作

二叉查找樹的刪除操作稍微複雜一點,要根據被刪除節點的子節點個數的不同而分三種情況:

  1. 第一種情況,要刪除的節點沒有子節點,只要將父節點中指向要刪除節點的指針置爲null即可. 如下圖刪除節點55
  2. 第二種情況,要刪除的節點有一個子節點,只要更新父節點中指向要刪除節點的指針,指向要刪除節點的子節點即可. 如下圖刪除節點13
  3. 第三種情況,如果要刪除的節點有兩個子節點
    1. 需要找到這個節點的右子樹中的最小節點, 把它替換到要刪除的節點上.
    2. 再刪除掉這個最小節點, 因爲最小節點肯定沒有左子節點(如果有左子結點,就不是最小節點).
    3. 根據上面兩條規則來刪除這個最小節點. 如下圖刪除節點18

img

刪除代碼:

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;
}

二叉查找樹的刪除操作有個簡單取巧的方法,只需將要刪除的節點標記爲已刪除,而並不真正從樹中將這個節點去掉.

這樣原本刪除的節點還需要存儲在內存中,比較浪費內存空間,不過刪除操作簡單很多,且並沒有增加插入,查找的代碼實現難度.

支持重複數據的二叉查找樹

在上面的案例中,默認節點中存儲的都是數字.但是一般在實際開發中可能存的是一個對象他會有很多字段.

所以一般取出對象中的一個字段作爲鍵值(key)來構建二叉樹,把其他字段叫做衛星數據.

這時候可能會遇到鍵值衝突的情況

  1. 一個節點存多個數據,通過鏈表或支持動態擴容的數組等結構將鍵值衝突的數據存儲在同一個節點上,不再贅述.
  2. 一個節點存一個數據,將要插入的數據放到該節點的右子樹,即將新插入的數據當做大於該節點的值來處理.如下三個圖

插入

img

查找

查找數據時遇到相同的節點並不停止,而是繼續在右子樹中查找直到遇到葉子節點.

這就可以把鍵值相等與要查找的值的所有節點都找到了.

img

刪除

刪除也需要查找所有要刪除的節點,然後按照深度大的先刪除的方法逐個刪除.

img

二叉查找樹的時間複雜度分析

二叉查找樹的插入,刪除,查找的時間複雜度分析

二叉查找樹形態各異,如下圖,爲同一組數據構造了三種二叉查找樹,他們的插入,刪除,查找的效率各不相同

img

上圖的第一種根節點的左右子樹極其不平衡,已退化爲鏈表,查找的時間複雜度爲O(n)

不論是插入,刪除,查找的時間複雜度都和樹的高度成正比,即O(height).也就是求一棵包含n個節點的完全二叉樹的高度.

而樹的高度等於最大層數減一

如上圖包含n個節點的完全二叉樹,

  • 第一層有 1 個節點
  • 第二層有 2 個節點
  • 第三層有 4 個節點
  • 依次類推,下面一層的節點個數是上一層的2倍
  • 第K層的節點個數就是2^(K-1)

但是,完全二叉樹的最後一層的節點個數不遵守這個規律

設最大層爲L,那他包含的節點個數在1到2^(L-1)之間

如果每一層的節點個數求和,總節點個數爲n,有n個節點那麼n滿足下面這個關係

n >= 1+2+4+8+...+2^(L-2)+1
n <= 1+2+4+8+...+2^(L-2)+2^(L-1)

藉助等比數列的求和公式,可以計算出L的範圍是[log2(n+1),log2n +1]

完全二叉樹的層數小於等於log2n +1,完全二叉樹的高度小於等於log2n

極度不平衡的二叉查找樹,查找性能不能滿足需求

所以需要構建一個任何時候,都能保持任意節點左右子樹都較爲平衡的二叉查找樹,也就是平衡二叉查找樹.

平衡二叉查找樹的高度接近logn,所以插入,刪除,查找的時間複雜度也比較穩定,爲O(logn)

對比散列表

散列表的插入,刪除,查找,並且散列表的這些操作時間複雜度是O(1),比二叉查找樹更高效,

而二叉查找樹在比較平衡的情況下,插入,刪除,查找的時間複雜度纔是O(logn),相對散列表並沒有什麼優勢.

之所以還用二叉查找樹是因爲:

  1. 散列表中的數據是無序的,想輸出有序的數據要先排序.而二叉查找樹的中序遍歷,可在O(n)的時間複雜度內輸出有序的數據序列
  2. 散列表擴容耗時,且遇到散列衝突時性能不穩定,即使二叉查找樹的性能不穩定,單最常用的平衡二叉查找樹的性能很穩定,且時間複雜度穩定在O(logn)
  3. 儘管散列表的查找等操作的時間複雜度是常量級的,但哈希衝突時,這個常量不一定比logn小,實際的查找速度並不一定比O(logn)快.加上哈希函數的耗時,未必比平衡二叉查找樹的效率高
  4. 散列表的構造比二叉查找樹複雜,散列函數的設計,衝突解決,擴容,縮容等.平衡二叉查找樹只要考慮平衡性即可,且解決方案成熟,固定。
  5. 爲了避免散列衝突過多,散列表裝載因子不能太大,尤其是基於開放尋址法解決衝突的散列表,不然會浪費存儲空間

所以平衡二叉查找樹在某些方面還是優於散列表的,平時結合需求選擇用就行.

小結

二叉查找樹是一種特殊的二叉樹, 支持快速地查找,插入,刪除操作.

二叉查找樹在沒有重複數據的情況下, 每個節點的值都大於左子樹節點的值,且小於右子樹節點的值.

遇到有重複數據的二叉查找樹

  1. 每個節點存儲多個相同的數據
  2. 每個節點存儲一個數據, 只需要稍加改造原來的插入,刪除,查找操作即可.

二叉查找樹的查找,插入,刪除等的時間複雜度跟樹的高度成正比

有兩個極端情況的時間複雜度分別爲O(n)和O(logn),對應二叉樹退化爲鏈表和完全二叉樹.

爲了避免退化,二叉查找樹有更加複雜的樹,即平衡二叉查找樹,時間複雜度可以做到穩定的O(logn).

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章