二叉排序樹能夠支援多種動態集合操作,它可以被用來表示有序集合,建立索引或優先佇列等。因此,在資訊學競賽中,二叉排序樹應用非常廣泛。
作用於二叉排序樹上的基本操作,其時間複雜度均與樹的高度成正比,對於一棵有 \(n\) 個節點的二叉樹,這些操作在最有情況下執行時間為 \(O( \log_2 n)\)。
但是,如果二叉樹退化成了一條 \(n\) 個節點組成的線性連結串列,則這些操作在最壞情況下的執行時間為 \(O(n)\)。
有些二叉排序樹的變形,其基本操作的效能在最壞情況下依然很好,如平衡樹(AVL)等。但是,它們需要額外的空間來儲存平衡資訊,且實現起來比較複雜。同時,如果訪問模式不均勻,平衡樹的效率就會受到影響,而伸展樹卻可以克服這些問題。
伸展樹(Splay Tree),是對二叉排序樹的一種改進。雖然它並不能保證樹一直是“平衡”的,但對於它的一系列操作,可以證明其每一步操作的“平攤時間”複雜度都是 \(O(\log_2 n)\) 。平攤時間是指在一系列最壞情況的操作序列中單次操作的平均時間。所以,從某種意義上來說,伸展樹也是一種平衡的二叉排序樹。而在各種樹形資料結構中,伸展樹的空間複雜度(不需要記錄用於平衡的冗餘資訊)和程式設計複雜度也都是很優秀的。
獲得較好平攤效率的一種方法就是使用“自調整”的資料結構,與平衡結構或有明確限制的資料結構相比,自調整的資料結構有一下幾個優點:
- 從平攤角度上,它們忽略常數因子,因此絕對不會差於有明確限制的資料結構,而且它們可以根據具體使用情況進行調整,所以在使用模式不均勻的情況下更加有效;
- 由於無需儲存平衡資訊或者其他限制資訊,所以所需的儲存空間更小;
- 它們的查詢和更新的演算法與操作都很簡單,易於實現。
當然,自調整的資料結構也有其潛在的缺點:
- 它們需要更多的區域性調整,尤其在查詢期間,而那些有明確限制的資料結構僅需要在更新期間進行調整,查詢期間則不需要;
- 一系列查詢操作中的某一個可能會耗時較長,這在實時處理的應用程式中可能是一個不足之處。
1. 伸展樹的主要操作
伸展樹是對二叉排序樹的一種改進。與二叉排序樹一樣,伸展樹也具有有序性,即伸展樹中的每一個節點 \(x\) 都滿足:該節點左子樹中的每一個元素都小於 \(x\),而其右子樹中的每一個元素都大於 \(x\)。
但是,與普通二叉排序樹不同的是,伸展樹可以“自我調整”,這就要依靠伸展樹的核心操作 —— \(\text{Splay(x, S)}\)。
1.1 伸展操作
伸展操作 \(\text{Splay(x, S)}\) 是在保持伸展樹有序的前提下,通過一系列旋轉,將伸展樹 \(\text{S}\) 中的元素 \(\text{x}\) 調整至數的根部。在調整的過程中,要分以下三種情況分別處理。
情況一:節點 \(\text{x}\) 的父節點 \(\text{y}\) 是根節點。
此時,
- 若 \(\text{x}\) 是 \(\text{y}\) 的左兒子,則我們進行一次右旋操作 \(\text{Zig(x)}\);
- 若 \(\text{x}\) 是 \(\text{y}\) 的右兒子,則我們進行一次左旋操作 \(\text{Zag(x)}\)。
經過旋轉,使 \(\text{x}\) 成為二叉排序樹 \(S\) 的根節點,且依然滿足二叉排序樹的性質。
\(\text{Zig}\) 操作和 \(\text{Zag}\) 操作如圖所示:
情況二:節點 \(\text{x}\) 的父節點 \(\text{y}\) 不是根節點,且 \(\text{x}\) 和 \(\text{y}\) 同為各自父節點的左兒子,或同為各自父節點的右兒子。
此時,我們設 \(\text{z}\) 為 \(\text{y}\) 的父節點,
- 若 \(\text{x}\) 和 \(\text{y}\) 同時是各自父節點的左兒子,則進行一次 \(\text{Zig-Zig}\) 操作;
- 若 \(\text{x}\) 和 \(\text{y}\) 同時是各自父節點的右兒子,則進行一次 \(\text{Zag-Zag}\) 操作。
如圖所示:
情況三:節點 \(\text{x}\) 的父節點 \(\text{y}\) 不是根節點,且 \(\text{x}\) 和 \(\text{y}\) 中的一個是其父節點的左兒子,另一個是其父節點的右兒子。
此時,我們設 \(\text{z}\) 為 \(\text{y}\) 的父節點,
- 若 \(\text{x}\) 是 \(\text{y}\) 的左兒子,\(\text{y}\) 是 \(\text{z}\) 的右兒子,則進行一次 \(\text{Zig-Zag}\) 操作;
- 若 \(\text{x}\) 是 \(\text{y}\) 的右兒子,\(\text{y}\) 是 \(\text{z}\) 的左二子,則進行一次 \(\text{Zag-Zig}\) 操作。
下面舉一個例子來體會上面的伸展操作。
如下圖所示,最左邊的一個單鏈先執行 \(\text{Splay(1, S)}\),我們將元素 \(1\) 調整到了伸展樹的根部。
執行幾次 \(\text{Splay(1, S)}\) 的效果
然後再執行 \(\text{Splay(2, S)}\),將元素 \(2\) 調整到伸展樹 \(\text{S}\) 的根部。如下圖所示:
執行幾次 \(\text{Splay(2, S)}\) 的效果
1.2 伸展樹的基本操作
利用伸展樹 Splay ,我們可以在伸展樹 \(S\) 上進行如下幾種基本操作。
(1) \(\text{Find(x, S)}\):判斷元素 \(\text{x}\) 是否在伸展樹 \(\text{S}\) 表示的有序集中。
首先,與在二叉排序樹中進行查詢操作操作一樣,在伸展樹中查詢元素 \(\text{x}\)。如果 \(\text{x}\) 在樹中,則再執行 \(\text{Splay(x, S)}\) 調整伸展樹。
(2) \(\text{Insert(x, S)}\):將元素 \(\text{x}\) 插入到伸展樹 \(S\) 表示的有序集中。
首先,與在二叉排序樹中進行插入操作一樣,將 \(\text{x}\) 插入到伸展樹 \(\text{S}\) 中的相應位置,再執行 \(\text{Splay(x, S)}\) 調整伸展樹。
(3) \(\text{Join(S1, S2)}\):將兩棵伸展樹 \(\text{S1}\) 與 \(\text{S2}\) 合併成為一棵伸展樹。其中,\(S1\) 的所有元素都小於 \(S2\) 的所有元素。
首先,找到伸展樹 \(S1\) 中最大的一個元素 \(\text{x}\),再通過 \(\text{Splay(x, S1)}\) 將 \(\text{x}\) 調整到伸展樹 \(S1\) 的根部。然後將 \(S2\) 作為 \(\text{x}\) 節點的右子樹插入,這樣就得到了新的伸展樹 \(S\),如圖所示:
\(\text{Join(S1, S2)}\) 的兩個步驟
(4)\(\text{Delete(x, S)}\):將元素 \(x\) 從伸展樹 \(S\) 所表示的有序集中刪除。
首先,執行 \(\text{Find(x, S)}\) 將 \(\text{x}\) 調整為根節點,然後再對左右子樹執行 \(\text{Join(S1, S2)}\) 操作即可。
(5)\(\text{Split(x, S)}\):以 \(x\) 為界,將伸展樹 \(S\) 分離為兩棵伸展樹 \(S1\) 和 \(S2\),其中,\(S1\) 的所有元素都小於 \(x\),\(S2\) 的所有元素都大於 \(x\)。
首先,執行 \(\text{Find(x, S)}\) 將 \(\text{x}\) 調整為根節點,則 \(\text{x}\) 的左子樹就是 \(\text{S1}\),右子樹就是 \(\text{S2}\)。如圖所示:
除了上述介紹的 \(5\) 種基本操作外,伸展樹還支援求最大值、最小值、求前趨、求後繼等多種操作,這些操作也都是建立在伸展樹操作 \(\text{Splay}\) 的基礎之上的。
2. 伸展樹的演算法實現
注:這裡的程式碼並不是最簡單的程式碼,而是基於上述思想實現的程式碼,更方便我們結合之前分析的內容來理解。
下面給出伸展樹的各種操作的演算法實現,它們都是基於如下伸展樹的型別定義:
int lson[maxn], // 左兒子編號
rson[maxn], // 右兒子編號
p[maxn], // 父節點編號
val[maxn], // 節點權值
sz; // 編號範圍 [1, sz]
struct Splay {
int rt; // 根節點編號
void zag(int x); // 左旋
void zig(int x); // 右旋
void splay(int x); // 伸展操作:將x移到根節點
int func_find(int v); // 查詢是否存在值為v的節點
void func_insert(int v); // 插入
void func_delete(int v); // 刪除
int get_max(); // 求最大值
int get_min(); // 求最小值
int get_pre(int v); // 求前趨
int get_suc(int v); // 求後繼
int join(int rt1, int rt2); // 合併
} tree;
1. 左旋操作
void Splay::zag(int x) {
int y = p[x], z = p[y], a = lson[x];
lson[x] = y; p[y] = x;
rson[y] = a; p[a] = y;
p[x] = z;
if (z) {
if (lson[z] == y) lson[z] = x;
else rson[z] = x;
}
}
Zag(x)操作
2. 右旋操作
void Splay::zig(int x) {
int y = p[x], z = p[y], a = rson[x];
rson[x] = y; p[y] = x;
lson[y] = a; p[a] = y;
p[x] = z;
if (z) {
if (lson[z] == y) lson[z] = x;
else rson[z] = x;
}
}
Zig(x)操作
3. 伸展操作
void Splay::splay(int x) {
while (p[x]) {
int y = p[x], z = p[y];
if (!z) {
if (x == lson[y]) zig(x);
else zag(x);
}
else if (lson[y] == x) {
if (lson[z] == y) { // zig-zig
zig(y);
zig(x);
}
else { // zig-zag
zig(x);
zag(x);
}
}
else { // rson[y] == x
if (lson[z] == y) { // zag-zig
zag(x);
zig(x);
}
else { // zag-zag
zag(y);
zag(x);
}
}
}
rt = x;
}
4. 查詢
int Splay::func_find(int v) {
int x = rt;
while (x) {
if (val[x] == v) {
rt = x;
splay(x);
return x;
}
else if (v < val[x]) x = lson[x];
else x = rson[x];
}
return 0; // 返回0說明沒找到
}
5. 插入
void Splay::func_insert(int v) {
val[++sz] = v;
if (rt == 0) {
rt = sz;
return;
}
int x = rt;
while (true) {
if (v < val[x]) {
if (lson[x]) x = lson[x];
else {
lson[x] = sz;
p[sz] = x;
break;
}
}
else {
if (rson[x]) x = rson[x];
else {
rson[x] = sz;
p[sz] = x;
break;
}
}
}
splay(rt = sz);
}
6. 刪除(會用到下面定義的join操作)
void Splay::func_delete(int v) {
int x = func_find(v);
if (!x) return;
int ls = lson[x], rs = rson[x];
lson[x] = rson[x] = 0;
p[ls] = p[rs] = 0;
rt = join(ls, rs);
}
7. 求最大值
int Splay::get_max() {
if (!rt) return 0;
int x = rt;
while (rson[x]) x = rson[x];
splay(rt = x);
return x;
}
8. 求最小值
int Splay::get_min() {
if (!rt) return 0;
int x = rt;
while (lson[x]) x = lson[x];
splay(rt = x);
return x;
}
9. 求前趨
int Splay::get_pre(int v) {
if (!rt) return 0;
int x = rt, ans = 0;
while (true) {
if (val[x] <= v) {
if (!ans || val[ans] < val[x]) ans = x;
if (rson[x]) x = rson[x];
else break;
}
else {
if (lson[x]) x = lson[x];
else break;
}
}
if (ans) splay(rt = ans);
return ans;
}
10. 求後繼
int Splay::get_suc(int v) {
if (!rt) return 0;
int x = rt, ans = 0;
while (true) {
if (val[x] >= v) {
if (!ans || val[ans] > val[x]) ans = x;
if (lson[x]) x = lson[x];
else break;
}
else {
if (rson[x]) x = rson[x];
else break;
}
}
if (ans) splay(rt = ans);
return ans;
}
11. 合併
int Splay::join(int rt1, int rt2) {
if (!rt1) return rt2;
if (!rt2) return rt1;
Splay tree1;
tree1.rt = rt1;
rt1 = tree1.get_max();
assert(rson[rt1] == 0);
rson[rt1] = rt2;
p[rt2] = rt1;
return rt1;
}
示例程式碼(對應題目:《怪物倉庫管理員(二)》):
#include <bits/stdc++.h>
using namespace std;
const int maxn = 500050;
int lson[maxn], // 左兒子編號
rson[maxn], // 右兒子編號
p[maxn], // 父節點編號
val[maxn], // 節點權值
sz; // 編號範圍 [1, sz]
struct Splay {
int rt; // 根節點編號
void zag(int x); // 左旋
void zig(int x); // 右旋
void splay(int x); // 伸展操作:將x移到根節點
int func_find(int v); // 查詢是否存在值為v的節點
void func_insert(int v); // 插入
void func_delete(int v); // 刪除
int get_max(); // 求最大值
int get_min(); // 求最小值
int get_pre(int v); // 求前趨
int get_suc(int v); // 求後繼
int join(int rt1, int rt2); // 合併
} tree;
/**
zag(int x) 左旋
*/
void Splay::zag(int x) {
int y = p[x], z = p[y], a = lson[x];
lson[x] = y; p[y] = x;
rson[y] = a; p[a] = y;
p[x] = z;
if (z) {
if (lson[z] == y) lson[z] = x;
else rson[z] = x;
}
}
/**
zig(int x) 右旋
*/
void Splay::zig(int x) {
int y = p[x], z = p[y], a = rson[x];
rson[x] = y; p[y] = x;
lson[y] = a; p[a] = y;
p[x] = z;
if (z) {
if (lson[z] == y) lson[z] = x;
else rson[z] = x;
}
}
/**
splay(int x) 伸展操作
*/
void Splay::splay(int x) {
while (p[x]) {
int y = p[x], z = p[y];
if (!z) {
if (x == lson[y]) zig(x);
else zag(x);
}
else if (lson[y] == x) {
if (lson[z] == y) { // zig-zig
zig(y);
zig(x);
}
else { // zig-zag
zig(x);
zag(x);
}
}
else { // rson[y] == x
if (lson[z] == y) { // zag-zig
zag(x);
zig(x);
}
else { // zag-zag
zag(y);
zag(x);
}
}
}
rt = x;
}
int Splay::func_find(int v) {
int x = rt;
while (x) {
if (val[x] == v) {
rt = x;
splay(x);
return x;
}
else if (v < val[x]) x = lson[x];
else x = rson[x];
}
return 0; // 返回0說明沒找到
}
void Splay::func_insert(int v) {
val[++sz] = v;
if (rt == 0) {
rt = sz;
return;
}
int x = rt;
while (true) {
if (v < val[x]) {
if (lson[x]) x = lson[x];
else {
lson[x] = sz;
p[sz] = x;
break;
}
}
else {
if (rson[x]) x = rson[x];
else {
rson[x] = sz;
p[sz] = x;
break;
}
}
}
splay(rt = sz);
}
void Splay::func_delete(int v) {
int x = func_find(v);
if (!x) return;
int ls = lson[x], rs = rson[x];
lson[x] = rson[x] = 0;
p[ls] = p[rs] = 0;
rt = join(ls, rs);
}
int Splay::get_max() {
if (!rt) return 0;
int x = rt;
while (rson[x]) x = rson[x];
splay(rt = x);
return x;
}
int Splay::get_min() {
if (!rt) return 0;
int x = rt;
while (lson[x]) x = lson[x];
splay(rt = x);
return x;
}
int Splay::get_pre(int v) {
if (!rt) return 0;
int x = rt, ans = 0;
while (true) {
if (val[x] <= v) {
if (!ans || val[ans] < val[x]) ans = x;
if (rson[x]) x = rson[x];
else break;
}
else {
if (lson[x]) x = lson[x];
else break;
}
}
if (ans) splay(rt = ans);
return ans;
}
int Splay::get_suc(int v) {
if (!rt) return 0;
int x = rt, ans = 0;
while (true) {
if (val[x] >= v) {
if (!ans || val[ans] > val[x]) ans = x;
if (lson[x]) x = lson[x];
else break;
}
else {
if (rson[x]) x = rson[x];
else break;
}
}
if (ans) splay(rt = ans);
return ans;
}
int Splay::join(int rt1, int rt2) {
if (!rt1) return rt2;
if (!rt2) return rt1;
Splay tree1;
tree1.rt = rt1;
rt1 = tree1.get_max();
assert(rson[rt1] == 0);
rson[rt1] = rt2;
p[rt2] = rt1;
return rt1;
}
int n, op, x;
int main() {
cin >> n;
while (n --) {
cin >> op;
if (op != 3 && op != 4) cin >> x;
if (op == 1) tree.func_insert(x);
else if (op == 2) tree.func_delete(x);
else if (op == 3) cout << val[tree.get_min()] << endl;
else if (op == 4) cout << val[tree.get_max()] << endl;
else if (op == 5) cout << val[tree.get_pre(x)] << endl;
else cout << val[tree.get_suc(x)] << endl;
}
return 0;
}