• 使用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了,擦,简直就是打压我,有兴趣自己去输出看一下。
  • 相关阅读:
    Atitit.异步编程技术原理与实践attilax总结
    AjaxToolKit之Rating控件的使用(http://www.soaspx.com/dotnet/ajax/ajaxtech/ajaxtech_20091021_1219.html)
    JavaScript初学指南
    javascript泛型集合类(转)
    HTTP 错误 404.2 Not Found 由于 Web 服务器上的“ISAPI 和 CGI 限制”列表设置,无法提供您请求的页面
    IIS连接oralce数据库时 提示“System.Data.OracleClient 需要 Oracle 客户端软件 8.1.7 或更高版本”
    配置错误定义了重复的“system.web.extensions/scripting/scriptResourceHandler” 解决办法
    self.location.href的具体用法(转)
    CSS Overflow属性详解(转)
    .net中使用showModalDialog打开模式窗口,在后台代码中使用Response.Write()会弹出新页面
  • 原文地址:https://www.cnblogs.com/jsydb/p/12535781.html
Copyright © 2020-2023  润新知