前端開發(fā)-- 二叉樹的相關(guān)算法

二叉樹和二叉搜索樹

二叉樹中的節(jié)點(diǎn)最多只能有2個(gè)子節(jié)點(diǎn):一個(gè)是左側(cè)子節(jié)點(diǎn),另外一個(gè)是右側(cè)子節(jié)點(diǎn)。
二叉搜索樹(BST)是二叉樹的一種,但是它只允許你在左側(cè)節(jié)點(diǎn)存儲(chǔ)(比父節(jié)點(diǎn))小的值,在右側(cè)節(jié)點(diǎn)存儲(chǔ)(比父節(jié)點(diǎn))大(或等于)的值
創(chuàng)建二叉搜索樹
1.我們要先聲明二叉搜索樹的結(jié)構(gòu):

function BinarySearchTree(){
  let Node = function(key){
    this.key = key;
    this.left = null;
    this.right = null;
  };
  let root = null;
  function show(){
    return this.key;
  }
}
二叉搜索樹的結(jié)構(gòu)

上面的數(shù)據(jù)結(jié)構(gòu),和鏈表很相似,都是東莞指針表示節(jié)點(diǎn)之間的關(guān)系(術(shù)語(yǔ)稱其為邊),對(duì)于樹來說,我們也是通過指針來指定下一個(gè)元素的,但是從圖中,我們也看到了,節(jié)點(diǎn)的指針,一個(gè)指向左側(cè)子節(jié)點(diǎn),另外一個(gè)指向右側(cè)的子節(jié)點(diǎn),其中,鍵是樹相關(guān)術(shù)語(yǔ)對(duì)節(jié)點(diǎn)的稱呼。

向樹中插入一個(gè)人鍵(節(jié)點(diǎn))

向樹中插入一個(gè)新的節(jié)點(diǎn),要經(jīng)歷三個(gè)步驟
第一步:創(chuàng)建用來表示新節(jié)點(diǎn)的Node的實(shí)例,只要向構(gòu)造函數(shù)中傳入我們想要傳入的節(jié)點(diǎn)的值,做指針和右指針會(huì)由構(gòu)造函數(shù)自動(dòng)的設(shè)置為null
第二步:需要驗(yàn)證這個(gè)插入的操作是否為一種特殊的情況,這個(gè)特殊的情況就是我們插入的節(jié)點(diǎn)是樹的第一個(gè)節(jié)點(diǎn),這個(gè)時(shí)候只需要將根節(jié)點(diǎn)指向新節(jié)點(diǎn)。
第三步:將節(jié)點(diǎn)加在非根節(jié)點(diǎn)的其他位置。

let insert = function(key){
  let newNode = new Node(key);
  if(root === null){
    root = newNode;
  }else{
    insertNode(root,newNode)
  }
};
//當(dāng)為非空樹的時(shí)候,我們進(jìn)行節(jié)點(diǎn)的插入的時(shí)候,進(jìn)行的步驟
let indsertNode = 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);
    }
  }
 }

首先我們先構(gòu)造出一個(gè)二叉搜索樹:

let tree = new BinarySearchTree();
tree.insert (11);
tree.insert (7);
tree.insert (15);
tree.insert (5);
tree.insert (3);
tree.insert (9);
tree.insert (8);
tree.insert (10);
tree.insert (13);
tree.insert (12);
tree.insert (14);
tree.insert (20);
tree.insert (18);
tree.insert (25);

我們現(xiàn)在構(gòu)造出來的二叉樹是:

插入二叉樹的結(jié)構(gòu)

當(dāng)我們想要插入一個(gè)鍵值為6的鍵,執(zhí)行下面的代碼:

tree.insert(6);

看一下我們究竟是怎么插入的:

插入二叉搜索樹

中序遍歷

中序遍歷是一種以上行順序訪問BST所有節(jié)點(diǎn)的遍歷方式,也就是是從小到大的順序訪問所有的節(jié)點(diǎn),讓我們看看具體的實(shí)現(xiàn)過程。

let inOrderTraverse = function (root){
  inOrderTraverseNode(root);
}

