【算法】快速排序 详解

news2025/1/13 10:26:00

在这里插入图片描述

快速排序 详解

  • 快速排序
    • 1. 挖坑法
    • 2. 左右指针法 (Hoare 法)
    • 3. 前后指针法
    • 4. 快排非递归
  • 代码优化

排序: 排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性: 假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中, r[i] = r[j], 且 r[i] 在 r[j] 之前,而在排序后的序列中, r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的;否则称为不稳定的。
(注意稳定排序可以实现为不稳定的形式, 而不稳定的排序实现不了稳定的形式)

在这里插入图片描述

内部排序: 数据元素全部放在内存中的排序。

外部排序: 数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

快速排序

快速排序是 Hoare 于 1962 年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

1. 挖坑法

  • 选择基准元素:从待排序的数组中选择一个元素作为基准(pivot)。通常情况下,可以选择第一个元素、最后一个元素、中间元素或者随机选择一个元素作为基准。

  • 分割数组:从数组的两端开始,分别设置两个指针,一个从左边(low指针)开始,一个从右边(high指针)开始,分别向中间移动,直到它们相遇。在移动过程中,通过比较元素与基准的大小关系,找到一个大于基准的元素和一个小于基准的元素,并将它们互换位置。

  • 继续分割:重复步骤2,直到low指针和high指针相遇。在此过程中,将小于基准的元素移到基准的左侧,将大于基准的元素移到基准的右侧,形成三个区域。

  • 递归排序:对左侧小于基准的区域和右侧大于基准的区域,分别递归地应用快速排序算法。

在这里插入图片描述

    public static void quickSort(int[] arr) {
        int len = arr.length;
        partition(arr, 0, len-1);
    }

    public static void partition(int[] arr, int left, int right) {
        if (left >= right) {
            return;
        }
        int pivot = left;
        int value = arr[left];
        // 从两边开始遍历
        int begin = left;
        int end = right;
        while (begin < end) {
            // 注意一定要带上 ==, 不然死循环
            while (begin < end && arr[end] >= value) {
                end--;
            }
            // 右边找到小于 value 的值
            arr[pivot] = arr[end];
            // end 变为 坑
            pivot = end;
            while (begin < end && arr[begin] <= value) {
                begin++;
            }
            // 左边找到大于 value 的值
            arr[pivot] = arr[begin];
            // begin 变为坑
            pivot = begin;
        }
        // value 找到自己的正确位置
        arr[pivot] = value;
        // 递归左边和右边
        partition(arr, left, pivot-1);
        partition(arr, pivot+1, right);
    }

2. 左右指针法 (Hoare 法)

从两边开始, 左边找到比基准值大的, 右边找比基准值小的, 找到后, 两边交换, 一直到左右两个指针相遇, 相遇位置即为基准值的正确位置, 然后递归确定左右两边的区间元素的位置。

	public static void quickSort(int[] arr) {
        int len = arr.length;
        partition(arr, 0, len-1);
    }


    public static void partition(int[] arr, int left, int right) {
        if (left >= right) {
            return;
        }
        int value = arr[left];
        int begin = left;
        int end = right;
        while (begin < end) {
            // 注意一定要带上 ==, 不然死循环
            while (begin < end && arr[end] >= value) {
                end--;
            }
            while (begin < end && arr[begin] <= value) {
                begin++;
            }
            swap(arr, begin, end);
        }
        swap(arr, left, begin);
        partition(arr, left, begin-1);
        partition(arr, begin+1, right);
    }
    public static void swap (int[] arr, int index1, int index2) {
        int temp = arr[index1];
        arr[index1] = arr[index2];
        arr[index2] = temp;
    }

3. 前后指针法

后面的指针指向小于基准值的最后一个, 前面的指针一直往后找, 找到小于基准值的就与后面指针的下一个位置交换, 后面的指针 ++, 直到前面的指针遍历完, 最后后面的指针的位置, 就是基准值的正确位置。然后再递归确定左右区间的元素的位置。

    public static void quickSort(int[] arr) {
        int len = arr.length;
        partition(arr, 0, len-1);
    }
    
    public static void partition(int[] arr, int left, int right) {
        if (left >= right) {
            return;
        }
        int value = arr[left];
        // 前后指针
        int end = left;
        int front = left + 1;
        while (front <= right) {
            if (arr[front] < value) {
                swap(arr, end+1, front);
                end++;
            }
            front++;
        }
        swap(arr, left, end);
        // 递归左边和右边
        partition(arr, left, end-1);
        partition(arr, end+1, right);
    }
    
    public static void swap (int[] arr, int index1, int index2) {
        int temp = arr[index1];
        arr[index1] = arr[index2];
        arr[index2] = temp;
    }

4. 快排非递归

