详解快速排序的类型和优化

news2024/10/6 6:49:59

详解快速排序的优化

  • 前言
  • 快排的多种写法
    • 霍尔法实现快排
      • 代码部分
    • 挖坑法
      • 思路讲解
      • 代码部分
    • 双指针法
      • 思路讲解
      • 代码部分
  • 针对排序数类型的优化
    • 针对接近或已经有序数列和逆序数列
      • 三数取中
        • 代码实现
      • 随机数
    • 针对数字中重复度较高的数
      • 三路划分
        • 思路讲解
        • 代码部分
    • 根据递归的特点进行优化
      • 插入排序优化快排
        • 代码部分
      • 非递归
        • 代码部分
        • 解释思路

前言

在博主之前刚学C的时候,曾经写了一篇快速排序
那个时候才疏学浅(虽然现在也是)
在经过深度学习了快速排序以后才知道自己对快速排序一无所知
所以特地再出了这样一篇博客用来详解快排

在这里插入图片描述

这个是本篇博客的大致内容和知识构图。

本篇也将分为多个部分来进行对树状图内容的讲解

快排的多种写法

霍尔法实现快排

这个霍尔法可以说是老朋友了,以前的老博客就是使用的霍尔法
霍尔法算是最早的快速排序,毕竟是创世人的实现法
但是有很多的坑,所以在实现时还是需要注意一下的
这里就挂个老地址:老霍尔法详解地址
在这里插入图片描述
这里再来张gif配合理解

代码部分

void Quick_sort(int* arr, int begin, int end)
{
    if (begin >= end)
        return;
    int left = begin;
    int right = end;
    int key = begin;
    while (left < right)
    {
        while (arr[key] <= arr[right] && left < right)
            right--;
        while (arr[key] >= arr[left] && left < right)
            left++;
        swap(arr, +
        left, right);
    }
    swap(arr,left, key);
    Quick_sort(arr, begin, left-1);
    Quick_sort(arr,left+1, end);
}

挖坑法

在这里插入图片描述

思路讲解

这里先解释一下思路,可能看了这个动态图还不够理解。
在这里插入图片描述

图中默认将最左边的数字当作key值,所以直接将key值取走,此时默认原位置没有数值,所以可以看作一个坑位。

这个时候左边小人脚下有坑,此时就需要一个数字来把坑填满
这个时候右边小人就可以动起来,寻找比key值要小的数来填满(这里默认将数字升序排列)

因为坑在左边小人脚下,左边的数希望放入比key值小的数字
在这里插入图片描述
右边小人找到5后,接下来把拿数字把坑填过去就好。
在这里插入图片描述
数字填过去后,右边小人脚下就有坑了,秉持着互帮互助原则,左边小人就需要找数字,帮右边的小人填好坑

右边的小人就需要比key值大的数字。
在这里插入图片描述
此时找到7后,就可以将R的坑填完

此时左边小人就又有坑了,就这样不停循环。

在这里插入图片描述
最后到坑位相遇

因为最开始是没有坑的小人先动,最后一定是不在坑上的小人向有坑的小人撞上相遇。
所以在我们拿不同的数当作key值时,取决于哪个没坑,哪个就先动

最后将key值填入相遇的坑中就可
之后就是进行递归的过程了。

代码部分

void DH_Quick_sort(int* arr,int begin,int end)
{
    if (begin >= end)
        return;
    int left= begin;
    int right=end;
    int key=arr[begin];
    while (left < right)
    {
        while (arr[right] >=key && right > left)
            right--;
        swap(arr, left, right);
        while (arr[left] <= key && right > left)
            left++;
        swap(arr, left, right);
    }
    arr[left] = key;
    DH_Quick_sort(arr, begin, left-1);
    DH_Quick_sort(arr, left+1, end);
}

双指针法

在这里插入图片描述
这个快排的实现法算是三种写法中最简洁的。
但是同时也是比较抽象的,因为完全不同于前两种的实现思路。

思路讲解

在这里插入图片描述

