谈一谈那些非正常排序题目

写在前面

在leetcode上面刷到几道很有意思的排序题,和传统的排序题不一样,很有意思。

75. 颜色分类

给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
注意:
不能使用代码库中的排序函数来解决这道题。

示例:
输入: [2,0,2,1,1,0]
输出: [0,0,1,1,2,2]

直接排序的方法我们就先不考虑了,因为最快也只能O(n*log(n))。

两遍扫描

最容易想到的解法,就是一次遍历统计出0,1,2的个数,然后再顺序放进数组。
这样需要遍历两次,思路很清晰,代码这里就略去了。

一遍扫描(荷兰国旗问题)

因为只有012三种数字,所以最后的结果肯定是这样的:
00000……00000011111……11111111222222……2222222
我们其实只要找到两个交界点就行了。
所以我们假设出两个指针,p1,p2,
初始化:p1 = 0, p2 = nums.size()-1。
然后再维护一个curr指针,这个指针从头扫到尾。
算法:
当curr<=p2:

  • nums[curr]==0: nums[curr]和nums[p1]交换,然后p1和curr都往后移。
  • nums[curr]==1: curr+1
  • nums[curr]==2: nums[curr]和nums[p2]交换,p2-1.

代码:

class Solution {
public:
    void sortColors(vector<int>& nums) {
        if(nums.empty()) return;
        int n = nums.size();
        int p1(0),p2(n-1),curr(0);
        while(curr<=p2){
            if(nums[curr]==0){
                swap(nums[p1],nums[curr]);
                ++p1;
                ++curr;
            }
            else if(nums[curr]==2){
                swap(nums[p2],nums[curr]);
                --p2;
            }
            else{
                ++curr;
            }
        }
        return;
    }
};

280. 摆动排序

给你一个无序的数组 nums, 将该数字 原地 重排后使得 nums[0] <= nums[1] >= nums[2] <= nums[3]…。
示例:
输入: nums = [3,5,2,1,6,4]
输出: 一个可能的解答是 [3,5,1,6,2,4]

排序+交换

最容易想到的就是先排序,然后从第二个开始,每两个交换一下,就可以满足要求了。

代码:

class Solution {
public:
    void wiggleSort(vector<int>& nums) {
        if(nums.empty()) return;
        sort(nums.begin(),nums.end());
        int p=1;
        while(p<nums.size()&&p+1<nums.size()){
            swap(nums[p],nums[p+1]);
            p+=2;
        }
        return;
    }
};

一次扫描

排序然后交换的方法,复杂度O(nlog(n))。
有没有更快的方法呢,当然有,我们直接判断相邻两个数字的大小关系,不满足要求就换就完事了。

代码:

class Solution {
public:
    void wiggleSort(vector<int>& nums) {
        bool up = true;
        for(int i=1; i<nums.size(); ++i){
            if(up){
                if(nums[i]<nums[i-1])
                    swap(nums[i],nums[i-1]);
            }
            else{
                if(nums[i]>nums[i-1])
                    swap(nums[i],nums[i-1]);
            }
            up = !up;
        }
        return;
    }
};

简化一下代码,去掉布尔标志,利用下标奇偶性。

代码:

class Solution {
public:
    void wiggleSort(vector<int>& nums) {
        for(int i=1; i<nums.size(); ++i){
            if((i%2==1&&nums[i]<nums[i-1]) || (i%2==0&&nums[i]>nums[i-1]))
                swap(nums[i],nums[i-1]);
        }
        return;
    }
};

有些读者可能就觉得简化代码没有意义,实际上这样简化一下,一方面可以养成比较好的代码习惯,减少不必要的代码,增加可读性,另一方面,简化后的代码性能得到提升,可以少量提速。
时间
上面三个代码的时间,可以发现越来越快。

再放一段更简洁的代码,感兴趣的朋友可以思考一下原理,欢迎评论留言讨论。

class Solution {
public:
    void wiggleSort(vector<int>& nums) {
        for(int i=1; i<nums.size(); ++i){
            if((i%2==1)==(nums[i]<nums[i-1]))
                swap(nums[i],nums[i-1]);
        }
        return;
    }
};

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]

