JavaScript 数据结构与算法(三):树

之前我们一直在谈的是一对一的线性结构,可现实中,还有很多一对多的情况需要处理,这种一对多的数据结构就是 —— 树。

(Tree)是 n(n≥0)个结点的有限集。n=0 时称为空树。在任意一棵非空树中:(1) 有且仅有一个特定的称为根(Root)的结点;(2) 当 n>1 时,其余结点可分为 m(m>0)个互不相交的有限集 T1、T2、……、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。

关于树的定义,需要注意的是:

  1. n>0 时根结点是唯一的,不可能存在多个根结点。
  2. m>0 时,子树的个数没有限制,但它们一定是互不相交的。
结点分类

树的结点包含一个数据元素及若干指向其子树的分支。 结点拥有的子树称为结点的度(Degree)。度为 0 的结点称为叶结点(Leaf)或终端结点;度不为 0 的结点称为非终端结点或分支结点。除根结点之外,分支结点也称为内部结点。树的度是树内各结点的度的最大值。

结点间关系
  • 结点的子树的根称为该结点的孩子(Child),相应地,该结点称为孩子的双亲(Parent)。
  • 同一个双亲的孩子之间互称兄弟(Sibling)。
  • 结点的祖先是从根到该结点所经分支上的所有结点。
  • 以某结点为根的子树中的任一结点都称为该结点的子孙。
树的其他相关概念
  • 结点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。
  • 树中结点的最大层次称为树的深度(Depth)或高度。
  • 如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。
  • 森林(Forest)是 m(m≥0)棵互不相交的树的集合。
对比线性表与树的结构

树的存储结构

🌲 双亲表示法

除了根结点外,其余每个结点,它不一定有孩子,但是一定有且仅有一个双亲。

我们可以以一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点到链表中的位置。结点结构如下图所示:

其中:

  • data 是数据域,存储结点的数据信息。
  • parent 是指针域,存储该结点的双亲在数组中的下标。

由于根结点是没有双亲的,所以我们约定根结点的位置域设置为 -1

🌰:树结构和对应树双亲表示如下图所示:

这样的存储结构,我们可以根据结点的 parent 指针很容易找到它的双亲结点,所用的时间复杂度为 O(1),直到 parent-1 时,表示找到了树结点的根。

可如果要知道结点的孩子是什么,需要遍历整个结构才行。我们可以改进一下:

增加一个结点最左边孩子的域,叫它长子域,这样就很容易得到结点的孩子。如果没有孩子的结点,这个长子域就设置为 -1

我们也可以增加一个右兄弟域(rightsib)来体现兄弟关系。同样的,如果右兄弟不存在,则赋值为 -1

🌲 孩子表示法

由于树中每个结点可能有多棵子树,可以考虑用多重链表,即 每个结点有多个指针域,其中每个指针指向一棵子树的根结点,这种方法叫做多重链表表示法。

🍃 方案一:

指针域的个数就等于树的度。结构如下图所示:

其中:

  • data 是数据域。
  • child1childd 是指针域,用来指向该结点的孩子结点。

(对于上面的树来说,树的度是 3 ,所以指针域的个数是 3)

这种方法当树中各结点的度相差很大时,显示是浪费空间的,因为有很多结点的指针域都是空的。但如果树的各结点度相差很小时,空间就会被充分利用。

🍃 方案二:

每个结点指针域的个数等于该结点的度,专门与一个位置来存储结点指针域的个数,其结构如下图所示:

其中:

  • data 为数据域。
  • degree 为度域,用来存储该结点的孩子结点的个数。
  • child1childd 为指针域,指向该结点的各个孩子的结点。

这种方法对空间利用率很高,但由于各个结点的链表是不相同的结构,加上要维护结点的度的数值,在运算上会带来时间的损耗。

孩子表示法,把每个结点的孩子结点排列起来,以单链表作存储结构,则 n 个结点有 n 个孩子链表,如果是叶子结点,则此单链表为空。然后 n 个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中。

其中:

  • data 是数据域,存储某结点的数据信息。
  • firstchild 是头指针域,存储该结点的孩子链表的头指针。
  • child 是数据域,用来存储某个结点在表头数组中的下标。
  • next 是指针域,用来存储指向某结点的下一个孩子结点的指针。

但是这样不易知道某个结点的双亲,可以把双亲表示法和孩子表示法综合一下:

称为双亲孩子表示法。

🌲 孩子兄弟表示法

任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我们设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。结点结构如下图:

其中:

  • data 是数据域。
  • firstchild 为指针域,存储该结点的第一个孩子结点的存储地址。
  • rightsib 是指针域,存储该结点的右兄弟结点的存储地址。

实现的示意图:

这种表示法,如果想查找某个结点的某个孩子非常方便,只需要通过 firstchild 找到此结点的长子,然后再通过长子结点的 rightsib 找到它的二弟,接着一直找下去。

但如果想找某个结点但双亲,再加一个 parent 指针域来解决即可。

我们可以把上面的图变形成下面的样子,这样它把一棵复杂的树变成了一棵二叉树。

二叉树和二叉查找树