首先设定两个坐标
一个在前,用来寻找比key值小的数(因为以升序为准)
一个在后,用来进行和前坐标的交换

之后前坐标不断向前走,寻找比key值小的数字。

在这里插入图片描述
但是我们发现,一开始的状态就是cur在小于key值的数字上。
那这样能交换吗?
显然是不可以的,因为我们此时prev是在key值上面
key一开始是不能被交换的
所以我们需要一开始就对prev进行++

这里直接把部分实现代码放出来比较好理解

 if (arr[cur] <= arr[key] && ++prev != cur)
            swap(arr, prev, cur);

这里我们就能看到当cur所在的值比key值小时,会进行判断,对prev进行++,++后判断prev是否与cur相等。

在这里插入图片描述
对prev++后这样显然不能进行交换,所以cur还是需要继续往前走。
在这里插入图片描述
cur走到2处时

 if (arr[cur] <= arr[key] && ++prev != cur)

前部分生效,进行对prev的++
在这里插入图片描述

发现prev++后与cur相等,所以cur继续前进。
在这里插入图片描述

cur碰到7后继续++
判断前半部分没生效,所以后半prev++不会触发
所以cur继续++,加到9
同样判断前半部分没生效,所以后半prev++不会触发。
所以prev停留在2处

cur到了3时
在这里插入图片描述
判断部分前后全都触发,prev++
在这里插入图片描述
发现prev与cur并不相同,此时就进行交换
在这里插入图片描述
之后cur继续前进

那为什么要这样设置prev的++判断呢?

**这样可以防止prev和cur相邻的情况
使cur碰到大于key值的节点后才能和prev拉开距离
**
这样就可以防止使两个相邻的小于key值的数进行无效
交换
保证了cur交换的值必定是大于key值的数和小于key值的数进行交换
同时,一开始prev是在key上,所以先++可以防止key值被交换走

在这里插入图片描述

此时就可以cur越界,key与prev进行交换,最后执行递归即可
在这里插入图片描述

代码部分

void DPt_Quick_sort(int* arr, int begin, int end)
{
    if (begin >= end)
        return;
    //这里不能用指针,不好进行赋值和传参
    //所以在前面讲解时用的是坐标一词
    int prev = begin;
    int cur = begin+1;
    int key = begin;
    while (cur <= end)
    {
        if (arr[cur] <= arr[key] && ++prev != cur)
            swap(arr, prev, cur);
        cur++;
    }
    swap(arr, prev, key);
    DPt_Quick_sort(arr, begin, prev-1);
    DPt_Quick_sort(arr, prev+1, end);
}

针对排序数类型的优化

针对接近或已经有序数列和逆序数列

众所周知,当我们使用快排时,默认将数组中的最左边的数当作比较值时

在这里插入图片描述
当对这样一个有序数组进行排序时
假设我们使用的是霍尔法
在这里插入图片描述
right会直接跑到left处。
此时进行递归命令时

 Quick_sort(arr, begin, left-1);
 Quick_sort(arr,left+1, end);

此时数组就会被分成
在这里插入图片描述
这样差距极大的两组进行递归。
众所周知递归最快的,深度最小的方式时
对一个数组进行(n/2)的分开递归
这样将一个数组分成两个差距极大的数组进行排列是十分麻烦的。

所以就出现了优化方法

三数取中

这个方法原理就是在这里插入图片描述
在进行取key值时,不使用默认的最左端的值当作key
将最左端和最右端的数以及他们中间的数进行比较
取出一个中间值当作key值

代码实现