inOrderTraverse方法接受一個(gè)根節(jié)點(diǎn)作為參數(shù)。在回調(diào)函數(shù)中定義我們對(duì)遍歷到的每一個(gè)節(jié)點(diǎn)進(jìn)行操作。由于我們?cè)贐ST中最常實(shí)現(xiàn)的算法就是遞歸。

var inOrderTraverseNode = function(node){
  if(node !== null){
    inOrderTraverseNode(node.left);
    console.log(node.show());
    inOrderTraverseNode(node.right);
  }
};

tree.inOrderTraverse (tree.root);
中序遍歷的示意圖

最終輸出的結(jié)果是:3 5 6 7 8 9 10 11 12 13 14 15 18 20 25

先序遍歷

先序遍歷是以優(yōu)先于后代節(jié)點(diǎn)的順序訪問每一個(gè)節(jié)點(diǎn),先序遍歷的一種應(yīng)用是打印一個(gè)結(jié)構(gòu)化的文件。我們看一下代碼的實(shí)現(xiàn)方式

let preOrderTraverse = function (root){
  preOrderTraverseNode(root);
}

先序遍歷和中序遍歷的不同點(diǎn)是:先序遍歷會(huì)訪問節(jié)點(diǎn)的本身,然后訪問它的左側(cè)節(jié)點(diǎn),最后是右側(cè)節(jié)點(diǎn),而中序遍歷,先訪問左子節(jié)點(diǎn),自身,右子節(jié)點(diǎn),還是上面的二叉搜索樹,我們對(duì)其進(jìn)行先序遍歷。

var preOrderTraverseNode = function(node){
  if(node !== null){
     console.log(node.show());
    preOrderTraverseNode (node.left);
    preOrderTraverseNode (node.right);
  }
};

tree.preOrderTraverse (tree.root);
先序遍歷

最終輸出的結(jié)果是:11 7 5 3 6 9 8 10 15 13 12 14 20 18 25

后序遍歷

后續(xù)遍歷就是先訪問節(jié)點(diǎn)的后代節(jié)點(diǎn),再訪問節(jié)點(diǎn)本身,后序遍歷的一種應(yīng)用是計(jì)算一個(gè)目錄和它的子目錄中所有的文件所占空間的大小。

let postOrderTraverse = function (root){
  postOrderTraverseNode(root);
}
var postOrderTraverseNode = function(node){
  if(node !== null){
    postOrderTraverseNode (node.left);
    postOrderTraverseNode (node.right);
    console.log(node.show());
  }
};

tree.preOrderTraverse (tree.root);
后序遍歷

搜索樹中的值

搜索樹上的最大值和最小值
在上面的二叉搜索樹中我們可以清楚的看見,最左側(cè)的子節(jié)點(diǎn)的值是最小的,最右側(cè)的子節(jié)點(diǎn)的值是最大的,這條信息為我們實(shí)現(xiàn)二叉搜索樹上的最大值和最小值提供了很大的幫助。

查找最大值最小值

現(xiàn)在編寫一下查找最小值的代碼:

let min = function (){
  return minNode(root);
};

let minNode = function(node){
  if(node){
    while(node && node.left !== null){
      node = node.left;
    }
    return node.key;
  }
  return null;
}

minNode方法允許我們從樹中任意一個(gè)節(jié)點(diǎn)開始尋找最小的鍵,我們可以使用來找到一棵樹或是他子樹最小的鍵。因此我們?cè)谡{(diào)用minNode方法的時(shí)候傳入了樹的根節(jié)點(diǎn),最終體現(xiàn)的結(jié)果是我們找到了最左邊的子節(jié)點(diǎn)。類似的我們可以找到樹中的最大值,代碼如下:

let max= function (){
  return maxNode(root);
};

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

搜索特定的值
當(dāng)我們想要查找在二叉樹中存不存在一個(gè)特定的值的時(shí)候,我們需要搜索二叉樹,現(xiàn)在看一下我們的搜索過程。

let search = function(key){
  return searchNode(root,key);
};

let searchNode = function(node, key){
  if(node === null){
    return false;
  }
  if(key < node.key){
    return searchNode(node.left,key);
  } else if(key > node.key){
    return searchNode(node.right,key);
  }else{
    return true;
  }
}

searchNode方法可以用來尋找一棵樹或是它的任意子樹中的一個(gè)特定的值,這也是為什么在調(diào)用的時(shí)候傳入一個(gè)節(jié)點(diǎn)作為參數(shù)。我們?cè)诔绦蜷_始的時(shí)候,需要判斷節(jié)點(diǎn)的合法性,如果是null的話,說明要找的鍵沒有找到,返回false。如果傳入的節(jié)點(diǎn)不為null,就要比較節(jié)點(diǎn)的鍵值和傳入值的鍵值大小。如果傳入值要大,那就向右邊遍歷二叉搜索樹,否則,就向左邊遍歷二叉樹。

