JS中的算法與數(shù)據(jù)結(jié)構(gòu)——二叉查找樹(Binary Sort Tree)

二叉查找樹(Binary Sort Tree)

我們之前所學(xué)到的列表敬锐,等都是一種線性的數(shù)據(jù)結(jié)構(gòu)免钻,今天我們將學(xué)習(xí)計(jì)算機(jī)中經(jīng)常用到的一種非線性的數(shù)據(jù)結(jié)構(gòu)——樹(Tree)糯彬,由于其存儲的所有元素之間具有明顯的層次特性纽乱,因此常被用來存儲具有層級關(guān)系的數(shù)據(jù)芳绩,比如文件系統(tǒng)中的文件掀亥;也會被用來存儲有序列表等。

在樹結(jié)構(gòu)中妥色,每一個結(jié)點(diǎn)只有一個前件搪花,稱為父結(jié)點(diǎn),沒有前件的結(jié)點(diǎn)只有一個嘹害,稱為樹的根結(jié)點(diǎn)撮竿,簡稱樹的(root)。每一個結(jié)點(diǎn)可以有多個后件笔呀,稱為該結(jié)點(diǎn)的子結(jié)點(diǎn)幢踏。沒有后件的結(jié)點(diǎn)稱為葉子結(jié)點(diǎn)。一個結(jié)點(diǎn)所擁有的子結(jié)點(diǎn)的個數(shù)稱為該結(jié)點(diǎn)的度许师,所有結(jié)點(diǎn)中最大的度稱為樹的度房蝉。樹的最大層次稱為樹的深度。

二叉樹

二叉樹是一種特殊的樹微渠,它的子節(jié)點(diǎn)個數(shù)不超過兩個搭幻,且分別稱為該結(jié)點(diǎn)的左子樹(left subtree)與右子樹(right subtree),二叉樹常被用作二叉查找樹和二叉堆或是二叉排序樹(BST)逞盆。

二叉樹

按一定的規(guī)則和順序走遍二叉樹的所有結(jié)點(diǎn)粗卜,使每一個結(jié)點(diǎn)都被訪問一次,而且只被訪問一次纳击,這個操作被稱為樹的遍歷,是對樹的一種最基本的運(yùn)算攻臀。由于二叉樹是非線性結(jié)構(gòu)焕数,因此,樹的遍歷實(shí)質(zhì)上是將二叉樹的各個結(jié)點(diǎn)轉(zhuǎn)換成為一個線性序列來表示刨啸。

按照根節(jié)點(diǎn)訪問的順序不同堡赔,樹的遍歷分為以下三種:前序遍歷,中序遍歷设联,后序遍歷善已;

前序遍歷:根節(jié)點(diǎn)->左子樹->右子樹

先序遍歷

中序遍歷:左子樹->根節(jié)點(diǎn)->右子樹

中序遍歷

后序遍歷:左子樹->右子樹->根節(jié)點(diǎn)

后序遍歷

因此我們可以得之上面二叉樹的遍歷結(jié)果如下:
前序遍歷:ABDEFGC
中序遍歷:DEBGFAC
后序遍歷:EDGFBCA

二叉查找樹(BST)

實(shí)際應(yīng)用中灼捂,樹的每個節(jié)點(diǎn)都會有一個與之相關(guān)的值對應(yīng),有時候會被稱為换团。因此悉稠,我們在構(gòu)建二叉查找樹的時候,確定子節(jié)點(diǎn)非常的重要艘包,通常將相對較小的值保存在左節(jié)點(diǎn)中的猛,較大的值保存在右節(jié)點(diǎn)中,這就使得查找的效率非常高想虎,因此被廣泛使用卦尊。

二叉查找樹的實(shí)現(xiàn)

根據(jù)上面的知識,我們了解到二叉樹實(shí)際上是由多個節(jié)點(diǎn)組成舌厨,因此我們首先就要定義一個Node類岂却,用于存放樹的節(jié)點(diǎn),其構(gòu)造與前面的鏈表類似裙椭。Node類的定義如下:

//節(jié)點(diǎn)定義

