数据结构与算法|「数据结构与算法Javascript描述」二叉树


「数据结构与算法Javascript描述」二叉树

  • 「数据结构与算法Javascript描述」二叉树
    • 1. 树的定义
    • 2. 二叉树
      • 2.1 二叉搜索树
      • 2.2 二叉搜索树的遍历
      • 2.3 二叉搜索树上进行查找
        • 2.3.1 查找最小值和最大值
        • 2.3.2 查找给定值
      • 2.4 二叉搜索树上删除节点
      • 2.5 更多二叉树

「数据结构与算法Javascript描述」二叉树 树是计算机科学中经常用到的一种数据结构。树是一种非线性的数据结构,以分层的方式存储数据。树被用来存储具有层级关系的数据,比如文件系统中的文件;树还被用来存储有序列表。本章将研究一种特殊的树:二叉树。选择树而不是那些基本的数据结构,是因为在二叉树上进行查找非常快(而在链表上查找则不是这样),为二叉树添加或删除元素 也非常快(而对数组执行添加或删除操作则不是这样)。
1. 树的定义 树由一组以边连接的节点组成。现实生活中最常见的树的例子是家谱,或是公司的组织架构图。
下图的树展示了更多有关树的术语,在后续讨论中将会提到。一棵树最上面的节点称为根节点,如果一个节点下面连接多个节点,那么该节点称为父节点,它下面的节点称为子节点。一个节点可以有 0 个、1 个或多个子节点。没有任何子节点的节点称为叶子节点。
数据结构与算法|「数据结构与算法Javascript描述」二叉树
文章图片

二叉树是一种特殊的树,它的子节点个数不超过两个。二叉树具有一些特殊的计算性质,使得在它们之上的一些操作异常高效。后续将深入讨论二叉树。
继续回到上图,沿着一组特定的边,可以从一个节点走到另外一个与它不直接相连的节点。从一个节点到另一个节点的这一组边称为路径,在图中用虚线表示。以某种特定顺序访问树中所有的节点称为树的遍历。
树可以分为几个层次,根节点是第 0 层,它的子节点是第 1 层,子节点的子节点是第 2层,以此类推。树中任何一层的节点可以都看做是子树的根,该子树包含根节点的子节点,子节点的子节点等。我们定义树的层数就是树的深度。
2. 二叉树 正如前面提到的那样,二叉树每个节点的子节点不允许超过两个。通过将子节点的个数限定为 2,可以写出高效的程序在树中插入、查找和删除数据。
在使用 JavaScript 构建二叉树之前,需要给我们关于树的词典里再加两个新名词。一个父节点的两个子节点分别称为左节点和右节点。在一些二叉树的实现中,左节点包含一组特定的值,右节点包含另一组特定的值。下图展示了一棵二叉树。
数据结构与算法|「数据结构与算法Javascript描述」二叉树
文章图片

