論二叉樹的CRUD

往期

前言

本文分爲入門和進階兩部分,建議有經驗的讀者直接閱讀進階部分。node

入門

關於二叉樹的概念

先說幾個定義,看下面這張圖:數組

橙色的圓表明的是根結點,構造一棵樹其實也就是構造一棵樹的根結點。橙色邊框的圓表明葉子結點(也叫外部結點external node), 它沒有子結點。灰色邊框的圓表明內部結點,它至少有一個子結點。bash

值得注意的是,高度和深度都是對於結點而言的,一棵樹的高度和深度其實表明的是根結點的高度和深度數據結構

本文定義根結點的深度爲0, 葉子結點的高度爲0, 根結點的高度等於樹中全部結點深度的最大值。(你也能夠把高度和深度認爲是同一個值,可是本文仍是根據國外教材的定義作出了區分)post

還有個概念叫作度,表明某個結點子結點的數量,葉子結點的度爲0, 對於二叉樹而言每一個結點的度最大值爲2。ui

有兩種典型的二叉樹: 滿(full)二叉樹和徹底(complete)二叉樹。用定義說有點抽象,看下面的圖像, 這就是一棵滿二叉樹:this

這是一棵徹底二叉樹: spa

這棵就不是徹底二叉樹, 把value爲K的結點移到虛線位置纔是徹底二叉樹:3d

建立二叉樹

如上文所說,建立二叉樹其實也就是構造其根結點,下面是結點的數據結構,記住它, 咱們後面會用到。code

function Node(val) {
  this.val = val;
  this.left = null;
  this.right = null;
}
複製代碼

接下來咱們先用層次遍歷(level order)生成的數組來建立二叉樹。

層次遍歷顧名思義就是一層一層去的遍歷樹的全部結點,好比如下的徹底二叉樹:

1
    / \
   2   3
  / \ /
 4  5 6
複製代碼

層次遍歷獲得的數組便爲[1, 2, 3, 4, 5, 6]

如下的二叉樹:

1
    / \
   2   3
  /   /
 4   5
複製代碼

層次遍歷獲得的數組爲[1, 2, 3, 4, null, 5]

若是你之前沒有接觸過遞歸,下面的代碼理解起來可能會有些許困難(不過不要緊,你能夠先繼續讀下去)。核心思想就是先構建好最左邊的分支,再去添加剩餘的結點。

function buildCompleteTree(arr, i, root) {
  if (i < arr.length) {
    root = new Node(arr[i]);
    // 若是難以理解的話,試試打印i的值 :)
    root.left = buildCompleteTree(arr, (i * 2) + 1, root.left);
    root.right = buildCompleteTree(arr, (i * 2) + 2, root.right);
  }
  // 建立二叉樹其實也就是構造其根結點
  return root;
}
複製代碼

讓咱們來試一試

const a = [1, 2, 3, 4, 5, 6];
const r = buildCompleteTree(a, 0, new Node());
// 固然沒有什麼問題
console.assert(r.left.right.val === 5);
複製代碼

遞歸遍歷二叉樹

好了咱們有一棵二叉樹了,接下來咱們試着用不一樣的方式去遍歷它。

二叉樹有三種常見的遍歷方式,分別是前序,中序和後序, 如下面的二叉樹爲例:

1
        /   \
       2     3
      / \   / \
     4  5  6  7
    / \  \   /
   8  9  10 11
複製代碼

讓咱們定義一些操做:

  • 操做A: 訪問某個結點,再訪問該結點的左子結點,而後訪問該子結點的左子結點,直到葉子結點則執行操做B
  • 操做B: 訪問某個結點的父結點的右子結點, 若該父結點的右子結點爲葉子結點,訪問該葉子結點。若該父結點的右子結點不爲葉子結點,則執行操做A

*注: 訪問在這裏表明的是訪問結點並記錄結點的值, 即下文中的result.push

前序遍歷,就是對根結點進行操做A, 獲得的數組就是[1, 2, 4, 8, 9, 5, null, 10, 3, 6, null, null, 7, 11]。

因此遞歸的寫法就是這樣的:

function preorderTraversal(root, result) {
  if (root) {
    result.push(root.val);
    // 訪問結點的左子結點,直到葉子結點
    preorderTraversal(root.left, result);
    preorderTraversal(root.right, result);
  }

  return result;
}
複製代碼

中序遍歷的操做爲:

  • 操做A: 訪問某個結點的最左葉子結點,並執行操做B
  • 操做B: 訪問某個結點的父結點, 若該父結點的右子結點爲葉子結點,訪問該葉子結點。若該父結點的右子結點不爲葉子結點,則執行操做A

中序遍歷,就是對根結點進行操做A, 獲得的數組就是[8, 4, 9, 2, null, 5, 10, 1, null, 6, null, 3, 11, 7]。

function inorderTraversal(root, result) {
  if (root) {
    // 訪問某個結點的最左葉子結點, 而後call stack彈出,訪問該葉子結點的父結點
    inorderTraversal(root.left, result);
    result.push(root.val);
    inorderTraversal(root.right, result);
  }

  return result;
}
複製代碼

後序遍歷的操做爲:

  • 操做A: 訪問某個結點的最左葉子結點,並執行操做B
  • 操做B: 訪問某個結點父結點的右子結點, 若該父結點的右子結點爲葉子結點,訪問該葉子結點,而後訪問該父結點。若該父結點的右子結點不爲葉子結點,則執行操做A