移除節(jié)點(diǎn)

移除節(jié)點(diǎn)是我們?cè)诙嫠阉鳂渲凶顝?fù)雜的一個(gè),首先我們先創(chuàng)建這個(gè)方法。

let remove = function(key){
  root = removeNode (root,key);
};

這個(gè)方法接收要移除的鍵并且調(diào)用了removeNode方法,傳入root和要移除的鍵作為參數(shù)。我們?cè)谝瞥倪^程中,root被賦予removeNode方法的返回值,是有一定的意義的。我們會(huì)在下面進(jìn)行詳細(xì)的講述。

let removeNode = function(node, key){
  if (node === null){
    return null;
  }
  if(key < node.key){
    node.left = removeNode (node.left, key);
    return mdoe;
  }else if(key > node.key){
    node.right = removeNode(node.right, key);
    return node;
  }else{  // 鍵等于node.key
    // 第一種情況-- 一個(gè)葉節(jié)點(diǎn)
    if(node.left === null && node.right === null){
      node = null;
      return node;
    }
    // 第二種情況-- 一個(gè)只有一個(gè)子節(jié)點(diǎn)的節(jié)點(diǎn)
    if(node.left=== null){
      node = node.right;
      return node;
    }else if(node.right === null){
      node = node.left;
      return node;
   }
   // 第三種情況-- 一個(gè)有兩個(gè)子節(jié)點(diǎn)的節(jié)點(diǎn)
    let aux = findMinNode(node.right);
    node.key = aux.key;
    node.right = removeNode (node.right,aux.key);
    return node;
  }
}

檢測(cè)到的節(jié)點(diǎn)是null,說明鍵值不存在于樹中,返回null。我們要做的第一件事情就是從樹中找到這個(gè)節(jié)點(diǎn)。因此,如果要找的鍵比當(dāng)前節(jié)點(diǎn)的值小,就沿著樹的左邊一直找到下一個(gè)節(jié)點(diǎn),如果找到的鍵比當(dāng)前節(jié)點(diǎn)的鍵值大,那么就沿著樹的右邊找到下一個(gè)節(jié)點(diǎn)。
如果我們要找到的鍵(鍵和node.key相等),據(jù)需要三種不同的情況

let findMinNode = function (node){
  while(node && node.left !== null ){
    node = node.left;
  }
  return node;
}

移除一個(gè)葉節(jié)點(diǎn)
這種情況是最簡(jiǎn)單最易懂的刪除方式。
我們要做的就是給這個(gè)節(jié)點(diǎn)賦予null值移除它。但是當(dāng)學(xué)習(xí)了鏈表的實(shí)現(xiàn)之后,我們知道僅僅是賦予了一個(gè)null值是不夠的,還需要處理指針,這個(gè)節(jié)點(diǎn)沒有任何的子節(jié)點(diǎn),但是有一個(gè)父節(jié)點(diǎn),需要通過返回null來將對(duì)應(yīng)的父節(jié)點(diǎn)指針賦予null值。
現(xiàn)在節(jié)點(diǎn)的值已經(jīng)是null,父節(jié)點(diǎn)指向它的指針也會(huì)接受到這個(gè)值,這也是我們要在函數(shù)中返回節(jié)點(diǎn)的值的原因。父節(jié)點(diǎn)總是會(huì)接受到函數(shù)的返回值。另外一種可行的辦法是將父節(jié)點(diǎn)和節(jié)點(diǎn)本身都作為參數(shù)傳入方法的內(nèi)部。

刪除一個(gè)葉節(jié)點(diǎn)

