数据结构初阶--排序2

目录

  • 前言
  • 快速排序
    • 思路
      • hoare版本
      • 代码实现
      • 挖坑法
      • 代码实现
      • 前后指针法
      • 代码实现
    • 快排优化
      • 三项取中法
      • 代码实现
      • 三指针
      • 代码实现
    • 快排非递归
      • 代码实现
  • 归并排序
    • 思路
    • 代码实现
    • 归并非递归
      • 代码实现
  • 计数排序
    • 思路
    • 代码实现

前言

本篇文章将继续介绍快排,归并等排序算法以及其变式。

快速排序

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

思路

其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
这里我们讲解三种方法:

hoare版本


hoare版本就是hoare这位大佬经过不断总结得出的方法,但这个方法存在很多需要注意的点,稍不注意就会出现bug。
首先,我们在找比key大或者小的数时,要保证left必须要小于right,因为完全有可能出现极端情况,所有数都比key小或者比key大,如果我们不添加这样的限制条件,就可能出现越界访问的情况。而且我们还需要注意书写的顺序,我们看下面这两个代码书写

while (left<right && arr[right]>=arr[keyi])
while (arr[right]>=arr[keyi]&& left<right)

显然,第一种书写才是正确的,因为我们要先判断再访问,这个顺序不能乱。
其次,我们还要特别注意的是,如果keyi在左边,那么右边就要先走,反之如果keyi在右边,那么左边就要先走,因为我们最后在交换keyi和左右相遇点的值时,要保证keyi的值大于相遇点的值,这时候我们就要注意,如果是左边先走,那么最后一次走是left走向right停下,这时候相遇的点是right,而right指向的值此时是比keyi大的,如果再交换,那么这个大的值就会跑到keyi的位置,这与我们的想得到的结果是背道而驰的,所以我们需要先让右先走,最后一次走就是right走left,这样再交换就对了。

这两个顺序不能换!(假设左边为keyi的情况下)
这是一趟找key将大于key和小于key分到左右两边的过程,接下来我们再递归key左边和右边直到begin和end相等为截止条件

代码实现

hoare版本的代码实现如下

//hoare
int PartSort1(int arr[],int left,int right)
{int keyi = left;while (left < right){while (left<right && arr[right]>=arr[keyi]){right--;}while (left < right && arr[left] <= arr[keyi]){left++;}Swap(&arr[right], &arr[left]);}Swap(&arr[keyi], &arr[left]);return left;
}
void QuickSort(int arr[],int begin,int end)
{if (begin >= end){return;}int keyi = PartSort1(arr,begin,end);QuickSort(arr,begin,keyi-1);QuickSort(arr,keyi+1,end);
}

挖坑法

挖坑法在hoare版本上做出了一定的优化,它可以有效避免我们需要考虑选左边为key时右边先走,右边为key时左边先走,因为我们在左边挖坑,肯定要右边找数来填,右边挖坑,肯定也要左边找数来填,我们通过图来分析

我们先将最左端下标赋值给hole,然后用key把最左端的值储存起来,避免被覆盖,然后右边先开始找小,找到了就把值放到左边的坑中,这样就形成了新坑(相当于把hole移到了新的位置),如此反复,直到左右相遇,再将我们一开始保存的key值放到hole中,整个流程就走完了。

代码实现

挖坑法的代码实现如下

int PartSort2(int arr[], int left, int right)
{int midi = GetMidiIndex(arr, left, right);Swap(&arr[left], &arr[midi]);int hole = left;int key = arr[left];while (left < right){while (left < right && arr[right] >= key){right--;}arr[hole] = arr[right];hole = right;while (left < right && arr[left] <= key){left++;}arr[hole] = arr[left];hole = left;}arr[hole] = key;return hole;
}
void QuickSort(int arr[],int begin,int end)
{if (begin >= end){return;}int keyi = PartSort2(arr,begin,end);QuickSort(arr,begin,keyi-1);QuickSort(arr,keyi+1,end);
}

前后指针法

前后指针的方法思路就是定义cur和prev两个指针,curleft+1的位置,而prev就在left的位置,cur不停往右走,走到right时停止,在走的过程中,遇到比key小的值就把该值与prev对调,同时prev也往后走,相当于prev和cur指针之间的值一直是大于key的,在对调的过程中交替着往后走,这样最终大于key的都在右边,小于key的都在做变了,我们再通过下面这幅图来分析。

代码实现

前后指针法的代码实现如下

