玩转快速排序(C语言版)

W...Y的主页 😊

代码仓库分享  💕


🍔前言:

本篇文章,我们来讲解一下神秘的快速排序。对于快速排序我相信大家都已经有所耳闻,但是快速排序是有很多的版本的。我们这次的目的就是快排的所有内容搞懂,废话不多说,让我们开始今天的内容。

🍟目录

快排的介绍

hoare版本

单趟排序 

 多趟排序

挖坑法

前后指针版本(双指针)

快排的优化

三数取中

小区间优化

快速排序之非递归


快排的介绍

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

基本思想:

  1. 选择一个基准元素(pivot)。可以选择数据集中的任意一个元素作为基准,一般情况下选择第一个元素或者最后一个元素作为基准。

  2. 将数据集中的其他元素按照与基准元素的大小关系进行分区。将小于基准的元素放在基准的左边,将大于基准的元素放在基准的右边。这个过程称为分区操作。

  3. 对分区后的左右两个子集,分别递归地应用相同的方法,继续进行分区操作,直到每个子集只包含一个元素或为空。

  4. 最后,将所有子集按照顺序拼接起来,就得到了一个有序的数据集。

快速排序的基本框架:

// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{
if(right - left <= 1)
return;
// 按照基准值对array数组的 [left, right)区间中的元素进行划分
int div = partion(array, left, right);
// 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
// 递归排[left, div)
QuickSort(array, left, div);
// 递归排[div+1, right)
QuickSort(array, div+1, right);
}

上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,同学们在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。

接下来我们来开始进入快速排序!!!

hoare版本

这是最原始的版本,也是第一代创始人hoare发明的版本。我们先来进行快排的单趟交换过程:

单趟排序 

这张图片就是hoare版本的全部过程,我们先来分析第一次的交换过程。

单趟过程:首先选取数组最左边的元素标记为key,然后创建两个指针left与right分布到数组的两边。right的指针先走,寻找比key小的元素,找不到就往前走,找到就停止。然后left后走,寻找比key值大的元素,找到后两个值交换即可。

然后我们来看一下单趟的代码:

while (left < right){while (left < right && a[right] >= a[keyi]){--right;}while (left < right && a[left] <= a[keyi]){++left;}Swap(&a[left], &a[right]);}

代码易错点: 1. 假设数组为上图数组,right寻找比key小的值,right指针就会一直往前寻找,就会导致数组越界。所以我们必须在while循环条件中加入(left<right)。

2.我们在while循环中可以看到第二个判断条件,是否加等于号。必须要加等于号,否则当left与right指向的元素相等时,交换后的数组继续进行循环就会出现死循环。下图为特殊情况图:

 多趟排序

当上面的单趟走完后,我们会发现,keyi左边的全是小于a[keyi]的,右边全是大于a[keyi]的。

当交换完所有的数后,left指针与right指针就会相遇,我们将相遇的地方与key元素交换即可完成一次排序。现在的数组相对于刚才已经变得有序了一点。我们可以分割重复这个思想,就可以完成整个排序。

分割思路:

类似于二叉树思想,我们可以将其进行风格key左边的都小于key,key右边的都大于key。将数组从key中间一分为二。

 将数组划分为[begin,keyi-1],  keyi,   [keyi+1,end],然后与上面单趟排序思想一致,继续进行递归排序。递归结束条件:当begin == end  或者是 数组错误(begin>end)时,则为结束条件。

那为什么相遇位置一定比key小呢?怎么确定的?

这就要多亏与right指针先走。

相遇情况:

1.R动L不动,去跟L相遇。 相遇位置一定是R的位置,L和R在上一轮交换过,交换以后L的位置的值一定比key小。

2.L动R不动,去跟R相遇。相遇位置一定是L的位置,L的位置比key小。

完整代码如下:

void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
int PartSort(int* a, int left, int right)
{int keyi = left;while (left < right){while (left < right && a[right] >= a[keyi]){--right;}while (left < right && a[left] <= a[keyi]){++left;}Swap(&a[left], &a[right]);}Swap(&a[left], &a[keyi]);return left;
}
void QuickSort(int* a, int begin, int end)
{if (begin >= end)return;int keyi = PartSort3(a, begin, end);QuickSort(a, begin, keyi - 1);QuickSort(a, keyi + 1, end);
}

挖坑法

对于hoare版本,许多人会觉得不好理解,觉得非常抽像。现在我们来讲一种更加直观的方法,虽然算法思路一样,但是更容易理解。

我们先来看一下思路流程图:

算法思路:

将最左边设置为key并且设置为坑位。什么是坑位,就是将其看成空位。所以我们就要将key对应的元素值进行标记存储防止被覆盖。然后创建两个指针left与right,与key值进行比较。right先走寻找比key小的值,找到后将其填入坑中。然后right指向的数组成为新的坑位。right停止left寻找比key大的元素,找到后将其移动到新坑位,这样依次类推即可。

