0

0

一文了解JS实现二叉搜索树的方法

青灯夜游

青灯夜游

发布时间:2020-07-15 16:56:58

|

2757人浏览过

|

来源于segmentfault

转载

一文了解JS实现二叉搜索树的方法

计算机科学中最常用和讨论最多的数据结构之一是二叉搜索树。这通常是引入的第一个具有非线性插入算法的数据结构。二叉搜索树类似于双链表,每个节点包含一些数据,以及两个指向其他节点的指针;它们在这些节点彼此相关联的方式上有所不同。二叉搜索树节点的指针通常被称为“左”和“右”,用来指示与当前值相关的子树。这种节点的简单 JavaScript 实现如下:

var node = {
    value: 125,
    left: null,
    right: null
};

从名称中可以看出,二叉搜索树被组织成分层的树状结构。第一个项目成为根节点,每个附加值作为该根的祖先添加到树中。但是,二叉搜索树节点上的值是唯一的,根据它们包含的值进行排序:作为节点左子树的值总是小于节点的值,右子树中的值都是大于节点的值。通过这种方式,在二叉搜索树中查找值变得非常简单,只要你要查找的值小于正在处理的节点则向左,如果值更大,则向右移动。二叉搜索树中不能有重复项,因为重复会破坏这种关系。下图表示一个简单的二叉搜索树。

1.png

上图表示一个二叉搜索树,其根的值为 8。当添加值 3 时,它成为根的左子节点,因为 3 小于 8。当添加值 1 时,它成为 3 的左子节点,因为 1 小于 8(所以向左)然后 1 小于3(再向左)。当添加值 10 时,它成为跟的右子节点,因为 10 大于 8。不断用此过程继续处理值 6,4,7,14 和 13。此二叉搜索树的深度为 3,表示距离根最远的节点是三个节点。

二叉搜索树以自然排序的顺序结束,因此可用于快速查找数据,因为你可以立即消除每个步骤的可能性。通过限制需要查找的节点数量,可以更快地进行搜索。假设你要在上面的树中找到值 6。从根开始,确定 6 小于 8,因此前往根的左子节点。由于 6 大于 3,因此你将前往右侧节点。你就能找到正确的值。所以你只需访问三个而不是九个节点来查找这个值。

要在 JavaScript 中实现二叉搜索树,第一步要先定义基本接口:

function BinarySearchTree() {
    this._root = null;
}

BinarySearchTree.prototype = {

    //restore constructor
    constructor: BinarySearchTree,

    add: function (value){
    },

    contains: function(value){
    },

    remove: function(value){
    },

    size: function(){
    },

    toArray: function(){
    },

    toString: function(){
    }

};

基本接与其他数据结构类似,有添加和删除值的方法。我还添加了一些方便的方法,size()toArray()toString(),它们对 JavaScript 很有用。

要掌握使用二叉搜索树的方法,最好从 contains() 方法开始。 contains() 方法接受一个值作为参数,如果值存在于树中则返回 true,否则返回 false。此方法遵循基本的二叉搜索算法来确定该值是否存在:

BinarySearchTree.prototype = {

    //more code

    contains: function(value){
        var found       = false,
            current     = this._root

        //make sure there's a node to search
        while(!found && current){

            //if the value is less than the current node's, go left
            if (value < current.value){
                current = current.left;

            //if the value is greater than the current node's, go right
            } else if (value > current.value){
                current = current.right;

            //values are equal, found it!
            } else {
                found = true;
            }
        }

        //only proceed if the node was found
        return found;
    },

    //more code

};

搜索从树的根开始。如果没有添加数据,则可能没有根,所以必须要进行检查。遍历树遵循前面讨论的简单算法:如果要查找的值小于当前节点则向左移动,如果值更大则向右移动。每次都会覆盖 current 指针,直到找到要找的值(在这种情况下 found 设置为 true)或者在那个方向上没有更多的节点了(在这种情况下,值不在树上)。

