数据结构第六讲: 树

第六讲 树

树是一种分层数据的抽象模型。最常见的树是家谱。(图来自网络)

img

在明代世系表这棵树中,所有的皇帝都被称为节点。朱元璋称为根节点。后代是皇帝的节点,称为内部节点。没有子元素的节点比如明思宗朱由检称为外部节点叶节点。朱棣及其后代节点称为朱元璋的子树

以明宣宗朱瞻基为例子,他拥有三个祖先节点。因此他的深度为3。

树的高度取决于节点深度的最大值。根节点出于第0层。朱棣属于第二层。以此类推。整个世系表中,他的高度为12。

二叉树

二叉树最多只能有·2个子节点。

img

如: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)

输出结果转化之后:

树的遍历

遍历一棵树,应当从顶层,左层还是右层开始?

遍历的方法需要以访问者模式(回调函数)体现。

树方法最常用的就是递归。那么应如何设计?

中序遍历:从最小到最大

中序遍历的顺序是“从最小到最大”。

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()在判断前执行。如果是在判断后执行,会是什么结果呢?

猜你喜欢

转载自www.cnblogs.com/djtao/p/10720962.html