function Node (data , left , right) {
    this.data = data;       // 數(shù)據(jù)
    this.left = left;       // 左節(jié)點(diǎn)
    this.right = right;     // 右節(jié)點(diǎn)
    this.show = show;       // 顯示節(jié)點(diǎn)數(shù)據(jù)
}

function show(){
    return this.data;
}

Node對象既保存了數(shù)據(jù)躏哩,也保存了它的左節(jié)點(diǎn)和右節(jié)點(diǎn)的鏈接,其中 show 方法用來顯示當(dāng)前保存在節(jié)點(diǎn)中數(shù)據(jù)骇陈。

現(xiàn)在我們可以創(chuàng)建一個類震庭,用來表示二叉查找數(shù)(BST),我們初始化類只包含一個成員你雌,一個表示二叉查找樹根節(jié)點(diǎn)的 Node 對象器联,初始化為 null , 表示創(chuàng)建一個空節(jié)點(diǎn)婿崭。

//二叉查找樹(BST)的類

function BST(){
    this.root = null;           // 根節(jié)點(diǎn)
    this.insert = insert;       // 插入節(jié)點(diǎn)
    this.preOrder = preOrder;   // 先序遍歷
    this.inOrder = inOrder;     // 中序遍歷
    this.postOrder = postOrder; // 后序遍歷
    this.find = find;           // 查找節(jié)點(diǎn)
    this.getMin = getMin;       // 查找最小值
    this.getMax = getMax;       // 查找最大值
    this.remove = remove;       // 刪除節(jié)點(diǎn)
}

現(xiàn)在拨拓,我們需要為我們的類添加方法。

首先就是 insert 方法氓栈,向樹中添加一個新節(jié)點(diǎn)渣磷,我們一起來看看這個方法;

insert:向樹中添加新節(jié)點(diǎn)

因?yàn)樘砑庸?jié)點(diǎn)會涉及到插入位置的問題授瘦,必須將其放到正確的位置上醋界,才能保證樹的正確性,整個過程較為復(fù)雜提完,我們一起來梳理一下:

首先要添加新的節(jié)點(diǎn)形纺,首先需要創(chuàng)建一個Node對象,將數(shù)據(jù)傳入該對象徒欣。

其次要檢查當(dāng)前的BST樹是否有根節(jié)點(diǎn)逐样,如果沒有,那么表示是一棵新數(shù),該節(jié)點(diǎn)就為該樹的根節(jié)點(diǎn)脂新,那么插入這個過程就結(jié)束了挪捕;否則,就要繼續(xù)進(jìn)行下一步了争便。

如果待插入節(jié)點(diǎn)不是根節(jié)點(diǎn)级零,那么就必須對BST進(jìn)行遍歷,找到合適的位置始花。該過程類似遍歷鏈表妄讯,用一個變量存儲當(dāng)前節(jié)點(diǎn),一層一層遍歷BST酷宵,算法如下:

  1. 設(shè)值當(dāng)前節(jié)點(diǎn)為根節(jié)點(diǎn)
  2. 如果待插入節(jié)點(diǎn)保存的數(shù)據(jù)小于當(dāng)前節(jié)點(diǎn)亥贸,則新節(jié)點(diǎn)為原節(jié)點(diǎn)的左節(jié)點(diǎn),反之浇垦,執(zhí)行第4步
  3. 如果當(dāng)前節(jié)點(diǎn)的左節(jié)點(diǎn)為null炕置,就將新節(jié)點(diǎn)放到這個位置,退出循環(huán)男韧;反之朴摊,繼續(xù)執(zhí)行下一次循環(huán)
  4. 設(shè)置新節(jié)點(diǎn)為原節(jié)點(diǎn)的右節(jié)點(diǎn)
  5. 如果當(dāng)前節(jié)點(diǎn)的右節(jié)點(diǎn)為null,就將新節(jié)點(diǎn)放到這個位置此虑,退出循環(huán)甚纲;反之,繼續(xù)執(zhí)行下一次循環(huán)

這樣朦前,就能保證每次添加的新節(jié)點(diǎn)能夠放到正確的位置上介杆,具體實(shí)現(xiàn)如下;

