【初阶数据结构】排序——交换排序

ops/2024/10/4 15:04:53/

目录

  • 前言
  • 冒泡排序
  • 快速排序
    • Hoare版
    • 前后指针版
    • 优化
      • 三数取中法
      • 取随机数做基准值
      • 小区间优化
    • 快排非递归版

请添加图片描述

前言

对于常见的排序算法有以下几种:
在这里插入图片描述
下面这节我们来看交换排序算法

冒泡排序

基本思想
在待排序序列中,每一次将相邻的元素进行两两比较,将较大(升序)或者较小(降序)的数字往后走每走完一轮最大或者最小的数都会在最后面

排序过程:
我们给一个要排序的序列,并想让其升序排序:

int arr[] = {5, 3, 9, 6, 2, 4, 7, 1, 8};

在这里插入图片描述
在这里插入图片描述
  我们每排完一趟就会有在剩余待排序序列中的最大值被放到最后。

  我们就可以用两层循环来控制。

  • 外循环控制冒泡排序的趟数,或者叫做轮数,由上图可以看出有n个数,则要排n-1轮
  • 内循环:控制每一轮排序时两两之间的比较,可以发现,随着轮数的增加,每排好一个数,两两之间的比较次数就会少一次。最开始的比较次数同样为n-1次。

由此我们可写得循环为:

for(int i = 0; i < n - 1; i++)
{for(int j = 1; j < n - i; j++)//最开始的比较次数共n-1次{//......}
}

完整代码:

