给定一个无序的数组 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. Oracle 查询当前用户下的所有表

    select table_name from user_tables;

  2. Ajax学习系列——向服务器发送请求

    1.如何发送请求? 如果需要向服务器发送请求,我们使用的是XMLHttpRequest对象中的open()和send()方法. var xhr = new XMLHttpRequest();//具体创 ...

  3. Go——标准库使用代理

    本文知识点 Go的安装 Go使用代理 Go进阶学习 环境配置 Go的安装 确认环境都安装好了,看看go的版本. go version 代码样例 使用代理,发送GET请求 package main im ...

  4. Flink Task 并行度

    并行的数据流 Flink程序由多个任务(转换/运算符,数据源和接收器)组成,Flink中的程序本质上是并行和分布式的. 在执行期间,流具有一个或多个流分区,并且每个operator具有一个或多个ope ...

  5. windows driver 延时

    #define Delay_One_MicroSecond (-10) #define Delay_One_MilliSecond (Delay_One_MicroSecond * 1000) voi ...

  6. html使用aes进行加密

    1.导入 aes.js 文件 !function(t,n){*t.length},toString:function(t){);o<r;o++){]>>>-o%*&;n ...

  7. 考研c语言基础 66++6

    1.数据类型 对于基本的数据类型,如整型int,long,...(考研中涉及处理的整数题目,如果没有特别要求用int足够了),字符型char,浮点型float.double...(对于处理小数问题,在 ...

  8. 65.ORM查询条件:gte,gt,lte和lt的使用

    1. gte: 代表的是大于等于,英文全称为:great than equal.举例:找到文章id大于等于3等文章,示例代码如下: 定义模型的示例代码如下: from django.db import ...

  9. python实现微信发送服务器监控报警消息代码实现

    这篇文章主要介绍了python3.8 微信发送服务器监控报警消息代码实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 ! python版本 > ...

  10. MVC学生管理系统-阶段V(模糊查询)

    项目源码 :https://download.csdn.net/download/weixin_44718300/11091042 此处省略一段话.去上一篇查看 NO01:修改list.jsp < ...