使用栈记录要排序的区间

    public static void quickSortNonR(int[] arr) {
        int len = arr.length;
        Stack<Integer> stack = new Stack<>();
        stack.push(arr.length-1);
        stack.push(0);
        while (!stack.isEmpty()) {
            int left = stack.pop();
            int right = stack.pop();

            if (left >= right) {
                continue;
            }
            int pivot = left;
            int value = arr[left];
            // 从两边开始遍历
            int begin = left;
            int end = right;
            while (begin < end) {
                // 注意一定要带上 ==, 不然死循环
                while (begin < end && arr[end] >= value) {
                    end--;
                }
                // 右边找到小于 value 的值
                arr[pivot] = arr[end];
                // end 变为 坑
                pivot = end;
                while (begin < end && arr[begin] <= value) {
                    begin++;
                }
                // 左边找到大于 value 的值
                arr[pivot] = arr[begin];
                // begin 变为坑
                pivot = begin;
            }
            // value 找到自己的正确位置
            arr[pivot] = value;
            // 右区间
            stack.push(right);
            stack.push(pivot+1);
            // 左区间
            stack.push(pivot-1);
            stack.push(left);
        }
    }

代码优化

优化一:
三数取中:
当我们找基准值时, 基准值越靠近是中位数,每次都近似于将一个大的区间分成两个相等的区间, 那么时间复杂度越低, 越靠近是 O(N*logN), 最坏的情况下, 每次确定一个元素, 即分成的两个区间其中一个只有一个元素, 那么如果每次都是这样的话, 最终的时间复杂度为 O(N*N), 所以选择基准值时, 越靠近中位数越好。

这里面以前后指针为例使用了 三数取中



    public static void quickSort(int[] arr) {
        int len = arr.length;
        partition(arr, 0, len-1);
    }

    /**
     *  前后指针
     */

    public static void partition(int[] arr, int left, int right) {
        if (left >= right) {
            return;
        }
        // 三数取中
        int midIndex = getMidIndex(arr, left, right);
        swap(arr, left, midIndex);

        int value = arr[left];
        // 前后指针
        int end = left;
        int front = left + 1;
        while (front <= right) {
            if (arr[front] < value) {
                swap(arr, end+1, front);
                end++;
            }
            front++;
        }
        swap(arr, left, end);
        // 递归左边和右边
        partition(arr, left, end-1);
        partition(arr, end+1, right);
    }



    public static int getMidIndex(int[] arr, int left, int right) {
        int midIndex = ((right - left) >> 1) + left;
        if (arr[left] <= arr[midIndex]) {
            if (arr[midIndex] <= arr[right]) {
                return midIndex;
            } else {
                if (arr[left] >= arr[right]) {
                    return left;
                } else {
                    return right;
                }
            }
        } else {
            if (arr[midIndex] >= arr[right]) {
                return midIndex;
            } else {
                if (arr[left] >= arr[right]) {
                    return right;
                } else {
                    return left;
                }
            }
        }
    }



    public static void swap (int[] arr, int index1, int index2) {
        int temp = arr[index1];
        arr[index1] = arr[index2];
        arr[index2] = temp;
    }

优化二:
小区间优化, 当区间中元素数量比较少时,区间中元素本身就接近有序, 使用直接插入排序能提高效率 (直接插入排序)
假如说有 100W 个数据, 就会调用 100W 次 partition 函数, 但是光最后两层就会调用近 80W 次, 所以使用小区间优化提高效率。

直接插入排序

    public static void insertSort(int[] arr) {
        int len = arr.length;
        for (int i = 1; i < len; i++) {
            // 从已经有序的位置从后往前开始比较
            int key = arr[i];
            int end = i-1;
            while (end >= 0 && arr[end] > key) {
                // 数据往后挪
                arr[end+1] = arr[end];
                end--;
            }
            // 找到了合适位置, 就插入进去
            arr[end+1] = key;
        }
    }

在快排中使用小区间优化


	 /**
     *   直接插入排序
     */
    public static void insertSort(int[] arr, int left, int right) {
        for (int i = left+1; i <= right; i++) {
            // 从已经有序的位置从后往前开始比较
            int key = arr[i];
            int end = i-1;
            while (end >= 0 && arr[end] > key) {
                // 数据往后挪
                arr[end+1] = arr[end];
                end--;
            }
            // 找到了合适位置, 就插入进去
            arr[end+1] = key;
        }
    }
	

	
    public static void quickSort(int[] arr) {
        int len = arr.length;
        partition(arr, 0, len-1);
    }

	public static void partition(int[] arr, int left, int right) {
        if (left >= right) {
            return;
        }
        // 小区间优化, 当数组中元素个数 <= 100 个时使用直接插入排序
        // 主要是减少了递归的次数
        if (right - left + 1 <= 100) {
            insertSort(arr, left, right);
            return;
        }
        // 三数取中
        int midIndex = getMidIndex(arr, left, right);
        swap(arr, left, midIndex);

        int value = arr[left];
        // 前后指针
        int end = left;
        int front = left + 1;
        while (front <= right) {
            if (arr[front] < value) {
                swap(arr, end+1, front);
                end++;
            }
            front++;
        }
        swap(arr, left, end);
        // 递归左边和右边
        partition(arr, left, end-1);
        partition(arr, end+1, right);
    }


