BST 二叉查找树

\(BST\) 二叉查找树

概念

二叉查找树(Binary Search Tree,\(BST\)是一种具有特殊性质的二叉树

  1. 树中每个节点均有权值

  2. 对于一个节点 \(u\),当 \(u\) 存在左子树时,其左子树内所有节点的值均小于(等于) \(u\) 的值

  3. 对于一个节点 \(u\),当 \(u\) 存在右子树时,其左子树内所有节点的值均大于 \(u\) 的值

  4. 树中每一个子树均为 \(BST\),如下图所示:

    graph TB 1((1)) 2((2)) 3((3)) 4((4)) 5((5)) 6((6)) 7((7)) 8((8)) 9((9)) 2-->1 2-->4 4-->3 4-->A((空)) 5-->2 5-->8 6-->B((空)) 6-->7 8-->6 8-->9

基本操作

1. \(BST\) 的遍历

已知一颗 \(BST\),若要从小到大输出它的节点权值,只需对它中序遍历(左中右符合定义中从小到大的顺序),时间复杂度为 \(O(n)\)

void print(int u) {
  if (!u) return ; // 空节点返回
  print(leftson[u]); // 遍历左子树
 	cout << val[p] << endl; // 输出u的值
  print(rightson[u]); // 遍历右子树
}

2. \(BST\) 查询元素位置

\(BST\) 查询元素位置的方法与二分查找相似:

  1. 开始查找
  2. 如果当前值与查找值相同,那么查找成功
  3. 如果查找值小于当前值,那么在左子树继续查找
  4. 同理,如果查找值大于当前值,那么在右子树继续查找
  5. 如果当前节点为空,则查找失败
int find(int u,int x) {
  if (!u) return 0; // 查找失败
  if (x == val[u]) return u; // 查找成功,返回节点编号
  if (x < val[p]) return find(leftson[u],x); // 查找左子树
  else return find(rightson[u],x); // 查找右子树
} 

\(BST\) 查询最值

利用 \(BST\) 中左子树在子树中永远最小的定义,我们可以得到与上类似的方法:

  1. 开始查找
  2. 始终查找左子树
  3. 左子树为空时,该节点的值为 \(BST\) 中的最小值
int findMin(int u) {
  if (!u) return 0; // 空节点
  if (!leftson[u]) return u; // 不存在左子树
  findMin(leftson[u]); // 查询左子树
}

\(BST\) 节点的插入

\(BST\) 插入节点的方法与二分查找相似,不断树中缩小范围,最后确定合适的位置插入:

  1. 开始插入
  2. 如果插入值小于当前值,那么在左子树继续查找
  3. 同理,如果插入值大于当前值,那么在右子树继续查找
  4. 如果当前节点为空,则在这个位置建立新节点,节点值为插入值

注意:在步骤 \(2\)\(3\) 中,对于相同权值的点,要么将它插在其中一个子树(本文不考虑重复),要么在节点中储存一个计数器记录有几个相同的节点

void insert(int &u,int x) {
  if (!u) u = newNode(x); // 新建节点
  else if (x < val[u]) insert(leftson[u],x); // 查询左子树
  else if (x > val[u]) insert(rightson[u],x); // 查询右子树
}

\(BST\) 节点的删除

\(BST\) 删除节点的方法较为复杂,其分为三种情况,令删除节点编号为 \(u\)

  1. \(u\)叶子节点,直接删除

    graph TB 1((1)) 2((2)) 3((3)) 2-->1 2-->3 A((空)) B((2)) C((3)) B-->A B-->C
  2. \(u\) 只有一个子节点,删除它后,我们用它的唯一的子节点替补它的位置

    graph TB 1((1)) 2((2)) 3((3)) 4((4)) 3-->2 2-->1 3-->4 A((1)) C((3)) D((4)) C-->A C-->D
  3. \(u\) 有两个子节点,我们的方法是用 \(u\) 右子树中的最小值来代替它,并将该最小值节点删除(根据 \(BST\) 的定义,这个点一定为前两种情况

    如下图(删除 \(4\) 号):

    graph TB 1((1)) 2((2)) 3((3)) 4(((4))) 5((5)) 6((6)) 7((7)) 2-->1 2-->4 4-->3 4-->7 7-->5 7-->A((空)) 5-->B((空)) 5-->6 a1((1)) a2((2)) a3((3)) a4(((5))) a5[5] a6((6)) a7((7)) a2-->a1 a2-->a4 a4-->a3 a4-->a7 a7-->a5 a7-->aA((空)) a5-->aB((空)) a5-->a6 b1((1)) b2((2)) b3((3)) b5(((5))) b6((6)) b7((7)) b2-->b1 b2-->b5 b5-->b3 b5-->b7 b7-->b6 b7-->bA((空))

观察 \(1\)\(2\) 可以发现,这两种情况是相同的(叶子节点可以被当作右子树为空的节点

int deleteMin(int &u) { // 删除该子树最小的节点
  if (!leftson[u]) {
    int t = val[u];
    u = rightson[u]; // 将右子树替补上来
    return t;
  }
  return deleteMin(leftson[u]);
}
void deleteNum(int &u,int x) {
  if (val[u] == x) {
    if (leftson[u] && rightson[u]) val[u] = deleteMin(rightson[u]); // 情况3
    else u = (leftson[u] ? leftson[u] : rightson[u]);
    return ;
  }
  if (val[x] > p) deleteNum(leftson[u],x);
  else deleteNum(rightson[u],x);
}

时间复杂度

我们发现,当出现特殊的数据时,时间复杂度可能退化为 \(O(n)\)

graph TB 1((1)) 2((2)) 3((3)) 4((4)) 5((5)) 1-->2 2-->3 3-->4 4-->5

这时,我们需要对这个数据结构进行优化(\(Treap\)\(Splay\)树 ……)
Treap

posted @ 2025-04-05 21:00  nightmare_lhh  阅读(28)  评论(0)    收藏  举报