大致思路与hoare版本相同,唯一不同点就是加入了”坑位“,我们可以看作数组的填空。

在这里我们就不过多赘述了,直接展示源代码:

void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
int PartSort2(int* a, int left, int right)
{int hoop = a[left];while (left < right){while (left < right && a[right] >= hoop){right--;}a[left] = a[right];while (left < right && a[left] <= hoop){left++;}a[right] = a[left];}a[left] = hoop;return left;
}
void QuickSort(int* a, int begin, int end)
{if (begin >= end)return;int keyi = PartSort3(a, begin, end);QuickSort(a, begin, keyi - 1);QuickSort(a, keyi + 1, end);
}

 这里我们还是使用了递归的思想进行操作。

前后指针版本(双指针)

前后指针的思路与前两个有很大的区别,也是目前最流行的写法。我们还是从图中了解一下其中原理。

算法思路:

创建两个指针变量,一个prev指向最开始节点,cur指针指向prev的后方,也就是cur = prev + 1。cur去寻找比key大的值,如果cur指向比key小,prev先加1然后进行交换(在前面如果没遇到比key大的值,就相当于自己给自己赋值)。当遇到比key大的值时,prev不动,cur++。直到cur>=数组长度end即可停止。当循环结束,我们将prev指向的位置与key交换,这样单趟思路就搞定了。

多趟思路就是进行拆分递归,下来就是代码展示:

void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
int PartSort3(int* a, int left, int right)
{int keyi = left;int prev = left;int cur = left + 1;while (cur <= right){if (a[cur] < a[keyi] && ++prev != cur){Swap(&a[cur], &a[prev]);}cur++;}Swap(&a[prev], &a[keyi]);return prev;
}
void QuickSort(int* a, int begin, int end)
{if (begin >= end)return;int keyi = PartSort3(a, begin, end);QuickSort(a, begin, keyi - 1);QuickSort(a, keyi + 1, end);
}

注意:在遍历时,判断++prev != cur可以去掉,这样做是防止自己给自己赋值,优化程序。 

快排的优化

三数取中

快排的速度非常快,一般情况下为O(nlogn),但是在特殊情况下速度就会特别慢,达到了O(n^2).那是什么情况下会出现这种场景呢?
key是取决于速度快慢的关键,当key值取到的值为数组元素的中间值时就非常理想。 

但是如果这个数组是比较有序的,每次取到的key值都是最大值或最小值,那么指针在寻找需要的元素时基本都会直接遍历整个数组。 所以我们就需要进行”三数取中“,我们知道数组的头尾,就可以算出数组的中间值,我们将这三个位置的数组进行比较,取最中间的值作为key即可。

这样做就可以优化快排最坏的情况,让时间复杂的基本维持到O(nlogn)。

代码展示:

int GetMidi(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 // a[left] > a[mid]{if (a[left] < a[right]){return left;}else if (a[right] > a[mid]){return right;}elsereturn mid;}
}

小区间优化

对于大量的数,我们可以使用快速排序,但是一些小区间的数使用快排反而没有插入排序……效率高, 所以我们进行小区间优化。如果数组大小小于10个,我们就使用插入排序即可,如果大于10个数就可以使用快排进行。

void QuickSort1(int* a, int begin, int end)
{if (begin >= end)return;if ((end - begin + 1) > 10){int keyi = PartSort3(a, begin, end);QuickSort(a, begin, keyi - 1);QuickSort(a, keyi + 1, end);}else{InsertSort(a + begin, end - begin + 1);}
}

插入排序的源代码在这寻找:插入排序之希尔排序——【数据结构】-CSDN博客

使用插入排序区间不一定是从头开始,所以在传参时要加begin。

快速排序之非递归

因为函数递归实在栈上开辟空间的,而栈上的空间只有4G左右,如果递归层数太深就会导致栈溢出。而非递归就是在堆上开辟空间,堆的空间非常大,我们有足够的空间去开辟,所以我们就使用非递归来完成。

算法思路:

我们利用栈先进后出的优势,存储其前后区间即可。假设数组区间为0~9,我们先存储右边再存储左边(因为取值时就可以先取左边再取出右边),然后进行函数调用排序将区间划分为[left , keyi - 1] keyi [ keyi +  1 , right ] ,再将区间的值进行右左入栈,然后再取两个值作为区间进行函数调用以此类推,直到栈为空为止。

我们将函数的递归转换成用栈来取区间。 

注意:一定要先存储右边,再存储左边 