int PartSort3(int arr[], int left, int right)
{int midi = GetMidiIndex(arr, left, right);Swap(&arr[left], &arr[midi]);int keyi = left;int prev = left;int cur = left + 1;while (cur <= right){if (arr[cur] < arr[keyi] && ++prev != cur){Swap(&arr[prev], &arr[cur]);}cur++;}Swap(&arr[prev], &arr[keyi]);keyi = prev;return keyi;
}
void QuickSort(int arr[],int begin,int end)
{if (begin >= end){return;}int keyi = PartSort3(arr,begin,end);QuickSort(arr,begin,keyi-1);QuickSort(arr,keyi+1,end);
}

快排优化

快排在大部分情况下性能都十分的优越,但数组难免会有一些极端的情况,比如我们在取key的值时,通常都是直接取最左或者最右的值,但是不排除最左或最右的值一直或大部分时候都是当前这段数列中最大或最小的,那么在排序的时候需要移动数据的次数就会大大增加,这是得不偿失的。还有一种情况,整组数据中与key相等的值占绝大部分,我们在前面三种方法中都没有单独考虑这种情况,这样移动数据的次数同样会增加很多,而且完全没有必要。接下来我们就通过三项取中三指针法对以上两种特殊情况进行优化。

三项取中法

首先是三项取中,我们可以考虑从最左,最右和中间的三个数中取第二大的值作为key值。这样就可以得到一个较平均的值。

代码实现

三项取中的代码如下,我们单独写一个函数,再将返回值与最左边/最右边的值互换(因为key还是取最左边或最右边的值)。

int GetMidiIndex(int arr[], int left, int right)
{int mid = (left + right) / 2;if (arr[mid] < arr[left]){if (arr[left] < arr[right]){return left;}else if (arr[right] < arr[mid]){return mid;}else{return right;}}else{if (arr[left] > arr[right]){return left;}else if (arr[right] > arr[mid]){return mid;}else{return right;}}
}

三指针

我们来看力扣里的一道题
力扣–排序数组
题目描述如下

描述很简单,就是让你排序数组,但是当我们把快排代码书写进去,发现居然显示超出时间限制了,我们来看未通过的测试用例:

我们发现这个测试用例的值全部都是2,这就是刚刚提到的大部分或全部和key值相等的情况,所以我们要采用hoare和前后指针结合的新方法–三指针来解决这类特殊情况。
三指针的基本思路是:创建left,right和cur三个指针,其中left到right之间放置和key值相等的值,cur用来从左往右遍历当前范围内的数据.
1,arr[cur]<key,交换arr[cur]和arr[left]的值,left++,cur++.
2,
3,

代码实现

三指针代码实现如下

void QuickSort(int arr[],int begin,int end)
{//三指针if (begin >= end){return;}int left = begin;int right = end;int cur = left + 1;int midi = GetMidiIndex(arr, left, right);Swap(&arr[left], &arr[midi]);int key = arr[midi];while (cur <= right){if (arr[cur] < key){Swap(&arr[left], &arr[cur]);++left;++cur;}if (arr[cur] > key){Swap(&arr[cur], &arr[right]);--right;}else{cur++;}}QuickSort(arr, begin, left - 1);QuickSort(arr, right + 1, end);
}

快排非递归

在学习完快排的递归操作后,我们现在来学习快排的非递归实现。
要实现快排非递归,我们需要借助栈这个数据结构,每次将左右端值压栈,然后再取出进行单趟排序,再修改左右端值再压栈,再取出,直到栈为空截止
和递归时的区间一样,此时数组被分为了[left,mid-1]mid[mid+1,right]三个部分,和递归终止条件begin>=end类似,只有当left<mid-1和mid+1<right才会继续进行压栈操作,否则不用再往里放入数据了
还要注意栈后进先出的原则,如果先放左再放右,那么取的时候取出来的顺序就是先右后左

代码实现

快排非递归的代码实现如下(栈的操作就没有具体写出了 可以参照栈的实现:栈的实现(C语言))

int PartSort1(int arr[],int left,int right)
{int midi = GetMidiIndex(arr, left, right);Swap(&arr[left], &arr[midi]);int keyi = left;while (left < right){while (left<right && arr[right]>=arr[keyi]){right--;}while (left < right && arr[left] <= arr[keyi]){left++;}Swap(&arr[right], &arr[left]);}Swap(&arr[keyi], &arr[left]);return left;}
void QuickSortNonR(int arr[], int left, int right)
{ST st;STInit(&st);STPush(&st, left);STPush(&st, right);while (!STEmpty(&st)){int right = STTop(&st);STPop(&st);int left = STTop(&st);STPop(&st);int mid = PartSort1(arr, left, right);if (right > mid + 1){STPush(&st, mid + 1);STPush(&st, right);}if (left < mid - 1){STPush(&st, left);STPush(&st, mid - 1);}}STDestroy(&st);
}

