1. 程式人生 > 其它 >程式設計師基本功系列5——二叉樹

程式設計師基本功系列5——二叉樹

1、二叉樹基礎

1.1、樹的幾個概念

    節點的高度:節點到葉子節點的最大路徑(邊數)

    節點的深度:根節點到這個節點所經歷的邊數

    節點的層數:節點的深度+1

    樹的高度:根節點的高度

  高度和深度的計數都是從0開始,來看個例子:

      

1.2、滿二叉樹和完全二叉樹

(1)滿二叉樹

    葉子節點全都在最後一層,除葉子節點外,每個節點都有左右兩個節點,這種二叉樹就是滿二叉樹。

      

    其中2號就是滿二叉樹。

(2)完全二叉樹

    葉子節點都在最底下兩層,最後一層的葉子節點都靠左排列,並且除了葉子節點,每個節點都有左右兩個節點,這種二叉樹就是完全二叉樹。上圖中3號就是一棵完全二叉樹。

    再來看幾個完全二叉樹和非完全二叉樹的例子:

      

    和滿二叉樹一樣,除了葉子節點其他節點都達到最大,這一點很好理解,那為什麼完全二叉樹的要求最後一層都靠左排列呢?

    要明白這一點就要看二叉樹的儲存方式?主要有兩種:鏈式儲存和基於陣列的順序儲存。

  鏈式儲存:這是最長用的二叉樹表示方式,每個節點三個欄位,一個儲存資料,另外兩個是指向左右節點的指標。

      

  基於陣列的順序儲存:把根節點儲存在下標 i = 1 的位置,那左子節點儲存在下標 2 * i = 2 的位置,右子節點儲存在 2 * i + 1 = 3 的位置。以此類推,B 節點的左子節點儲存在 2 * i = 2 * 2 = 4 的位置,右子節點儲存在 2 * i + 1 = 2 * 2 + 1 = 5 的位置。

      

    完全二叉樹葉子節點都靠左排列就是為了節省儲存空間。

1.3、二叉樹的遍歷

  經典的三種方式:前序遍歷、中序遍歷、後序遍歷。

    前序遍歷是指,對於樹中的任意節點來說,先列印這個節點,然後再列印它的左子樹,最後列印它的右子樹。

    中序遍歷是指,對於樹中的任意節點來說,先列印它的左子樹,然後再列印它本身,最後列印它的右子樹。

    後序遍歷是指,對於樹中的任意節點來說,先列印它的左子樹,然後再列印它的右子樹,最後列印這個節點本身 

  實際上,前中後序遍歷就是遞迴過程,我們直接看程式碼:

void preOrder(Node* root) {
  if (root == null
) return; print root // 此處為虛擬碼,表示列印root節點 preOrder(root->left); preOrder(root->right); } void inOrder(Node* root) { if (root == null) return; inOrder(root->left); print root // 此處為虛擬碼,表示列印root節點 inOrder(root->right); } void postOrder(Node* root) { if (root == null) return; postOrder(root->left); postOrder(root->right); print root // 此處為虛擬碼,表示列印root節點 }

  三種遍歷,每個節點最多會被訪問兩次,與節點個數正相關,所以時間複雜度是 O(n)。

2、二叉查詢樹

  二叉查詢樹是二叉樹中最常用的一種型別,也就二叉搜尋樹。它支援快速查詢、插入和刪除資料。

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

2.1、二叉查詢樹的操作

(1)二叉查詢樹的查詢操作

  先取根節點,如果它等於我們要查詢的資料,那就返回。如果要查詢的資料比根節點的值小,那就在左子樹中遞迴查詢;如果要查詢的資料比根節點的值大,那就在右子樹中遞迴查詢。

public class BinarySearchTree {
  private Node tree;
public static class Node { private int data; private Node left; private Node right; public Node(int data) { this.data = data; } } 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; } }

(2)二叉查詢樹的插入操作

  二叉查詢樹的插入過程有點類似查詢操作。新插入的資料一般都是在葉子節點上,所以我們只需要從根節點開始,依次比較要插入的資料和節點的大小關係。如果要插入的資料比節點的資料大,並且節點的右子樹為空,就將新資料直接插到右子節點的位置;如果不為空,就再遞迴遍歷右子樹,查詢插入位置。同理,如果要插入的資料比節點數值小,並且節點的左子樹為空,就將新資料插入到左子節點的位置;如果不為空,就再遞迴遍歷左子樹,查詢插入位置。

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

(3)二叉查詢樹的刪除操作

  刪除操作比較複雜,因為可能涉及到重構,根據要刪除節點的子節點個數不同,分三種情況考慮:

    

  •如果要刪除的節點沒有子節點,只需要直接將父節點中,指向要刪除節點的指標置為 null。比如圖中的刪除節點 55。

  •如果要刪除的節點只有一個子節點(只有左子節點或者右子節點),只需要更新父節點中指向要刪除節點的指標,讓它指向要刪除節點的子節點就可以了。比如圖中的刪除節點 13。

  •如果要刪除的節點有兩個子節點,這就比較複雜了。需要找到這個節點的右子樹中的最小節點,把它替換到要刪除的節點上。然後再刪除掉這個最小節點,因為最小節點肯定沒有左子節點(如果有左子結點,那就不是最小節點了),所以,可以應用上面兩條規則來刪除這個最小節點。比如圖中的刪除節點 18。

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

(4)二叉查詢樹的其他操作

  除了插入、刪除、查詢操作之外,二叉查詢樹中還可以支援快速地查詢最大節點和最小節點、前驅節點和後繼節點。

  另外還有一個重要特性,二叉查詢樹的中序遍歷可以輸出有序的資料序列,時間複雜度 O(n),非常高效。

(5)時間複雜度分析

  二叉查詢樹的查詢、插入和刪除的時間複雜度跟跟樹的高度成正比,所以時間複雜度就是 O(height)。在極端情況下,二叉查詢樹會退化成連結串列,時間複雜度就是 O(n),如果二叉樹是平衡二叉樹(3節講),平衡二叉樹的樹高接近 O(logn),

所以對於平衡二叉樹來說時間複雜度就是 O(logn)。

2.2、支援重複資料的二叉查詢樹

  很多時候,在實際的開發中,在二叉查詢樹中儲存的,是一個包含很多欄位的物件。利用物件的某個欄位作為鍵值(key)來構建二叉查詢樹。把物件中的其他欄位叫作衛星資料。

  上面小節中所提到的操作都是針對不存在重複鍵值的情況,那如果存在重複鍵值怎麼解決呢?幾種思路:

(1)二叉樹的每個節點不僅儲存一個數據,可以通過連結串列和支援動態擴容的陣列等資料結構,把相同鍵值的資料儲存在一個節點上。

(2)每個節點還是隻儲存一個數據,如果遇到相同的鍵值,將要插入的資料放到右子樹,也就是把相同的鍵值按大於這個節點的值來處理。

    當要查詢資料的時候,遇到值相同的節點,並不停止查詢操作,而是繼續在右子樹中查詢,直到遇到葉子節點,才停止。這樣就可以把鍵值等於要查詢值的所有節點都找出來。

      

(3)對於要刪除的資料,並不真正刪除,只是將節點標記為刪除狀態,這樣就避免刪除帶來的複雜性,但是比較浪費儲存空間,並且當資料量越大,效能會越低。