學習數據結構與算法之二叉搜索樹

本系列全部文章:
第一篇文章:學習數據結構與算法之棧與隊列
第二篇文章:學習數據結構與算法之鏈表
第三篇文章:學習數據結構與算法之集合
第四篇文章:學習數據結構與算法之字典和散列表
第五篇文章:學習數據結構與算法之二叉搜索樹javascript

二叉搜索樹簡介

二叉樹是一種非線性數據結構,其中的每一個元素咱們稱爲節點,二叉樹中每一個節點最多隻能有兩個子節點;沒有父節點的節點稱爲根節點,沒有子節點的節點稱爲葉節點。二叉搜索樹是二叉樹的一種,其特徵是左側子節點存儲比父節點小的值,右側子節點存儲比父節點大(或等於父節點)的值。下圖就是一顆典型的二叉搜索樹:java

二叉搜索樹

二叉搜索樹的實現

二叉搜索樹的節點,咱們用相似雙向鏈表的方式存儲節點(都包含兩個對其餘節點的引用),可是這裏兩個引用指向的分別是左右兩個子節點。node

function BinarySearchTree () {
  // 二叉樹的鍵
  var Node = function (key) {
    // 鍵值
    this.key = key
    // 左節點
    this.left = null
    // 右節點
    this.right = null
  }
  
  // 根節點
  var root = null
}

二叉搜索樹須要實現如下方法:git

  • insert(key):向樹中插入一個新的鍵
  • search(key):在樹中查找一個鍵,若是節點存在返回tue,不然返回false
  • inOrderTraverse:經過中序遍歷方式遍歷全部節點
  • preOrderTraverse:經過先序遍歷方式遍歷節點
  • postOrderTraverse:經過後序遍歷方式遍歷全部節點
  • min:返回樹中最小的值
  • max:返回樹中最大的值
  • remove(key):從樹中移除某個鍵

注意:本文中不少地方使用了遞歸的方法,若是不瞭解遞歸,能夠先看看這個知乎問題-遞歸github

實現insert

// 用於插入節點
var insertNode = function (node, newNode) {
  // 在二叉搜索樹中,比父節點小的值存在左側節點,大於等於父節點的存在右側節點
  // 若要插入一個節點(根節點已存在),首先與根節點比大小,若比根節點小則應插入根節點的左側
  // 若是左側已存在節點,則遞歸調用函數,將左側節點傳入遞歸函數做爲當前節點
  // 若是插入的節點比當前節點大且當前節點右側爲空,則插入右側
  // 若是插入節點比根節點大,原理同上
  if (newNode.key < node.key) {
    if (node.left === null) {
      node.left = newNode
    } else {
      insertNode(node.left, newNode)
    }
  } else {
    if (node.right === null) {
      node.right = newNode
    } else {
      insertNode(node.right, newNode)
    }
  }
}

// 插入
this.insert = function (key) {
  var node = new Node(key)

  if (root === null) {
    root = node
  } else {
    insertNode(root, node)
  }
}

實現search

這裏一樣藉助一個輔助函數使用,輔助函數一樣是用了遞歸,簡單比較輸入的key與當前節點的key,當相等時(意味着找到了目標節點)就返回true;當查找完最末端的節點時,即傳入的node爲null時,就返回false,表示未找到。算法

有人可能會懷疑,這樣真的找到嗎?實際上,因爲二叉搜索樹子節點「左小右大」的性質,一個特定的值在二叉搜索樹中的大體位置是可預見的(即便是插入那個值也不會跑出那個範圍)。因此僅僅經過簡單的比較key就能在某個範圍中找到目標節點,並且這種方法不用遍歷整棵樹去找,很是節省性能。segmentfault

var searchNode = function (node, key) {
  if (node === null) {
    false
  }

  if (key < node.key) {
    return searchNode(node.left, key)
  } else if (key > node.key) {
    return searchNode(node.right, key)
  } else {
    return true
  }
}

// 查找節點
this.search = function (key) {
  return searchNode(root, key)
}

實現中序遍歷

接下來就是三個遍歷方法,先從中序遍歷開始,其做用是按順序(從小到大)訪問整棵樹的全部節點,也就是常見的升序排序。數據結構

其實這三種遍歷並無那麼複雜,簡單地觀察一下回調函數(也就是訪問key)的位置,就能看出來是哪一種排序。函數

var inOrderTraverseNode = function (node, callback) {
  if (node !== null) { // 中止遞歸的條件
    inOrderTraverseNode(node.left, callback)
    callback(node.key)
    inOrderTraverseNode(node.right, callback)
  }
}

// 中序遍歷
this.inOrderTraverse = function (callback) {
  inOrderTraverseNode(root, callback)
}

實現先序遍歷

var preOrderTraverseNode = function (node, callback) {
  if (node !== null) {
    callback(node.key)
    preOrderTraverseNode(node.left, callback)
    preOrderTraverseNode(node.right, callback)
  }
}

// 先序遍歷
this.preOrderTraverse = function (callback) {
  preOrderTraverseNode(root, callback)
}

實現後序遍歷

var postOrderTraverseNode = function (node, callback) {
  if (node !== null) {
    postOrderTraverseNode(node.left, callback)
    postOrderTraverseNode(node.right, callback)
    callback(node.key)
  }
}

// 後序遍歷
this.postOrderTraverse = function (callback) {
  postOrderTraverseNode(root, callback)
}

