当前位置: 首页 > news >正文

【数据结构】排序(sort) -- 交换排序(冒泡快排)

目录

一、交换排序

二、冒泡排序(bubble sort)

1. 基本思想

2. 思路介绍

3. 代码实现

4. 冒泡排序的优化

5. 特性总结

三、快速排序(quick sort)

1. 基本思想

2. 划分思路的分类

(1)hoare版本

a. 思路介绍

b. 代码实现

(2)挖坑法

a. 思路介绍

b .代码实现

(3)前后指针版本

a. 思路介绍

b. 代码实现

3. 对快速排序的优化

(1)优化的原因

(2)优化办法

4. 快速排序的非递归实现

5. 特性总结

四、总结


一、交换排序

常见的排序算法有:

而本篇文章要介绍的是交换排序。

插入排序可以分为 冒泡排序 和 快速排序 。本文重点介绍了快速排序的认识


二、冒泡排序(bubble sort)

1. 基本思想

        通过多次遍历待排序的序列,依次比较相邻元素,并根据大小关系交换它们的位置,使得每一轮遍历后,当前未排序部分的最大(或最小)元素“冒泡”到正确的位置。

2. 思路介绍

思路:

  1. 将待排序的数据划分为有序区和无序区,初始时有序区为空,无序区包含所有待排序数据。
  2. 对于无序区从前向后依次相邻数据进行比较,,若返序则交换,从而时值较小的数据向前移动,值较大的数据向后移动。(就像水中的气泡,体积大的现浮上来)
  3. 重复步骤2,直到无序区没有返序的数据。

对于一组数据:3,44,38,5,47,15,36,26,27,2,46,4,19,50,48  它的排序个过程如下:

排序动态图如下:

3. 代码实现

C语言代码实现:

void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}// 冒泡排序
void BubbleSort(int* a, int n)
{for (int i = 0; i < n; i++)// n个数据比较n-1趟{for (int j = 1; j < n - i; j++)// 每一趟比较n-1-i{if (a[j - 1] > a[j]){Swap(&a[j - 1], &a[j]);}}}
}

4. 冒泡排序的优化

上述冒泡排序的代码中,可以发现无论数据是否有序,它都会消耗O(n^2)的时间性能。如果要解决这一问题,可以增加一个判断标志,判断是否已经有序。

// 冒泡排序
void BubbleSort(int* a, int n)
{for (int i = 0; i < n; i++)// n个数据比较n-1趟{bool exchange = false;for (int j = 1; j < n - i; j++)// 每一趟比较n-1-i{if (a[j - 1] > a[j]){//如果已经进入了需要交换的这一步,说明数据还存在无序的数据Swap(&a[j - 1], &a[j]);exchange = true;}}if (exchange == false){break;}}
}

5. 特性总结

冒泡排序的特性总结:

  1. 冒泡排序是一种非常容易理解的排序
  2. 时间复杂度:O(N^2) 
  3. 空间复杂度:O(1)
  4. 稳定性:稳定

三、快速排序(quick sort)

1. 基本思想

        快速排序是一种高效的分治排序算法,其核心思想是通过选择一个基准元素(pivot),将序列划分为两部分,使得基准元素的左边部分的所有元素小于等于基准值,基准元素的右边部分的所有元素大于等于基准值,然后递归地对这两部分进行排序,左右子序列重复该过程,直到所有元素都排列在相应位置上为止。最终完成整个序列的排序。

排序基本逻辑:

        首先选择基准元素key值,基准值下标为keyi。然后进行划分,使得基准元素的左边部分的所有元素小于等于基准值,基准元素的右边部分的所有元素大于等于基准值,此时key所在位置的数据位置就定下来了。再分别对左右两边的区间 [left, keyi-1] 和 [keyi+1, right] 分别进行划分,当然每次划分后都会出现这两种区间,重复此过程,直到每个划分区间只有一个元素就停止划分。如图所示:

可以发现这二叉树有点相似。

代码逻辑实现(递归):

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

2. 划分思路的分类

        知道了快排的逻辑方法,只要通过划分找到每一次划分后的key的位置就可以完成这个快排算法了。对于划分方法,又可以分为多种,如:hoare法,挖坑法,前后指针法。但它们都有一个共同点:都需要选取一个位置的元素为基准值,我们选取的是最左边的位置或者最右边的位置,不会选取中间的位置。对于不同的排序,需要注意:

  • 排升序:
  1. 如果选取左边做key,就要从右边先走,找小
  2. 如果选取右边做key,就要从左边先走,找大
  • 排降序:
  1. 如果选取左边做key,就要从右边先走,找大
  2. 如果选取右边做key,就要从左边先走,找小

