由leetcode324题谈起

问题描述

给定一个数组A[0…n],重新排列数组,使得有:

A[0] < A[1] > A[2] < A[3] …

注:默认给出的数据一定有解。
注2:上述< 以及 > 不具有传递性。

朴素的想法1

  1. 排序
  2. 对半划分
    -若数组长度为偶数,直接对半划分
    -若数组长度为奇数,将最小的元素放置到输出数组的最后,剩下的部分均匀划分为左右两部分
  3. 按照降序,依次从左右两部分中选取元素,顺序填入输出数组中

e.g.

A={1 2 3 1 2 3 2}

  1. 排序
    1 1 2 2 2 3 3
  2. 对半划分
    1 2 2 | 2 3 3 |1
    <----- | <----
  3. 依次填入
    _ _ _ _ _ _ 1   1 2 2   2 3 3
             <—   <—
    2 3 _ _ _ _ 1   1 2    2 3
                <—   <—
    2 3 2 3 _ _ 1   1     2
             <—   <—
    2 3 2 3 1 2 1

注:在3中必须按照降序依次填入,否则无法处理4 5 5 6这种情况,具体分析见后文正确性分析。

伪代码————

naiveWiggleSort1(A[0...n])
		sort(A[0...n]);
		auxA = A;
		if(n+1%2 == 0)  //数组长度为偶数
			int k = 0;
			for(int i = 0, j = ((n+1) / 2); j <= n; i++, j++)
				A[k] = auxA[i];
				A[k+1] = auxA[j];
				k+=2;	

		else//数组长度为奇数
			A[n] = auxA[0];
			int k = 0;
			for(int i = 0, j = ((n-1+1)/2); j <= n-1; i++, j++)
				A[k] = auxA[i];
				A[k+1] = auxA[j];
				k+=2;

	注:注意上述i, j, k索引不要越界

复杂度分析————
  主要复杂度来自于排序,是O(nlgn);空间复杂度是O(n),因为使用了一个辅助数组。

正确性分析————
  主要关注点在于,为什么在3中必须按照降序排序填入?
  对于4 5 5 6,如果按照升序排序填入的话会出现下述情况:

4 5 _ _
4 5 5 6

  显然不符合题目要求。为什么呢?
  因为:按照升序排序填入的话,先填入左半部分的最小的4,然后填入右半部分最小的5,再填入左半部分第二小的5。显然这种填入方法可能会出现,右半部分上一次填入的值与左半部分这次填入的值大小相同。

  那么按照降序填入为什么即可以呢?

5 6 _ _
5 6 4 5

  因为:第一次填入了左半部分最大的值,然后填入右半部分最大的值,再填入的时候,不可能出现一个值比左半部分最大值小,却比右半部分最大值大。

朴素的想法2

  1. 排序

  2. 数组划分
    将数组中的元素划分成三部分,左边<中位数的部分;右边>中位数的部分;中间等于中位数的部分

  3. 填入
    对于中位数左边的部分:从右向左往偶数位置填入(注意索引是从0开始的)。
    对于中位数右边的部分:从左向右往往奇数位置填入
    最后将所有中位数填入剩下位置

        e.g. 1 2 3 1 2 3 2 
        1、排序
                1 1 2 2 2 3 3 
        2、对半划分
                保留所有中位数2 2 2     左部分 1 1              右部分  3 3 
                                          ---->                   <----
        3、填入
               |0|1|2|3|4|5|6|
               |—|—|—|—|—|—|—|
               |*|*|*|*|*|*|*|
    
           a、填左半部分——————从右向左往偶数位置填入    
               |0|1|2|3|4|5|6|
               |—|—|—|—|—|—|—|
               |*|*|*|*|1|*|1|
           b、填右半部分——————从左往右向奇数位置填入
               |0|1|2|3|4|5|6|
               |—|—|—|—|—|—|—|
               |*|3|*|3|1|*|1|
           c、剩下位置填入所有中位数
               |0|1|2|3|4|5|6|
               |—|—|—|—|—|—|—|
               |2|3|2|3|1|2|1|

        4、故最后的解为 2 3 2 3 1 2 1

