http://www.csie.ntnu.edu.tw/~u91029/Numbers.html#a1
Binary Search Tree (二元搜尋樹)
置放大量數字並進行排序的資料結構。原理是 Divide and Conquer ,把所有數字分成一個與兩半,樹根在中間,左子樹較小,右子樹較大,然後遞迴分割下去。
插入、刪除、搜尋的時間複雜度等同於二元搜尋樹的高度。資料可以動態增加和減少,二元搜尋樹的高度亦會變動,因此時間複雜度最差為 O(N) ,最佳為 O(logN) 。所有節點連成一線的時候是最差的,所有節點形成 full tree 是最佳的。
空間複雜度等同於節點數目,空間複雜度是 O(N) 。
尋找極小值、極大值,從樹根開始往左小孩走到底、往右小孩走到底就可以了。時間複雜度為 O(logN) 。
樹葉可以額外建立線索( Thread ),如此就能依照大小順序走訪元素。建立線索不影響時間複雜度與空間複雜度。
可以直接使用 STL 的 set 、 map 。
AVL Tree (平衡二元搜尋樹)
二元搜尋樹可以進行改良,令每個葉子的高度落差不會太多,讓各項操作的運行速度很穩定均勻,不會出現忽快忽慢的極端現象。限制樹上所有節點,其左右兩子樹的高度差不會超過一,此舉造成整棵樹的高度為 O(logN) 。每當插入、刪除,高度差超過一,就馬上在高度落差太大的子樹上面,對子樹的根使用右旋轉或左旋轉,把左右子樹的高度差維持在一以下。
插入、刪除、搜尋的時間複雜度為 O(logN) 。旋轉的時間複雜度是 O(1) ,最多旋轉兩次就會平衡。至於空間複雜度仍是 O(N) 。
最佳二元搜尋樹( Optimal Binary Search Tree )
如果二元搜尋樹的資料不會變動,則可以依照每個節點被搜尋到的頻率,使用 Dynamic Programming 求得結構最佳的二元搜尋樹,藉此加快搜尋速度。建立時間為 O(N^2) 。
UVa 10304
排名( Ranking )
二元搜尋樹雖然有排序的功效,但是卻沒有排名的功效。如果想知道資料的排名,那麼就要在每個節點新增一個變數,記錄其子樹的節點個數,如此便可以用 Divide and Conquer 的方式求得每筆資料的排名。不影響時間複雜度與空間複雜度。
UVa 10909
以陣列表示二元搜尋樹
利用陣列索引值定位到樹的節點。優點是實作輕鬆,結構簡單,效率高。缺點是當二元搜尋樹沒有形成 full tree 的時候,會有很多陣列空間沒有使用到,浪費記憶體,甚至記憶體會不敷使用。
int left_child(int index) {return index * 2;}
int right_child(int index) {return index * 2 + 1;}
void binary_tree()
{
int tree[5 + 1]; // array[0]不使用,只有五個節點。
cout << "根為" << tree[1];
cout << "根的左邊小孩是" << tree[ left_child(1) ];
cout << "根的右邊小孩是" << tree[ right_child(1) ];
}
實作
struct Node
{
Node* l, *r;
int h, c, key;
Node(int key): l(0), r(0), h(1), c(1), key(key) {}
void update()
{
h = 1 + max(l?l->h:0, r?r->h:0);
c = (l?l->c:0) + 1 + (r?r->c:0);
}
};
Node* rL(Node* x) // 左旋轉
{
Node* y = x->r;
x->r = y->l; y->l = x;
x->update(); y->update();
return y;
}
Node* rR(Node* x) // 右旋轉
{
Node* y = x->l;
x->l = y->r; y->r = x;
x->update(); y->update();
return y;
}
Node* balance(Node* x)
{
x->update();
if (x->l->h > 1 + x->r->h)
{
if (x->l->l->h < x->l->r->h) x->l = rL(x->l);
x = rR(x);
}
else if (x->r->h > 1 + x->l->h)
{
if (x->r->r->h < x->r->l->h) x->r = rR(x->r);
x = rL(x);
}
return x;
}
Node* insert(Node* x, int key)
{
if (x == 0) return new Node(key);
if (key < x->key)
x->l = insert(x->l, key);
else
x->r = insert(x->r, key);
return balance(x);
}
Red-Black Tree
紅黑樹的功效等同平衡二元搜尋樹,但是效率更勝一籌。
Skip Lists
置放大量數字並進行排序的資料結構。不用樹狀結構,而改用高度不同的 Linked List 來連接資料。資料結構在概念上可以表示成 Left Child-Right Sibling Binary Tree 的模式。時間複雜度與空間複雜度與 Binary Search Tree 皆相同,但是實際運作的效率比 Binary Search Tree 還要好。
Heap
程度★ 難度★★★
Binary Heap
常簡稱 Heap ,置放大量數字,並且可以隨時求最大(小)值的資料結構。插入、刪除、求極值的時間複雜度為 O(logN) ,資料可以動態增加和減少。
可以直接使用 STL 的 priority_queue 。
UVa 501 10587
Binomial Heap
置放大量數字,並且可以隨時求最大(小)值的資料結構。遞迴結合多個高度不同的 Binomial Tree ,所構成的一個資料結構,以抽象化的角度來看像是一個二進位系統。兩個 Binomial Heap 在結合的時候,原理就像是在做二進位加法一樣,因而得此名。
Fibonacci Heap
置放大量數字,並且可以隨時求最大(小)值的資料結構。一個規則極其詭異的結構,重點在於他有一個特殊功能叫做 decrease key ,可以搜尋數字,並且還可以減少該數字,時間複雜度均攤之後僅有 O(1) 。另外,插入的時間複雜度均攤之後僅有 O(1) 。
Quake Heap
置放大量數字,並且可以隨時求最大(小)值的資料結構。與 Fibonacci Heap 功能相同,但是據說簡單很多。
http://www.cs.uwaterloo.ca/~tmchan/pub.html
Binary Search Tree
BST 也可以當作 Heap 使用。
Heap::push = BST::insert
Heap::pop = BST::get_extremum + BST::delete
Heap::peek = BST::get_extremum
Heap::merge = BST::merge
Heap 的每一項操作,都可以用 BST 兜出來,時間複雜度完全一樣,唯一的例外是: Heap 可以預先偷看將要彈出來的元素,僅需 O(1) 時間; BST 要偷看,就等同於需要花費 O(logN) 時間搜尋最小值或最大值。
Treap
Binary Search Tree 與 Binary Heap 進行合體術後變成的東西,令數字有額外的優先次序,同時具有優先次序的排序功能與求最大(小)值的功能。
van Emde Boas Tree ( vEB Tree )
置放大量正整數(與零)的資料結構,並且可以作為 Double Ended Priority Queue 。利用 Divide and Conquer ,將 0 到 K-1 的整數分為 sqrt(K) 個區塊,每個區塊的範圍大小為 sqrt(K) ,接著各區塊各自遞迴下去。
以另一個角度來看,就是把一個整數從中間切開,成為高位數字部份和低位數字部份,把高位數字抽取出來,作為索引值,找出對應的陣列格子,並遞迴下去以儲存低位數字。跟 Trie 有點像。
每次搜尋、插入、刪除為 O(loglogK) ,總時間複雜度為 O(NloglogK) ,其中 N 為正整數個數, K 為這些正整數的最大值。
速度是快了那麼一點,然而缺點是記憶體用很兇,空間複雜度為 O(K) 。【待補分析】
其實我們也可以使用 Counting Sort 來紀錄正整數,速度還比 vEB Tree 快,只不過 Counting Sort 不能動態排序罷了。
struct vEB_Tree // [0, K-1] have m digits in binary
{
int digit; // store m。或者紀錄陣列有多大(遞迴有多深)。
vEB_Tree* lower[possibility of m/2 digits];
};
若要作為 Double Ended Priority Queue ,則在每個節點加上兩個變數,紀錄該子樹目前擁有的數字的大小範圍。
struct vEB_Tree
{
int digit;
vEB_Tree* lower[possibility of m/2 digits];
int min, max; // 子樹目前擁有的數字們,最大值與最小值。
};
Search Tree
程度★ 難度★★★Binary Search Tree (二元搜尋樹)
置放大量數字並進行排序的資料結構。原理是 Divide and Conquer ,把所有數字分成一個與兩半,樹根在中間,左子樹較小,右子樹較大,然後遞迴分割下去。
插入、刪除、搜尋的時間複雜度等同於二元搜尋樹的高度。資料可以動態增加和減少,二元搜尋樹的高度亦會變動,因此時間複雜度最差為 O(N) ,最佳為 O(logN) 。所有節點連成一線的時候是最差的,所有節點形成 full tree 是最佳的。
空間複雜度等同於節點數目,空間複雜度是 O(N) 。
尋找極小值、極大值,從樹根開始往左小孩走到底、往右小孩走到底就可以了。時間複雜度為 O(logN) 。
樹葉可以額外建立線索( Thread ),如此就能依照大小順序走訪元素。建立線索不影響時間複雜度與空間複雜度。
可以直接使用 STL 的 set 、 map 。
AVL Tree (平衡二元搜尋樹)
二元搜尋樹可以進行改良,令每個葉子的高度落差不會太多,讓各項操作的運行速度很穩定均勻,不會出現忽快忽慢的極端現象。限制樹上所有節點,其左右兩子樹的高度差不會超過一,此舉造成整棵樹的高度為 O(logN) 。每當插入、刪除,高度差超過一,就馬上在高度落差太大的子樹上面,對子樹的根使用右旋轉或左旋轉,把左右子樹的高度差維持在一以下。
插入、刪除、搜尋的時間複雜度為 O(logN) 。旋轉的時間複雜度是 O(1) ,最多旋轉兩次就會平衡。至於空間複雜度仍是 O(N) 。
最佳二元搜尋樹( Optimal Binary Search Tree )
如果二元搜尋樹的資料不會變動,則可以依照每個節點被搜尋到的頻率,使用 Dynamic Programming 求得結構最佳的二元搜尋樹,藉此加快搜尋速度。建立時間為 O(N^2) 。
UVa 10304
排名( Ranking )
二元搜尋樹雖然有排序的功效,但是卻沒有排名的功效。如果想知道資料的排名,那麼就要在每個節點新增一個變數,記錄其子樹的節點個數,如此便可以用 Divide and Conquer 的方式求得每筆資料的排名。不影響時間複雜度與空間複雜度。
UVa 10909
以陣列表示二元搜尋樹
利用陣列索引值定位到樹的節點。優點是實作輕鬆,結構簡單,效率高。缺點是當二元搜尋樹沒有形成 full tree 的時候,會有很多陣列空間沒有使用到,浪費記憶體,甚至記憶體會不敷使用。
int left_child(int index) {return index * 2;}
int right_child(int index) {return index * 2 + 1;}
void binary_tree()
{
int tree[5 + 1]; // array[0]不使用,只有五個節點。
cout << "根為" << tree[1];
cout << "根的左邊小孩是" << tree[ left_child(1) ];
cout << "根的右邊小孩是" << tree[ right_child(1) ];
}
實作
struct Node
{
Node* l, *r;
int h, c, key;
Node(int key): l(0), r(0), h(1), c(1), key(key) {}
void update()
{
h = 1 + max(l?l->h:0, r?r->h:0);
c = (l?l->c:0) + 1 + (r?r->c:0);
}
};
Node* rL(Node* x) // 左旋轉
{
Node* y = x->r;
x->r = y->l; y->l = x;
x->update(); y->update();
return y;
}
Node* rR(Node* x) // 右旋轉
{
Node* y = x->l;
x->l = y->r; y->r = x;
x->update(); y->update();
return y;
}
Node* balance(Node* x)
{
x->update();
if (x->l->h > 1 + x->r->h)
{
if (x->l->l->h < x->l->r->h) x->l = rL(x->l);
x = rR(x);
}
else if (x->r->h > 1 + x->l->h)
{
if (x->r->r->h < x->r->l->h) x->r = rR(x->r);
x = rL(x);
}
return x;
}
Node* insert(Node* x, int key)
{
if (x == 0) return new Node(key);
if (key < x->key)
x->l = insert(x->l, key);
else
x->r = insert(x->r, key);
return balance(x);
}
Red-Black Tree
紅黑樹的功效等同平衡二元搜尋樹,但是效率更勝一籌。
Skip Lists
置放大量數字並進行排序的資料結構。不用樹狀結構,而改用高度不同的 Linked List 來連接資料。資料結構在概念上可以表示成 Left Child-Right Sibling Binary Tree 的模式。時間複雜度與空間複雜度與 Binary Search Tree 皆相同,但是實際運作的效率比 Binary Search Tree 還要好。
Heap
程度★ 難度★★★
Binary Heap
常簡稱 Heap ,置放大量數字,並且可以隨時求最大(小)值的資料結構。插入、刪除、求極值的時間複雜度為 O(logN) ,資料可以動態增加和減少。
可以直接使用 STL 的 priority_queue 。
UVa 501 10587
Binomial Heap
置放大量數字,並且可以隨時求最大(小)值的資料結構。遞迴結合多個高度不同的 Binomial Tree ,所構成的一個資料結構,以抽象化的角度來看像是一個二進位系統。兩個 Binomial Heap 在結合的時候,原理就像是在做二進位加法一樣,因而得此名。
Fibonacci Heap
置放大量數字,並且可以隨時求最大(小)值的資料結構。一個規則極其詭異的結構,重點在於他有一個特殊功能叫做 decrease key ,可以搜尋數字,並且還可以減少該數字,時間複雜度均攤之後僅有 O(1) 。另外,插入的時間複雜度均攤之後僅有 O(1) 。
Quake Heap
置放大量數字,並且可以隨時求最大(小)值的資料結構。與 Fibonacci Heap 功能相同,但是據說簡單很多。
http://www.cs.uwaterloo.ca/~tmchan/pub.html
Binary Search Tree
BST 也可以當作 Heap 使用。
Heap::push = BST::insert
Heap::pop = BST::get_extremum + BST::delete
Heap::peek = BST::get_extremum
Heap::merge = BST::merge
Heap 的每一項操作,都可以用 BST 兜出來,時間複雜度完全一樣,唯一的例外是: Heap 可以預先偷看將要彈出來的元素,僅需 O(1) 時間; BST 要偷看,就等同於需要花費 O(logN) 時間搜尋最小值或最大值。
Treap
Binary Search Tree 與 Binary Heap 進行合體術後變成的東西,令數字有額外的優先次序,同時具有優先次序的排序功能與求最大(小)值的功能。
van Emde Boas Tree ( vEB Tree )
置放大量正整數(與零)的資料結構,並且可以作為 Double Ended Priority Queue 。利用 Divide and Conquer ,將 0 到 K-1 的整數分為 sqrt(K) 個區塊,每個區塊的範圍大小為 sqrt(K) ,接著各區塊各自遞迴下去。
以另一個角度來看,就是把一個整數從中間切開,成為高位數字部份和低位數字部份,把高位數字抽取出來,作為索引值,找出對應的陣列格子,並遞迴下去以儲存低位數字。跟 Trie 有點像。
每次搜尋、插入、刪除為 O(loglogK) ,總時間複雜度為 O(NloglogK) ,其中 N 為正整數個數, K 為這些正整數的最大值。
速度是快了那麼一點,然而缺點是記憶體用很兇,空間複雜度為 O(K) 。【待補分析】
其實我們也可以使用 Counting Sort 來紀錄正整數,速度還比 vEB Tree 快,只不過 Counting Sort 不能動態排序罷了。
struct vEB_Tree // [0, K-1] have m digits in binary
{
int digit; // store m。或者紀錄陣列有多大(遞迴有多深)。
vEB_Tree* lower[possibility of m/2 digits];
};
若要作為 Double Ended Priority Queue ,則在每個節點加上兩個變數,紀錄該子樹目前擁有的數字的大小範圍。
struct vEB_Tree
{
int digit;
vEB_Tree* lower[possibility of m/2 digits];
int min, max; // 子樹目前擁有的數字們,最大值與最小值。
};