banner
Koresamuel

Koresamuel

Sometimes ever, sometimes never.
github
twitter
email

二叉樹

二叉樹,顧名思義,每個節點最多有兩個 “叉”,也就是兩個子節點,分別是左子節點和右子節點。不過,二叉樹並不要求每個節點都有兩個子節點,有的節點只有左子節點,有的節點只有右子節點。
image

編號 2 的二叉樹中,葉子節點全都在最底層,除了葉子節點之外,每個節點都有左右兩個子節點,這種二叉樹就叫做 滿二叉樹

編號 3 的二叉樹中,葉子節點都在最底下兩層,最後一層的葉子節點都靠左排列,並且除了最後一層,其他層的節點個數都要達到最大,這種二叉樹叫做 完全二叉樹

如何表示(或者存儲)一棵二叉樹?#

一種是基於指針或者引用的二叉 鏈式存儲法,一種是基於數組的 順序存儲法

image

鏈式存儲法。從圖中你應該可以很清楚地看到,每個節點有三個字段,其中一個存儲數據,另外兩個是指向左右子節點的指針。我們只要拎住根節點,就可以通過左右子節點的指針,把整棵樹都串起來。這種存儲方式我們比較常用。大部分二叉樹代碼都是通過這種結構來實現的。

image

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

剛剛舉的例子是一棵完全二叉樹,所以僅僅 “浪費” 了一個下標為 0 的存儲位置。如果是非完全二叉樹,其實會浪費比較多的數組存儲空間。可以看舉的下面這個例子

image

所以,如果某棵二叉樹是一棵完全二叉樹,那用數組存儲無疑是最節省內存的一種方式。因為數組的存儲方式並不需要像鏈式存儲法那樣,要存儲額外的左右子節點的指針。這也是為什麼完全二叉樹會單獨拎出來的原因,也是為什麼完全二叉樹要求最後一層的子節點都靠左的原因。

二叉樹的遍歷#

經典的方法有三種,前序遍歷、中序遍歷和後序遍歷:

image

  • 前序遍歷:對於樹中的任意節點來說,先打印這個節點,然後再打印它的左子樹,最後打印它的右子樹。
  • 中序遍歷:對於樹中的任意節點來說,先打印它的左子樹,然後再打印它本身,最後打印它的右子樹。
  • 後序遍歷:對於樹中的任意節點來說,先打印它的左子樹,然後再打印它的右子樹,最後打印這個節點本身。
    實際上就是遍歷本身節點的順序問題。
// 先序
function preOrder(root) {
  if (root == null) return;
  console.log(root.value);
  preOrder(root.left);
  preOrder(root.right);
}

// 中序
function inOrder(root) {
  if (root == null) return;
  inOrder(root.left);
  console.log(root.value);
  inOrder(root.right);
}

// 後序
function postOrder(root) {
  if (root == null) return;
  postOrder(root.left);
  postOrder(root.right);
  console.log(root.value);
}

二叉查找樹#

也叫二叉搜索樹,二叉查找樹最大的特點就是,支持動態數據集合的快速插入、刪除、查找操作。
二叉查找樹要求,在樹中的任意一個節點,其左子樹中的每個節點的值,都要小於這個節點的值,而右子樹節點的值都大於這個節點的值。

image

1. 二叉查找樹的查找操作#

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

image

function TreeNode(val) {
  this.val = val;
  this.left = this.right = null;
}
function searchBST(root, val) {
  while (root !== null && val !== root.val) {
    root = val < root.val ? root.left : root.right;
  }
  return root;
}

2. 二叉查找樹的插入操作#

如果要插入的數據比節點的數據大,並且節點的右子樹為空,就將新數據直接插到右子節點的位置;如果不為空,就再遞歸遍歷右子樹,查找插入位置。同理,如果要插入的數據比節點數值小,並且節點的左子樹為空,就將新數據插入到左子節點的位置;如果不為空,就再遞歸遍歷左子樹,查找插入位置。

