IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 算法6 排序算法 QuickSort 快速排序 -> 正文阅读

[数据结构与算法]算法6 排序算法 QuickSort 快速排序

?????

Quick sort 快速排序

???

????????快算排序 Quick Sort ,可能是应用最为广泛的算法,被视为20世纪科学和工程领域的十大算法之一。其流行的原因是因为它实现简单,可适用于不同数据,并且在一般场景下比其他算法要更快。其优点是:

可借用一个很小空间的辅助栈来进行原地排序;

对于长度为N的数组,其时间复杂度为 NlogN;

快速排序的内循环短小,这意味它无论在理论上还是在实际中,它都要更快。

缺点:

????????要小心的避免错误,防止性能退化为N2 。

算法思想:

? ? ? ? 快速排序 和 归并排序一样都是使用了分治思想的排序算法。他们都是将一个数组,一分为二,独自独立排序。 归并排序是将数组分为两个子数组分别排序,并将有序的子数组归并将整个数组进行排序;而快速排序是当子数组都有序了之后,整个数组自然就实现了有序。归并的切分位置就在数组中间位置;而快速排序的切分位置partition 取决于数组的内容。

Partition过程大致如图所示:

?

? ? ? ?快速排序需要推举出一个元素来作为我们的基准元素;如图所示:我们推举出我们的首个元素作为基准元素(V)。帮助我们的基准元素找他应该所处的位置过程,我们将其命名为 Partition的过程,即找到 j 的过程。

? ? ? ? 结合如图所示,我们来看我们应该如何去做这件事:

在这个过程中,我们需要满足的V前面的元素是不大于V的,V后面的元素是不小于V的。即 arr[l+1...j] < v && arr[j+1...i-1] > v

? ? ? ? ?图中变量 , v :基准元素? ,l :元素开始位置索引,j基准位置索引,i 遍历比较过程中的当前时刻的位置索引;

蓝色元素e本身比较前的位置就处于不小于V的位置,如果发现e 大于 V 不动,i++遍历下一个元素;如果发现当前遍历元素e小于V,则需要将e和紫色部分的首元素交换,并且j++。如此进行下去,直到i遍历完整个数组后,把l位置所在元素,和 j 位置所在元素 进行交换后,即可完成整个Partition的过程。

算法实现:

基于以上内容,我们来实现第一版的算法基本实现:

package com.cosyit.offer.algorithms;

import java.util.Arrays;

public class QuickSort1 {

    private static void quickSort(Comparable[] a, int n) {
        __quickSort(a, 0, n - 1); // a[0] - a[length-1]
    }

    private static void __quickSort(Comparable[] a, int l, int r) {
        //设置临界条件。  obscure
        if (l >= r) return;
        //隔板分区
        int p = partition(a, l, r);
        // l 到 p-1 的分区部分 进行快速排序。
        __quickSort(a, l, p - 1);
        // p+1 到 r 的分区部分 进行快速排序。
        __quickSort(a, p + 1, r);
    }

    /**
     * 推举出一个元素作为隔板元素以进行分区,隔板分区后该元素将处于其顺序的位置,并返回该隔板元素的索引。
     * 元素a[p]的隔板效果:左边的比隔板元素小 a[l...p-1] < arr[p],右边都比隔板元素大 a[p] < a[p+1...r]
     *
     * @param a 传入的数组。
     * @param l 左边界元素索引。
     * @param r 有边界元素索引。
     * @return 隔板操作后的,隔板位置索引。
     */
    private static int partition(Comparable[] a, int l, int r) {

        // 1.推举出一个元素,这里我们推举我们的头部元素为隔板元素。
        Comparable v = a[l];

        // 2.遍历整个数组。a[l+1...j]<v ,v < a[j+1...i),i是开区间,因为i的元素是我们当前需要考察的元素。
        int j = l; //j:不大于v左边分区的尾部元素位置索引。
        /**
         * 这里有一个定义的技巧,就是 j 和 i 的初始值设计:
         * * 为什么 把j的初始化值设置为l ,  因为这样设计 a[l+1...j]即a[l+1...l],保证了小于v的分区,从开始是不存在的分区。
         * * 为什么 把i的初始化值设置为l+1, 因为这样设计 a[j+1...i]即a[l+1...l+1) ,保证了大于v这个分区,从开始是不存在的分区。
         */
        for (int i = l + 1; i <= r; i++) {
            if (less(a[i], v)) {
                swap(a, j + 1, i);
                j++;
            }
        }

        //放置隔板:i 遍历完的时候,元素v即a[l] 和 j进行交换。把j的标记位置索引 j 返回。
        swap(a, l, j);

        return j;
    }


