1. 程式人生 > >數據結構第六講: 樹

數據結構第六講: 樹

後序遍歷 因此 模型 過程 else roo pri 二叉 ==

第六講 樹

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

技術分享圖片

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

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

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

二叉樹

二叉樹最多只能有·2個子節點。

技術分享圖片

如:B為A的左側子節點。E為A的右側子節點。

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

技術分享圖片

功能的逐步實現

js創建一棵二叉樹(BinarySearchTree),可以借鑒鏈表的思路

技術分享圖片

還記得鏈表(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)

輸出結果轉化之後:

技術分享圖片

樹的遍歷

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

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

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

中序遍歷:從最小到最大

中序遍歷的順序是“從最小到最大”。

技術分享圖片

  • 每次遞歸前,應檢查傳入的節點是否為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()在判斷前執行。如果是在判斷後執行,會是什麽結果呢?

數據結構第六講: 樹