快速排序

快速排序思想

快速排序号称20世纪最伟大的十大算法之一,也是nlogn级别的排序算法,它的思想是类似冒泡排序,是一种交换排序,同时加入分治法

1593061994(1).jpg

上图中我们选取待排序数组第一个元素为基准元素,通过比较交换,将比基准元素小的元素放在左边,比基准元素大的放在右边。那么此时基准元素(紫色元素),就放在了最终排序后数组应该在的位置。然后通过同样的方式,将左边(绿色)和右边(橙色)部分排序。过称如下:

1593062585(1).jpg

每轮分成3个步骤:

  1. 选取基准元素
  2. 基准元素方法排好序后的位置
  3. 继续拆分,直到剩下一个待排序元素

如何编码 ?

当知道了思想后,下面就要考虑到编码。不管是算法还是排序中,边界的定义和指针的指向是最重要的两个因素,只有定义好了边界范围和指针的含义才能写好算法。

待排序数组  4 7 6 5 3 2 8 1
算法.png

按照上面的思想,定义一下边界:

  1. 选取待排序数组第一个元素为基准元素4
  2. 左边边界为l(left),右边界为r(right),形成全闭区间
  3. j指向左边<=4区域的第一个元素
  4. i执行待比较的元素e

执行逻辑 单边循环法
由上面得知,两个区间的范围是[l+1...j]<=4和[j+1...r]>4,经过上面一轮比较后,4就在放在排好序的位置,然后分成左右2部分,然后这两部分再分别继续上面的步骤。

代码如下:

   /**
     * 单边循环法
     * @param arr 排序数组
     * @param l 左边界 起始0
     * @param r 右边界 其实length-1
     */
    public static void quickSort(int[] arr, int l, int r) {
        if (l >= r) {//只有一个元素 无需排序
            return;
        }

        //找到 arr[l] 的位置
        int p = partition(arr, l, r);

        quickSort(arr, l, p - 1);
        quickSort(arr, p + 1, r);

    }

    /**
     * 
     * @param arr
     * @param l
     * @param r
     * @return
     */
    public static int partition(int[] arr, int l, int r) {
        int v = arr[l];

        //左边区间为[l+1...j] 初始时j=l那么[l+1...l]是个无效区间
        //其实就是初始化时 左边区间是没有元素的
        int j = l;

        for (int i = l + 1; i <= r; i++) {
//        if(arr[i]>v){
//            //上面知道 i++ 但是已经在循环中了 那么就什么都不做
//        }

            if (arr[i] <= v) {
                swap(arr, j + 1, i);
                j++;
            }
        }
        swap(arr, l, j);
        return j;
    }

上面的代码,完全是按照我们分析的步骤写的。理解后在看下开始的图:

1593062585(1).jpg

更能理解这里的思想和在递归中都做了什么

双边循环法

上面我们用的是单边循环法,也就是i指针从左到右一直移动。下面再介绍下双边循环法,也就是从两边循环。

1593070285(1).jpg

双边循环,那么也就是需要两个指针一起移动。

  1. 数组范围[satrtIndex...endIndex]
  2. left代表左区间最后一个元素[startIndex...left] 并不断向右移动
  3. left代表右区间最后一个元素[right...endIndex] 并不断向左移动
  4. 当 left和right 指向同一个元素截至

接下来进行第1次循环,从right指针开始,让指针所指向的元素和基准元素做
比较。如果大于pivot ,则指针向左移动;如果小于等于pivot ,则right指针停
止移动,切换到left指针。

1593070285(1).jpg

由于1eft开始指向的是基准元素,判断肯定相等,所以left右移1位

由于7>4 , 1eft指针在元素7的位置停下。这时,让left和right指针所指向的元
素进行交换。

1593070810(1).jpg

第二次循环

1593071025(1).jpg

代码实现

public static void quickSort(int[] arr, int startIndex, int endIndex) {
        if (startIndex >= endIndex) {
            return;
        }

        int p = partition(arr, startIndex, endIndex);
        quickSort(arr, startIndex, p - 1);
        quickSort(arr, p + 1, endIndex);
    }

    public static int partition(int[] arr, int startIndex, int endIndex) {
        int v = arr[startIndex];
        int left = startIndex;
        int right = endIndex;

        while (left != right) {

            //TODO 这里移动是先移动右边 再移动左边 否则当 left=3 right=5 时
            //先移动左边 那么left=right的位置在5的位置,那么4和5交换,最后显然不是有序的
            while (right > left && arr[right] > v) {
                right--;
            }
            while (left < right && arr[left] <= v) {
                left++;
            }

            if (left < right) {
                swap(arr, left, right);
            }


        }
        swap(arr, startIndex, left);
        return left;

    }

注意点

在TODO位置处这里移动是先移动右边 再移动左边 否则当 left=3 right=5 时,先移动左边 那么left=right的位置在5的位置,那么4和5交换,最后显然不是有序的

复杂度分析
上图中也知道8个元素递归3轮,4个元素2轮,2个元素1轮,正好是log28=3,log24=2,log2^2=1,每轮排序O(n) ,所以是nlogn

优化

1. 数据小的时候用插入排序,这个优化和归并一样
2. 随机选取基准元素的选择
1593072531(1).jpg

