快速排序及优化

news2024/12/23 14:06:40

快速排序及优化

概要

关于快速排序的原理不赘述,可以查看912. 排序数组 - 力扣(Leetcode)

本篇文章旨在提供快速排序的C#实现,并通过随机pivot,三数取中,小区间使用插入排序,栈实现,合并pivot以缩小分割范围等方式对快速排序进行优化。

要点

  • 快速排序是不稳定排序

  • 快速排序的期望时间复杂度为O(nlog⁡n),最差为O(n)。

  • 快速排序的期望空间复杂度为O(logn),最差情况下是O(n),取决于递归的次数。

  • 快速排序通常是用于排序的最佳使用选择,这是因为它的平均性能较好。虽然最差情况下时间复杂度是O(n^2)。

  • 因此围绕着快速排序的各种优化都是集中于如何让它的时间复杂度达到期望的时间复杂度。

算法步骤

  1. 挑选基准值
  2. 分割(partition)三块
  3. 递归排序子序列

基础版—使用边界作为pivot

简单版,以最左元素为pivot。

这个的思路是i从左往右找,j从右往左找,便于理解但是效率不高。

public class Solution {
    private int PartSort(int[] nums, int l, int r) {
        Console.WriteLine($"part sort, l={l}, right={r}");

        int i = l + 1;
        int j = r;
        int pivot = GetPivot(nums, l);

        while (i <= j) {
            // 找到本轮的i,j位置
            while (i <= j && nums[i] < pivot) {
                i++;
            }
            while (i <= j && nums[j] > pivot) {
                j--;
            }

            // 进行交换
            if (i <= j) {
                (nums[i], nums[j]) = (nums[j], nums[i]);
                i++;
                j--;
            }
        }

        // 将 pivot 放到正确的位置
        (nums[l], nums[j]) = (nums[j], nums[l]);
        return j;
    }


    private int GetPivot(int[] nums,int l){
        return nums[l];
    }
    // 快排
    public void QuickSort(int[] nums,int l,int r){
        if(l<r ){
            // 分治
            int pos = PartSort(nums,l,r);
            // 递归
            QuickSort(nums,l,pos-1);
            QuickSort(nums,pos+1,r);
        }

    }

    public int[] SortArray(int[] nums) {
        QuickSort(nums,0,nums.Length - 1);
        return nums;
    }
}

改进思路-随机pivot

随机pivot是为了减少选到最大最小值的概率。

但随机也会选到不好的pivot,实际上随机数对排序提供的帮助不大。

给出实现方案,在这个实现方案中同时改进了指针遍历的方式,i,j指针都从左往右找。

public class Solution
{
    private Random random = new Random();
    private int Partition(int[] nums, int l, int r)
    {
        // 随机pivot并和最右侧的数交换
        int i = l + random.Next(r - l + 1);
        int pivot = nums[i];
        (nums[r], nums[i]) = (nums[i], nums[r]);
        // 初始化i
        i = l - 1;
        // j指针找小,i指针留在原地不动,等到j指针找到了,i指针再动
        for (int j = l; j < r; j++)
        {
            if (nums[j] < pivot)
            {
                i++;
                (nums[i], nums[j]) = (nums[j], nums[i]);
            }
        }
        (nums[i + 1], nums[r]) = (nums[r], nums[i + 1]);
        return i + 1;
    }



    // 快排
    public void QuickSort(int[] nums, int l, int r)
    {
        if (l < r)
        {
            // 分治
            int pos = Partition(nums, l, r);
            // 递归
            QuickSort(nums, l, pos - 1);
            QuickSort(nums, pos + 1, r);
        }

    }

    public int[] SortArray(int[] nums)
    {
        QuickSort(nums, 0, nums.Length - 1);
        return nums;
    }
}
花头—使用栈实现

上述代码是递归实现的,自然可以改成栈形式。