//插入新節(jié)點(diǎn)

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é)點(diǎn)春哨,我們需要有遍歷BST的能力,上面我們也提到了是三種遍歷方式恩伺。其中中序遍歷是最容易實(shí)現(xiàn)的赴背,我們需要升序的方法訪問樹中的所有節(jié)點(diǎn),先訪問左子樹晶渠,在訪問根節(jié)點(diǎn)凰荚,最后是右子樹,我們采用遞歸來實(shí)現(xiàn)褒脯!

inOrder:中序遍歷

 // 中序遍歷
 
function inOrder (node) {
    if( !(node == null )){
        inOrder( node.left );
        console.debug( node.show() + ' ');
        inOrder( node.right );
    }
}

怎么樣浇揩,了解了原理,實(shí)現(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ǔ),相信先序遍歷的實(shí)現(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:后序遍歷

后序遍歷的實(shí)現(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

二叉查找樹的查找運(yùn)算

對于BST通常有一下三種的查找類型:

  1. 查找給定值
  2. 查找最大值
  3. 查找最小值

我們接下來一起來討論三種查找的方式的實(shí)現(xiàn)。

要查找BST中的最小值和最大值是非常簡單的硫痰。因?yàn)檩^小的值總是在左子節(jié)點(diǎn)上衩婚,要想查找BST中的最小值,只需遍歷左子樹效斑,直到找到最后一個節(jié)點(diǎn)即可非春。同理,查找最大值缓屠,只需遍歷右子樹奇昙,直到找到最后一個節(jié)點(diǎn)即可。

getMin:查找最小值

遍歷左子樹敌完,直到左子樹的某個節(jié)點(diǎn)的 left 為 null 時储耐,該節(jié)點(diǎn)保存的即為最小值

//查找最小值

function getMin( ) {
    var current = this.root;
    while ( !( current.left == null ) ){
        current = current.left;
    }
    return current.show();
}

getMax:查找最大值

遍歷右子樹,直到右子樹的某個節(jié)點(diǎn)的 right 為 null 時滨溉,該節(jié)點(diǎn)保存的即為最大值

//查找最大值
 
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é)點(diǎn)保存的值的大小,通過比較业踏,就能確定給定值在不在當(dāng)前節(jié)點(diǎn)禽炬,根據(jù)BST的特點(diǎn),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é)點(diǎn),反之返回null伐脖;

//查找不存在的值
console.log('find:' + nums.find(66));       // find : null

//查找存在的值
console.log('find:' + nums.find(99) );      // find : [object Object]

二叉查找樹的刪除運(yùn)算

從BST中刪除節(jié)點(diǎn)的操作最為復(fù)雜热幔,其復(fù)雜程度取決于刪除的節(jié)點(diǎn)位置。如果待刪除的節(jié)點(diǎn)沒有子節(jié)點(diǎn)讼庇,那么非常簡單绎巨。如果刪除包含左子節(jié)點(diǎn)或者右子節(jié)點(diǎn),就變得稍微有些復(fù)雜蠕啄。如果刪除包含兩個節(jié)點(diǎn)的節(jié)點(diǎn)最為復(fù)雜场勤。

我們采用遞歸方法戈锻,來完成復(fù)雜的刪除操作,我們定義 remove() 和 removeNode() 兩個方法和媳;算法思想如下:

  1. 首先判斷當(dāng)前節(jié)點(diǎn)是否包含待刪除的數(shù)據(jù)格遭,如果包含,則刪除該節(jié)點(diǎn)留瞳;如果不包含拒迅,則比較當(dāng)前節(jié)點(diǎn)上的數(shù)據(jù)和待刪除樹的的大小關(guān)系。如果待刪除的數(shù)據(jù)小于當(dāng)前節(jié)點(diǎn)的數(shù)據(jù)她倘,則移至當(dāng)前節(jié)點(diǎn)的左子節(jié)點(diǎn)繼續(xù)比較璧微;如果大于,則移至當(dāng)前節(jié)點(diǎn)的右子節(jié)點(diǎn)繼續(xù)比較硬梁。
  2. 如果待刪除節(jié)點(diǎn)是葉子節(jié)點(diǎn)(沒有子節(jié)點(diǎn))前硫,那么只需要將從父節(jié)點(diǎn)指向它的鏈接指向變?yōu)閚ull;
  3. 如果待刪除節(jié)點(diǎn)含有一個子節(jié)點(diǎn)靶溜,那么原本指向它的節(jié)點(diǎn)需要做調(diào)整开瞭,使其指向它的子節(jié)點(diǎn);
  4. 最后罩息,如果待刪除節(jié)點(diǎn)包含兩個子節(jié)點(diǎn)嗤详,可以選擇查找待刪除節(jié)點(diǎn)左子樹上的最大值或者查找其右子樹上的最小值,這里我們選擇后一種瓷炮。