int  GetNumMid(int* arr, int left, int right)
{
    int mid = (left + right) / 2;
    if (arr[left] > arr[right])
    {
        if (arr[left] < arr[mid])
            return left;
        if (arr[left] > arr[mid] && arr[mid] < arr[right])
            return right;
        if (arr[left] > arr[mid] && arr[mid] > arr[right])
            return mid;
        if (arr[left] == arr[mid] || arr[right] == arr[mid])
            return mid;
    }
    else if (arr[left] < arr[right])
    {
        if (arr[left] > arr[mid])
            return left;
        if (arr[mid] > arr[right])
            return right;
        if (arr[mid] > arr[left] && arr[mid] < arr[right])
            return mid;
        if (arr[mid] > arr[left] && arr[mid] > arr[right])
            return right;
        if (arr[right] == arr[mid] || arr[left] == arr[mid])
            return mid;
    }
    else
        return right;
}
void Quick_sort_GM(int* arr, int begin, int end)
{
    if (begin >= end)
        return;
    int left=begin;
    int right=end;
    //取出中间值
    int mid=GetNumMid(arr, left, right);
    //将中间值和最左端的值交换
    //就是将新key移动到最左端
    if (mid != left)
        swap(arr, mid, left);
    int key = left;
    while (left < right)
    {
        while (arr[right] >= arr[key] && left < right)
            right--;
        while (arr[left] <= arr[key] && left < right)
            left++;
        swap(arr, left, right);
    }
    swap(arr, left, key);
    Quick_sort_GM(arr, begin, left - 1);
    Quick_sort_GM(arr, left+1, end);
}

在这里插入图片描述
这样以后将key作为三数中的中间大的数作为值,有效防止了之前的情况。

随机数

随机数就是将key值用数组中随机一个数,作为key值进行比较交换。
这个其实比较吃运气,这个代码就略了,个人认为三数取中更为巧妙一点,而且代码不难,就是随机生成一个数。

针对数字中重复度较高的数

与有序和逆序数列同理,如果一个数组一个相同的数字占多,就会让左右节点相遇较晚,使递归的两个数列分的数差距较大,增加递归的深度,降低效率

三路划分

思路讲解

我们原本实现快速排序,通过的时候两路划分
在这里插入图片描述

这样会省去等于key值的数,所以会导致遇到相同的数字时,递归的数字分配的不好。

所以就产生了我们的三路划分优化
在这里插入图片描述
就是说将小于等于key值的数放到中间,这里随便举一个例子谈谈过程。

在这里插入图片描述
三路划分需要设定三个指针,这里我们就边走边看情况
在这里插入图片描述
在这里插入图片描述

这样就走好了一步,cur继续比较,(这个key知识用来存储key值的,原本的位置换走了也没什么关系,(忘记把箭头去掉了))
在这里插入图片描述
在这里插入图片描述
cur这时又发现了情况的不一样
在这里插入图片描述
在这里插入图片描述
这样三种情况都好了,接下来结束条件就是cur和right相遇的一刻
在这里插入图片描述
在这里插入图片描述
这样递归数列就被成了这样三个部分,中间部分可以不用进行递归,只需要把左边和右边的区间进行递归就可以了

代码部分

void Twd_Quick_sort(int* arr,int begin,int end)
{
    if (begin >= end)
        return;
    int left = begin;
    int key = arr[begin];
    int right = end;
    int cur = left + 1;
    while ( cur <= right)
    {
        if (key < arr[cur])
        {
            swap(arr, cur, right);

            right--;
        }
        else if (key > arr[cur])
        {
            swap(arr,cur,left);
            cur++;
            left++;
        }
        else
        {
            cur++;
        }
    }
    Twd_Quick_sort(arr, begin, left - 1);
    Twd_Quick_sort(arr, right + 1, end);
}

根据递归的特点进行优化

插入排序优化快排

众所周知,插入排序的特点是,越接近有序的数组,插入排序的效率就越高。
希尔排序就是依据插入排序这个特点而优化产生的。
那我们是不是也可以用插入排序来优化快速排序呢?
答案确实是可以的
在这里插入图片描述
我们知道递归算法如果分割合理,就是一颗完美二叉树,这里我们就用二叉树做例子。

在这里插入图片描述
从图上可以看到最后一层的递归节点个数占了递归的绝大部分,并且这个时候数组也接近有序,正好可以用插入排序进行优化。

代码部分

