彻底理解树(Tree)结构

树结构是一种分层的、非线性的数据结构,用于组织和存储数据。它由节点(node)和边(edge)组成,通常被用来表示具有父子关系的层次化数据。树结构在计算机科学中非常重要,广泛用于管理数据(如文件系统、数据库索引、网页 DOM 等)

认识

  • 树在我们的生活中处处可见,树通常有一个根,连接着根的是树干 d8bd-aa6c9ba83d8ef9407dd07b00d4a0b8de.jpg

  • 树干到上面之后会进行分叉成树枝,树枝还会分叉成更小的树枝,在树枝的最后是叶子

  • 专家们对树的结构进行了抽象,发现树可以模拟生活中的很多场景

  • 比如我们熟悉的 DOM Tree
    在这里插入图片描述

术语

在这里插入图片描述

  • 空树n(n≥0)个节点构成的有限集合,当n=0时,称为空树

  • 子树(SubTree:对于任一棵非空树(n > 0),树中有一个称为根(Root)的特殊节点用 r 表示,其余节点可分为m(m>0)个互不相交的有限集 T1,T2,...,Tm,其中每个集合本身又是一棵树,称为原来树的子树

  • : 父节点与子节点之间的关系就相当于一条边

  • 节点的度(Degree:节点的子树个数

  • 树的深度(Depth:对于任意节点nn的深度为从根到 n 的唯一路径长,根的深度为 0

  • 树的高度(Height:从根节点到叶子节点的最长路径上的边数称为树的高度

  • 叶子节点(Leaf:没有子节点的节点称为叶子节点

  • 父节点(Parent:一个节点的直接上层节点称为它的父节点

  • 子节点(Child:一个节点下直接连接的节点称为它的子节点,子节点也称孩子节点

  • 兄弟节点(Sibling:具有同一父节点的各节点彼此是兄弟节点

  • 路径:指从一个节点到另一个节点的一系列连接顺序,是一条唯一的路径,因为树的结构不允许循环

  • 路径长度:路径长度是指路径上边的总数量,在树结构中常用来计算节点的深度或高度

  • 节点的层级(Level:从根节点开始,根节点所在的层级记为 1,往下每一层的层级递增 1

表示方式

  • 普通表示
    在这里插入图片描述

  • 儿子-兄弟表示:是一种用二叉树来表示普通多叉树的数据结构方法

    • 每个节点的左子节点表示它的第一个孩子

    • 每个节点的右子节点表示它的下一个兄弟

在这里插入图片描述

旋转45度不是必要的步骤,但它让我们更清晰地理解多叉树如何被转换成二叉树
在这里插入图片描述

二叉树

如果树中每个节点最多只能有两个子节点,这样的树就称为二叉树,几乎所有的树都可以表示成二叉树的形式,所以二叉树是很重要的

  • 二叉树的定义:二叉树可以为空,也就是没有节点。若不为空,则它是由根节点和称为其左子树TL和右子树TR的两个不相交的二叉树组成

  • 二叉树有五种形态
    在这里插入图片描述

特性

二叉树有几个比较重要的特性,在笔试题中比较常见,以下图作为参考:
在这里插入图片描述

  • 层为 h 的二叉树,至少有 h 个节点,最多可以有 2^h − 1 个节点

  • 一棵二叉树i 层的最大节点数为:2^(i-1),i >= 1

  • 在任何非空的二叉树中,如果叶子节点数为 n0,度为 2 的节点(有两个子节点的节点)数为 n2,则满足关系n0 = n2 + 1,比如上图叶子结点为 HIEFJ,度为2的为ABCD,满足公式

完美二叉树

完美二叉树(Perfect Binary Tree) ,也称为满二叉树(Full Binary Tree),在二叉树中,除了最下一层的叶节点外,每层节点都有2个子节点,就构成了满二叉树

  • 节点数:对于树的高度为 h 的满二叉树,总节点数 N 是:N = 2^(h+1) - 1

  • 叶子节点数:一棵高度为 h 的满二叉树,叶子节点数为:2^h

  • 非叶子节点数:在满二叉树中,非叶子节点的数量(即内部节点数)为:2^h - 1

  • 层节点数:在满二叉树的第 k 层(从 0 开始编号)上,节点数为:2^k
    在这里插入图片描述

完全二叉树

完全二叉树是一种特殊的二叉树,其中所有层都被完全填满,除了最后一层。最后一层的节点都从左到右连续排列,没有空位

  • 节点排列:从根节点到倒数第二层,每一层的节点数都达到最大值。最后一层的节点从左到右依次排列,但最后一层不必完全填满

  • 节点数关系:具有n个节点的完全二叉树的深度为 ⌊log2n⌋ + 1⌊ ⌋ 表示向下取整)

  • 节点编号规律:节点编号可以用于定位树中每个节点的父节点和子节点位置,使得完全二叉树在实现堆等数据结构时特别高效

    • 根节点编号为 1:从根节点开始编号,根节点的编号是 1

    • 子节点的编号

      若节点编号为 i,左子节点的编号为 2i,右子节点的编号为 2i+1

      例如,编号为 1 的节点的左子节点编号为 2×1=2,右子节点编号为 2×1+1=3

    • 父节点的编号:若节点编号为 i,父节点的编号为 ⌊i/2⌋,例如编号为 4 的节点的父节点编号为 ⌊4/2⌋=2

  • 叶子节点:所有叶子节点都位于倒数第一层或倒数第二层,且位于倒数第一层的叶子节点从左到右连续排列

  • 数组表示:完全二叉树可以很方便地用数组表示,每个节点按上述编号存储在数组的对应位置。这样的表示方式可以减少指针的使用,便于存储和查找。这一特性使得完全二叉树在实现堆结构时尤其有用
    在这里插入图片描述

存储

二叉树的存储常见的方式是顺序存储(数组实现)和链式存储(链表实现)

顺序存储

  • 完全二叉树:按从上至下、从左到右顺序存储
    在这里插入图片描述

  • 非完全二叉树:要转成完全二叉树才可以按照上面的方案存储,但是会造成很大的空间浪费
    在这里插入图片描述

链式存储

二叉树最常见的方式还是使用链表存储,每个节点封装成一个NodeNode中包含存储的数据有左节点的引用和右节点的引用
在这里插入图片描述

二叉搜索树

二叉搜索树(BST,Binary Search Tree)特殊的二叉树可以为空,也称二叉排序树或二叉查找树,由于其性质,二叉搜索树能够高效地执行查找、插入和删除操作,平均时间复杂度为 O(log⁡n),在最坏情况下(如退化为链表)为 O(n)

特点

在这里插入图片描述

  • 是相对较小的值总是保存在左节点上,相对较大的值总是保存在右节点上

  • 左子树的值小于根节点的值:在每个节点的左子树上,所有节点的值都小于这个节点的值

  • 右子树的值大于根节点的值:在每个节点的右子树上,所有节点的值都大于这个节点的值

  • 左右子树本身也是二叉搜索树:这意味着二叉搜索树具有递归的结构,子树也符合二叉搜索树的定义

  • 这种方式就是二分查找的思想,查找所需的最大次数等于二叉搜索树的深度

常见操作

  • insert(value):向树中插入一个新的数据

  • search(value):在树中查找一个数据,如果节点存在,则返回true;如果不存在,则返回false

  • min:返回树中最小的值/数据

  • max:返回树中最大的值/数据

  • preOrderTraverse:通过先序遍历方式遍历所有节点

  • inOrderTraverse:通过中序遍历方式遍历所有节点

  • postOrderTraverse:通过后序遍历方式遍历所有节点

  • levelOrderTraverse:通过层序遍历方式遍历所有节点

  • remove(value):从树中移除某个数据

封装

封装BSTree的类:

  • 先封装一个用于保存每一个节点的类Node,包含属性:节点对应的value,指向的左子树left,指向的右子树right,指向父节点的parent

  • 对于BSTree来说,只需要保存根节点即可,因为其他节点都可以通过根节点找到

class TreeNode<T> {
  value: T;
  left: TreeNode<T> | null = null;
  right: TreeNode<T> | null = null;
  parent: TreeNode<T> | null = null;
  constructor(value: T) {
    this.value = value;
  }
  get isLeft() {
    return !!(this.parent && this === this.parent.left);
  }
  get isRight() {
    return !!(this.parent && this === this.parent.right);
  }
}

class BSTree<T = number> {
  private root: TreeNode<T> | null = null;
}

插入数据

空树是直接设置为root,不为空树时直接循环比较插入

在这里插入图片描述

insert(value: T) {
  const newNode = new TreeNode(value);

  if (!this.root) {
    this.root = newNode;
  } else {
    /* 非递归 */
    // while (current) {
    //   if (current.value > value) {
    //     if (!current.left) {
    //       current.left = newNode;
    //       current = null;
    //     } else {
    //       current = current.left;
    //     }
    //   } else {
    //     if (!current.right) {
    //       current.right = newNode;
    //       current = null;
    //     } else {
    //       current = current.right;
    //     }
    //   }
    // }

    /* 递归 */
    this.insertNode(this.root, newNode);
  }
}
private insertNode(node: TreeNode<T>, newNode: TreeNode<T>) {
  if (newNode.value < node.value) {
    // 插入的值小于比较值,判断其左边,为空直接插入,不为空递归再次判断
    if (!node.left) {
      node.left = newNode;
    } else {
      this.insertNode(node.left, newNode);
    }
  } else {
    if (!node.right) {
      node.right = newNode;
    } else {
      this.insertNode(node.right, newNode);
    }
  }
}

遍历数据

针对所有的二叉树都是适用的,不仅仅是二叉搜索树,遍历一棵树是指访问树的每个节点(也可以对每个节点进行某些操作,我们实现简单的打印),二叉树的遍历常见的有四种方式:先序遍历、中序遍历、后序遍历和层序遍历

先序遍历

根 -> 左 -> 右

  • 首先访问根节点

  • 再先序遍历其左子树

  • 最后先序遍历其右子树

在这里插入图片描述

preOrderTraverse() {
  this.preOrderTraverseNode(this.root);
}
private preOrderTraverseNode(node: TreeNode<T> | null) {
  if (node) {
    console.log(node.value); // 先遍历打印出根节点
    this.preOrderTraverseNode(node.left);
    this.preOrderTraverseNode(node.right);
  }
}

非递归代码参考:
在这里插入图片描述

中序遍历

左 -> 根 -> 右

  • 先遍历其左子树

  • 再访问根节点

  • 最后遍历其右子树
    在这里插入图片描述

inOrderTraverse() {
  this.inOrderTraverseNode(this.root);
}
private inOrderTraverseNode(node: TreeNode<T> | null) {
  if (node) {
    this.inOrderTraverseNode(node.left);
    console.log(node.value);
    this.inOrderTraverseNode(node.right);
  }
}

非递归代码参考:
在这里插入图片描述

后序遍历

左 -> 右 -> 根

  • 先遍历其左子树

  • 再遍历其右子树

  • 最后访问根节点
    在这里插入图片描述

postOrderTraverse() {
  this.postOrderTraverseNode(this.root);
}
private postOrderTraverseNode(node: TreeNode<T> | null) {
  if (node) {
    this.postOrderTraverseNode(node.left);
    this.postOrderTraverseNode(node.right);
    console.log(node.value);
  }
}

非递归代码参考:
在这里插入图片描述

层序遍历

层序遍历通常借助队列来完成,这也是队列的一个经典应用场景,我们这里直接使用数组来模拟队列

  • 先把根节点放入队列

  • 取出根节点,队列放入根节点的左右子树

  • 取出左子树,队列放入左子树的左右子树,如此循环
    在这里插入图片描述

levelOrderTraverse() {
  if (!this.root) return;
  let queue: TreeNode<T>[] = [this.root];
  while (queue.length) {
    const current = queue.shift()!;
    console.log(current.value);
    current.left && queue.push(current.left);
    current.right && queue.push(current.right);
  }
}

最小最大值

getMinValue(): T | null {
  if (!this.root) return null;
  let current = this.root;
  while (current.left) {
    current = current.left;
  }
  return current.value;
}

getMaxValue(): T | null {
  if (!this.root) return null;
  let current = this.root;
  while (current.right) {
    current = current.right;
  }
  return current.value;
}

搜索特定值

search(value: T): boolean {
  /* 递归 */
  return !!this.searchNode(this.root, value);

  /* 非递归 */
  // if (!this.root) return false;
  // let current: TreeNode<T> | null = this.root;
  // while (current) {
  //   if (value > current.value) {
  //     current = current.right;
  //   } else if (value < current.value) {
  //     current = current.left;
  //   } else {
  //     return true;
  //   }
  // }
  // return false;
}
private searchNode(node: TreeNode<T> | null, value: T): TreeNode<T> | null {
  if (!node) return null;
  if (value < node.value) {
    return this.searchNode(node.left, value);
  } else if (value > node.value) {
    return this.searchNode(node.right, value);
  } else {
    return node;
  }
}

删除数据

删除比较复杂,我们需要一点点的来解析,主要分为以下几种情况:

  • 没有子节点时:要删除节点的左右子树都为null

    • 如果要删除的是根节点,则直接删除设为null即可
      在这里插入图片描述

    • 如果不是根节点,判断要删除节点是父节点的左子树还是右子树,则需让其父节点的相关子树指向null即可
      在这里插入图片描述

    if (!delNode.left && !delNode.right) {
      // 1.1 是根节点时
      if (delNode === this.root) {
        this.root = null;
      } else {
        if (delNode.isLeft) {
          delNode.parent!.left = null;
        } else {
          delNode.parent!.right = null;
        }
      }
    }
    
  • 有一个子节点时:要删除节点的左右子树其中一个不为null

    • 如果要删除的是根节点,让根指向其子节点
      在这里插入图片描述

    • 如果不是根节点,判断要删除节点是父节点的左子树还是右子树,则需让其父节点的相关子树指向子节点
      在这里插入图片描述

    if ((delNode.left && !delNode.right) || (delNode.right && !delNode.left)) {
      // 2.1 有左子结点
      if (delNode.left) {
        if (delNode === this.root) {
          this.root = delNode.left;
        } else {
          if (delNode.isLeft) {
            delNode.parent!.left = delNode.left;
          } else {
            delNode.parent!.right = delNode.left;
          }
        }
      } else {
        if (delNode === this.root) {
          this.root = delNode.right;
        } else {
          if (delNode.isLeft) {
            delNode.parent!.left = delNode.right;
          } else {
            delNode.parent!.right = delNode.right;
          }
        }
      }
    }
    
  • 有两个子节点时:要删除节点的左右子树都不为null

    • 要删除的节点有两个子节点,甚至子节点还有子节点,这种情况下需要从下面的子节点中找到一个节点,来替换当前的节点

    • 这个替换的节点怎么找呢?

      比删除节点小一点点的节点,一定是删除节点左子树的最大值

      比删除节点大一点点的节点,一定是删除节点右子树的最小值

      那么这两个节点就是前驱节点和后继节点是指在中序遍历(即左-根-右顺序)中的前一个节点和后一个节点,我们以后继节点实现代码

    • 找到后继节点后,让删除节点替换为后继节点,若后继节点等于删除节点的右子树时,让删除节点的右子树指向后继节点的右子树
      在这里插入图片描述

    • 当后继节点有右子树时,把右子树提到后继节点的位置
      在这里插入图片描述

      在这里插入图片描述

    if (delNode.left && delNode.right) {
      const successor = this.getSuccessor(delNode);
      delNode.value = successor.value;
      if (delNode.right === successor) {
        delNode.right = successor.right;
      } else {
        successor.parent!.left = successor.right;
        if (successor.right) {
          successor.right.parent = successor.parent;
        }
      }
    }
    
  • 抽取后完整代码如下:

    // 获取删除节点的后继节点
    private getSuccessor(node: TreeNode<T>): TreeNode<T> {
      let current = node.right!;
      while (current.left) {
        current = current.left;
      }
      return current;
    }
    // 删除值
    remove(value: T): boolean {
      const delNode = this.searchNode(this.root, value);
      if (!delNode) return false;
    
      if (delNode.left && delNode.right) {
        const successor = this.getSuccessor(delNode);
        delNode.value = successor.value;
        if (delNode.right === successor) {
          delNode.right = successor.right;
        } else {
          successor.parent!.left = successor.right;
          if (successor.right) {
            successor.right.parent = successor.parent;
          }
        }
      } else {
        const replaceNode = delNode.left || delNode.right;
        if (delNode === this.root) {
          this.root = replaceNode;
        } else if (delNode.isLeft) {
          delNode.parent!.left = replaceNode;
        } else {
          delNode.parent!.right = replaceNode;
        }
    
        if (replaceNode) {
          replaceNode.parent = delNode.parent;
        }
      }
    
      return true;
    }
    

全部代码

import { btPrint } from "hy-algokit";

class TreeNode<T> {
  value: T;
  left: TreeNode<T> | null = null;
  right: TreeNode<T> | null = null;
  parent: TreeNode<T> | null = null;
  constructor(value: T) {
    this.value = value;
  }
  get isLeft() {
    return !!(this.parent && this === this.parent.left);
  }
  get isRight() {
    return !!(this.parent && this === this.parent.right);
  }
}

class BSTree<T = number> {
  private root: TreeNode<T> | null = null;

  // 打印树结构
  print() {
    btPrint(this.root);
  }

  // 插入数据
  insert(value: T) {
    const newNode = new TreeNode(value);

    if (!this.root) {
      this.root = newNode;
    } else {
      /* 非递归 */
      // while (current) {
      //   if (current.value > value) {
      //     if (!current.left) {
      //       current.left = newNode;
      //       current = null;
      //     } else {
      //       current = current.left;
      //     }
      //   } else {
      //     if (!current.right) {
      //       current.right = newNode;
      //       current = null;
      //     } else {
      //       current = current.right;
      //     }
      //   }
      // }

      /* 递归 */
      this.insertNode(this.root, newNode);
    }
  }
  private insertNode(node: TreeNode<T>, newNode: TreeNode<T>) {
    if (newNode.value < node.value) {
      // 插入的值小于比较值,判断其左边,为空直接插入,不为空递归再次判断
      if (!node.left) {
        node.left = newNode;
        newNode.parent = node;
      } else {
        this.insertNode(node.left, newNode);
      }
    } else {
      if (!node.right) {
        node.right = newNode;
        newNode.parent = node;
      } else {
        this.insertNode(node.right, newNode);
      }
    }
  }

  // 先序遍历
  preOrderTraverse() {
    this.preOrderTraverseNode(this.root);
  }
  private preOrderTraverseNode(node: TreeNode<T> | null) {
    if (node) {
      this.preOrderTraverseNode(node.left);
      // console.log(node.value); // 先遍历打印出根节点
      this.preOrderTraverseNode(node.right);
      console.log(node.value); // 先遍历打印出根节点
    }
  }

  // 中序遍历
  inOrderTraverse() {
    this.inOrderTraverseNode(this.root);
  }
  private inOrderTraverseNode(node: TreeNode<T> | null) {
    if (node) {
      this.inOrderTraverseNode(node.left);
      console.log(node.value);
      this.inOrderTraverseNode(node.right);
    }
  }

  // 后序遍历
  postOrderTraverse() {
    this.postOrderTraverseNode(this.root);
  }
  private postOrderTraverseNode(node: TreeNode<T> | null) {
    if (node) {
      this.postOrderTraverseNode(node.left);
      this.postOrderTraverseNode(node.right);
      console.log(node.value);
    }
  }

  // 层序遍历
  levelOrderTraverse() {
    if (!this.root) return;
    let queue: TreeNode<T>[] = [this.root];
    while (queue.length) {
      const current = queue.shift()!;
      console.log(current.value);
      current.left && queue.push(current.left);
      current.right && queue.push(current.right);
    }
  }

  // 获取最小值
  getMinValue(): T | null {
    if (!this.root) return null;
    let current = this.root;
    while (current.left) {
      current = current.left;
    }
    return current.value;
  }

  // 获取最大值
  getMaxValue(): T | null {
    if (!this.root) return null;
    let current = this.root;
    while (current.right) {
      current = current.right;
    }
    return current.value;
  }

  // 搜索值
  search(value: T): boolean {
    /* 递归 */
    return !!this.searchNode(this.root, value);

    /* 非递归 */
    // if (!this.root) return false;
    // let current: TreeNode<T> | null = this.root;
    // while (current) {
    //   if (value > current.value) {
    //     current = current.right;
    //   } else if (value < current.value) {
    //     current = current.left;
    //   } else {
    //     return true;
    //   }
    // }
    // return false;
  }
  private searchNode(node: TreeNode<T> | null, value: T): TreeNode<T> | null {
    if (!node) return null;
    if (value < node.value) {
      return this.searchNode(node.left, value);
    } else if (value > node.value) {
      return this.searchNode(node.right, value);
    } else {
      return node;
    }
  }

  // 获取删除节点的后继节点
  private getSuccessor(node: TreeNode<T>): TreeNode<T> {
    let current = node.right!;
    while (current.left) {
      current = current.left;
    }
    return current;
  }
  // 删除值
  remove(value: T): boolean {
    const delNode = this.searchNode(this.root, value);
    if (!delNode) return false;

    if (delNode.left && delNode.right) {
      const successor = this.getSuccessor(delNode);
      delNode.value = successor.value;
      if (delNode.right === successor) {
        delNode.right = successor.right;
      } else {
        successor.parent!.left = successor.right;
        if (successor.right) {
          successor.right.parent = successor.parent;
        }
      }
    } else {
      const replaceNode = delNode.left || delNode.right;
      if (delNode === this.root) {
        this.root = replaceNode;
      } else if (delNode.isLeft) {
        delNode.parent!.left = replaceNode;
      } else {
        delNode.parent!.right = replaceNode;
      }

      if (replaceNode) {
        replaceNode.parent = delNode.parent;
      }
    }

    return true;
  }
}

class Phone {
  constructor(public name: string, public price: number) {}
  /* 
    在 JavaScript 中,valueOf() 方法的作用是让对象在参与一些特定运算(比如数学运算和比较运算)时,
    将对象转换为一个原始值。通过添加 valueOf() 方法,
    可以让对象在数值操作中自动返回指定的属性值(在此例中为 price)
  */
  valueOf() {
    return this.price;
  }
}

const phone1 = new Phone("sanxing", 910);
const phone2 = new Phone("xiaomi", 760);
const phone3 = new Phone("oppo", 740);
const phone4 = new Phone("huawei", 800);
const phone5 = new Phone("apple", 1200);
const obst = new BSTree<Phone>();
obst.insert(phone1);
obst.insert(phone2);
obst.insert(phone3);
obst.insert(phone4);
obst.insert(phone5);

obst.print();
/* 
  1. 这时打印是下面的内容,显然是错误的,这是因为比较的问题,我们想让他根据价格形成树结构,该怎么做哪?
  [object Object]
         └───────────────┐
                  [object Object]
                         └───────┐
                          [object Object]
                                 └───┐
                                 [object Object]
                                     └─┐
                                     [object Object]

  2. 我们根绝价格形成树结构只需要在Phone中加入valueOf方法即可
         910      
    ┌───┴───┐
   760    1200
  ┌─┴─┐
 740 800
*/

const bst = new BSTree();

// 插入测试
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);
bst.insert(6);
bst.print();
/* 
                 11
        ┌───────┴───────┐
        7              15
    ┌───┴───┐       ┌───┴───┐
    5       9      13      20
  ┌─┴─┐   ┌─┴─┐   ┌─┴─┐   ┌─┴─┐
  3   6   8  10  12  14  18  25

  
*/

// 遍历测试
// bst.preOrderTraverse();
// bst.inOrderTraverse();
// bst.postOrderTraverse();
// bst.levelOrderTraverse();

console.log(bst.getMinValue()); // 3
console.log(bst.getMaxValue()); // 25
console.log(bst.search(3)); // true
console.log(bst.search(11)); // true
console.log(bst.search(13)); // true
console.log(bst.search(30)); // false

// 删除测试
// 1. 删除叶子节点
console.log(bst.remove(3));
console.log(bst.remove(14));
console.log(bst.remove(18));
bst.print();
/* 
              11
        ┌───────┴───────┐
        7              15
    ┌───┴───┐       ┌───┴───┐
    5       9      13      20
    └─┐   ┌─┴─┐   ┌─┘       └─┐
      6   8  10  12          25
*/

// 2. 删除有一个节点的
console.log(bst.remove(5));
console.log(bst.remove(13));
console.log(bst.remove(20));
bst.print();
/* 
               11
        ┌───────┴───────┐
        7              15
    ┌───┴───┐       ┌───┴───┐
    6       9      12      25
          ┌─┴─┐
          8  10
*/

// 删除有两个节点的
bst.remove(11);
bst.print();
/* 
               12
        ┌───────┴───────┐
        7              15
    ┌───┴───┐           └───┐
    6       9              25
          ┌─┴─┐
          8  10
*/

bst.remove(12);
bst.print();
/* 
               15
        ┌───────┴───────┐
        7              25
    ┌───┴───┐
    6       9
          ┌─┴─┐
          8  10
*/

bst.remove(7);
bst.print();
/* 
               15
        ┌───────┴───────┐
        8              25
    ┌───┴───┐
    6       9
            └─┐
             10
*/

缺陷

  • 二叉搜索树作为数据存储的结构有重要的优势:可以快速地找到给定关键字的数据项,并且可以快速地插入和删除数据项

  • 但是二叉搜索树有一个很麻烦的问题:如果插入的数据是有序的数据,比如有一棵初始化为 33 20 77 的二叉树,插入下面的数据:15 11 4 2
    在这里插入图片描述

  • 插入连续数据后,二叉树分布的不均匀,我们称这种树为非平衡树

  • 对于一棵平衡二叉树来说,插入/查找等操作的效率是O(logN),对于一棵非平衡二叉树,相当于编写了一个链表,查找效率变成了O(N)

平衡二叉树

平衡二叉树是为了解决普通二叉搜索树在极端情况下(例如插入有序数据时)退化成链表的问题,在这种树中,每个节点的左子树和右子树的高度差不能超过一定的值

在这里插入图片描述

常见的平衡树

AVL树和红黑树是两种常见的平衡二叉树实现,它们通过旋转等方式来维持树的平衡,确保查找、插入和删除操作的时间复杂度保持在 O(log⁡n) 以内。在实际应用中,红黑树由于其效率和实现复杂度的平衡,广泛用于各种编程语言的标准库中

  • AVL树:
    • AVL树是最早的一种平衡树,它有些办法保持树的平衡(每个节点多存储了一个额外的数据)

    • 因为AVL树是平衡的,所以时间复杂度也是O(logN),但是每次插入/删除操作相对于红黑树效率都不高,所以整体效率不如红黑树

  • 红黑树:
    • 红黑树也通过一些特性来保持树的平衡

    • 因为是平衡树,所以时间复杂度也是在O(logN)

    • 另外插入/删除等操作,红黑树的性能要优于AVL树,所以现在平衡树的应用基本都是红黑树

AVL树和红黑树

具体学习这篇文章:待后面补充

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值