快速排序全面详解

目录

1 基本思想

2 排序步骤

3 代码实现

3.1 区间划分算法(hoare初始版本):

3.2 主框架

4 区间划分算法

4.1 hoare法

4.2 挖坑法

4.3 前后指针法

5 快排优化

5.1 取key方面的优化

5.2 递归方面的优化

5.3 区间划分方面的优化

6 快排非递归实现

6.1 栈实现(代码+图解)

6.2 队列实现

7 特性总结


1 基本思想

快速排序采用分治法,任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

2 排序步骤

  1. 选取基准值,通过区间划分算法把待排序序列分割成左右两部分。

  2. 左右序列中递归重复1。

3 代码实现

3.1 区间划分算法(hoare初始版本):

区间划分算法有三个版本:hoare法,挖坑法,前后指针法,这里介绍hoare法,也是快排的初始划分法。

三种划分方法的结果可能不同,但都是让小的值往前靠拢,大的值往后靠拢,让整个序列逐渐趋于有序。

步骤:

  1. 默认序列最左边的元素为基准值key,设置left,right指针;

  2. left找大,right找小,right要先找,都找到后交换a[left]和a[right];

  3. 重复步骤3

  4. 当left == right时,交换key和相遇位置的元素,完成分割。

走完这一趟后,key值左边都不比key大,key值右边都不比key小,key值到了他排序后应该在的位置,不需要挪动这个元素了。

图解:

算法分析:

为什么能保证相遇位置的值一定比key值小,然后交换?

关键点就是让right先找!

相遇有两种情况:

  • left往right靠拢,与right相遇:right先找到了小的元素,相遇后的值一定比key小。

  • right往left靠拢,与left相遇:left指针指向的元素是上一波交换过后的元素,该元素比key小。

假如我们让left先找的话,相遇位置比key值大,不能交换。

代码:

int Partion(int* a, int left, int right)
{int keyI = left;
​//left == right两个指针相遇,退出循环while (left < right){//right先找,right找小while (left < right && a[right] >= a[keyI]){right--;}
​//left找大while (left < right && a[left] <= a[keyI]){left++;}
​//都找到了,交换Swap(&a[left], &a[right]);}
​//left和right相遇,交换key和相遇位置元素Swap(&a[keyI], &a[left]);
​return left;
}

划分方法一般不用hoare,是因为这种算法实现的代码很容易出现bug,比如:

  1. key值一般取最左边或者最右边的值,但是要注意key不能用变量保存,而是要保存key的下标keyI,否则最后key与相遇位置的交换并没有真正交换数组中的key。(注意:有些划分算法是用变量保存key,有些是保存下标keyI,视情况而定。)

  2. right找小和left找大的过程中,要保证left < right,否则可能出现数组越界,比如1,9,6,4,2,7,8,2 ;右边的值都比key大,会导致越界。

  3. a[right] >= a[keyI]或者a[left] <= a[keyI]时,才能--right或者++left;如果是a[right] > a[keyI]或者a[left] < a[keyI]可能出现死循环,比如a[left] == a[right] == key时,交换完后不进入内部while,外部while陷入死循环。

3.2 主框架

void _QuickSort(int* a, int begin, int end)
{if (begin >= end)return;
​//根据基准值把数组划分成左右序列int keyI = Partion(a, begin, end);
​//左右序列递归划分下去_QuickSort(a, begin, keyI - 1);_QuickSort(a, keyI + 1, end);
}
​
void QucikSort(int* a, int n)
{_QuickSort(a, 0, n - 1);
}

上述为快速排序递归实现的主框架,与二叉树前序遍历规则非常像。

二叉树的递归终止条件是空树,快排的终止条件是数组只有一个元素(left==right)或者数组区间不存在(left>right)。

浅画一下展开图:

4 区间划分算法

前面所说,hoare划分法有一定的缺陷,我们再介绍其他两种常用的划分方法。

4.1 hoare法

int Partion(int* a, int left, int right)
{int keyI = left;
​//left == right两个指针相遇,退出循环while (left < right){//right先找,right找小while (left < right && a[right] >= a[keyI]){right--;}
​//left找大while (left < right && a[left] <= a[keyI]){left++;}
​//都找到了,交换Swap(&a[left], &a[right]);}
​//left和right相遇,交换key和相遇位置元素Swap(&a[keyI], &a[left]);
​return left;
}