public class Solution
{
    private Random random = new Random();
    private int Partition(int[] nums, int l, int r)
    {
        // 随机pivot并和最右侧的数交换
        int i = l + random.Next(r - l + 1);
        int pivot = nums[i];
        (nums[r], nums[i]) = (nums[i], nums[r]);
        // 初始化i
        i = l - 1;
        // j指针找小,i指针留在原地不动,等到j指针找到了,i指针再动
        for (int j = l; j < r; j++)
        {
            if (nums[j] < pivot)
            {
                i++;
                (nums[i], nums[j]) = (nums[j], nums[i]);
            }
        }
        (nums[i + 1], nums[r]) = (nums[r], nums[i + 1]);
        return i + 1;
    }



    // 快排
    public void QuickSort(int[] nums, int l, int r)
    {
        Stack<int> stack = new Stack<int>();
        stack.Push(r);
        stack.Push(l);
        
        while(stack.Count > 0){
            l = stack.Pop();
            r = stack.Pop();
            if(l<r){
                int pos = Partition(nums,l,r);
                // 递归改栈
                stack.Push(r);
                stack.Push(pos+1);
                stack.Push(pos-1);
                stack.Push(l);
            }
        }


    }

    public int[] SortArray(int[] nums)
    {
        QuickSort(nums, 0, nums.Length - 1);
        return nums;
    }
}

改进思路—三数取中作pivot

为了让选择的pivot更接近中位数,可以将头中尾三个数字先进行排序,然后用三个数字取中间数。这样可以保证取出来的不是最小的也不是最大的,最起码是第二小/大的。

而且随机数本身也有开销。可以用median-of-three来替代随机数取pivot。

理论上三数取中会更接近于中位数。但LeetCode的测试用例可能比较特殊,我提交后发现运行效率更低了……

public class Solution
{

    private void DealPivot(int[] nums,int l,int r){
        int med =  (r+l)/2;
        if (nums[l] > nums[med]) {
            (nums[l], nums[med]) = (nums[med], nums[l]);
        }
        if (nums[l] > nums[r]) {
            (nums[l], nums[r]) = (nums[r], nums[l]);
        }
        if (nums[med] > nums[r]) {
            (nums[med], nums[r]) = (nums[r], nums[med]);
        }
        (nums[r-1], nums[med]) = (nums[med], nums[r-1]);
    }
    private int Partition(int[] nums, int l, int r)
    {
        // 随机pivot并和最右侧的数交换
        DealPivot(nums,l,r);
        int pivot = nums[r];
        // 初始化i
        int i = l - 1;
        // j指针找小,i指针留在原地不动,等到j指针找到了,i指针再动
        for (int j = l; j < r; j++)
        {
            if (nums[j] < pivot)
            {
                i++;
                (nums[i], nums[j]) = (nums[j], nums[i]);
            }
        }
        (nums[i + 1], nums[r]) = (nums[r], nums[i + 1]);
        return i + 1;
    }



    // 快排
    public void QuickSort(int[] nums, int l, int r)
    {
        if (l < r)
        {
            // 分治
            int pos = Partition(nums, l, r);
            // 递归
            QuickSort(nums, l, pos - 1);
            QuickSort(nums, pos + 1, r);
        }

    }

    public int[] SortArray(int[] nums)
    {
        QuickSort(nums, 0, nums.Length - 1);
        return nums;
    }
}

改进思路—小区间使用插入排序

插入排序在8个左右以下的时候效率非常高。尤其是已部分排序的数组。

实现很简单,就是QuickSort执行递归前,先看看r-l是否小等于8,如果是,不用QuickSort,而用InsertSort

public class Solution
{

  private Random random = new Random();
    private int Partition(int[] nums, int l, int r)
    {
        // 随机pivot并和最右侧的数交换
        int i = l + random.Next(r - l + 1);
        int pivot = nums[i];
        (nums[r], nums[i]) = (nums[i], nums[r]);
        // 初始化i
        i = l - 1;
        // j指针找小,i指针留在原地不动,等到j指针找到了,i指针再动
        for (int j = l; j < r; j++)
        {
            if (nums[j] < pivot)
            {
                i++;
                (nums[i], nums[j]) = (nums[j], nums[i]);
            }
        }
        (nums[i + 1], nums[r]) = (nums[r], nums[i + 1]);
        return i + 1;
    }

