今天看到 V2EX 上有人讨论 社招还会问 “请手写选择排序算法” 吗?,看来仍是有不少人关心的。结合本身最近面试的经历,我能够明确的告诉你们,相似这种问题,只要你的工做经验小于 10 年,基本上逃不掉。劝你们不如抽点时间早作准备。面试
面试中遇到问快排的,如上面那个帖子中的状况。你就能够上一份简式快排了,何谓简式?最短的代码表述快排的思想。算法
快排的思想,实质是分治法。基于什么来分?找一个支点来分,一般称之为 pivot, 而这个分的过程称之为 partition, 基于以上两点,咱们用递归的方式描述快排:ui
void quicksort(int arr[], int l, int r) { if (l < r) { int pivot = partition(arr, l, r); quicksort(arr, l, pivot-1); quicksort(arr, pivot+1, r); } }
如何?简单吧。有人说面试的时候手写快排,若是提早没有背下来的话,确定歇菜。我不认为这样基础的算法是须要背的,上面这个递归,如此简洁,如此美,真的须要硬记?code
有人说,这个好理解,关键在于 partition
如何实现。的确,partition
是快排的灵魂。CLRS 里采用了以尾巴为支点的策略,我在这里与其保持一致:排序
int partition(int arr[], int l, int r) { int k = l, pivot = arr[r]; for (int i = l; i < r; ++i) if (arr[i] <= pivot) std::swap(arr[i], arr[k++]); std::swap(arr[k], arr[r]); return k; }
这算法用白话说,就是从头至尾迭代,和支点比较,大的无论,小的换。换了才日后看。最后支点戳中间,算是分界线。递归
如get
3,7,8,5,2,1,9,5,4
这么来一下,就成了:it
3,2,1,4,7,8,9,5,5 ^^^^^ | ^^^^^^^^^
而后一样的手法分别解决两边,这样递归的解下去。io
其实上面的那份,已经能够解决面试中的问题了。但其实有很大的缺陷,如当全部元素都相同的状况下,partition
将一直返回 r
, 递归的深度高达 N
,每一次递归中 partition
又循环 N
次,时间复杂度直接飙到了 O(N^2). 这显然很是的不值当。class
因而咱们以为分两份太粗,分三份试试?
如
5 7 4 3 1 2 6 5 5
也来那么一下,成为:
4 3 1 2 5 5 5 7 6 ^^^^^^^ | | ^^^
这就是三明治的原理了,左边是小于 pivot 的,中间是等于 pivot 的,右边是大于 pivot 的。中间部分不参与递归,分治的是两边。
咱们需稍稍改变下 partition
的实现,显然咱们此次但愿返回的两个支点(左右边界):
// 3-way partition std::pair<int, int> partition(int arr[], int l, int r) { int k = l, p = r; for (int i = l; i < p; ) if (arr[i] < arr[r]) std::swap(arr[i++], arr[k++]); else if (arr[i] == arr[r]) std::swap(arr[i], arr[--p]); else ++i; // move pivots to centre int m = std::min(p-k, r-p+1); for (int i = 0; i < m; ++i) std::swap(arr[k+i], arr[r-i]); return std::make_pair(k, r-p+k); }
而 quicksort
也须要稍稍改变一点:
void quicksort(int arr[], int l, int r) { if (l < r) { auto pivot = partition(arr, l, r); quicksort(arr, l, pivot.first-1); quicksort(arr, pivot.second+1, r); } }
若是在遇到所有元素相同的状况,时间复杂度成功的减小到 O(N). 算是一个突破吧。
其实 CLRS 随后还提到了以随机位置为 pivot 的思路,我称之为东北乱炖。那是只针对 pivot 选取的改变,基于上述代码,改造起来是很是容易的。这里就不作过多实现,留做感兴趣者本身练习吧。
上述三道菜,基本可以解决面试中可能遇到的种种状况了。让面试官吃饱,颇有必要~