後序遍歷,就是對根結點進行操做A, 獲得的數組就是[8, 9, 4, null, 10, 5, 2, null, null, 6, 11, 7, 3, 1]。

function postorderTraversal(root, result) {
  if (root) {
    postorderTraversal(root.left, result);
    // 訪問某個結點父結點的右子結點
    postorderTraversal(root.right, result);
    result.push(root.val);
  }

  return result;
}
複製代碼

進階

關於二叉搜索樹

因爲單純討論的二叉樹的結點插入和刪除沒有太大的現實意義,筆者仍是決定介紹二叉搜索樹的結點插入和刪除。

二叉搜索樹也叫二叉查找樹, 或是二叉排序樹,簡寫爲BST(Binary Search Tree)

它有個很是重要的性質: 若左子樹不爲空,則左子樹上全部結點的值小於等於其根結點的值; 若右子樹不空,則右子樹上全部結點的值大於等於其根結點的值。例如:

5
    /   \
   3     7
  / \   / \
 2  5  6   8
複製代碼

由這個性質不難推斷出BST中序遍歷獲得的序列是升序的。

BST插入結點

因爲BST的有序性質,咱們只須要給定value就能夠作到插入結點, 以上面的BST爲例,插入value分別爲4, 5, 10的結點

function insertIntoBST(root, val) {
  if (!root) {
    // 找到葉子結點,賦值爲左/右結點
    return new Node(val);
  }

  // 你也能夠把與根結點value相同的結點放在根結點的右子樹
  if (val <= root.val) {
    root.left = insertIntoBST(root.left, val);
  } else if (val > root.val) {
    root.right = insertIntoBST(root.right, val);
  }

  return root;
}
複製代碼
const a = [5, 3, 7, 2, 5, 6, 8];
const r = buildCompleteTree(a, 0, new Node());

insertIntoBST(r, 4);
insertIntoBST(r, 5);
insertIntoBST(r, 10);
// 固然依然是有序的
console.warn(inorderTraversal(r, []));
複製代碼

還有一種寫法雖然增長了一些代碼量, 可是能夠少遞歸一層

function insertNode(root, val) {
  if (val <= root.val) {
    if (!root.left) {
      root.left = new Node(val);
    } else {
      insertNode(root.left, val);
    }
  } else if (val > root.val) {
    if (!root.right) {
      root.right = new Node(val);
    } else {
      insertNode(root.right, val);
    }
  }

  return root;
}
複製代碼

BST刪除結點

刪除結點須要分三種狀況討論:

  • 第一種是刪除的結點爲葉子結點,這種狀況很是簡單,將該葉子結點置爲null便可。
  • 第二種是刪除的結點有左結點/右結點,這種狀況也很是簡單,將刪除的結點置爲該子結點便可。
  • 第三種是刪除的結點有左右兩個子結點,這種狀況就比較複雜了,須要將刪除結點的右子結點的最左葉子結點, 置爲刪除結點的左子結點, 再將刪除結點置爲刪除結點的右子結點。
8
        /   \
       6     12
      / \   / \
     4  7  9  13
    / \  \   /
   2  5  8 13
複製代碼

假如咱們要刪除value爲12的那個結點, 刪除後的BST爲:

8
        /   \
       6     13
      / \    /
     4  7   13
    / \  \  /
   2  5  8  9
複製代碼
function deleteNode(root, val) {
  if (!root) {
    return null;
  }

  if (root.val === val && (!root.left || !root.right)) {
    return root.left || root.right;
  } else if (root.val === val) {
    let temp = root.right;
    while (temp.left) {
      temp = temp.left;
    }
    // 刪除結點的右子結點的最左葉子結點, 置爲刪除結點的左子結點
    temp.left = root.left;

    return root.right;
  }

  if (val < root.val) {
    // 若爲葉子結點,置爲null, 不然置爲給定的結點
    root.left = deleteNode(root.left, val);
  } else if (val > root.val) {
    root.right = deleteNode(root.right, val);
  }

  return root;
}
複製代碼

非遞歸遍歷二叉樹

非遞歸的寫法也是須要掌握的,注意註釋裏的內容!

function preorderTraversal(root) {
  const result = [];
  const stack = [root];
  
  while(stack.length > 0) {
    const current = stack.pop();
    result.push(current.val);
    // 先push右子結點,保證先訪問到左子結點(後push的先pop)
    stack.push(current.right);
    stack.push(current.left);
  }
  
  return result;
}
複製代碼
function inorderTraversal(root) {
  const result = [];
  const stack = [];
  let current = root;

  while (current || stack.length > 0) {
    while (current) {
      stack.push(current);
      current = current.left;
    }

    // 訪問某個結點的最左葉子結點, 而後call stack彈出,訪問該葉子結點的父結點
    current = stack.pop();
    result.push(current.val);
    // 若該父結點的右子結點爲葉子結點,訪問該葉子結點。若該父結點的右子結點不爲葉子結點,則執行操做A
    current = current.right;
  }

  return result;
}
複製代碼
function postorderTraversal(root) {
  const result = [];
  // 保證根結點在結果的末尾
  const stack = [root];

  while (stack.length > 0) {
    const current = stack.pop();
    if (current) {
      result.unshift(current.val);
      stack.push(current.left);
      // 後push右子結點,保證先訪問到左子結點(先push的先unshift)
      stack.push(current.right);
    }
  }

  return result;
}
複製代碼

好了,以上就是關於二叉樹CRUD的所有內容。

相關文章
相關標籤/搜索