总结:

  • 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  • 时间复杂度: O (N*logN)
  • 空间复杂度: O(logN)
  • 是不稳定排序
  • 对数据比较敏感: 当数据本身就是有序时, 情况最坏, 因为每次只能确定一个数据, 时间复杂度为 O(N*N)
    在这里插入图片描述

好啦, 以上就是对快速排序的讲解, 希望能帮到你 !
评论区欢迎指正 !

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/981122.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

链接服务器导致SQL Server停止响应

概要 如果多个实例中同时存在数据源为对方实例的链接服务器&#xff0c;并且开启了“分发服务器”的属性&#xff0c;您可能会遇到这种情况。 现象 14:31时&#xff0c;在SSMS中检查HIS实例是否有复制订阅时&#xff0c;点击了”发布服务器属性“后&#xff0c;SSMS一直无法响…

(二十四)大数据实战——Flume数据流监控之Ganglia的安装与部署

前言 本节内容我们主要介绍一下Flume数据流的监控工具Ganglia。Ganglia是一个开源的分布式系统性能监控工具。它被设计用于监视大规模的计算机群集&#xff08;包括集群、网格和云环境&#xff09;&#xff0c;以便收集和展示系统和应用程序的性能数据。Ganglia 可以轻松地扩展…

Tugraph图学习技术详解

文章目录 TuGraph图学习目录图学习典型工作流程整体学习架构加速稀疏计算GPC编译加速 编译加速编译加速流水线GPCSPMM和SDDMM优化SPMM DSL代码生成SDMM DSL代码生成AutoTune-Cost Model 加速效果一键加速 TuGraph图学习实践目录TuGraph采样TuGraph采样算子全图训练采样算子介绍…

启动hadoop并测试问题合集

首先hadoop和jdk都已经装好了的&#xff0c;如下&#xff1a; 然后相应的这五个配置文件也配好了&#xff1a; 然后格式化了&#xff1a; cd /opt/hadoop/bin/ sudo ./hdfs namenode -format &#xff08;显示这个就为成功&#xff0c;很长的&#xff0c;慢慢找&#xff09; …

[当人工智能遇上安全] 8.基于API序列和机器学习的恶意家族分类实例详解

您或许知道&#xff0c;作者后续分享网络安全的文章会越来越少。但如果您想学习人工智能和安全结合的应用&#xff0c;您就有福利了&#xff0c;作者将重新打造一个《当人工智能遇上安全》系列博客&#xff0c;详细介绍人工智能与安全相关的论文、实践&#xff0c;并分享各种案…

如何实现软件的快速交付与部署?

一、低代码开发 微服务、平台化、云计算作为当前的IT技术热点&#xff0c;主要强调共享重用&#xff0c;它们促进了软件快速交付和部署。 但现实的痛点却是&#xff0c;大多数软件即使采用了微服务技术或者平台化思路&#xff0c;也难以做到通过软件共享重用来快速满足业务需求…

MSTP + Eth-Trunk配置实验 华为实验手册

1.1 实验介绍 1.1.1 关于本实验 以太网是当今现有局域网LAN&#xff08;Local Area Network&#xff09;采用的最通用的通信协议标准&#xff0c;以太网作为一种原理简单、便于实现同时又价格低廉的局域网技术已经成为业界的主流。 本实验主要介绍了LAN网络中的Eth-Trunk技术…

比亚迪海豹:特斯拉强劲对手,瑞银拆解成本比同级车型低15%~35%

瑞银证券日前对中国电动车产品比亚迪海豹进行了拆解&#xff0c;发现海豹具有强大的成本优势&#xff0c;而这个优势主要来自于中国本土生产和国内完善的电动车供应链以及比亚迪的垂直整合体系和零部件高度集成性。比亚迪的整车成本比同级别竞争车型分别低15%至35%。 瑞银预测&…

获取街道、乡镇级的地图geoJson数据,使用echarts绘制地图

在此以泰州靖江市为例为例&#xff0c;记录一下实现过程 1、整体完成后实现的效果如下 2、获取数据 &#xff08;1&#xff09;DataV.GeoAtlas 第一个能想到的获取数据的网站就是它&#xff0c; 是阿里推出的一个用于获取全国、各省、各市以及个县级市详细地图信息的json文…