以下版本都是以选取左边做key,就要从右边先走,找小,排升序为例的。

(1)hoare版本

a. 思路介绍

划分操作:

对于区间 [left, right ] 进行一次划分(如果选取左边做key,就要从右边先走,找小--排升序为例):

        此时key所在位置是左边第一个元有位置。右边的right先开始向左移动,找比key值更小的元素,找到后停下,然后,左边的left才向右开始移动,找比key值大的元素,找到后停下,然后交换此时left和right所在的元素。重复操作,直到left和right相遇,相遇后就将key所在位置的元素与相遇位置的元素进行交换。此时,key的左边数据就都小于或等于key,右边数据就大于或等于key,将原来的区间划分为了两部分。

        这样就完成了一次划分

如图是选取左边做key,右边取小的一次划分的一个动态图示例:

划分详细过程如下:

b. 代码实现

对于划分部分的代码实现:

//hoare版本
int partion(int* a, int left, int right)
{//对[left,right]这部分区间的数据进行划分,并返回划分后的key值int keyi = left;//初始时,key值在该区间最左边;keyi为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]);//交换左右两边的值}//此时left=rightSwap(&a[keyi], &a[left]);keyi = left;return keyi;
}

(2)挖坑法

a. 思路介绍

进行划分的操作是:

对于区间 [left, right ] 进行一次划分(如果选取左边做key,就要从右边先走,找小--排升序为例):

        将基准值(左边第一个元素)放在一个临时变量key中,此时在第一个元素位置就形成了一个坑位,然后从右边(即另一边)开始找比key值更小的值,找到后,将找到的值放入上次的坑中,此时该位置有形成了一个坑,再从左边开始找比可以key值更大的值,找到后,将找到的值放入上一次形成的坑中。重复次操作,直到left与right相遇。相遇时,相遇为值又是一个坑,这时就将key值填入该位置。此时key的左边数据就都小于或等于key,右边数据就大于或等于key,将原来的区间划分为了两部分。

如图是挖坑法的选取左边做key,右边取小的一次划分的一个动态图示例:

b .代码实现

C语言实现:

//挖坑法
int partion(int* a, int left, int right)
{//对[left,right]这部分区间的数据进行划分,并返回划分后的key值所在的位置int key = a[left];//初始时,key值在该区间最左边;将a[left]的值用变量key存储起来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;//此时形成新的坑位}//此时left=right=holea[hole] = key;return hole;
}

(3)前后指针版本

a. 思路介绍

对于区间 [left, right ] 进行一次划分的思路:

        它需要两个指针prev和cur。初始时prev指向该序列最前面的开头,而cur指向prev的后一个位置。

然后开始判断cur指向的数据与key的大小:

  • 如果cur指向的数据比key小,找到后,prev后移一位,再将prev和cur指向的数据相互互换,最后cur再后移一位。
  • 如果cur指向的数据比key大,则只用将cur后移一位。

        重复上述操作,直到cur越界,越界后就将prev指向的位置的数据与key所在位置进行交换。

        这样也保证了此时key的左边数据就都小于或等于key,右边数据就大于或等于key,将原来的区间划分为了两部分。

说明:prev一定是在cur的前面的。

如图是前后指针法的选取左边做key,右边取小的一次划分的一个动态图示例:

b. 代码实现

C语言代码实现:

//前后指针法
int partion(int* a, int left, int right)
{//对[left,right]这部分区间的数据进行划分,并返回划分后的key值int keyi = left;//初始时,key值在该区间最左边;keyi为key值的下标,因为通过下标才能修改数组的内容//在数组中,prev和cur用下标表示比较方便int prev = left;int cur = prev + 1;while (cur <= right){if (a[cur] < a[keyi]){prev++;Swap(&a[cur], &a[prev]);}cur++;//不论a[cur]比key大还是小,都会将cur后移}//此时cur已经越界Swap(&a[keyi], &a[prev]);keyi = prev;return keyi;
}

3. 对快速排序的优化

(1)优化的原因

时间复杂度最好情况:

        理想状态下,快排每次在划分完后,都会产生2个大小相等的区间 [left, keyi-1] 和 [keyi+1, right],而这2个区间再次进行划分,又会产生4个大小相等的区间,这4个区间继续进行划分,又会产生8个大小相等的区间,重复划分下去,直到每个区间中只有一个元素时才停止划分。

        这种情况就和满二叉树的递归相似。这样子的时间性能就是快排中最好的情况。

        时间复杂度是O(N*logN)。

