[LeetCode]Wiggle Sort

题目:Wiggle Sort II

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.

Follow Up:
Can you do it in O(n) time and/or in-place with O(1) extra space?

给定一个未排序的数组nums,重新排序它使得nums [0] <nums [1]> nums [2] <nums [3] ....

注意:
您可以假设所有输入都有有效的答案。

要求:
时间复杂度O(n),空间复杂度O(1)

思路:

实际上它是将数组分成以中位数为界限,大于中位数的和小于中位数的两个子集;然后,将它们重新组合,只要一个大于中位数的子集和一个小于中位数的子集相邻就可以。

但是,因为数组元素可以重复,所以等于中位数的元素可能不止一个;此时,等于中位数的数,可能一部分需要在小于中位数的子集中,一部分在大于中位数的子集中;

这样要保证上面的规律,只需要使大于中位数的值对应小于中位数的子集中等于中位数的值的那部分数。

例如:

arr[] = {4,5,5,5,5,6,6,6},中位数median = 5;

小于中位数的子集:smaller[] = {4,5,5,5};

大于中位数的子集:larger[] = {5,6,6,6};

则结果:result[] = {5,6,5,6,5,6,4,5};//保证larger中的6对应smaller中的5

那么首先如何求数组的中位数?

直接的方法是排序找中间的数,但是复杂度太高;

那么,如果能联想到快速排序中的分配的思想,就能更快的找到中位数:

快速排序中,一趟能够找到中心元素的有序后的位置,但是它是随机选一个元素作中心元素的;如果能一下子选中中位数作中心元素,就能通过这个分配方法,一趟确定中位数;

实际上,这是很困难的,这样就只能通过折半的方法逐渐靠近,找到中位数。这样一分析,实际上,它的复杂度也是O(nlogn),实际比排序的速度要快(不需要排序所有元素)。

如果知道C++标准库提供的nth_element(First,pos,Last);的函数就能通过这个函数一下子找到中位数。具体函数的信息:http://blog.csdn.net/gxiaob/article/details/8475558

nth_element可以使第n大元素处于第n位置(从0开始,其位置是下标为 n的元素),并且比这个元素小的元素都排在这个元素之前,比这个元素大的元素都排在这个元素之后,但不能保证他们是有序的。

nth_element()的实现可以参考:http://www.cppblog.com/feng/archive/2008/11/06/66141.html

通过上面的方法就能找到中位数。

然后怎样排序?

排序相对来讲比较简单,我的想法很直接:

设置两个标志,分别指示小于中位数的子集中,需要替换的位置;和大于中位数的子集中,需要替换的位置。

  1. 小于中位数的标志从尾到头,每次隔一个位置移动;
  2. 大于中位数的标志从头到尾,每次隔一个位置移动;

注意:开始以为只要保证两个子集中较大的数对应较大的数就可以了,顺序无所谓,后来发现,当测试用例是{4,5,5,6}时,只能是上面的顺序;因为{4,5}和{5,6}如果直接从小到大的顺序就变成了{4,5,5,6},这样不符合题目的排序要求;只能是{5,6,4,5}

这样,按照顺序遍历数组,当遇到大于中位数的元素,将它与大于中位数的标志对应的元素替换,并且移动标志;当遇到小于中位数的元素,将它与小于中位数的标志对应的元素替换,并且移动标志。

void LeetCode::wiggleSort(vector<int>& nums){
    if (nums.empty())return;
    int n = nums.size();

    auto pos = nums.begin() + n / 2;
    nth_element(nums.begin(), pos, nums.end());//求中位数
    int median = *pos;

    int slast = n - 1 - !(n & 0x01), lfirst = 1;
    for (int i = 0; i < n; ++i){
        if (nums[i] > median && (!(i & 0x01) || i >= lfirst)){//大于中位数,下一个位置是奇数位置(找大于中位数的数值),且已经找到了,则跳过
            swap(nums[i], nums[lfirst]);
            lfirst += 2;//下一个要大于中位数的位置
        }
        else if (nums[i] < median && (i & 0x01 || i < slast)){//小于中位数,且该位置若是偶数位置(找小于中位数的数值),同时已经找到了,则跳过
            swap(nums[i--], nums[slast]);
            slast -= 2;//下一个要小于中位数的位置
        }
    }
}
slast = n - 1 - !(n & 0x01)是小于中位数的标志,它的初值与数组大小的奇偶性相关;
注意:因为标志的移动速度比数组遍历速度快,所以已经找到的元素,再遍历的时候要跳过。

思路2:

给定数组Arr[] = {a0,a1,a2,a3,a4,a5,a6,a7,a8,a9};中位数是aj

将它分为三个子集:

  1. (>aj)larger[] = {b0,b1,b2}//无序
  2. (=aj)equal[] = {b3,b4,b5,b6}
  3. (<aj)smaller[] = {b7,b8,b9}//无序

然后:

  1. 大于中位数的元素:我们可以把它们放在前几个奇数位;
  2. 小于中位数的元素:我们可以把它们放在最后几个偶数位;
  3. 等于中位数的元素:我们可以把它们放在剩余的位置。

假设数组变成下面的顺序Brr[] = {b0,b1,b2,b3,b4,b5,b6,b7,b8,b9}

转换后的数组的元素    b0 b1 b2 b3 b4 b5 b6 b7 b8 b9

它对应的结果数组的下标  1 3 5 7 9 0 2 4 6 8

重新排列后:

结果数组的下标: 0 1 2 3 4 5 6 7 8 9

转换后数组的元素b5 b0 b6 b1 b7 b2 b8 b3 b9 b4

因此首先找到数组Brr在按照上面的规律转换坐标就能的到结果。

可以通过下面的函数实现坐标的转换:

int map_index(int i, int n) {
    return (2 * i + 1) % (n | 1);
}

实际实现时,不需要保存Brr,在找到Brr的元素时立即去转换坐标就可以了。

void LeetCode::wiggleSort(vector<int>& nums){
    if (nums.empty())return;
    int n = nums.size();

    auto pos = nums.begin() + n / 2;
    //nth_element函数找到有序后pos位置对应的值,并将它放到pos的位置。
    nth_element(nums.begin(), pos, nums.end());//求中位数
    int median = *pos;

    //求第i(i < (n + 1) / 2)个大于中位数的值和第i(i > n/2)个小于中位数的值
    //该表达式,根据i的递增,按照1 3 5 7...0 2 4 6...的顺序的到值
    auto m = [n](int i) { return (2 * i + 1) % (n | 1); };//lambda表达式
    //larger记录大于中位数的值的位置,smaller记录小于中位数的值的位置
    int larger = 0, mid = 0, smaller = n - 1;
    while (mid <= smaller) {
        if (nums[m(mid)] > median) {//大于中位数
            swap(nums[m(larger)], nums[m(mid)]);//放到larger的位置
            ++larger;
            ++mid;
        }
        else if (nums[m(mid)] < median) {//小于中位数
            swap(nums[m(mid)], nums[m(smaller)]);
            --smaller;
        }
        else {
            ++mid;
        }
    }
}

注意:

  1. 上面使用lambda表达式来实现坐标转换。
  2. m(mid)随着mid的取值从0依次递增,取值为1 3 5 7...0 2 4 6...
原文地址:https://www.cnblogs.com/yeqluofwupheng/p/6869691.html