    private static void swap(Comparable[] arr, int i, int j) {
        Comparable temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    private static boolean less(Comparable a, Comparable b) {
        return a.compareTo(b) < 0;
    }

    public static void main(String[] args) {
        Integer[] arr = {6, 7, 1, 4, 3, 5, 2, 0};

        quickSort(arr, arr.length);

        System.out.println(Arrays.toString(arr));

    }

}

基础算法的改进:

? ? ? 改进1:? 推举首元素为隔板元素的是有弊端的,因为往往人们处理的数据大概率会是一个近乎有序的数组,这样的数据在进行 递归时候,分区的递归树的高度会很高,递归树的平衡度会很差。基于这个问题,我们应使 隔板元素 保持随机性,来避免这个问题。固添加如下代码以做优化:

因为大部分的代码都是冗余的,关键代码贴出来即可

?

改进2:如果处理切分元素有大量重复元素,而导致切分不平衡的情况,栈深度会变大。

???????

?

优化一下思路,如图所示:

??????????之前,我们把大于V和小于V的放在了数组的一端,现在把大于V和小于V的放在数据的两端。

????????如图所示:i,j 的位置为比较元素e,i++往右跑,j--往左跑。

i在遍历过程中遇到e>=v 不满足小于v的情况,停止i++;同理,j--的过程中遇到 e<= v 不满足大于v的情况,停止j--;

i和j交换位置,交换位置后,i++查看下一个元素,j++查看下一个元素。

直到 i 和 j 索引重合,意味着遍历结束。

? ? ? ? 实际上,按照这个逻辑走完,其实 左分区 是小于等于v的元素集合,右分区是大于等于v的元素集合。

????????如果 i 和 j 都指向了 等于v的元素,两个仍然要交换一下位置,这样就遇到大量等于v的元素的情况,也可以尽量平分这些元素。

算法思想实现:

package com.cosyit.offer.algorithms;

import java.util.Arrays;
import java.util.Random;

public class QuickSort3 {

    private static void quickSort(Comparable[] a, int n) {
        __quickSort(a, 0, n - 1); // a[0] - a[length-1]
    }

    private static void __quickSort(Comparable[] a, int l, int r) {
        //设置临界条件。  obscure
        if (l >= r) return;
        //隔板分区
        int p = partition(a, l, r);
        // l 到 p-1 的分区部分 进行快速排序。
        __quickSort(a, l, p - 1);
        // p+1 到 r 的分区部分 进行快速排序。
        __quickSort(a, p + 1, r);
    }