时间复杂度最坏情况:

但是,如果我们要排序的是一个逆序的数据。我们还是选择第一个数据转为基准值key,那么这时在进行划分时,就不会有两个区间了。因为是逆序,那么就有:如果要排升序,而此时是一个降序的话,key仍然选择第一个数据,当进行第一次划分后,key就就处在最后一个位置了,此时,假如有n个数,那么key前面就有n-1个数据,后面就没有数据了,此时第一次划分就结束了。然后再对前面这个区间进行划分,如此重复的话,直到只剩第一个数据时排序才结束。如图:

同理,如果对一个升序,排降序也是如此。

当然这种情况是最坏的情况了,时间复杂度为O(n^{2})

原因:

        以对一个降序,要排升序为例。由于是一个降序,所以第一个元素一定是最大的一个元素,最大的元素如果在升序中,一定是处于最后一个元素的,所以,对于第一次划分,就是将第一个元素固定位置,只是要固定的位置是最后一个元素而已,同样的,对于后续的区间划分也是如此。如此重复,就会出现这种情况了。

解决办法;

        如果遇到这种情况,就需要对所选的基准值key进行调整,尽量不让它处于最大或最小,如果比较处在中间大小就很好了。

(2)优化办法

方法一:随机选key

        由于我们所选的key的位置一定是在第一个或者最后一个,即位置不能变,所以就需要找一个值来与它进行交换。

        那么就可以通过产生随机下标,让产生的随机下标对应的值与第一个位置的值进行交换。

C语言代码实现如下:

//交换
void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
//hoare版本
int partion1(int* a, int left, int right)
{// 随机选keyint randi = left + (rand() % (right - left));Swap(&a[left], &a[randi]);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]);//交换左右两边的值}//此时left=rightSwap(&a[keyi], &a[left]);keyi = left;return keyi;
}//挖坑法
int partion2(int* a, int left, int right)
{// 随机选keyint randi = left + (rand() % (right - left));Swap(&a[left], &a[randi]);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;//此时形成新的坑位}//此时left=right=holea[hole] = key;return hole;
}//前后指针法
int partion3(int* a, int left, int right)
{// 随机选keyint randi = left + (rand() % (right - left));Swap(&a[left], &a[randi]);int keyi = left;//在数组中,prev和cur用下标表示比较方便int prev = left;int cur = prev + 1;while (cur <= right){if (a[cur] < a[keyi]){prev++;Swap(&a[cur], &a[prev]);}cur++;//不论a[cur]比key大还是小,都会将cur后移}//此时cur已经越界Swap(&a[keyi], &a[prev]);keyi = prev;return keyi;
}

方法二:三数取中

让待排序数组这个区间中的第一个数据、中间的数据以及最后一个数据,进行比较,选择中间的数据做key,当然这得到的数据也要和这个区间的第一个数据进行交换。

C语言代码实现如下:

//得到中间数
int GetMidNumi(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;}else{return right;}}else // a[left] > a[mid]{if (a[mid] > a[right]){return mid;}else if (a[left] < a[right]){return left;}else{return right;}}
}
//交换
void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
//hoare版本
int partion1(int* a, int left, int right)
{//三数取中int midi = GetMidNumi(a, left, right);if(midi != left)//避免重复{Swap(&a[midi], &a[left]);}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]);//交换左右两边的值}//此时left=rightSwap(&a[keyi], &a[left]);keyi = left;return keyi;
}//挖坑法
int partion2(int* a, int left, int right)
{//三数取中int midi = GetMidNumi(a, left, right);if (midi != left)//避免重复{Swap(&a[midi], &a[left]);}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;//此时形成新的坑位}//此时left=right=holea[hole] = key;return hole;
}//前后指针法
int partion3(int* a, int left, int right)
{//三数取中int midi = GetMidNumi(a, left, right);if (midi != left)//避免重复{Swap(&a[midi], &a[left]);}int keyi = left;//在数组中,prev和cur用下标表示比较方便int prev = left;int cur = prev + 1;while (cur <= right){if (a[cur] < a[keyi]){prev++;Swap(&a[cur], &a[prev]);}cur++;//不论a[cur]比key大还是小,都会将cur后移}//此时cur已经越界Swap(&a[keyi], &a[prev]);keyi = prev;return keyi;
}