当考虑某种特殊的二叉树,比如二叉搜索树时,确定子节点非常重要。二叉搜索树是一种特殊的二叉树,相对较小的值保存在左节点中,较大的值保存在右节点中。这一特性使得查找的效率很高,对于数值型和非数值型的数据,比如单词和字符串,都是如此。下面我们来介绍一下二叉树的实现。
2.1 二叉搜索树
二叉搜索树由节点组成,所以我们要定义的第一个对象就是 Node,该对象和前面介绍链表时的对象类似。Node 类的定义如下:
function Node(data, left, right) { this.data = https://www.it610.com/article/data; this.left = left; this.right = right; this.show = function() { return this.data; } }

Node 对象既保存数据,也保存和其他节点的链接(left 和 right),show() 方法用来显示保存在节点中的数据。
现在可以创建一个类,用来表示二叉搜索树(BST)。我们让类只包含一个数据成员:一个表示二叉搜索树根节点的 Node 对象。该类的构造函数将根节点初始化为 null,以此创建一个空节点。
BST 先要有一个 insert() 方法,用来向树中加入新节点。这个方法有点复杂,需要着重讲解。首先要创建一个 Node 对象,将数据传入该对象保存。
其次检查 BST 是否有根节点,如果没有,那么这是棵新树,该节点就是根节点,这个方法到此也就完成了;否则,进入下一步。
如果待插入节点不是根节点,那么就需要准备遍历 BST,找到插入的适当位置。该过程类似于遍历链表。用一个变量存储当前节点,一层层地遍历 BST。
进入 BST 以后,下一步就要决定将节点放在哪个地方。找到正确的插入点时,会跳出循环。查找正确插入点的算法如下:
  1. 设根节点为当前节点。
  2. 如果待插入节点保存的数据小于当前节点,则设新的当前节点为原节点的左节点;反 之,执行第 4 步。
  3. 如果当前节点的左节点为 null,就将新的节点插入这个位置,退出循环;反之,继续执行下一次循环。
  4. 设新的当前节点为原节点的右节点。
  5. 如果当前节点的右节点为 null,就将新的节点插入这个位置,退出循环;反之,继续执行下一次循环。
有了上面的算法,就可以开始实现 BST 类了。下面包含了 BST 类和 Node 类的定义:
function Node(data, left, right) { this.data = https://www.it610.com/article/data; this.left = left; this.right = right; this.show = function() { return this.data; } }function BST() { this.root = null; this.insert = function(data) { const node = new Node(data, null, null); if (this.root === null) { this.root = node; } else { let current = this.root; while(true) { if (data < current.data) { if (current.left === null) { current.left = node; break; } current = current.left; } else { if (current.right === null) { current.right = node; break; } current = current.right; } } } } }

2.2 二叉搜索树的遍历
现在 BST 类已经初步成型,但是操作上还只能插入节点,我们需要有能力遍历 BST,这样就可以按照不同的顺序,比如按照数字大小或字母先后,显示节点上的数据。
有三种遍历 BST 的方式:中序、先序和后序。中序遍历按照节点上的键值,以升序访问 BST 上的所有节点。先序遍历先访问根节点,然后以同样方式访问左子树和右子树。后序遍历先访问叶子节点,从左子树到右子树,再到根节点。
需要中序遍历的原因显而易见,但为什么需要先序遍历和后序遍历就不是那么明显了。我们先来实现这三种遍历方式,在后续中再解释它们的用途。
中序遍历使用递归的方式最容易实现。该方法需要以升序访问树中所有节点,先访问左子树,再访问根节点,最后访问右子树。
中序遍历的代码如下:
function inOrder(node) { if (node !== null) { inOrder(node.left); console.log(node.show()); inOrder(node.right); } }

下图展示了 inOrder() 方法的访问路径。
数据结构与算法|「数据结构与算法Javascript描述」二叉树
文章图片

先序遍历的定义如下:
function preOrder(node) { if (node !== null) { console.log(node.show()); preOrder(node.left); preOrder(node.right); } }

注意 inOrder()preOrder() 方法的唯一区别,就是 if 语句中代码的顺序。在 inOrder()方法中,show() 函数像夹在两个递归调用之间;在 preOrder() 方法中,show()函数放在两个递归调用之前。
下图展示了先序遍历的访问路径。
数据结构与算法|「数据结构与算法Javascript描述」二叉树
文章图片

后序遍历的定义如下:
function postOrder(node) { if (node !== null) { postOrder(node.left); postOrder(node.right); console.log(node.show()); } }

后序遍历的访问路径如下图所示。
数据结构与算法|「数据结构与算法Javascript描述」二叉树
文章图片

2.3 二叉搜索树上进行查找
2.3.1 查找最小值和最大值 查找 BST 上的最小值和最大值非常简单。因为较小的值总是在左子节点上,在 BST 上查找最小值,只需要遍历左子树,直到找到最后一个节点。
getMin() 方法查找 BST 上的最小值,该方法的定义如下:
BST.prototype.getMin = function() { let current = this.root; while (current.left !== null) { current = current.left; } return current.data; }

在 BST 上查找最大值,只需要遍历右子树,直到找到最后一个节点,该节点上保存的值即为最大值。
getMax() 方法的定义如下:
BST.prototype.getMax = function() { let current = this.root; while (current.right !== null) { current = current.right; } return current.data; }

2.3.2 查找给定值 在 BST 上查找给定值,需要比较该值和当前节点上的值的大小。通过比较,就能确定如果给定值不在当前节点时,该向左遍历还是向右遍历。
find() 方法用来在 BST 上查找给定值,定义如下:
BST.prototype.find = function(data) { let current = this.root; while(current !== null) { if (current.data =https://www.it610.com/article/== data) { return current; } else if (current.data < data) { current = current.right; } else { current = current.left; } } return null; }

如果找到给定值,该方法返回保存该值的节点;如果没找到,该方法返回 null
2.4 二叉搜索树上删除节点
从 BST 上删除节点的操作最复杂,其复杂程度取决于删除哪个节点。如果删除没有子节点的节点,那么非常简单。如果节点只有一个子节点,不管是左子节点还是右子节点,就变得稍微有点复杂了。删除包含两个子节点的节点最复杂。
为了管理删除操作的复杂度,我们使用递归操作,同时定义两个方法:remove()removeNode()
从 BST 中删除节点的第一步是判断当前节点是否包含待删除的数据,如果包含,则删除该节点;如果不包含,则比较当前节点上的数据和待删除的数据。如果待删除数据小于当前节点上的数据,则移至当前节点的左子节点继续比较;如果删除数据大于当前节点上的数据,则移至当前节点的右子节点继续比较。
如果待删除节点是叶子节点(没有子节点的节点),那么只需要将从父节点指向它的链接指向 null
如果待删除节点只包含一个子节点,那么原本指向它的节点久得做些调整,使其指向它的子节点。
最后,如果待删除节点包含两个子节点,正确的做法有两种:要么查找待删除节点左子树上的最大值,要么查找其右子树上的最小值。这里我们选择后一种方式。
我们需要一个查找子树上最小值的方法,后面会用它找到的最小值创建一个临时节点。将临时节点上的值复制到待删除节点,然后再删除临时节点。下图展示了这一过程。
数据结构与算法|「数据结构与算法Javascript描述」二叉树
文章图片

整个删除过程由两个方法完成。remove() 方法只是简单地接受待删除数据,调用 removeNode()删除它,后者才是完成主要工作的方法。两个方法的定义如下:
BST.prototype.remove = function(data) { this.root = this.removeNode(this.root, data); }BST.prototype.removeNode = function (node, data) { if (node === null) { return null; } if (data =https://www.it610.com/article/== 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.getSmallest(node.right); node.data = tempNode.data; node.right = this.removeNode(node.right, 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; } }

2.5 更多二叉树
BST存在一个问题:取决于你添加的节点数,树的一条边可能会非常深;也就是说,树的一条分支会有很多层,而其他的分支却只有几层,如下图所示:
数据结构与算法|「数据结构与算法Javascript描述」二叉树
文章图片

这会在需要在某条边上添加、移除和搜索某个节点时引起一些性能问题。为了解决这个问题,有一种树叫作阿德尔森-维尔斯和兰迪斯树(AVL树)。AVL树是一种自平衡二叉搜索树,意思是任何一个节点左右两侧子树的高度之差最多为1。也就是说这种树会在添加或移除节点时尽量试着成为一棵完全树。
【数据结构与算法|「数据结构与算法Javascript描述」二叉树】关于树的内容有很多,例如还有红黑树、线索树等,后续会为读者一一介绍,敬请期待。

    推荐阅读