上期回顾: 【算法】初阶数据结构
个人主页:C_GUIQU
归属专栏:算法
目录
正文
1. 线段树
1.1 线段树的基本概念
线段树是一种基于区间的数据结构,常用于高效地处理区间查询(如区间求和、区间最值等)以及区间更新操作。它将一个区间不断地划分成更小的子区间,构建成一棵二叉树的形式,树的每个节点对应一个区间。
例如,对于区间 [1, 10]
,根节点对应整个区间 [1, 10]
,根节点的左子节点可能对应区间 [1, 5]
,右子节点对应区间 [6, 10]
,然后继续细分下去,直到达到最底层的叶子节点(每个叶子节点对应一个单个元素的区间)。
1.2 线段树的构建
以下是用C++实现线段树构建(以区间求和为例)的代码示例,这里假设给定一个数组 nums
,要构建基于其区间的线段树,线段树节点结构体定义如下:
struct SegmentTreeNode {
int start, end; // 节点对应的区间范围
int sum; // 该区间的元素和
SegmentTreeNode* left;
SegmentTreeNode* right;
SegmentTreeNode(int s, int e) : start(s), end(e), sum(0), left(NULL), right(NULL) {}
};
SegmentTreeNode* buildSegmentTree(vector<int>& nums, int start, int end) {
if (start > end) return NULL;
SegmentTreeNode* root = new SegmentTreeNode(start, end);
if (start == end) { // 叶子节点,对应单个元素区间
root->sum = nums[start];
} else {
int mid = start + (end - start) / 2;
root->left = buildSegmentTree(nums, start, mid);
root->right = buildSegmentTree(nums, mid + 1, end);
root->sum = root->left->sum + root->right->sum; // 区间和等于左右子区间和相加
}
return root;
}
1.3 线段树的更新操作
当需要更新数组中某个元素的值(对应线段树中某个叶子节点所在区间),并相应地更新线段树各相关节点的区间和时,可以通过以下代码实现(假设要更新位置 index
的值为 newVal
):
void update(SegmentTreeNode* root, int index, int newVal) {
if (root->start == root->end && root->start == index) { // 找到对应叶子节点
root->sum = newVal;
return;
}
int mid = root->start + (root->end - root->start) / 2;
if (index <= mid) {
update(root->left, index, newVal);
} else {
update(root->right, index, newVal);
}
root->sum = root->left->sum + root->right->sum; // 更新父节点的区间和
}
1.4 线段树的查询操作
比如要查询区间 [left, right]
的元素和,可以用如下代码实现:
int query(SegmentTreeNode* root, int left, int right) {
if (root->start == left && root->end == right) {
return root->sum;
}
int mid = root->start + (root->end - root->start) / 2;
if (right <= mid) {
return query(root->left, left, right);
} else if (left > mid) {
return query(root->right, left, right);
} else {
return query(root->left, left, mid) + query(root->right, mid + 1, right);
}
}
1.5 线段树的应用场景
- 区间统计问题:在处理大量区间求和、求最值等统计需求且需要频繁更新数据的场景中很有用,比如统计一个数组在不同区间内的元素总和,当数组元素有变化时能快速更新并获取新的区间统计值,常用于动态规划的优化以及一些与区间相关的算法竞赛题目中。
- 处理二维区间问题(扩展到二维线段树):可以进一步拓展到二维平面上处理矩形区间的相关问题,如二维区间求和、求最值等,在图形处理、地理信息系统等领域有潜在应用场景。
2. 树状数组
2.1 树状数组的基本概念
树状数组(Binary Indexed Tree,BIT)也叫二叉索引树,同样是用于高效处理区间查询和单点更新的数据结构,相较于线段树,它的实现更为简洁。它利用了二进制的一些特性来巧妙地维护区间信息,每个节点可以看作是覆盖了一定区间范围的元素和(或其他满足可叠加性质的统计值)。
例如,对于一个长度为 n
的数组 arr
,树状数组 bit
中的 bit[i]
所管理的区间范围与 i
的二进制表示相关,它覆盖了原数组中从 i - lowbit(i) + 1
到 i
的元素(lowbit(i)
表示 i
的二进制表示中最低位的 1 以及后面的 0 所构成的数值)。
2.2 树状数组的关键操作
- 获取
lowbit
值:这是树状数组操作的基础,代码实现如下:
int lowbit(int x) {
return x & (-x);
}
- 单点更新:当更新原数组中某个元素的值时,需要相应地更新树状数组中相关节点的值,代码示例如下(假设要更新数组
arr
中下标为index
的元素,值增加delta
,对应的树状数组为bit
):
void update(int index, int delta, vector<int>& bit) {
for (; index < bit.size(); index += lowbit(index)) {
bit[index] += delta;
}
}
- 区间查询:例如要查询区间
[1, right]
的元素和,可以通过以下代码实现(基于树状数组bit
):
int query(int right, const vector<int>& bit) {
int sum = 0;
for (; right > 0; right -= lowbit(right)) {
sum += bit[right];
}
return sum;
}
2.3 树状数组的应用场景
- 动态前缀和问题:在需要频繁更新数组元素值,同时又要快速获取前缀和(进而可以推导出任意区间和)的场景下,树状数组表现出色,比如实时统计学生成绩的动态排名变化(每次成绩更新后快速获取前缀和来确定排名区间),相较于直接计算前缀和,它可以在更短的时间复杂度内完成更新和查询操作。
- 逆序对问题的求解:通过巧妙地利用树状数组的特性,可以高效地统计一个数组中逆序对的数量,这在排序算法优化以及一些与数组顺序相关的数据分析场景中有应用。
3. 平衡二叉搜索树(以AVL树为例)
3.1 AVL树的基本概念
AVL树是一种自平衡的二叉搜索树(Binary Search Tree,BST),它在满足二叉搜索树性质(左子树上所有节点的值都小于根节点的值,右子树上所有节点的值都大于根节点的值,且左右子树也分别是二叉搜索树)的基础上,还保证任意节点的左右子树的高度差的绝对值不超过1,通过这样的平衡机制来避免二叉搜索树退化成链表的情况,从而保证较好的查找、插入和删除操作的时间复杂度(平均和最坏情况都能维持在 O ( log n ) O(\log n) O(logn))。
3.2 AVL树的平衡调整
当插入或删除节点导致树的平衡性被破坏时,需要进行相应的调整操作,主要有四种旋转操作来恢复平衡:左旋、右旋、左右旋、右左旋。
- 左旋:以某个节点
x
为例,如果它的右子树的高度比左子树高,且插入或删除操作影响了右子树导致不平衡,可能需要进行左旋操作,大致过程是将x
的右子节点y
提升为新的根节点,x
变为y
的左子节点,同时调整相关子树的位置,代码示例(这里是简化示意,实际中节点结构体包含更多信息):
struct AVLTreeNode {
int val;
AVLTreeNode* left;
AVLTreeNode* right;
int height; // 节点所在子树的高度
AVLTreeNode(int v) : val(v), left(NULL), right(NULL), height(1) {}
};
AVLTreeNode* leftRotate(AVLTreeNode* x) {
AVLTreeNode* y = x->right;
x->right = y->left;
y->left = x;
// 更新节点高度(这里省略具体高度计算函数实现,一般根据左右子树高度最大值加1来计算)
x->height = max(getHeight(x->left), getHeight(x->right)) + 1;
y->height = max(getHeight(y->left), getHeight(y->right)) + 1;
return y;
}
-
右旋:与左旋对称,当左子树高度过高且不平衡时使用,操作过程是将左子节点提升为新根节点,原根节点变为其右子节点等,代码实现类似左旋。
-
左右旋和右左旋:在更复杂的不平衡情况(比如先左旋后右旋或者先右旋后左旋的情况)下使用,综合了上述两种基本旋转操作的步骤。
3.3 AVL树的插入和删除操作
插入和删除操作基本遵循二叉搜索树的插入和删除流程,但在操作完成后需要检查树的平衡性,若不平衡则进行相应的平衡调整操作。以下是插入操作的大致代码示例(同样省略部分辅助函数实现):
AVLTreeNode* insert(AVLTreeNode* root, int val) {
if (root == NULL) return new AVLTreeNode(val);
if (val < root->val) {
root->left = insert(root->left, val);
} else {
root->left = insert(root->right, val);
}
// 更新当前节点高度
root->height = max(getHeight(root->left), getHeight(root->right)) + 1;
// 获取平衡因子(左右子树高度差)
int balanceFactor = getBalanceFactor(root);
// 根据平衡因子判断是否需要进行平衡调整
if (balanceFactor > 1 && val < root->left->val) { // 左左情况,右旋
return rightRotate(root);
}
if (balanceFactor < -1 && val > root->right->val) { // 右右情况,左旋
return leftRotate(root);
}
// 其他情况(左右旋、右左旋等)类似进行判断和操作
return root;
}
3.4 AVL树的应用场景
- 高效的动态查找表:在需要频繁进行插入、删除和查找操作,并且对时间复杂度要求较稳定的场景中应用广泛,比如数据库索引的实现,能够在对数时间复杂度内完成各种操作,快速定位到所需的数据记录。
- 有序数据的管理:对于需要维护一组有序数据,同时又可能随时有数据增减变化的情况,AVL树提供了一种高效的数据组织方式,保证数据的有序性以及操作的高效性。
4. 红黑树
4.1 红黑树的基本概念
红黑树也是一种自平衡的二叉搜索树,它通过给节点染上红色或黑色,并遵循一系列特定的规则来保证树的平衡性。其规则主要有:
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色。
- 所有叶子节点(空节点,通常用
NULL
表示)都是黑色。 - 如果一个节点是红色的,则它的子节点必须是黑色的。
- 从任意一个节点到其每个叶子节点的所有路径上包含相同数目的黑色节点。
通过这些规则,红黑树在插入和删除操作后可以通过相对简单的调整操作来维持平衡,保证查找、插入和删除操作的时间复杂度在最坏情况下依然为 O ( log n ) O(\log n) O(logn)。
4.2 红黑树的基本操作及平衡调整
-
插入操作:插入新节点时,先按照二叉搜索树的规则找到插入位置插入新节点(通常初始化为红色),然后根据红黑树的规则进行平衡调整,可能涉及到变色和旋转操作(左旋、右旋等)。例如,当插入的新节点导致出现两个连续的红色节点(违反规则4)时,需要进行相应的调整来恢复红黑树的性质,具体调整方式根据不同的情况有多种,如叔父节点是红色、叔父节点是黑色等不同场景下有不同的处理步骤。
-
删除操作:先按照二叉搜索树的删除流程(如果是删除有两个子节点的节点,通常用其前驱或后继节点替换等方式)删除节点,然后同样要检查红黑树的性质是否被破坏,若破坏则进行平衡调整,调整过程比插入操作更为复杂,涉及到判断节点颜色、子树情况等多方面因素来决定是变色还是旋转操作。
4.3 红黑树的应用场景
- 标准库中的实现:在很多编程语言的标准库中,比如C++ 的
map
、set
等关联容器(底层基于红黑树实现),用于高效地存储和查找键值对,保证了插入、删除以及查找操作的高效性和稳定性,无论数据如何变化,操作的时间复杂度都能维持在对数级别。 - 操作系统中的资源管理:在操作系统对进程、内存块等资源进行管理时,红黑树可以用来构建高效的查找和管理结构,例如按照进程的优先级、内存地址等属性构建红黑树,方便快速查找、插入和删除相关资源节点,同时保证整体结构的平衡性和操作效率。
5. B树与B+树
5.1 B树的基本概念
B树是一种多路平衡查找树,它适用于磁盘等外部存储设备的数据存储和检索,特点是每个节点可以有多个子节点(一般远大于2,不像二叉树只有两个子节点),并且节点中存储多个键值对。B树的一个非根节点至少包含 t - 1
个键值对(t
称为阶数,是一个预先定义的参数,通常根据磁盘块大小等因素确定),最多包含 2t - 1
个键值对,根节点最少可以只有1个键值对。
例如,对于一个3阶B树,每个节点的键值对数量范围是 1
到 5
,每个节点的子节点数量比键值对数量多1,即范围是 2
到 6
。
5.2 B树的查找、插入和删除操作
-
查找操作:从根节点开始,根据节点中存储的键值对来判断向下查找的子节点路径,类似二叉搜索树的查找方式,只是在每个节点处要比较多个键值对,直到找到目标键值对或者到达叶子节点确定不存在该键值对为止。
-
插入操作:先通过查找操作确定插入位置,若插入后节点的键值对数量不超过上限,则直接插入;若超过上限,则需要对节点进行分裂操作,将节点一分为二,并将中间的键值对上移到父节点等,可能还会引发父节点的进一步分裂,依次向上调整,保证B树的结构性质。
-
删除操作:先找到要删除的键值对,然后根据节点的不同情况(如该节点是叶子节点还是非叶子节点,节点的键值对数量等)进行不同的处理,可能涉及到合并节点、借调键值对等操作来维持B树的结构完整性。
5.3 B+树的基本概念与特点
B+树是B树的一种变体,它与B树的主要区别在于:
- 所有的非叶子节点只存储键值,不存储实际的数据记录,数据记录只存储在叶子节点上。
- 叶子节点之间通过指针连接形成一个有序链表,方便范围查询。
这样的结构使得B+树在数据库索引等需要频繁进行范围查询以及大量数据存储和检索的场景中更具优势,因为可以沿着叶子节点的链表快速遍历获取一段范围内的数据。
5.4 B树与B+树的应用场景
- 数据库索引:B树和B+树是数据库中常用的索引结构,比如MySQL中的InnoDB存储引擎使用B+树来构建索引,能够高效地支持对大量数据的快速查找、插入和删除操作,尤其是在处理范围查询(如查询某个区间内的所有记录)时,B+树的优势更加明显,通过叶子节点链表可以快速定位到满足条件的所有数据记录。
- 文件系统索引:在文件系统中,为了快速定位文件,也常采用类似B树或B+树的结构来构建目录索引,方便根据文件名等信息快速查找文件所在的磁盘位置,提高文件访问的效率。
6. 跳表
6.1 跳表的基本概念
跳表(Skip List)是一种基于有序链表的数据结构,它通过在原始链表的基础上添加多层索引链表来提高查找效率,有点类似于在书本中添加目录,使得可以快速跳过一些不必要的节点进行查找。
在跳表中,最底层是原始的有序链表,存储了所有的数据元素,然后每隔一定数量的节点就向上抽出一些节点构建上一层的索引链表,每一层的索引链表节点数量依次递减,最顶层的索引链表节点最少。并且同一列上的节点(跨越各层的垂直方向上的节点)是相互关联的,这样在查找元素时,可以从顶层索引链表开始,快速跳过很多节点,定位到目标元素所在的大致区间,然后再逐步下到低层链表进行更精细的查找,类似二分查找的思想。
例如,有一个存储整数的跳表,最底层链表节点依次为 1
、3
、4
、6
、8
、9
、11
、12
等,然后可能每隔 2
个节点抽取一个节点构建上一层索引链表,像 1
、6
、11
等构成第二层索引链表,再往上可能继续抽取构建更高层索引链表。
6.2 跳表的构建与插入操作
- 构建跳表节点结构体:以下是一个简单的跳表节点结构体定义示例,每个节点包含数据值、指向下一层同位置节点的指针数组(用于跨越不同层)以及一个用于标记是否是头节点的标识(方便操作)。
const int MAX_LEVEL = 16; // 设定最大层数,可根据实际情况调整
struct SkipListNode {
int val;
SkipListNode* next[MAX_LEVEL];
bool is_head;
SkipListNode(int v, bool head = false) : val(v), is_head(head) {
for (int i = 0; i < MAX_LEVEL; ++i) {
next[i] = NULL;
}
}
};
- 插入操作:插入新元素时,首先要确定新元素应该插入的位置以及它在各层索引链表中的位置。通常会通过随机生成一个层数(新节点要跨越的层数,层数越高概率越低,一般符合幂律分布)来决定新节点在跳表中的“高度”,然后像在有序链表中插入节点一样,从顶层索引链表开始查找合适的插入位置,依次向下调整各层的指针关系,将新节点插入到相应位置。示例代码如下(这里是简化示意,省略了一些边界判断和辅助函数等):
SkipListNode* insert(SkipListNode* head, int val) {
SkipListNode* cur[MAX_LEVEL];
SkipListNode* p = head;
for (int i = MAX_LEVEL - 1; i >= 0; --i) {
while (p->next[i]!= NULL && p->next[i]->val < val) {
p = p->next[i];
}
cur[i] = p;
}
p = p->next[0];
if (p == NULL || p->val!= val) { // 不存在重复元素才插入
int level = randomLevel(); // 随机生成新节点的层数,这里假设randomLevel函数已定义
SkipListNode* newNode = new SkipListNode(val);
for (int i = 0; i < level; ++i) {
newNode->next[i] = cur[i]->next[i];
cur[i]->next[i] = newNode;
}
}
return head;
}
6.3 跳表的查找与删除操作
- 查找操作:从顶层索引链表开始,根据节点的值与目标值的大小关系,沿着指针向右移动或者向下移动到下一层链表继续查找,直到找到目标元素或者确定目标元素不存在为止。代码示例如下:
bool search(SkipListNode* head, int val) {
SkipListNode* p = head;
for (int i = MAX_LEVEL - 1; i >= 0; --i) {
while (p->next[i]!= NULL && p->next[i]->val < val) {
p = p->next[i];
}
}
p = p->next[0];
return p!= NULL && p->val == val;
}
- 删除操作:先通过查找操作确定要删除元素的位置,如果找到目标元素,则从顶层索引链表开始,调整各层相应节点的指针关系,将目标元素所在节点从各层链表中移除,同时要注意处理一些边界情况,比如删除节点后某些层的链表可能出现断链等情况需要修复。示例代码如下(同样是简化示意):
SkipListNode* deleteNode(SkipListNode* head, int val) {
SkipListNode* cur[MAX_LEVEL];
SkipListNode* p = head;
for (int i = MAX_LEVEL - 1; i >= 0; --i) {
while (p->next[i]!= NULL && p->next[i]->val < val) {
p = p->next[i];
}
cur[i] = p;
}
p = p->next[0];
if (p!= NULL && p->val == val) {
for (int i = 0; i < MAX_LEVEL; ++i) {
if (cur[i]->next[i] == p) {
cur[i]->next[i] = p->next[i];
}
}
delete p;
}
return head;
}
6.4 跳表的应用场景
- 高效的动态有序集合实现:在一些需要频繁进行插入、删除和查找操作的有序数据场景中,跳表是一种很好的选择,比如在 Redis 数据库中,有序集合(Sorted Set)的底层部分实现就采用了跳表,它能够在相对简单的实现逻辑下,提供与平衡二叉搜索树相近的时间复杂度(查找、插入、删除操作平均时间复杂度都为 O ( log n ) O(\log n) O(logn)),同时代码实现相对更易于理解和维护。
- 范围查询优化:由于跳表天然的多层链表结构,在进行范围查询(比如查找大于等于某个值且小于等于另一个值的所有元素)时,可以通过在各层索引链表中快速定位起始和结束的大致区间,然后在底层链表中精准遍历获取范围内的元素,效率较高,适用于一些对有序数据范围查询有需求的应用,如实时数据分析系统中对有序时间序列数据的区间查询等。
7. 持久化数据结构(以持久化线段树为例)
7.1 持久化数据结构的基本概念
持久化数据结构是指在对数据结构进行修改操作(如插入、删除等)时,能够保留其历史版本的数据结构。也就是说,在任何时刻都可以访问数据结构在过去某个时间点的状态,就好像对数据结构的每一次修改都被记录下来了一样,这在很多需要回溯数据状态、进行版本管理或者实现一些具有历史记录需求的算法中非常有用。
以持久化线段树为例,常规的线段树在进行更新操作后,原来的状态就被改变了,但持久化线段树可以让我们在更新后依然能获取到更新前的线段树结构及对应区间信息等。
7.2 持久化线段树的实现原理
实现持久化线段树主要利用了数据结构的复制和共享部分节点的思想。当对线段树进行更新操作(比如更新某个区间内的值)时,并不是直接在原线段树上修改,而是创建新的节点来存储更新后的值,对于那些没有被更新影响到的区间对应的节点,依然可以共享原来线段树中的节点,通过巧妙地构建节点之间的关系,形成一棵新的线段树版本,同时保留了之前线段树的结构完整性。
例如,假设有一棵线段树用于区间求和,初始时表示区间 [1, 10]
的线段树已经构建好,当要更新区间 [3, 6]
的值时,只需要创建新的节点来替换原线段树中受影响的从根节点到叶子节点路径上的相关节点,而其他未受影响的子树节点依然可以复用原来的节点,这样就生成了一棵新的线段树版本,且原来的线段树版本依然可以被访问到。
7.3 持久化线段树的操作示例
以下是简单的持久化线段树节点结构体定义以及更新操作的示例代码(这里以区间求和为例,且简化了一些部分,重点体现持久化的思想):
struct PersistentSegmentTreeNode {
int start, end;
int sum;
PersistentSegmentTreeNode* left;
PersistentSegmentTreeNode* left;
PersistentSegmentTreeNode* right;
PersistentSegmentTreeNode(int s, int e) : start(s), end(e), sum(0), left(NULL), right(NULL) {}
};
PersistentSegmentTreeNode* update(PersistentSegmentTreeNode* root, int index, int newVal) {
if (root == NULL) return root;
if (root->start == root->end && root->start == index) {
PersistentSegmentTreeNode* newNode = new PersistentSegmentTreeNode(root->start, root->end);
newNode->sum = newVal;
return newNode;
}
int mid = root->start + (root->end - root->start) / 2;
if (index <= mid) {
PersistentSegmentTreeNode* newLeft = update(root->left, index, newVal);
PersistentSegmentTreeNode* newNode = new PersistentSegmentTreeNode(root->start, root->end);
newNode->sum = (newLeft!= NULL? newLeft->sum : root->left->sum) + (root->right!= NULL? root->right->sum : 0);
newNode->left = newLeft;
newNode->right = root->right;
return newNode;
} else {
PersistentSegmentTreeNode* newRight = update(root->right, index, newVal);
PersistentSegmentTreeNode* newNode = new PersistentSegmentTreeNode(root->start, root->end);
newNode->sum = (root->left!= NULL? root->left->sum : 0) + (newRight!= NULL? newRight->sum : root->right->sum);
newNode->left = root->left;
newNode->right = newRight;
return newNode;
}
}
7.4 持久化数据结构的应用场景
- 版本控制系统:在软件开发中的版本控制系统(如 Git)里,文件的不同版本可以看作是数据结构的不同历史状态,持久化数据结构的思想可以用于高效地存储和管理这些版本,方便用户随时回溯到某个历史版本,查看代码的历史状态以及进行版本对比等操作。
- 游戏中的存档与回滚:游戏中玩家的游戏进度、游戏世界状态等可以用相关的数据结构来表示,通过使用持久化数据结构,游戏可以实现存档功能,并且在玩家需要回滚到之前某个存档点时,能够准确地恢复当时的游戏状态,提供更好的游戏体验以及方便进行游戏测试等工作。
8. 伸展树(Splay Tree)
8.1 伸展树的基本概念
伸展树是一种自调整的二叉搜索树,它的核心特点是在对树进行查找、插入、删除等操作后,会通过一系列的旋转操作将刚刚被访问的节点(查找操作的目标节点、插入或删除的节点所在位置等)移动到树的根节点位置,这个过程被称为伸展(Splay)操作。通过不断地将频繁访问的节点调整到根节点附近,使得后续对这些节点的访问能够更快地进行,从整体上提高了二叉搜索树操作的时间效率,尽管单次操作的时间复杂度在最坏情况下可能不是最优的,但经过一系列操作后的平均性能表现较好,接近平衡二叉搜索树的效率。
8.2 伸展树的伸展操作
伸展操作主要基于三种旋转方式:左旋、右旋以及它们的组合(类似前面平衡二叉搜索树中的旋转操作,但应用场景和具体执行时机不同),根据被访问节点与它的父节点、祖父节点的位置关系来决定具体的旋转策略,常见的情况有 zig(被访问节点是其父节点的左子节点)、zag(被访问节点是其父节点的右子节点)以及 zig-zig(被访问节点是其父节点的左子节点,其父节点又是祖父节点的左子节点)、zig-zag(被访问节点是其父节点的左子节点,其父节点又是祖父节点的右子节点)等不同组合情况,每种情况对应不同的旋转步骤来将节点逐步移动到根节点位置。例如,对于 zig-zig 情况的旋转操作代码示例(这里是简化示意,节点结构体包含值、左右子节点指针等常规信息)如下:
struct SplayTreeNode {
int val;
SplayTreeNode* left;
SplayTreeNode* right;
SplayTreeNode(int v) : val(v), left(NULL), right(NULL) {}
};
SplayTreeNode* zigZig(SplayTreeNode* grandparent, SplayTreeNode* parent, SplayTreeNode* node) {
if (parent == grandparent->left) {
grandparent->left = parent->right;
parent->right = grandparent;
} else {
grandparent->right = parent->left;
parent->left = grandparent;
}
return parent; // 旋转后,parent节点成为新的根节点
}
8.3 伸展树的查找、插入和删除操作
-
查找操作:基本的查找流程类似普通二叉搜索树,从根节点开始根据节点值与目标值的大小关系向下查找,当找到目标节点或者确定目标节点不存在后,对最后访问到的节点(如果找到就是目标节点,没找到就是查找路径上最后到达的节点)进行伸展操作,将其移动到根节点位置。
-
插入操作:先按照二叉搜索树的插入规则找到插入位置插入新节点,然后对新插入的节点进行伸展操作,使其成为根节点。
-
删除操作:先通过查找操作确定要删除节点的位置,如果找到目标节点,通常用其前驱或后继节点(在二叉搜索树顺序意义下)的值来替换要删除节点的值,然后删除对应的节点,再对替换值的那个节点(原来的前驱或后继节点)进行伸展操作,使其成为根节点。
8.4 伸展树的应用场景
- 缓存管理:在计算机系统的缓存管理中,伸展树可以用来存储缓存中的数据块以及对应的索引信息,由于经常被访问的数据块经过伸展操作后会靠近根节点,下次访问时能更快地找到,类似于将热点数据提升到更容易获取的位置,提高缓存的命中率,进而提升系统整体的运行效率。
- 动态搜索场景:在一些需要动态地频繁查找、插入和删除元素,且元素访问频率不均衡的有序数据管理场景中,伸展树可以根据实际的访问情况自动调整树的结构,使得频繁访问的元素更容易被操作,比普通二叉搜索树在平均性能上更有优势,例如在实时数据分析系统中对不断更新的有序数据进行查询操作时可应用伸展树。
9. 斐波那契堆(Fibonacci Heap)
9.1 斐波那契堆的基本概念
斐波那契堆是一种可合并堆的数据结构,它支持插入、查找最小值、删除最小值以及合并两个堆等操作,并且在一些操作上相比普通的二叉堆(如前面介绍的小顶堆、大顶堆)有着更优的时间复杂度特性(虽然其实际实现相对复杂一些)。它由一组最小堆有序树(每棵树都是最小堆结构,根节点的值最小)组成,并且有一个指向最小元素所在树的根节点的指针,同时还维护了一些额外的信息用于高效地实现各种操作。
斐波那契堆中的树并不要求是完全二叉树,其结构相对更灵活,节点除了包含值、子节点指针等常规信息外,还包含指向兄弟节点的指针(用于构建树与树之间的关联)等,使得它在合并等操作上能够更高效地进行。
9.2 斐波那契堆的关键操作
-
插入操作:插入新元素时,创建一个只包含该元素的新树(满足最小堆性质),然后将这棵新树插入到斐波那契堆中,同时更新指向最小元素所在树的根节点的指针(如果新插入元素比当前最小元素还小,则更新该指针指向新插入元素所在的树的根节点),插入操作的时间复杂度可以达到 O ( 1 ) O(1) O(1),这比普通二叉堆的插入操作(一般为 O ( log n ) O(\log n) O(logn))在时间复杂度上更优。
-
查找最小值操作:直接通过指向最小元素所在树的根节点的指针获取最小值,时间复杂度为 O ( 1 ) O(1) O(1)。
-
删除最小值操作:先移除最小元素所在的树的根节点,然后将其所有子节点(这些子节点本身也是树)都变成独立的树,并加入到斐波那契堆中,接着进行一系列复杂的合并操作(基于树的度数等条件,度数指的是节点的子节点数量),保证堆的结构性质,这个操作的平摊时间复杂度为 O ( log n ) O(\log n) O(logn),尽管单次操作在最坏情况下时间复杂度较高,但经过多次操作平均来看性能较好。
-
合并操作:要合并两个斐波那契堆,只需将两个堆中的所有树合并到一起,然后重新确定最小元素所在的树的根节点,操作简单且时间复杂度为 O ( 1 ) O(1) O(1),方便在需要合并多个堆结构的场景中使用。
9.3 斐波那契堆的应用场景
- 优化图算法中的优先队列使用:在一些图算法(如 Dijkstra 最短路径算法)中,需要频繁地进行插入元素到优先队列、查找最小值。
结语
感谢您的阅读!期待您的一键三连!欢迎指正!