JavaScript實(shí)現(xiàn)二叉樹(shù)定義、遍歷及查找的方法詳解
本文實(shí)例講述了JavaScript實(shí)現(xiàn)二叉樹(shù)定義、遍歷及查找的方法。分享給大家供大家參考,具體如下:
二叉樹(shù)(binary tree)
在寫這篇文章之前說(shuō)一下數(shù)據(jù)結(jié)構(gòu)和算法這個(gè)系列,這個(gè)系列包含了很多東西,比如啥子排序,線性表,廣義表,樹(shù),圖這些大家都是知道的,但是這些東西我們學(xué)了之后工作中能用到的又有多少呢,據(jù)我所知絕大部分公司,一線碼農(nóng),屌絲,程序猿是用不到這些東西,既然這樣為啥子我還要強(qiáng)調(diào)這個(gè)系列呢,本人覺(jué)得算法和數(shù)據(jù)結(jié)構(gòu)是程序的基本功,前提想脫離一線碼農(nóng),普通程序猿行列,說(shuō)得通俗一點(diǎn)就是讓自己變的更牛逼。其次語(yǔ)言都是想通的,只要是掌握了一門語(yǔ)言學(xué)習(xí)其他語(yǔ)言就如同順?biāo)浦郏毁M(fèi)一點(diǎn)力氣。另外還有一點(diǎn)就是我會(huì)一直把這個(gè)系列寫下去, 雖然網(wǎng)上一搜一大筐,已經(jīng)寫爛了,但是我寫作的目的有兩個(gè),第一和大家分享, 第二可以讓自己更深入的理解。好了,其他的不多說(shuō)了,最近復(fù)習(xí)了一下二叉樹(shù), 就先寫這個(gè),后面會(huì)依次的加上排序, 線性表,廣義表。。。。等等
二叉樹(shù)
一說(shuō)到二叉樹(shù)我們肯定會(huì)問(wèn),什么是二叉樹(shù),二叉樹(shù)是個(gè)啥子?xùn)|東,拿來(lái)有啥子用嘛,我們?yōu)樯蹲右獙W(xué)習(xí)它嘛? 如果當(dāng)初你在學(xué)習(xí)二叉樹(shù)的時(shí)候你沒(méi)有問(wèn)過(guò)自己這些問(wèn)題,那么你對(duì)它的了解也僅僅也只是了解。那我們現(xiàn)在來(lái)說(shuō)說(shuō)什么是二叉樹(shù),二叉樹(shù)就是一種數(shù)據(jù)結(jié)構(gòu), 它的組織關(guān)系就像是自然界中的樹(shù)一樣。官方語(yǔ)言的定義是:是一個(gè)有限元素的集合,該集合或者為空、或者由一個(gè)稱為根的元素及兩個(gè)不相交的、被分別稱為左子樹(shù)和右子樹(shù)的二叉樹(shù)組成。至于為啥子要學(xué)習(xí)它,媽媽總是說(shuō),孩子,等你長(zhǎng)大了就明白了。
二叉樹(shù)的性質(zhì)
性質(zhì)1:二叉樹(shù)第i層上的節(jié)點(diǎn)數(shù)目最多為2i-1(i≥1);
性質(zhì)2:深度為k的二叉樹(shù)至多有2k-1個(gè)結(jié)點(diǎn)(k≥1)。
性質(zhì)3: 在任意-棵二叉樹(shù)中,若葉子結(jié)點(diǎn)(即度為0的結(jié)點(diǎn))的個(gè)數(shù)為n0,度為1的結(jié)點(diǎn)數(shù)為n1,度為2的結(jié)點(diǎn)數(shù)為n2,則no=n2+1。
二叉樹(shù)的存儲(chǔ)結(jié)構(gòu)與構(gòu)建
二叉樹(shù)的存儲(chǔ)方式有兩種,一種順序存儲(chǔ),比如:
var binaryTree = ['a', 'b', 'c', 'd', 'e', 'f', 'h', 'i'];
這就是一顆二叉樹(shù),假設(shè)binaryTree[i]是二叉樹(shù)的一個(gè)節(jié)點(diǎn),那么它的左孩子節(jié)點(diǎn) leftChild = binaryTree[i*2+1]那么相應(yīng)的右孩子節(jié)點(diǎn) rightChild = binaryTree[i*2+2]; 一般情況下順序存儲(chǔ)的這種結(jié)構(gòu)用的較少,另外一種存儲(chǔ)方式就是鏈?zhǔn)酱鎯?chǔ),下面我會(huì)用代碼來(lái)詳細(xì)描述二叉樹(shù)式結(jié)構(gòu)的構(gòu)建與存儲(chǔ)方式,構(gòu)建二叉樹(shù)也有兩種方式一種是遞歸方式構(gòu)建,這種很簡(jiǎn)單,另一種是非遞歸方法構(gòu)建,這種呢相對(duì)于前一種復(fù)雜一點(diǎn)點(diǎn),不過(guò)也不用擔(dān)心,我在代碼中加上詳細(xì)的注釋,一步一步的走下去。我們現(xiàn)在就以26個(gè)英文字母來(lái)構(gòu)建二叉樹(shù)
在構(gòu)建二叉樹(shù)之前我們會(huì)用到一個(gè)節(jié)點(diǎn)對(duì)象,節(jié)點(diǎn)對(duì)象如下:(注意:關(guān)于javascript的面向?qū)ο?,原型,語(yǔ)法特點(diǎn)我會(huì)放在javascript語(yǔ)言知識(shí)點(diǎn)這個(gè)系列)
/* *二叉樹(shù)的節(jié)點(diǎn)對(duì)象 */ function Node() { this.text = ''; //節(jié)點(diǎn)的文本 this.leftChild = null; //節(jié)點(diǎn)的左孩子引用 this.rightChild = null; //節(jié)點(diǎn)右孩子引用 }
遞歸構(gòu)建二叉樹(shù)
在構(gòu)建好二叉樹(shù)節(jié)點(diǎn)之后我們緊接著用遞歸來(lái)構(gòu)建二叉樹(shù)
var charecters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']; function buildTree(node, i) { var leftIndex = 2*i+1, //左孩子節(jié)點(diǎn)的索引 rightIndex = 2*i+2; //右孩子節(jié)點(diǎn)的索引 if(leftIndex < charecters.length) { //判斷索引的長(zhǎng)度是否超過(guò)了charecters數(shù)組的大小 var childNode = new Node(); //創(chuàng)建一個(gè)新的節(jié)點(diǎn)對(duì)象 childNode.text = charecters[leftIndex]; //給節(jié)點(diǎn)賦值 node.leftChild = childNode; //給當(dāng)前節(jié)點(diǎn)node加入左孩子節(jié)點(diǎn) buildTree(childNode, leftIndex); //遞歸創(chuàng)建左孩子 } if(rightIndex < charecters.length) { //下面注釋參照上面的構(gòu)建左孩子的節(jié)點(diǎn) var childNode = new Node(); childNode.text = charecters[rightIndex]; node.rightChild = childNode; buildTree(childNode, rightIndex); } } //下面構(gòu)造二叉樹(shù) var node = new Node(); node.text = charecters[0]; buildTree(node, 0); //索引i是從0開(kāi)始構(gòu)建
非遞歸構(gòu)建二叉樹(shù)
下面是以非遞歸方式構(gòu)建二叉樹(shù):
var root; function createBinaryTree() { var len = charecters.length, //數(shù)組的長(zhǎng)度 index = 0, //索引從0開(kāi)始 nodes = new Array(); //創(chuàng)建一個(gè)臨時(shí)數(shù)組,用于存放二叉樹(shù)節(jié)點(diǎn) //循環(huán)創(chuàng)建二叉樹(shù)節(jié)點(diǎn)存放到數(shù)組中 for (var i = 0 ; i < charecters.length ; i++) { var node = new Node(); node.text = charecters[i]; nodes.push(node); } //循環(huán)建立二叉樹(shù)子節(jié)點(diǎn)的引用 while(index < len) { var leftIndex = 2*index+1, //當(dāng)前節(jié)點(diǎn)左孩子索引 rightIndex = 2*index+2; //當(dāng)前節(jié)點(diǎn)右孩子索引 //給當(dāng)前節(jié)點(diǎn)添加左孩子 nodes[index].leftChild = nodes[leftIndex]; //給當(dāng)前節(jié)點(diǎn)添加右孩子 nodes[index].rightChild = nodes[rightIndex]; index++; } root = nodes[0]; }
二叉樹(shù)的三種遍歷
好了,現(xiàn)在我們已經(jīng)成功構(gòu)建了二叉樹(shù)的鏈?zhǔn)浇Y(jié)構(gòu),在構(gòu)建了二叉樹(shù)的鏈?zhǔn)浇Y(jié)構(gòu)后我們進(jìn)入二叉樹(shù)的最基本的遍歷了,遍歷有三種最基本的遍歷,我不說(shuō)想必大家都知道,先序遍歷,中序遍歷和后續(xù)遍歷。雖然這三種遍歷遞歸方式都比較簡(jiǎn)單,但非遞歸方式就不是那么容易了,當(dāng)時(shí)我在實(shí)現(xiàn)的時(shí)候都卡了半天,真的是說(shuō)起來(lái)容易做起來(lái)難啊,在實(shí)現(xiàn)遍歷前我們首先要來(lái)實(shí)現(xiàn)的是棧,因?yàn)樵诜沁f歸遍歷的時(shí)候會(huì)用到棧,那到底什么是棧呢,這里我就簡(jiǎn)單介紹下吧,有興趣的朋友可以去維基百科有權(quán)威的定義,棧和隊(duì)列也是一種數(shù)據(jù)結(jié)構(gòu),棧存放數(shù)據(jù)的時(shí)候是先進(jìn)先出,而隊(duì)列是先進(jìn)后出。
實(shí)現(xiàn)棧的對(duì)象
下面用javascript來(lái)實(shí)現(xiàn)棧的對(duì)象
function Stack() { var stack = new Array(); //存放棧的數(shù)組 //壓棧 this.push = function(o) { stack.push(o); }; //出棧 this.pop = function() { var o = stack[stack.length-1]; stack.splice(stack.length-1, 1); return o; }; //檢查棧是否為空 this.isEmpty = function() { if(stack.length <= 0) { return true; } else { return false; } }; } //使用方式如下 var stack = new Stack(); stack.push(1); //現(xiàn)在棧中有一個(gè)元素 stack.isEmpty(); //false , 棧不為空 alert(stack.pop()); //出棧, 打印1 stack.isEmpty(); //true, 此時(shí)棧為空,因?yàn)樵谡{(diào)用了stack.pop()之后元素出棧了,所以為空
1. 先序遍歷
在實(shí)現(xiàn)了棧對(duì)象以后我們首先來(lái)進(jìn)行先序遍歷的遞歸方式
function firstIteration(node) { if(node.leftChild) { //判斷當(dāng)前節(jié)點(diǎn)是否有左孩子 firstIteration(node.leftChild); //遞歸左孩子 } if(node.rightChild) { //判斷當(dāng)前節(jié)點(diǎn)是否有右孩子 firstIteration(node.rightChild); //遞歸右孩子 } } //遞歸遍歷二叉樹(shù) firstIteration(root);
先序遍歷的非遞歸方式
上面的代碼大家可以在firstIteration()方法中加個(gè)alert()函數(shù)來(lái)驗(yàn)證是否正確。那么下面就要說(shuō)說(shuō)先序遍歷的非遞歸方式,遍歷思想是這樣的:先訪問(wèn)根節(jié)點(diǎn)在訪問(wèn)左節(jié)點(diǎn), 最后訪問(wèn)右節(jié)點(diǎn)。從根節(jié)點(diǎn)一直往下訪問(wèn)找左孩子節(jié)點(diǎn),直到最后一個(gè)左孩子節(jié)點(diǎn)(將這條路徑保存到棧中),然后再訪問(wèn)最后一個(gè)左孩子的兄弟節(jié)點(diǎn)(右孩子節(jié)點(diǎn)),之后回溯到上一層(將棧中的元素取出 就是出棧),又開(kāi)始從該節(jié)點(diǎn)(回溯到上一層的節(jié)點(diǎn))一直往下訪問(wèn)找左孩子節(jié)點(diǎn)... 直到棧中的元素為空,循環(huán)結(jié)束。
function notFirstIteration(node) { var stack = new Stack(), //開(kāi)辟一個(gè)新的棧對(duì)象 resultText = ''; //存放非遞歸遍歷之后的字母順序 stack.push(root); //這個(gè)root在上面非遞歸方式構(gòu)建二叉樹(shù)的時(shí)候已經(jīng)構(gòu)建好的 var node = root; resultText += node.text; while(!stack.isEmpty()) { while(node.leftChild) { //判斷當(dāng)前節(jié)點(diǎn)是否有左孩子節(jié)點(diǎn) node = node.leftChild; //取當(dāng)前節(jié)點(diǎn)的左孩子節(jié)點(diǎn) resultText += node.text; //訪問(wèn)當(dāng)前節(jié)點(diǎn) stack.push(node); //將當(dāng)前節(jié)點(diǎn)壓入棧中 } stack.pop(); //出棧 node = stack.pop().rightChild; //訪問(wèn)當(dāng)前節(jié)點(diǎn)的兄弟節(jié)點(diǎn)(右孩子節(jié)點(diǎn)) if(node) { //當(dāng)前節(jié)點(diǎn)的兄弟節(jié)點(diǎn)不為空 resultText += node.text; //訪問(wèn)當(dāng)前節(jié)點(diǎn) stack.push(node); //將當(dāng)前節(jié)點(diǎn)壓入棧中 } else { //當(dāng)前節(jié)點(diǎn)的兄弟節(jié)點(diǎn)為空 node = stack.pop(); //在回溯到上一層 } } } //非遞歸先序遍歷 notFirstIteration(root);
2. 中序遍歷
只要把思路理清楚了現(xiàn)實(shí)起來(lái)其實(shí)還是挺容易的,只要我們熟悉了一種二叉樹(shù)的非遞歸遍歷方式,其他幾種非遞歸方式就容易多了,照著葫蘆畫瓢,下面是中序遍歷的遞歸方式,中序遍歷的思想是:先訪問(wèn)左孩子節(jié)點(diǎn),在訪問(wèn)根節(jié)點(diǎn),最后訪問(wèn)右節(jié)點(diǎn)
var strText = ""; function secondIteration(node) { //訪問(wèn)左節(jié)點(diǎn) if(node.leftChild) { if(node.leftChild.leftChild) { secondIteration(node.leftChild); } else { strText += node.leftChild.text; } } //訪問(wèn)根節(jié)點(diǎn) strText += node.text; //訪問(wèn)右節(jié)點(diǎn) if(node.rightChild) { if(node.rightChild.leftChild) { secondIteration(node.rightChild); } else { strText += node.rightChild.text; } } } secondIteration(root); alert(strText);
中序遍歷的非遞歸方式
思想是:1. 從根節(jié)點(diǎn)一直往下找左孩子節(jié)點(diǎn),直到找到最后一個(gè)左孩子節(jié)點(diǎn)(用棧將此路徑保存,但不訪問(wèn))2.訪問(wèn)最后一個(gè)左孩子節(jié)點(diǎn),然后再訪問(wèn)根節(jié)點(diǎn)(要先彈出棧,就是在棧中取上一層節(jié)點(diǎn))3.在訪問(wèn)當(dāng)前節(jié)點(diǎn)(最后一個(gè)左孩子節(jié)點(diǎn))的兄弟節(jié)點(diǎn)(右孩子節(jié)點(diǎn)),這里要注意如果兄弟節(jié)點(diǎn)是一個(gè)葉節(jié)點(diǎn)就直接訪問(wèn),否則是兄弟節(jié)點(diǎn)是一顆子樹(shù)的話不能馬上訪問(wèn),要先來(lái)重復(fù) 1, 2,3步驟, 直到棧為空,循環(huán)結(jié)束
function notSecondIteration() { var resultText = '', stack = new Stack(), node = root; stack.push(node); while(!stack.isEmpty()) { //從根節(jié)點(diǎn)一直往下找左孩子節(jié)點(diǎn)直到最后一個(gè)左孩子節(jié)點(diǎn),然后保存在棧中 while(node.leftChild) { node = node.leftChild; stack.push(node); } //彈出棧 var tempNode = stack.pop(); //訪問(wèn)臨時(shí)節(jié)點(diǎn) resultText += tempNode.text; if(tempNode.rightChild) { node = tempNode.rightChild; stack.push(node); } } alert(resultText); }
3. 后續(xù)遍歷
最后就還剩下一種遍歷方式,二叉樹(shù)的后續(xù)遍歷,后續(xù)遍歷的思想是:先訪問(wèn)左孩子節(jié)點(diǎn),然后在訪問(wèn)右孩子節(jié)點(diǎn),最后訪問(wèn)根節(jié)點(diǎn)
后續(xù)遍歷的遞歸方式
var strText = ''; function lastIteration(node) { //首先訪問(wèn)左孩子節(jié)點(diǎn) if(node.leftChild) { if(node.leftChild.leftChild) { lastIteration(node.leftChild); } else { strText += node.leftChild.text; } } //然后再訪問(wèn)右孩子節(jié)點(diǎn) if(node.rightChild) { if(node.rightChild.rightChild) { lastIteration(node.rightChild); } else { strText += node.rightChild.text; } } //最后訪問(wèn)根節(jié)點(diǎn) strText += node.text; } //中序遞歸遍歷 lastIteration(root); alert(strText);
后續(xù)非遞歸遍歷
后續(xù)非遞歸遍歷的思想是:1.從根節(jié)點(diǎn)一直往下找左孩子節(jié)點(diǎn),直到最后一個(gè)左孩子節(jié)點(diǎn)(將路徑保存到棧中,但不訪問(wèn))2.彈出棧訪問(wèn)最后一個(gè)左孩子節(jié)點(diǎn) 3.進(jìn)入最后一個(gè)左孩子節(jié)點(diǎn)的兄弟節(jié)點(diǎn),如果兄弟節(jié)點(diǎn)是葉節(jié)點(diǎn)就訪問(wèn)它,否則將該節(jié)點(diǎn)重復(fù) 1, 2步驟, 直到棧中的元素為空,循環(huán)結(jié)束。3.訪問(wèn)根節(jié)點(diǎn)
function notLastIteration() { var strText = '', stack = new Stack(); nodo = root; stack.push(node); while(!stack.isEmpty()) { while(node.leftChild) { node = node.leftChild; stack.push(node); } //彈出棧 var tempNode = stack.pop(); //訪問(wèn)左孩子節(jié)點(diǎn) strText += tempNode.text; //訪問(wèn)右孩子節(jié)點(diǎn) if(tempNode.rightChild) { if(tempNode.rightChild.leftChild || tempNode.rightChild.rightChild) { //判斷最后一個(gè)左孩子節(jié)點(diǎn)的兄弟節(jié)點(diǎn)是否為頁(yè)節(jié)點(diǎn) stack.push(tempNode.rightChild); } else { strText += tempNode.rightChild.text; } } } alert(strText); }
更多關(guān)于JavaScript相關(guān)內(nèi)容感興趣的讀者可查看本站專題:《JavaScript數(shù)據(jù)結(jié)構(gòu)與算法技巧總結(jié)》、《JavaScript數(shù)學(xué)運(yùn)算用法總結(jié)》、《JavaScript排序算法總結(jié)》、《JavaScript遍歷算法與技巧總結(jié)》、《JavaScript查找算法技巧總結(jié)》及《JavaScript錯(cuò)誤與調(diào)試技巧總結(jié)》
希望本文所述對(duì)大家JavaScript程序設(shè)計(jì)有所幫助。
相關(guān)文章
layui監(jiān)聽(tīng)下拉選框選中值變化的方法(包含監(jiān)聽(tīng)普通下拉選框)
今天小編大家分享一篇layui監(jiān)聽(tīng)下拉選框選中值變化的方法(包含監(jiān)聽(tīng)普通下拉選框),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-09-09JavaScript將數(shù)組轉(zhuǎn)換成CSV格式的方法
這篇文章主要介紹了JavaScript將數(shù)組轉(zhuǎn)換成CSV格式的方法,實(shí)例分析了javascript使用valueOf方法將數(shù)組值轉(zhuǎn)換為csv格式字符串的技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-03-03支付寶小程序自定義彈窗dialog插件的實(shí)現(xiàn)代碼
支付寶小程序官方提供的alert提示框、dialog對(duì)話框、model彈窗功能比較有限,有些都不能隨意自定義修改的。這篇文章主要介紹了支付寶小程序自定義彈窗dialog插件的實(shí)現(xiàn)代碼,需要的朋友可以參考下2018-11-11微信小程序?qū)崿F(xiàn)判斷是分享到群還是個(gè)人功能示例
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)判斷是分享到群還是個(gè)人功能,結(jié)合實(shí)例形式分析了微信小程序分享與判斷功能的實(shí)現(xiàn)原理、步驟及相關(guān)操作技巧,需要的朋友可以參考下2019-05-05微信小程序動(dòng)態(tài)顯示項(xiàng)目倒計(jì)時(shí)
這篇文章主要為大家詳細(xì)介紹了微信小程序動(dòng)態(tài)顯示項(xiàng)目倒計(jì)時(shí),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-06-06Javascript模擬實(shí)現(xiàn)new原理解析
這篇文章主要介紹了Javascript模擬實(shí)現(xiàn)new原理解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03javascript系統(tǒng)時(shí)間設(shè)置操作示例
這篇文章主要介紹了javascript系統(tǒng)時(shí)間設(shè)置操作,涉及javascript時(shí)間運(yùn)算與判斷相關(guān)操作技巧,需要的朋友可以參考下2019-06-06js es6系列教程 - 新的類語(yǔ)法實(shí)戰(zhàn)選項(xiàng)卡(詳解)
下面小編就為大家?guī)?lái)一篇js es6系列教程 - 新的類語(yǔ)法實(shí)戰(zhàn)選項(xiàng)卡(詳解)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-09-09javascript兩種function的定義介紹及區(qū)別說(shuō)明
javascript兩種function的定義方式function a(){}和a=function(){}具體使用如下,感興趣的朋友可以參考下,希望對(duì)你對(duì)你學(xué)習(xí)function的定義有所幫助2013-05-05