了解二叉树和二叉树快速查询

了解二叉树

二叉树是一个树形结构,每个节点最多有两个叉,及最多有两个子节点分别是左子节点和右子节点。左右两个子节点也分别有其对应的左右子节点。

一些相关术语

  • 节点:包含一个数据元素及若干指向子树分支的信息。
  • 节点的度:一个节点含有的字节点的个数,二叉树的节点的度最大为2。
  • 根节点:第一个节点。根节点不存在父节点。
  • 叶子节点:也称为终端节点,没有子树的节点或者度为零的节点。
  • 分支节点:也称为非终端节点,度不为零的节点称为非终端节点。

js模拟二叉树的简单实现

kotlin 复制代码
class TreeNode {
  val: any;
  left: TreeNode | null;
  right: TreeNode | null;
  constructor(val?: any, left?: TreeNode | null, right?: TreeNode | null) {
    this.val = val;
    this.left = left === undefined ? null : left;
    this.right = right === undefined ? null : right;
  }
}

满二叉树和完全二叉树

每层节点个数都达到了最大的二叉树就是满二叉树

最后一层叶子节点都靠左排列,且除了最后一层,其他层的节点个数都达到了最大的二叉树是完全二叉树。

💡 找规律:满二叉树一定是是完全二叉树,完全二叉树不一定是满二叉树

二叉树的三种遍历方式

二叉树中一个很重要的操作就是如何遍历树中所有的节点,按照深度优先原则,二叉树有三种经典的遍历方法:前序遍历、中序遍历和后序遍历。

前序遍历指的是:对于树中的任意节点,先遍历当前节点、再遍历当前节点的左子树,最后遍历当前节点的右子树,按照这个顺序直到遍历完整棵树。上面这个二叉树遍历完成之后的顺序:[A,B,D,E,C,F,G]。

中序遍历指的是:对于树中的任意节点,先遍历当前节点的左子树、再遍历当前节点,最后遍历当前节点的右子树,按照这个顺序直到遍历完整棵树。上面这个二叉树遍历完成之后的顺序:[D,B,E,A,F,C,G]。

后序遍历指的是:对于树中的任意节点,先遍历当前节点的左子树、再遍历当前节点的右子树,最后遍历当前节点,按照这个顺序直到遍历完整棵树。上面这个二叉树遍历完成之后的顺序:[D,E,B,F,G,C,A]。

因为整个遍历过程天然具有递归的性质,我们一般直接用递归函数来模拟这一过程,遇到空节点后终止递归。时间复杂度为O(n)。

scss 复制代码
function preorderTraversal(root: TreeNode | null): number[] {
    const res: number[] = [];
    // preOrderTraversal(root, res);
		// middleOrderTraversal(root, res);
		// postOrderTraversal(root, res);
    return res;
};

// 二叉树的前序遍历
const preOrderTraversal = (node: TreeNode | null, res: number[]) => {
    if (!node) return;
    // 按照中、左、右的顺序循序遍历
    res.push(node.val);
    traversal(node.left, res);
    traversal(node.right, res);
}

// 二叉树的中序遍历
const middleOrderTraversal = (node: TreeNode | null, res: number[]) => {
    if (!node) return;
    // 按照左、中、右的顺序循序遍历
    traversal(node.left, res);
    res.push(node.val);
    traversal(node.right, res);
}

// 二叉树的后序遍历
const postOrderraversal = (node: TreeNode | null, res: number[]) => {
    if (!node) return;
    // 按照左、右、中的顺序循序遍历
    traversal(node.left, res);
    traversal(node.right, res);
    res.push(node.val);
}

二叉搜索树(Binary Search Tree)

二叉搜索树是二叉树中最常见的一种类型,二叉搜索树是为了快速查询而生的,它要求在树中的任意一个节点,若其左子树不为空,则其左子树的每一个节点的节点值,都要小于当前节点的节点值。若其右子树不为空,则其右子树的每一个节点的节点值,都要大于当前节点的节点值。所以当对二叉搜索树进行中序遍历后,可以输出有序的数据序列。由于它的有序性,二叉搜索树可以在O(logn)的时间复杂度内进行查找操作。

实现二叉搜索树查询、插入、删除

二叉搜索树的查询

类似于数组中的二分查找,先取根节点,比较要查询值和根节点值。当查询值小于根节点值时,那就在左子树中递归查找,当查询值大于根节点值时,那就在右子树中递归查找,直至相等。时间复杂度一般为O(logn)。
💡 二叉搜索树虽然是一个天然的二分结构,能很好的利用二分查找快速定位数据,但是它存在极端场景比如根节点是最小值,之后每次插入的元素都是树内最大元素,就会导致整个二叉树就全都只有右子节点,从二叉搜索树退化成一个链表,此时时间复杂度为O(n)

kotlin 复制代码
function searchBST(root: TreeNode | null, val: number): TreeNode | null {
  if (!root || root.val === val) return root;
  if (val < root.val) {
    return searchBST(root.left, val);
  } else {
    return searchBST(root.right, val);
  }
}

二叉搜索树的插入

新插入的数据一般都在叶子节点上,基于二叉树的查询,我们只需要从根节点开始,依次比较要插入的数据和节点值的大小关系。

  • 如果要插入的数据比当前节点值大且当前节点的右子树为空,就将新数据插入到当前节点的右子节点。
  • 如果要插入的数据比当前节点值小且当前节点的左子树为空,就将新数据插入到当前节点的左子节点。

