紅黑樹的插入和遍歷時間複雜度分析
在平常的工作中,最常用的一種資料結構恐怕是std::map了。因此對其的時間複雜度分析是有必要的,編寫程式時做到心中有底。
一、理論分析
在stl中std::map和std::set都採用紅黑樹的方式實現。我們知道插入一個元素到紅黑樹的時間為log(N),其中N為當前紅黑樹的元素個數,因此,採用插入方式構建元素個數為N的紅黑樹的時間複雜度為:
log(1) + log(2) + log(N-1) = log((N-1)!) = Nlog(N)
那麼採用迭代器遍歷一棵紅黑樹的時間複雜度是多少呢? 是O(N)。 也就是說非遞迴遍歷一棵紅黑樹的時間複雜度和遍歷陣列的時間複雜度是一樣的,多麼令人驚奇的結果。
我們將分析得出這一結果。採用迭代器遍歷紅黑樹的演算法主要在迭代器增1操作:
1. 判斷右子樹是不是空,如果不為空,找到右子樹的最小值begin(right(tree)),結束。如果右子樹為空,如果右子樹為空,轉2;
2. 往根節點爬,直到父節點為空或者本節點是父節點的左子節點,然後取父節點的值。
我們將證明紅黑樹的一條邊最多被訪問兩次:一條邊最多隻能被從父節點到子節點訪問一次和從子節點到父節點訪問一次。如果有第三次訪問,注意到我們的遍歷過程是完全無狀態的(步驟1和2判斷的唯一是根據當前節點,沒有任何其餘狀態變數)。那麼必然會導致至少一個訪問的重複,與現實矛盾。證明出一條邊最多被訪問兩次。另外一條邊最小要被訪問一次,原因是很顯然的。因此二叉樹的遍歷是O(E)的,其中E為樹的邊數,我們知道一個節點的節點數和邊數的關係為N = E + 1,故得出迭代器遍歷一棵紅黑樹的時間複雜度是O(N)。
二、實驗證明
空口無憑,下面採用程式測試理論是否和實際相符。採用std::set<int>做為實驗物件,對其分別插入和遍歷10000、100000、1000000和10000000次,得到的時間消耗如下表:
單位/微秒
插入 |
遍歷 |
|
10000次 |
9070 |
111 |
100000次 |
611655 |
2641 |
1000000次 |
1575464 |
26836 |
10000000次 |
12621089 |
251810 |
從遍歷的時間消耗很容易看出遍歷是線性時間的,並且對於比較小的遍歷次數,遍歷消耗的時間還會減小。
但插入的時間消耗甚至小於線性時間(亞線性?)這可能與插入的資料有關吧,插入的資料是從0開始增1的,結果還有待分析。
附錄:
測試程式環境
系統:Windows 7。
開發工具:VS208,Release編譯。
程式:
#include <set>
#include <boost/chrono.hpp>
#include <iostream>
void test(const int N)
{
std::cout << "N = " << N << std::endl;
std::set<int> si;
boost::chrono::high_resolution_clock::time_point t1 = boost::chrono::high_resolution_clock::now();
for (int i = 0; i < N; i++)
{
si.insert(i);
}
boost::chrono::high_resolution_clock::time_point t2 = boost::chrono::high_resolution_clock::now();
for (std::set<int>::iterator i = si.begin(); i != si.end(); ++i)
{
volatile int j = *i;
}
boost::chrono::high_resolution_clock::time_point t3 = boost::chrono::high_resolution_clock::now();
std::cout << "insert time elapse " << boost::chrono::duration_cast<boost::chrono::microseconds>(t2 - t1) << std::endl;
std::cout << "traverse time elapse " << boost::chrono::duration_cast<boost::chrono::microseconds>(t3 - t2) << std::endl;
}
int _tmain(int argc, _TCHAR* argv[])
{
test(10000);
test(100000);
test(1000000);
test(10000000);
return 0;
}