void insert_Quick_sort(int* arr, int begin, int end)
{
    if (begin >= end)
        return;
    if ((end - begin + 1) < 3)
    {
        int left = begin;
        int right = end;
        int key = begin;
        while (left < right)
        {
            while (arr[key] <= arr[right] && left < right)
                right--;
            while (arr[key] >= arr[left] && left < right)
                left++;
            swap(arr, left, right);
        }
        swap(arr, left, key);
        insert_Quick_sort(arr, begin, left - 1);
        insert_Quick_sort(arr, left + 1, end);
    }
    else
        insert_sort_left(arr+begin,end-begin+1);
}

这里主要就是控制right和left之间的数字间隔,来进行选择是进行递归还是进行插入排序。

非递归

递归最大的缺点是什么?就是容易导致栈溢出。
这个应该人人都知道,递归需要不停对栈帧申请空间,然后就会导致栈溢出,而这个时候就需要改成非递归。
基本上能用递归写的都需要学会转化成非递归。

我们要模仿递归时,首先要了解递归每次在干什么。
递归每次传值时

 insert_Quick_sort(arr, begin, left - 1);
 insert_Quick_sort(arr, left + 1, end);

这个递归每次在跑时,都是传了下一次要排序的区间。

了解了这个以后,我们想要模拟实现递归,就需要把每次需要排序的区间给记录下来

区间就是数据,而处理数据的结构,就是我们在学的数据结构,在我们以前学的结构中,有啥结构可以完美处理呢?没错,就是栈,其实队列也可以
但是能用顺序表的地方尽量用顺序表,而不是链表。

代码部分

int part_sort(int* arr, int begin, int end)
{
    int left = begin;
    int right = end;
    int key = begin;
    while (left < right)
    {
        while (arr[key] <= arr[right] && left < right)
            right--;
        while (arr[key] >= arr[left] && left < right)
            left++;
        swap(arr, left, right);
    }
    swap(arr, left, key);
    return key;
}

void Quick_Sort_UNrecursion(int* arr,int left,int right)
{
    Stack ST;
    STinit(&ST);
    STpush(&ST, right);
    STpush(&ST, left);
    while (!STempty(&ST))
    {
        int begin = STtop(&ST);
        STpop(&ST);
        int end = STtop(&ST);
        STpop(&ST);
        int keyi=part_sort(arr, begin, end);
        if(end>keyi+1)
        {
            STpush(&ST, end);
            STpush(&ST, keyi + 1);
        }
        if (begin < keyi - 1)
        {
            STpush(&ST, keyi - 1);
            STpush(&ST, begin);
        }
    }
    STdestroy(&ST);
}

解释思路

这里我们选择先看代码进行解释,便于理解

int part_sort(int* arr, int begin, int end)
{
    int left = begin;
    int right = end;
    int key = begin;
    while (left < right)
    {
        while (arr[key] <= arr[right] && left < right)
            right--;
        while (arr[key] >= arr[left] && left < right)
            left++;
        swap(arr, left, right);
    }
    swap(arr, left, key);
    return key;
}

这个部分是将霍尔快排的单个排序取了出来。

我们主要看实现递归的部分

	Stack ST;
    STinit(&ST);
    STpush(&ST, right);
    STpush(&ST, left);

这里我们首先初始化栈,然后先将最开始的begin和end排序区间存入
这里我们先将right放入,之后再访问时要先读取left(因为栈)

 while (!STempty(&ST))
    {
        int begin = STtop(&ST);
        STpop(&ST);
        int end = STtop(&ST);
        STpop(&ST);
        int keyi=part_sort(arr, begin, end);

这里的top 和pop是对左区间和右区间进行读取,并进行出栈
最后将区间内的数进行排列,并将下两个排序的区间割点存储。


//这里的范围是begin  key-1 【key】 key+1   end
 if(end>keyi+1)
        {
            STpush(&ST, end);
            STpush(&ST, keyi + 1);
        }
        if (begin < keyi - 1)
        {
            STpush(&ST, keyi - 1);
            STpush(&ST, begin);
        }
    }
    STdestroy(&ST);

判断当节点范围
如果只有一个数或不存在时则退出
否则 存入。