4.2 挖坑法

步骤:

  1. 默认序列最左边的元素为基准值key,把值挖走用key变量保存,该位置为一个坑。

  2. 右边找小,找到后把值填给坑位,该位置成为新的坑位。

  3. 左边找大,找到后把值填给坑位,该位置成为新的坑位。

  4. 重复步骤2~3。

  5. 左右相遇,相遇位置也是个坑位,key值填入坑位。

图解:

代码:

int Partion2(int* a, int left, int right)
{int key = a[left];int hole = left;
​while (left < right){while (left < right && a[right] >= key){right--;}a[hole] = a[right];hole = right;
​while (left < right && a[left] <= key){left++;}a[hole] = a[left];hole = left;}
​a[hole] = key; 
​return hole;
}

与前面代码不同的是,这里的key值我们不存下标,用一个变量保存。

4.3 前后指针法

步骤:

  1. 默认序列最左边的元素为基准值key,设置prev指针 == left,cur指针 == left+1。

  2. cur找小,找到后,prev++,a[prev]和a[cur]交换。

  3. 重复步骤2。

  4. cur走完以后,a[prev]和key交换。

图解:

代码:

int Partion3(int* a, int left, int right)
{int keyI = left;int prev = left, cur = prev + 1;
​while (cur <= right){if (a[cur] < a[keyI] && ++prev != cur)Swap(&a[prev], &a[cur]);
​++cur;}Swap(&a[prev], &a[keyI]);
​return prev; 
}

为了避免自己和自己交换,prev先++判断和cur是否相等,相等就不交换。

很明显这种分割方法的代码相比前面两种简单了许多,这种划分法也是最常用的。

5 快排优化

5.1 取key方面的优化

最理想的情况就是key值每次都是中间的值,快排的递归就是一个完美的二分。

快排在面对一些极端数据时效率会明显下降;就比如完全有序的序列,这种序列的基准值key如果再取最左边或者最右边的数,key值就是这个序列的最值,复杂度会变成O(N^2):

这时候就可以用三数取中法来解决这个弊端,三个数为:a[left],a[mid],a[right],这样就可以尽量避免key值选到最值的情况。

//三数取中法选key值
int GetMidIndex(int* a, int left, int right) 
{int mid = (left + right) / 2; 
​if (a[left] < a[mid]){if (a[mid] < a[right])return mid;else if (a[left] > a[right])return left; elsereturn right; }else  //mid < left{if (a[left] < a[right])return left;else if (a[mid] > a[right])return mid; elsereturn right; }
}
​
//前后指针划分
int Partion3(int* a, int left, int right)
{//中间值的下标为midI,a[left]替换为此中间值int midI = GetMidIndex(a, left, right);  Swap(&a[left], &a[midI]); 
​int keyI = left;int prev = left, cur = prev + 1;
​while (cur <= right){if (a[cur] < a[keyI] && ++prev != cur)Swap(&a[prev], &a[cur]);
​++cur;}Swap(&a[prev], &a[keyI]);
​return prev; 
}

除了三数取中法,我们还可以考虑随机数法,都能在一定程度上避免这种极端情况。

srand((unsigned int)time(NULL));
​
//前后指针划分
int Partion3(int* a, int left, int right)
{//随机数取keyint keyI = left + rand() % (right - left + 1);Swap(&a[left], &a[keyI]); 
​int keyI = left;int prev = left, cur = prev + 1;
​while (cur <= right){if (a[cur] < a[keyI] && ++prev != cur)Swap(&a[prev], &a[cur]);
​++cur;}Swap(&a[prev], &a[keyI]);
​return prev; 
}

5.2 递归方面的优化

我们知道,递归深度太深并不是一件好事,所以我们可以针对递归方面来进行优化,减少绝大多数的递归调用。

如何优化呢?当递归到区间内元素个数<=10时,调用直接插入排序。

void _QuickSort(int* a, int begin, int end)
{if (begin >= end)return;
​//区间内元素个数 <= 10,调用直接插入排序if (end - begin + 1 <= 10){InsertSort(a + begin, end - begin + 1);//注意:起始地址是a + begin,不是a}else{//根据基准值把数组划分成左右序列int keyI = Partion3(a, begin, end); 
​//左右序列递归划分下去_QuickSort(a, begin, keyI - 1); _QuickSort(a, keyI + 1, end); }
}