移除有一個(gè)左側(cè)或右側(cè)子節(jié)點(diǎn)的節(jié)點(diǎn)
現(xiàn)在讓我們來看第二種情況,移除一個(gè)有左側(cè)子節(jié)點(diǎn),或是右側(cè)子節(jié)點(diǎn)的節(jié)點(diǎn),在這種情況下,我們需要跳過這個(gè)節(jié)點(diǎn),直接將父節(jié)點(diǎn)指向它指針指向的子節(jié)點(diǎn)。
如果這個(gè)節(jié)點(diǎn)沒有左側(cè)子節(jié)點(diǎn),也就是它只有一個(gè)右側(cè)子節(jié)點(diǎn),因此我們那對(duì)他的引用改成了對(duì)它右側(cè)子節(jié)點(diǎn)的引用,并返回更新后的節(jié)點(diǎn)。現(xiàn)在讓我們以一張圖片的形式來展現(xiàn)移除只有一個(gè)左子節(jié)點(diǎn)或右側(cè)子節(jié)點(diǎn)的節(jié)點(diǎn)的過程。

移除只有一個(gè)子節(jié)點(diǎn)的節(jié)點(diǎn)

移除有兩個(gè)子節(jié)點(diǎn)的節(jié)點(diǎn)
現(xiàn)在是第三種情況,也就是最復(fù)雜的情況,那么就是要移除節(jié)點(diǎn)的兩個(gè)子節(jié)點(diǎn)-- 左側(cè)子節(jié)點(diǎn)和右側(cè)子節(jié)點(diǎn)。要移除有兩個(gè)知己誒單的節(jié)點(diǎn)的時(shí)候,需要執(zhí)行四個(gè)步驟:
(1)找到需要?jiǎng)h除的節(jié)點(diǎn)。找到他右邊子樹最小的節(jié)點(diǎn)(來代替該節(jié)點(diǎn)的位置)
(2)然后,用他右邊子樹最小的節(jié)點(diǎn)來更新這個(gè)節(jié)點(diǎn)的值,通過這一步,我們改變了這個(gè)節(jié)點(diǎn)的鍵,也就是說,他被移除了
(3)但是,這樣的話,樹上就有兩個(gè)擁有相同鍵的節(jié)點(diǎn)了,怎樣是不行的,要繼續(xù)把右子樹的最小節(jié)點(diǎn)進(jìn)行移除,畢竟它已經(jīng)被移到被刪除的節(jié)點(diǎn)的位置了。
(4) 最后,向它的父節(jié)點(diǎn)返回更新后的節(jié)點(diǎn)的引用。
findMinNode方法的實(shí)現(xiàn)和min方法的實(shí)現(xiàn)方式是一樣的,唯一的不同之處在于,在min方法中只返回鍵,但是findMinNode返回了節(jié)點(diǎn)。

移除有兩個(gè)子節(jié)點(diǎn)的節(jié)點(diǎn)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 上一篇文章講述了樹的概念, 特征以及分類, 旨在讓我們理解什么是樹, 樹的一些常用的概念是什么,樹的分類有哪些等。...
    DevCW閱讀 2,232評(píng)論 4 10
  • 如需轉(zhuǎn)載, 請(qǐng)咨詢作者, 并且注明出處.有任何問題, 可以關(guān)注我的微博: coderwhy, 或者添加我的微信: ...
    coderwhy閱讀 9,177評(píng)論 12 36
  • 樹的概述 樹是一種非常常用的數(shù)據(jù)結(jié)構(gòu),樹與前面介紹的線性表,棧,隊(duì)列等線性結(jié)構(gòu)不同,樹是一種非線性結(jié)構(gòu) 1.樹的定...
    Jack921閱讀 4,763評(píng)論 1 31
  • 【顏語(yǔ)心詩(shī)】保險(xiǎn)事業(yè)的三七開。三分在產(chǎn)品學(xué)習(xí),七分在市場(chǎng)開發(fā)。三分在錢的功能,七分在人性把握。三分在促成,七分在服...
    顏丙翔閱讀 206評(píng)論 0 0
  • 昨天聽了盧福漢老師講廣府文化清明禁忌,原來清明一定得祭自己的祖先。作為祖籍揚(yáng)州的我,是否應(yīng)該先自覺學(xué)些規(guī)矩了。那,...
    禪靜一生閱讀 998評(píng)論 1 4

友情鏈接更多精彩內(nèi)容