因此葱色,我們需要一個查找樹上最小值的方法,后面會用它找到最小值創(chuàng)建一個臨時節(jié)點(diǎn)娘香,將臨時節(jié)點(diǎn)上的值復(fù)制到待刪除節(jié)點(diǎn)苍狰,然后再刪除臨時節(jié)點(diǎn);

我們上面說會用到兩個方法烘绽,其中 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é)點(diǎn)
function removeNode( node , data ) {
    if( node == null ) {
        return null;
    }
    if(data == node.data) {
        // 沒有子節(jié)點(diǎn)的節(jié)點(diǎn)
        if(node.left == null && node.right == null) {
            return null;
        }
        // 沒有左子節(jié)點(diǎn)的節(jié)點(diǎn)
        if(node.left == null) {
            return node.right;
        }
        // 沒有右子節(jié)點(diǎn)的節(jié)點(diǎn)
        if(node.right == null) {
            return node.left;
        }
        // 有2個子節(jié)點(diǎn)的節(jié)點(diǎn)
        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é)點(diǎn)試試翔忽。

//刪除根節(jié)點(diǎn)
nums.remove(23);

inOrder(nums.root);

// 3 16 22 37 45 99

成功了!
現(xiàn)在盏檐,我們的BST算是完整了歇式。

我們現(xiàn)在已經(jīng)可以利用js是實(shí)現(xiàn)一個簡單的BST了,實(shí)際中樹的使用會很廣泛胡野,希望大家能多去了解了解樹的數(shù)據(jù)結(jié)構(gòu)材失,多動手實(shí)踐,相信你會有不少的收獲硫豆!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末龙巨,一起剝皮案震驚了整個濱河市笼呆,隨后出現(xiàn)的幾起案子册舞,更是在濱河造成了極大的恐慌安疗,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件晌纫,死亡現(xiàn)場離奇詭異昼榛,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)剔难,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門胆屿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人偶宫,你說我怎么就攤上這事非迹。” “怎么了纯趋?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵憎兽,是天一觀的道長。 經(jīng)常有香客問我吵冒,道長纯命,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任痹栖,我火速辦了婚禮亿汞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘揪阿。我一直安慰自己疗我,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布南捂。 她就那樣靜靜地躺著吴裤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪溺健。 梳的紋絲不亂的頭發(fā)上麦牺,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機(jī)與錄音矿瘦,去河邊找鬼枕面。 笑死,一個胖子當(dāng)著我的面吹牛缚去,可吹牛的內(nèi)容都是我干的潮秘。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼易结,長吁一口氣:“原來是場噩夢啊……” “哼枕荞!你這毒婦竟也來了柜候?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤躏精,失蹤者是張志新(化名)和其女友劉穎渣刷,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體矗烛,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡辅柴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了瞭吃。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碌嘀。...
    茶點(diǎn)故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖歪架,靈堂內(nèi)的尸體忽然破棺而出股冗,到底是詐尸還是另有隱情,我是刑警寧澤和蚪,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布止状,位于F島的核電站,受9級特大地震影響攒霹,放射性物質(zhì)發(fā)生泄漏怯疤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一剔蹋、第九天 我趴在偏房一處隱蔽的房頂上張望旅薄。 院中可真熱鬧,春花似錦泣崩、人聲如沸少梁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽凯沪。三九已至,卻和暖如春买优,著一層夾襖步出監(jiān)牢的瞬間妨马,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工杀赢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留烘跺,地道東北人。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓脂崔,卻偏偏與公主長得像滤淳,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子砌左,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容