所以循环的结束点以 while (!STempty(&ST))栈为空则停止
当栈中的待排序区间全都排序结束时则排序完成

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

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

相关文章

JSP招投标管理系统myeclipse开发mysql数据库WEB结构java编程

一、源码特点 JSP 招投标管理系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 JSP招投标管理系统myeclipse开发mysql数据库W 二、功能介绍 主要功能&#xff1a; …

BPMN2.0 任务-接收任务手动任务

接收任务 接收任务是一个简单的任务,它等待特定消息的到来。 当流程执行到接收任务时,流程状态将提交给持久性存储。流程将保持这种等待状态,直到流程引擎接收到特定的消息,这将触发接收任务之外流程的继续进行。 接收任务用左上角有一个消息图标的标准BPMN 2.0任务(圆角…

C++新特性总结

&#xff08;智能指针&#xff0c;一些关键字&#xff0c;自动类型推导auto&#xff0c;右值引用移动语义完美转发&#xff0c;列表初始化&#xff0c;std::function & std::bind & lambda表达式使回调更方便&#xff0c;c11关于并发引入了好多好东西&#xff0c;有&am…

vivado工程转换到quartus下联合modelsim仿真

vivado用习惯了&#xff0c;现在快速换到quartus下仿真测试。写一个操作文档&#xff0c;以fpga实现pcm编码为例。 目录 一、建立工程 1、准备源码和仿真文件 2、新建工程 3、加载源文件 4、选择器件 5、仿真器配置 6、工程信息 二、配置工程 7、设置顶层文件 8、配置…

【多线程】初识线程,基础了解

目录 认识线程 概念 什么是线程&#xff1f; 为啥要有线程 进程和线程的区别 Java 的线程 和 操作系统线程 的关系 创建线程 1.继承 Thread 类 2.实现 Runnable 接口 3.通过匿名内部类方式创建Thread与实现Runnable 4.Lmabda表达式 Thread 类及常见方法 Thread 的常见构造方法…

点评项目最后一篇:基于HyperLogLog实现UV统计

文章目录 1 HyperLogLog2 测试百万数据的统计 1 HyperLogLog 首先我们搞懂两个概念&#xff1a; UV&#xff1a;全称Unique Visitor&#xff0c;也叫独立访客量&#xff0c;是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站&#xff0c;只记录1次。…

无线键盘有几种连接方式?(USB接收器连接(无线2.4g)、蓝牙连接、wi-fi连接、红外线连接)

文章目录 无线键盘有哪几种连接方式&#xff1f;各连接方式优缺点 无线键盘有哪几种连接方式&#xff1f; 无线键盘有以下几种连接方式&#xff1a; 通过USB接收器连接&#xff08;无线2.4g&#xff09;&#xff1a;无线键盘通过USB接收器与电脑连接&#xff0c;一般需要插入电…

3W字吃透:微服务 sentinel 限流 底层原理和实操

40岁老架构师尼恩的掏心窝&#xff1a; 现在拿到offer超级难&#xff0c;甚至连面试电话&#xff0c;一个都搞不到。 尼恩的技术社群中&#xff08;50&#xff09;&#xff0c;很多小伙伴凭借 “左手云原生 右手大数据 SpringCloud Alibaba 微服务“三大绝活&#xff0c;拿…

MySQL 数据库 增删查改、克隆、外键 等操作

数据库中有数据表&#xff0c;数据表中有一条一条的记录。 可以用Navicat 等远程连接工具链接数据库&#xff0c;不过数据库需要开启授权。 SQL 字段数据类型 int&#xff1a;整型&#xff0c;默认长度是11 float&#xff1a;单精度浮点&#xff0c;4字节32位 double&#x…

Kyligence Zen产品体验-让大数据分析更轻松

很高兴有机会为大家分享我对 Kyligence Zen 产品的真实体验。在过去的几周中&#xff0c;我花费了大量时间使用这个企业级指标平台&#xff0c;并发现它在帮助企业处理和分析海量数据方面拥有强大的能力。 作为数据分析师&#xff0c;我们经常需要处理和分析大量的数据。这可能…

K8S管理系统项目实战[前端开发]-1

前端 Vue 入门与进阶 Vue ElementPlus 组件库 K8s管理系统项目实战[前端开发]-1 项目概述、框架搭建 Vue前端开发&#xff1a;整体布局 Vue前端开发&#xff1a;工作流 Vue前端开发&#xff1a;登录登出、部署、总结 一、项目慨述 本节是k8s管理系统项目实战的前端开发…

Endnote引用中文、英文参考文献方法(不用手敲中文文献信息)

目录 引用英文参考文献 插入中文参考文献格式 导入出错的解决方案 引用英文参考文献 打开知网的官网&#xff0c;选择想要导出成endnote格式的文献&#xff08;在文献前面打√&#xff0c;即为选中状态&#xff09;&#xff0c;在导出与分析-导出文献-endnote&#xff0c;即…

配置Visual Studio Code连接远程服务器

目录 一、Windows用户需要先配置好本地的Remote SSH相关服务 二、打开VS Code&#xff0c;在扩展中搜索"Remote - SSH"并安装​编辑 三、详细操作 四、在出现的config配置文件中写入以下信息 五、点击VSCode界面最左侧的远程资源管理器 六、输入密码&#xff0…

记录一个特殊场景下,修改代码,idea 需要 mvn clean install的问题

修改java代码&#xff0c;mvn clean install 才能生效的场景&#xff0c;在网上看到一些方法&#xff0c;但是都没有生效。eg: 删除.idea&#xff0c; 删除后重新导入。每次修改代码测试的时候都要手动 clean install才可以&#xff0c;比较麻烦。 跑代码的时候&#xff0c;自…

HALCON多线程框架

1.整体架构 HALCON多线程处理框架基本思想是单独一个线程获取待处理图像&#xff0c;若干个图像处理线程并行执行 2.架构图 3.HALCON多线程并行相关算子 broadcast_condition clear_barrier clear_condition clear_event clear_message clear_message_queue clear_mutex cre…

Mac 安装 Python3.10 和 配置环境

Mac 安装 Python3.10 并且配置环境 一、Python的安装 访问官网&#xff1a;https://www.python.org/选择系统&#xff08;Mac&#xff09;: [https://www.python.org/downloads/macos/] 3.选择一个版本下载。我这里选择了3.10.X 下载页面&#xff1a;https://www.python.or…

Qt Creator 快捷键

一、快捷键 下面是Qt常用快捷键用法&#xff0c;如下&#xff1a; 1、构建 快捷键功能ctrl/注释ctrlr运行ctrlb编译 2、查找 快捷键功能ctrlf单文件查找ctrlshiftf多文件查找ctrlshiftu查找所有使用该符号的地方&#xff08;先选中某个符号&#xff0c;执行快捷键&#xff0…

句子扩写软件-文案扩写软件

什么是扩写软件 扩写软件是一种工具&#xff0c;可以帮助用户对一段文本进行扩展&#xff0c;增加文本的长度和信息量&#xff0c;同时保持原始内容的基本意思和结构不变。简单来说&#xff0c;扩写软件就是通过自然语言处理技术&#xff0c;对原始文本进行修改或补充&#xf…

搭建一个SpringSecurity项目

首先需要有一点的SpringBoot基础再来搭建会更好一点 springsecurity项目主要用于权限&#xff0c;验证等操作&#xff0c;非常方便 选择新建项目——选择spring Initializr项目 相对应的设置大家可以自行设置然后点击下一步 这里我选择两个依赖&#xff0c;也不用过多&#xff…

java实现mysql两个数据库对比表结构是否一致

【使用场景】 本地开发完后&#xff0c;增加或者删除了字段&#xff0c;或者修改了字段属性。 如何判定现场环境和本地环境的数据表结构一致性&#xff1f; 肉眼看的话&#xff0c;实在是一个大的工作量&#xff0c;所以开发了此工具。 【类存放路径】 CompareDbMain是主函数…