数据结构_手撕八大排序(计数,快排,归并,堆排,希尔,选择,插入,冒泡)

✨✨所属专栏:数据结构✨✨

✨✨作者主页:嶔某✨✨

排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不断地在内外存之间移动数据的排序

function/数据结构-排序算法 · 钦某/c-language-learning - 码云 - 开源中国 (gitee.com)

常见排序算法 

插入排序 

示意图:

 排序过程:

  • 从第一个元素开始,该元素可以认为已经被排序;
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  • 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  • 将新元素插入到该位置后;
  • 重复步骤2~5。

 时间复杂度:

O(n^2) 

代码实现: 

//插入排序
void InsertSort(int* a, int n)
{for (int i = 0; i < n - 1; i++){int end = i;int tmp = a[end + 1];while (end >= 0){if (a[end] > tmp){a[end + 1] = a[end];end--;}elsebreak;}a[end + 1] = tmp;}
}

希尔排序

示意图:

排序过程:

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  • 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  • 按增量序列个数k,对序列进行k 趟排序;
  • 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

 随着增量因子(gap)的减少,这其实是一个使数组逐渐变有序的过程。那么有个问题,gap如何变化才能使算法得到最大的优化呢?

gap越大,大的数可以越快跳到后面,小的数可以越快跳到前面,越不接近有序;gap越小,跳的越慢,但是越接近有序。当 gap == 1时就相当于插入排序。

这个算法的发明者用的是gap /= 2,有人曾今算过(这里需要很高的数学水平,我还是算了)gap = gap / 3 + 1,为最优解。加一是为了让最后一趟排序的gap为1。

时间复杂度:

第一趟排序分为gap = n / 3组(忽略+1),每组三个数据,最坏情况就是逆序,向前调整(1+2)次,第一趟就的消耗是 n。第二趟gap = n / 9 组每组 9 个数据,最坏情况调整(1+2+......+7+8)次,第二趟的消耗是 4n。

但是,第二趟以后的每一趟都不是最坏的情况,所以第二趟的消耗到不了4n,具体是多少?这里需要加一些概率的公式

最后一趟gap = 1,直接就是插入排序,消耗是n,根据这个我们大概画出了下图。

 最后这个排序的时间复杂度大约为:n^(1.3)

代码实现: 

// 希尔排序
void ShellSort(int* a, int n)
{int gap = n;while (gap > 1){gap = gap / 3 + 1;for (int i = 0; i < n - gap; i++){int end = i;int tmp = a[end + gap];while (end >= 0){if (a[end] > a[end + gap]){a[end + gap] = a[end];end -= gap;}elsebreak;}a[end + gap] = tmp;}}
}

选择排序

示意图:

排序过程:

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

  • 初始状态:无序区为R[1..n],有序区为空;
  • 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  • n-1趟结束,数组有序化了。

另外我们在遍历时可以同时将无序区中最大的数和最小的数的下标分别记录,将最大数放在无序区的末尾,将最小数放在无序区的开头。

注意这里有个坑 :当一趟排序记录的mini == end时,将max处的数交换到end处,这是end处的数就是最大的数了,之后再将mini处的数交换到start处(这时mini处的数是最大的),这样就没有完成任务。

解决办法是:在交换之前,做一个判断更新mini的值。

时间复杂度:

O(n^2) 

代码实现:

//选择排序
void SelectSort(int* a, int n)
{int start = 0;int end = n - 1;while (end > start){int mini = start;int maxi = end;for (int i = start; i <= end; i++){if (a[mini] > a[i])mini = i;if (a[maxi] < a[i])maxi = i;}if (mini == end){Swap(&a[maxi], &a[end]);mini = maxi;Swap(&a[mini], &a[start]);}else{Swap(&a[maxi], &a[end]);Swap(&a[mini], &a[start]);}end--;start++;}
}

堆排序

示意图:

排序过程:

这里参考下数据结构_堆的代码:function/数据结构-堆 · 钦某/c-language-learning - 码云 - 开源中国 (gitee.com)

  • 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
  • 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  • 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆(堆的元素个数减一),然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

时间复杂度:

O(n*log(n))

代码实现: 

void AdjustDown(int* a, int n, int parent)
{int child = parent * 2 + 1;while (child < n){if (a[child] < a[child + 1] && child + 1 < n)child++;if (a[child] > a[parent]){Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}elsebreak;}
}//堆排序
void HeapSort(int* a, int n)
{for (int i = (n - 2) / 2; i >= 0; i--)AdjustDown(a, n, i);for (int i = n - 1; i > 0; i--){Swap(&a[0], &a[i]);AdjustDown(a, i, 0);}
}

冒泡排序 

示意图:

排序过程:

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  • 针对所有的元素重复以上的步骤,除了最后一个;
  • 重复步骤1~3,直到排序完成。

时间复杂度:

O(n^2)

代码实现: 

//冒泡排序
void BubbleSort(int* a, int n)
{for (int i = n; i > 0; i--){int prev = 0;int cur = 1;int falg = 1;while (cur < i){if (a[prev] > a[cur]){falg = 0;Swap(&a[prev], &a[cur]);}prev = cur;cur++;}if (falg == 1)break;}
}

快速排序

示意图:

排序过程:

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  • 从数列中挑出一个元素,称为 “基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

hoare: 

 单趟:我们定义一个key = left,右边先走,右边end--找小(比a[key]小),左边begin++找大,都找到了就交换,当begin > end跳出循环,再将a[key]和a[begin]交换。

为什么这里要右边先走?为什么相遇时位置比key小?

左边做key,右边先走,可以保证相遇位置比key小

场景:

L遇R:R先走,停下来,R停下条件是遇到比key小的值,一定比key小,L没有找大的,遇到R停下了

R遇L:R先走,找小,没有找到比key小的,直接跟L相遇了。L停留的位置是上一轮交换的位置,上一轮交换,把比key小的值换到L的位置了

相反:如果让右边做key,左边先走,i可以保证相遇位置比key要大

双指针:

单趟:定义一个prev,一个cur,cur找小,找不到小的prev和cur一起加加,找到了就把cur位置和prev++位置的值交换,prev和cur中间的值都是大的,将大的值一起往后挪。最后prev位置的值一定比key位置的值小,将prev和key位置的值交换。

每次递归分别传begin两侧的区间。

算法优化:

小区间优化

我们知道,递归定义的快排随着区间的越来越小,需要递归的次数越来越多,如果次数很多,每次调用函数都要压栈,有可能导致栈溢出。类似二叉树,最后一次递归占总递归次数的50%,倒数第二次占了25%,倒数第三次占12.5%。如果我们能将这部分递归占用缓解,就能使算法优化。所以当区间小于10的时候,我们调用选择排序。