代码展示:

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>typedef int STDataType;
typedef struct Stack
{STDataType* a;int top;int capacity;
}ST;void STInit(ST* ps);
void STDestroy(ST* ps);
void STPush(ST* ps, STDataType x);
void STPop(ST* ps);
int STSize(ST* ps);
bool STEmpty(ST* ps);
STDataType STTop(ST* ps);
#define _CRT_SECURE_NO_WARNINGS 1
#include"Stack.h"
void STInit(ST* ps)
{assert(ps);ps->a = NULL;ps->capacity = 0;ps->top = 0;
}
void STDestroy(ST* ps)
{assert(ps);free(ps->a);ps->a = NULL;ps->top = ps->capacity = 0;
}
void STPush(ST* ps, STDataType x)
{assert(ps);if (ps->top == ps->capacity){int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;STDataType* tmp = (STDataType*)realloc(ps->a, sizeof(STDataType) * newCapacity);if (tmp == NULL){perror("realloc");exit(-1);}ps->a = tmp;ps->capacity = newCapacity;}ps->a[ps->top] = x;ps->top++;
}
void STPop(ST* ps)
{assert(ps);assert(ps->top > 0);--ps->top;
}
int STSize(ST* ps)
{assert(ps);return ps->top;
}
bool STEmpty(ST* ps)
{assert(ps);return ps->top == 0;
}
STDataType STTop(ST* ps)
{assert(ps);assert(ps->top > 0);return ps->a[ps->top - 1];
}
void QuickSortNonR(int* a, int begin, int end)
{ST st;STInit(&st);STPush(&st, end);STPush(&st, begin);while (!STEmpty(&st)){int left = STTop(&st);STPop(&st);int right = STTop(&st);STPop(&st);int keyi = PartSort3(a, left, right);if (keyi + 1 < right){STPush(&st, right);STPush(&st, keyi + 1);}if (left < keyi - 1){STPush(&st, keyi - 1);STPush(&st, left);}}STDestroy(&st);
}

以上是本次快速排序全部内容,对大家有帮助的麻烦三连支持一下!!!

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

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

相关文章

什么是GraphQL?它与传统的REST API有什么不同?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 什么是GraphQL&#xff1f;⭐ 与传统的REST API 的不同⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣…

JavaScript处理点击事件

在介绍点击事件之前&#xff0c;先给它们讲一些概念 事件监听 在JavaScript中&#xff0c;可以使用事件监听&#xff08;Event Listener&#xff09;来响应和处理各种事件。事件监听器是一种能够捕捉特定事件并执行相应代码的机制。事件监听器允许您在特定事件发生时执行自定…

Eclipse MAT解析headp dump,total size小于file size

1. 问题描述 使用Eclipse MAT分析20GB的heap dump文件 最后解析出来dump size只有1GB 2. 原因&#xff1a;heap dump中包含许多unreachable objects Eclipse MAT的官方文档&#xff0c;《Basic Tutorial》章节&#xff0c;有对上图的Overview page做介绍 针对total size小…

Qt扩展-QCustomPlot 用户交互

QCustomPlot 用户交互 一、概述二、操作范围三、选择机制1. 控制Graph的可选择性和选择状态2. 所选对象的外观3. 多部分对象4. 对选择变化做出反应 四、用户交互信号 一、概述 QCustomPlot提供了多个内置的用户交互。它们大致可以分为 通过用鼠标拖动和滚动鼠标滚轮进行范围操…

typescript映射类型

ts映射类型简介 TypeScript中的映射类型&#xff08;Mapped Type&#xff09;是一种高级类型&#xff0c;它允许我们基于现有类型创建新的类型&#xff0c;同时对新类型的每个属性应用一个转换函数。通过使用映射类型&#xff0c;我们可以方便地对对象的属性进行批量操作&…

华为数通方向HCIP-DataCom H12-831题库(单选题:181-200)

第181题 以下关于OSPF的5类LSA中的转发地址(ForwardingAddress,FA) 的描述,正确的是哪一项? A、当FA地址为0.0.0.0时,收到该LSA的路由器认为到达目的网段的数据包应该发往对应的ABR,因此将到达ABR的下一跳地址作为这条外部路由的下一跳 B、当FA地址为0.0.0.0时,收到该LS…

C++算法 —— 动态规划(12)两道小题

文章目录 1、动规思路简介2、组合总和Ⅳ3、卡特兰数 背包问题需要读者先明白动态规划是什么&#xff0c;理解动规的思路&#xff0c;并不能给刚接触动规的人学习。所以最好是看了之前的动规博客&#xff0c;以及背包博客&#xff0c;或者你本人就已经懂得动规了。 1、动规思路简…

(二)激光线扫描-相机标定

1. 何为相机标定? 当相机拍摄照片时,我们看到的图像通常与我们实际看到的不完全相同。这是由相机镜头引起的,而且发生的频率比我们想象的要高。 这种图像的改变就是我们所说的畸变。一般来说,畸变是指直线在图像中出现弯曲或弯曲。 这种畸变我们可以通过相机标定来进行解…

