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

了解二叉树

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

一些相关术语

  • 节点:包含一个数据元素及若干指向子树分支的信息。
  • 节点的度:一个节点含有的字节点的个数,二叉树的节点的度最大为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+树则是将数据都存放到叶子节点,并将叶子节点连接成链表,以便于范围查询。感兴趣的可以深入了解一下。

相关推荐
攀小黑17 分钟前
vue3+Web Speech API 封装了一个弹窗语音输入
前端·macos·xcode
roamingcode18 分钟前
4.5 小时,从一句话需求到可安装的 Chrome 插件:一次 AI 结对开发的完整复盘
前端·人工智能·chrome·claude·codex
飞Link29 分钟前
动作方法中的分割与过度分割方法全解析(含代码实战与踩坑案例)
人工智能·python·算法·计算机视觉
铅笔小新z40 分钟前
【数据结构】栈和队列
数据结构
️学习的小王1 小时前
Windows Claude Code 接入 Playwright‑MCP,调用本机Edge浏览器(避坑完整教程)
前端·windows·edge
律宏阔1 小时前
CloakBrowser 开发踩坑笔记
前端·浏览器
Made in Haven7121 小时前
HTML课程笔记补2
前端·笔记·html
张宇Joaquin1 小时前
鲲鹏统一并行加速库KUPL--异步数据拷贝
线性代数·算法·矩阵·numpy
GlobalInfo1 小时前
创新不是追逐热点,是在变化中建立长期竞争力
大数据·算法
Cry丶1 小时前
Vue 3 业务管理页面实战:组件拆分、父子通信与弹窗复用
前端·javascript·vue.js·父子通信·组件拆分·弹窗复用