时间复杂度为O(logn)

scss 复制代码
function insertIntoBST(root: TreeNode | null, val: number): TreeNode | null {
  if (!root) return new TreeNode(val);
  if (val > root.val) {
    if (root.right) {
      insertIntoBST(root.right, val);
    } else {
      root.right = new TreeNode(val);
    }
  }
  if (val < root.val) {
    if (root.left) {
      insertIntoBST(root.left, val);
    } else {
      root.left = new TreeNode(val);
    }
  }
  return root;
}

二叉搜索树的删除

不同于二叉搜索树的查询和插入,删除操作需要考虑要删除节点的子节点个数,所以相对比较复杂,需要分三种情况处理

  • 要删除节点不存在子节点:直接将父节点中指向要删除节点的指针指向null。
  • 要删除节点只存在一个子节点(左或右):将要删除节点的父节点指向要删除节点的子节点。
  • 要删除节点的两个子节点都存在:找到要删除节点的右子树中最小节点,把它替换到要删除节点的位置上。
ini 复制代码
function deleteNode(root: TreeNode | null, key: number): TreeNode | null {
  if (!root) return null;
  if (key < root.val) {
    root.left = deleteNode(root.left, key);
  } else if (key > root.val) {
    root.right = deleteNode(root.right, key);
  } else {
    if (root.left) {
      let node = root.left;
      while (node.right) {
        node = node.right;
      }
      node.right = root.right;
      return root.left;
    } else {
      return root.right;
    }
  }
  return root;
}

简单拓展一下-数据库索引

在关系型数据库系统中,索引用于加快查询操作。二叉搜索树可以用作索引结构,其中每个节点包含一个索引键和指向对应数据行的指针。利用二叉搜索树的有序性,在树中进行查找操作,可以在O(logn)的时间复杂度内找到满足查询条件的数据行,而不需要遍历整个数据库表。 不过显然,只是简单的二叉搜索树还不够。还需要解决几个问题:二叉树退化问题和查询效率问题。同时还需要考虑范围查询的问题,毕竟查询一个范围的数据这个场景还是很常见的。

自平衡二叉树

为了解决二叉搜索树退化成链表的问题,就出现了自平衡二叉树。自平衡树在二叉搜索树上增加了一些约束,每个节点的左子树和右子树高度差不能超过1,这样,我的查询操作的时间复杂度才能维持在O(logn); 我们需要在数据插入或删除的时候进行特定平衡操作,当插入/删除操作导致树不平衡时通过旋转来调整节点的位置,使树重新平衡。

B树

二叉搜索树虽然可以保持查询操作的时间复杂度,但因为他每个节点只有两个子节点,那么当节点个数越多的时候,树的层数就越多,这样就会增加磁盘的I/O次数,从而影响数据查询的效率,为了减少树的层数,B树就出现了,B树允许每个节点存在多个子节点从而降低树的层数。B树的每个节点可以有M个子节点,M称为B树的阶。 假设M = 3,那就是一个3阶的B树,特点就是每个节点最多有2个(M-1个)数据和最多有3(M)个子节点。在插入的时候,超过要求的话,就会分裂节点。

💡 关于M阶B树最多有(M-1)个数据和M个子节点:就像一条绳子,切一刀会分成两段,切两刀,会分成三段。(M-1)个数据,可以把数据范围分成M个。
💡 磁盘I/O(Input/Output)是指计算机系统与磁盘存储设备之间进行的数据读取和写入操作。磁盘I/O是计算机系统中常见的一种I/O操作类型,用于从磁盘读取数据到内存或将数据从内存写入磁盘。磁盘I/O是一种相对较慢的操作,而磁盘 I/O 次数越多,所消耗的时间也就越大。所以一般索引的数据结构能狗在尽可能少的磁盘的 I/O 操作中完成查询工作。

B+数

而B+ 树是基于 B 树的一个升级,他将所有的数据都放到了叶子结点上,并将叶子结点连接起来,组成了一个有序链表。这种设计对范围查找非常有帮助,比如说我们想知道 12 月 1 日和 12 月 12 日之间的订单,这个时候可以先查找到 12 月 1 日所在的叶子节点,然后利用链表向右遍历,直到找到 12 月12 日的节点,这样就不需要从根节点查询了,进一步节省查询需要的时间。

总结

二叉搜索树是一个天然的二分结构,可以很好的利用二分查找快速查询,但在某些极端场景下,二叉树会退化成链表,为了解决这个问题,就有了自平衡二叉树,他会在插入/删除之后进行自旋转操作以维持树的平衡。

而树的高度决定了磁盘I/O的次数,为了降低磁盘I/O和提高查询效率,B树增加了叉数,以降低树的高度,而B+树则是将数据都存放到叶子节点,并将叶子节点连接成链表,以便于范围查询。感兴趣的可以深入了解一下。

相关推荐
王解34 分钟前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
我不当帕鲁谁当帕鲁39 分钟前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂43 分钟前
工程化实战内功修炼测试题
前端·javascript
爱吃生蚝的于勒44 分钟前
C语言内存函数
c语言·开发语言·数据结构·c++·学习·算法
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新5 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html
秦jh_6 小时前
【Linux】多线程(概念,控制)
linux·运维·前端
蜗牛快跑2136 小时前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程