这种优化其实可以减少绝大多数的递归调用,我们把快排的递归划分想象成一颗二叉树,区间长度小于10的数组大概在这棵二叉树的最后三层,而最后三层占了整棵树结点个数的80%多(最后一层50%,倒数第二层25%...),类比快排的递归来看,我们省去了80%多的递归调用,并且对于数据规模较小的情况下,直插和快排的效率差不了多少,所以这是一个极大的优化,算法库中的sort函数也大多是这种优化。

5.3 区间划分方面的优化

快排针对某些极端数据,效率会下降至O(N^2),这种极端数据我们前面说过:

  • 完全有序的序列算是一个,

  • 还有一种极端数据就是数组中某个元素(我们称为x)大量出现,甚至数组中全部都是一个元素x。

针对情况一,我们可以优化取key来解决这个问题,针对情况二,这种方法不奏效。

那么我们可以从区间划分算法下手:

  • 以前的区间划分算法(前后指针法)是双路划分,也就是一遍走完之后,数组被划分成[left, keyI - 1], keyI, [keyI + 1, right]三部分,左区间 < key, 右区间 >= key,然后左右两个区间再递归划分下去;

  • 这种划分方法有一个弊端,就是x大量出现时(甚至整个数组都是一种元素x),会导致左右区间的元素数量严重失衡,导致快排效率下降。

  • 这里我们就可以使用三路划分了,所谓三路划分,就是数组被划分成三部分:< key、==key、 和> key三个部分,我们只需递归划分<key和>key这两部分的区间。

  • 由于key值很容易取到x(一旦取到x,左右区间的size一定会大大减小),这种算法一定程度上提高了效率。

如何实现?详见LeetCode912:

class Solution {
public:pair<int, int> Partion(vector<int>& nums, int begin, int end){//随机数取keyint keyI = begin + rand() % (end - begin + 1);swap(nums[begin], nums[keyI]);
​int key = nums[begin];int left = begin, right  = end, cur = left + 1;while(cur <= right){if(nums[cur] < key){swap(nums[left++], nums[cur++]);}else if(nums[cur] == key){cur++;}else{swap(nums[right--], nums[cur]);}}return make_pair(left, right);}
​void QuickSort(vector<int>& nums, int begin, int end){if(end - begin + 1 <= 1)return;
​pair<int, int> p = Partion(nums, begin, end);
​QuickSort(nums, begin, p.first - 1);QuickSort(nums, p.second + 1, end);}
​vector<int> sortArray(vector<int>& nums) {srand((unsigned int)time(NULL));QuickSort(nums, 0, nums.size() - 1);return nums;}
};

6 快排非递归实现

快排的非递归我们可以使用一个栈(深度优先遍历)或者一个队列实现(广度优先遍历)。

6.1 栈实现(代码+图解)

