LeetCode Problem 324: Wiggle Sort Ⅱ

问题描述

原题链接
Given an unsorted array nums, reorder it such that nums[0] < nums[1] > nums[2] < nums[3]….

Example:
(1) Given nums = [1, 5, 1, 1, 6, 4], one possible answer is [1, 4, 1, 5, 1, 6].
(2) Given nums = [1, 3, 2, 2, 3, 1], one possible answer is [2, 3, 1, 3, 1, 2].

Note:
You may assume all input has valid answer.

解题经过

这题对我来说太难了,想了很久没有一个能通过的办法。

具体来说,有以下几种想法:

  1. 先从小到大排序,然后让后半部分插入到前半部分中去。对于nums = [4, 5, 5, 6],不行,输出结果还是[4, 5, 5, 6],正解之一为[5, 6, 4, 5]。
  2. 第二种方法稍复杂些。观察可知,要求结果序列满足“小,大,小,大…”模式。如果,从前往后一个位置一个位置地考虑,第一个“小”应该怎么选择呢?只要不是余下序列里面最大的数就可以了。我们可以一次发现两个数,选择较小者。同样地,第二个位置的“大”,只要满足“大于前一个数且不是余下序列里最小的数”就可以了,也可以用“每次发现两个大于前位置数的数,选择较大者”这样的办法。以此类推。可是,这样做并不能保证每次都能找到符合要求的数,这依赖输入数组的原始顺序。有可能剩下来两个相等的数,分不出大小。例如对于nums=[4, 5, 5, 6],输出结果是[4, 6, 5, 5],是错误的。对于nums=[5, 6, 5, 4], 输出结果是[5, 6, 4, 5],这是正确的。
  3. 还是一个位置一个位置地考虑,换个简单点的办法来选“小”和“大”。注意到,选“小”不能是最大,选“大”不能是最小,考虑这样做:当需要选“小”的时候,在序列里找满足“大“”小”关系(即位置在前的数大,位置在后的数小)的数对,然后交换位置即可。这样找到的“小”确保有数比它大。如果在序列后面找不到更小的数,那么说明当前位置的数是最小的,也满足不最大条件(当然还得比前一个数大)。找“大”的时候类似。但是,这样还是不能保证一定能找到解。

计算机有多聪明,真是使用它的人决定的。像我的计算机就蠢得不会这样的锯齿形排序。:(

讨论区的解答

StefanPochmann的帖子介绍了一种名为“虚拟下标”(virtual indexing)的技巧。其实质为对同一数组的下标的重新表示。一般来说,下标都是从零开始一直递增一的。但是,如果可以破坏下标和位置之间这种固有的对应关系,也就是让下标0指向第三个位置或者第五个位置,而不是第一个位置的话,有时候可以帮助我们巧妙地解决问题。算法的魅力就在于一个“巧”字吧。

可以分两步解决这个问题。第一步,找出中位数,第二步,排序。

这个排序,并不是递增排序,而是类似Dutch national flag problem 。选择一个数为分界点,比它小的在一边,比它大的在另一边,和它相等的在中间。就好像把数个三种颜色的排成一直线的球,按颜色站队,相同颜色的在一起,不同颜色的分开,最终形成三段颜色的球线, 而相同颜色的球之间的次序并不重要。这和快速排序的步骤有相似之处。

和之前提出的第一种想法类似,以下标来说,我们需要排列数组,使得下标为1, 3, 5, 7, … 的数比下标为0, 2, 4, 6, 8…的数要大。但是奇数下标之间没有大小要求,偶数下标之间也是。所以不需要彻彻底底地递增排序。但是如果直接对原数组三分排序,不过是分成小-中-大三堆数字而已,并不满足题目要求。

这时,考虑如下坐标转换:

A(i) = nums[(2*i+1)/(n|1)]

其中n|1只是处理奇偶的区别,为偶数加一。n为nums的元素个数。当n=10时可以看到如下对应关系:

A(0) – > nums[1]
A(1) – > nums[3]
A(2) – > nums[5]
A(3) – > nums[7]
A(4) – > nums[9]
A(5) – > nums[0]
A(6) – > nums[2]
A(7) – > nums[4]
A(8) – > nums[6]
A(9) – > nums[8]

注意这样就把奇下标和偶下标分开了。对以A(i)表示的新数列按大于中位数的在前,小于中位数的在后排序。就可以得到下标表示为奇数的nums要大于下标表示为偶数的nums。于是可得所求数列。这种方法不仅在复杂度上要优于彻底排序的方法,而且对于像[4, 5, 5, 6]这样有重复数字的输入,因为模糊了奇(偶)数下标之间的大小关系,所以有很好的处理结果。

关于三分排序,可以参照上面提到的荷兰国旗问题。那么,中位数怎么求?

对于c++语言,参考使用库函数nth_element。这个函数可以确定第n个位置的数,并保证n前面位置的数不比它大,n后面位置的数不比他小。取n为数组中间位置,即可求得中位数。这让我想起了概率统计里的nth percentile。差不多一样的东西。

代码1如下:

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++;
    }
}

感想

路漫漫,道阻且长。


  1. 代码源自StanfanPochmann。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值