10

今天看到 V2EX 上有人讨论 社招还会问 “请手写选择排序算法” 吗?,看来还是有很多人关心的。结合自己最近面试的经历,我可以明确的告诉大家,类似这种问题,只要你的工作经验小于 10 年,基本上逃不掉。劝大家不如抽点时间早做准备。

简式快排

面试中遇到问快排的,如上面那个帖子中的情况。你就可以上一份简式快排了,何谓简式?最短的代码表述快排的思想。

快排的思想,实质是分治法。基于什么来分?找一个支点来分,通常称之为 pivot, 而这个分的过程称之为 partition, 基于以上两点,我们用递归的方式描述快排:

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);
    }
}

如何?简单吧。有人说面试的时候手写快排,如果提前没有背下来的话,肯定歇菜。我不认为这样基础的算法是需要背的,上面这个递归,如此简洁,如此美,真的需要硬记?

有人说,这个好理解,关键在于 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;
}

这算法用白话说,就是从头到尾迭代,和支点比较,大的不管,小的换。换了才往后看。最后支点戳中间,算是分界线。


3,7,8,5,2,1,9,5,4

这么来一下,就成了:

3,2,1,4,7,8,9,5,5
^^^^^ | ^^^^^^^^^

然后同样的手法分别解决两边,这样递归的解下去。

来份三明治

其实上面的那份,已经可以解决面试中的问题了。但其实有很大的缺陷,如当所有元素都相同的情况下,partition 将一直返回 r, 递归的深度高达 N,每一次递归中 partition 又循环 N 次,时间复杂度直接飙到了 O(N^2). 这显然非常的不值当。

于是我们觉得分两份太粗,分三份试试?

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 选取的改变,基于上述代码,改造起来是非常容易的。这里就不做过多实现,留作感兴趣者自己练习吧。


上述三道菜,基本能够解决面试中可能遇到的种种情况了。让面试官吃饱,很有必要~


pezy
3.1k 声望332 粉丝

一个 C++ 程序员