4. 快速排序的非递归实现

        快速排序的非递归实现需要借助 这一数据结构来实现。

        使用栈,其中在栈中存储的元素类型是区间。初始时,将待排序的数据的区间先入栈,然后通过取栈顶的操作,来得到待划分区间,然后间其出栈,再对所取的栈顶元素进行划分,划分完后,会产生两个区间,如果这两个区间中有元素个数一定大于1的区间,就将该元素个数一定大于1的区间入栈。后续重复次操作,直到栈为空才停止。这时就排完序了。

        对于,某一数据进行的非递归快排的操作如图所示:

代码实现:

使用栈的结构,就需要使用到栈的处理函数。

栈的知识链接为:【数据结构】栈(stack)_数据结构栈-CSDN博客

则C语言代码实现:

void QuickSortNotRecursive(int* a, int left, int right)
{Stack st;Init(&st);//存储区间Interval tmp = { left,right };//将初始数据区间入栈Push(&st, tmp);//直到栈空才停止while (!IsEmpty(&st)){Interval cur = GetTop(&st);//取栈顶Pop(&st);//出栈int keyi= partion1(a, cur.left, cur.right);//划分区间//划分后:[cur.left,keyi-1]  [keyi+1,cur.right]//判断是否有元素个数大于1的区间,若有,则将它们入栈if (cur.left < keyi - 1){Interval tmp = { cur.left,keyi - 1 };Push(&st, tmp);}if (keyi + 1 < cur.right){Interval tmp = { keyi + 1,cur.right };Push(&st, tmp);}}Destroy(&st);//销毁动态顺序表
}

5. 特性总结

快速排序的特性总结

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(logN)
  4. 稳定性:不稳定

四、总结

  • 冒泡排序通过多次遍历序列,比较相邻元素并交换位置,时间复杂度O(n²),空间复杂度O(1)。
  • 快速排序采用分治思想,选取基准值将序列划分为两部分递归排序,平均时间复杂度O(nlogn),但最坏情况会退化为O(n²),文章详细介绍了hoare、挖坑法和前后指针三种划分方法,并提出了随机选key和三数取中两种优化方案,最后还给出了快速排序的非递归实现。

感谢各位观看!希望能多多支持!

http://www.dtcms.com/a/318966.html

相关文章:

  • 接入免费的数字人API接口详细教程!!!——小甲鱼数字人
  • OpenCV的关于图片的一些运用
  • 一个基于 select 实现的多路复用 TCP 服务器程序:
  • Opencv-管理图片
  • 计算机视觉--opencv(代码详细教程)
  • ansible-playbook之获取服务器IP存储到本地文件
  • Spring事务失效场景?
  • 光纤滑环 – 光纤旋转接头(FORJ)- 杭州驰宏科技
  • 科技云报到:热链路革命:阿卡 CRM 的 GTM 定位突围
  • 芯谷科技--高效噪声降低解决方案压缩扩展器D5015
  • 全球化2.0 | 泰国IT服务商携手云轴科技ZStack重塑云租赁新生态
  • 安全守护,温情陪伴 — 智慧养老产品上新
  • Element Plus实现分页查询
  • 码头岸电系统如何保障供电安全?安科瑞绝缘监测及故障定位方案解析
  • Rust爬虫与代理池技术解析
  • NAS技术在县级融媒体中心的架构设计与安全运维浅析
  • VSCode ssh一直在Setting up SSH Host xxx: Copying VS Code Server to host with scp等待
  • 支付宝小程序商城怎么搭?ZKmall开源商城教你借力蚂蚁生态做增长
  • 【Agent】ReAct:最经典的Agent设计框架
  • 【pytorch(06)】全连接神经网络:基本组件认知,线性层、激活函数、损失函数、优化器
  • Django 表单:深度解析与最佳实践
  • 高性能分布式通信框架:eCAL 介绍与应用
  • 解锁高效开发:AWS 前端 Web 与移动应用解决方案详解
  • 区块链技术原理(2) -数据结构
  • 云平台运维工具 ——AWS 原生工具
  • 告别Cursor!最强AI编程辅助Claude Code安装到使用全流程讲解
  • MySQL面试题及详细答案 155道(061-080)
  • 【图文教程】三步用Cpolar+JuiceSSH实现手机远程连接内网Linux虚拟机
  • 平台服务器被入侵,使用WAF能防范吗?
  • 机器学习——04 逻辑回归