contains() 中使用的方法也可用于在树中插入新值。主要区别在于你要寻找放置新值的位置,而不是在树中查找值:

BinarySearchTree.prototype = {

    //more code

    add: function(value){
        //create a new item object, place data in
        var node = {
                value: value,
                left: null,
                right: null
            },

            //used to traverse the structure
            current;

        //special case: no items in the tree yet
        if (this._root === null){
            this._root = node;
        } else {
            current = this._root;

            while(true){

                //if the new value is less than this node's value, go left
                if (value < current.value){

                    //if there's no left, then the new node belongs there
                    if (current.left === null){
                        current.left = node;
                        break;
                    } else {
                        current = current.left;
                    }

                //if the new value is greater than this node's value, go right
                } else if (value > current.value){

                    //if there's no right, then the new node belongs there
                    if (current.right === null){
                        current.right = node;
                        break;
                    } else {
                        current = current.right;
                    }       

                //if the new value is equal to the current one, just ignore
                } else {
                    break;
                }
            }
        }
    },

    //more code

};

在二叉搜索树中添加值时,特殊情况是在没有根的情况。在这种情况下,只需将根设置为新值即可轻松完成工作。对于其他情况,基本算法与 contains() 中使用的基本算法完全相同:新值小于当前节点向左,如果值更大则向右。主要区别在于,当你无法继续前进时,这就是新值的位置。所以如果你需要向左移动但没有左侧节点,则新值将成为左侧节点(与右侧节点相同)。由于不存在重复项,因此如果找到具有相同值的节点,则操作将停止。

在继续讨论 size() 方法之前,我想深入讨论树遍历。为了计算二叉搜索树的大小,必须要访问树中的每个节点。二叉搜索树通常会有不同类型的遍历方法,最常用的是有序遍历。通过处理左子树,然后是节点本身,然后是右子树,在每个节点上执行有序遍历。由于二叉搜索树以这种方式排序,从左到右,结果是节点以正确的排序顺序处理。对于 size() 方法,节点遍历的顺序实际上并不重要,但它对 toArray() 方法很重要。由于两种方法都需要执行遍历,我决定添加一个可以通用的 traverse() 方法:

BinarySearchTree.prototype = {

    //more code

    traverse: function(process){

        //helper function
        function inOrder(node){
            if (node){

                //traverse the left subtree
                if (node.left !== null){
                    inOrder(node.left);
                }            

                //call the process method on this node
                process.call(this, node);

                //traverse the right subtree
                if (node.right !== null){
                    inOrder(node.right);
                }
            }
        }

        //start with the root
        inOrder(this._root);
    },

    //more code

};

此方法接受一个参数 process,这是一个应该在树中的每个节点上运行的函数。该方法定义了一个名为 inOrder() 的辅助函数用于递归遍历树。注意,如果当前节点存在,则递归仅左右移动(以避免多次处理 null )。然后 traverse() 方法从根节点开始按顺序遍历,process() 函数处理每个节点。然后可以使用此方法实现size()toArray()toString()

BinarySearchTree.prototype = {

    //more code

    size: function(){
        var length = 0;

        this.traverse(function(node){
            length++;
        });

        return length;
    },

    toArray: function(){
        var result = [];

        this.traverse(function(node){
            result.push(node.value);
        });

        return result;
    },

    toString: function(){
        return this.toArray().toString();
    },

    //more code

};

size()toArray() 都调用 traverse() 方法并传入一个函数来在每个节点上运行。在使用 size()的情况下,函数只是递增长度变量,而 toArray() 使用函数将节点的值添加到数组中。 toString()方法在调用 toArray() 之前把返回的数组转换为字符串,并返回  。

删除节点时,你需要确定它是否为根节点。根节点的处理方式与其他节点类似,但明显的例外是根节点需要在结尾处设置为不同的值。为简单起见,这将被视为 JavaScript 代码中的一个特例。

删除节点的第一步是确定节点是否存在:

BinarySearchTree.prototype = {

    //more code here

    remove: function(value){

        var found       = false,
            parent      = null,
            current     = this._root,
            childCount,
            replacement,
            replacementParent;

        //make sure there's a node to search
        while(!found && current){

            //if the value is less than the current node's, go left
            if (value < current.value){
                parent = current;
                current = current.left;

            //if the value is greater than the current node's, go right
            } else if (value > current.value){
                parent = current;
                current = current.right;

            //values are equal, found it!
            } else {
                found = true;
            }
        }

        //only proceed if the node was found
        if (found){
            //continue
        }

    },
    //more code here

};

remove()方法的第一部分是用二叉搜索定位要被删除的节点,如果值小于当前节点的话则向左移动,如果值大于当前节点则向右移动。当遍历时还会跟踪 parent 节点,因为你最终需要从其父节点中删除该节点。当 found 等于 true 时,current 的值是要删除的节点。

删除节点时需要注意三个条件:

Avatar AI
Avatar AI

AI成像模型,可以从你的照片中生成逼真的4K头像

下载

1、叶子节点

2、只有一个孩子的节点

3、有两个孩子的节点

从二叉搜索树中删除除了叶节点之外的内容意味着必须移动值来对树正确的排序。前两个实现起来相对简单,只删除了一个叶子节点,删除了一个带有一个子节点的节点并用其子节点替换。最后一种情况有点复杂,以便稍后访问。

在了解如何删除节点之前,你需要知道节点上究竟存在多少个子节点。一旦知道了,你必须确定节点是否为根节点,留下一个相当简单的决策树:

BinarySearchTree.prototype = {

    //more code here

    remove: function(value){

        var found       = false,
            parent      = null,
            current     = this._root,
            childCount,
            replacement,
            replacementParent;

        //find the node (removed for space)

        //only proceed if the node was found
        if (found){

            //figure out how many children
            childCount = (current.left !== null ? 1 : 0) + 
                         (current.right !== null ? 1 : 0);

            //special case: the value is at the root
            if (current === this._root){
                switch(childCount){

                    //no children, just erase the root
                    case 0:
                        this._root = null;
                        break;

                    //one child, use one as the root
                    case 1:
                        this._root = (current.right === null ? 
                                      current.left : current.right);
                        break;

                    //two children, little work to do
                    case 2:

                        //TODO

                    //no default

                }        

            //non-root values
            } else {

                switch (childCount){

                    //no children, just remove it from the parent
                    case 0:
                        //if the current value is less than its 
                        //parent's, null out the left pointer
                        if (current.value < parent.value){
                            parent.left = null;

                        //if the current value is greater than its
                        //parent's, null out the right pointer
                        } else {
                            parent.right = null;
                        }
                        break;

                    //one child, just reassign to parent
                    case 1:
                        //if the current value is less than its 
                        //parent's, reset the left pointer
                        if (current.value < parent.value){
                            parent.left = (current.left === null ? 
                                           current.right : current.left);

                        //if the current value is greater than its 
                        //parent's, reset the right pointer
                        } else {
                            parent.right = (current.left === null ? 
                                            current.right : current.left);
                        }
                        break;    

                    //two children, a bit more complicated
                    case 2:

                        //TODO          

                    //no default

                }

            }

        }

    },

    //more code here

};

处理根节点时,这是一个覆盖它的简单过程。对于非根节点,必须根据要删除的节点的值设置 parent 上的相应指针:如果删除的值小于父节点,则 left 指针必须重置为 null (对于没有子节点的节点)或删除节点的 left 指针;如果删除的值大于父级,则必须将 right 指针重置为 null 或删除的节点的 right指针。

如前所述,删除具有两个子节点的节点是最复杂的操作。考虑二元搜索树的以下表示。

2.png

根为 8,左子为 3,如果 3 被删除会发生什么?有两种可能性:1(3 左边的孩子,称为有序前身)或4(右子树的最左边的孩子,称为有序继承者)都可以取代 3。