image

function TreeNode(val) {
  this.val = val;
  this.left = this.right = null;
}
function insertBST(root, val) {
  if (root === null) {
    root = new TreeNode(val);
    return root;
  }
  while (root != null) {
    if (val > root.val) {
      if (root.right == null) {
        root.right = new TreeNode(val);
        return root;
      }
      root = root.right;
    } else {
      if (root.left == null) {
        root.left = new TreeNode(val);
        return root;
      }
      root = root.left;
    }
  }
}

3. 二叉查找樹的刪除操作#

針對要刪除節點的子節點個數的不同,我們需要分三種情況來處理。

  • 如果要刪除的節點沒有子節點,我們只需要直接將父節點中,指向要刪除節點的指針置為 null。比如圖中的刪除節點 55。
  • 如果要刪除的節點只有一個子節點(只有左子節點或者右子節點),我們只需要更新父節點中,指向要刪除節點的指針,讓它指向要刪除節點的子節點就可以了。比如圖中的刪除節點 13。
  • 如果要刪除的節點有兩個子節點,這就比較複雜了。我們需要找到這個節點的右子樹中的最小節點,把它替換到要刪除的節點上。然後再刪掉這個最小節點,因為最小節點肯定沒有左子節點(如果有左子結點,那就不是最小節點了),所以,我們可以應用上面兩條規則來刪除這個最小節點。比如圖中的刪除節點 18。

image

function TreeNode(val) {
  this.val = val;
  this.left = this.right = null;
}
function deleteNode(root, key) {
  if (!root) return null;
  if (root.val > key) {
    root.left = deleteNode(root.left, key);
    return root;
  }
  if (root.val < key) {
    root.right = deleteNode(root.right, key);
    return root;
  }
  if (root.val === key) {
    if (!root.left && !root.right) {
      return null;
    }
    if (!root.right) {
      return root.left;
    }
    if (!root.left) {
      return root.right;
    }
    let p = root.right;
    while (p.left) {
      p = p.left;
    }
    root.right = deleteNode(root.right, p.val);
    p.right = root.right;
    p.left = root.left;
    return p;
  }
};

平衡二叉查找樹#

平衡二叉樹:二叉樹中任意一個節點的左右子樹的高度相差不能大於 1。從這個定義來看,完全二叉樹、滿二叉樹其實都是平衡二叉樹,但是非完全二叉樹也有可能是平衡二叉樹。

image

平衡二叉查找樹不僅滿足上面平衡二叉樹的定義,還滿足二叉查找樹的特點。最先被發明的平衡二叉查找樹是AVL樹,它嚴格符合平衡二叉查找樹的定義,即任何節點的左右子樹高度相差不超過 1,是一種高度平衡的二叉查找樹。
發明平衡二叉查找樹這類數據結構的初衷是,解決普通二叉查找樹在頻繁的插入、刪除等動態更新的情況下,出現時間複雜度退化的問題。
所以,平衡二叉查找樹中 “平衡” 的意思,其實就是讓整棵樹左右看起來比較 “對稱”、比較 “平衡”,不要出現左子樹很高、右子樹很矮的情況。這樣就能讓整棵樹的高度相對來說低一些,相應的插入、刪除、查找等操作的效率高一些。

紅黑樹#

紅黑樹的英文是 “Red-Black Tree”,簡稱 R-B Tree。它是一種不嚴格的平衡二叉查找樹。紅黑樹中的節點,一類被標記為黑色,一類被標記為紅色。除此之外,一棵紅黑樹還需要滿足這樣幾個要求:

  • 根節點是黑色的
  • 每個葉子節點都是黑色的空節點,也就是說,葉子節點不存儲數據
  • 任何相鄰的節點都不能同時為紅色,也就是說,紅色節點是被黑色節點隔開的
  • 每個節點,從該節點到達其可達葉子節點的所有路徑,都包含相同數目的黑色節點
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。