zoukankan      html  css  js  c++  java
  • 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:]
    
  • 相关阅读:
    华为EC169在MAC 10.9.6下的安装方法
    sqlmap用户手册 | WooYun知识库
    光纤光猫连接自己路由器的设定
    C# 里窗体里(windows form)怎么播放音乐
    让我们写的程序生成单个的exe文件(C#winform程序举例)
    Basic EEG waves 四种常见EEG波形
    Hemodynamic response function (HRF)
    Parseval's theorem 帕塞瓦尔定理
    Typical EEG waveforms during sleep 睡眠状态下的几种典型EEG波形
    EEG preprocessing
  • 原文地址:https://www.cnblogs.com/wwj99/p/12320817.html
Copyright © 2011-2022 走看看