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

2022-10-24 18:43:20 浏览数 (1)

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

树是计算机科学中经常用到的一种数据结构。树是一种非线性的数据结构,以分层的方式存储数据。树被用来存储具有层级关系的数据,比如文件系统中的文件;树还被用来存储有序列表。本章将研究一种特殊的树:二叉树。选择树而不是那些基本的数据结构,是因为在二叉树上进行查找非常快(而在链表上查找则不是这样),为二叉树添加或删除元素 也非常快(而对数组执行添加或删除操作则不是这样)。

1. 树的定义

树由一组以边连接的节点组成。现实生活中最常见的树的例子是家谱,或是公司的组织架构图。

下图的树展示了更多有关树的术语,在后续讨论中将会提到。一棵树最上面的节点称为「根节点」,如果一个节点下面连接多个节点,那么该节点称为「父节点」,它下面的节点称为「子节点」。一个节点可以有 0 个、1 个或多个子节点。没有任何子节点的节点称为「叶子节点」

一棵树的局部

「二叉树」是一种特殊的树,它的子节点个数不超过两个。二叉树具有一些特殊的计算性质,使得在它们之上的一些操作异常高效。后续将深入讨论二叉树。

继续回到上图,沿着一组特定的边,可以从一个节点走到另外一个与它不直接相连的节点。从一个节点到另一个节点的这一组边称为「路径」,在图中用虚线表示。以某种特定顺序访问树中所有的节点称为「树的遍历」

树可以分为几个层次,根节点是第 0 层,它的子节点是第 1 层,子节点的子节点是第 2层,以此类推。树中任何一层的节点可以都看做是子树的根,该子树包含根节点的子节点,子节点的子节点等。我们定义树的层数就是树的深度。

2. 二叉树

正如前面提到的那样,二叉树每个节点的子节点不允许超过两个。通过将子节点的个数限定为 2,可以写出高效的程序在树中插入、查找和删除数据。

在使用 JavaScript 构建二叉树之前,需要给我们关于树的词典里再加两个新名词。一个父节点的两个子节点分别称为「左节点」「右节点」。在一些二叉树的实现中,左节点包含一组特定的值,右节点包含另一组特定的值。下图展示了一棵二叉树。

二叉树

当考虑某种特殊的二叉树,比如「二叉搜索树」时,确定子节点非常重要。二叉搜索树是一种特殊的二叉树,相对较小的值保存在左节点中,较大的值保存在右节点中。这一特性使得查找的效率很高,对于数值型和非数值型的数据,比如单词和字符串,都是如此。下面我们来介绍一下二叉树的实现。

2.1 二叉搜索树

「二叉搜索树」由节点组成,所以我们要定义的第一个对象就是 Node,该对象和前面介绍链表时的对象类似。Node 类的定义如下:

代码语言:javascript复制
function Node(data, left, right) {
  this.data = 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 类的定义:

代码语言:javascript复制
function Node(data, left, right) {
  this.data = 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 上的所有节点。先序遍历先访问根节点,然后以同样方式访问左子树和右子树。后序遍历先访问叶子节点,从左子树到右子树,再到根节点。

需要中序遍历的原因显而易见,但为什么需要先序遍历和后序遍历就不是那么明显了。我们先来实现这三种遍历方式,在后续中再解释它们的用途。

中序遍历使用递归的方式最容易实现。该方法需要以升序访问树中所有节点,先访问左子树,再访问根节点,最后访问右子树。

中序遍历的代码如下:

代码语言:javascript复制
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());
  }
}

后序遍历的访问路径如下图所示。

后序遍历的访问路径

2.3 二叉搜索树上进行查找

2.3.1 查找最小值和最大值

查找 BST 上的最小值和最大值非常简单。因为较小的值总是在左子节点上,在 BST 上查找最小值,只需要遍历左子树,直到找到最后一个节点。

getMin() 方法查找 BST 上的最小值,该方法的定义如下:

代码语言:javascript复制
BST.prototype.getMin = function() {
  let current = this.root;
  while (current.left !== null) {
    current = current.left;
  }
  return current.data;
}

在 BST 上查找最大值,只需要遍历右子树,直到找到最后一个节点,该节点上保存的值即为最大值。

getMax() 方法的定义如下:

代码语言:javascript复制
BST.prototype.getMax = function() {
  let current = this.root;
  while (current.right !== null) {
    current = current.right;
  }
  return current.data;
}
2.3.2 查找给定值

在 BST 上查找给定值,需要比较该值和当前节点上的值的大小。通过比较,就能确定如果给定值不在当前节点时,该向左遍历还是向右遍历。

find() 方法用来在 BST 上查找给定值,定义如下:

代码语言:javascript复制
BST.prototype.find = function(data) {
  let 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;
}

如果找到给定值,该方法返回保存该值的节点;如果没找到,该方法返回 null

2.4 二叉搜索树上删除节点

从 BST 上删除节点的操作最复杂,其复杂程度取决于删除哪个节点。如果删除没有子节点的节点,那么非常简单。如果节点只有一个子节点,不管是左子节点还是右子节点,就变得稍微有点复杂了。删除包含两个子节点的节点最复杂。

为了管理删除操作的复杂度,我们使用递归操作,同时定义两个方法:remove()removeNode()

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

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

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

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

我们需要一个查找子树上最小值的方法,后面会用它找到的最小值创建一个临时节点。将临时节点上的值复制到待删除节点,然后再删除临时节点。下图展示了这一过程。

删除包含两个子节点的节点

整个删除过程由两个方法完成。remove() 方法只是简单地接受待删除数据,调用 removeNode()删除它,后者才是完成主要工作的方法。两个方法的定义如下:

代码语言:javascript复制
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 === 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存在一个问题:取决于你添加的节点数,树的一条边可能会非常深;也就是说,树的一条分支会有很多层,而其他的分支却只有几层,如下图所示:

image-20220129120852646

这会在需要在某条边上添加、移除和搜索某个节点时引起一些性能问题。为了解决这个问题,有一种树叫作「阿德尔森-维尔斯和兰迪斯树」(AVL树)。AVL树是一种自「平衡二叉搜索树」,意思是任何一个节点左右两侧子树的高度之差最多为1。也就是说这种树会在添加或移除节点时尽量试着成为一棵完全树。

关于树的内容有很多,例如还有「红黑树」「线索树」等,后续会为读者一一介绍,敬请期待。

0 人点赞