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

了解二叉树

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

image.png

一些相关术语

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

js模拟二叉树的简单实现

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;
  }
}

满二叉树和完全二叉树

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

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

image.png

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

二叉树的三种遍历方式

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

image.png

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

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

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

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


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)的时间复杂度内进行查找操作。

image.png

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

二叉搜索树的查询

类似于数组中的二分查找,先取根节点,比较要查询值和根节点值。当查询值小于根节点值时,那就在左子树中递归查找,当查询值大于根节点值时,那就在右子树中递归查找,直至相等。时间复杂度一般为O(logn)。

💡 二叉搜索树虽然是一个天然的二分结构,能很好的利用二分查找快速定位数据,但是它存在极端场景比如根节点是最小值,之后每次插入的元素都是树内最大元素,就会导致整个二叉树就全都只有右子节点,从二叉搜索树退化成一个链表,此时时间复杂度为O(n)

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)

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。
  • 要删除节点只存在一个子节点(左或右):将要删除节点的父节点指向要删除节点的子节点。
  • 要删除节点的两个子节点都存在:找到要删除节点的右子树中最小节点,把它替换到要删除节点的位置上。
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)个子节点。在插入的时候,超过要求的话,就会分裂节点。

image.png

💡 关于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+树则是将数据都存放到叶子节点,并将叶子节点连接成链表,以便于范围查询。感兴趣的可以深入了解一下。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值