面试官:来,这位精神小伙,简简单单写个快速排序吧

点赞再看,养成习惯,您动动手指对原创作者意义非凡
备战2021秋招面试 微信搜索【CodePan】关注更多新鲜好文和互联网大厂的笔经面经。
作者@试问道

面试现场,终于到了紧张刺激的手撕代码环节,你忐忑的心情随着考官的一句话归于平静。
“ 给 你 二 十 分 钟 , 你 就 写 个 快 排 吧 ” {\color{red}“给你二十分钟,你就写个快排吧”}
你简直不敢相信眼前这个穿着格子衬衫,牛仔裤角有些发白,头发在风中有些凌乱但仍然难掩老程序员气质的面试官竟然问出这么水的问题,你有些诧异的眼神瞥了一下他,他只是推了推镜框,厚厚的镜片埋下的眼睛连正眼都没有看你。你压抑住自己兴奋激动的心情,“老子早写了八百遍了”,但还是装出若有所思的样子,大约过了十分钟,你把代码甩到了他的脸上,不好意思,这是在梦里, 现实中你恭恭敬敬地交出了如下代码,并幻想着面试官向你投来赞许的目光,后生可畏呀:

面试官:来,这位精神小伙,简简单单写个快速排序吧_第1张图片

眼看面试官一愣,门口的保安拿着对讲机已经蠢蠢欲动,另外一只手隐隐约约拿了样东西,像是砖头,但比砖头略大。你迟疑了几秒,“不好意思不好意思,拿错了,应该是这个“,你挠了挠头,抱歉的样子还是一如往常帅气逼人。

void QuickSort_Core(vector<int> &A, int Left, int Right){
    if(Left>=Right) {return;}
    int pivot = A[0]; 
    int i=Left;
    int j=Right-1;
    while(1){
        while(A[++i] < pivot){}   //从左往右扫,碰到大于pivot的值,就记录下来
        while(A[--j] > pivot){}   //从右往左扫,碰到小于pivot的值,就记录下来
        if(i<j){        //如果没有扫完整个数组就停下,说明有值需要交换
            swap(A[i],A[j]);        
        }
        else
        {
            break;
        }
    }
    swap(A[i],A[Right-1]);  //把pivot插入指针i,j相遇点
    QuickSort_Core(A,Left,i-1);
    QuickSort_Core(A,i+1,Right);
}
int main(){
    vector<int> A = {};
    int N = A.size();
    Quick_Sort(A,0,N-1);
    cout<<"{"; 
    return 0;
}

你以为自己大功告成,结果面试官追问:
“ 简 单 说 下 它 的 算 法 思 想 吧 ” {\color{red}“简单说下它的算法思想吧”}

”快排采用的是分治的策略,根据选定的pivot,即主元,通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,就可以达到整个数据变成有序序列。“

你的回答熟练的让人心疼,但是面试官好像并没有什么回应。

“ 你 简 单 分 析 下 这 个 快 速 排 序 的 时 间 、 空 间 复 杂 度 吧 ” {\color{red}“你简单分析下这个快速排序的时间、空间复杂度吧”}

意料之中,ez。

”是这样的,快速排序是现实应用中最快的排序算法,它的时间复杂度可以达到 O ( N log ⁡ N ) O(N\log N) O(NlogN),这是因为它采用了“分而治之”的思想。同时需要 O ( log ⁡ N ) O(\log N) O(logN)的空间复杂度,这是他递归调用需要的额外栈空间。“

回答完这道,面试官终于抬起了头开始正眼看你啦,你觉得自己的自信心达到了新的高度。至此,一切还在你的掌控之中。

面试官又扶了扶眼镜,问道:
“ 那 你 说 下 你 写 的 这 个 代 码 最 坏 时 间 复 杂 度 是 多 少 , {\color{red}“那你说下你写的这个代码最坏时间复杂度是多少,} 也 是 O ( N log ⁡ N ) 吗 ” {\color{red}也是O(N\log N)吗”} O(NlogN)

你开始碰到了之前没碰到的问题了,有点慌,但是应场能力还是可以的,你只是用余光警惕地瞟了一眼门口的保安,然后赶紧回答道:
”我觉得我写的这个代码 特别完美一不小心咋还说出心里话了捏 在处理本身就是有序的数组时可能会重复排序递归的子数组。“
“ 那 应 该 是 多 少 呢 ” {\color{red}“那应该是多少呢”}
你开始有了一丝慌张, O . . . O ( N 2 ) O...O(N^2) O...O(N2)?,你不是很自信的答道。
“ 那 你 能 分 析 一 下 , 为 什 么 吗 ? ” {\color{red}“那你能分析一下,为什么吗?”}
“emmmmm那个,应该是嵌套递归相关,emmm”,

