324. 摆动排序 II

难度中等

给定一个无序的数组 nums,将它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]... 的顺序。

示例 1:

输入: nums = [1, 5, 1, 1, 6, 4]
输出: 一个可能的答案是 [1, 4, 1, 5, 1, 6]

示例 2:

输入: nums = [1, 3, 2, 2, 3, 1]
输出: 一个可能的答案是 [2, 3, 1, 3, 1, 2]

说明:
你可以假设所有输入都会得到有效的结果。

进阶:
你能用 O(n) 时间复杂度和 / 或原地 O(1) 额外空间来实现吗?

1. 解法1:排序

首先,我们可以很容易想到一种简单的解法:将数组进行排序,然后从中间位置进行等分(如果数组长度为奇数,则将中间的元素分到前面),然后将两个数组进行穿插

例如:
对于数组[1, 5, 2, 4, 3],我们将其排序,得到[1, 2, 3, 4, 5],然后将其分割为[1, 2, 3]和[4, 5],对两个数组进行穿插,得到[1, 4, 2, 5, 3]。

但是这一解法有一个问题,例如,对于数组[1, 2, 2, 3],按照这种做法求得的结果仍为[1, 2, 2, 3]。如果题目不要求各元素严格大于或小于相邻元素,即,只要求nums[0] <= nums[1] >= nums[2] <= nums[3]…,那么这一解法是符合要求的,但题目要求元素相互严格大于或小于,那么需要稍微做一点改进。

为了方便阅读,我们在下文中定义较小的子数组为数组A,较大的子数组为数组B。显然,出现上述现象是因为nums中存在重复元素。实际上,由于穿插之后,相邻元素必来自不同子数组,所以A或B内部出现重复元素是不会出现上述现象的。所以,出现上述情况其实是因为数组A和数组B出现了相同元素,我们用r来表示这一元素。而且我们可以很容易发现,如果A和B都存在r,那么r一定是A的最大值B的最小值,这意味着r一定出现在A的尾部,B的头部。其实,如果这一数字的个数较少,不会出现这一现象,只有当这一数字个数达到原数组元素总数的一半,才会在穿插后的出现在相邻位置。以下举几个例子进行形象地说明:

例如,对于数组[1,1,2,2,3,3],分割为[1,1,2]和[2,3,3],虽然A和B都出现了2,但穿插后为[1,2,1,3,2,3],满足要求。
而如果2的个数再多一些,即[1,1,2,2,2,3],分割为[1,1,2]和[2,2,3],最终结果为[1,2,1,2,2,3],来自A的2和来自B的2出现在了相邻位置。