    // 插入排序
    private void InsertSort(int[] nums,int l,int r){
        int j;
        int key;
        for(int i=l+1;i<=r;i++){
            key = nums[i];
            j = i-1;
            while(j >= l && key < nums[j]){
                // 后移
                nums[j+1] = nums[j];
                j--;
            }
            nums[j+1] = key;
        }
    }


    // 快排
    public void QuickSort(int[] nums, int l, int r)
    {
        if (l < r)
        {
            if(r-l<=7){
                InsertSort(nums,l,r);
            }
            else{
                // 分治
                int pos = Partition(nums, l, r);
                // 递归
                QuickSort(nums, l, pos - 1);
                QuickSort(nums, pos + 1, r);
            }
        }

    }

    public int[] SortArray(int[] nums)
    {
        QuickSort(nums, 0, nums.Length - 1);
        return nums;
    }
}

改进思路—缩小分割范围,与pivot相同的合并在一起

可以把和pivot相同的数合并到pivot左右的位置,这样分割后两边的范围就会变小,省去了很多不必要的排序。

尤其适用于大量重复的数组。

912. 排序数组 - 力扣(Leetcode)加了个五万个2的测试用例……官方用例都要通不过了。

这个思路可以很好地解决这个用例,耗时从1720ms提升到252ms……

改进前:

image-20230406112226002

改进后:

image-20230406112150546

具体来说就是在执行Partition的时候增加两个指针,分别是指向比pivot小的lt和指向比pivot大的rt。

j用来遍历数组。

当nums[j]==pivot的时候,不再需要交换元素。这样经过循环后。

当nums[j]<pivot的时候,交换元素,并且lt左移1。

当nums[j]>pivot的时候,交换元素,并且gt右移1。

  1. l <= lt < jnums[l...lt]中的元素都小于pivot
  2. lt < j <= gtnums[lt+1...j-1]中的元素都等于pivot
  3. gt < rnums[gt+1...r-1]中的元素都大于pivot

QuickSort原本是Sort pos两边的数组,现在只需要Sortlt左边的和rt右边的数组。

完整代码如下:

public class Solution
{
    private Random random = new Random();

    private (int, int) Partition(int[] nums, int l, int r)
    {
        // 随机pivot并和最右侧的数交换
        int i = l + random.Next(r - l + 1);
        int pivot = nums[i];
        (nums[r], nums[i]) = (nums[i], nums[r]);

        // 初始化i, j, k
        int lt = l; // 指向比pivot小的数
        int gt = r - 1; // 指向比pivot大的数
        int j = l;

        while (j <= gt)
        {
            if (nums[j] < pivot)
            {
                (nums[lt], nums[j]) = (nums[j], nums[lt]);
                lt++;
                j++;
            }
            else if (nums[j] > pivot)
            {
                (nums[gt], nums[j]) = (nums[j], nums[gt]);
                gt--;
            }
            else
            {
                j++;
            }
        }

        (nums[gt + 1], nums[r]) = (nums[r], nums[gt + 1]);
        return (lt, gt + 1);
    }

    // 插入排序
    private void InsertSort(int[] nums, int l, int r)
    {
        int j;
        int key;
        for (int i = l + 1; i <= r; i++)
        {
            key = nums[i];
            j = i - 1;
            while (j >= l && key < nums[j])
            {
                // 后移
                nums[j + 1] = nums[j];
                j--;
            }
            nums[j + 1] = key;
        }
    }

    // 快排
    public void QuickSort(int[] nums, int l, int r)
    {
        if (l < r)
        {
            if (r - l <= 7)
            {
                InsertSort(nums, l, r);
            }
            else
            {
                // 分治
                (int pos_l, int pos_r) = Partition(nums, l, r);
                // 递归
                QuickSort(nums, l, pos_l - 1);
                QuickSort(nums, pos_r + 1, r);
            }
        }
    }

    public int[] SortArray(int[] nums)
    {
        QuickSort(nums, 0, nums.Length - 1);
        return nums;
    }
}

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

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

相关文章

Makefile学习