幸运的是,老练的面试官还是给了你一些帮助,你踉踉跄跄终于写出如下推导:
假设我递归的时候,每次都选取数组第一个值作为pivot,那么如果一个数组如果本身就是有序的,比如1到N,我选取1做pivot,其他数和他比较分为两个子集,再递归,但是这里,化成的子集就是2到N,又重复,(近似等于没有分治)。

面试官:来,这位精神小伙,简简单单写个快速排序吧_第2张图片
推导式可以写成,

T ( N ) = O ( N ) + T ( N − 1 ) T(N) = O(N) + T(N-1) T(N)=O(N)+T(N1) = O ( N ) + O ( N − 1 ) + T ( N − 2 ) = . . . =O(N)+O(N-1) +T(N-2)=... =O(N)+O(N1)+T(N2)=... = O ( N 2 ) =O(N^2) =O(N2)

好,面试官看你踉踉跄跄答出来了,于是决定还能再 虐虐你 多交流一下,于是问道, “ 那 你 能 改 进 一 下 这 个 代 码 尽 量 规 避 这 个 问 题 吗 ? ” {\color{red}“那你能改进一下这个代码尽量规避这个问题吗?”}
你知道一定是和pivot的选取有关,所以第一个念头就是,随机数。
“可以在数组中,随机取一个吧” 你答道,
“ 可 是 引 入 随 机 数 函 数 的 过 程 又 会 引 来 更 多 的 复 杂 度 呀 。 {\color{red}“可是引入随机数函数的过程又会引来更多的复杂度呀。}
看你面露难色,面试官又说道,
事 实 上 你 可 以 通 过 取 数 组 首 、 尾 、 中 的 中 位 数 的 方 式 {\color{red}事实上你可以通过取数组首、尾、中的中位数的方式} 得 到 p i v o t , 这 样 就 不 会 出 现 选 中 最 小 值 的 情 况 , {\color{red}得到pivot,这样就不会出现选中最小值的情况,} pivot 你 可 以 实 现 一 下 代 码 吗 ? ” {\color{red}你可以实现一下代码吗?”}
你有了思路,就开始写起了代码,经过一段时间的苦思冥想,你得到如下的pivot选取办法,

int Median3(vector<int> &A, int Left, int Right){
    int center = (Left+Right)/2;
    if(A[Left] > A[center]){        //两两比较,将三个数排序
        swap(A[Left],A[center]);
    }
    if(A[center] > A[Right]){
        swap(A[Right],A[center]);
    }
    if(A[Left] > A[center]){
        swap(A[Left],A[center]);
    }
    swap(A[center],A[Right-1]);    //取出中间那个并和最右边倒数第二个调换位置,这样只需要考虑Left+1到Right-2之间的排序
    return A[Right-1];  //返回值当作pivot
}

你甚至耍了点小聪明,把数组的前一个和后两个都一起排序好了。但是,到了现在,快排问法的多种多样已经让你始料未及。

“ 不 错 , 那 你 回 答 一 下 如 果 和 p i v o t 比 较 过 程 中 , {\color{red}“不错,那你回答一下如果和pivot比较过程中,} pivot 如 果 出 现 相 等 的 情 况 怎 么 办 呢 ? 可 以 直 接 跳 过 吗 ” {\color{red}如果出现相等的情况怎么办呢?可以直接跳过吗”}

你认为如果比较的话会有很多多余的操作,所以你回答应该跳过。

” 你 再 想 想 , 是 这 样 吗 ? ” {\color{red}”你再想想,是这样吗?”}

到这里,你已经明显感觉到压力。

是 这 样 的 , 不 停 , 直 接 跳 过 确 实 可 以 避 免 不 必 要 的 比 较 操 作 , {\color{red}是这样的,不停,直接跳过确实可以避免不必要的比较操作,} 但 是 你 想 象 如 果 是 给 一 个 全 为 1 的 数 组 排 序 , 最 后 你 的 两 个 {\color{red}但是你想象如果是给一个全为1的数组排序,最后你的两个} 1 指 针 会 相 遇 在 数 组 尾 端 边 缘 , 也 就 是 p i v o t 在 那 , 这 就 和 你 {\color{red}指针会相遇在数组尾端边缘,也就是pivot在那,这就和你} pivot 选 定 p i v o t 在 第 一 个 元 素 那 没 区 别 了 , 还 是 O ( N 2 ) 。 但 是 , {\color{red}选定pivot在第一个元素那没区别了,还是O(N^2)。但是,} pivotO(N2) 如 果 你 停 下 来 比 较 的 话 , 虽 然 耗 费 一 点 运 算 时 间 , {\color{red}如果你停下来比较的话,虽然耗费一点运算时间,} 但 是 却 能 使 你 从 两 边 扫 过 来 的 指 针 相 遇 在 大 致 中 间 的 位 置 , {\color{red}但是却能使你从两边扫过来的指针相遇在大致中间的位置,} 使 这 样 又 可 以 分 治 , 复 杂 度 可 以 又 达 到 O ( N log ⁡ N ) 。 ” {\color{red}这样又可以分治,复杂度可以又达到O(N\log N)。”} O(NlogN)

