完美洗牌问题
1.问题引入
给定一个长度为偶数的数组arr,长度记为2*N。前N个为左部分,后N个为右部分。 arr就可以表示为{L1,L2,…,Ln,R1,R2,…,Rn},请将数组调整成{R1,L1,R2,L2,…,Rn,Ln}的样子。要求空间复杂度O(1),时间复杂度尽量低
扩展问题:给定数组arr,将其最后变为[a,b,c,d,e,f],其中满足a<=b,b>=c,c<=d,d>=e…的排序
2.思路
通过小数量级的例子,来观察下标的变化
例1:
index 1 2 3 4 //注意这里下标我们默认从1开始
arr = a b c d
target= c a d b
元素 | 初始位置 | 变换后的位置 |
---|---|---|
a | 1 | 2 |
b | 2 | 4 |
c | 3 | 1 |
d | 4 | 3 |
例2:
index = 1 2 3 4 5 6
arr = a b c d e f
target= d a e b f c
元素 | 初始位置 | 变换后的位置 |
---|---|---|
a | 1 | 2 |
b | 2 | 4 |
c | 3 | 6 |
d | 4 | 1 |
e | 5 | 3 |
f | 6 | 5 |
通过上面的规律我们不难总结下标变换的规律:
当1<=index<=len/2 index->2*index
当len/2<index<=n index->2*(index-len/2)-1
所以我们只需要依次遍历数组中的每个位置,并且通过上述的计算公式得出其目标位置即可,但是题目要求额外空间复杂度为O(1),所以我们就不能额外创建一个O(n)的空间来存放数组;通过上面的两种情况我们不难发现:
例1,存在一个环1-2-4-3-1。例2,存在两个环1-2-4-1 3-6-5-3;就说明对于一个arr,肯定存在有限的几个环,我们只需要依次处理这几个环,把元素放入正确的位置。
3.归纳
对于一个数组长度M=(3k)-1,他的所有环的出发点{1,3,9,…,3(k-1)};所以对于一个长度为n(偶数)的数组,我们总是可以将其剥分为若干个长度为(3^k)-1,然后依次对这些数组进行位置的调整。
例:
n=12 arr={L1,L2,L3,L4,L5,L6,R1,R2,R3,R4,R5,R6}
我们寻找长度小于等于12,并且满足(3^k)-1<=12的最大的k,
1.可以算出k=2,即我们先解决index[0...8]这些位置的数字,即这一步我们先将数组变为
{[L1,L2,L3,L4,R1,R2,R3,R4],L5,L6,R5,R6}->{R1,L1,R2,L2,R3,L3,R4,L4,L5,L6,R5,R6}
2.接着寻找k解决L5,L6,R5,R6部分,可知k=1;
...
4.代码
import java.util.Arrays;
public class ShuffleProblem {
/**
* 数组下标从1开始
*
* @param i 调整前的位置
* @param len 数组的长度
* @return
*/
public static int modifyIndex(int i, int len) {
if (i <= len / 2) {
return 2 * i;
} else {
return 2 * (i - (len / 2)) - 1;
}
}
/**
* 对数组arr[L...R]进行逆序
*
* @param arr
* @param L
* @param R
*/
public static void reverse(int[] arr, int L, int R) {
while (L < R) {
int tmp = arr[L];
arr[L++] = arr[R];
arr[R--] = tmp;
}
}
/**
* [L..M]为左半部分,[M+1...R]为右半部分,左右两部分交换
*
* @param arr
* @param L
* @param M
* @param R
*/
public static void rotate(int[] arr, int L, int M, int R) {
reverse(arr, L, M);
reverse(arr, M + 1, R);
reverse(arr, L, R);
}
/**
* 主函数
*
* @param arr
*/
public static void shuffle(int[] arr) {
if (arr != null && arr.length != 0 && (arr.length & 1) == 0) {
shuffle(arr, 0, arr.length - 1);
}
}
/**
* 在[L...R]做完美洗牌
*
* @param arr
* @param L
* @param R
*/
public static void shuffle(int[] arr, int L, int R) {
while (R - L + 1 > 0) { //切成一块一块的,每一块长度满足(3^k)-1
int len = R - L + 1;
int base = 3;
int k = 1;
//计算小于等于len,并且是离len最近的,满足(3^k)-1的数
//也就是找到最大的k,满足3^k<=len+1
while (base < (len + 1) / 3) {
base *= 3;
k++;
}
//当前要解决数组[0...base-1]的部分
int half = (base - 1) / 2;
//找到[L...R]中点位置
int mid = (L + R) / 2;
//此时要旋转的部分为[L+half...mid] [mid+1...mid+half]
//此时下标是从0开始
rotate(arr, L + half, mid, mid + half);
cycles(arr, L, base - 1, k);
L = L + base - 1;
}
}
/**
* 从start位置开始,往右len的长度这一段,做下标连续推
* 出发的位置一次为1,3,9...
*
* @param arr
* @param start
* @param len
* @param k
*/
public static void cycles(int[] arr, int start, int len, int k) {
//i 一共有多少个环需要循环
//trigger 环的相对初始位置
//下标从0开始
for (int i = 0, trigger = 1; i < k; i++, trigger *= 3) {
int preValue = arr[start + trigger - 1];
int cur = modifyIndex(trigger, len);
while (cur != trigger) {
int tmp = arr[start + cur - 1];
arr[start + cur - 1] = preValue;
preValue = tmp;
cur = modifyIndex(cur, len);
}
arr[cur + start - 1] = preValue;
}
}
public static void wiggleSort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
Arrays.sort(arr);
if ((arr.length & 1) == 1) {
shuffle(arr, 1, arr.length - 1);
} else {
shuffle(arr, 0, arr.length - 1);
for (int i = 0; i < arr.length; i += 2) {
int tmp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = tmp;
}
}
}
public static void main(String[] args) {
int[] arr = new int[]{2, 4, 6, 8, 1, 3, 5, 7};
shuffle(arr);
System.out.println(Arrays.toString(arr));
}
}
扩展问题也是基于洗牌的思想,大家可以自信思考,欢迎评论区讨论!