什么是Makefile 使用 GCC 编译器在 Linux 进行 C 语言编译&#xff0c;通过在终端执行 gcc 命 令来完成 C 文件的编译&#xff0c;如果我们的工程只有一两个 C 文件还好&#xff0c;需要输入的命令不多&#xff0c;当文件有几十、上百甚至上万个的时候用终端输入 GCC 命令的方…

NIFI大数据进阶_实时同步MySql的数据到Hive中去_可增量同步_实时监控MySql数据库变化_操作方法说明_01---大数据之Nifi工作笔记0033

然后我们来看如何把mysql数据实时同步到hive中去 可以看到,其实就是使用 CaptureChangeMySql来获取mysql中变化的数据,具体就是增删改数据 然后再用RouteOnAttribute通过属性,也就是根据是增删改的话,根据这个属性进行路由 把数据路由到不同的位置 然后再用EvaluateJsonPa…

创客匠人人物访谈:客户,如何反推创业发展? |

古语有云&#xff1a;“君者&#xff0c;舟也&#xff1b;庶人者&#xff0c;水也&#xff1b;水则载舟&#xff0c;水则覆舟”。 从创业的角度来看&#xff0c;客户&#xff0c;就是创业这艘小船在市场的海洋里能够安稳航行的必要条件。 在创业的过程中&#xff0c;一定是先有…

STC89C52串口通信当中的UART

1.串口介绍 串口是一种应用十分广泛的通讯接口&#xff0c;串口成本低、容易使用、通信线路简单&#xff0c;可实现两个设备的互相通信。 单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信&#xff0c;极大的扩展了单片机的应用范围&#xf…

Vue 也能复用模板了

文章目录Vue 也能复用模板了1. 怎么用1.1 基本使用1.2 传递参数2. 怎么实现的3. 注意事项4. 未来发展Vue 也能复用模板了 相信很多使用 Vue 的同学会遇到这样一个问题&#xff1a; “组件里有几个部分的模板&#xff08;html 结构&#xff09;是相同的&#xff0c;要想复用的…

使用线性回归构建波士顿房价预测模型

使用线性回归构建波士顿房价预测模型 描述 波士顿房价数据集统计了波士顿地区506套房屋的特征以及它们的成交价格&#xff0c;这些特征包括周边犯罪率、房间数量、房屋是否靠河、交通便利性、空气质量、房产税率、社区师生比例&#xff08;即教育水平&#xff09;、周边低收入…

前馈PID控制(热交换器/反应釜温度控制)

如何利用PID进行温度控制请参看下面博客文章: 博途PID 1200/1500PLC PID_Compact比例作用权重b微分作用权重c解读(PI-D控制器 I-PD控制器)_RXXW_Dor的博客-CSDN博客很多人会问PLC自带的PID指令和我们自己设计的PID有什么区别,这个问题要看你和什么PID控制器作对比,PID负反…

python真的如此好吗?

作为一名合格的&#xff08;准&#xff09;程序员&#xff0c;必做的一件事是关注编程语言的热度&#xff0c;编程榜代表了编程语言的市场占比变化&#xff0c;它的变化更预示着未来的科技风向和机会&#xff01; Python霸占榜首 只因它真的很强 Python&#xff0c;年龄可能比…

2023年的深度学习入门指南(5) - HuggingFace Transformers库

2023年的深度学习入门指南(5) - HuggingFace Transformers库 这一节我们来学习下预训练模型的封装库&#xff0c;Hugging Face的Transformers库的使用。Hugging Face的库非常活跃&#xff0c;比如支持LLaDA大规型的类&#xff0c;是在本文开始写作的前一天发布的。 库新到这种…

JavaWeb——锁策略, cas和synchronized优化过程

目录 一、锁策略 1、悲观锁和乐观锁 2、轻量级锁和重量级锁 3、自旋锁和挂起等待锁 4、互斥锁和读写锁 5、可重入锁和不可重入锁 6、公平锁和非公平锁 二、cas和synchronized 优化过程 1、CAS&#xff08;compare and swap&#xff09; &#xff08;1&#xff09;、原…

企业网站架构部署与优化

