求逆序数的三种数据结构比较

来源:互联网 发布:五金小助手软件 编辑:程序博客网 时间:2024/05/17 22:34
本文比较 树状数组,线段树,还有一种unnamed的树状结构,在求逆序数中的运行效率。

给定数组a,如果有i, j, i < j且a[i] > a[j],我们称之为一个逆序(反序),求逆序数即找出这样的i,j对的数目。如果通过交换相邻元素来对数组排序,那么逆序数正好是最少的交换次数。长度为n的排列,其逆序数的期望是n(n-1)/4,方差是n(2n+5)(n-1)/72。逆序数的平均数可以大概视为n^2/4,标准差大概视为(1/6)n^(3/2)。

经典的求逆序数的算法可以由归并排序给出,但是在实现细节上可能会出错。而使用辅助数据结构则使得实现难度降低。

最简单的是用线段树:从左往右扫描,每扫描到一个数时,则通过线段树求出当前线段树上大于当前元素的元素的个数。然后将当前元素加到线段树中。

在这里,上述算法也可以用树状数组(binary index tree)来实现。在添加元素时,从大到小,可以理解为从最小元素到当前元素添加了一根线段。在查询时,从小到大累加,可以理解为求当前元素被多少个线段覆盖,也就是比当前元素大的元素的数量。

还有一种树状结构,对于[l, r]的一个区间,令mid=l+r>>1用tree[mid]来维护[mid, r]上的元素个数。
插入时,如果需要插入的元素正好是mid,则计数加1,算法结束。
如果在[mid+1, r]上,则计数加1,同时递归到右区间:l = mid + 1。
如果在[l, mid-1]上,则tree[mid]不变,同时递归到左区间上:r = mid - 1;

对于查询基本上和插入一样。


为了简单对运行效率,生成了[1, 10000]的随机数一千万个(这样的测试方法不一定能反应出效率,我只是偷懒,只简单设计这样的方法)。为了方便实现,求的是a[i] >= a[j]的pair数,否则,很多细节需要修改,而修改后,可能对于某些结构,多了一些判断,会影响效率。

对于线段树的实现,分别使用了递归和非递归,其中queryB是递归的,queryBext是非递归的。可以看出unnamed的树状结构和线段树有某些联系。
#include <cstdio>#include <iostream>#include <cstring>#include <cassert>#include <ctime>using namespace std;typedef long long int64;const int maxn = 10001;int tree[maxn];int tree1[maxn];int tree2[maxn*4];int64 sa, sb, sc;static inline void insertA(int x){for (; x; x -= x & -x) ++tree[x];}static inline int queryA(int x){int r = 0;for (; x < maxn; x += x & -x) r += tree[x];return r;}static inline int queryB(int root, int l, int r, int x){if (l == r) return tree2[root]++;int mid = l + r >> 1, lc = root << 1, rc = lc + 1;++tree2[root];if (x <= mid) return tree2[rc] + queryB(lc, l, mid, x);else return queryB(rc, mid+1, r, x);}static inline int queryBext(int root, int l, int r, int x){int ret = 0;while (l <= r){if (l == r) return ret + tree2[root]++;int mid = l + r >> 1, lc = root << 1, rc = lc + 1;++tree2[root];if (x <= mid){ret += tree2[rc];r = mid;root = lc;}else{l = mid + 1;root = rc;}}}void calA(){srand(123);fill(tree, tree+maxn, 0);int start = clock();for (int i = 0; i < 10000000; ++i){int u = rand() % 10000 + 1;int a = queryA(u);insertA(u);sa += a;}int use = clock() - start;cerr << use << endl;}void calB(){srand(123);fill(tree2, tree2+maxn*4, 0);int start = clock();for (int i = 0; i < 10000000; ++i){int u = rand() % 10000 + 1;int a = queryB(1, 1, 10000, u);sb += a;}int use = clock() - start;cerr << use << endl;}static inline int query_and_insert(int x){int l = 1, r = 10000;int ret = 0;while (l <= r){int mid = l + r >> 1;if (x == mid){ret += tree1[x]++;break;}else if (x > mid){++tree1[mid];l = mid + 1;}else{ret += tree1[mid];r = mid - 1;}}return ret;}void calC(){srand(123);fill(tree1, tree1+maxn, 0);int start = clock();for (int i = 0; i < 10000000; ++i){int u = rand() % 10000 + 1;sc += query_and_insert(u);}int use = clock() - start;cerr << use << endl;}int main(){calA();calB();calC();cerr << sa << " " << sb << " " << " " << sc << endl;return 0;}


在我的计算机上,使用非递归的线段树的结果是:
575
906
842
25006976835482 25006976835482  25006976835482
使用递归的线段树的结果是:
553
939
824
25006976835482 25006976835482  25006976835482


机器和编译器:
win7 64bit, i7 860, gcc version 4.8.1 (tdm64-2), 编译选项-O2
0 0