JS數據結構與算法_樹

上一篇:JS數據結構與算法_集合&字典javascript

1、遞歸

學習樹離不開遞歸。java

1.1 介紹

遞歸是一種解決問題的方法,它解決問題的各個小部分,直到解決最初的大問題。遞歸一般涉及函數調用自身。

通俗的解釋:年級主任須要知道某個年級的數學成績的平均值,他無法直接獲得結果;年級主任須要問每一個班的數學老師,數學老師須要問班上每一個同窗;而後再沿着學生-->老師-->主任這條線反饋,才能獲得結果。遞歸也是如此,本身沒法直接解決問題,將問題給下一級,下一級若沒法解決,再給下一級,直到有結果再依次向上反饋。node

咱們常見的使用遞歸解決的問題,以下:算法

// 斐波拉契數列
function fibo(n) {
    if (n === 0 || n === 1) return n; // 邊界
    return fibo(n - 1) + fibo(n - 2);
}
// 階乘
function factorial(n) {
    if (n === 0 || n === 1) return 1; // 邊界
    return facci(n - 1) * n;
}

他們有共同的特色,也是遞歸的特色:segmentfault

  1. 有邊界條件,防止無限遞歸
  2. 函數自身調用

1.2 高效遞歸的兩個方法

以斐波拉契數列舉例,下面是n=6時斐波拉契數列的計算過程。緩存

clipboard.png

咱們能夠發現,這裏面存在許多重複的計算,數列越大重複計算越多。數據結構

如何避免呢?利用緩存,將fib(n)計算後的值存儲,後面使用時,若存在直接取用,不存在則計算閉包

(1)緩存Memoizer函數

const fibo_memo = function() {
  const temp = {0: 0, 1: 1}; // 須要用閉包緩存
  return function fib(n) {
      if (!(n in temp)) { // 緩存中無對應數據時,向下計算查找
          temp[n] = fib(n - 1) + fib(n - 2);
      }
      return temp[n];
  }
}()

(2)遞推法(動態規劃)post

動態規劃並不屬於高效遞歸,可是也是有效解決問題的一個方法。

動態規劃:從底部開始解決問題,將全部小問題解決掉,而後合併成一個總體解決方案,從而解決掉整個大問題;
遞歸:從頂部開始將問題分解,經過解決掉全部分解的小問題來解決整個問題;

使用動態規劃解決斐波那契數列

function fibo_dp(n) {
    let current = 0;
    let next = 1;
    for(let i = 0; i < n; i++) {
        [current, next] = [next, current + next];
    }
    return current;
}

(3)效率對比

const arr = Array.from({length: 40}, (_, i) => i);

// 普通
console.time('fibo');
arr.forEach((e) => { fibo(e); });
console.timeEnd('fibo');

// 緩存
console.time('fibo_memo');
arr.forEach((e) => { fibo_memo(e); });
console.timeEnd('fibo_memo');

// 動態規劃
console.time('fibo_dp');
arr.forEach((e) => { fibo_dp(e); });
console.timeEnd('fibo_dp');

// 打印結果【40】
fibo: 1869.665ms
fibo_memo: 0.088ms
fibo_dp: 0.326ms
// 當打印到【1000】時,普通的已溢出
fibo_memo: 0.370ms
fibo_dp: 16.458ms

總結:從上面的對比結果可知,使用緩存的性能最佳

2、樹

一個樹結構包含一系列存在父子關係的節點。每一個節點都有一個父節點(除了頂部的第一個
節點)以及零個或多個子節點:

clipboard.png

2.1 相關術語

  • 節點:樹中的每一個元素都叫做節點
  • 根節點:位於樹頂部的節點叫做根節點
  • 內部節點/分支節點:至少有一個子節點的節點稱爲內部節點或;
  • 外部節點/葉節點:沒有子元素的節點稱爲外部節點葉節點
  • 子女節點:7和15爲11的子女節點
  • 父節點:11爲7和15的父節點
  • 兄弟節點:同一個父節點的子女節點互稱爲兄弟;7和15互爲兄弟節點
  • 祖先節點:從根節點到該節點所通過分支上的全部節點;如節點3的祖先節點爲 11,7,8
  • 子孫節點:以某一節點構成的子樹,其下全部節點均爲其子孫節點;如12和14爲13的子孫節點
  • 節點所在層次:根節點爲1層,依次向下
  • 樹的深度:樹中距離根節點最遠的節點所處的層次就是樹的深度;上圖中,樹的深度是4
  • 節點的度:結點擁有子結點的數量;
  • 樹的度:樹中節點的度的最大值;
  • 有序樹
  • 無序樹

