LeetCode——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) 额外空间来实现吗?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/wiggle-sort-ii
一
这道题给了我们一个无序数组,让我们排序成摆动数组,满足nums[0] < nums[1] > nums[2] < nums[3]...,并给了我们例子。我们可以先给数组排序,然后在做调整。调整的方法是找到数组的中间的数,相当于把有序数组从中间分成两部分,然后从前半段的末尾取一个,在从后半的末尾去一个,这样保证了第一个数小于第二个数,然后从前半段取倒数第二个,从后半段取倒数第二个,这保证了第二个数大于第三个数,且第三个数小于第四个数,以此类推直至都取完,参见代码如下:
解法一:
// O(n) space
class Solution {
public:
void wiggleSort(vector<int>& nums) {
vector<int> tmp = nums;
int n = nums.size(), k = (n + 1) / 2, j = n;
sort(tmp.begin(), tmp.end());
for (int i = 0; i < n; ++i) {
nums[i] = i & 1 ? tmp[--j] : tmp[--k];
}
}
};
二
解法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-partition
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;
}
}
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-partition
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;
}
}
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)。
快速选择 + 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--));
else
j++;
}
}
};
时间复杂度与解法2相同,为O(N),空间复杂度为O(1)。
当然,也可以在解法1中利用虚地址方法,即利用虚地址对nums进行排序,那么时间复杂度为O(NlogN),空间复杂度为O(1)。
三
先排序,再插空
class Solution:
def wiggleSort(self, nums: List[int]) -> None:
nums.sort(reverse=True)
mid = len(nums) // 2
nums[1::2],nums[0::2] = nums[:mid], nums[mid:]
LeetCode——324. 摆动排序 II的更多相关文章
- Java实现 LeetCode 324 摆动排序 II
324. 摆动排序 II 给定一个无序的数组 nums,将它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]- 的顺序. 示例 1: 输入: n ...
- Leetcode 324.摆动排序II
摆动排序II 给定一个无序的数组 nums,将它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]... 的顺序. 示例 1: 输入: nums ...
- 324. 摆动排序 II(三路划分算法)
题目: 给定一个无序的数组 nums,将它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]... 的顺序. 示例 1: 输入: nums = [ ...
- [LeetCode] 324. Wiggle Sort II 摆动排序 II
Given an unsorted array nums, reorder it such that nums[0] < nums[1] > nums[2] < nums[3]... ...
- [Leetcode] 第324题 摆动排序II
一.题目描述 给定一个无序的数组 nums,将它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]... 的顺序. 示例 1: 输入: nums ...
- 324 Wiggle Sort II 摆动排序 II
给定一个无序的数组nums,将它重新排列成nums[0] < nums[1] > nums[2] < nums[3]...的顺序.例子:(1) 给定nums = [1, 5, 1, ...
- [Swift]LeetCode324. 摆动排序 II | Wiggle Sort II
Given an unsorted array nums, reorder it such that nums[0] < nums[1] > nums[2] < nums[3]... ...
- leetcode324 摆动排序II
1. 首先考虑排序后交替插入 首尾交替插入,这种方法对于有重复数字的数组不可行: class Solution { public: void wiggleSort(vector<int> ...
- C#LeetCode刷题-排序
排序篇 # 题名 刷题 通过率 难度 56 合并区间 31.2% 中等 57 插入区间 30.4% 困难 75 颜色分类 48.6% 中等 147 对链表进行插入排序 50.7% 中等 ...
随机推荐
- HttpServletRequest HttpServletResponse接口详解
HttpServletRequest接口最常用的方法就是获得请求中的参数,这些参数一般是客户端表单中的数据.同时,HttpServletRequest接口可以获取由客户端传送的名称,也可以获取产生请求 ...
- Hive 数据类型 + Hive sql
Hive 数据类型 + Hive sql 基本类型 整型 int tinyint (byte) smallint(short) bigint(long) 浮点型 float double 布尔 boo ...
- linux之 文本编辑 的基础知识点
第一步 打开终端 创建文件命令 touch 文件名.后缀名 打开文件命令 vi 文件名.后缀名 (此时进去txt文件之后为一般模式,你无法对文件进行增删改) 之后按 i 或 a 或o 都 ...
- Spring 事件(2)- 自定义事件
Spring 系列教程 Spring 框架介绍 Spring 框架模块 Spring开发环境搭建(Eclipse) 创建一个简单的Spring应用 Spring 控制反转容器(Inversion of ...
- Linux下录屏
我喜欢的: Gnome系用户,按ctrl+shift+alt+r,屏幕右上角有红点出现,开始录屏,结束的话再按一次ctrl+shift+alt+r,录好的视频在 ~/Videos下 ffmpeg # ...
- 【Python】【Django】登录用户-链接Mysql
- tornado反向解析
tornado反向解析 在路由中添加name属性,并且不能使用元组路由,应当由tornado.web.url定义路由. app = tornado.web.Application([ (r'/', I ...
- html 基础 (9.19 第八天)
一.HTML:超文本标记语言,是一种标签语言,不是编程语言,显示数据有双标签<body></body> 和单标签<img src=# / >, 标签大小写都可以通过 ...
- Windows2008R2安装DNS和SQLServer200r2服务 (9.18第七天)
原文网址:https://www.cnblogs.com/yankaohaitaiwei/p/11538205.html 二.IIS搭建web服务器 1.格式化D盘,一定要选择NTFS!!!不然后面添 ...
- Maven:java.lang.RuntimeException: Unexpected error: java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty
直接参考前面一篇文章中间介绍的第2种方法即可:Maven:sun.security.validator.ValidatorException: PKIX path building failed: s ...