這裏先停一下:的確看回調函數就能知道這是哪一種遍歷,可是這些函數遞歸理解起來確實有點困難,這裏我建議在重複的大問題面前先拆成小問題來看:post

請看這個最簡單的二叉樹

若是如今先序遍歷這個二叉樹,它的順序應該是M -> H -> Z;中序遍歷的順序是H -> M -> Z;後序遍歷是:H -> Z -> M

那麼再看下面這棵大樹的中序遍歷就會好理解了:先從根節點左側子樹開始遍歷,左側子樹裏面又有小左側子樹,裏面最小的由3,5,6組成的子樹就和上面最簡單的二叉樹同樣了。這時遍歷從3開始,以正常的中序遍歷順序3 -> 5 -> 6。當遍歷完6以後咱們能夠將這個小的子樹當作一個總體,這個總體和上面的父節點7以及右邊的子樹也組成了一個簡單的二叉樹結構,而後正常遍歷7 -> 右側子樹,右側子樹中依舊按照中序遍歷的順序:8 -> 9 -> 10,按此順序不斷遍歷完全部的節點。

實現min和max

這個兩個方法其實挺簡單的,最小的節點就在二叉搜索樹的最左;反之,最大的就在最右。

var minNode = function(node) {
  // 若是node存在,則開始搜索。能避免樹的根節點爲Null的狀況
  if (node) {
    // 只要樹的左側子節點不爲null,則把左子節點賦值給當前節點。
    // 若左子節點爲null,則該節點確定爲最小值。
    while (node && node.left !== null) {
      node = node.left
    }
    return node.key
  }
  return null
}

var maxNode = function(node) {
  if (node) {
    while (node && node.right !== null) {
      node = node.right
    }
    return node.key
  }
  return null
}

// 找到最小節點
this.min = function () {
  return minNode(root)
}

// 找到最大節點
this.max = function () {
  return maxNode(root)
}

實現remove

好了,如今剩下最後一個方法了,先深吸一口氣。。。

接下來實現的方法號稱全書最複雜的方法,鑑於本人目前水平有限,我只能將本身看懂的思路寫出來,若是講得很差你們能夠去看原書《學習JavaScript數據結構與算法》。

下面進入正題:

移除二叉搜索樹中的一個節點須要考慮三種狀況:

  1. 刪除的是葉節點(沒有子節點的節點)
  2. 刪除的節點有一側子節點
  3. 刪除的節點有兩側子節點

仍是老原則,化繁爲簡。

先看第一個比較簡單的:既然它沒有子節點,那就先找到它,再直接將它與父節點的聯繫切斷就好了;

第二個就稍微複雜一點:你得先把它刪掉,而後把它的子節點接到它的父節點上去;

第三個最複雜:你不能直接刪掉它,你應該在它的右側子樹裏面找到最小的那個節點把它替換掉,而後爲防止重複,把替換它的節點刪掉就萬事大吉了。

這裏前兩種狀況都還能理解,因此我只解釋爲何是右側子樹的最小節點。

其實這是爲了防止順序亂掉而作的處理,舉個例子:

仍是以前的那張圖,我要刪掉15這個節點,那麼這時不管是把20仍是13接到根節點11下面都會致使二叉搜索樹「左小右大」的結構大亂(就像曹操若是沒有接班人就死了北方就會大亂),所以最好的辦法是找一個比他大一點的節點來替換它(找一個強一點的接班人坐他的位子維持秩序)。

這裏爲啥是大一點而不是大不少?由於大太多也會致使結構混亂(過於強勢成爲暴君就不給底下人活路了)。因此就選了一個大一點的節點替換到這個位置上來,同時爲防止重複就刪掉了原來的節點(接班人不能身兼兩職因此要辭掉原來的職位)。

說到這裏我就直接貼代碼了,反正如今讓我寫,一時半會是寫不出來的,所以僅供觀摩:

// 這個輔助函數和minNode函數是同樣的,只不過返回值不同
var findMinNode = function (node) {
  if (node === null) {
    while (node && node.left !== null) {
      node = node.left
    }
    return node
  }
  return null
}

var removeNode = function (node, key) {
  if (node === null) {
    return null
  }

  if (key < node.key) {
    node.left = removeNode(node.left, key)
    return node
  } else if (key > node.key) {
    node.right = removeNode(node.right, key)
    return node
  } else {
    // 第一種狀況:刪除葉節點
    if (node.left === null && node.right === null) {
      node = null
      return node
    }

    // 第二種狀況:刪除一側有子節點的節點
    // 將一側的子節點替換爲當前節點
    if (node.left === null) {
      node = node.right
      return node
    } else if (node.right === null) {
      node = node.left
      return node
    }

    // 第三種狀況:刪除兩側都有子節點的節點
    // 找到當前節點右側子樹中最小的那個節點,替換掉要刪除的節點
    // 而後再把右側子樹中最小的節點移除
    var aux = findMinNode(node.right)
    node.key = aux.key
    node.right = removeNode(node.right, aux.key)
    return node
  }
}

// 刪除節點
this.remove = function (key) {
  root = removeNode(root, key)
}

源代碼在此:

二叉搜索樹的實現-源代碼

小結

實現二叉搜索樹花了好長時間,後面的圖也是挺麻煩的數據結構,可是這段時間不停地學習數據結構也是讓本身獲得了很大成長。繼續加油~

相關文章
相關標籤/搜索