原地排序与链表翻转
有个长度为 n 的 (key,val) 数组 a,其中 key 是 int 类型,并且其值在 [0, n) 之间(前闭后开,包括 0 不包括 n),该数组按 key 是乱序的,但没有重复 key。
题目要求:对 a 原地按 key 排序,可以使用常数个临时变量,不允许使用其它额外空间,时间复杂度必须是O(n)
我曾经写过另外一个问题:排列的分解(必须看),与此有点类似:但其实不同。
本质的不同在于:那个问题是对循环链表逆时针旋转1个单位,而该问题是要对循环链表顺时针旋转1个单位。逆时针旋转很容易,因为是 forward copy。但是顺时针旋转需要 backward copy,对数组(更 general 一点,是 bidirectional iterator)的 backward copy 很容易,但是对单链表的 backward copy,就比较困难了。
为了解决 backward copy 的问题,我们可以把这个循环链表先进行翻转,然后再 forward copy,思路有了,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
#include <stdio.h> #include <algorithm> #include <vector> struct A { int key; // must be one kind of integer type int val; // should be any type, use int for demo }; void reorder(A* a, int n) { for (int i = 0; i < n; ++i) { if (a[i].key == i) continue; int j = i; int k = a[i].key; do { // reverse the list int next = a[k].key; a[k].key = j; j = k; k = next; } while (k != i); a[i].key = j; int tmp = a[i].val; do { // reorder: move to right place a[k].key = k; a[k].val = a[j].val; k = j; j = a[j].key; } while (j != i); a[k].key = k; a[k].val = tmp; } } int main() { int n = 20; std::vector<A> a(n); for (int i = 0; i < n; ++i) a[i].key = a[i].val = i; std::random_shuffle(a.begin(), a.end()); for (int i = 0; i < n; ++i) printf("%d %dn", a[i].key, a[i].val); printf("----------------n"); reorder(&a[0], (int)a.size()); for (int i = 0; i < n; ++i) printf("%d %dn", a[i].key, a[i].val); return 0; } |
- 如果 val 类型的拷贝代价很低(比如象上面demo中的int),可以也可以不用翻转链表,多拷贝两次即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void reorder(A* a, int n) { for (int i = 0; i < n; ++i) { if (a[i].key == i) continue; int j = i; int k = a[i].key; int t = a[i].val; do { int t2 = a[k].val; a[j].key = j; a[k].val = t; j = k; t = t2; k = a[k].key; } while (k != i); a[j].key = j; a[i].val = t; } } |
在最坏情况下,这个版本对 val 的拷贝次数是上个版本的3倍,但是因为避免了翻转链表,也就减少了多一个循环的开销,这样的循环开销也许会造成大量的cpu cache 失效…… 各有利弊