题意:给定一个降序的正数数组,要求按【最小、最大、次小、次大…】的顺序重新排序。期望的时间复杂度为O(n),空间复杂度为O(1),即不能申请额外数组。例如:输入【7,6,5,4,3,2,1】输出【1,7,2,6,3,5,4】
分析:
首先,计算每个元素要挪到哪里感觉很简单,目测是这样:
1. 旧位置i < 2/n:新位置j=2 * i +1
2. 旧位置i >= 2/n: 新位置j = 2 * (n - 1 - i)
然后就是沿链轮换了。本例中 a[0]=7应该被移到a[1], a[1]本来的数6应该被移到a[3], a[3]本来的数4应该被移到a[6]... 最后发现,把input数组变成output数组, 需要三组轮换:
1. a[0]->a[1]->a[3]->a[6]->a[0]
2. a[2]->a[5]-a[2]
3. a[4]位置不变
完成每个轮换只需要一个额外的变量,腾出一个数组位置。因此空间复杂度O(1)。每个元素只会被挪动一次,时间复杂度O(n)。
此外还需要标记哪些元素已经被移动过了。通常另开一个数组标记会比较方便。。但是这题不允许,所以可以把每个移动过的元素都都取负作为标记,之后再还原。(也可以用其他的标记方式)
这也对应一个群论中的结论:置换可以分解成若干个不相交的轮换。
代码
#include <stdio.h> int trans(int len, int i) { if(len % 2 == 0) { if(i < len / 2) return i * 2 + 1; else return (len - i - 1) * 2; } else { if(i < len / 2) return i * 2 + 1; else if(i > len / 2) return (len - i - 1) * 2; else return len - 1; } } void swap(int *a, int *b) { *a ^= *b; *b ^= *a; *a ^= *b; } int main(void) { int arr[] = {7, 6, 5, 4, 3, 2, 1}; //start int len = sizeof(arr) / sizeof(*arr); for(int i = 0; i < len; i++) { if(arr[i] > 0) { int j = i, tmp = arr[i]; while(arr[j = trans(len, j)] > 0) { swap(&tmp, &arr[j]); // 每次与前一个交换 arr[j] = 0 - arr[j]; } } } for(int i = 0; i < len; i++) { arr[i] = 0 - arr[i]; } //print for(int i = 0; i < len; i++) { printf("%d ", arr[i]); } }
这个问题其实是一个经典问题,
完美洗牌问题:
玩过扑克牌的朋友都知道,在一局完了之后洗牌,洗牌人会习惯性的把整副牌大致分为两半,两手各拿一半对着对着交叉洗牌。
2004年,microsoft 的 Peiyush Jain 在他发表一篇名为:“A Simple In-Place Algorithm for In-Shuffle” 的论文中提出了完美洗牌算法。
什么是完美洗牌问题呢?即给定一个数组
a1,a2,a3, …, an, b1, b2, b3, ..., bn
最终把它置换成
b1, a1, b2, a2, a3, b3,…, bn, an
这个完美洗牌问题本质上与本题完全一致的,可以在O(n)内改成形式相同。
参考链接:
1. https://www.zhihu.com/question/50512830/answer/121386043
2. https://www.zhihu.com/question/50512830/answer/121334031
3. https://www.jianshu.com/p/9c841ad88ded