使用JavaScript浅谈树

什么是树?

没错,就是森林中的树哈哈。其实一两句话很难说清楚这个玩意儿,请自行百度,但是碍于形式,给出一个简单的定义:树有一组以边链接的节点组成。

但是要注意了哈,这个定义给出了很多信息:

1.节点(说明我们要创建一个节点类,代表树中的每一个节点的数据结构)

2.边(就是当前节点指向另一个节点的链接,就是边,提前透露一下,我定义的节点类中left和right就是边)

正如题目所说,是浅谈树,那就不做过多理论上的分析,要想知道理论,还是那句话请自行百度,好吧!。

还多bb一句,树一定是有一个跟节点的。

下面给出树中的每一个节点的数据类型,且看代码:

class Node {
    constructor(data,left,right) {
        // 节点保存的数据
        this.data = data;
        // 节点的左子节点
        this.left = left;
        // 节点的右子节点
        this.right = right;
        // 该节点保存的数据出现的次数,默认只出现一次
        this.count = 1;
    }
}

看,这就是一个节点的定义,不复杂是吧。这里解释一下为什么说left和right是边,你想这两个属性是指向子节点的,如果有指向(也就是子节点存在),那么这两个节点之间是不是就有一个联系,而这个联系就是用left和right来表示的,所以说就是树的边。(解释的还可以把,自夸中)。

现在我们来想一下树有些什么操作,算了不想了,我给出了下面几种操作,不够的自行补充:(tip:我们现在写的树是根据二叉查找树BST来定义的)。

简单的解释一下BST的定义:

1.树中每个节点的子节点不允许超过两个(也就是二叉树的定于)

2.相对较小的值保存在左子节点中

3.相对较大的值保存在右子节点中

看有哪些骚操作:

增加节点

1.如果树中没有跟节点,那么插入的节点就是跟节点

2.如果有了树在插入之前有了根节点,就比较麻烦一点,且看怎么处理

  2.1 设置当前节点为根节点。

      2.2 如果新节点的值小于当前节点的值,则设置当前节点为原节点的左节点;反之,执行2.4

  2.3. 如果当前节点左节点为null,那就把新节点插入到这个位置,退出循环;反之,继续执行下一次循环

  2.4. 如果新节点的值大于当前节点的值,则设置当前节点为原节点的右节点;

  2.5. 如果当前节点右节点为null,那就把新节点插入到这个位置,退出循环;反之,继续执行下一次循环

ok,有了这个思考过程,我们来写一下把,毕竟光说不练假把式:

//
class BST {
    constructor() {
        this.root = null;
    }
    // insert:插入节点函数
    insert(data) {
        var node = new Node(data,null,null);
        if (this.root === null) {
            this.root = node;
            return;
        }
        var current = this.root;
        var parent;
        while (true) {
            parent = current;
            if (data < current.data) {
                current = current.left;
                if (current === null) {
                    parent.left = node;
                    break;
                }
            }
            else {
                current = current.right;
                if (current === null) {
                    parent.right = node;
                    break;
                }
            }
        }
    }
}

看插入函数,算法一大推,实际代码还是很少的。

遍历节点

1. 中序遍历(按照节点上的值,以升序访问BST上所有的节点)

2. 先序遍历(先访问根节点,然后以同样的方式访问左子树和右子树)

3. 后序遍历(先访问叶子节点,从左子树到有子树,在到根节点)

不要问我为什么会存在这三种遍历方式,(解释起来很麻烦,百度看别人的反而更加容易理解)

我们来看看三种遍历方式的实现

class BST {
    constructor() {
        this.root = null;
    }
    // insert:插入节点函数
    insert(data) {
        var node = new Node(data,null,null);
        if (this.root === null) {
            this.root = node;
            return;
        }
        var current = this.root;
        var parent;
        while (true) {
            parent = current;
            if (data < current.data) {
                current = current.left;
                if (current === null) {
                    parent.left = node;
                    break;
                }
            }
            else {
                current = current.right;
                if (current === null) {
                    parent.right = node;
                    break;
                }
            }
        }
    }
    // inOrder:中序遍历
    inOrder(node) {
        if (node !== null) {
            this.inOrder(node.left);
            console.log(node.show());
            this.inOrder(node.right);
        }
    }
    // preOrder:先序遍历
    preOrder(node) {
        if (node !== null) {
            console.log(node.show());
            this.preOrder(node.left);
            this.preOrder(node.right);
        }
    }
    // postOrder: 后序遍历
    postOrder(node) {
        if (node !== null) {
            this.preOrder(node.left);
            this.preOrder(node.right);
            console.log(node.show());
        }
    }
}

我相信,睿智的你是不会被这个递归思想难倒的,我就不多bb了,我们来测试一下:

 

 

还行,没事看点黄色的东西。

查找操作 

不知道你还记得我们插入节点的时候,有一个很巧妙的操作,那就是相对于较小的节点总是放在左边的,相对较大的节点总是放在右边的。有了这个特性,那么我们在二叉查找树中进行查找岂不是很简单。下面实现三种查找

