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:]
posted @ 2020-02-17 11:05  小萝卜鸭  阅读(216)  评论(0编辑  收藏  举报