二叉排序树
一、性质
- 左子树 < 根节点
- 右子树 > 根节点
- 中序遍历的结果是一个有序序列
数据结构,就是定义一种性质并且维护这种性质。
二、插入操作
- 插入新的结点,一定会做做为叶子节点
三、删除操作
- 删除度为0的结点,直接删除
- 删除度为1的结点,把孤儿子树挂到其父节点上面
- 删除度为2的结点,可以转化成删除度为1的结点
对于度为2的结点:
- 前驱:左子树的最大值
- 后继:右子树的最小值
四、随堂练习
- 插入顺序会影响最终的树形结构
- 不同的树形结构,查找效率不同
平均查找效率:结点查找次数的期望值, 总 次 数 结 点 数 量 \frac{总次数}{结点数量} 结点数量总次数,假设每个结点都被等概率查找。
五、扩展内容
-
二叉排序树删除代码优化
- 删除掉处理度为0的代码逻辑,不影响代码整体功能
-
如何解决排名相关排名相关的检索需求
-
- 修改二叉排序树的结构定义,增加 size 字段,记录每棵树的节点数量
- k = L S − 1 k = LS - 1 k=LS−1,根节点就是排名第 k 位的元素
- k ≤ L S k \le LS k≤LS,排名第 k 位的元素在左子树中
- k > L S , s e a r c h k ( r o o t − > r c h i l d , k − L S − 1 ) k \gt LS,search_k(root->rchild, k - LS - 1) k>LS,searchk(root−>rchild,k−LS−1)
- 解决 Top-K 问题(找到小于第 k 位的所有元素)
- 根节点就是第 k 位元素的话,就把左子树中的值全部输出出来
- 第 k 位在左子树中,前 k 位元素全都在左子树中
- 第 k 位在右子树中,说明根节点和左子树中的元素,都是前 k 位元素里面的值
- 二叉排序树和快速排序的关系
- 二叉排序树是快速排序在思维逻辑结构层面用的数据结构
- 思考1:快速排序的时间复杂度和二叉排序树建树时间复杂度之间的关系
- 思考2:快速选择算法和二叉排序树之间的关系
- 程序=算法+数据结构
所谓算法设计及分析能力:分类讨论及归纳总结的能力
-
六、代码演示
#include <stdio.h>
#include <stdlib.h>
#define KEY(n) (n ? n->key : -1)
#define SIZE(n) (n ? n->size : 0)
#define L(n) (n ? n->lchild : NULL)
typedef struct Node {
int key, size;
struct Node *lchild, *rchild;
} Node;
void update_size(Node *root);
Node *getNewNode(int key) {
Node *p = (Node *)malloc(sizeof(Node));
p->key = key;
p->size = 1;
p->lchild = p->rchild = NULL;
return p;
}
int search(Node *root,int val) {
if (!root) return 0;
if (root->key == val) return 1;
if (root->key > val) return search(root->lchild, val);
return search(root->rchild, val);
}
Node *insert(Node *root, int key) {
if (!root) return getNewNode(key);
if (root->key == key) return root;
if (key < root->key) root->lchild = insert(root->lchild, key);
else root->rchild = insert(root->rchild, key);
update_size(root);
return root;
}
Node *predecessor(Node *root) {
Node *temp = root->lchild;
while (temp->rchild) temp = root->rchild;
return temp;
}
int search_k(Node *root, int k) {
if (!root) return -1;
if (SIZE(L(root)) == k - 1) return root->key;
if (k <= SIZE(L(root))) {
return search_k(root->lchild, k);
}
return search_k(root->rchild, k - SIZE(L(root->lchild)) - 1);
}
void update_size(Node *root) {
root->size = SIZE(root->lchild) + SIZE(root->rchild) + 1;
return ;
}
Node *erase(Node *root, int key) {
if (!root) return NULL;
if (key < root->key) {
root->lchild = erase(root->lchild, key);
} else if (key > root->key) {
root->rchild = erase(root->rchild, key);
} else {
/*if (root->lchild == NULL && root->rchild == NULL) {
free(root);
return NULL;
} else */
if (root->lchild == NULL || root->rchild == NULL) {
Node *temp = root->lchild ? root->rchild : root->rchild;
free(root);
return temp;
} else {
Node *temp = predecessor(root);
root->key = temp->key;
root->lchild = erase(root->lchild, temp->key);
}
}
update_size(root);
return root;
}
void clear(Node *root) {
if (!root) return ;
clear(root->lchild);
clear(root->rchild);
free(root);
return ;
}
void print(Node *root) {
printf("%d[%d], %d, %d\n", KEY(root), SIZE(root),KEY(root->lchild), KEY(root->rchild));
return ;
}
void output(Node *root) {
if (!root) return ;
output(root->lchild);
print(root);
output(root->rchild);
return ;
}
void output_k(Node *root, int k) {
if (k == 0 || !root) return ;
if (k <= SIZE(L(root))) {
output_k(root->lchild, k);
} else {
output(root->lchild);
print(root);
output_k(root->rchild, k - SIZE(L(root->lchild)) - 1);
}
return ;
}
int main() {
int op, val;
Node *root = NULL;
while (~scanf("%d%d", &op, &val)) {
switch (op) {
case 0: printf("search %d, result : %d\n", val, search(root, val)); break;
case 1: root = insert(root, val); break;
case 2: root = erase(root, val); break;
case 3: {
printf("search k = %d, result : %d\n", val, search_k(root, val));
output_k(root, val);
printf("----------\n");
} break;
case 4: {
printf("output top-%d elements\n", val);
output_k(root, val);
printf("----------\n");
} break;
}
if (op == 1 || op == 2) {
output(root);
printf("------------\n");
}
}
return 0;
}
平衡二叉树排序树 之 AVL 树
一、学习重点
- 平衡二叉树排序树,本质上也是二叉排序树,所以拥有二叉排序树的所有性质
- 平衡二叉树排序树的学习重点,在于平衡条件以及平衡调整的相关学习
二、性质
- 平衡条件:左右子树高度差不超过 1
三、课中思考
- AVL 树改进的是节点数量的下限
- 树高 = 生命长度,节点数量 = 生命财富,不同的算法,达到的结果是不同的
- 教育提升的是底限,而不是上限,上限取决于能力和运气
思考:
- 高度为 H 的BS树,所包含的结点数量在上面范围之内?
- 高度为H的AVL树,所包含的结点数量在上面范围之内?
BS树: H ≤ H\le H≤SIZE ≤ 2 H − 1 \le2^H-1 ≤2H−1
AVL树:设low(H)为高度为H的树最少包含结点,则 l o w ( H − 2 ) + l o w ( H − 1 ) + 1 ≤ S I Z E ≤ 2 H − 1 low(H - 2) + low(H - 1) + 1 \le SIZE \le 2^H - 1 low(H−2)+low(H−1)+1≤SIZE≤2H−1, 设结点数为n,树高为h,则有 h = log a ( n ) h=\log_a(n) h=loga(n), 1.5 ≤ a ≤ 2 1.5\le a \le 2 1.5≤a≤2
四、平衡调整策略
基本操作:
左旋:要旋转的结点K1的右节点K3成为新的根节点,原K3的左子树称为K1的右子树
右旋:要旋转的结点K1的左节点K2成为新的根节点,原K2的右子树称为K1的左子树
-
发生在回溯阶段的,第一个失衡节点处
-
理解平衡调整策略的关键在于:分析清楚四种情况下,ABCD 四棵子树树高的关系
-
LL,大右旋
设A、B、C、D的树高分别为 h 1 h_1 h1、 h 2 h_2 h2、 h 3 h_3 h3、 h 4 h_4 h4,则存在以下关系:
h 1 + 1 = m a x ( h 3 , h 4 ) + 3 → h 1 = m a x ( h 3 , h 4 ) + 2 = h 2 + 1 h_1 + 1 = max(h_3, h_4) + 3\rightarrow h_1 = max(h_3, h_4) + 2 = h_2 + 1 h1+1=max(h3,h4)+3→h1=max(h3,h4)+2=h2+1
则调整后:
A和K1, B和K3高度相等
-
LR,先小左旋,再大右旋
m a x ( h 2 , h 3 ) = h 4 = h 1 max(h_2, h_3) = h_4 = h_1 max(h2,h3)=h4=h1
-
RL,先小右旋,再大左旋
-
RR,大左旋
五、代码演示
- 插入和删除以后,注意重新计算树高字段
- 引入了 NIL 阶段,代替 NULL,NULL 不可访问,NIL 是一个是实际节点,可访问
#include <iostream>
#include <cstdio>
#include <stdlib.h>
#define L(n) (n->lchild)
#define R(n) (n->rchild)
#define H(n) (n->h)
typedef struct Node {
int key, h;
struct Node *lchild, *rchild;
} Node;
Node __NIL;
#define NIL (&__NIL)
__attribute__((constructor))
void init_NIL() {
NIL->key = 0, NIL->h = 0;
NIL->lchild = NIL->rchild = NIL;
return ;
}
void update_high(Node *root);
Node *getNewNode(int key) {
Node *p = (Node *)malloc(sizeof(Node));
p->key = key;
p->h = 1;
p->lchild = p->rchild = NIL;
return p;
}
void clear(Node *root) {
if (root == NIL) return ;
clear(root->lchild);
clear(root->rchild);
free(root);
return ;
}
Node *left_rotate(Node *root) {
Node *temp = root->rchild;
root->rchild = temp->lchild;
temp->lchild = root;
update_high(root);
update_high(temp);
return temp;
}
Node *right_rotate(Node *root) {
Node *temp = root->lchild;
root->lchild = temp->rchild;
temp->rchild = root;
update_high(root);
update_high(temp);
return temp;
}
Node *maintain(Node *root) {
if (abs(H(L(root)) - H(R(root))) <= 1) return root;
if (root->lchild->h > root->rchild->h) {
if (root->lchild->lchild->h < root->lchild->rchild->h) {
root->lchild = left_rotate(root->lchild);
}
root = right_rotate(root);
} else {
if (root->rchild->rchild->h < root->rchild->lchild->h) {
root->rchild = right_rotate(root->rchild);
}
root = left_rotate(root);
}
return root;
}
void update_high(Node *root) {
root->h = (H(L(root)) > H(R(root)) ? H(L(root)) : H(R(root))) + 1;
}
void print(Node *root) {
printf("%d[%d], %d, %d\n", root->key, root->h, root->lchild->key, root->rchild->key);
return ;
}
void output(Node *root) {
if (root == NIL) return ;
print(root);
output(root->lchild);
output(root->rchild);
return ;
}
Node *insert(Node *root, int key) {
if (root == NIL) return getNewNode(key);
if (root->key == key) return root;
if (root->key > key) {
root->lchild = insert(root->lchild, key);
} else {
root->rchild = insert(root->rchild, key);
}
update_high(root);
return maintain(root);
}
Node *predecessor(Node *root) {
Node *temp = root->lchild;
while (temp->rchild) temp = temp->rchild;
return temp;
}
Node *erase(Node *root, int key) {
if (root == NIL) return NIL;
if (key < root->key) {
root->lchild = erase(root->lchild, key);
} else if (root->key < key) {
root->rchild = erase(root->rchild, key);
} else {
if (root->lchild == NIL || root->rchild == NIL) {
Node *temp = root->lchild ? root->lchild : root->rchild;
free(root);
return temp;
} else {
Node *temp = predecessor(root);
root->key = temp->key;
root->lchild = erase(root->lchild, temp->key);
}
}
update_high(root);
return maintain(root);
}
int main() {
int op, val;
Node *root = NIL;
while (~scanf("%d%d", &op, &val)) {
switch (op) {
case 0: root = erase(root, val);
case 1: root = insert(root, val);
}
output(root);
printf("--------\n");
}
return 0;
}
扩展小tips
vim按列添加和删除:
-
按列添加,首先按
ctrl + V
,接下来用箭头键选中列,再按shift + I
,输出要添加的内容,再按ESC
键。
-
按列删除,首先按
ctrl + V
,接下来用箭头键选中列,再按x
即可删除选择列的内容。