1. 查找最大值

2.查找最小值

3.查找给定的值


代码如下:

// max:查找最大值
    max() {
        var current = this.root;
        while (current.right !== null) {
            current = current.right;
        }
        return current.data;
    }
    // min:查找最小值
    min() {
        var current = this.root;
        while (current.left !== null) {
            current = current.left;
        }
        return current.data;
    }
    // find:查找给定值
    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
    }

老规矩,测试一下:

 

 接下来就是删除节点操作了。

节点删除

其实节点删除是最复杂的操作,因为删除的节点不一样,对应的操作也不一样,树的构成不一样,那么操作也不一样。举个栗子,比如树只有一个根节点,那么就直接删除了,什么操作又不用进行。但是万一,该节点下面还有一个左节点或者右节点或者左右节点都存在了?所以我们要思考一下。

这里为了让大家弄明白删除的操作思考,我就照搬别人的解释了:

从BST中删除节点的第一步是判断当前节点是否包含待删除的节点,如果包含,则删除该节点;如果不包含,则比较当前节点上的数据和待删除数据的。如果待删除数据小于当前节点上的数据,则移至当前节点的左子节点继续操比较;如果待删除数据大于当前节点上的数据,则移至当前节点的右子节点继续比较。

如果待删除节点是叶子节点(没有子节点),那么只需要将父节点指向它的链接指向null,也就是删除当前边。

如果待删除节点只包含一个子节点,那么原本指向它的节点就要做一些调整,使其指向它的子节点。

如果待删除节点包含两个子节点,正确的做法有两种:要么查找待删除节点左子树上的最大值,要么就查找待删除节点右子树上的最小值。(这里我们选择第一种方法)

我们需要一个查找子树上最大值的方法,后面会用它的值创建一个临时节点,将临时节点上的值复制到待删除节点上,就完成了删除节点,然后删除这个临时节点。

为什么要这么操作:因为我们知道,二叉查找树的左边是相对较小的,右边是相对较大的(看insert方法或者定义),那么我们删除它的父节点,父节点删除了,它的子节点应该要保存下来,还是要构成二叉查找树的形态,所以只有左子树的最大值或者右子树上最小的值去替代父节点,才能继续维持这个结构(你说是不是,嘿嘿)

好了,bb一大推,我们来看一下代码怎么实现:

// remove:删除节点
    remove(data) {
        this.root =  this.removeNode(this.root,data);
    }
    //removeNode:实际操作删除的方法
    removeNode(node,data) {
        if (node === null) {
            return null;
        }
        if (data === node.data) {
            // 没有子节点
            if(node.left === null && node.right === null) {
                return null;
            }
            // 没有左节点
            if (node.left === null) {
                return node.right;
            } 
            // 没有右节点
            if(node.right === null) {
                return node.left;
            }
            // 有两个节点
            var tempNode = this.getMax(node.left);
            node.data = tempNode.data;
            node.left = this.removeNode(node.left,tempNode.data);
            return node;
        }
        else if (data < node.data) {
            node.left = this.removeNode(node.left,data);
            return node;
        }
        else {
            node.right = this.removeNode(node.right,data);
            return node;
        }
    }
    // getMax:获取左子树上的最大值
    getMax(node) {
        var current = node;
        while (current.right !== null) {
            current = current.right;
        }
        return current.data;
    }

不得不承认,这段代码需要好好回味一下。

测试一下:

 

简直就是ojbk。

还坚持一下,我们在来看一个计数功能。

计数

 BST的一个用途就是记录一组数据出现的次数,所以我们现在要修改一下新增函数,如果树中有该节点那么我们将其count加一,没有就直接插入节点。

看看代码:

 // insert:插入节点函数
    insert(data) {
        var node = new Node(data, null, null);
        if (this.root === null) {
            this.root = node;
            return;
        }
        var hasNode = this.find(data);
        if (hasNode) {
            // 更新计数
            this.update(hasNode);
            return;
        }
        var current = this.root;
        var parent;
        while (true) {
            parent = current;
            if (data < current.data) {
                current = current.left;
                if (current === null) {
                    parent.left = node;
                    break;
                }
            }
            else {
                current = current.right;
                if (current === null) {
                    parent.right = node;
                    break;
                }
            }
        }
    }
    // update:更新节点计算
    update(node){
        node.count++;
    }

同时为了方便我们看结果,我修改了一下Node类的show方法:

 // show:显示当前节点的值
    show() {
        console.log(this.data+'出现了'+this.count+'次');
        return this.data;
    }

测试一下:

 

var bst = new BST();
bst.insert(3);
bst.insert(4);
bst.insert(5);
bst.insert(2);
bst.insert(1);
bst.insert(1);
bst.insert(2);
bst.insert(4);
bst.insert(4);
bst.preOrder(bst.root);
这里本来是有截图的,但是我上传的时候,遇到502了,擦,简直就是打压我,有兴趣自己去输出看一下。
posted @ 2020-03-21 22:11  只会一点前端  阅读(368)  评论(0编辑  收藏  举报