一朋友面试的时候被问到了STL里的sort函数,被怼到怀疑人生,我听了那些问题发现也不会,研究了好久,网上也没有详细解释的,今天突然灵感爆发,想明白了几个问题
可能有的人会觉得sort这么简单, 有什么好问的, 那你可以看看如下几个问题你能否答得上来
实际上,STL中的sort是一种混合排序,它应用了快速排序、堆排序和插入排序,以下是各个排序应用时的情况:
当你回答出这些时,面试官就可以继续深入问下去了
这个问题我一般都这么回答:快速排序是基于分治法的一种排序法,对于整个排序区间,先找一个枢纽,比如这个区间的第一个数,然后把比这个枢纽小的数放左边,比枢纽大的放右边,对枢纽的左右两个区间进行刚刚相同的步骤,当区间长度都为1时,就排好序了
时间复杂度为nlogn,空间复杂度为logn
快速排序最差复杂度为n^2
快速排序优化就只有一个地方,那就时枢纽的选择,优化方法有两种,一种是在区间内随机选取一个枢纽,第二种就是STL中sort快速排序部分使用的优化:取区间第一个数,中间的数以及最后一个数的中位数作为枢纽
实际上这个操作在STL里有个函数可以专门实现——partition函数,它的复杂度是o(n)的,它的实现过程如下:
取区间第一个数,中间的数以及最后一个数的中位数作为枢纽,把该枢纽与区间第一个数位置交换一下,用一个临时变量储存枢纽,然后使用双指针的思想,i 指针指向区间第一个数,j 指针指向区间最后一个数,接下来有两个操作
交替重复以上两个操作,当i和j指针相遇时,把枢纽放入相遇位置就行了
这个时候就不要再回答为了防止快速排序退化了,其实面试官想问的是nlogn复杂度的排序算法还有比如归并排序,那为什么要选择堆排序?
这个时候就要从空间复杂度上回答了,堆排序是可以原地实现的,空间复杂度为o(1),而归并排序空间复杂度为o(n)
这时候就不要说什么用堆实现,不要就讲一下堆的结构什么的,面试官都问具体实现了,那么建堆操作也是要具体讲清楚的,不多解释,直接上代码
//代码来自https://github.com/huihut/interview/blob/master/Algorithm/HeapSort.cpp
#include
#include
using namespace std;
// 堆排序:(最大堆,有序区)。从堆顶把根卸出来放在有序区之前,再恢复堆。
void max_heapify(int arr[], int start, int end) {
//建立父節點指標和子節點指標
int dad = start;
int son = dad * 2 + 1; //它数组从0开始,所以堆中父亲左右儿子是dad*2+1和dad*2+2
while (son <= end) { //若子節點指標在範圍內才做比較
if (son + 1 <= end && arr[son] < arr[son + 1]) //先比較兩個子節點大小,選擇最大的
son++;
if (arr[dad] > arr[son]) //如果父節點大於子節點代表調整完畢,直接跳出函數
return;
else { //否則交換父子內容再繼續子節點和孫節點比較
swap(arr[dad], arr[son]);
dad = son;
son = dad * 2 + 1;
}
}
}
void heap_sort(int arr[], int len) {
//初始化,i從最後一個父節點開始調整(就是从叶子的父亲开始调整)
for (int i = len / 2 - 1; i >= 0; i--)
max_heapify(arr, i, len - 1);
//先將第一個元素和已经排好的元素前一位做交換,再從新調整(刚调整的元素之前的元素),直到排序完畢(想到于不断地把堆顶取出来放后面,类似选择排序的过程,只不过用堆进行了优化)
for (int i = len - 1; i > 0; i--) {
swap(arr[0], arr[i]);
max_heapify(arr, 0, i - 1);
}
}
int main() {
int arr[] = { 3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6 };
int len = (int) sizeof(arr) / sizeof(*arr);
heap_sort(arr, len);
for (int i = 0; i < len; i++)
cout << arr[i] << ' ';
cout << endl;
return 0;
}
为什么面试官会这么问呢?因为他已经问了你堆排序和快速排序的实现,这个时候你对他们的时空复杂度已经很清楚了,这时候你意识到其实堆排序的空间复杂度比快速排序还要低(堆排序o(1),快速排序o(logn) ),而且堆排序还不会退化。
所以面试官想知道为什么快速排序效率要比堆排序高?
这个问题如果没去了解过,真的很难回答
实际上我们学习算法,研究的都是理论复杂度,而实际工程应用时,还会考虑算法实际的效率,这不仅涉及到算法的理论复杂度,还涉及到硬件、操作系统等问题
而快速排序就是一个典型的例子
同样是nlogn的时间复杂度,为什么堆排序和归并排序整体效率不如快速排序呢?这是因为计算机硬件中有一个高速缓存区(cache),它的访问读取速度非常快(比内存还要快很多),它通常集成在CPU上,所以其容量十分有限,通常CPU会把经常访问到的数暂存到缓存里,CPU找数据也会先从缓存里找。
快速排序因为其用到了一个枢纽,这个枢纽的访问次数非常之多,那么其就会被放入缓存中,那么访问枢纽的效率就会非常高,所以快速排序整体效率会比其他几个排序要高。
当数列大致有序时,比如我们现在每个区间的大小已经排好了,但是区间内16个数字还没有排好,这时候插入排序的表现会更好。
但是为什么呢?插入排序的复杂度为n^2,即使区间长度比较小,但是其复杂度并不会因此降低啊,这个问题困扰了我很久,但是突然有一天我想明白了
插入排序复杂度为n^2,那我们考虑最坏情况,每个区间的复杂度都是
15 + 14 + … … + 1 + 0 = 15 ∗ ( 15 + 1 ) / 2 … … … … … … … … … … … … ( 1 ) 15 + 14 + …… + 1 + 0 = 15 * (15 + 1)/2………………………………(1) 15+14+……+1+0=15∗(15+1)/2………………………………(1)
即插入排序移动次数之和
那么这时候其实就只有 n / 16个大小为16的区间,那么实际上用插入排序 排序这n / 16个区间的复杂度为
( 1 ) ∗ n / 16 = 7.5 n (1) * n / 16 = 7.5n (1)∗n/16=7.5n
平均复杂度假设是最坏复杂度的一半,即
7.5 n / 2 = 3.25 n … … … … … … … … … … … … ( 2 ) 7.5n / 2 = 3.25n………………………………(2) 7.5n/2=3.25n………………………………(2)
再回到快速排序部分,按一般情况,递归到区间长度为16时候的复杂度为
n ∗ ( l o g n − l o g 16 ) … … … … … … … … … … … … ( 3 ) n*(logn - log16)………………………………(3) n∗(logn−log16)………………………………(3)
如果n比较大,(3)可以约等于nlogn
那么总的复杂度就是
( 2 ) + ( 3 ) = ( − 0.75 + l o g n ) ∗ n … … … … … … … … … … … … ( 4 ) (2) + (3) = (-0.75 + logn)*n………………………………(4) (2)+(3)=(−0.75+logn)∗n………………………………(4)
是不是有点意想不到?以上推导很多博客都只是说一笔略过,说什么插入排序在大致有序的情况下效率更高,但是为什么却没有讲,很多东西不清楚原理,面试的时候就很容易露馅了
没怎么接触工程的时候,我们总是仅仅考虑算法的理论复杂度,实际上,理论复杂度往往只是工程代码设计上的一个参考,要考虑整体效率,往往还要考虑诸如计算机硬件、编译器、操作系统等,再对算法进行优化