二叉树 (Binary Tree)是 n(n≥0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。

二叉树特点

特点:

  • 每个结点最多有两棵子树(没有子树或有一棵子树都可)。
  • 左子树和右子树是有顺序的,次序不能任意颠倒。
  • 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。

二叉树具有五种基本形态:

  1. 空二叉树
  2. 只有一个根结点
  3. 根结点只有左子树
  4. 根结点只有右子树
  5. 根结点既有左子树又有右子树
实现二叉查找树

BST(Binary Search Tree)目的是为了提高查找的性能,其查找在平均和最坏的情况下都是 log n 级别,接近二分查找。

特点 是:每个节点的值大于其任意左侧子节点的值,小于其任意右节点的值。

// Node 对象既保存数据,也保存和其他节点的链接(left 和 right)
function Node(data, left, right) {
  this.data = data;
  this.left = left;
  this.right = right;
  this.show = show;
}
// show() 方法用来显示 保存在节点中的数据
function show() {
  return this.data;
}

// 创建一个类,用来表示二叉查找树(BST)
function BST() {
  this.root = null;
  this.insert = insert;
  this.inOrder = inOrder;
}
// insert() 方法,用来向树中加入新节点
function insert(data) {
  var n = new Node(data, null, null);
  if (this.root == null) {
    this.root = n;
  } else {
    var current = this.root;
    var parent;
    while (true) {
      parent = current;
      if (data < current.data) {
        current = current.left;
        if (current == null) {
          parent.left = n;
          break;
        }
      } else {
        current = current.right;
        if (current == null) {
          parent.right = n;
          break;
        }
      }
    }
  }
}

查找正确插入点的算法:
(1) 设根节点为当前节点。
(2) 如果待插入节点保存的数据小于当前节点,则设新的当前节点为原节点的左节点;反之,执行第 4 步。
(3) 如果当前节点的左节点为 null,就将新的节点插入这个位置,退出循环;反之,继续执行下一次循环。
(4) 设新的当前节点为原节点的右节点。
(5) 如果当前节点的右节点为 null,就将新的节点插入这个位置,退出循环;反之,继续执行下一次循环。

遍历二叉查找树

二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。

有三种遍历 BST 的方式:中序、先序和后序。

  • 中序遍历按照节点上的键值,以升序访问 BST 上的所有节点。
  • 先序遍历先访问根节点,然后以同样方式访问左子树和右子树。
  • 后序遍历先访问叶子节点,从左子树到右子树,再到根节点。
🌰 中序遍历
function inOrder(node) {
  if (!(node == null)) {
    inOrder(node.left);
    console.log(node.show() + " ");
    inOrder(node.right);
  }
}

var nums = new BST();
nums.insert(23);
nums.insert(45);
nums.insert(16);
nums.insert(37);
nums.insert(3);
nums.insert(99);
nums.insert(22);
console.log("Inorder traversal: ");
inOrder(nums.root);

输出结果:

Inorder traversal:
3 16 22 23 37 45 99

下图展示了中序遍历 inOrder() 方法的访问路径:

在二叉树上进行查找

对 BST 通常有下列三种类型的查找:
(1) 查找给定值; (2) 查找最小值; (3) 查找最大值。

🌰 查找最小值

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

getMin() 方法查找 BST 上的最小值,该方法沿着 BST 的左子树挨个遍历,直到遍历到 BST 最左边的节点,该节点被定义为:

current.left = null;

这时,当前节点上保存的值就是最小值。

function getMin() {
  var current = this.root;
  while (!(current.left == null)) {
    current = current.left;
  }
  return current.data;
}

var nums = new BST();
nums.insert(23);
nums.insert(45);
nums.insert(16);
nums.insert(37);
nums.insert(3);
nums.insert(99);
nums.insert(22);
var min = nums.getMin();
console.log("The minimum value of the BST is: " + min);

输出结果:

The minimum value of the BST is: 3
从二叉查找树上删除节点
  • 从 BST 中删除节点的第一步是判断当前节点是否包含待删除的数据:
    • 如果包含,则删除该节点;
    • 如果不包含,则比较当前节点上的数据和待删除的数据:
      • 如果待删除数据小于当前节点上的数据,则移至当前节点的左子节点继续比较;
      • 如果删除数据大于当前节点上的数据,则移至当前节点的右子节点继续比较。
  • 如果待删除节点是叶子节点(没有子节点的节点),那么只需要将从父节点指向它的链接指向 null
  • 如果待删除节点只包含一个子节点,那么原本指向它的节点就得做些调整,使其指向它的子节点。
  • 最后,如果待删除节点包含两个子节点,有两种方式:
    • 要么查找待删除节点左子树上的最大值,
    • 要么查找其右子树上的最小值。

下面的方法中我们选择后一种方式。

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

// remove() 方法只是简单地接受待删除数据
function remove(data) {
  root = removeNode(this.root, data);
}
// 调用 removeNode() 才删除它
function 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 = getSmallest(node.right);
    node.data = tempNode.data;
    node.right = removeNode(node.right, tempNode.data);
    return node;
  } else if (data < node.data) {
    node.left = removeNode(node.left, data);
    return node;
  } else {
    node.right = removeNode(node.right, data);
    return node;
  }
}



🔗:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值