JS中的算法與數(shù)據(jù)結(jié)構(gòu)之二叉查找樹(Binary Sort Tree)實例詳解
本文實例講述了JS中的算法與數(shù)據(jù)結(jié)構(gòu)之二叉查找樹(Binary Sort Tree)。分享給大家供大家參考,具體如下:
二叉查找樹(Binary Sort Tree)
我們之前所學(xué)到的列表,棧等都是一種線性的數(shù)據(jù)結(jié)構(gòu),今天我們將學(xué)習(xí)計算機(jī)中經(jīng)常用到的一種非線性的數(shù)據(jù)結(jié)構(gòu)——樹(Tree),由于其存儲的所有元素之間具有明顯的層次特性,因此常被用來存儲具有層級關(guān)系的數(shù)據(jù),比如文件系統(tǒng)中的文件;也會被用來存儲有序列表等。
在樹結(jié)構(gòu)中,每一個結(jié)點只有一個前件,稱為父結(jié)點,沒有前件的結(jié)點只有一個,稱為樹的根結(jié)點,簡稱樹的根(root)。每一個結(jié)點可以有多個后件,稱為該結(jié)點的子結(jié)點。沒有后件的結(jié)點稱為葉子結(jié)點。一個結(jié)點所擁有的子結(jié)點的個數(shù)稱為該結(jié)點的度,所有結(jié)點中最大的度稱為樹的度。樹的最大層次稱為樹的深度。
二叉樹
二叉樹是一種特殊的樹,它的子節(jié)點個數(shù)不超過兩個,且分別稱為該結(jié)點的左子樹(left subtree)與右子樹(right subtree),二叉樹常被用作二叉查找樹和二叉堆或是二叉排序樹(BST)。
二叉樹
按一定的規(guī)則和順序走遍二叉樹的所有結(jié)點,使每一個結(jié)點都被訪問一次,而且只被訪問一次,這個操作被稱為樹的遍歷,是對樹的一種最基本的運算。由于二叉樹是非線性結(jié)構(gòu),因此,樹的遍歷實質(zhì)上是將二叉樹的各個結(jié)點轉(zhuǎn)換成為一個線性序列來表示。
按照根節(jié)點訪問的順序不同,樹的遍歷分為以下三種:前序遍歷,中序遍歷,后序遍歷;
前序遍歷:根節(jié)點->左子樹->右子樹
先序遍歷
中序遍歷:左子樹->根節(jié)點->右子樹
中序遍歷
后序遍歷:左子樹->右子樹->根節(jié)點
后序遍歷
因此我們可以得之上面二叉樹的遍歷結(jié)果如下:前序遍歷:ABDEFGC 中序遍歷:DEBGFAC 后序遍歷:EDGFBCA
二叉查找樹(BST)
實際應(yīng)用中,樹的每個節(jié)點都會有一個與之相關(guān)的值對應(yīng),有時候會被稱為鍵。因此,我們在構(gòu)建二叉查找樹的時候,確定子節(jié)點非常的重要,通常將相對較小的值保存在左節(jié)點中,較大的值保存在右節(jié)點中,這就使得查找的效率非常高,因此被廣泛使用。
二叉查找樹的實現(xiàn)
根據(jù)上面的知識,我們了解到二叉樹實際上是由多個節(jié)點組成,因此我們首先就要定義一個Node類,用于存放樹的節(jié)點,其構(gòu)造與前面的鏈表類似。Node類的定義如下:
//節(jié)點定義 function Node (data , left , right) { this.data = data; // 數(shù)據(jù) this.left = left; // 左節(jié)點 this.right = right; // 右節(jié)點 this.show = show; // 顯示節(jié)點數(shù)據(jù) } function show(){ return this.data; }
Node對象既保存了數(shù)據(jù),也保存了它的左節(jié)點和右節(jié)點的鏈接,其中 show 方法用來顯示當(dāng)前保存在節(jié)點中數(shù)據(jù)。
現(xiàn)在我們可以創(chuàng)建一個類,用來表示二叉查找數(shù)(BST),我們初始化類只包含一個成員,一個表示二叉查找樹根節(jié)點的 Node 對象,初始化為 null , 表示創(chuàng)建一個空節(jié)點。
//二叉查找樹(BST)的類 function BST(){ this.root = null; // 根節(jié)點 this.insert = insert; // 插入節(jié)點 this.preOrder = preOrder; // 先序遍歷 this.inOrder = inOrder; // 中序遍歷 this.postOrder = postOrder; // 后序遍歷 this.find = find; // 查找節(jié)點 this.getMin = getMin; // 查找最小值 this.getMax = getMax; // 查找最大值 this.remove = remove; // 刪除節(jié)點 }
現(xiàn)在,我們需要為我們的類添加方法。
首先就是 insert 方法,向樹中添加一個新節(jié)點,我們一起來看看這個方法;
insert:向樹中添加新節(jié)點
因為添加節(jié)點會涉及到插入位置的問題,必須將其放到正確的位置上,才能保證樹的正確性,整個過程較為復(fù)雜,我們一起來梳理一下:
首先要添加新的節(jié)點,首先需要創(chuàng)建一個Node對象,將數(shù)據(jù)傳入該對象。
其次要檢查當(dāng)前的BST樹是否有根節(jié)點,如果沒有,那么表示是一棵新數(shù),該節(jié)點就為該樹的根節(jié)點,那么插入這個過程就結(jié)束了;否則,就要繼續(xù)進(jìn)行下一步了。
如果待插入節(jié)點不是根節(jié)點,那么就必須對BST進(jìn)行遍歷,找到合適的位置。該過程類似遍歷鏈表,用一個變量存儲當(dāng)前節(jié)點,一層一層遍歷BST,算法如下:
- 設(shè)值當(dāng)前節(jié)點為根節(jié)點
- 如果待插入節(jié)點保存的數(shù)據(jù)小于當(dāng)前節(jié)點,則新節(jié)點為原節(jié)點的左節(jié)點,反之,執(zhí)行第4步
- 如果當(dāng)前節(jié)點的左節(jié)點為null,就將新節(jié)點放到這個位置,退出循環(huán);反之,繼續(xù)執(zhí)行下一次循環(huán)
- 設(shè)置新節(jié)點為原節(jié)點的右節(jié)點
- 如果當(dāng)前節(jié)點的右節(jié)點為null,就將新節(jié)點放到這個位置,退出循環(huán);反之,繼續(xù)執(zhí)行下一次循環(huán)
這樣,就能保證每次添加的新節(jié)點能夠放到正確的位置上,具體實現(xiàn)如下;
//插入新節(jié)點 function insert(data) { var n = new Node( data , null , null ); if( this.root == null ){ this.root = n; }else{ var current = this.root; var parent; while( true ){ parent = current; if( data < current.data ){ current = current.left; if( current == null ){ parent.left = n ; break; } }else{ current = current.right; if( current == null ){ parent.right = n; break; } } } } }
現(xiàn)在BST類已初步成型,但操作還僅僅限于插入節(jié)點,我們需要有遍歷BST的能力,上面我們也提到了是三種遍歷方式。其中中序遍歷是最容易實現(xiàn)的,我們需要升序的方法訪問樹中的所有節(jié)點,先訪問左子樹,在訪問根節(jié)點,最后是右子樹,我們采用遞歸來實現(xiàn)!
inOrder:中序遍歷
// 中序遍歷 function inOrder (node) { if( !(node == null )){ inOrder( node.left ); console.debug( node.show() + ' '); inOrder( node.right ); } }
怎么樣,了解了原理,實現(xiàn)起來還是蠻簡單的~
我們用一段代碼來測試一下我們所寫的中序遍歷:
var nums = new BST(); //插入數(shù)據(jù) nums.insert(23); nums.insert(45); nums.insert(16); nums.insert(37); nums.insert(3); nums.insert(99); nums.insert(22);
上述插入數(shù)據(jù)后,會形成如下的二叉樹
BST
中序遍歷結(jié)果如下:
//中序遍歷 console.log("Inorder traversal: "); inOrder(nums.root); // Inorder traversal: // 3 16 22 23 37 45 99
preOrder:先序遍歷
有了中序遍歷的基礎(chǔ),相信先序遍歷的實現(xiàn)你已經(jīng)想出來,怎么樣?看看對嗎?
//先序遍歷 function preOrder( node ) { if( !(node == null )){ console.log( node.show() + ' '); preOrder( node.left ); preOrder( node.right ); } }
怎么樣,看起來是不是和中序遍歷差不多,唯一的區(qū)別就是 if 語句中代碼的執(zhí)行順序,中序遍歷中 show 方法放在兩個遞歸調(diào)用之間,先序遍歷則放在遞歸調(diào)用之前。
先序遍歷結(jié)果如下:
// 先序遍歷 console.log("Preorder traversal: "); preOrder(nums.root); // Preorder traversal: // 23 16 3 22 45 37 99
postOrder:后序遍歷
后序遍歷的實現(xiàn)和前面的基本相同,將 show 方法放在遞歸調(diào)用之后執(zhí)行即可
//后序遍歷 function postOrder ( node ) { if( !(node == null ) ){ postOrder( node.left ); postOrder( node.right ); console.log( node.show() + ' '); } }
后序遍歷結(jié)果如下:
// 后序遍歷 console.log("Postorder traversal: "); postOrder(nums.root); // Postorder traversal: // 3 22 16 37 99 45 23
二叉查找樹的查找運算
對于BST通常有一下三種的查找類型:
- 查找給定值
- 查找最大值
- 查找最小值
我們接下來一起來討論三種查找的方式的實現(xiàn)。
要查找BST中的最小值和最大值是非常簡單的。因為較小的值總是在左子節(jié)點上,要想查找BST中的最小值,只需遍歷左子樹,直到找到最后一個節(jié)點即可。同理,查找最大值,只需遍歷右子樹,直到找到最后一個節(jié)點即可。
getMin:查找最小值
遍歷左子樹,直到左子樹的某個節(jié)點的 left 為 null 時,該節(jié)點保存的即為最小值
//查找最小值 function getMin( ) { var current = this.root; while ( !( current.left == null ) ){ current = current.left; } return current.show(); }
getMax:查找最大值
遍歷右子樹,直到右子樹的某個節(jié)點的 right 為 null 時,該節(jié)點保存的即為最大值
//查找最大值 function getMax () { var current = this.root; while ( !( current.right == null ) ) { current = current.right; } return current.show(); }
我們還是利用前面構(gòu)建的樹來測試:
// 最小值 console.log('min:' + nums.getMin() ); // min : 3 //最大值 console.log('max:' + nums.getMax() ); // max : 99
在BST上查找給定值,需要比較給定值和當(dāng)前節(jié)點保存的值的大小,通過比較,就能確定給定值在不在當(dāng)前節(jié)點,根據(jù)BST的特點,qu接下來是向左還是向右遍歷;
//查找給定值 function find ( data ) { var current = this.root; while ( current != null ){ if( current.data == data ){ return current; }else if( current.data < data ){ current = current.right; }else{ current = current.left; } } return null; }
如果找到給定值,該方法返回保存該值的節(jié)點,反之返回null;
//查找不存在的值 console.log('find:' + nums.find(66)); // find : null //查找存在的值 console.log('find:' + nums.find(99) ); // find : [object Object]
二叉查找樹的刪除運算
從BST中刪除節(jié)點的操作最為復(fù)雜,其復(fù)雜程度取決于刪除的節(jié)點位置。如果待刪除的節(jié)點沒有子節(jié)點,那么非常簡單。如果刪除包含左子節(jié)點或者右子節(jié)點,就變得稍微有些復(fù)雜。如果刪除包含兩個節(jié)點的節(jié)點最為復(fù)雜。
我們采用遞歸方法,來完成復(fù)雜的刪除操作,我們定義 remove() 和 removeNode() 兩個方法;算法思想如下:
- 首先判斷當(dāng)前節(jié)點是否包含待刪除的數(shù)據(jù),如果包含,則刪除該節(jié)點;如果不包含,則比較當(dāng)前節(jié)點上的數(shù)據(jù)和待刪除樹的的大小關(guān)系。如果待刪除的數(shù)據(jù)小于當(dāng)前節(jié)點的數(shù)據(jù),則移至當(dāng)前節(jié)點的左子節(jié)點繼續(xù)比較;如果大于,則移至當(dāng)前節(jié)點的右子節(jié)點繼續(xù)比較。
- 如果待刪除節(jié)點是葉子節(jié)點(沒有子節(jié)點),那么只需要將從父節(jié)點指向它的鏈接指向變?yōu)閚ull;
- 如果待刪除節(jié)點含有一個子節(jié)點,那么原本指向它的節(jié)點需要做調(diào)整,使其指向它的子節(jié)點;
- 最后,如果待刪除節(jié)點包含兩個子節(jié)點,可以選擇查找待刪除節(jié)點左子樹上的最大值或者查找其右子樹上的最小值,這里我們選擇后一種。
因此,我們需要一個查找樹上最小值的方法,后面會用它找到最小值創(chuàng)建一個臨時節(jié)點,將臨時節(jié)點上的值復(fù)制到待刪除節(jié)點,然后再刪除臨時節(jié)點;
我們上面說會用到兩個方法,其中 remove 方法只是簡單的接收待刪除數(shù)據(jù),調(diào)用 removeNode 刪除它,主要工作在 removeNode 中完成,定義如下:
//刪除操作 function remove( data ) { removeNode( this.root , data); } //查找最小值 function getSmallest(node) { if (node.left == null) { return node; } else { return getSmallest(node.left); } } //刪除節(jié)點 function removeNode( node , data ) { if( node == null ) { return null; } if(data == node.data) { // 沒有子節(jié)點的節(jié)點 if(node.left == null && node.right == null) { return null; } // 沒有左子節(jié)點的節(jié)點 if(node.left == null) { return node.right; } // 沒有右子節(jié)點的節(jié)點 if(node.right == null) { return node.left; } // 有2個子節(jié)點的節(jié)點 var tempNode = getSmallest(node.right); node.data = tempNode.data; node.right = removeNode(node.right,tempNode.data); return node; }else if(data < node.data) { node.left = removeNode( node.left,data); return node; }else { node.right = removeNode( node.right,data); return node; } }
現(xiàn)在我們來刪除節(jié)點試試。
//刪除根節(jié)點 nums.remove(23); inOrder(nums.root); // 3 16 22 37 45 99
成功了!現(xiàn)在,我們的BST算是完整了。
我們現(xiàn)在已經(jīng)可以利用js是實現(xiàn)一個簡單的BST了,實際中樹的使用會很廣泛,希望大家能多去了解了解樹的數(shù)據(jù)結(jié)構(gòu),多動手實踐,相信你會有不少的收獲!
感興趣的朋友可以使用在線HTML/CSS/JavaScript代碼運行工具:http://tools.jb51.net/code/HtmlJsRun測試上述代碼運行效果。
更多關(guān)于JavaScript相關(guān)內(nèi)容感興趣的讀者可查看本站專題:《JavaScript數(shù)學(xué)運算用法總結(jié)》、《JavaScript數(shù)據(jù)結(jié)構(gòu)與算法技巧總結(jié)》、《JavaScript數(shù)組操作技巧總結(jié)》、《JavaScript排序算法總結(jié)》、《JavaScript遍歷算法與技巧總結(jié)》、《JavaScript查找算法技巧總結(jié)》及《JavaScript錯誤與調(diào)試技巧總結(jié)》
希望本文所述對大家JavaScript程序設(shè)計有所幫助。
- JavaScript數(shù)據(jù)結(jié)構(gòu)與算法之二叉樹遍歷算法詳解【先序、中序、后序】
- JavaScript數(shù)據(jù)結(jié)構(gòu)與算法之二叉樹實現(xiàn)查找最小值、最大值、給定值算法示例
- JavaScript數(shù)據(jù)結(jié)構(gòu)與算法之二叉樹添加/刪除節(jié)點操作示例
- js實現(xiàn)無限層級樹形數(shù)據(jù)結(jié)構(gòu)(創(chuàng)新算法)
- javascript將扁平的數(shù)據(jù)轉(zhuǎn)為樹形結(jié)構(gòu)的高效率算法
- 淺談JavaScript構(gòu)造樹形結(jié)構(gòu)的一種高效算法
- JavaScript樹結(jié)構(gòu)深度優(yōu)先算法
相關(guān)文章
javascript 用函數(shù)語句和表達(dá)式定義函數(shù)的區(qū)別詳解
本篇文章主要介紹了javascript 用函數(shù)語句和表達(dá)式定義函數(shù)的區(qū)別。需要的朋友可以過來參考下,希望對大家有所幫助2014-01-01javascript window.confirm確認(rèn) 取消對話框?qū)崿F(xiàn)代碼小結(jié)
本文章講述的三種都是基于了javascript confirm提示確認(rèn)框的做法了,只是在不同的地方寫哦,有需要的同學(xué)可參考一下2012-10-10layui 實現(xiàn)自動選擇radio單選框(checked)的方法
今天小編就為大家分享一篇layui 實現(xiàn)自動選擇radio單選框(checked)的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-09-09Bootstrap Fileinput 4.4.7文件上傳實例詳解
這篇文章主要為大家詳細(xì)介紹了Bootstrap Fileinput 4.4.7文件上傳實例,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-07-07JavaScript之創(chuàng)意時鐘項目(實例講解)
下面小編就為大家?guī)硪黄狫avaScript之創(chuàng)意時鐘項目。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-10-10uniapp?u-picker多列用法以及設(shè)置默認(rèn)選中值
uview組件庫u-picker組件可能很多人不熟悉,下面這篇文章主要給大家介紹了關(guān)于uniapp?u-picker多列用法以及設(shè)置默認(rèn)選中值的相關(guān)資料,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06原生js實現(xiàn)倒計時功能(多種格式調(diào)用)
本文主要介紹了原生js實現(xiàn)倒計時(多種格式調(diào)用)的方法,具有一定的參考價值,下面跟著小編一起來看下吧2017-01-01Bootstrap~多級導(dǎo)航(級聯(lián)導(dǎo)航)的實現(xiàn)效果【附代碼】
下面小編就為大家分享一篇Bootstrap~多級導(dǎo)航(級聯(lián)導(dǎo)航)的實現(xiàn)效果【附代碼】。小編覺得挺不錯。希望對大家有所幫助。一起跟隨小編過來看看吧2016-03-03