void QuickSort(int* a, int left, int right)
{if (left >= right)return;if (right - left + 1 < 10){InsertSort(a + left, right - left + 1);//小区间优化return;}int keyi = partsort3(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}

三数取中

当数组已经有序,或者第一个数很小,或者很大时,时间复杂度就会退化为O(n^2),这时我们可以改变key值,从第一个值,中间的值和末尾值,三个值中选出中间的那个作为key值,保证每次的key都接近中间数。除了三数取中,还可以产生随机值来改变基准值。

int Midofthree(int* a,int x, int y, int z)
{if (a[x] > a[y])if (a[x] > a[z])if (a[y] > a[z])return y;elsereturn z;elsereturn y;else//a<bif (a[x] < a[z])if (a[y] < a[z])return y;else//b>creturn z;else//a>creturn x;
}

时间复杂度:

O(n*log(n))

代码实现:

(1)hoare版本:

//hoare版本
int partsort1(int* a, int left, int right)
{int begin = left, end = right;int x = Midofthree(a, left, right, (right + left) / 2);Swap(&a[x], &a[left]);int key = left;while (begin < end){while (begin < end){if (a[end] < a[key])break;end--;}while (begin < end){if (a[begin] > a[key])break;begin++;}Swap(&a[begin], &a[end]);}Swap(&a[key], &a[begin]);return begin;
}

(2)双指针版本:

//双指针版本
int partsort2(int* a, int left, int right)
{int x = Midofthree(a, left, right, (right + left) / 2);Swap(&a[x], &a[left]);int keyi = left;int prev = left;int cur = prev + 1;while (cur <= right){if (a[cur] < a[keyi] && ++prev != cur)Swap(&a[cur], &a[prev]);cur++;}Swap(&a[prev],&a[keyi]);return prev;
}

 (3)挖坑版本:

//挖坑版本
int partsort3(int* a, int left, int right)
{int x = Midofthree(a, left, right, (right + left) / 2);Swap(&a[x], &a[left]);int key = a[left];int pit = left;int begin = left;int end = right;while (begin < end){while (a[end] >= key && begin < end)end--;a[pit] = a[end];pit = end;while (a[begin] <= key && begin < end)begin++;a[pit] = a[begin];pit = begin;}a[pit] = key;return pit;
}

递归:

void QuickSort(int* a, int left, int right)
{if (left >= right)return;if (right - left + 1 < 10){InsertSort(a + left, right - left + 1);//小区间优化return;}int keyi = partsort1(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}

非递归: 

这里不一定要用栈,也可以用队列来存储,如果你想,甚至可以用顺序表。栈只是为了更好的模拟递归的过程。

function/数据结构_栈 · 钦某/c-language-learning - 码云 - 开源中国 (gitee.com)

void QuickSortNonR(int* a, int left, int right)
{ST s;STInit(&s);STPush(&s, right);STPush(&s, left);while (!STEmpty(&s)){int begin = STTop(&s);STPop(&s);int end = STTop(&s);STPop(&s);int mid = partsort1(a, begin, end);if (mid + 1 < end)//注意这里需判断{STPush(&s, end);STPush(&s, mid + 1);}if (begin < mid - 1)//注意这里需判断{STPush(&s, mid - 1);STPush(&s, begin);}}
}

归并排序

示意图:

排序过程:

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

  • 把长度为n的输入序列分成两个长度为n/2的子序列;
  • 对这两个子序列分别采用归并排序;
  • 将两个排序好的子序列合并成一个最终的排序序列。

时间复杂度:

O(n*log(n))

代码实现: 

递归:

void _MergeSort(int* a, int* tmp, int left, int right)
{if (left >= right)return;int mid = (left + right) / 2;_MergeSort(a, tmp, left, mid);_MergeSort(a, tmp, mid + 1, right);int begin1 = left, end1 = mid;int begin2 = mid + 1, end2 = right;int i = left;while (begin1 <= end1 && begin2 <= end2){if(a[begin1] < a[begin2])tmp[i++] = a[begin1++];elsetmp[i++] = a[begin2++];}while (begin1 <= end1)tmp[i++] = a[begin1++];while (begin2 <= end2)tmp[i++] = a[begin2++];memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));
}void MergeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc is fail");return;}_MergeSort(a, tmp, 0, n - 1);free(tmp);tmp = NULL;
}

非递归

void MergeSortNonR(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc is fail");return;}int gap = 1;while (gap < n){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;int j = i;if (begin2 >= n)break;if (end2 >= n)end2 = n - 1;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2])tmp[j++] = a[begin1++];elsetmp[j++] = a[begin2++];}while (begin1 <= end1)tmp[j++] = a[begin1++];while (begin2 <= end2)tmp[j++] = a[begin2++];memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));}gap *= 2;}free(tmp);tmp = NULL;
}

计数排序

示意图:

排序过程:

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。这就导致了这个排序算法

  • 找出待排序的数组中最大和最小的元素;
  • 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  • 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

时间复杂度:

 在理想情况下,这个排序算法的时间复杂度能达到O(n),非常的快。

代码实现:

void CountSort(int* a, int n)
{int min = a[0];int max = a[0];for (int i = 0; i < n; i++){if (a[i] > max)max = a[i];if (a[i] < min)min = a[i];}int range = max - min + 1;int* x = (int*)calloc(range, sizeof(int));if (x == NULL){perror("calloc is fail");return;}for (int i = 0; i < n; i++)x[a[i] - min]++;int j = 0;for (int i = 0; i < n; i++)while (x[i]--)a[j++] = i + min;free(x);
}

总结: 

上面的排序分别对同样的十万个随机数进行排序,所消耗的时间如下图:(ps:仅供参考)

本期博客到这里就结束了,如果有什么错误,欢迎指出,如果对你有帮助,请点个赞,谢谢!

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

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

相关文章

算法学习笔记(7.7)-贪心算法(Dijkstra算法-最短路径问题)

目录 1.最短路径问题 2.Dijkstra算法介绍 3.Dijkstra算法演示 4.Dijkstra算法的代码示例 1.最短路径问题 图论中的一个经典问题&#xff0c;通常是指在一个加权图中找到从一个起始顶点到目标顶点的最短路径。 单源最短路径问题&#xff1a;给定一个加权图和一个起始顶点&…

Python易错点总结

目录 多分支选择结构 嵌套选择 用match模式识别 match与if的对比 案例&#xff1a;闰年判断 三角形的判断 用whlie循环 高斯求和 死循环 用for循环 ​编辑continue​编辑 whlie与else结合 pass 序列 列表&#xff08;有序&#xff09; 元组&#xff08;有序&…

在虚拟机上搭建 Docker Kafka 宿主机器程序无法访问解决方法

1、问题描述 在虚拟机CentOS-7上搭建的Docker Kafka ,docker内部可以创建Topic、可以生产者数据、可以消费数据&#xff0c;而在宿主机开发程序无法消费Docker Kafka的数据。 1.1、运行情况 [dockerlocalhost ~]$ docker ps -a CONTAINER ID IMAGE COMMAND…

区块链的基本原理和优势

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

使用亮数据代理IP爬取PubMed文章链接和邮箱地址

&#x1f482; 个人网站:【 摸鱼游戏】【神级代码资源网站】【工具大全】&#x1f91f; 一站式轻松构建小程序、Web网站、移动应用&#xff1a;&#x1f449;注册地址&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交…

Redis页面优化

文章目录 1.Redis页面缓存1.思路分析2.首先记录一下目前访问商品列表页的QPS1.线程组配置10000次请求2.请求配置3.开始压测1.压测第一次 平均QPS为6122.压测第二次 平均QPS为6153.压测第三次 平均QPS为617 3.然后记录一下访问商品详情页的QPS1.线程组配置10000次请求2.请求配置…

【人工智能】第三部分:ChatGPT的应用场景和挑战

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

Pyinstaller安装与使用

一、Pyinstaller简介 PyInstaller将Python应用程序冻结(打包)独立可执行文件中。它可以构建较小的可执行文件,它是完全多平台的,并且使用OS支持来加载动态库,从而确保完全兼容。 二、Pyinstaller安装 1、下载安装 首先安装“pip install pywin32” 其次“pip install …

亿发软件:信息化与数字化,相互交织的科技双引擎

在现代科技发展的浪潮中&#xff0c;信息化和数字化是两个频繁被提及的关键词。尽管它们在很多情况下被视为同义词&#xff0c;但其实两者有着本质的区别和相互影响的关系。究竟是信息化推动了数字化&#xff0c;还是数字化引领了信息化的进程&#xff1f;本文将深入探讨信息化…

C++第二十五弹---从零开始模拟STL中的list(下)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】 目录 1、函数补充 2、迭代器完善 3、const迭代器 总结 1、函数补充 拷贝构造 思路&#xff1a; 先构造一个头结点&#xff0c;然后将 lt 类中的元…

10.dockerfile自动构建镜像

dockerfile自动构建镜像 类似ansible剧本&#xff0c;大小几kb 手动做镜像&#xff1a;大小几百M 首先创建一个dockerfile的路径&#xff0c;便于在路径下存在多个路径每个路径下都是dockerfile命名的脚本 注释&#xff1a;文件必须为&#xff1a;dockerfile或者Dockerfile …

基于深度学习的中文标点预测模型-中文标点重建(Transformer模型)【已开源】

基于深度学习的中文标点预测模型-中文标点重建&#xff08;Transformer模型&#xff09;提供模型代码和训练好的模型 前言 目前关于使用深度学习对文本自动添加标点符号的研究并不多见&#xff0c;已知的开源项目也较少&#xff0c;而对该领域的详细介绍更是稀缺。然而&#x…

【vscode-快捷键 一键JSON格式化】

网上有很多JSON格式化工具&#xff0c;也有很多好用的在线json格式化工具。但是其实Vscode里面的可以直接格式化JSON&#xff0c;这里分享一个我常用的小插件 Prettify JSON 未格式化的JSON数据 召唤出命令行&#xff0c;输入prettify JSON 即可! ✿✿ヽ(▽)ノ✿

OpenAI模型规范概览

这是OpenAI对外分享的模型规范文档&#xff08;Model Spec&#xff09;&#xff0c;它定义了OpenAI希望在API接口和ChatGPT&#xff08;含GPT系列产品&#xff09;中模型的行为方式&#xff0c;这也是OpenAI超级对齐团队奉行的行为准则&#xff0c;希望能对国内做RLHF的同学有帮…

力扣爆刷第148天之贪心算法五连刷(区间合并)

力扣爆刷第148天之贪心算法五连刷&#xff08;区间合并&#xff09; 文章目录 力扣爆刷第148天之贪心算法五连刷&#xff08;区间合并&#xff09;一、406. 根据身高重建队列二、452. 用最少数量的箭引爆气球三、435. 无重叠区间四、763. 划分字母区间五、56. 合并区间六、738.…

安卓约束性布局学习

据说这个布局是为了解决各种布局过度前套导致代码复杂的问题的。 我想按照自己想实现的各种效果来逐步学习&#xff0c;那么直接拿微信主页来练手&#xff0c;用约束性布局实现微信首页吧。 先上图 先实现顶部搜索框加号按钮 先实现 在布局中添加一个组件&#xff0c;然后摆放…

【java】速度搭建一个springboot项目

使用软件&#xff1a;IDEA&#xff0c;mysql 使用框架&#xff1a;springboot mybatis-plus druid 坑点 使用IDEA搭建一个springboot项目的时候&#xff0c;需要考虑一下IDEA版本支持的JDK版本以及maven版本。否则再构建项目&#xff0c;引入pom的时候就会报错。 需要检查…

PostgreSQL基础(十):PostgreSQL的并发问题

文章目录 PostgreSQL的并发问题 一、事务的隔离级别 二、MVCC PostgreSQL的并发问题 一、事务的隔离级别 在不考虑隔离性的前提下&#xff0c;事务的并发可能会出现的问题&#xff1a; 脏读&#xff1a;读到了其他事务未提交的数据。&#xff08;必须避免这种情况&#xf…

docker命令 docker ps -l (latest)命令在 Docker 中用于列出最近一次创建的容器

文章目录 12345 1 docker ps -l 命令在 Docker 中用于列出最近一次创建的容器。具体来说&#xff1a; docker ps&#xff1a;这个命令用于列出当前正在运行的容器。-l 或 --latest&#xff1a;这个选项告诉 docker ps 命令只显示最近一次创建的容器&#xff0c;不论该容器当前…