如上图,当数组像上面时,每次选的基准值要么最大,要么最小,就无法起到分治的效果,从而退化成O(n^2),随意可以随机原则数组中的数值,然后与arr[0]交换,再在排序

3. 双路排序
4. 三路排序

双路排序

双路排序思想,当数组相同元素比较多的时候也会退化成O(n^2),如给定一个数组如下:

算法.png

左边相同元素非常多。导致分治不平均。最后算法退化O(n^2)
双路排序就是将拆分的2个数组分配到两端,左边遍历的元素为ei,右边遍历元素为ej,当ei>4=和ej<=4时交换。那么此时相同的元素就会分到2端,而不会只在一端,退化成O(n^2)

代码实现

 //对arr[l...r]进行快速排序
    private static void quickSort(Comparable[] arr, int l, int r) {

//        if (l >= r) {//当l=r的时候说明只有一个元素  那么他就是有序的
//            return;
//        }
        if (r - l <= 15) {
            Main5.sort(arr, l, r);
            return;
        }

        /**
         * TODO 优化1 : 上面边界的优化  对于小规模数组, 使用插入排序
         *
         *
         *  if( r - l <= 15 ){
         *      InsertionSort.sort(arr, l, r);
         *      return;
         *  }
         *
         *
         */

        int p = partition(arr, l, r);

        quickSort(arr, l, p - 1);
        quickSort(arr, p + 1, r);


    }

    /**
     * 这个的特点就是  当出现了值相等的情况 我们不具体像第一版代码 给他划分到某一端  而是取分布在两端了
     *
     * @param arr
     * @param l
     * @param r
     * @return
     */
    // 双路快速排序的partition
    // 返回p, 使得arr[l...p-1] <= arr[p] ; arr[p+1...r] >= arr[p]
    // 双路快排处理的元素正好等于arr[p]的时候要注意,详见下面的注释:)
    private static int partition(Comparable[] arr, int l, int r) {


        // TODO 最重要的优化:  随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
        // 这样我们和l交换 这样退化成一个链表 也就是O(n*n)的概率是非常低的
        swap(arr, l, (int) (Math.random() * (r - l + 1)) + l);

        //我们找到这个数组中第一个位置 也就是l的位置 为基准数
        Comparable v = arr[l];

        // TODO arr[l+1...i) <= v; arr(j...r] >= v
        int i = l + 1;
        int j = r;

        while (true) {
            // 注意这里的边界, arr[i].compareTo(v) < 0, 不能是arr[i].compareTo(v) <= 0
            // 思考一下为什么?
            //TODO 因为 如果相等 就违背了我们这么设计的初衷
            // 如果我们存在很多相同的元素 原来的排序就会导致 相同的元素永远在一侧 ,如果这里加上等于号
            // [1,1,1,1,1,1,2] 按照之前的逻辑 v我们取1  那么就会导致 一侧是全数据
            // 另一侧只有2  如过不加等于 那么 根据我们的逻辑会分成[111] [1112] 分布均匀


            while (i <= r && arr[i].compareTo(v) < 0) {
                i++;
            }
            
            while (j >= l + 1 && arr[j].compareTo(v) > 0) {
                j--;
            }
            // 对于上面的两个边界的设定
            // 注意这里的边界, j >= l + 1和i <= r, 不能是j > l + 1和i < r
            // TODO 因为 这里的区间是arr[l+1...i) <= v; arr(j...r] >= v i和j是指向将要排序的元素
            //      所以i j) {
                break;
            }

            swap(arr, i, j);
            i++;
            j--;
        }

        swap(arr, l, j);
        return j;
    }
    

三路排序

明白了双路排序,再来看三路排序,其本质的事项是一样的.

图片.png

将数组分成3部分,并按照图中标示定义边界位置

  • lt
  • i待查看的元素
  • gt >v 左边界

e==v

图片.png

直接i++

e
图片.png
  1. arr[i]与arr[lt+1]交换
  2. lt++
  3. i++

e>v

图片.png
  1. arr[i]与arr[gt+1]交换
  2. gt--

==注意此时i是不用变的因为gt+1的位置也是未排序的,交换后i位置还是待排序元素,所以i不变==

最后排序完成后的样子

图片.png

代码实现
private static void quickSort(int[] arr, int l, int r) {
        if (l >= r)
            return;

        int p = partition(arr, l, r);

        quickSort(arr, l, p - 1);
        quickSort(arr, p + 1, r);
    }


    public static int partition(int[] arr, int l, int r) {
        int v = arr[l];
        //[l+1...i) (j...r]
        int i = l + 1, j = r;

        while (true) {
            while (i <= r && arr[i] < v) {
                i++;
            }

            while (j >= l + 1 && arr[j] > v) {
                j--;
            }
            if (i > j) {
                break;
            }

            Main.swap(arr, j, i);
            j--;
            i++;
        }

        Main.swap(arr, l, j);

        return j;
    }

思考

随机数组第K大的元素

快速排序 经过随机选择,当partition后返回P
当arr[k]>arr[p] 那么在右侧,否则在左侧,
那么每次查找就减少一半 
最终通过O(n)完成查找

O(n)=n/2+n/4+n/...=2n

你可能感兴趣的:(快速排序)