數據結構第六講: 樹

第六講 樹

樹是一種分層數據的抽象模型。最多見的樹是家譜。(圖來自網絡)$h _r$javascript

img

在明代世系表這棵樹中,全部的皇帝都被稱爲節點。朱元璋稱爲根節點。後代是皇帝的節點,稱爲內部節點。沒有子元素的節點好比明思宗朱由檢稱爲外部節點葉節點。朱棣及其後代節點稱爲朱元璋的子樹java

以明宣宗朱瞻基爲例子,他擁有三個祖先節點。所以他的深度爲3。node

樹的高度取決於節點深度的最大值。根節點出於第0層。朱棣屬於第二層。以此類推。整個世系表中,他的高度爲12。json

二叉樹

二叉樹最多隻能有·2個子節點。後端

img

如:B爲A的左側子節點。E爲A的右側子節點。網絡

二叉搜索樹(BST)是一種特殊的節點。左側子節點存放比父節點小的值。右側子節點存放大於等於父節點的值、數據結構

功能的逐步實現

js建立一棵二叉樹(BinarySearchTree),能夠借鑑鏈表的思路ide

還記得鏈表(linkList)嗎,能夠經過指針來表示節點之間的關係。同時,還能夠用對象來實現這個二叉樹,函數

實現如下功能:工具

  • insert(key):在樹中插入一個新鍵
  • search(key):在樹中查找一個鍵,存在則返回true,不然爲false
  • inOderTraverse,preOderTraverse,postOderTraverse:中序/先序/後序遍歷全部節點
  • min/max:返回樹中最小/最大的鍵值
  • remove:從樹中移除某個鍵。

插入節點

// 樹
class BinarySearchTree{
    constructor(){
        this.Node=function(key){
            this.key=key;
            this.left=null;
            this.right=null;
        }

        this.root=null
        this.insertNode=this.insertNode.bind(this)
    }

    insertNode(_root,_node){
        if(_root.key>_node.key){
            if(_root.left==null){
                _root.left=_node;
            }else{
                this.insertNode(_root.left,_node);
            }
        }else{
            if(_root.right==null){
                _root.right=_node;
            }else{
                this.insertNode(_root.right,_node)
            }
        }
    }

    // 插入
    insert(key){
        let Node=this.Node;
        let node=new Node(key);
        if(this.root==null){
            this.root=node;
        }else{
            this.insertNode(this.root,node)
        }
    }
}

跑一下測試用例:

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

輸出結果轉化以後:

樹的遍歷

遍歷一棵樹,應當從頂層,左層仍是右層開始?

遍歷的方法須要以訪問者模式(回調函數)體現。

樹方法最經常使用的就是遞歸。那麼應如何設計?

中序遍歷:從最小到最大

中序遍歷的順序是「從最小到最大」。

img

  • 每次遞歸前,應檢查傳入的節點是否爲null。這是遞歸中止的條件。
  • 調用相同的函數訪問左側子節點。直到找到最小的。
  • 訪問完了,再訪問最近的右側節點,直到不可訪問。
// 中序遍歷
    inOrderTraverse(callback){
        // 中序遍歷所需的必要方法
        const inOrderTraverseNode=(_root,_callback=()=>{})=>{
            // 從頂層開始遍歷
            if(_root!==null){
                inOrderTraverseNode(_root.left,_callback);
                _callback(_root.key);
                inOrderTraverseNode(_root.right,_callback);
            }
        }
        inOrderTraverseNode(this.root,callback);
    }

打印結果發現,其實這個遍歷實現了樹的key值從小到大排列。

a.inOrderTraverse((key)=>{console.log(key)})
// 3 5 6 7 8 9 10 11 12 13 14 15 18 20 25
先序遍歷:如何打印一個結構化的數據結構

先序遍歷的過程:

这里写图片描述

先把左側子節點所有訪問完了,再尋找一個距此時位置(「親緣關係」)最近的右側節點。

preOrderTraverse(callback){
        // 中序遍歷所需的必要方法
        const preOrderTraverseNode=(_root,_callback=()=>{})=>{
            // 從頂層開始遍歷
            if(_root!==null){
                _callback(_root.key);
                preOrderTraverseNode(_root.left,_callback);
                preOrderTraverseNode(_root.right,_callback);
            }
        }
        preOrderTraverseNode(this.root,callback);
    }

因此,所謂先序遍歷就是把callback的位置提早了。

後序遍歷:從左到右先遍歷子代

这里写图片描述

後續遍歷是先訪問一個樹的後代節點。最後才訪問自己。

那麼後序遍歷的方法是否是把callback放到最後執行呢?

是的。簡直無腦。