归并排序

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

思路

归并的递归思路和二叉树的后序遍历操作思想类似,这个是有别于快排的,快排前序操作思想,每次选出一个key值,将比key大的放右边,比key小的放左边,再在小区间里选出key,再重复操作,而归并排序思想和这个完全相反,我们要先保证最后的数组是有序的,再两两归并,四四归并,八八归并,直到整个数组都有序。所以我们要把递归操作写在归并操作前面,下面来讲归并操作:
归并操作和力扣一道合并两个有序数组类似,我们要能进行归并操作也是首先要保证两个子序列有序,感兴趣的读者可以去做一下这道题:
力扣–合并两个有序数组
我们归并的主体思路是:
1,再动态开辟一个数组temp
2,分别用begin1和begin2作为下标遍历两个数组,依次比较,将较小值尾插
3,设定end1和end2分别作为两个数组的截止条件,当某一个数组走到尾后,另一个数组剩余的值一定大于走完这个数组的所有值,所以直接把剩余数值依次拷贝到temp数组即可
4,最后再将temp数组拷贝回arr进行递归

代码实现

归并排序的代码实现如下:

void _MergeSort(int arr[],int temp[],int begin,int end)
{if (begin == end){return;}int mid = (begin + end) / 2;_MergeSort(arr, temp, begin, mid);_MergeSort(arr, temp, mid + 1, end);int begin1 = begin, end1 = mid;int begin2 = mid+1, end2 = end;int i = begin;while (begin1 <= end1 && begin2 <= end2){if (arr[begin1] < arr[begin2]){temp[i++] = arr[begin1++];}else{temp[i++] = arr[begin2++];}}while (begin1 <= end1){temp[i++] = arr[begin1++];}while (begin2 <= end2){temp[i++] = arr[begin2++];}memcpy(arr + begin, temp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int arr[], int n)
{int* temp = (int*)malloc(sizeof(int) * n);if (temp == NULL){perror("malloc fail");return;}_MergeSort(arr,temp,0,n-1);free(temp);
}

归并非递归

在学完归并排序的递归操作后,我们再来学习归并操作非递归实现。
我们要想实现非递归,其实就相当于从递归操作最深处开始利用循环逐步往浅走即我们首先要两两归并(一个数据我们默认可以看作有序),再四四归并,再八八归并,直到全部有序,但我们会发现一个问题,只有当数据个数为2的整数倍次方时,才能保证每次归并都不会发生越界,否则无法保证
可能有些抽象,我们通过画图来理解:
假设我们有10个数据。

我们定义gap为间隔,第一次是1,第二次是2,以此类推,我们再用i来每次遍历数组归并(每次往后走2gap),每两组需要归并的数组(比如图中第一次归并的2和3)左右区间分别为 i i+gap-1 i+gap i+2gap-1.,因为i我们添加了限制条件,是恒小于长度的,所以不会越界,但剩下三个边界都有可能超出范围,我们依据图来分析

我们可以把这三种情况的修正分为两类
1,第一种和第二种情况,有一个区间根本不存在,所以我们直接break。
2,第三种情况,两个区间都存在,我们只需要将右区间的右边界修改为n-1即可

如果我们在最后才拷贝数据,数组最后可能会有随机值出现,所以我们归并一次,就拷贝一次

代码实现

归并排序非递归代码实现如下

void MergeSortNonR(int arr[], int n)
{int* temp = (int*)malloc(sizeof(int) * n);if (temp == NULL){perror("malloc fail");}int gap = 1;while (gap < n){int j = 0;for (int i = 0; i < n; i += 2 * gap){int begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + 2 * gap - 1;if (end1 >= n || begin2 >= n){break;}if (end2 >= n){end2 = n - 1;}while (begin1 <= end1 && begin2 <= end2){if (arr[begin1] < arr[begin2]){temp[j++] = arr[begin1++];}else{temp[j++] = arr[begin2++];}}while (begin1 <= end1){temp[j++] = arr[begin1++];}while (begin2 <= end2){temp[j++] = arr[begin2++];}memcpy(arr + i, temp + i, sizeof(int) * (end2 - i + 1));}gap *= 2;}free(temp);
}

计数排序

计数排序是一种非比较排序,计数排序又称为鸽巢原理,是一种对哈希表的变形应用。

思路

计数排序的思路比较简单,先遍历一遍数组,确定最大和最小值,作为我们创建哈希表的边界依据,我们把最小的数作为基准。然后我们再遍历一遍数组,统计相同元素出现的次数,即在对应的位置++(每个位置最初的值都为0).
然后再遍历一遍哈希表将统计的结果放回到原数组中即可。
我们不难发现,当数据很集中时,它的时间复杂度为0(N),是比堆排,希尔排序甚至比快排(O(N*logN))还要快的,但我们也就可以发现它的劣势,那就是当数据非常分散时,它的效果是很不好的。

代码实现

计数排序的代码实现如下;

void CountSort(int* a, int n)
{int min = a[0], max = a[0];for (int i = 0; i < n; i++){if (a[i] < min){min = a[i];}if (a[i] > max){max = a[i];}}int range = max - min + 1;int* countA = (int*)malloc(sizeof(int) * range);memset(countA, 0, sizeof(int) * range);// 统计次数for (int i = 0; i < n; i++){countA[a[i] - min]++;}// 排序int k = 0;for (int j = 0; j < range; j++){while (countA[j]--){a[k++] = j + min;}}
}

到这里,六大排序的全部内容就讲完了,如有出入,欢迎指正。

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

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

相关文章

antd-React Table 中文转化

1.首先需要进行中文包导入 2.引入标签对Table进行包裹即可 import zh_CN from antd/lib/locale-provider/zh_CN;import {ConfigProvider} from antd;<ConfigProvider locale{zh_CN}><Tablecolumns{columns}rowKey{record > record.id}dataSource{data}pagination{p…

【ArcGIS】shp导入报错ORA-00911无效字符

这个当个问题记录以下&#xff0c;就是shp文件名或者字段名有非正常字符&#xff0c;修改下名称重新导入即可&#xff1b; 直接改shp没法修改字段&#xff0c;会报错&#xff0c;需要先转化为gdb文件&#xff0c;然后在修改

el-table组件插槽“slot-scope”

目录 一、代码展示 二、返回的数组对象不含value或者ispass&#xff0c;不会报错 三、插槽里面放的是要手动输入的值时 一、代码展示 <el-table v-loading"loading" :data"checklistList" selection-change"handleSelectionChange"><…

OV7670摄像头模块的使用

OV7670摄像头模块介绍 OV7670 CAMERACHIPTM 图像传感器&#xff0c;体积小、 工作电压低&#xff0c;提供单片 VGA 摄像头和影像处理器的所有功能。通过 SCCB 总线控制&#xff0c;可以输出整帧、子采样、取窗口等方式的各种分辨率 8 位影响数据。该产 品 VGA 图像最高达到 30…

【Linux】内存使用相关

free 命令 查看内存大小 free -g :G单位 free -h : 可读性较高较理解 free -m : MB单位 total: 总内存used: 正在运行的进程使用的内存(used total – free – buff/cache)free: 未使用的内存 (free total – used – buff/cache)shared: 多个进程共享的内存buffers: 内存保留…

面试题更新之-伪元素和伪类

文章目录 伪元素和伪类是什么&#xff1f;伪元素&#xff08;Pseudo-elements&#xff09;:伪类&#xff08;Pseudo-classes&#xff09;: css伪元素和伪类的区别使用css伪元素的好处使用css伪类的好处 伪元素和伪类是什么&#xff1f; 在CSS中&#xff0c;伪元素&#xff08;…

xpath下载安装——Python爬虫xpath插件下载安装(2023.7亲测可用!!)

目录 1.免费下载插件链接&#xff08;若失效评论区留言发送最新链接&#xff09;&#xff08;2023.7亲测可用&#xff09; 2.安装插件 &#xff08;1&#xff09;打开chrome浏览器页面&#xff0c;点击&#xff1a;右上角三个点 > 扩展程序 > 管理拓展程序 &#xff…

gitbash2.41安装教程——2023.07

文章目录 1、下载安装包2、安装 1、下载安装包 进入官网下载&#xff0c;官网链接 上面有多种系统可以选择&#xff0c;我是windows&#xff0c;点击windows进行下载 这里可以直接下载最新版本的git 2.41.0 64位。 下载可能有点慢&#xff0c;耐心等待。 2、安装 下载完…

JVM——类加载和垃圾回收

目录 前言 JVM简介 JVM内存区域划分 JVM的类加载机制 1.加载 双亲委派模型 2.验证 验证选项 3.准备 4.解析 5.初始化 触发类加载 JVM的垃圾回收策略 GC 一&#xff1a;找 谁是垃圾 1.引用计数 2.可达性分析 &#xff08;这个方案是Java采取的方案&#x…

基于单片机智能台灯坐姿矫正器视力保护器的设计与实现

功能介绍 以51单片机作为主控系统&#xff1b;LCD1602液晶显示当前当前光线强度、台灯灯光强度、当前时间、坐姿距离等&#xff1b;按键设置当前时间&#xff0c;闹钟、提醒时间、坐姿最小距离&#xff1b;通过超声波检测坐姿&#xff0c;当坐姿不正容易对眼睛和身体腰部等造成…

MySQL索引详解

索引 在MySQL中&#xff0c;查询方式可以根据访问表数据的方式分为两种&#xff1a;全表扫描和使用索引。 全表扫描&#xff08;Full Table Scan&#xff09;&#xff1a; 全表扫描是指在查询过程中&#xff0c;MySQL会遍历整个表的每一行来检查满足查询条件的数据。当查询条件…

Ubuntu下安装、配置及重装CUDA教程

安装CUDA 前往Nvidia CUDA Tools官网选择对应的架构和版本下载CUDA 以如下架构和版本为例&#xff1a; 查看显卡驱动 nvidia-smi如果显卡驱动已经装了&#xff0c;那么在CUDA安装过程中不用再勾选安装driver 下载并安装CUDA wget https://developer.download.nvidia.co…

Spring AOP

目录 AOP 理解AOP AOP组成 AOP的优点 Spring AOP 使用Spring AOP 定义切面和切点 定义通知 动态代理 织入 AOP 理解AOP AOP即面向切面编程&#xff0c;简单来说&#xff0c;就是把一部分通用的功能集中的放在一个地方处理的思想。假如某一段代码很多地方要用到&…

创建型模式 - 建造者模式

概述 将一个复杂对象的构建与表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 分离了部件的构造(由Builder来负责)和装配(由Director负责)。 从而可以构造出复杂的对象。这个模式适用于&#xff1a;某个对象的构建过程复杂的情况。 由于实现了构建和装配的解耦。…

pytest 参数化进阶

目录 前言&#xff1a; 语法 参数化误区 实践 简要回顾 前言&#xff1a; pytest是一个功能强大的Python测试框架&#xff0c;它提供了参数化功能&#xff0c;可以帮助简化测试用例的编写和管理。 语法 本文就赶紧聊一聊 pytest 的参数化是怎么玩的。 pytest.mark.par…

openGauss学习笔记-13 openGauss 简单数据管理-DELETE语句

文章目录 openGauss学习笔记-13 openGauss 简单数据管理-DELETE语句13.1 语法格式13.2 参数说明13.3 示例 openGauss学习笔记-13 openGauss 简单数据管理-DELETE语句 DELETE语句可以从指定的表里删除满足WHERE子句的行。如果WHERE子句不存在&#xff0c;将删除表中所有行&…

css 禁止多次点击导致的选中了目标div的文字

像下面这样的情况&#xff0c;就可以用这种方法避免掉 禁止多次点击&#xff0c;导致的&#xff0c;选中了目标div的文字 或者 禁止多次点击&#xff0c;导致&#xff0c;html结构被选中显示出来 .targetDiv {-webkit-user-select: none;-moz-user-select: none;-ms-user-sel…

【云原生】Docker的初步认识,安装与基本操作

一、Docker的相关知识 Docker是一个开源的应用容器引擎&#xff0c;基于go语言开发并遵循了apache2.0协议开源。 Docker是在Linux容器里运行应用的开源工具&#xff0c;是一种轻量级的“虚拟机”。 Docker 的容器技术可以在一台主机上轻松为任何应用创建一个轻量级的、可移植的…

SpringCloud整合Sentinel

文章目录 1、Sentinel介绍2、安装Sentinel控制台3、微服务整合Sentinel 1、Sentinel介绍 阿里开源的流量控制组件官网&#xff1a;https://sentinelguard.io/zh-cn/index.html承接了阿里双十一大促流量的核心场景&#xff0c;如秒杀、消息削峰填谷、集群流量控制、实时熔断下游…

Python自动化之pytest常用插件

目录 1、失败重跑 pytest-rerunfailures 2、多重校验 pytest-assume 3、设定执行顺序 pytest-ordering 4、用例依赖&#xff08;pytest-dependency&#xff09; 5.分布式测试(pytest-xdist) 6.生成报告&#xff08;pytest-html&#xff09; 1、失败重跑 pytest-rerunfailu…