注:反过来,从左向右往偶数位置填左半部分;从右向左往奇数位置填右半部分是不可以的。这样在中位数个数占据数组一半的时候,会在数组中间位置出现两个连续的空缺,最后将中位数填进去不符合要求。详细原因见后文正确性分析

伪代码————

naiveWiggleSort2(A[0...n])
        sort(A[0...n]);
        auxA = A;
        int pivot = auxA[(n+2)/1];
        int odd = -1;
        int even = (n%2 == 0) ? n+2 : n+1;
        for(auto ele : auxA)
                if (ele < pivot)
                        even-=2;
                        A[even] = ele;
                else if (ele > pivot)
                        odd+=2;
                        A[odd] = ele;

        //A中未填入位置全部填pivot,即[0,odd]之间的偶数位置,[even,n]之间的奇数位置;
        for(int i = 0; i < even & i < odd; i = i+2)
                A[i] = pivot;
        for(int i = (n%2 == 0) ? n-1 : n ; i > even & i > odd ; i = i-2)
                A[i] = pivot;

正确性说明————注意,本文中默认数组索引从0开始(偶数开始的)
根据题目描述,我们发现偶数位置的值都是小于其相邻位置(奇数位置)的值的。

所以将数组分成三部分:小于中位数的,大于中位数的,等于中位数的。

我们往奇数位置填入大于中位数的值,往偶数位置填入小于中位数的值,显然,此时任何已经填入的位置其相邻的奇偶位置都是符合题目要求的。

最后将中位数填入剩下位置。如果剩下位置是奇数位置,其左右侧都是小于中位数的,符合要求;如果剩下位置是偶数位置,那么其左右侧都是大于中位数的,符合要求。

问题来了:如果最后剩下两个连续的空缺呢?
:在问题有解的前提下,不可能剩余两个连续的空缺的。因为我们的填入是从两头进行的,如果出现连续的两个空缺说明什么问题呢?说明空缺肯定出现在数组的中间位置,也就是说明了已经填入的非中位数的元素个数没有中位数多,那么显然该中情况下是没有解的。另外由于索引是从0开始的,所以左边第一个填入的位置是1号位置,这样在中位数个数是整个数组一半(数组长度为奇数的时候,是一半多半个,譬如数组长度为5的时候,中位数个数是3)的时候,从左往右填入,与从右往左填入恰好相连,恰好没有连续空缺。读者可以手动使用1 1 2 2 2 2 3 3试一试两种填入方法,体会上述描述。

综上,在三轮填入后,数组仍然符合题目要求,即得到最终解。

复杂度分析————
主要的时间复杂度来自于排序,所以是O(nlgn);;另外由于借助了一个辅助数组,所以空间复杂度是O(n)。
根据上面的正确性分析,我们知道只要能够将数组划分成三部分就可以了,不需要排序,所以只需要找到中位数就可以了,该操作可以在O(n)时间内实现,读者可以自己搜索Top-K问题。所以此时算法可以在O(n)时间复杂度实现,空间复杂度还是O(n)。
更进一步,希望能够直接在遍历数组的过程中,原址排列。这样就能够实现时间复杂度为O(n),空间复杂度为O(1)的算法,这就是后面我们重点描述的改进算法***。并且会由此衍生到快排的PARTITION过程,荷兰国旗问题。同时针对这类问题,给出一种统一的思考模式***

朴素的想法2衍生出去的另一做法

详见https://blog.csdn.net/LemintC/article/details/98847743

一些其他总结

详见https://blog.csdn.net/LemintC/article/details/98847743

能否通过同样的衍生方法修改朴素的想法1?

不可以,因为朴素想法1将数组按照排序后的位置,划分成等长的两部分,如果存在重复元素,譬如上述实例中的元素2,其可能既属于左半部分,又属于右半部分,我们无法在遍历过程中遭遇2的时候判断,其应该划分为哪个部分。
总的说来:朴素算法1,是根据数组的长度与元素的位置来划分数组的。而朴素算法1是根据元素的值来划分数组的。所以朴素算法1必须排序后根据元素位置划分,而朴素算法2则可以直接在遍历过程中,根据访问的元素的值来完成划分的同时,将元素填入正确位置。

参考

https://blog.csdn.net/LemintC/article/details/98847743
https://blog.csdn.net/LemintC/article/details/94549317

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值