问题描述
给定一个数组A[0…n],重新排列数组,使得有:
A[0] < A[1] > A[2] < A[3] …
注:默认给出的数据一定有解。
注2:上述< 以及 > 不具有传递性。
朴素的想法1
- 排序
- 对半划分
-若数组长度为偶数,直接对半划分
-若数组长度为奇数,将最小的元素放置到输出数组的最后,剩下的部分均匀划分为左右两部分 - 按照降序,依次从左右两部分中选取元素,顺序填入输出数组中
e.g.
A={1 2 3 1 2 3 2}
- 排序
1 1 2 2 2 3 3 - 对半划分
1 2 2 | 2 3 3 |1
<----- | <---- - 依次填入
_ _ _ _ _ _ 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
-
排序
-
数组划分
将数组中的元素划分成三部分,左边<中位数的部分;右边>中位数的部分;中间等于中位数的部分 -
填入
对于中位数左边的部分:从右向左往偶数位置填入(注意索引是从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