全网最简洁的快速排序和寻找第K大元素

Posted 纵横千里,捭阖四方

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了全网最简洁的快速排序和寻找第K大元素相关的知识,希望对你有一定的参考价值。

快速排序是很多人面试的梦魇,例如有个小伙伴去美图面试,技术面已经过了,到了算法环节,面试官直接这么出题:

然后不出意外,不出所料,这位小伙伴直接走人了。这个题怎么的难吗?快速排序很多人都知道怎么回事,但是为什么写不出来具体实现,更不能针对这个题的要求做修改呢?网上能找到一堆的材料,说得非常复杂,写的时候可能会定义多个方法, 我估计大部分人看的欲望都没有。我们今天就给出一种最简洁的实现方法。

1. 快速排序就是双指针+二叉树的前序遍历

快速排序也是我们在算法书里面认识的老朋友了。我们在《一维数组》一章提到过”双指针思路“:在处理奇偶等情况时会使用两个游标,一个从前向后,一个是从后向前来比较,根据结果来决定继续移动还是停止等待。快速排序的每一轮进行的时候都是类似的双指针策略,而递归的过程本质上就是二叉树的前序递归调用。

快速排序是将分治法运用到排序问题的典型例子,基本思想是:通过一个标记pivot元素将n个元素的序列划分为左右两个子序列left和right,其中left中的元素都比pivot小,right的都比pivot的大,然后再次堆left和right各自再执行快速排序,在将左右子序列排好序之后,整个序列就有序了。这里排序进行左右划分的时候是一直划分到子序列只包含一个元素的情况,然后再递归返回。

我们以关键字序列26,53,48,15,13,48,32,15看一下一次划分的过程:

上面红框位置表示当前已经被赋值给了pivot或者其他位置,可以空出来放移动来的新元素了。我们可以看到26最终被放到了属于自己的位置上,不会再变化。而左侧的都比15小,左侧都比15大,因此26的左右两侧可以分别再进行排序。

这一轮过程是什么呢?就是数组增删的时候经常用的双指针策略,我们在数组部分讲过,不再赘述。而这里的每一轮都是一个相向的双指针而已,没有任何神秘的。

至于实现,网上有很多,类型千差万别,有的还定义了多个方法,一看就复杂,基本不太可能掌握,我认为下面这种方式最简单、最直接了,代码如下:

 void quickSort(int[] array, int start, int end) 
        if (start >= end) 
            return;
        
        //这里就是一个相向的双指针操作
        int left = start, right = end;
        int pivot = array[(start + end) / 2];
        
        while (left <= right) 
            while (left <= right && array[left] < pivot) 
                left++;
            
            while (left <= right && array[right] > pivot) 
                right--;
            
            if (left <= right) 
                int temp = array[left];
                array[left] = array[right];
                array[right] = temp;
                left++;
                right--;
            
        
        //先处理元素再分别递归处理两侧分支,与二叉树的前序遍历非常像
        quickSort(array, start, right);
        quickSort(array, left, end);   
    

是不是比很多一坨一坨的代码要简洁很多?

测试代码:

 public static void main(String[] args) 
        int[] array = 6, 3, 2, 1, 4, 5, 8, 7;
        quickSort(array, 0, array.length - 1);
        System.out.println(Arrays.toString(array));
    

2.数组中第K大的数字

我们在堆部分分析过这个问题,这里看看如何基于快速排序来做,这个题目出现的频率非常高,甚至在很多时候,面试官直接要求基于快速排序来解决这个问题。而且我们要直接改造一下上面的快排来解决,而不是另起炉灶,只有这样平时的练习才有效果。

为什么能用快速排序来解决呢?我们还是看上面排序的序列:26,53,48,15,13,48,32,15

我们第一次选择了26为哨兵,进行一轮的排序过程为:

 

上面红框位置表示当前已经被赋值给了pivot或者其他位置,可以空出来放移动来的新元素了。我们可以看到26最终被放到了属于自己的位置上,不会再变化,而26的左右两侧可以分别再进行排序。

这里还有一个关键信息, 我们可以知道26的索引为3,所以递增排序之后26一定是第4大的元素 。这就是解决本问题的关键,既然知道26是第4大,那如果我要找第2大,一定是要到右边找。如果要找第6大,一定要到左边找(当然,如果降序排序就反过来了),而不需要的那部分就不用管了。这就是为什么能用快速排序解决这个问题。

我们仍然采用升序排列,这样就可以直接改造上面的快速排序的代码来。这里参数k只要变成array.length - k + 1即可。

public  int quickSort(int[] array, int start, int end, int k) 
        if (start == end) 
            return array[start];
        
         //这里和上面快排的完全一样,
        int left = start, right = end, pivot = array[(start + end) >> 1];
        while (left < right) 
            while (left <= right && array[left] < pivot) 
                left++;
            
            while (left <= right && array[right] > pivot) 
                right--;
            
            if (left < right) 
                int temp = array[left];
                array[left] = array[right];
                array[right] = temp;
                left++;
                right--;
            
        
        //这里注意两点:1.不用所有区间都递归,只递归我们需要的部分即可
        //2.有些区间可以不处理,但是长度必须要算 ,也就是k的值要适时调整
        int cnt = right - start + 1;
        if (k <= cnt) 
           return quickSort(array, start, right, k);
        else
           return quickSort(array, right + 1, end, k - cnt);
        
    

给一个测试方法:

public static void main(String[] args) 
        int[] array = 6, 3, 2, 4, 5, 8, 7;
        int k = 2;//找第二大元素
        int n = array.length - k + 1;
        System.out.println(quickSort(array, 0, array.length - 1, n));
    

我们尽量能套用已经练习过的代码,否则核心逻辑都要考虑相反情况,会让我们面试时现场写变得非常困难。

以上是关于全网最简洁的快速排序和寻找第K大元素的主要内容,如果未能解决你的问题,请参考以下文章

寻找数组中第K大的元素

有序数组寻找中位数以及寻找K大元素

寻找第K大 —— 快排思想 / 堆排思想

寻找第K大 —— 快排思想 / 堆排思想

寻找第K大 —— 快排思想 / 堆排思想

寻找第K大 —— 快排思想 / 堆排思想