Medium | LeetCode 324. 摆动排序 II | Top-K大数 + 三路排序
324. 摆动排序 II
给你一个整数数组 nums
,将它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]...
的顺序。
你可以假设所有输入数组都可以得到满足题目要求的结果。
示例 1:
输入:nums = [1,5,1,1,6,4]
输出:[1,6,1,5,1,4]
解释:[1,4,1,5,1,6] 同样是符合题目要求的结果,可以被判题程序接受。
示例 2:
输入:nums = [1,3,2,2,3,1]
输出:[2,3,1,3,1,2]
提示:
1 <= nums.length <= 5 * 104
0 <= nums[i] <= 5000
- 题目数据保证,对于给定的输入
nums
,总能产生满足题目要求的结果
进阶:你能用 O(n) 时间复杂度和 / 或原地 O(1) 额外空间来实现吗?
解题思路
方法一: 排序
一种办法是很容易想到的, 首先将数组排序。然后分成等长的两部分。最后将这两部分交错放在一块。比如[4,2,5,1,3,7,6], 分成[1,2,3,4], 和[5,6,7]两部分。然后交错, 得到[1,5,2,6,3,7,4]。得到一个摆动序列。
但是这种问题存在一个问题。对于数组有重复数字的情况。比如[1,2,2,3]。 分成[1, 2], [2, 3]。然后进行交错。 2总会相邻。就不能保证摆动的严格大于或者严格小于的效果了。所以这种解法的关键问题在于分割后等长的两个数组里其头部和尾部有数字重复, 需要做的就是把这两部分的数字错开。
要解决这一问题,我们需要使A的r和B的r在穿插后尽可能分开。一种可行的办法是将A和B反序:反序之后, 原来较小数组的最大数到头部去了, 原来较大数组的较小数到尾部去了, 这样就能最大程度上避免这两部分的数字相邻。
例如,对于数组[1,1,2,2,2,3],分割为[1,1,2]和[2,2,3],分别反序后得到[2, 1, 1]和[3, 2, 2],此时2在A头部,B尾部,穿插后就不会发生相邻了。
当然,这只能解决r的个数等于(length(nums) + 1)/2的情况,如果r的个数大于(length(nums) + 1)/2,还是会出现相邻。但实际上,这种情况是不存在有效解的,也就是说,这种数组对于本题来说是非法的。
public void wiggleSort(int[] nums) {
// 将数组切成两半, 数组大小为奇数时, 左边数组较大。
int mid = (nums.length + 1) >> 1;
int[] preHalf = new int[mid];
int[] posthalf = new int[nums.length - mid];
// 排序
Arrays.sort(nums);
// 拷贝两个数组的数字
System.arraycopy(nums, 0, preHalf, 0, mid);
System.arraycopy(nums, mid, posthalf, 0, nums.length - mid);
// 反转两个数组
reverse(preHalf, 0, preHalf.length - 1);
reverse(posthalf, 0, posthalf.length - 1);
// 将两个数组交错
int index = 0, i = 0;
while (i < mid - 1) {
nums[index++] = preHalf[i];
nums[index++] = posthalf[i];
i++;
}
if (i < preHalf.length) {
nums[index++] = preHalf[i];
}
if (i < posthalf.length) {
nums[index] = posthalf[i];
}
}
public void reverse(int[] nums, int left, int right) {
while (left < right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
right--;
}
}
方法二: 中位数(快排) + 三路排序
三路排序问题: Easy | LeetCode 75. 颜色分类 | 快速排序 如此题。
思路还是和第一种思路一样, 不过没有必要对整个数组进行排序。有更好的解决办法。
首先找到中位数, 这里Top-K大数的算法即可解决。然后利用三路排序算法(荷兰国旗问题)将和中位数相等的所有数字放到中间。即可替换第一种的排序算法。
public void wiggleSort(int[] nums) {
// 前半部分的数量 可能小于 后半部分的数量
int mid = (nums.length + 1) >> 1;
int[] preHalf = new int[mid];
int[] posthalf = new int[nums.length - mid];
/** 将解法一的代码复制过来, 重写下面的排序部分 */
// Arrays.sort(nums);
int left = 0, right = nums.length - 1;
while (true) {
int leftTemp = left, rightTemp = right;
int pivot = nums[left];
while (left < right) {
// 先从右往左扫描, 找到一个小于等于pivot的数字, 放到left处
while (left < right && nums[right] >= pivot) right--;
nums[left] = nums[right];
/**
* 这里不能 nums[left++] = nums[right];
* 举例 [1, 1, 2, 2, 2, 1], pivot = 1
* 从右向左扫描后 [1, 1, 2, 2, 2, 1]
* 从左向右扫描后 [1, 1, 2, 2, 2, 2]
* 此时left = 2, right = 4
* 接着从右向左会导致 left = right = 2
* 加入此时 nums[left++] = nums[right]; 会导致 left = 3
* 最后将枢纽值赋值到left的时候会出现错误
*
*/
// 从左往右扫描, 找到一个大于pivot的数字, 放到right处
while (left < right && nums[left] <= pivot) left++;
nums[right] = nums[left];
}
nums[left] = pivot;
if (left > mid - 1) {
right = left - 1;
left = leftTemp;
} else if (left < mid - 1) {
left = left + 1;
right = rightTemp;
} else {
break;
}
}
/** 排序部分 重写完毕 */
/**
* 以上部分为完全解决问题: 比如 [5, 3, 1, 2, 5, 5, 8, 7, 6], 中位数是5
* 然后切开 [5, 2, 1, 3, 5], [6, 7, 8, 5] , 最终得到 [5, 6, 2, 7, 1, 8, 3, 5, 5]
* 中位数部分有重复
*
* 所以还要做一个处理, 把中位数的数字全部放在中间, 把比中位数小的数字放在左边, 比中位数大的数字全部放右边
* 是一个典型的 荷兰国旗问题
*/
int l = 0, cur = 0, r = nums.length - 1;
while (cur < r){
if (nums[cur] > nums[mid-1]){
swap(nums, cur, r);
--r;
} else if (nums[cur] < nums[mid-1]){
swap(nums, cur, l);
++l;
++cur;
} else {
++cur;
}
}
/** 荷兰国旗问题 写完*/
// 分别将两半部分复制进两个数组
System.arraycopy(nums, 0, preHalf, 0, mid);
System.arraycopy(nums, mid, posthalf, 0, nums.length - mid);
// 将两个数组反序
reverse(preHalf, 0, preHalf.length - 1);
reverse(posthalf, 0, posthalf.length - 1);
int index = 0, i = 0;
while (i < nums.length >> 1) {
// 因为前半部分的数量 可能小于 后半部分的数量, 所以要先穿插后半部分
nums[index++] = preHalf[i];
nums[index++] = posthalf[i];
i++;
}
if (i < preHalf.length) {
nums[index] = preHalf[i];
}
}