Vue框架学习记录之环境安装与第一个Vue项目

Node.js的安装与配置 首先是Node.js的安装&#xff0c;安装十分简单&#xff0c;只需要去官网下载安装包后&#xff0c;一路next即可。 Node.js是一个开源的、跨平台的 JavaScript 运行时环境 下载地址&#xff0c;有两个版本&#xff0c;一个是推荐的&#xff0c;一个是最新…

【洛谷 P1328】[NOIP2014 提高组] 生活大爆炸版石头剪刀布 题解(模拟+向量)

[NOIP2014 提高组] 生活大爆炸版石头剪刀布 题目描述 石头剪刀布是常见的猜拳游戏:石头胜剪刀,剪刀胜布,布胜石头。如果两个人出拳一样&#xff0c;则不分胜负。在《生活大爆炸》第二季第 8 集中出现了一种石头剪刀布的升级版游戏。 升级版游戏在传统的石头剪刀布游戏的基础…

肖sir__设计测试用例方法之状态迁移法05_(黑盒测试)

设计测试用例方法之状态迁移法 一、状态迁移图 定义&#xff1a;通过描绘系统的状态及引起系统状态转换的事件&#xff0c;来表示系统的行为 案例&#xff1a; &#xff08;1&#xff09; 订机票案例1&#xff1a; l向航空公司打电话预定机票—>此时机票信息处于“完成”状…

Elasticsearch实战(五):Springboot实现Elasticsearch电商平台日志埋点与搜索热词

文章目录 系列文章索引一、提取热度搜索1、热搜词分析流程图2、日志埋点&#xff08;1&#xff09;排除logback的默认集成。&#xff08;2&#xff09;引入log4j2起步依赖&#xff08;3&#xff09;设置配置文件&#xff08;4&#xff09;配置文件模板&#xff08;5&#xff09…

使用高斯混合模型进行聚类

一、说明 高斯混合模型 &#xff08;GMM&#xff09; 是一种基于概率密度估计的聚类分析技术。它假设数据点是由具有不同均值和方差的多个高斯分布的混合生成的。它可以在某些结果中提供有效的聚类结果。 二、Kmean算法有效性 K 均值聚类算法在每个聚类的中心周围放置一个圆形边…

效果好的it监控系统特点

一个好的IT监控系统应该具备以下特点&#xff1a;  全面性&#xff1a;IT监控系统应该能够监视和管理IT系统的所有方面&#xff0c;包括网络、服务器、应用程序和数据库等。这样可以确保系统的各个方面都得到充分的监视和管理。  可靠性&#xff1a;IT监控系统需要保持高可…

docker 跨平台构建镜像

我们在开发环境构建的镜像在生产环境大多不可用&#xff0c;我们在开发中一般使用 Windows 或者 MAC 系统&#xff0c;部署多半是 linux 环境。那么这篇文章能帮到你。 文章目录 首先构建环境进阶 首先 首先你需要有一个 Dockerfile 文件。 举例&#xff1a;这里以一个 pytho…

SpringMVC之综合案例

SpringMVC注解 Controller: 标记一个类为控制器&#xff08;处理请求的类&#xff09;&#xff0c;将其作为Spring MVC的组件进行管理。 RequestMapping: 将请求URL映射到具体的处理方法上。可以用在类级别和方法级别&#xff0c;用于指定URL路径。 RequestParam: 用于将请求…

流式数据处理与高吞吐消息传递:深入探索Kafka技术的奥秘

Kafka 是一种高吞吐量、分布式、基于发布/订阅的消息系统&#xff0c;最初由 LinkedIn 公司开发&#xff0c;使用Scala 语言编写&#xff0c;目前是 Apache 的开源项目。 Kafka 概念 Zookeeper 集群是一个基于主从复制的高可用集群&#xff0c;每个服务器承担如下三种角色中的…

【网络编程】C++实现网络通信服务器程序||计算机网络课设||Linux系统编程||TCP协议(附源码)

TCP网络服务器 &#x1f40d; 1.程序简洁&#x1f98e;2. 服务端ServerTcp程序介绍&#x1f996;3.线程池ThreadPool介绍&#x1f995; 4.任务类Task介绍&#x1f419;5. 客户端Client介绍&#x1f991;6.运行结果&#xff1a;&#x1f990; 7. 源码&#x1f99e;7.1 serverTcp…

C++内存管理(3)——内存池

1. 默认内存管理函数的不足&#xff08;为什么使用内存池&#xff09; 利用默认的内存管理操作符 new/delete 和函数 malloc()/free() 在堆上分配和释放内存会有一些额外的开销。 系统在接收到分配一定大小内存的请求时&#xff0c;首先查找内部维护的内存空闲块表&#xff0…