这两个选项中的任何一个都是合适的。要查找有序前驱,即删除值之前的值,请检查要删除的节点的左子树,并选择最右侧的子节点;找到有序后继,在删除值后立即出现的值,反转进程并检查最左侧的右子树。其中每个都需要另一次遍历树来完成操作:

BinarySearchTree.prototype = {

    //more code here

    remove: function(value){

        var found       = false,
            parent      = null,
            current     = this._root,
            childCount,
            replacement,
            replacementParent;

        //find the node (removed for space)

        //only proceed if the node was found
        if (found){

            //figure out how many children
            childCount = (current.left !== null ? 1 : 0) + 
                         (current.right !== null ? 1 : 0);

            //special case: the value is at the root
            if (current === this._root){
                switch(childCount){

                    //other cases removed to save space

                    //two children, little work to do
                    case 2:

                        //new root will be the old root's left child
                        //...maybe
                        replacement = this._root.left;

                        //find the right-most leaf node to be 
                        //the real new root
                        while (replacement.right !== null){
                            replacementParent = replacement;
                            replacement = replacement.right;
                        }

                        //it's not the first node on the left
                        if (replacementParent !== null){

                            //remove the new root from it's 
                            //previous position
                            replacementParent.right = replacement.left;

                            //give the new root all of the old 
                            //root's children
                            replacement.right = this._root.right;
                            replacement.left = this._root.left;
                        } else {

                            //just assign the children
                            replacement.right = this._root.right;
                        }

                        //officially assign new root
                        this._root = replacement;

                    //no default

                }        

            //non-root values
            } else {

                switch (childCount){

                    //other cases removed to save space 

                    //two children, a bit more complicated
                    case 2:

                        //reset pointers for new traversal
                        replacement = current.left;
                        replacementParent = current;

                        //find the right-most node
                        while(replacement.right !== null){
                            replacementParent = replacement;
                            replacement = replacement.right;
                        }

                        replacementParent.right = replacement.left;

                        //assign children to the replacement
                        replacement.right = current.right;
                        replacement.left = current.left;

                        //place the replacement in the right spot
                        if (current.value < parent.value){
                            parent.left = replacement;
                        } else {
                            parent.right = replacement;
                        }          

                    //no default

                }

            }

        }

    },

    //more code here

};

具有两个子节点的根节点和非根节点的代码几乎相同。此实现始终通过查看左子树并查找最右侧子节点来查找有序前驱。遍历是使用 while 循环中的 replacementreplacementParent 变量完成的。

  replacement中的节点最终成为替换 current 的节点,因此通过将其父级的 right 指针设置为替换的 left 指针,将其从当前位置移除。

对于根节点,当 replacement 是根节点的直接子节点时,replacementParent 将为 null,因此 replacementright 指针只是设置为 root 的 right 指针。

最后一步是将替换节点分配到正确的位置。对于根节点,替换设置为新根;对于非根节点,替换被分配到原始 parent 上的适当位置。

关于此实现的说明:始终用有序前驱替换节点可能导致不平衡树,其中大多数值会位于树的一侧。不平衡树意味着搜索效率较低,因此在实际场景中应该引起关注。在二叉搜索树实现中,要确定是用有序前驱还是有序后继以使树保持适当平衡(通常称为自平衡二叉搜索树)。

这个二叉搜索树实现的完整源代码可以在我的GitHub 中找到。对于替代实现,你还可以查看 Isaac SchlueterGitHub fork

本文转载自:https://segmentfault.com/a/1190000020044659英文原文地址:https://humanwhocodes.com/blog/2009/06/09/computer-science-in-javascript-binary-search-tree-part-1/作者:Nicholas C. Zakas

推荐教程:《JavaScript视频教程

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

16

2026.03.11

Go高并发任务调度与Goroutine池化实践
Go高并发任务调度与Goroutine池化实践