關於數的深度和高度的問題,不一樣的教材有不一樣的說法,具體能夠參考樹的高度和深度以及結點的高度和深度這篇文章

2.2 認識二叉搜索樹BST

2.2.1 定義

二叉樹是樹的一種特殊狀況,每一個節點最多有有兩個子女,分別稱爲該節點的左子女和右子女,就是說,在二叉樹中,不存在度大於2的節點。

二叉搜索樹(BST)是二叉樹的一種,可是它只容許你在左側節點存儲(比父節點)小的值, 在右側節點存儲(比父節點)大(或者等於)的值。

上圖展現的即是二叉搜索數

2.2.2 特色

  • 同一層,數值從左到右依次增長
  • 以某一祖先節點爲參考,該節點左側值均小於節點值,右側值均大於節點值
  • 在二叉樹的第i(i>=1)層,最多有x^(i-1)個節點
  • 深度爲k(k>=1)的二叉樹,最少有k個節點,最多有2^k-1個節點
  • 對於一棵非空二叉樹,葉節點的數量等於度爲2的節點數量加1

滿二叉樹:深度爲k的滿二叉樹,是有2^k-1個節點的二叉樹,每一層都達到了能夠容納的最大數量的節點

2.2.3 基礎方法

  • insert(key): 向樹中插入一個新的鍵;
  • inOrderTraverse: 經過中序遍歷方式遍歷全部節點
  • preOrderTraverse: 經過先序遍歷方式遍歷全部節點
  • postOrderTraverse: 經過後序遍歷方式遍歷全部節點
  • getMin: 返回樹中最小的值/鍵
  • getMax: 返回樹中最大的值/鍵
  • find(key): 在樹中查找一個鍵,若是節點存在則返回該節點不存在則返回null
  • remove(key): 從樹中移除某個鍵

2.3 BST的實現

2.3.1 基類

// 基類
class BinaryTreeNode {
  constructor(data) {
    this.key = data;
    this.left = null;
    this.right = null;
  }
}

下圖展示了二叉搜索樹數據結構的組織方式:

clipboard.png

2.3.2 BST類

//二叉查找樹(BST)的類
class BinarySearchTree {
  constructor() {
    this.root = null; // 根節點
  }
  
  insert(){} // 插入節點
  preOrderTraverse(){} // 先序遍歷
  inOrderTraverse(){} // 中序遍歷
  postOrderTraverse(){} // 後序遍歷
  search(){} // 查找節點
  getMin(){} // 查找最小值
  getMax(){} // 查找最大值
  remove(){} // 刪除節點
}

2.3.3 insert方法

insert某個值到樹中,必須依照二叉搜索樹的規則【每一個節點Key值惟一,最多有兩個節點,且左側節點值<父節點值<右側節點值

不一樣狀況具體操做以下:

  • 根節點爲null,直接賦值插入節點給根節點;
  • 根節點不爲null,按照BST規則找到left/rightnull的位置並賦值
insert(key) {
  const newNode = new BinaryTreeNode(key);
  if (this.root !== null) {
    this.insertNode(this.root, newNode);
  } else {
    this.root = newNode;
  }
}

insertNode(node, newNode) {
  if (newNode.key < node.key) {
    if (node.left === null) {// 左側
      node.left = newNode;
    } else {
      this.insertNode(node.left, newNode);
    }
  } else {
    if (node.right === null) {// 右側
      node.right = newNode;
    } else {
      this.insertNode(node.right, newNode);
    }
  }
}

下圖爲在已有BST的基礎上插入值爲6的節點,步驟以下:

clipboard.png

  1. 有無根節點?有;對比根節點值(6<11),根節點左側判斷;
  2. 第二層左側節點是否爲null?不爲;對比第二層左側節點的值(6<7),繼續左側判斷;
  3. 第三層左側節點是否爲null?不爲;對比第三層左側節點的值(6>5),以右側判斷;
  4. 第四層右側節點是否爲null?爲;插入該處