void QuickSortNonRByStack(int* a, int n)
{Stack st; StackInit(&st);int begin = 0, end = n - 1;//先Push右边界,在Push左边界//记住push顺序,取top的时候左右不要取反了StackPush(&st, end);StackPush(&st, begin);
​while (!StackEmpty(&st)){int begin = StackTop(&st);  StackPop(&st);  int end = StackTop(&st); StackPop(&st); 
​int keyI = Partion3(a, begin, end);//[begin, keyI - 1]  keyI  [keyI + 1, end]
​//先递归到左区间,所以右区间先入栈if (keyI + 1 < end) {//先Push右边界,在Push左边界StackPush(&st, end);   StackPush(&st, keyI + 1);   }
​if (begin < keyI - 1){//先Push右边界,在Push左边界StackPush(&st, keyI - 1); StackPush(&st, begin);}}
​StackDestory(&st);
}
​
 

6.2 队列实现

void QuickSortNonRByQueue(int* a, int n)
{Queue q;QueueInit(&q);int begin = 0, end = n - 1; QueuePush(&q, begin); QueuePush(&q, end); 
​while (!QueueEmpty(&q)){int begin = QueueFront(&q);QueuePop(&q);int end = QueueFront(&q); QueuePop(&q); 
​int keyI = Partion3(a, begin, end); 
​if (begin < keyI - 1) {QueuePush(&q, begin);  QueuePush(&q, keyI - 1);}
​if (keyI + 1 < end) {QueuePush(&q, keyI + 1); QueuePush(&q, end);   }}QueueDestory(&q); 
}

7 特性总结

  • 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序

  • 时间复杂度:O(NlogN)

最好情况,每次key都在中间位置,正好二分

最坏情况,每次key都是最值,复杂度O(N^2)

平均情况(带优化),复杂度O(NlogN)

  • 空间复杂度:O(logN)

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微信小程序入门讲解【超详细】

一. 微信小程序简介 1.1 什么是小程序 2017年度百度百科十大热词之一 微信小程序&#xff08;wei xin xiao cheng xu&#xff09;&#xff0c;简称小程序&#xff0c;英文名Mini Program&#xff0c;是一种不需要下载安装即可使用的应用( 张小龙对其的定义是无需安装&#xf…

【C++进阶】:特殊类的设计

特殊类的设计 一.设计一个类不能被拷贝二.设计一个类只能在堆上创建对象三.设计一个类只能在栈上创建4.设计一个类不能被继承五.设计一个类只能有一个对象(单例模式) 一.设计一个类不能被拷贝 拷贝只会放生在两个场景中&#xff1a;拷贝构造函数以及赋值运算符重载&#xff0c…

《动手学深度学习 Pytorch版》 8.5 循环神经网络的从零开始实现

%matplotlib inline import math import torch from torch import nn from torch.nn import functional as F from d2l import torch as d2lbatch_size, num_steps 32, 35 train_iter, vocab d2l.load_data_time_machine(batch_size, num_steps) # 仍然使用时间机器数据集8.…

微信小程序入门级

目录 一.什么是小程序&#xff1f; 二.小程序可以干什么&#xff1f; 三.入门使用 3.1. 注册 3.2. 安装 3.3.创建项目 3.4.项目结构 3.5.应用 好啦今天就到这里了&#xff0c;希望能帮到你哦&#xff01;&#xff01;&#xff01; 一.什么是小程序&#xff1f; 微信小程…

PyTorch 深度学习之卷积神经网络(基础篇)Basic CNN(九)

0. Revision: Fully connected Neural Network 全连接 1. Convolution Neural Network 保留空间信息 1.1 Convolution Convolution-Single Input Channel 单通道 数乘 3 input Channels 3通道 N input Channels N input Channels and M output channel M 个卷积核 1.2 conv…

npm安装依赖报错npm ERR! code ENOTFOUND npm ERR! errno ENOTFOUND、npm run dev报错记录

npm安装依赖报错npm ERR! code ENOTFOUND npm ERR! errno ENOTFOUND_得我所得&#xff0c;爱我所爱的博客-CSDN博客npm安装依赖报错今天在学习webpack的时候&#xff0c;在使用npm install来安装一个局部的webpack时候&#xff0c;报出一下错误:npm ERR! code ENOTFOUNDnpm ERR…

Python批量测试IP端口GUI程序(Tkinter)

一、实现样式 批量IP与端口中间用“,”分割&#xff0c;点击Telnet进行测试&#xff0c;前提是你电脑安装了telnet客户端&#xff0c;Clear按钮用来清空文本框。 二、核心点 1、使用Tkinter来制作桌面GUI页面 2、使用telnetlib模块测试telnet端口 三、困难点 1、测试结果…

15.项目讲解之前端页面的实现

项目讲解之前端页面的实现 本项目前端使用HBuilerX软件编写HBuilderX下载安装配置一键直达&#xff0c; uniapp框架uniapp官网&#xff0c; 使用Element-ui组件Element-ui组件网址进行前端页面的完成。 前端项目下载地址 前端项目 前端项目展示 首页 首页展示 echarts实现…

前端页面布局之【响应式布局】

目录 &#x1f31f;前言&#x1f31f;优点&#x1f31f;缺点&#x1f31f;media兼容性&#x1f31f;利用CSS3-Media Query实现响应式布局&#x1f31f;常见的媒体类型&#x1f31f;常见的操作符&#x1f31f;属性值&#x1f31f;设备检测&#x1f31f;响应式阈值选取&#x1f3…

【使用教程】在Ubuntu下PMM60系列一体化伺服电机通过SDO跑循环同步位置模式详解

本教程将指导您在Ubuntu操作系统下使用SDO&#xff08;Service Data Object&#xff09;来配置和控制PMM60系列一体化伺服电机以实现循环同步位置模式。我们将介绍必要的步骤和命令&#xff0c;以确保您能够成功地配置和控制PMM系列一体化伺服电机。 01.准备工作 在正式介绍之…

位于同一子网下的ip在子网掩码配置错误的情况下如何进行通信(wireshrak抓包分析)

前言 最近看书发现个问题&#xff0c;正好想学习下wireshark的使用&#xff0c;于是抓包做了下实验。 问题是这样的&#xff0c;假设有服务器A和服务器B&#xff0c;正确配置下两者处于同一子网&#xff1b;此时B的网络配置正确&#xff0c;而A在配置子网掩码时出了错&#xff…

PromptScript:轻量级 DSL 脚本,加速多样化的 LLM 测试与验证

TL&#xff1b;DR 版本 PromptScript 是一个轻量级的 Prompt 调试用的 DSL &#xff08;Yaml&#xff09;脚本&#xff0c;以用于快速使用、构建 Prompt。 PromptScript 文档&#xff1a;https://framework.unitmesh.cc/prompt-script Why PromptScript &#xff1f; 几个月前&…

Qtcreator console 中文 乱码

开发环境&#xff1a;windows11 x64 位&#xff1b;Qt Creator 11.0.3&#xff1b;Based on Qt 6.4.1 (MSVC 2019, x86_64) 报错内容如图所示&#xff1a; 解决方法如下&#xff1a;

unity2022版本 实现手机虚拟操作杆

简介 在许多移动游戏中&#xff0c;虚拟操纵杆是一个重要的用户界面元素&#xff0c;用于控制角色或物体的移动。本文将介绍如何在Unity中实现虚拟操纵杆&#xff0c;提供了一段用于移动控制的代码。我们将讨论不同类型的虚拟操纵杆&#xff0c;如固定和跟随&#xff0c;以及如…

关于EEGLAB安装时报错“未定义函数或者变量‘EEGLAB’”

按照其他博主写的&#xff0c;下载EEGLAB&#xff08;最新版&#xff09;&#xff0c;然后解压&#xff0c;打开matlab&#xff08;2019a&#xff09;导入路径&#xff0c;在前述步骤都正确的情况下&#xff0c;命令行输入eeglab&#xff0c;matlab提示"未定义函数或者变量…

网络原理之初识

文章目录 前言计算机网络的发展史网络互连局域网局域网组建网络的方式 广域网网络通信基础IP 地址端口号网络协议五元组协议分层OSI 七层模型TCP/IP 五层协议网络设备所在分层 封装和分用 前言 在这个信息爆炸的时代&#xff0c;计算机网络已经渗透到我们生活的方方面面&#…

算法村开篇

大家好我是苏麟从今天开始我将带来算法的一些习题和心得体会等等...... 算法村介绍 我们一步步地学习算法本专栏会以闯关的方式来学习算法 循序渐进地系统的学习算法并掌握大部分面试知识 , 期待和大家一起进步 . 索大祝大家学有所成 , 前程似锦.

ESP32网络开发实例-从SD卡加载Web页面文件

从SD卡加载Web页面文件 文章目录 从SD卡加载Web页面文件1、应用介绍2、软件准备3、硬件准备4、Web页面代码实现5、Web服务器代码实现在文中,将展示如何构建一个 Web 服务器,为存储在SD卡中的 HTML 和 CSS 文件提供服务。 我们不必将 HTML 和 CSS 文本硬编码入代码中,而是创建…

多目标水母搜索算法(Multi-Objective Jellyfish Search algorithm,MOJS)求解微电网优化--提供MATLAB代码

一、微网系统运行优化模型 微电网优化模型介绍&#xff1a; 微电网多目标优化调度模型简介_IT猿手的博客-CSDN博客 参考文献&#xff1a; [1]李兴莘,张靖,何宇,等.基于改进粒子群算法的微电网多目标优化调度[J].电力科学与工程, 2021, 37(3):7 二、多目标水母搜索算法MOJS …

局域网上IP多播与IP单播关于MAC地址的区别

IP单播进行到局域网上的时候&#xff1a; 网际层使用IP地址进行寻址&#xff0c;各路由器收到IP数据报后&#xff0c;根据其首部中的目的IP地址的网络号部分&#xff0c;基于路由表进行查表转发。 查表转发的结果可指明IP数据报的下一跳路由器的IP地址&#xff0c;但无法指明…