面试被问到这个问题然后一时没想到什么好方法……
收集了一下网上的做法:排序、堆、快排。
排序
排序自然就不用说了,nlogn的算法随便选一个,然后取中间位置的值。
堆
堆的做法不是简单的堆排序,它先将前一半(len+2>>1)的元素进行堆排序(小根堆),然后后一半的元素逐个和堆顶比较,如果更小则舍弃,如果较大则替换为堆顶并调整堆。这样可以保证堆里剩下的是数组中的较大值。对于奇数长度的数组来说,直接取堆顶即可;对于偶数长度的数组,取堆顶和它较小的子节点的平均值。
快排
快速排序的思想一直没记住,正好借这个机会记录一下。而且这好像是一种比较好写的时间复杂度较低的方法(平均O(n))。
快速排序的思想是,每次确定一个元素的位置,并且将数组在这个元素的位置分割成两部分,一侧全大于等于该元素,另一侧全小于等于该元素。
具体的方法是,在当前数组中随机取一个数p,然后将它交换到第0个元素,然后用首尾两个指针遍历数组。尾部的指针先走,直到遇到第一个不大于p的值。然后首部的指针再走,直到遇到第一个不小于p的值。此时,将两个指针指向的数交换。上述过程一直进行到两个指针相遇。这时将指针指向的数和第0个元素交换,然后以这个位置为分界点,分出两个子数组,对子数组进行快速排序。
上述过程强调了尾部指针先走,为什么不能首部指针先走?
考虑指针相遇的条件,要么是完成一次交换后,先行的指针找不到满足要求的数,直到和另一个指针会合,要么是先行的指针找到满足条件的数之后,后行的指针找不到满足要求的数,和它会合。对于先行指针是尾部指针的情况而言,两种情况都会找到小于p的数。因此,就可以把p和它交换以使前半部分全部小于p而后半部分全部大于p。
如果首部指针先走,那么就会导致指针相遇时找到的数是大于p的数,这样就不能使结果满足快排的要求。
当然,如果是选择将p和结尾的数交换,那么就要让首部指针先走。总之就是把p放在哪边,另一边的指针就先走。
以上就是朴素的快速排序的方法了。贴个代码,用洛谷1177排序模板的数据测的。看运行时间后两个测试点应该是卡了重复数据。对重复数据做了一些处理,没有其它的优化。
void quick_sort(vector<int>& arr, int l, int r)
{
if (l >= r)
return;
int p = rand() % (r - l + 1) + l;
if (r != p)
swap(arr[p], arr[r]);
int ls = l;
int rs = r;
rs = rs;
while (ls != rs)
{
while (ls != rs && arr[ls] <= arr[r])
++ls;
while (ls != rs && arr[rs] >= arr[r])
--rs;
if (ls != rs)
swap(arr[ls], arr[rs]);
}
swap(arr[r], arr[ls]);
int temp=arr[ls];
while(ls>l&&arr[ls]==temp)--ls;
while(rs<r&&arr[rs]==temp)++rs;
quick_sort(arr, l, ls);
quick_sort(arr, rs, r);
}
回到中位数的问题。快速排序每一步都能确定一个数出现的位置,那么当确定一个数的是在数组的一半位置时,就说明它是中位数。相对于快速排序而言,找中位数不需要对两个子数组都进行处理,只需要处理其中一个子数组即可。
在平均情况下,每次进行上述操作,数组的长度缩小一半,这样来说就需要logn次的递归,每次便利的长度减半,求和的结果就是2n,所以平均复杂度是O(n)。但它的时间消耗不稳定,最差情况和快排一样是平方级别。