数据结构-二叉搜索树 详解

😊 | Powered By HeartFireY | Binary Search Tree

一、二叉搜索树 简介

二叉搜索树(BST, Binary Search Tree)是一种基于二叉树的树形数据结构,定义(满足的性质)如下

  1. 空树是二叉搜索树
  2. 若二叉搜索树的左子树不为空,则其左子树上所有点的权值均小于其根节点的权值;
  3. 若二叉搜索树的右子树不为空,则其右子树上所有点的权值均大于其根节点的权值;
  4. 二叉搜索树的左右子树均为二叉搜索树

不难发现,二叉搜索树是采用递归进行定义的。

我们可以通过例子来具体了解二叉搜索树的结构,如图所示是一颗二叉搜索树:

数据结构-二叉搜索树 详解_子树

二叉搜索树的基本操作所花费的时间与树的高度成正比。对于一个有数据结构-二叉搜索树 详解_算法_02个节点的二叉搜索树:

  • 最优时间复杂度为数据结构-二叉搜索树 详解_算法_03,此时左右平衡
  • 最差时间复杂度为数据结构-二叉搜索树 详解_算法_04,此时二叉搜索树退化为链

二、二叉搜索树-基本操作

我们采用数组表示法来储存一棵搜索树:

int val[x];    //节点x所储存的值
int cnt[x]; //节点x所储存的值出现的次数
int siz[x]; //节点x的子树大小
int lc[x]; //节点x的左孩子
int rc[x]; //节点x的右孩子

为了说明方便,我们定义数据结构-二叉搜索树 详解_算法_05表示二叉树的高度。

1.插入操作

二叉搜索树的建树逻辑是比较简单的,假设我们需要向以数据结构-二叉搜索树 详解_算法_06为根节点的二叉搜索树中插入一个值为数据结构-二叉搜索树 详解_子树_07的新节点。

定义该操作为数据结构-二叉搜索树 详解_二叉树_08,则步骤总结如下:

  1. 判断数据结构-二叉搜索树 详解_数据结构_09是否为空树,如果为空树则建立根节点直接存放数据;
  2. 如果数据结构-二叉搜索树 详解_数据结构_09的权值等于数据结构-二叉搜索树 详解_算法_11,则该节点对应出现的次数自增数据结构-二叉搜索树 详解_二叉树_12次;
  3. 如果数据结构-二叉搜索树 详解_数据结构_09的权值大于数据结构-二叉搜索树 详解_算法_11,则递归向数据结构-二叉搜索树 详解_数据结构_09的左子树中插入数据结构-二叉搜索树 详解_算法_11
  4. 如果数据结构-二叉搜索树 详解_数据结构_09的权值小于数据结构-二叉搜索树 详解_算法_11,则递归向数据结构-二叉搜索树 详解_数据结构_09的右子树中插入数据结构-二叉搜索树 详解_算法_11

根据步骤,我们可以写出插入操作的代码:

void insert(int &root, int v){
if(!root){
val[root = ++sum] = v, cnt[root] = siz[root] = 1;
lc[root] = rc[root] = 0;
return;
}
siz[root]++;
if(val[root] == v){
cnt[root]++;
return;
}
if(val[root] > v) insert(lc[root], v);
if(val[root] < v) insert(rc[root], v);
}

根据上述步骤的描述可以知道:插入操作的时间复杂度为数据结构-二叉搜索树 详解_二叉树_21,其中数据结构-二叉搜索树 详解_算法_05为这棵二叉搜索树的高度。

2.删除操作

删除操作涉及的情况较多,需要进行分类讨论:

假设我们已经拥有一颗根节点为数据结构-二叉搜索树 详解_算法_06的二叉搜索树,现在我们需要删除一个权值为数据结构-二叉搜索树 详解_子树_07的节点。