华为云云耀云服务器L实例评测|Huawei Cloud EulerOS 自动化环境部署

[toc] Huawei Cloud EulerOS 自动化环境部署 云耀云服务器L实例【Huawei Cloud EulerOS 2.0 64bit】 Python Git Google Chrome Chromedriver Selenium More… 1. Python 镜像创建后自带。 2.Git 拉取项目。 sudo yum install git3. Google Chrome 使用root权限或sudo权…

【算法|动态规划No.11】leetcode53. 最大子数组和

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【手撕算法系列专栏】【LeetCode】 &#x1f354;本专栏旨在提高自己算法能力的同时&#xff0c;记录一下自己的学习过程&#xff0c;希望…

【image captioning】CaMEL: Mean Teacher Learning for Image Captioning(实现流程)

CaMEL: Mean Teacher Learning for Image Captioning(实现流程) 作者:安静到无声 个人主页 目录 CaMEL: Mean Teacher Learning for Image Captioning(实现流程)环境设置数据准备Evaluation训练程序推荐专栏参考代码: CaMEL: Mean Teacher Learning for Image Captioning.…

C++_pen_静态与常量

成员 常成员、常对象&#xff08;C推荐使用 const 而不用#define,mutable&#xff09; const 数据成员只在某个对象生存周期内是常量&#xff0c;而对于整个类而言却是可变的&#xff08;static除外&#xff09; 1.常数据成员&#xff08;构造函数初始化表赋值&#xff09; c…

常见的几种排序方式

常见的几种排序方式 1. 排序的概念2. 常见排序算法的实现2.1 插入排序2.1.1基本思想2.1.2 直接插入排序2.1.3 希尔排序( 缩小增量排序 ) 2.2 选择排序2.2.1基本思想2.2.2 直接选择排序:2.2.3 堆排序 2.3 交换排序2.3.1冒泡排序2.3.2 快速排序 2.4 归并排序2.4.1 基本思想2.4.2 …

【C语言进阶(11)】动态内存管理

文章目录 Ⅰ 存在动态内存分配的原因Ⅱ 动态内存函数1. malloc2. calloc3. realloc4. free (重要) Ⅲ 常见动态内存错误1. 对 NULL 指针的解引用操作2. 对动态开辟空间的越界访问3. 对非动态开辟内存使用 free 释放4. 使用 free 释放一块动态开辟内存的一部分5. 对同一块动态内…

Rust冒泡排序

Rust冒泡排序 这段代码定义了一个名为 bubble_sort 的函数&#xff0c;接受一个可变的整数类型数组作为输入&#xff0c;然后使用嵌套的循环来实现冒泡排序。外部循环从数组的第一个元素开始迭代到倒数第二个元素&#xff0c;内部循环从数组的第一个元素开始迭代到倒数第二个元…

【10】c++设计模式——>依赖倒转原则

关于依赖倒转原则&#xff0c;对应的是两条非常抽象的描述&#xff1a; 1.高层模块不应该依赖低层模块&#xff0c;两个都应该依赖抽象。 2.抽象不应该依赖细节&#xff0c;细节应该依赖抽象。 先用人话解释一下这两句话中的一些抽象概念&#xff1a; 1.高层模块&#xff1a;可…

字典与数组第七讲:工作表数据计算时为什么要采用数组公式(一)

《VBA数组与字典方案》教程&#xff08;10144533&#xff09;是我推出的第三套教程&#xff0c;目前已经是第二版修订了。这套教程定位于中级&#xff0c;字典是VBA的精华&#xff0c;我要求学员必学。7.1.3.9教程和手册掌握后&#xff0c;可以解决大多数工作中遇到的实际问题。…

使用关键字interface来声明使用接口-PHP8知识详解

继承特性简化了对象、类的创建&#xff0c;增加了代码的可重用性。但是php8只支持单继承&#xff0c;如果想实现多继承&#xff0c;就需要使用接口。PHP8可以实现多个接口。 接口类通过关键字interface来声明&#xff0c;接口中不能声明变量&#xff0c;只能使用关键字const声明…

【Golang】并发

并发 有人把Go语言比作 21 世纪的C语言 第一是因为Go语言设计简单 第二则是因为 21 世纪最重要的就是并发程序设计&#xff0c;而 Go 从语言层面就支持并发。同时实现了自动垃圾回收机制 先来了解一些概念&#xff1a; 进程/线程 进程是程序在操作系统中的一次执行过程&#…

创建GCP service账号并管理权限

列出当前GCP项目的所有service account 我们可以用gcloud 命令 gcloud iam service-accounts list gcloud iam service-accounts list DISPLAY NAME EMAIL DISABLED terraform …