目录结构
有序表介绍
一.有序表概述
有序表可以理解C++/Java中的抽象类; \从数据结构角度出发,
它是一种抽象数据结构。它提供实现功能需求,但没有要求严格的具体实现。
对比熟悉无序表(通常是哈希表);
哈希表特点,它的关键字 k e y key key不是有序组织的, 它通过哈希函数打乱离散分布的结果。
有序表实现的方式就是花费一些代价, 将这些关键字key顺序组织起来, 对比无序表它可以实现丰富有序的操作。
若你没有听说过有序表的概念, 那么看完上面的文字, 大概对有序表的实现有概念了吧。
比方说,尝试用一个数组有序的组织数据, 或者有序维护单向双向链表等等。
答案是否定的, 有序表还有一个特点, 它必须保证足够高的效率。
具体, 保证增删查改操作均达到 O ( l o g n ) O(logn) O(logn)的水平, 有序数组增删操作最坏是 O ( n ) O(n) O(n),有序链表查询操作最坏是 O ( n ) O(n) O(n)。
数据结构的抽象描述是这样的:元素按一定顺序(一般非降序或非升序)排列、且可支持高效检索或操作的一类线性表(或广义表).
二.实现有序表的数据结构
常见实现有序表的数据结构:
- 有序数组: 尽管增删 O ( n ) O(n) O(n), 但支持二分查找 O ( l o g n ) O(logn) O(logn)。 实现简单,多查少改场景适用。
- 链表(跳表):单个有序链表组织没有查询优势,但层级有序链表即跳表, 实现全 O ( l o g n ) O(logn) O(logn)操作。
- (自)平衡树结构:数据结构两大阴间->AVL树,红黑树. 竞赛版:替罪羊树,Treap,FHQ Treap,Splay树; 其它的比如数据库中的B树,B*变种,2-3树,2-3-4树这些。有些场景线段树和树状数组也可以充当有序表。
前言
本篇介绍AVL树的算法模板静态数组实现, 有关于类模板(动态空间使用,释放)实现笔者暂时不更。
笔者会不断完善本篇内容, 请各位不吝赐教。
前置知识: 数据结构-二叉搜索树
编程语言:C++. 后续会补充Java版本.
本文基于如下链接实现的AVL树
Luogu3369:普通平衡树
实现了如下操作:
插入、删除、获取关键字的排名、获取排名的关键字,查找前驱和后继等基础功能.
本篇实现了有序表集而非有序表映射, 笔者可以根据下面代码扩展对应的功能。
正文部分
概述
二叉搜索树的时间复杂度依赖高度 O ( h ) O(h) O(h), 最坏是 O ( n ) O(n) O(n).
能否实现一棵自动平衡的树, 将高度h控制在下界 l o g n logn logn.
AVL树是最早提出满足这种要求的树。 它对平衡性的定义非常严格。
针对一般的二叉搜索树, 若采用动态实现的方式, 那么在经过插入或者删除可能会破坏树的平衡性。AVL树通过旋转这种检查调整的操作, 每次插入和删除都会进行这样的检测并将树调整平衡。
理解旋转操作是实现AVL树的重点。
数据结构与全局变量
const static int N = 1e6+10;//数据范围
int head = 0; // AVL树的根节点
int cnt; // 当前最后使用过节点的编号
int key[N]; // 节点的值
int height[N]; // 节点的高度
int leftChild[N]; // 节点的左子节点
int rightChild[N]; // 节点的右子节点
int key_count[N]; // 节点值的重复次数
int size[N]; // 子树大小
head
: AVL 树的根节点编号。cnt
: 用于分配新的节点编号。key
: 存储每个节点的值。height
: 存储每个节点的高度。leftChild
和rightChild
: 存储每个节点的左子节点和右子节点的编号。key_count
: 处理重复元素,记录每个值的出现次数。size
: 记录每个节点所在子树的总节点数(包括重复计数)。
下面我解释一下为什么引入某些数组?
- 由于本题要求记录关键字, 但不需要存储键值对。 因此不需要映射关系的
value
数组。 - 由于AVL树的定义,需要记录当前节点的树高信息来维护平衡性, 需要
height
数组。 - 本题可以重复添加关键字。 针对重复关键字可以采取词频统计的方式。避免了为重复关键字分配节点编号。 引入
key_count
数组。 - 本题要计算排名引入一个
size数组
记录以当前节点为根节点的子树大小可以简化计算。 - 采用递归实现,因此不需要
parent
数组记录每个节点的双亲节点编号。
API
以下是下面内容介绍实现的函数
//函数声明
//up整合子树信息更新当前头节点
void up(int i);
//左旋操作
int leftRotate(int i);
//右旋操作
int rightRotate(int i);
//查询当前值的排名
int getRank(int val);
//查询当前值的排名: 辅助函数
int getRank(int i,int val);
//查询排名为x的关键字
int getIndex(int x);
//查询排名为x的关键字:辅助函数
int getIndex(int i,int x);
//获取节点的前驱节点关键字
int pre(int i);
//pre的辅助函数
int pre(int i,int num);
//获取节点的后继节点关键字
int post(int i);
//post的辅助函数
int post(int i,int num);
//添加元素, 若有多个则增加词频。
void add(int val);
int add(int i,int val);
//删除元素, 若有多个则减少一个词频即可
void remove(int val);
int remove(int i,int val);
辅助函数
更新节点信息 (up
)
//更新size,height信息。 根据孩子更新信息
void up(int i){
size[i] = size[leftChild[i]]+size[rightChild[i]]+key_count[i];
height[i] = max(height[leftChild[i]], height[rightChild[i]]) + 1;
}
- 功能: 根据当前节点的左右子节点信息,更新当前节点的
size
和height
。 - 参数:
i
- 当前节点编号。
左旋操作 (leftRotate
)
左旋操作
左旋操作的理解要点是:
节点i和它右孩子的颠倒父子关系; 同时这种操作不会破坏二叉搜索树的性质;理解这过程左右树高的变化。
如下代码流程:
- 定义变量r,
r=rightChild[i]
- 让
r
的左孩子成为i
的右孩子。(i对r的单向父亲关系解除了) - 让i成为r的左孩子(r对i绑定父子关系)。
- i在r下面, 先对下面的i整合信息更新 u p ( i ) up(i) up(i), 再对 u p ( r ) up(r) up(r)的正确。
- 由于原先以i节点为根的子树变成r为根节点, 向上返回r为左旋后的新节点。
int leftRotate(int i){
int r = rightChild[i];
rightChild[i] = leftChild[r];
leftChild[r] = i;
up(i);
up(r);
return r;
}
- 功能: 对节点
i
进行左旋,保持 AVL 树的平衡。 - 返回值: 旋转后新的根节点编号。
右旋操作 (rightRotate
)
右旋操作
右旋操作的理解要点和左旋操作一致。 可以视作左旋的对称过程。
int rightRotate(int i){
int l = leftChild[i];
leftChild[i] = rightChild[l];
rightChild[l] = i;
up(i);
up(l);
return l;
}
- 功能: 对节点
i
进行右旋,保持 AVL 树的平衡。 - 返回值: 旋转后新的根节点编号。
维护平衡 —四种旋转方式(maintain
)
int maintain(int i){
int lh = height[leftChild[i]];
int rh = height[rightChild[i]];
if(lh - rh > 1){
// 左子树高,需要右旋
if(height[leftChild[leftChild[i]]] >= height[rightChild[leftChild[i]]]){
i = rightRotate(i);
} else {
leftChild[i] = leftRotate(leftChild[i]);
i = rightRotate(i);
}
}
else if(rh - lh > 1){
// 右子树高,需要左旋
if(height[rightChild[rightChild[i]]] >= height[leftChild[rightChild[i]]]){
i = leftRotate(i);
} else {
rightChild[i] = rightRotate(rightChild[i]);
i = leftRotate(i);
}
}
return i;
}
- 功能: 检查并维护节点
i
的平衡,通过旋转操作确保 AVL 树的性质。 - 返回值: 维护平衡后的节点编号。
主要操作函数
插入操作 (add
)
int add(int i, int val){
if(i == 0){
key[++cnt] = val;
height[cnt] = key_count[cnt] = size[cnt] = 1;
return cnt;
}
if(key[i] == val){
key_count[i]++;
}
else if(key[i] > val){
leftChild[i] = add(leftChild[i], val);
}
else{
rightChild[i] = add(rightChild[i], val);
}
up(i);
return maintain(i);
}
void add(int val){
head = add(head, val);
}
- 功能: 向 AVL 树中插入值
val
。 - 参数:
val
- 要插入的值。
- 说明:
- 如果当前节点为空,则创建新节点。
- 如果值已存在,增加
key_count
。 - 根据值的大小决定插入到左子树还是右子树。
- 更新节点信息并维护平衡。
删除操作 (remove
)
int removeMostLeft(int i, int mostLeft){
if(i == mostLeft){
return rightChild[mostLeft];
} else {
leftChild[i] = removeMostLeft(leftChild[i], mostLeft);
up(i);
return maintain(i);
}
}
int remove(int i, int val