    /**
     * 推举出一个元素作为隔板元素以进行分区,隔板分区后该元素将处于其顺序的位置,并返回该隔板元素的索引。
     * 元素a[p]的隔板效果:左边的比隔板元素小 a[l...p-1] < arr[p],右边都比隔板元素大 a[p] < a[p+1...r]
     *
     * @param a 传入的数组。
     * @param l 左边界元素索引。
     * @param r 有边界元素索引。
     * @return 隔板操作后的,隔板位置索引。
     */
    private static int partition(Comparable[] a, int l, int r) {
        // core 保持随机性。
        //生成随机索引。 l + 随机[0 ... r - l + 1)的数
        int randomIndex = l + new Random().nextInt(r - l + 1);
        //和首元素交换。
        swap(a, l, randomIndex);

        // 1.推举出一个元素,这里我们推举我们的头部元素为隔板元素。
        Comparable v = a[l];

        int i = l + 1, j = r;

        while (true) {
            //  i在运动的过程中,i <= r 防止索引越界。
            while (i <= r && less(a[i], v)) i++; //  i往前走不停止。
            while (j >= l + 1 && less(v, a[j])) j--; // j往前走不停止。
            if (i > j) break;

            //都停下来了,交换元素。
            swap(a, i, j);
            //各自指向下一个元素,继续。
            i++;
            j--;

        }

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


    private static void swap(Comparable[] arr, int i, int j) {
        Comparable temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    private static boolean less(Comparable a, Comparable b) {
        return a.compareTo(b) < 0;
    }

    public static void main(String[] args) {
        Integer[] arr = {6, 7, 1, 4, 3, 5, 2, 0};

        quickSort(arr, arr.length);

        System.out.println(Arrays.toString(arr));

    }

}

改进3:以上实现中,对于大量重复的元素,会不必要的将等值的元素进行交换。为了规避这个问题,有一种经典的实现方式 --- 三路切分的快速排序 Quick Sort 3 Way。

算法思路:?

之前在切分过程中都是切分为大于基准元素V和小于基准元素V的情况,三路排序则是切分为 等于基准元素V,大于V和小于V 三种情况。

?

??????????如图所示? l 为 首元素,此处放置了基准元素。 lt 指向小于V分区 的最后一个元素;gt指向大于V分区的 第一个元素。e 是活动元素a[i]? ——?当前要和基准元素V进行比较的元素。

? ? ① e小于V,e 和 ==v 的第一个元素交换 即 a [ lt + 1 ] ,然后 lt++,? i++;

? ? ② e大于V,e和 a[gt-1] 元素交换后,gt -- ,但是此时 i不用处理,因为i的元素是一个gt-1换过来的新元素 ;

? ? ③ 当元素e 即 a[i] 和基准元素V相等,直接 i++ 继续处理下一个元素即可。

? ? 当整个数组 i 和 gt 索引重合的时候,就是所有元素遍历完成的时候。最后,l位置元素和 v 进行交换。lt 位置即为返回的位置。?

三路排序算法思想实现:

package com.cosyit.offer.algorithms;

import java.util.Arrays;
import java.util.Random;

public class QuickSort3Ways {


    private static void quickSort3Ways(Comparable[] a, int l, int r) {

        if(r<=l) return;


        // core 保持随机性。
        //生成随机索引。 l + 随机[0 ... r - l + 1)的数
        int randomIndex = l + new Random().nextInt(r - l + 1);
        //和首元素交换。
        swap(a, l, randomIndex);

        // 1.推举出一个元素,这里我们推举我们的头部元素为隔板元素。
        Comparable v = a[l];

        int lt = l;  // a[l+1 ... lt] < v , a[l+1 ... lt]初始范围为空。
        int gt = r + 1; // a[gt...r] > v ,a[gt...r] 初始范围为空。
        int i = l + 1;// l为首个基准元素V,遍历元素从l+1开始。     a[lt+1...i) == v

        while (i < gt) {  //只要i在比较的过程中,i没有和gt碰上,就一直遍历。

            int cmp = a[i].compareTo(v);
            if (cmp < 0) {
                swap(a, i, lt + 1);
                lt++; i++;
            } else if ( cmp >0) {
                swap(a, i, gt - 1);
                gt--;
            } else {
                i++;
            }
        }


        //v和lt交换
        swap(a, l, lt); lt--;

        //此时, ==v 的部分已经在有序的部分,无须排序。
        
        //排序 l...lt 的左分区。
        quickSort3Ways(a, l, lt );
        //排序 gt...r 的右分区。
        quickSort3Ways(a, gt, r);

    }


    private static void swap(Comparable[] arr, int i, int j) {
        Comparable temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }


    public static void main(String[] args) {
        Integer[] arr = {6, 7, 1, 4, 3, 5, 2, 0};

        quickSort3Ways(arr,0, arr.length-1);

        System.out.println(Arrays.toString(arr));

    }

}

以上代码可以将和切分元素相等的元素归位,这样和v相等的元素在就不会在递归函数中处理了,在递归的函数中和v相等的元素,不会参与排序了,这样提高了效率。

?

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-11-05 00:49:39  更:2022-11-05 00:50:14 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/15 12:32:57-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码