给定一个无序的数组 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的更多相关文章

  1. Java实现 LeetCode 324 摆动排序 II

    324. 摆动排序 II 给定一个无序的数组 nums,将它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]- 的顺序. 示例 1: 输入: n ...

  2. Leetcode 324.摆动排序II

    摆动排序II 给定一个无序的数组 nums,将它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]... 的顺序. 示例 1: 输入: nums ...

  3. 324. 摆动排序 II(三路划分算法)

    题目: 给定一个无序的数组 nums,将它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]... 的顺序. 示例 1: 输入: nums = [ ...

  4. [LeetCode] 324. Wiggle Sort II 摆动排序 II

    Given an unsorted array nums, reorder it such that nums[0] < nums[1] > nums[2] < nums[3]... ...

  5. [Leetcode] 第324题 摆动排序II

    一.题目描述 给定一个无序的数组 nums,将它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]... 的顺序. 示例 1: 输入: nums ...

  6. 324 Wiggle Sort II 摆动排序 II

    给定一个无序的数组nums,将它重新排列成nums[0] < nums[1] > nums[2] < nums[3]...的顺序.例子:(1) 给定nums = [1, 5, 1, ...

  7. [Swift]LeetCode324. 摆动排序 II | Wiggle Sort II

    Given an unsorted array nums, reorder it such that nums[0] < nums[1] > nums[2] < nums[3]... ...

  8. leetcode324 摆动排序II

      1. 首先考虑排序后交替插入 首尾交替插入,这种方法对于有重复数字的数组不可行: class Solution { public: void wiggleSort(vector<int> ...

  9. C#LeetCode刷题-排序

    排序篇 # 题名 刷题 通过率 难度 56 合并区间   31.2% 中等 57 插入区间   30.4% 困难 75 颜色分类   48.6% 中等 147 对链表进行插入排序   50.7% 中等 ...

随机推荐

  1. HttpServletRequest HttpServletResponse接口详解

    HttpServletRequest接口最常用的方法就是获得请求中的参数,这些参数一般是客户端表单中的数据.同时,HttpServletRequest接口可以获取由客户端传送的名称,也可以获取产生请求 ...

  2. Hive 数据类型 + Hive sql

    Hive 数据类型 + Hive sql 基本类型 整型 int tinyint (byte) smallint(short) bigint(long) 浮点型 float double 布尔 boo ...

  3. linux之 文本编辑 的基础知识点

    第一步 打开终端 创建文件命令 touch 文件名.后缀名 打开文件命令 vi 文件名.后缀名 (此时进去txt文件之后为一般模式,你无法对文件进行增删改) 之后按 i    或 a    或o  都 ...

  4. Spring 事件(2)- 自定义事件

    Spring 系列教程 Spring 框架介绍 Spring 框架模块 Spring开发环境搭建(Eclipse) 创建一个简单的Spring应用 Spring 控制反转容器(Inversion of ...

  5. Linux下录屏

    我喜欢的: Gnome系用户,按ctrl+shift+alt+r,屏幕右上角有红点出现,开始录屏,结束的话再按一次ctrl+shift+alt+r,录好的视频在 ~/Videos下 ffmpeg # ...

  6. 【Python】【Django】登录用户-链接Mysql

  7. tornado反向解析

    tornado反向解析 在路由中添加name属性,并且不能使用元组路由,应当由tornado.web.url定义路由. app = tornado.web.Application([ (r'/', I ...

  8. html 基础 (9.19 第八天)

    一.HTML:超文本标记语言,是一种标签语言,不是编程语言,显示数据有双标签<body></body> 和单标签<img src=# / >, 标签大小写都可以通过 ...

  9. Windows2008R2安装DNS和SQLServer200r2服务 (9.18第七天)

    原文网址:https://www.cnblogs.com/yankaohaitaiwei/p/11538205.html 二.IIS搭建web服务器 1.格式化D盘,一定要选择NTFS!!!不然后面添 ...

  10. 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 ...