红黑树的插入和遍历时间复杂度分析
在平常的工作中,最常用的一种数据结构恐怕是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;
}