// 後序遍歷
    postOrderTraverse(callback){
        // 中序遍歷所需的必要方法
        const postOrderTraverseNode=(_root,_callback=()=>{})=>{
            // 從頂層開始遍歷
            if(_root!==null){
                
                postOrderTraverseNode(_root.left,_callback);
                postOrderTraverseNode(_root.right,_callback);
                _callback(_root.key);//我在後面
            }
        }
        postOrderTraverseNode(this.root,callback);
    }

搜索特定值

//是否存在
    search(_key,_root){
        if(!_root){
            _root=this.root
        }

        if(!_root){
            return false;
        }else if(_root.key==_key){
            return true;
        }
        
        if(_root.key>_key){
            if(_root.left==null){
                return false;
            }else{
                if(_root.left.key==_key){
                    return true
                }else{
                    return this.search(_key,_root.left)
                }
            }
        }else{
            if(_root.right==null){
                return false
            }else{
                if(_root.right.key==_key){
                    return true
                }else{
                    return this.search(_key,_root.right)
                }
            }
        }
    }

查找最大/最小值

// 工具函數
    find(_root,side){
        if(!_root[side]){
            return _root.key
        }else{
            return this.find(_root[side],side)
        }   
    }

    // 最大值,不斷查找右邊
    max(){
        return this.find(this.root,'right')
    }

    // 最小值
    min(){
        return this.find(this.root,'left')
    }

會發現這是個很是輕鬆的事。

移除一個節點

Bst最麻煩的方法莫過於此。

  • 首先,你得找到這個節點=>遞歸終止的條件

  • 其次,判斷這個節點(_root)的父節點(parentNode)和這個節點的子節點(_root.left、_root.right)判斷:

    • 若是_root沒有子節點,那麼直接把父節點對應的side值設爲null

    • 若是_root擁有一個子節點,跳過這個節點,直接把父節點的指針指向這個子節點。

    • 若是兩個都有:

      • 找到_root右邊子樹的最小節點_node,而後令parentNode的指針指向這個節點
      • _node的父節點刪除指向_node的指針。
_remove(_node,_key,parentNode,side){
        if(_key<_node.key){
            return this._remove(_node.left,_key,_node,'left')
        }else if(_key>_node.key){
            return this._remove(_node.right,_key,_node,'right')
        }else if(_node.key==_key){
            
            // 頂層:移除根節點
            if(!parentNode){
                this.root=null;
                return this.root;
            }else{
                if(!_node.left&&!_node.right){
                    // 刪除的若是是葉節點
                    parentNode[side]=null
                }else if(_node.left&&!_node.right){
                    let tmp=_node.left;
                    parentNode[side]=tmp

                }else if(_node.right&&!_node.left){
                    let tmp=_node.right;
                    parentNode[side]=tmp
                }else{
                    let tmpRight=_node.right;

                    // 找到右側子樹的最小節點。__node
                    let __node=this.find(tmpRight,'left');
                    // 刪除這個節點。
                    this._remove(tmpRight,__node.key);
                    // 從新賦值
                    parentNode[side]=__node.key;
                }

                return this.root
                
            }
        }
    }

    remove(key){
        if(this.search(key)){
            return this._remove(this.root,key)
        }else{
            console.log('未找到key')
            return false;
        }
    }

a.remove(15)

打印結果以下

測試經過。

作一道練習

在實際工做生活中,好比一本書常分爲第一講,第1-1節,第2-1節...,第二講:第2-1節...

若是後端發給你一個這樣的數據:

let data = [{
    id: '1',
    children: [{
        id: `1-1`,
        children: [{
            id: '1-1-1',
            children: [{
                id: '1-1-1-1'
            },{
                id:'1-1-1-2'
            }]
        },{
            id:'1-1-2',
            children: [{
                id: '1-1-2-1'
            },{
                id:'1-1-2-2'
            }]
        }]
    },{
        id:'2',
        children:[{
            id:'2-1'
        },{
            id:'2-2',
            children:[{
                id:'2-2-1'
            },{
                id:'2-2-2',
                children: [{
                    id: '2-2-2-1'
                },{
                    id:'2-2-2-2'
                }]
            }]
        }]
    }]
}]

如何扁平化以下的json對象?

const flatJson=(_data,arr)=>{
    if(!arr){
        arr=[]
    }
    
    for(let i=0;i<_data.length;i++){
        console.log(_data[i].id)
        arr.push(_data[i].id);
        if(_data[i].children){
            flatJson(_data[i].children,arr)
        }
    }

    return arr;
}

console.log(flatJson(data))

測試用例結果經過:

能夠進一步思考:這裏arr.push()在判斷前執行。若是是在判斷後執行,會是什麼結果呢?

相關文章
相關標籤/搜索