排序+穿插

还是可以先排个序,然后因为这题要求是严格摇摆,不能有等于,所以分成两块之后要逆序穿插。

代码:

class Solution {
public:
    void wiggleSort(vector<int>& nums) {
        vector<int> a(nums);
        int n = nums.size();
        sort(a.begin(),a.end());
        int l,r;
        r = n-1;
        l = (n-1)/2;
        int p = 0;
        while(l>=0 && r>(n-1)/2){
            nums[p] = a[l];
            nums[p+1] = a[r];
            p += 2;
            --l;
            --r;
        }
        if(n%2==1){
            nums[n-1] = a[0];
        }
        return;
    }
};

快速选择 + 3way-partition

思路和第一个解法是一样的,但是不需要排序,用快速选择可以更快的打到目的,这里po一个我参考的链接,解释的很好,我就不赘述了(我也讲不清-逃)

传送门

376. 摆动序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

示例 1:
输入: [1,7,4,9,2,5]
输出: 6
解释: 整个序列均为摆动序列。

dp

暴力就不写了,没提升。这题很容易想到dp,维护两个数组up和down记录到当前位置升序或降序的最长长度。
转移方程:
up[i] = max(up[i],down[j]+1) j<i
down[i] = max(down[i],up[j]+1) j<i

代码:

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int l = nums.size();
        if(l<2) return l;
        vector<int> up(l+1,0);
        vector<int> down(l+1,0);
        for(int i=0; i<l; ++i)
            for(int j=0; j<i; ++j){
                if(nums[i]>nums[j])
                    up[i] = max(up[i],down[j]+1);
                else if(nums[i]<nums[j])
                    down[i] = max(down[i],up[j]+1);
            }
        return max(down[l-1],up[l-1])+1;
    }
};

线性dp

n^2的解法总是有些不尽人意,这里思考一下能不能线性解决呢,我们观察原数组,不难发现相邻两个数只有三种状态:

  1. 上升的位置,意味着 nums[i] > nums[i - 1]nums[i]>nums[i−1]
  2. 下降的位置,意味着 nums[i] < nums[i - 1]nums[i]<nums[i−1]
  3. 相同的位置,意味着 nums[i] == nums[i - 1]nums[i]==nums[i−1]

然后我们还是维护两个dp数组up和down:

  1. 如果 nums[i] > nums[i-1]nums[i]>nums[i−1] ,意味着这里在摆动上升,前一个数字肯定处于下降的位置。所以 up[i] = down[i-1] + 1up[i]=down[i−1]+1 , down[i]down[i] 与 down[i-1]down[i−1] 保持相同。

  2. 如果 nums[i] < nums[i-1]nums[i]<nums[i−1] ,意味着这里在摆动下降,前一个数字肯定处于下降的位置。所以 down[i] = up[i-1] + 1down[i]=up[i−1]+1 , up[i]up[i] 与 up[i-1]up[i−1] 保持不变。

  3. 如果 nums[i] == nums[i-1]nums[i]==nums[i−1] ,意味着这个元素不会改变任何东西因为它没有摆动。所以 down[i]down[i] 与 up[i]up[i] 与 down[i-1]down[i−1] 和 up[i-1]up[i−1] 都分别保持不变。

代码:

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int l = nums.size();
        if(l<2) return l;
        vector<int> up(l+1,0);
        vector<int> down(l+1,0);
        up[0] = down[0] = 1;
        for(int i=1; i<l; ++i)
            if(nums[i]>nums[i-1]){
                down[i] = down[i-1];
                up[i] = down[i-1]+1;
            }
            else if(nums[i]<nums[i-1]){
                up[i] = up[i-1];
                down[i] = up[i-1]+1;
            }
            else{
                up[i] = up[i-1];
                down[i] = down[i-1];
            }
        return max(down[l-1],up[l-1]);
    }
};
原创文章 23 获赞 41 访问量 3977

猜你喜欢

转载自blog.csdn.net/u011708337/article/details/105560605