定义该操作为​​delete(root, v)​​,则步骤总结如下:

  1. 查找:首先需要在节点为数据结构-二叉搜索树 详解_数据结构_09的二叉搜索树上找到该节点;
  2. 判断删除方式:
  • 不完全删除:如果该节点的数据结构-二叉搜索树 详解_数据结构_26值大于数据结构-二叉搜索树 详解_二叉搜索树_27,则只需要对该节点的数据结构-二叉搜索树 详解_数据结构_26做减数据结构-二叉搜索树 详解_二叉搜索树_27运算;
  • 完全删除:如果该节点的数据结构-二叉搜索树 详解_数据结构_26值为数据结构-二叉搜索树 详解_二叉搜索树_27,则需要对该节点进行完全删除:
  1. 如果数据结构-二叉搜索树 详解_子树_32为叶子节点,则直接删除该节点即可;
  2. 如果数据结构-二叉搜索树 详解_子树_32为链节点(只有一个孩子的节点),则返回该节点的孩子;
  3. 如果数据结构-二叉搜索树 详解_子树_32具有两个非空的子节点,则一般用其左子树的最大值或右子树的最小值代替它,然后将它删除。

与插入操作相同,时间复杂度为数据结构-二叉搜索树 详解_二叉树_21

int delmin(int &root){
if(!lc[root]){
int u = root;
root = rc[root];
return u;
}
else{
int u = delmin(lc[0]);
siz[root] = --cnt[u];
return u;
}
}

void delete(int &root, int v){
siz[root]--;
if(val[root] == v){
if(cnt[root] > 1){
cnt[root]--;
return;
}
if(lc[root] && rc[root]) root = delmin(rc[root]);
else root = lc[root] + rc[root];
return;
}
if(val[root] > v) del(lc[root], v);
if(val[root] < v) del(rc[root], v);
}

3.遍历操作

由二叉搜索树的递归定义可知:对二叉搜索树进行中序遍历,得到的序列是非下降序列。

定义该操作为​​travel(int root)​​,则代码如下(实际上与普通二叉树的中序遍历区别不大)

void travel(int root){
if(!root) return;
travel(lc[root]);
for(int i = 1; i <= cnt[root]; i++) cout << "val[root]" << endl;
travel(rc[root]);
}

时间复杂度数据结构-二叉搜索树 详解_二叉搜索树_36

4.查找最大值/最小值

对二叉搜索树查询最大值最小值是非常简单操作:我们根据二叉搜索树的定义可以知道:二叉搜索树的最小值为二叉搜索树左链的端点,与之相反,二叉搜索树的最大值为右链的端点。

定义​​getmin(int root)​​​为寻找最小值的操作,​​getmax(int root)​​为寻找最大值的操作,则代码如下:

int getmin(int root){
if(!lc[root]) return root;
else return getmin(lc[root]);
}

int getmax(int root){
if(!rc[root]) return root;
else return getmax(rc[root]);
}

与插入删除操作相同,时间复杂度为数据结构-二叉搜索树 详解_二叉树_21

5.查询给定元素的排名

排名定义为将数组元素排序后第一个相同元素之前的数的个数加一。

我们在开始时定义并维护数据结构-二叉搜索树 详解_二叉树_38,用于储存每个根节点的子树大小。查找一个子树的大小,首先从根节点跳到这个元素:

  • 如果向右跳,则答案加上左子树节点个数(数据结构-二叉搜索树 详解_子树_39)、和当前节点重复的数的个数(数据结构-二叉搜索树 详解_子树_40);
  • 如果向左跳,则不需要额外附加;
  • 最后答案加上终点的左儿子子树大小+1

定义​​queryrnk(int root, int v)​​,则代码如下:

int queryrnk(int root, int v){
if(val[root] == v) return siz[lc[root]] + 1;
if(val[root] > v) return queryrnk(lc[root], v);
if(val[root] < v) return queryrnk(rc[root], v) + size[lc[root]] + cnt[root];
}

时间复杂度数据结构-二叉搜索树 详解_二叉树_21

6.查询排名为数据结构-二叉搜索树 详解_算法_42的元素

我们需要熟悉一条结论:在一棵子树中,根节点的排名取决于其左子树的大小。

  • 若其左子树的大小数据结构-二叉搜索树 详解_二叉树_43,则该元素位于左子树中;
  • 若其左子树的大小数据结构-二叉搜索树 详解_二叉搜索树_44中,则该元素为子树的根节点;
  • 若其左子树的大小数据结构-二叉搜索树 详解_算法_45,则该元素位于右子树中。
int querykth(int root, int k){
if(siz[lc[root]] >= k) return querykth(lc[root], k);
if(siz[lc[root]] < k - cnt[root]) return querykth(rc[root], k - siz[lc[root]] - cnt[root]);
return val[root]; //如果要拆查询排名为k的元素对应的节点,直接return 0即可
}