2.3.4 樹的遍歷

樹的遍歷,核心爲遞歸:根節點須要找到其每個子孫節點,可是並不知道這棵樹有多少層。所以,它找到其子節點,子節點也不知道,依次向下找,直到葉節點。

訪問樹的全部節點有三種方式:中序、先序和後序。下面依次介紹

(1)中序遍歷

中序遍歷是一種以上行順序訪問BST全部節點的遍歷方式,也就是以從最小到最大的順序訪問全部節點。中序遍歷的一種應用就是<u>對樹進行排序操做</u>

inOrderTraverse(callback) {
  this.inOrderTraverseNode(this.root, callback);
}

inOrderTraverseNode(node, callback) {
  if (node !== null) {
    this.inOrderTraverseNode(node.left, callback);
    callback(node.key);
    this.inOrderTraverseNode(node.right, callback);
  }
}

下面的圖描繪了中序遍歷方法的訪問路徑:

clipboard.png

(2)先序遍歷

先序遍歷是以優先於後代節點的順序訪問每一個節點的。先序遍歷的一種應用是<u>打印一個結構化的文檔</u>

preOrderTraverse(callback) {
  this.preOrderTraverseNode(this.root, callback);
}

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

下面的圖描繪了先序遍歷方法的訪問路徑:

clipboard.png

(3)後序遍歷

後序遍歷則是先訪問節點的後代節點,再訪問節點自己。後序遍歷的一種應用是<u>計算一個目錄和它的子目錄中全部文件所佔空間的大小</u>

postOrderTraverse(callback) {
  this.postOrderTraverseNode(this.root, callback);
}

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

下面的圖描繪了後序遍歷方法的訪問路徑:

clipboard.png

2.3.5 查找方法

(1)最值
觀察下圖,咱們能夠很是直觀的發現左下角爲最小值,右下角爲最大值

clipboard.png

具體代碼實現以下

getMin() {
  const ret = this.getMinNode();
  return ret && ret.key;
}

getMinNode(node = this.root) {
  if (node) {
    while (node && node.left !== null) {
      node = node.left;
    }
  }
  return node;
}

getMax() {
  const ret = this.getMaxNode();
  return ret && ret.key;
}

getMaxNode(node = this.root) {
  if (node) {
    while (node && node.right !== null) {
      node = node.right;
    }
  }
  return node;
}

(2)find()方法

遞歸找到與目標key值相同的節點,並返回;具體實現以下:

find(key) {
  return this.findNode(this.root, key);
}

findNode(node, key) {
  if (node === null) {
    return null;
  }
  if (key < node.key) {
    return this.findNode(node.left, key);
  }
  if (key > node.key) {
    return this.findNode(node.right, key);
  }
  return node;
}

2.3.6 remove()方法

移除節點是這一類方法中最爲複雜的操做,首先須要找到目標key值對應的節點,而後根據不一樣的目標節點類型須要有不一樣的操做

remove(key) {
  return this.removeNode(this.root, key);
}

removeNode(node, key) {
  if (node === null) {
    return null;
  }
  if (key < node.key) { // 目標key小於當前節點key,繼續向左找
    node.left = this.removeNode(node.left, key);
    return node;
  }
  if (key > node.key) { // 目標key小於當前節點key,繼續向右找
    node.right = this.removeNode(node.right, key);
    return node;
  }

  // 找到目標位置
  if (node.left === null && node.right === null) { // 目標節點爲葉節點
    node = null;
    return node;
  }
  if (node.right === null) { // 目標節點僅有左側節點
    node = node.left;
    return node;
  }
  if (node.left === null) { // 目標節點僅有右側節點
    node = node.right;
    return node;
  }

  // 目標節點有兩個子節點
  const tempNode = this.getMinNode(node.right); // 右側最小值
  node.key = tempNode.key;
  node.right = this.removeNode(node.right, node.key);
  return node;
}

目標節點爲葉節點圖例:子節點賦值爲null,並將目標節點指向null

clipboard.png

目標節點爲僅有左側子節點或右側子節點圖例:將目標節點的父節點指向子節點

clipboard.png

目標節點有兩個子節點:根據BST的構成規則,以目標節點右側樹最小值替換從新鏈接

clipboard.png

上一篇:JS數據結構與算法_集合&字典

相關文章
相關標籤/搜索