本专题围绕 Go 语言在高并发任务处理场景中的实践展开,系统讲解 Goroutine 调度模型、Channel 通信机制以及并发控制策略。内容包括任务队列设计、Goroutine 池化管理、资源限制控制以及并发任务的性能优化方法。通过实际案例演示,帮助开发者构建稳定高效的 Go 并发任务处理系统,提高系统在高负载环境下的处理能力与稳定性。

23

2026.03.10

Kotlin Android模块化架构与组件化开发实践
Kotlin Android模块化架构与组件化开发实践

本专题围绕 Kotlin 在 Android 应用开发中的架构实践展开,重点讲解模块化设计与组件化开发的实现思路。内容包括项目模块拆分策略、公共组件封装、依赖管理优化、路由通信机制以及大型项目的工程化管理方法。通过真实项目案例分析,帮助开发者构建结构清晰、易扩展且维护成本低的 Android 应用架构体系,提升团队协作效率与项目迭代速度。

75

2026.03.09

JavaScript浏览器渲染机制与前端性能优化实践
JavaScript浏览器渲染机制与前端性能优化实践

本专题围绕 JavaScript 在浏览器中的执行与渲染机制展开,系统讲解 DOM 构建、CSSOM 解析、重排与重绘原理,以及关键渲染路径优化方法。内容涵盖事件循环机制、异步任务调度、资源加载优化、代码拆分与懒加载等性能优化策略。通过真实前端项目案例,帮助开发者理解浏览器底层工作原理,并掌握提升网页加载速度与交互体验的实用技巧。

95

2026.03.06

Rust内存安全机制与所有权模型深度实践
Rust内存安全机制与所有权模型深度实践

本专题围绕 Rust 语言核心特性展开,深入讲解所有权机制、借用规则、生命周期管理以及智能指针等关键概念。通过系统级开发案例,分析内存安全保障原理与零成本抽象优势,并结合并发场景讲解 Send 与 Sync 特性实现机制。帮助开发者真正理解 Rust 的设计哲学,掌握在高性能与安全性并重场景中的工程实践能力。

218

2026.03.05

PHP高性能API设计与Laravel服务架构实践
PHP高性能API设计与Laravel服务架构实践

本专题围绕 PHP 在现代 Web 后端开发中的高性能实践展开,重点讲解基于 Laravel 框架构建可扩展 API 服务的核心方法。内容涵盖路由与中间件机制、服务容器与依赖注入、接口版本管理、缓存策略设计以及队列异步处理方案。同时结合高并发场景,深入分析性能瓶颈定位与优化思路,帮助开发者构建稳定、高效、易维护的 PHP 后端服务体系。

420

2026.03.04

AI安装教程大全
AI安装教程大全

2026最全AI工具安装教程专题:包含各版本AI绘图、AI视频、智能办公软件的本地化部署手册。全篇零基础友好,附带最新模型下载地址、一键安装脚本及常见报错修复方案。每日更新,收藏这一篇就够了,让AI安装不再报错!

168

2026.03.04

Swift iOS架构设计与MVVM模式实战
Swift iOS架构设计与MVVM模式实战

本专题聚焦 Swift 在 iOS 应用架构设计中的实践,系统讲解 MVVM 模式的核心思想、数据绑定机制、模块拆分策略以及组件化开发方法。内容涵盖网络层封装、状态管理、依赖注入与性能优化技巧。通过完整项目案例,帮助开发者构建结构清晰、可维护性强的 iOS 应用架构体系。

222

2026.03.03

C++高性能网络编程与Reactor模型实践
C++高性能网络编程与Reactor模型实践

本专题围绕 C++ 在高性能网络服务开发中的应用展开,深入讲解 Socket 编程、多路复用机制、Reactor 模型设计原理以及线程池协作策略。内容涵盖 epoll 实现机制、内存管理优化、连接管理策略与高并发场景下的性能调优方法。通过构建高并发网络服务器实战案例,帮助开发者掌握 C++ 在底层系统与网络通信领域的核心技术。

33

2026.03.03

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 6万人学习

TypeScript 教程
TypeScript 教程

共19课时 | 3.4万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 3.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号