出现这一问题是因为重复数在A和B中的位置决定的,因为r在A尾部,B头部,所以如果r个数太多(大于等于(length(nums) + 1)/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,还是会出现相邻。但实际上,这种情况是不存在有效解的,也就是说,这种数组对于本题来说是非法的。

此时我们得到了第一个解法,由于需要使用排序,所以时间复杂度为O(NlogN),由于需要存储A和B,所以空间复杂度为O(N)。

class Solution {public void wiggleSort(int[] nums) {Arrays.sort(nums);int n = nums.length;int[] temp = nums.clone();for (int i = 0; i < n / 2; i++) {nums[2 * i] = temp[(n - 1) / 2 - i];nums[2 * i + 1] = temp[n - 1 - i]; }if (n % 2 == 1) nums[n - 1] = temp[0];}
}

2. 解法2:快速选择 + 3-way-partition

上一解法之所以时间复杂度为O(NlogN),是因为使用了排序。但回顾解法1,我们发现,我们实际上并不关心A和B内部的元素顺序,只需要满足A和B长度相同(或相差1),且A中的元素小于等于B中的元素,且r出现在A的头部和B的尾部即可。实际上,由于A和B长度相同(或相差1),所以r实际上是原数组的中位数,下文改用mid来表示。因此,我们第一步其实不需要进行排序,而只需要找到中位数即可。而寻找中位数可以用快速选择算法实现,时间复杂度为O(n)。

该算法与快速排序算法类似,在一次递归调用中,首先进行partition过程,即利用一个元素将原数组划分为两个子数组,然后将这一元素放在两个数组之间。两者区别在于快速排序接下来需要对左右两个子数组进行递归,而快速选择只需要对一侧子数组进行递归,所以快速选择的时间复杂度为O(n)。详细原理可以参考有关资料,此处不做赘述。

在C++中,可以用STL的nth_element()函数进行快速选择,这一函数的效果是将数组中第n小的元素放在数组的第n个位置,同时保证其左侧元素不大于自身,右侧元素不小于自身。

找到中位数后,我们需要利用3-way-partition算法将中位数放在数组中部,同时将小于中位数的数放在左侧,大于中位数的数放在右侧。该算法与快速排序的partition过程也很类似,只需要在快速排序的partition过程的基础上,添加一个指针k用于定位大数:

int i = 0, j = 0, k = nums.size() - 1;while(j < k){if(nums[j] > mid){swap(nums[j], nums[k]);--k;}else if(nums[j] < mid){swap(nums[j], nums[i]);++i;++j;}else{++j;}}

在这一过程中,指针j和k从左右两侧同时出发相向而行,每次要么j移动一步,要么k移动一步,直到相遇为止。这一过程的时间复杂度显然为O(N)。

至此,原数组被分为3个部分,左侧为小于中位数的数,中间为中位数,右侧为大于中位数的数。之后的做法就与解法1相同了:我们只需要将数组从中间等分为2个部分,然后反序,穿插,即可得到最终结果。以下为完整实现:

class Solution {public:void wiggleSort(vector<int>& nums) {auto midptr = nums.begin() + nums.size() / 2;nth_element(nums.begin(), midptr, nums.end());int mid = *midptr;// 3-way-partitionint i = 0, j = 0, k = nums.size() - 1;while(j < k){if(nums[j] > mid){swap(nums[j], nums[k]);--k;}else if(nums[j] < mid){swap(nums[j], nums[i]);++i;++j;}else{++j;}}if(nums.size() % 2) ++midptr;vector<int> tmp1(nums.begin(), midptr);vector<int> tmp2(midptr, nums.end());for(int i = 0; i < tmp1.size(); ++i){nums[2 * i] = tmp1[tmp1.size() - 1 - i];}for(int i = 0; i < tmp2.size(); ++i){nums[2 * i + 1] = tmp2[tmp2.size() - 1 - i];}}
};

快速选择过程也可以手动实现,以下为手动实现的完整代码:

class Solution {public:void wiggleSort(vector<int>& nums) {int len = nums.size();quickSelect(nums, 0, len, len / 2);auto midptr = nums.begin() + len / 2;int mid = *midptr;// 3-way-partitionint i = 0, j = 0, k = nums.size() - 1;while(j < k){if(nums[j] > mid){swap(nums[j], nums[k]);--k;}else if(nums[j] < mid){swap(nums[j], nums[i]);++i;++j;}else{++j;}}if(nums.size() % 2) ++midptr;vector<int> tmp1(nums.begin(), midptr);vector<int> tmp2(midptr, nums.end());for(int i = 0; i < tmp1.size(); ++i){nums[2 * i] = tmp1[tmp1.size() - 1 - i];}for(int i = 0; i < tmp2.size(); ++i){nums[2 * i + 1] = tmp2[tmp2.size() - 1 - i];}}private:void quickSelect(vector<int> &nums, int begin, int end, int n){int t = nums[end - 1];int i = begin, j = begin;while(j < end){if(nums[j] <= t){swap(nums[i++], nums[j++]);}else{++j;}}if(i - 1 > n){quickSelect(nums, begin, i - 1, n);}else if(i <= n){quickSelect(nums, i, end, n);}}
};

由于省略了排序过程,且快速选择和3-way-partition的时间复杂度都为O(N),所以这一解法时间复杂度为O(N)。和解法1相同,解法2也需要保存A数组和B数组,所以空间复杂度不变,仍未O(N)。

java解法

class Solution {int n=-1;public void wiggleSort(int[] nums) {//找到中位数索引int midIndex = this.quickSelect(nums,0,nums.length-1);//找到中位数int mid = nums[midIndex];n=nums.length;//三分法for(int i=0,j=0,k=nums.length-1;j<=k;){if(nums[V(j)]>mid){swap(nums,V(j++),V(i++));}else if(nums[V(j)]<mid){swap(nums,V(j),V(k--));}else{j++;}}}public int V(int i){return (1+2*(i)) % (n|1);}public void swap(int[] nums,int i,int j){int t = nums[i];nums[i]=nums[j];nums[j]=t;}public int quickSelect(int[] nums,int left,int right){int pivot = nums[left];int l = left;int r = right;while(l<r){while(l<r&&nums[r]>=pivot){r--;}if(l<r){nums[l++]=nums[r];}while(l<r&&nums[l]<=pivot){l++;}if(l<r){nums[r--]=nums[l];}}nums[l]=pivot;if(l==nums.length/2){return l;}else if(l>nums.length/2){return this.quickSelect(nums,left,l-1);}else{return this.quickSelect(nums,l+1,right);}}
}

3. 解法3:快速选择 + 3-way-partition + 虚地址

接下来,我们思考如何简化空间复杂度。上文提到,解法1和2之所以空间复杂度为O(N),是因为最后一步穿插之前,需要保存A和B。在这里我们使用所谓的虚地址的方法来省略穿插的步骤,或者说将穿插融入之前的步骤,即在3-way-partiton(或排序)的过程中顺便完成穿插,由此来省略保存A和B的步骤。“地址”是一种抽象的概念,在本题中地址就是数组的索引。

BTW,由于虚地址较为抽象,需要读者有一定的数学基础和抽象思维能力,如果实在理解不了没有关系,解法2已经是足够优秀的解法。

如果读者学习过操作系统,可以利用操作系统中的物理地址空间和逻辑地址空间的概念来理解。简单来说,这一方法就是将数组从原本的空间映射到一个虚拟的空间,虚拟空间中的索引和真实空间的索引存在某种映射关系。在本题中,我们需要建立一种映射关系来描述“分割”和“穿插”的过程,建立这一映射关系后,我们可以利用虚拟地址访问元素,在虚拟空间中对数组进行3-way-partition或排序,使数组在虚拟空间中满足某一空间关系。完成后,数组在真实空间中的空间结构就是我们最终需要的空间结构。

在某些场景下,可能映射关系很简洁,有些场景下,映射关系可能很复杂。而如果映射关系太复杂,编程时将会及其繁琐容易出错。在本题中,想建立一个简洁的映射,有必要对前面的3-way-partition进行一定的修改,我们不再将小数排在左边,大数排在右边,而是将大数排在左边,小数排在右边,在这种情况下我们可以用一个非常简洁的公式来描述映射关系:#define A(i) nums[(1+2*(i)) % (n|1)],i是虚拟地址,(1+2*(i)) % (n|1)是实际地址。其中n为数组长度,‘|’为按位或,如果n为偶数,(n|1)为n+1,如果n为奇数,(n|1)仍为n。

Accessing A(0) actually accesses nums[1].
Accessing A(1) actually accesses nums[3].
Accessing A(2) actually accesses nums[5].
Accessing A(3) actually accesses nums[7].
Accessing A(4) actually accesses nums[9].
Accessing A(5) actually accesses nums[0].
Accessing A(6) actually accesses nums[2].
Accessing A(7) actually accesses nums[4].
Accessing A(8) actually accesses nums[6].
Accessing A(9) actually accesses nums[8].

以下为完整代码:

class Solution {public:void wiggleSort(vector<int>& nums) {int n = nums.size();// Find a median.auto midptr = nums.begin() + n / 2;nth_element(nums.begin(), midptr, nums.end());int mid = *midptr;// Index-rewiring.#define A(i) nums[(1+2*(i)) % (n|1)]// 3-way-partition-to-wiggly in O(n) time with O(1) space.int i = 0, j = 0, k = n - 1;while (j <= k) {if (A(j) > mid)swap(A(i++), A(j++));else if (A(j) < mid)swap(A(j), A(k--));elsej++;}}
};

时间复杂度与解法2相同,为O(N),空间复杂度为O(1)。

当然,也可以在解法1中利用虚地址方法,即利用虚地址对nums进行排序,那么时间复杂度为O(NlogN),空间复杂度为O(1)。

作弊方式,打败百分百

采用一个数组arr来记录每个数出现的次数,然后使用两个变量odd和even分别代表较大数和较小数。从大到小遍历arr数组,取出单数和双数放在原来的nums数组中,完成替代。

class Solution {public void wiggleSort(int[] nums) {int max=Integer.MIN_VALUE;for(int n:nums){max=Math.max(n,max);}int[] arr=new int[max+1];for(int n:nums){arr[n]++;}int odd=1;int even=0;int i;for(i=arr.length-1;i>=0;i--){while(odd<nums.length&&arr[i]>0){nums[odd]=i;odd+=2;arr[i]--;}if(odd>=nums.length)break;}for(;i>=0;i--){while(even<nums.length&&arr[i]>0){nums[even]=i;even+=2;arr[i]--;}if(even>=nums.length)break;}}
}

【leetcode】324.摆动排序 II (四种解法,快速排序+3way-partition等,java实现)相关推荐

  1. 324. Wiggle Sort II | 324. 摆动排序 II(降序穿插)

    题目 https://leetcode.com/problems/wiggle-sort-ii/submissions/ 题解 没有一次想到正确的方法,是在 WA 的测试用例的提示下,一点一点修正,才 ...

  2. LeetCode 280. 摆动排序

    文章目录 1. 题目 2. 解题 1. 题目 给你一个无序的数组 nums, 将该数字 原地 重排后使得 nums[0] <= nums[1] >= nums[2] <= nums[ ...

  3. 算法-寻找数组中的重复值,四种解法

    算法-寻找数组中的重复值 寻找数组中的重复值 寻找数组中的重复值 题目来源于:Leetcode-287.本题归类到简单我无法理解-要满足四个条件需要用很特定的解法,面试中要是用到的话很可能是在给自己挖 ...

  4. 荷兰国旗排序的几种解法

    荷兰国旗排序的几种解法 leetcode 排序 算法 分治 Given an array with n objects colored red, white or blue, sort them so ...

  5. 奖券数目c语言答案,2015 年蓝桥杯 C 语言 B 组省赛第 1 题: 奖券数目 (四种解法 + 详细分析)...

    题目 奖券数目 有些人很迷信数字,比如带"4"的数字,认为和"死"谐音,就觉得不吉利. 虽然这些说法纯属无稽之谈,但有时还要迎合大众的需求.某抽奖活动的奖券号码 ...

  6. 四种解法——求子序列的最大连续子序和(普通解法、求和解法、分治法、O(n)级解法)(面试经典题)

    励志用少的代码做高效表达 在这四种解法里,解法一是通法,可以学到规律和知识,做基础之用:解法二在解法一的基础上做改进,锻炼思维:解法三则是大名鼎鼎的分治法,涉及到递归的知识,算是"高效算法设 ...

  7. mysql排序的四种方式

    mysql排序的四种方式 第一种,默认排序 第二种,field函数排序 第三种,条件排序 第四种,多重条件排序 第一种,默认排序 按照 order by 字段1 desc/asc, 字段2 desc/ ...

  8. python整数拆分dp算法_整数拆分问题的四种解法【转载】

    http://blog.csdn.net/u011889952/article/details/44813593 整数拆分问题的四种解法 原创 2015年04月01日 21:17:09 整数划分问题是 ...

  9. 【剑指Offer】剪绳子问题——四种解法

    剪绳子问题--四种解法 题目描述: 输入描述: 返回值描述: 示例1: 解题思路: 方法1:暴力递归 方法2:记忆化递归 方法三:动态规划 方法四,数学原理 题目描述: 给你一根长度为n的绳子,请把绳 ...

最新文章

  1. 人体姿态估计(Human Pose Estimation)技巧方法汇总
  2. 学术界盛事揭幕:一图解读跨越百余年的诺贝尔奖
  3. python程序设计报告-《Python程序设计》 实验报告.doc
  4. 杭电多校(三)2019.7.29--暑假集训
  5. Centos7常用命令[系统的关机、重启以及登出]
  6. Redis-相关概念记录
  7. 不知道Mysql排序的特性,加班到12点,认了认了!
  8. Chronos首页、文档和下载 - 作业调度器 - 开源中国社区
  9. IO子系统的层次结构
  10. FFT(FastFourier Transform,快速傅立叶变换)
  11. 点击触发ajax重复提交表单,屡次连续点击致使Ajax重复提交
  12. P2P协议:我下小电影,99%急死你
  13. 中国银联:金融概述、收单和清算、代收代付
  14. 使用正则表达式在Java中悬挂缩进段落
  15. MySQL-存储引擎-索引-锁-集群
  16. 让自己更优秀的 16 条法则(建议收藏)
  17. Mendix for Manufacturing Industries指南
  18. python、pip安装
  19. MOOS例程HelloWorld-详细注释
  20. 动态绑定和静态绑定详解

热门文章

  1. IDEA2018.1.4 破解教程
  2. [精易软件开发工程师Leo学习笔记]010模块化开发+API
  3. 基于android平台语音日程软件的设计与实现,基于Android平台语音日程软件的设计与实现...
  4. 2020~2022年软件测试的五大趋势
  5. 电脑浏览器被劫持的用户求助
  6. Linux微信1001无标题,微信个性签名1001无标题
  7. C51单片机之点亮LED灯
  8. 对称矩阵及稀疏矩阵浅谈
  9. Pandas入门之常用函数介绍
  10. 蓝桥杯31天冲刺之十一 [java]