原地排序与链表翻转

有这样一个问题:

有个长度为 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,思路有了,代码如下:

#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 %d\n", a[i].key, a[i].val);
	printf("----------------\n");
	reorder(&a[0], (int)a.size());
	for (int i = 0; i < n; ++i)
		printf("%d %d\n", a[i].key, a[i].val);
	return 0;
}

  • 如果 val 类型的拷贝代价很低(比如象上面demo中的int),可以也可以不用翻转链表,多拷贝两次即可:
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 失效…… 各有利弊

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

Terark-CTO-雷鹏

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值