void Swap(int* px, int* py)
{int tmp = *px;*px = *py;*py = tmp;
}
void BubbleSort(int* a, int n)
{for(int i = 0; i < n - 1; i++){for(int j = 1; j < n - i; j++)//最开始的比较次数共n-1次{if(a[i - 1] > a[i]{Swap(a[i - 1],a[i]);}}}
}

  排序代码已经结束了,但是我们通过第二个图可以发现,到后面已经有序了,但还是在进行判断冒泡,因此我们可以对上述程序进行改良:
  多增加一个变量exchange = 0,如果它进行了排序,则exchange = 1,如果没有排序则exchange = 0,证明此时已经完成了排序,则直接跳出循环即可。
改良后的排序代码:

void BubbleSort(int* a, int n)
{for(int i = 0; i < n - 1; i++){int exchange = 0;for(int j = 1; j < n - i; j++){if(a[i - 1] > a[i]{exchange = 1;Swap(a[i - 1],a[i]);}}if(exchange == 0)//没有变化证明已经有序了,直接结束即可break;}
}

冒泡排序特性总结:

  • 时间复杂度:O(N^2^)
  • 空间复杂度:O(1)
  • 稳定性:稳定

快速排序

  快速排序是Hoare于1962年提出的一种二叉树结构的交换排序算法

Hoare版

基本思想
1.任取待排序元素序列中的的某元素作为基准值,并定义两个指针分别指向序列的开始结尾,遍历该数据,根据规则进行交换以及指针的移动直至两指针相遇。
2.此时将基准值两指针相遇指向的值交换,此时基准值就到了正确的位置。
3.此时将该序列分成了左右两边,左边序列的数都比此基准值右边序列的数都比此基准值
4.再重复上述步骤,可将所有数排好。

其实也就是递归。

当说可能不太清楚,下面我们通过图来解释:
在这里插入图片描述
在这里插入图片描述

总体代码如下:

void Swap(int* px, int* py)
{int tmp = *px;*px = *py;*py = tmp;
}void QuickSort1(int* a, int left, int right)
{if (left >= right)//递归结束条件,如果区间中只有一个值或不存在return;int begin = left, end = right;int key = left;//用的都是下标while (left < right){while (left < right && a[right] >= a[key])//让右指针先走,右指针要找到比a[key]小的right--;while (left < right && a[left] <= a[key])//左指针后走left++;Swap(&a[left], &a[right]);}//此时left和right相遇Swap(&a[left], &a[key]);//更新key的下标key = left;//分了左右两个区间[begin, key-1] key [key+1, end]//递归,使剩余区间都按上面操作QuickSort1(a, begin, key - 1);QuickSort1(a, key + 1, end);
}

拓展:我们最后是让left和right相遇所指向的值与基准值交换,我们是要升序排序,这也就是二者相遇指向的值必定小于基准值,这是为什么呢?
原因如下:

  1. 很重要的一点就是右边先走保证的,二者相遇无非就两种情况,一种是left遇上right,另一种就是right遇上left。下面来分别讨论:
  2. left遇上right:right先走,当right走到比key小的值就停了下来,left再走,要找比key大的值,但是它找不到,一直走就会和right想遇,此时相遇所指向的值就比key小。
  3. right遇上left:如果第一轮二者就相遇:right先走,right没有找到比key小的值,就一路左移,遇到left,也就是key的位置。
  4. right遇上left:二者第一轮以后相遇,此时二者是经过第一轮的交换,也就是left此时指向的位置比key小,right先走,它没有找到比key小的值,一直左移就和left相遇,此时指向的还是比key小的值。

因此,只要保证让right先走,在升序时就能保证相遇时所指向的值小于基准值。

前后指针版

给定两个指针,一个prev指向key位置处,另一个cur指向prev的下一个位置。
基本思想

  1. a[cur] >= a[key],++cur;
  2. a[cur] < a[key],++prev,交换prev和cur位置的值,++cur;

在这里插入图片描述
  通过上述步骤可以发现,prev和cur之间的数都是比基准值大的,也就是通过不断交换,将比基准值大的数都被放在了prev和cur之间,则当cur走到最后时,二者之间就都是比cur大的了。
后面的同样根据上述步骤重复进行即可得到。
具体代码如下:

void QuickSort2(int* a, int left, int right)
{if (left >= right)//递归结束条件,如果区间中只有一个值或不存在return;int prev = left, cur = left + 1;int key = left;while (cur <= right){if (a[cur] < a[key]){++prev;Swap(&a[prev], &a[cur]);cur++;}elsecur++;}Swap(&a[key], &a[prev]);key = prev;//分了左右两个区间[begin, key-1] key [key+1, end]//递归,使剩余区间都按上面操作QuickSort2(a, left, key - 1);QuickSort2(a, key + 1, right);
}

优化

  由于快排是一种递归式的排序,时间复杂度和堆排差不多,都是O(NlogN)。但如果序列有序或者接近有序时,此时时间复杂度就会高很多,就几乎达到了O(N^2^)。这是取基准值一直取的第一个导致的。

在这里插入图片描述
因此解决办法就是在每次对基准值的查找不要一直是第一个。下面有几种方式:

三数取中法

基本思想
在需要排序的序列中,找到最左边最右边以及中间的三个数,将这三个数比较大小,取中间大的数字。并将此数字作为基准值,然后将其与最左边的数交换,使基准值保持在最左边的位置,便于后续遍历。

适用序列:有序或接近有序的情况。
核心:找到中间大的数。
在这里插入图片描述
具体代码如下:

int GetMid(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])//a[mid]>=a[right]return right;elsereturn left;}else//a[left]>=a[mid]{if (a[right] < a[mid])return mid;else if (a[left] < a[right])//a[mid]<=a[right]return left;elsereturn right;}
}
void QuickSort1(int* a, int left, int right)
{if (left >= right)//递归结束条件,如果区间中只有一个值或不存在return;int begin = left, end = right;//找到中间的数int mid = GetMid(a, left, right);//与最左边的值交换Swap(&a[mid], &a[left]);int key = left;//用的都是下标//......
}

取随机数做基准值

  我们不想每次都取最左边的值做基准值,那我们在[left, right]此区间内找随机值即可。思想还是比较简单的。
具体代码如下:

void QuickSort1(int* a, int left, int right)
{if (left >= right)//递归结束条件,如果区间中只有一个值或不存在return;int begin = left, end = right;//要使得所取的随机数在[left, right]区间中int randi = rand() % (right - left +1);//left不一定为0randi += left;//与最左边的值交换Swap(&a[randi], &a[left]);int key = left;//用的都是下标//......
}

小区间优化

  正如堆所示,最后两到三层的节点数量占了整个的百分之八十左右,此时我们用递归消耗是比较大的,因此我们可以在最后的区间内使用插入排序。减少消耗
代码如下:

void QuickSort1(int* a, int left, int right)
{if (left >= right)//递归结束条件,如果区间中只有一个值或不存在return;int begin = left, end = right;//小区间优化if (right - left + 1 < 10){InsertSort(a + left, right - left + 1);}else{//要使得所取的随机数在[left, right]区间中int randi = rand() % (right - left + 1);//left不一定为0randi += left;//与最左边的值交换Swap(&a[randi], &a[left]);/*int mid = GetMid(a, left, right);Swap(&a[mid], &a[left]);*/int key = left;//用的都是下标//.........QuickSort1(a, begin, key - 1);QuickSort1(a, key + 1, end);}}

快排特性总结:

  1. 时间复杂度:O(NlogN),每趟确定的元素呈现指数增长。
  2. 空间复杂度:O(logN),递归是需要用的栈空间
  3. 稳定性:不稳定

快排非递归版

  将递归转化为非递归,可以借助循环来解决。
  我们可以借助栈后进先出的思想来模拟递归区间的使用。
如图:
在这里插入图片描述
  判断是否入栈:看区间是否有两个以上的值,如果只有一个值就没有必要入栈排序了。
  判断while循环结束的条件:栈不为空则继续,为空则结束。
代码如下:

#include"Stack.h"
void QuickSortNonR(int* a, int left, int right)
{ST st;STInit(&st);//初始化栈STPush(&st, right);//先进右STPush(&st, left);//再进左while (!STEmpty(&st))//循环条件是栈不为空{int begin = STTop(&st);//左是后进的,所以取出来就是beginSTPop(&st);int end = STTop(&st);STPop(&st);// 单趟int keyi = begin;int prev = begin;int cur = begin + 1;while (cur <= right){if (a[cur] < a[key]){++prev;Swap(&a[prev], &a[cur]);cur++;}elsecur++;}Swap(&a[keyi], &a[prev]);keyi = prev;// [begin, keyi-1] keyi [keyi+1, end] if (keyi + 1 < end)//如果区间有两个值以上则入区间{STPush(&st, end);STPush(&st, keyi + 1);}if (begin < keyi-1){STPush(&st, keyi-1);STPush(&st, begin);}}STDestroy(&st);//要记得把栈销毁
}

感谢大家观看,如果大家喜欢,希望大家一键三连支持一下,如有表述不正确,也欢迎大家批评指正。
请添加图片描述


http://www.ppmy.cn/ops/120979.html

相关文章

maven安装本地jar包到本地仓库

有时候我们需要把本地的 jar 包 install 到本地的 maven 仓库&#xff0c;这时就需要手动install依赖项。例如&#xff0c;把下面的 zhdx-license-1.0.jar 安装到本地 maven 仓库的操作如下&#xff1a; <dependency><groupId>com.zhdx</groupId><artifa…

【IPv6】IPv6地址格式及地址分类(组播、单播、任播)整理

IPv6地址格式 IPv6 地址从 IPv4 地址的 32 bits 扩展到 128 bits&#xff0c;IPv6 地址的表示、书写方式也从 IPv4 的点分十进制&#xff0c;修改16进制的冒号分割 IPv4 点分格式(.) 192.168.11.11 IPv6 冒号分割(:) 2408:8459:3032:0000:0000:0000:0001:a9fd IPv6 的规范…

集合框架01:集合的概念、Collection体系、Collection接口

1.集合的概念 集合是对象的容器&#xff0c;定义了多个对象进行操作的常用方法。可实现数组的功能。 集合和数组的区别&#xff1a; 1.数组长度固定&#xff0c;集合长度不固定&#xff1b; 2.数组可以存储基本类型和引用类型&#xff0c;集合只能存储引用类型&#xff1b; …

27 Vue3之unocss原子化

前置知识 什么是原子化 CSS 原子化 CSS 是一种 CSS 的架构方式&#xff0c;它倾向于小巧且用途单一的 class&#xff0c;并且会以视觉效果进行命名。 为什么使用 原子化 CSS 传统方案 制作原子化 CSS 的传统方案其实就是提供所有你可能需要用到的 CSS 工具。例如&#xff0c…

Solidity 设计模式:实现灵活与可扩展的智能合约架构

Solidity 作为以太坊智能合约的主要编程语言&#xff0c;拥有许多独特的设计模式&#xff0c;这些模式帮助开发者实现更加灵活、可扩展和安全的合约架构。设计模式不仅能够简化开发过程&#xff0c;还能减少常见的编程错误&#xff0c;并提高智能合约的可维护性和可升级性。本文…

Java中的对象生命周期管理:从Spring Bean到JVM对象的深度解析

Java中的对象生命周期管理&#xff1a;从Spring Bean到JVM对象的深度解析 大家好&#xff0c;我是微赚淘客返利系统3.0的小编&#xff0c;是个冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天我们来聊聊Java中的对象生命周期管理&#xff0c;尤其是从Spring Be…

深入学习并发编程中的 synchronized

文章目录 并发编程中的三个问题可见性原子性有序性 了解Java内存模型JMMsynchronized 保证三大特性synchronized 保证原子性synchronized 保证可见性synchronized 保证有序性 synchronized 的特性可重入特性不可中断特性 通过反汇编学习synchronized原理当修饰代码块时当修饰方…

C++平台跳跃游戏

目录 开头程序Game.cpp源文件Player.h头文件Player.cpp源文件 程序的流程图程序游玩的效果下一篇博客要说的东西 开头 大家好&#xff0c;我叫这是我58。 程序 Game.cpp源文件 #include <iostream> #include "Player.h" using namespace std; void printma…