系列文章目录 文章目录系列文章目录一、LAMP概述与简介1.LAMP2.各组件的主要作用如下&#xff1a;二、1.编译安装Apache http服务2.编译安装 Mysql 服务3.编译安装 PHP 解析环境总结一、LAMP概述与简介 1.LAMP LAMP架构是目前成熟的企业网站应用模式之一&#xff0c;指的是协…

如何高效建立知识库?

建立知识库是为了更好地管理和传承知识&#xff0c;提高团队的效率和成果。在建立知识库的过程中&#xff0c;需要注意一些关键点&#xff0c;以确保知识库的高效性和可持续性。本文将介绍如何高效建立知识库以及需要注意的事项。 一、建立知识库的步骤 1.明确知识库的目的和…

SpringBoot 默认数据库连接池 HikariCP

目录 引言 1、问题描述 2、SpringBoot默认的数据库连接池 3、HikariCP是什么 4、测试依赖 5、配置文件 5.1、数据库连接参数 5.2、连接池数据基本参数 5.3、连接检查参数 5.4、事务相关参数 5.5、JMX参数 6、HikariCP源码浅析 6.1、HikariConfig--连接池配置的加载…

Observability:使用 OpenTelemetry 和 Elastic 监控 OpenAI API 和 GPT 模型

作者&#xff1a;David Hope ChatGPT 现在很火&#xff0c;它打破了互联网。 作为 ChatGPT 的狂热用户和 ChatGPT 应用程序的开发者&#xff0c;我对这项技术的可能性感到无比兴奋。 我看到的情况是&#xff0c;基于 ChatGPT 的解决方案将呈指数级增长&#xff0c;人们将需要监…

Shiro概述

文章目录1.权限的管理1.1 什么是权限管理1.2 什么是身份认证1.3 什么是授权2.Shiro概述2.1 什么是Shiro2.2 Shiro 与 SpringSecurity 的对比2.3 基本功能3.shiro的核心架构4.shiro中的认证4.1 认证4.2 shiro中认证的关键对象4.3 身份认证流程4.4.登录认证实例4.5 自定义Realm5.…

Python标记数组的连通域

文章目录连通域标记structure参数操作连通域定位连通域连通域标记 通过label函数&#xff0c;可以对数组中的连通区域进行标注&#xff0c;效果如下 from scipy.ndimage import label import numpy as np a np.array([[0,0,1,1,0,0],[0,0,0,1,0,0],[1,1,0,0,1,0],[0,0,0,1,0…

虚拟机里安装ubuntu-23.04-beta-desktop-amd64,开启SSH(换源、备份),配置中文以及中文输入法

一、下载 官网 清华镜像站(推荐) 二、配置虚拟机 【自定义】 点击“下一步”&#xff0c;此处【默认】&#xff0c;再点击“下一步”。 点击“稍后安装操作系统”&#xff0c;再点击“下一步”。 点击“Linux(L)”&#xff0c;版本选择【Ubuntu 64 位】&#xff0c;再点击…

轻量级网页RSS阅读器selfoss

什么是 selfoss &#xff1f; selfoss 是一个多用途的 RSS 阅读器和提要聚合 Web 应用程序。它使您可以在一个地方轻松关注来自不同网站、社交网络和其他平台的更新。它是用 PHP 编写的&#xff0c;基本上可以让您在任何地方运行它。 安装 在群晖上以 Docker 方式安装。 在注…

【前沿技术】问答pk【ChatGPT Vs Notion AI Vs BAT AI 】

目录 写在前面 问题&#xff1a; 1 ChatGPT 1.1 截图 ​1.2 文字版 2 Notion AI 2.1 截图 2.2 文字版 3 BAT AI 3.1 截图 3.2 文字版 总结 序言 所有幸运和巧合的事&#xff0c;要么是上天注定&#xff0c;要么是一个人偷偷的在努力。 突发奇想&#xff0c;问三个…

机器学习---聚类算法

目录【写在前面】1、确认安装有scikit-learn库2、使用 make _ classification ()建立数据集3、使用模型进行分类头文件汇总亲和力传播聚合聚类BIRCH 聚类DBSCAN【本人的毕业设计系统中有用到】K-均值高斯混合模型【写在最后】【写在前面】 sklearn和scikit-learn&#xff1a; …