这时候,你觉得面试官的格子衫和牛仔裤是那么迷人,连隐隐可见的头油都显得光彩夺目,发出关爱智障的光。

那 好 , 接 下 来 最 后 一 个 问 题 吧 , 你 说 说 快 速 排 序 有 什 么 弊 端 ? {\color{red}那好,接下来最后一个问题吧,你说说快速排序有什么弊端?}

你收起了你的痴汉脸,赶紧端正了下,答道:

“快速排序的问题可能就在于它是使用递归的,有的时候效率比较低”

什 么 时 候 呢 ? {\color{red}什么时候呢?}

“因为它是要占用大量系统的堆栈,有很多push和pop的操作,如果数据规模不大的话,是不需要递归的,可以直接用简单排序,比如插入排序就可以了。”

说罢,你擦了擦汗,看着快排已经问了将近一个小时,你似乎对快排有了新的理解。

不 错 , 那 你 能 写 下 完 整 的 代 码 吗 ? {\color{red}不错,那你能写下完整的代码吗?}

???说好的最后一个问题呢???

面试官:来,这位精神小伙,简简单单写个快速排序吧_第3张图片

为了读者的一个赞 呸, 为了这个offer,拼了。于是,你又花了些时间,以上面的代码为基础,得到了完整版快速排序,这里的threshold就是你设立的阈值,数组低于这个值时,就可以用插入排序了。

#include 
#include 

using namespace std;
void swap(int &A,int &B){
    int temp = B;
    B = A;
    A = temp;
}

int Median3(vector<int> &A, int Left, int Right){
    int center = (Left+Right)/2;
    if(A[Left] > A[center]){        //两两比较,将三个数排序
        swap(A[Left],A[center]);
    }
    if(A[center] > A[Right]){
        swap(A[Right],A[center]);
    }
    if(A[Left] > A[center]){
        swap(A[Left],A[center]);
    }
    swap(A[center],A[Right-1]);    //取出中间那个并和最右边倒数第二个调换位置,这样只需要考虑Left+1到Right-2之间的排序
    return A[Right-1];
}

void QuickSort_Core(vector<int> &A, int Left, int Right){
	if(Right-Left>=threshold){   //满足就快排
		if(Left>=Right) {return;}
	    int pivot = Median3(A, Left, Right); //此时数组的最左边和最右边的值已经确定,不需要再考虑
	    int i=Left;
	    int j=Right-1;
	    while(1){
	        while(A[++i] < pivot){}   //从左往右扫,碰到大于pivot的值,就记录下来
	        while(A[--j] > pivot){}   //从右往左扫,碰到小于pivot的值,就记录下来
	        if(i<j){        //如果没有扫完整个数组就停下,说明有值需要交换
	            swap(A[i],A[j]);        
	        }
	        else
	        {
	            break;
	        }
	    }
	    swap(A[i],A[Right-1]);  //把pivot插入指针i,j相遇点
	    QuickSort_Core(A,Left,i-1);
	    QuickSort_Core(A,i+1,Right);
	}
	
	void Quick_Sort(vector<int> &A, int N){
	    QuickSort_Core(A, 0, N-1);
	}
	else{   //不满足就插入排序
		Insert_Sort(A[remainingpart],Right-Left+1);
	}   
}

int main(){
    vector<int> A = {};
    int N = A.size();
    Quick_Sort(A,N);
    cout<<"{";
    for(auto e:A){
        cout<<e<<",";
}
    cout<<"}";    

    return 0;
}

” 其 实 还 有 很 多 问 题 没 有 问 到 , {\color{red}”其实还有很多问题没有问到,} 但 是 时 间 有 限 , 面 试 就 到 这 里 吧 “ {\color{red}但是时间有限,面试就到这里吧“}

看着写满C++代码的白纸,你长舒了一口气,起身离开。

经过门口时,保安向你会心一笑,默默放下了手中比砖头还厚的《Java核心技术》,喃喃说道:Java才是世界上最好的编程语言



回到学校,同学问你面试如何,

“没啥,面了个快排。”
“那么简单?这波血赚。”


创作不易,你的鼓励是我创作的动力,如果你有收获,点个赞吧

你可能感兴趣的:(数据结构与算法,算法,快速排序,排序算法,数据结构,面试)