排序算法,最全的10大排序算法详解(Sort Algorithm)
文章目录
- 排序算法,最全的10大排序算法详解(Sort Algorithm)
- 排序算法分类
- 排序算法稳定性
- 时间复杂度(time complexity)
- 1#时间复杂度的意义
- 2#基本操作执行次数
- 如何推导出时间复杂度呢?有如下几个原则:
- 3#让我们回头看看刚才的四个场景。
- 对数(log、logarithm)
- 对数数轴与天文数字
- 1# 基数排序(radix sort)
- 2# 冒泡排序(Bubble Sort)
- 3# 希尔排序(shellSort)
- 4# 快速排序(quickSort)
- 5# 堆排序(heapSort)
- 6# 归并排序(mergeSort)
- 7# 插入排序(Insertion Sort)
- 8# 选择排序(Selection sort)
- 9# 计数排序(CountingSort)
- 10# 桶排序(bucketSort)
- PS#桶排序&计数排序&基数排序
- 参考资料
- 参考书目
- 参考程序
- 参考习题
排序算法,最全的10大排序算法详解(Sort Algorithm)
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制和规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析。
也就说,把一串数通过一通操作变成有序的一串数,这个操作就要用到排序算法。
下面是十大经典排序算法。
(1)冒泡排序;
(2)选择排序;
(3)插入排序;;
(4)希尔排序;
(5)归并排序;;
(6)快速排序;
(7)基数排序;
(8)堆排序;
(9)计数排序;
(10)桶排序。
我将会采用自己的方法和别人的各种讲法汇总起来讲解
排序算法分类
十种常见排序算法可以分为两大类:
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等
排序算法稳定性
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面
需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法。
时间复杂度(time complexity)
在计算机科学中,算法的时间复杂度(Time complexity)是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷时的情况。例如,如果一个算法对于任何大小为 n (必须比 n0 大)的输入,它至多需要 5n3 + 3n 的时间运行完毕,那么它的渐近时间复杂度是 O(n3)。
为了计算时间复杂度,我们通常会估计算法的操作单元数量,每个单元运行的时间都是相同的。因此,总运行时间和算法的操作单元数量最多相差一个常量系数。
相同大小的不同输入值仍可能造成算法的运行时间不同,因此我们通常使用算法的最坏情况复杂度,记为 T*(*n*) ,定义为任何大小的输入 n 所需的最大运行时间。另一种较少使用的方法是平均情况复杂度,通常有特别指定才会使用。时间复杂度可以用函数 T(n) 的自然特性加以分类,举例来说,有着 T(n) = O(n) 的算法被称作“线性时间算法”;而 T(n) = O(M*n) 和 M**n= O(T(n)) ,其中 M ≥ n > 1 的算法被称作“指数时间算法”。
1#时间复杂度的意义
究竟什么是时间复杂度呢?让我们来想象一个场景:某一天,小灰和大黄同时加入了一个公司…
一天过后,小灰和大黄各自交付了代码,两端代码实现的功能都差不多。大黄的代码运行一次要花100毫秒,内存占用5MB。小灰的代码运行一次要花100秒,内存占用500MB。于是…
由此可见,衡量代码的好坏,包括两个非常重要的指标:
1.运行时间;
2.占用空间。
2#基本操作执行次数
关于代码的基本操作执行次数,我们用四个生活中的场景,来做一下比喻:
**场景1:**给小灰一条长10寸的面包,小灰每3天吃掉1寸,那么吃掉整个面包需要几天?
答案自然是 3 X 10 = 30天。
如果面包的长度是 N 寸呢?
此时吃掉整个面包,需要 3 X n = 3n 天。
如果用一个函数来表达这个相对时间,可以记作 T(n) = 3n。
**场景2:**给小灰一条长16寸的面包,小灰每5天吃掉面包剩余长度的一半,第一次吃掉8寸,第二次吃掉4寸,第三次吃掉2寸…那么小灰把面包吃得只剩下1寸,需要多少天呢?
这个问题翻译一下,就是数字16不断地除以2,除几次以后的结果等于1?这里要涉及到数学当中的对数,以2位底,16的对数,可以简写为log16。
因此,把面包吃得只剩下1寸,需要 5 X log16 = 5 X 4 = 20 天。
如果面包的长度是 N 寸呢?
需要 5 X logn = 5logn天,记作 T(n) = 5logn。
**场景3:**给小灰一条长10寸的面包和一个鸡腿,小灰每2天吃掉一个鸡腿。那么小灰吃掉整个鸡腿需要多少天呢?
答案自然是2天。因为只说是吃掉鸡腿,和10寸的面包没有关系 。
如果面包的长度是 N 寸呢?
无论面包有多长,吃掉鸡腿的时间仍然是2天,记作 T(n) = 2。
**场景4:**给小灰一条长10寸的面包,小灰吃掉第一个一寸需要1天时间,吃掉第二个一寸需要2天时间,吃掉第三个一寸需要3天时间…每多吃一寸,所花的时间也多一天。那么小灰吃掉整个面包需要多少天呢?
答案是从1累加到10的总和,也就是55天。
如果面包的长度是 N 寸呢?
此时吃掉整个面包,需要 1+2+3+…+ n-1 + n = (1+n)*n/2 = 0.5n^2 + 0.5n。
记作 T(n) = 0.5n^2 + 0.5n。
上面所讲的是吃东西所花费的相对时间,这一思想同样适用于对程序基本操作执行次数的统计。刚才的四个场景,分别对应了程序中最常见的四种执行方式:
如何推导出时间复杂度呢?有如下几个原则:
- 如果运行时间是常数量级,用常数1表示;
- 只保留时间函数中的最高阶项;
- 如果最高阶项存在,则省去最高阶项前面的系数。
3#让我们回头看看刚才的四个场景。
场景1:
T(n) = 3n
最高阶项为3n,省去系数3,转化的时间复杂度为:
T(n) = O(n)
场景2:
T(n) = 5logn
最高阶项为5logn,省去系数5,转化的时间复杂度为:
T(n) = O(logn)
场景3:
T(n) = 2
只有常数量级,转化的时间复杂度为:
T(n) = O(1)
场景4:
T(n) = 0.5n^2 + 0.5n
最高阶项为0.5n^2,省去系数0.5,转化的时间复杂度为:
T(n) = O(n^2)
这四种时间复杂度究竟谁用时更长,谁节省时间呢?稍微思考一下就可以得出结论:
O(1)< O(logn)< O(n)< O(n^2)
在编程的世界中有着各种各样的算法,除了上述的四个场景,还有许多不同形式的时间复杂度,比如:
O(nlogn), O(n^3), O(m*n),O(2^n),O(n!)
遨游在代码的海洋里,我们会陆续遇到上述时间复杂度的算法。
对数(log、logarithm)
在数学中,对数是对求幂的逆运算,正如除法是乘法的倒数,反之亦然。 这意味着一个数字的对数是必须产生另一个固定数字(基数)的指数。 在简单的情况下,乘数中的对数计数因子。更一般来说,乘幂允许将任何正实数提高到任何实际功率,总是产生正的结果,因此可以对于b不等于1的任何两个正实数b和x计算对数。
如果a的x次方等于N(a>0,且a≠1),那么数x叫做以a为底N的对数(logarithm),记作x=log_a N。其中,a叫做对数的底数,N叫做真数。
例:23是8,指数为3,底数为2,结果是8。那么以2为底8的对数就是求指数3。
对数数轴与天文数字
对数数轴
我们要是想把为 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IExzVVJ3-1629094072579)(https://www.zhihu.com/equation?tex=%5Ctimes+2)] 坐标系继续画下去是困难的,因为指数增长太快了(指数级增长):
尽量缩小才画到对数为5的地方,我相信你已经快看不清了,如果画到对数为100的地方,地球都摆不下这个长度。
我们可以保持对数值等距离摆放,这就是对数坐标系:
是不是可以摆下更多的对数了?
天文数字
对数是将数轴进行强力的缩放,再大的数字都经不起对数缩放,如果我选用10为底的话,一亿这么大的数字,在对数数轴上也不过是8。这对于天文学里的天文数字简直是强有力的武器。
要是不进行缩放的话,地球和太阳是不可能同框的:
1# 基数排序(radix sort)
时间复杂度:O (nlog®m)
首先,基数排序的名字和计数排序很像,而且原理都是基于桶的原理,但是略有不同,不要记混了。
通俗解释:输入的n个数,对于这n个数,对照每一位比较,每一位的数字只能是0~9,所以最多有10个桶,最后排序到最后一个位,逐个排列。(好吧,总觉得解释的听不懂)
科学解释(但我一开始看不懂):基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
看不懂文字,就看图!
https://visualgo.net/zh/sorting,这里是排序算法集合,很多排序算法的演示都有。visualgo主站还有其他数据结构的演示,比如链表。
行了,上代码
Java
public class RadixSort implements IArraySort {@Overridepublic int[] sort(int[] sourceArray) throws Exception {// 对 arr 进行拷贝,不改变参数内容int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);int maxDigit = getMaxDigit(arr);return radixSort(arr, maxDigit);}/*** 获取最高位数*/private int getMaxDigit(int[] arr) {int maxValue = getMaxValue(arr);return getNumLenght(maxValue);}private int getMaxValue(int[] arr) {int maxValue = arr[0];for (int value : arr) {if (maxValue < value) {maxValue = value;}}return maxValue;}protected int getNumLenght(long num) {if (num == 0) {return 1;}int lenght = 0;for (long temp = num; temp != 0; temp /= 10) {lenght++;}return lenght;}private int[] radixSort(int[] arr, int maxDigit) {int mod = 10;int dev = 1;for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {// 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)int[][] counter = new int[mod * 2][0];for (int j = 0; j < arr.length; j++) {int bucket = ((arr[j] % mod) / dev) + mod;counter[bucket] = arrayAppend(counter[bucket], arr[j]);}int pos = 0;for (int[] bucket : counter) {for (int value : bucket) {arr[pos++] = value;}}}return arr;}/*** 自动扩容,并保存数据** @param arr* @param value*/private int[] arrayAppend(int[] arr, int value) {arr = Arrays.copyOf(arr, arr.length + 1);arr[arr.length - 1] = value;return arr;}
}
C++
int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{int maxData = data[0]; ///< 最大数/// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。for (int i = 1; i < n; ++i){if (maxData < data[i])maxData = data[i];}int d = 1;int p = 10;while (maxData >= p){//p *= 10; // Maybe overflowmaxData /= 10;++d;}return d;
/* int d = 1; //保存最大的位数int p = 10;for(int i = 0; i < n; ++i){while(data[i] >= p){p *= 10;++d;}}return d;*/
}
void radixsort(int data[], int n) //基数排序
{int d = maxbit(data, n);int *tmp = new int[n];int *count = new int[10]; //计数器int i, j, k;int radix = 1;for(i = 1; i <= d; i++) //进行d次排序{for(j = 0; j < 10; j++)count[j] = 0; //每次分配前清空计数器for(j = 0; j < n; j++){k = (data[j] / radix) % 10; //统计每个桶中的记录数count[k]++;}for(j = 1; j < 10; j++)count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中{k = (data[j] / radix) % 10;tmp[count[k] - 1] = data[j];count[k]--;}for(j = 0; j < n; j++) //将临时数组的内容复制到data中data[j] = tmp[j];radix = radix * 10;}delete []tmp;delete []count;
}
2# 冒泡排序(Bubble Sort)
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来
算法描述:
1.比较相邻两个数据如果。第一个比第二个大,就交换两个数
2对每一个相邻的数做同样1的工作,这样从开始一队到结尾一队在最后的数就是最大的数
3.针对所有元素上面的操作,除了最后一个
4.重复1~3步骤,知道顺序完成。
原理:比较两个相邻的元素,将值大的元素交换到右边
思路:依次比较相邻的两个数,将比较小的数放在前面,比较大的数放在后面。
(1)第一次比较:首先比较第一和第二个数,将小数放在前面,将大数放在后面。
(2)比较第2和第3个数,将小数 放在前面,大数放在后面。
…
(3)如此继续,知道比较到最后的两个数,将小数放在前面,大数放在后面,重复步骤,直至全部排序完成
(4)在上面一趟比较完成后,最后一个数一定是数组中最大的一个数,所以在比较第二趟的时候,最后一个数是不参加比较的。
(5)在第二趟比较完成后,倒数第二个数也一定是数组中倒数第二大数,所以在第三趟的比较中,最后两个数是不参与比较的。
(6)依次类推,每一趟比较次数减少依次
C语言:
#include <stdio.h>
void bubble_sort(int arr[], int len) {int i, j, temp;for (i = 0; i < len - 1; i++)for (j = 0; j < len - 1 - i; j++)if (arr[j] > arr[j + 1]) {temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;}
}
int main() {int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };int len = (int) sizeof(arr) / sizeof(*arr);bubble_sort(arr, len);int i;for (i = 0; i < len; i++)printf("%d ", arr[i]);return 0;
}
C++中可以把交换部分变成swap函数,swap(arr[i], arr[j]);
我们只需要记住
for (int i=0;i<n-1;i++)for (int j=n-1;j>i;j--)if (a[j]>a[j-1]) swap(a[j],a[j-1]);
稍微加入改进
for (int i = 0 ;i<n;i++) {bool flag = 1 ;for (int j = n-1;j>i;j--){if (a[j]>a[j-1]) {swap(a[j],a[j-1]) ;flag =0 ;}}if (flag) break; // 中途结束算法
}
#include <bits/stdc++.h>
using namespace std;
int a[10000001] ;
int main(){int n , sum = 0 ;cin>>n ;for (int i = 1;i<= n;i++) cin>> a[i] ;for (int i = 1 ;i<n;i++)for (int j =1;j<=n-i;j++)if (a[j] > a[j+1]) sum ++ ;cout<< sum <<endl;return 0 ;
}
3# 希尔排序(shellSort)
看懂了插入排序再来,对不起编排顺序有误。
希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。
基本思想
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2 =1(dtdt−1{d_t}\ \ {d_{t-1}}dt dt−1 …
该方法实质上是一种分组插入方法
比较相隔较远距离(称为增量)的数,使得数移动时能跨过多个元素,则进行一次比较就可能消除多个元素交换。D.L.shell于1959年在以他名字命名的排序算法中实现了这一思想。算法先将要排序的一组数按某个增量d分成若干组,每组中记录的下标相差d.对每组中全部元素进行排序,然后再用一个较小的增量对它进行,在每组中再进行排序。当增量减到1时,整个要排序的数被分成一组,排序完成。
一般的初次取序列的一半为增量,以后每次减半,直到增量为1。
给定实例的shell排序的排序过程
假设待排序文件有10个记录,其关键字分别是:
49,38,65,97,76,13,27,49,55,04。
增量序列的取值依次为:
5,2,1
#include <stdio.h>
int shsort(int s[], int n) /* 自定义函数 shsort()*/
{int i,j,d;d=n/2; /*确定固定增虽值*/while(d>=1){for(i=d+1;i<=n;i++) /*数组下标从d+1开始进行直接插入排序*/{s[0]=s[i]; /*设置监视哨*/j=i-d; /*确定要进行比较的元素的最右边位置*/while((j>0)&&(s[0]<s[j])){s[j+d]=s[j]; /*数据右移*/j=j-d; /*向左移d个位置V*/}s[j + d]=s[0]; /*在确定的位罝插入s[i]*/}d = d/2; /*增里变为原来的一半*/}
return 0;
}int main()
{int a[11],i; /*定义数组及变量为基本整型*/printf("请输入 10 个数据:\n");for(i=1;i<=10;i++)scanf("%d",&a[i]); /*从键盘中输入10个数据*/shsort(a, 10); /* 调用 shsort()函数*/printf("排序后的顺序是:\n");for(i=1;i<=10;i++)printf("%5d",a[i]); /*输出排序后的数组*/printf("\n");return 0;
}
请输入 10 个数据:
69 56 12 136 3 55 46 99 88 25
排序后的顺序是:3 12 25 46 55 56 69 88 99 136
4# 快速排序(quickSort)
当然,我们可以使用algorithm库自带的sort函数,这是c++给你提供的最方便最快捷的排序函数,可以排序数组和动态数组。例如
bool cmp(int a, int b) // 数组类型适当更改
{return a > b; // 重载从大到小
}vector<int>a;
int t;
while (cin >> t)a.push_back(t);
sort(a.begin(), a.end()); // 默认从小到大排序
sort(a.begin(), a.end(), cmp); // 改为从大到小
int array[5] = {6, 7, 2, 3, 5};
sort(array, array + 5); // 从小到大
sort(array, array + 5, cmp); // 同理,改为从大到小
我这个伪代码写得很匆忙,不理解可以百度一下,看看别人的详细代码。
sort函数大概就是这个意思,但是真正快速排序的原理是什么呢?我们要自己写出代码才能明白。,
基本思想:
通过一趟排序将要排序的数据分割成独立的两部分:分割点左边都是比它小的数,右边都是比它大的数。
然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
详细的图解往往比大堆的文字更有说明力,所以直接上图:
上图中,演示了快速排序的处理过程:
初始状态为一组无序的数组:2、4、5、1、3。
经过以上操作步骤后,完成了第一次的排序,得到新的数组:1、2、5、4、3。
新的数组中,以2为分割点,左边都是比2小的数,右边都是比2大的数。
因为2已经在数组中找到了合适的位置,所以不用再动。
2左边的数组只有一个元素1,所以显然不用再排序,位置也被确定。(注:这种情况时,left指针和right指针显然是重合的。因此在代码中,我们可以通过设置判定条件left必须小于right,如果不满足,则不用排序了)。
而对于2右边的数组5、4、3,设置left指向5,right指向3,开始继续重复图中的一、二、三、四步骤,对新的数组进行排序。
核心代码:
public int division(int[] list, int left, int right) {// 以最左边的数(left)为基准int base = list[left];while (left < right) {// 从序列右端开始,向左遍历,直到找到小于base的数while (left < right && list[right] >= base)right--;// 找到了比base小的元素,将这个元素放到最左边的位置list[left] = list[right];// 从序列左端开始,向右遍历,直到找到大于base的数while (left < right && list[left] <= base)left++;// 找到了比base大的元素,将这个元素放到最右边的位置list[right] = list[left];}// 最后将base放到left位置。此时,left位置的左侧数值应该都比left小;// 而left位置的右侧数值应该都比left大。list[left] = base;return left;
}private void quickSort(int[] list, int left, int right){// 左下标一定小于右下标,否则就越界了if (left < right) {// 对数组进行分割,取出下次分割的基准标号int base = division(list, left, right);System.out.format("base = %d:\t", list[base]);printPart(list, left, right);// 对“基准标号“左侧的一组数值进行递归的切割,以至于将这些数值完整的排序quickSort(list, left, base - 1);// 对“基准标号“右侧的一组数值进行递归的切割,以至于将这些数值完整的排序quickSort(list, base + 1, right);}
}
C++:
#include <stdio.h>
#include <stdlib.h>void display(int* array, int size) {for (int i = 0; i < size; i++) {printf("%d ", array[i]);}printf("\n");
}int getStandard(int array[], int i, int j) {// 基准数据int key = array[i];while (i < j) {// 因为默认基准是从左边开始,所以从右边开始比较// 当队尾的元素大于等于基准数据 时,就一直向前挪动 j 指针while (i < j && array[j] >= key) {j--;}// 当找到比 array[i] 小的时,就把后面的值 array[j] 赋给它if (i < j) {array[i] = array[j];}// 当队首元素小于等于基准数据 时,就一直向后挪动 i 指针while (i < j && array[i] <= key) {i++;}// 当找到比 array[j] 大的时,就把前面的值 array[i] 赋给它if (i < j) {array[j] = array[i];}}// 跳出循环时 i 和 j 相等,此时的 i 或 j 就是 key 的正确索引位置// 把基准数据赋给正确位置array[i] = key;return i;
}void QuickSort(int array[], int low, int high) {// 开始默认基准为 lowif (low < high) {// 分段位置下标int standard = getStandard(array, low, high);// 递归调用排序// 左边排序QuickSort(array, low, standard - 1);// 右边排序QuickSort(array, standard + 1, high);}
}// 合并到一起快速排序
// void QuickSort(int array[], int low, int high) {// if (low < high) {// int i = low;
// int j = high;
// int key = array[i];
// while (i < j) {// while (i < j && array[j] >= key) {// j--;
// }
// if (i < j) {// array[i] = array[j];
// }
// while (i < j && array[i] <= key) {// i++;
// }
// if (i < j) {// array[j] = array[i];
// }
// }
// array[i] = key;
// QuickSort(array, low, i - 1);
// QuickSort(array, i + 1, high);
// }
// }int main() {int array[] = {49, 38, 65, 97, 76, 13, 27, 49, 10};int size = sizeof(array) / sizeof(int);// 打印数据printf("%d \n", size);QuickSort(array, 0, size - 1);display(array, size);// int size = 20;// int array[20] = {0}; // 数组初始化// for (int i = 0; i < 10; i++) { // 数组个数// for (int j = 0; j < size; j++) { // 数组大小// array[j] = rand() % 1000; // 随机生成数大小 0~999// }// printf("原来的数组:");// display(array, size);// QuickSort(array, 0, size - 1);// printf("排序后数组:");// display(array, size);// printf("\n");// }return 0;
}
#include<iostream>
using namespace std;
int n,a[1000001];
void qsort(int l,int r)//应用二分思想
{int mid=a[(l+r)/2];//中间数int i=l,j=r;do{while(a[i]<mid) i++;//查找左半部分比中间数大的数while(a[j]>mid) j--;//查找右半部分比中间数小的数if(i<=j)//如果有一组不满足排序条件(左小右大)的数{swap(a[i],a[j]);//交换i++;j--;}}while(i<=j);//这里注意要有=if(l<j) qsort(l,j);//递归搜索左半部分if(i<r) qsort(i,r);//递归搜索右半部分
}
int main()
{cin>>n;for(int i=1;i<=n;i++) cin>>a[i];qsort(1,n);for(int i=1;i<=n;i++) cout<<a[i]<<" ";
}
快速排序算法的性能
时间复杂度
当数据有序时,以第一个关键字为基准分为两个子序列,前一个子序列为空,此时执行效率最差。
而当数据随机分布时,以第一个关键字为基准分为两个子序列,两个子序列的元素个数接近相等,此时执行效率最好。
所以,数据越随机分布时,快速排序性能越好;数据越接近有序,快速排序性能越差。
空间复杂度
快速排序在每次分割的过程中,需要 1 个空间存储基准值。而快速排序的大概需要 Nlog2N次的分割处理,所以占用空间也是 Nlog2N 个。
算法稳定性
在快速排序中,相等元素可能会因为分区而交换顺序,所以它是不稳定的算法。
时间复杂度:
最好:O(nlog2n)O(n log_{2} n)O(nlog2n)
最坏:O(n2)O(n^2)O(n2)
平均:O(nlog2n)O(n log_{2} n)O(nlog2n)
空间复杂度:O(nlog2n)O(n log_{2} n)O(nlog2n)
5# 堆排序(heapSort)
简介
堆排序(英语:Heapsort)是指利用 二叉堆 这种数据结构所设计的一种排序算法。堆排序的适用数据结构为数组。
工作原理
本质是建立在堆上的选择排序。
排序过程
首先建立大顶堆,然后将堆顶的元素取出,作为最大值,与数组尾部的元素交换,并维持残余堆的性质;
之后将堆顶的元素取出,作为次大值,与数组倒数第二位元素交换,并维持残余堆的性质;
以此类推,在第n-1 次操作后,整个数组就完成了排序。
在数组上建立二叉堆
从根节点开始,依次将每一层的节点排列在数组里。
于是有数组中下标为 i 的节点,对应的父结点、左子结点和右子结点如下:
iParent(i) = (i - 1) / 2;
iLeftChild(i) = 2 * i + 1;
iRightChild(i) = 2 * i + 2;
算法评估
稳定性:不稳定,如同选择排序
速度:O(nlogn),算是高效算法
空间:由于可以在输入数组上建立堆,所以这是一个原地算法。
// C++ Version
void sift_down(int arr[], int start, int end) {// 计算父结点和子结点的下标int parent = start;int child = parent * 2 + 1;while (child <= end) { // 子结点下标在范围内才做比较// 先比较两个子结点大小,选择最大的if (child + 1 <= end && arr[child] < arr[child + 1]) child++;// 如果父结点比子结点大,代表调整完毕,直接跳出函数if (arr[parent] >= arr[child])return;else { // 否则交换父子内容,子结点再和孙结点比较swap(arr[parent], arr[child]);parent = child;child = parent * 2 + 1;}}
}void heap_sort(int arr[], int len) {// 从最后一个节点的父节点开始 sift down 以完成堆化 (heapify)for (int i = (len - 1 - 1) / 2; i >= 0; i--) sift_down(arr, i, len - 1);// 先将第一个元素和已经排好的元素前一位做交换,再重新调整(刚调整的元素之前的元素),直到排序完毕for (int i = len - 1; i > 0; i--) {swap(arr[0], arr[i]);sift_down(arr, 0, i - 1);}
}
6# 归并排序(mergeSort)
归并排序(英语:Merge sort,或 mergesort),是创建在归并操作上的一种有效的排序算法。1945年由约翰·冯·诺伊曼首次提出。
该算法是采用分治法( Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。
- 分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
- 再来看看 治 阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将【45,7,8】和【1,2,3,6】
两个已经有序的子序列,合并为最终序列【1,2,3,4,5,6,7,8】,来看下实现步骤
伪代码
归并排序评估:
- 最好最坏平均时间复杂度nlogn
- 空间复杂度高O(n)
- 是高效算法中唯一“稳定”的排序算法
- 较少用于内部排序,较多用于外部排序
模板:
// C++ Version
void merge(int ll, int rr) {// 用来把 a 数组 [ll, rr - 1] 这一区间的数排序。 t// 数组是临时存放有序的版本用的。if (rr - ll <= 1) return;int mid = ll + (rr - ll >> 1);merge(ll, mid);merge(mid, rr);int p = ll, q = mid, s = ll;while (s < rr) {if (p >= mid || (q < rr && a[p] > a[q])) {t[s++] = a[q++];// ans += mid - p;} elset[s++] = a[p++];}for (int i = ll; i < rr; ++i) a[i] = t[i];
}
// 关键点在于一次性创建数组,避免在每次递归调用时创建,以避免内存分配的耗时。
我们发现去掉代码中的注释,ans中保存的就是逆序对的数量
7# 插入排序(Insertion Sort)
基本思想:每一步将一个待排序的数据插入到前面已经排好序的有序序列中,直到插完所有元素为止。
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
程序在设计的时候应该注意一些点:每次要定一个基准数,逐个比较,覆盖的时候要留有备份。
C++函数代码:
void insertion_sort(int arr[],int len){for(int i=1;i<len;i++){int key=arr[i]; // 标兵int j=i-1;while((j>=0) && (key<arr[j])){ // 后移操作arr[j+1]=arr[j];j--;}arr[j+1]=key; // 最终赋值标兵}
}
C#函数代码:
public static void InsertSort(int[] array)
{for(int i = 1;i < array.length;i++){int temp = array[i];for(int j = i - 1;j >= 0;j--){if(array[j] > temp){array[j + 1] = array[j];array[j] = temp;}elsebreak;}}
}
程序需要,把这个函数复制,传递对应参数。
在插入排序中,我们可以用二分法来优化,从而更快确定数字
8# 选择排序(Selection sort)
选择排序是一种最简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
这是首批的O(n2)算法,如下是算法步骤。
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
C 语言
void swap(int *a,int *b)
{int temp = *a;*a = *b;*b = temp;
}
void selection_sort(int arr[], int len)
{int i,j;for (i = 0 ; i < len - 1 ; i++){int min = i;for (j = i + 1; j < len; j++) if (arr[j] < arr[min]) min = j; swap(&arr[min], &arr[i]); }
}
注:这种排序算法非常的**慢!慢!慢!**初学者可以使用这种算法,进阶后绝对不要使用这种算法,至少也要用上插入排序、桶排序之类的,除非排序时n<10000,或者简单的做个测试的时候使用。
9# 计数排序(CountingSort)
评估
稳定性:稳定
速度:O(n+m),m代表待排序数据的值域大小
工作原理:
计数排序的工作原理是使用一个额外的数组 C,其中第 i个元素是待排序数组 A 中值等于 i的元素的个数,然后根据数组 C来将 A中的元素排到正确的位置。
它的工作过程分为三个步骤:
- 计算每个数出现了几次;
- 求出每个数出现次数的 前缀和;
- 利用出现次数的前缀和,从右至左计算每个数的排名。
// C++ Version
const int N = 100010;
const int W = 100010;int n, w, a[N], cnt[W], b[N];void counting_sort() {memset(cnt, 0, sizeof(cnt));for (int i = 1; i <= n; ++i) ++cnt[a[i]];for (int i = 1; i <= w; ++i) cnt[i] += cnt[i - 1];for (int i = n; i >= 1; --i) b[cnt[a[i]]--] = a[i];
}
10# 桶排序(bucketSort)
桶排序评估:
速度:O(n)
空间复杂度:需要建立一个容量为输入数字中的最大数+1的数组,所以空间复杂度超高,所以输入数字最好不要超过10000000
// C++ Version
#include <iostream>
#include <cstring>
using namespace std;
#define N 100010
int a[N];
int n, t;
int main()
{memset(a, 0, sizeof(a));cin >> n;for (int i = 0; i < n; i++){cin >> t;a[t]++;}// 从小到大,要从大到小只需要改一下for循环开头// for (int i = N - 1; i >= 0; i--)for(int i = 0; i < N; i++){for (int j = 0; j < a[i]; j++)cout << i << " " ;}cout << endl;return 0;
}
PS#桶排序&计数排序&基数排序
基数排序有两种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值;
参考资料
- 漫画科普排序时间复杂度:https://blog.csdn.net/qq_41523096/article/details/82142747
- 时间复杂度:https://zh.wikipedia.org/wiki/时间复杂度
- 对数(logarithm):https://baike.baidu.com/item/对数/91326?fromtitle=log&fromid=39110&fr=aladdin
- 如何理解对数:https://www.zhihu.com/question/26097157/answer/265975884
- 基数排序:https://www.runoob.com/w3cnote/radix-sort.html
- 基数排序:https://github.com/hustcc/JS-Sorting-Algorithm/blob/master/10.radixSort.md
- 冒泡排序-三分钟彻底理解冒泡排序:https://www.cnblogs.com/bigdata-stone/p/10464243.html
- 冒泡排序:https://github.com/Github-Programer/JS-Sorting-Algorithm/blob/master/1.bubbleSort.md
- 插入排序(插入排序(图解)):https://blog.csdn.net/qq_33289077/article/details/90370899
- 十大经典排序算法(动图演示):https://zhuanlan.zhihu.com/p/73714165 (知乎,引用图片和简介)
- 百度百科,选择排序:https://baike.baidu.com/item/选择排序/9762418?fr=aladdin
- 希尔排序,CSDN:https://blog.csdn.net/qq_39207948/article/details/80006224
- 希尔排序,百度百科:https://baike.baidu.com/item/希尔排序
- 快速排序算法详解(原理、实现和时间复杂度):http://data.biancheng.net/view/117.html
- 快排:https://www.jianshu.com/p/5f38dd54b11f
- 《排序算法和高精度运算》贾志勇,中国计算机学会
- 桶排序:https://oi-wiki.org/basic/heap-sort/
- 计数排序:https://oi-wiki.org/basic/counting-sort/
参考书目
- 《信息学奥赛课课通》
- 《信息学奥赛一本通,C++篇》
- 《啊哈!算法》
- 《数据结构与算法分析,C语言描述》[美]马克·艾伦·维斯 著
参考程序
我将7种常用排序放集合在了一个程序里,你只需要在文件的同目录下创建一个RandintModel.txt
,其中的输入格式如下
n (一共n个数,至少大于1000,要不然测不出速度,当然你可以自己写一个随机数生成程序)
n个数
输出的是排序时间和一个文件Lastlog.txt,里面是最后一次的排序结果,不要用notepad打开,会卡死。(建议使用notepad++)
// SortingSpeed.cpp : This file contains the 'main' function. Program execution begins and ends there.
//#include <iostream>
#include <windows.h>
#include <cstdio>
#include <fstream>
#include <vector>
#include <array>
#include <algorithm>
#include <numeric>
//numeric_limits
using namespace std;
DWORD time_start, time_end;
int a[100010], n;void max_heapify(int arr[], int start, int end) {int dad = start;int son = dad * 2 + 1;while (son <= end) {if (son + 1 <= end && arr[son] < arr[son + 1])son++;if (arr[dad] > arr[son])return;else {swap(arr[dad], arr[son]);dad = son;son = dad * 2 + 1;}}
}void heap_sort(int arr[], int len) {for (int i = len / 2 - 1; i >= 0; i--)max_heapify(arr, i, len - 1);for (int i = len - 1; i > 0; i--) {swap(arr[0], arr[i]);max_heapify(arr, 0, i - 1);}
}
typedef struct _Range {int start, end;
} Range;Range new_Range(int s, int e) {Range r;r.start = s;r.end = e;return r;
}void swap(int* x, int* y) {int t = *x;*x = *y;*y = t;
}void quick_sort(int arr[], const int len) {if (len <= 0)return;Range* r = new Range[len];int p = 0;r[p++] = new_Range(0, len - 1);while (p) {Range range = r[--p];if (range.start >= range.end)continue;int mid = arr[(range.start + range.end) / 2];int left = range.start, right = range.end;do {while (arr[left] < mid) ++left;while (arr[right] > mid) --right;if (left <= right) {swap(&arr[left], &arr[right]);left++;right--;}} while (left <= right);if (range.start < right) r[p++] = new_Range(range.start, right);if (range.end > left) r[p++] = new_Range(left, range.end);}
}
//void Merge(vector<int>& Array, int front, int mid, int end) {// // preconditions:
// // Array[front...mid] is sorted
// // Array[mid+1 ... end] is sorted
// // Copy Array[front ... mid] to LeftSubArray
// // Copy Array[mid+1 ... end] to RightSubArray
// vector<int> LeftSubArray(Array.begin() + front, Array.begin() + mid + 1);
// vector<int> RightSubArray(Array.begin() + mid + 1, Array.begin() + end + 1);
// int idxLeft = 0, idxRight = 0;
// LeftSubArray.insert(LeftSubArray.end(), numeric_limits<int>::max());
// RightSubArray.insert(RightSubArray.end(), numeric_limits<int>::max());
// // Pick min of LeftSubArray[idxLeft] and RightSubArray[idxRight], and put into Array[i]
// for (int i = front; i <= end; i++) {// if (LeftSubArray[idxLeft] < RightSubArray[idxRight]) {// Array[i] = LeftSubArray[idxLeft];
// idxLeft++;
// }
// else {// Array[i] = RightSubArray[idxRight];
// idxRight++;
// }
// }
//}
//
//void MergeSort_Rec(vector<int>& Array, int front, int end) {// if (front >= end)
// return;
// int mid = (front + end) / 2;
// MergeSort_Rec(Array, front, mid);
// MergeSort_Rec(Array, mid + 1, end);
// Merge(Array, front, mid, end);
//}
void insertion_sort(int arr[], int len) {for (int i = 1; i < len; i++) {int key = arr[i];int j = i - 1;while ((j >= 0) && (key < arr[j])) {arr[j + 1] = arr[j];j--;}arr[j + 1] = key;}
}
int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{int maxData = data[0]; ///< 最大数/// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。for (int i = 1; i < n; ++i){if (maxData < data[i])maxData = data[i];}int d = 1;int p = 10;while (maxData >= p){//p *= 10; // Maybe overflowmaxData /= 10;++d;}return d;/* int d = 1; //保存最大的位数int p = 10;for(int i = 0; i < n; ++i){while(data[i] >= p){p *= 10;++d;}}return d;*/
}
void radixsort(int data[], int n) //基数排序
{int d = maxbit(data, n);int* tmp = new int[n];int* count = new int[10]; //计数器int i, j, k;int radix = 1;for (i = 1; i <= d; i++) //进行d次排序{for (j = 0; j < 10; j++)count[j] = 0; //每次分配前清空计数器for (j = 0; j < n; j++){k = (data[j] / radix) % 10; //统计每个桶中的记录数count[k]++;}for (j = 1; j < 10; j++)count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶for (j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中{k = (data[j] / radix) % 10;tmp[count[k] - 1] = data[j];count[k]--;}for (j = 0; j < n; j++) //将临时数组的内容复制到data中data[j] = tmp[j];radix = radix * 10;}delete[]tmp;delete[]count;
}
void selection_sort(int arr[], int len)
{int i, j;for (i = 0; i < len - 1; i++){for (j = i + 1; j < len; j++)if (arr[j] < arr[i])swap(arr[j], arr[i]); }
}
void shell_sort(int arr[], int len) {int gap, i, j;int temp;for (gap = len >> 1; gap > 0; gap >>= 1)for (i = gap; i < len; i++) {temp = arr[i];for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap)arr[j + gap] = arr[j];arr[j + gap] = temp;}
}
void bubble_sort(int arr[], int len)
{int i, j, temp;for (i = 0; i < len - 1; i++)for (j = 0; j < len - 1 - i; j++)if (arr[j] > arr[j + 1]) {temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;}
}
void menu()
{printf("Sorting Algorithm\n------------------\n");printf("1.希尔排序\n");printf("2.冒泡排序\n");printf("3.选择排序\n");printf("4.基数排序\n");printf("5.插入排序\n");printf("6.堆排序\n");printf("7.快速排序\n");// newprintf("--Choose Number--\n------------------\n");
}
void game()
{menu();int choose;cin >> choose;time_start = GetTickCount();if (choose == 1)shell_sort(a, n);else if (choose == 2)bubble_sort(a, n);else if (choose == 3)selection_sort(a, n);else if (choose == 4)radixsort(a, n);else if (choose == 5)insertion_sort(a, n);else if (choose == 6)heap_sort(a, n);else if (choose == 7)quick_sort(a, n);// newtime_end = GetTickCount();cout << "Time = " << (time_end - time_start) << "ms\n ";
}
int main()
{ifstream fin("RandintModel.txt");ofstream fout("Lastlog.txt");fin >> n;for (int i = 0; i < n; i++)fin >> a[i];game();printf("-----排序结果-----\n");for (int i = 0; i < n; i++){fout << a[i] << " ";} fout << endl;std::cout << "Hello World!\n";return 0;
}
参考习题
P1097 [NOIP2007 提高组] 统计数字
P1093 [NOIP2007 普及组] 奖学金
P1059 [NOIP2006 普及组] 明明的随机数
P1094 [NOIP2007 普及组] 纪念品分组
P1161 开灯
洛谷中选择排序
算法题目可以找到更多题目。
排序算法,最全的10大排序算法详解(Sort Algorithm)相关推荐
- dijkstra算法原理_这 10 大基础算法,程序员必知必会!
来源:博客园原文地址:http://kb.cnblogs.com/page/210687/算法一:快速排序算法快速排序是由东尼·霍尔所发展的一种排序算法.在平均状况下,排序n个项目要Ο(nlogn)次 ...
- 10大排序算法之二:冒泡排序【稳定的】,但复杂度高,一般不用冒泡排序的
10大排序算法之二:冒泡排序[稳定的],但复杂度高,一般不用冒泡排序的 提示:整个算法界,一共有十大排序算法,每一个算法都要熟悉,才算是算法入门 算法界的十大排序算法分别是: 选择排序.冒泡排序.插入 ...
- js排序的时间复杂度_JavaScript实现十大排序算法
一 : 冒泡排序 人们开始学习排序算法时,通常都先学冒泡算法,因为它在所有排序算法中最简单.然而, 从运行时间的角度来看,冒泡排序是最差的一个,接下来你会知晓原因 冒泡排序比较所有相邻的两个项,如果第 ...
- matlab中gad,10大经典算法matlab代码以及代码详解【数学建模、信号处理】
[实例简介] 10大算法程序以及详细解释,包括模拟退火,禁忌搜索,遗传算法,神经网络.搜索算法. 图论. 遗传退火法.组合算法.免疫算法. 蒙特卡洛.灰色预测.动态规划等常用经典算法.是数学建模.信号 ...
- 屏幕小于6英寸的手机_6英寸屏幕真的大么?你可能对“全面屏”有什么误解!详解全面屏手机那些事...
6英寸屏幕真的大么?你可能对"全面屏"有什么误解!详解全面屏手机那些事 2018-05-15 10:31:35 225点赞 278收藏 118评论 小编注:想获得更多专属福利吗?金 ...
- 大数据是什么和大数据技术十大核心原理详解
一.数据核心原理 从"流程"核心转变为"数据"核心 大数据时代,计算模式也发生了转变,从"流程"核心转变为"数据&quo ...
- 大数据技术十大核心原理详解
一.数据核心原理--从"流程"核心转变为"数据"核心 大数据时代,计算模式也发生了转变,从"流程"核心转变为"数据"核心 ...
- 最全的jquery datatables api 使用详解
https://www.cnblogs.com/amoniyibeizi/p/4548111.html 最全的jquery datatables api 使用详解 学习可参考:http://www.g ...
- Gartner2019年十大安全项目详解
(文章来源https://www.sec-un.org/gartner2019年十大安全项目详解/ ) 1. 概述 2019年2月11日,Gartner一改过去在年度安全与风险管理峰会上发表10大安全 ...
最新文章
- saltstack源码安装nrpe
- Tree-Structured LSTM模型
- raver php,为PhpStorm添加Laravel 代码智能提示功能
- ML之模型文件:机器学习、深度学习中常见的模型文件(.h5、.keras)简介、h5模型文件下载集锦、使用方法之详细攻略
- 成功解决NameError: name 'file' is not defined
- linux目录删除不释放空间,删除linux文件后,磁盘空间未释放的解决办法
- 高中技校学计算机,我没考上高中,英语数学极差,想上技校学计算机专业,玩代码的那种,有前途吗?...
- 结合反向传播算法使用python实现神经网络的ReLU、Sigmoid、Affine、Softmax-with-Loss层
- 移动App 网络优化细节探讨
- 线程中yield的用法
- 7个示例科普CPU Cache(from 酷壳网)
- PDF怎么裁剪页面,PDF裁剪页面的方法
- RRR-RR五边形平面并联机构分析:Kinematics of a five-bar RRR-RR mechanism
- 从iRedMail 创建web服务学习Nginx
- 同源策略——CORS和JSONP劫持漏洞
- 听听那冷雨 余光中
- charles(抓包神器)
- 【C语言航路】第一站:初识C语言(四)
- 线性回归和卡方分布与方差分析
- CNN中的translation equivariant和translation invariant
热门文章
- scrapy打包exe 成功详细教程
- 计算机中心2018年工作总结,2018年计算机程序员的年终工作总结范文
- 理财入门:基金(简述,主要是指数基金)
- 如何通过华硕路由器官方自带功能实现远程FTP、远程观影、远程同步、远程访问登陆界面,有了IPV6,甚至可以买个域名再实现黑裙远程登录
- iOS 疑难杂症 学习笔记
- 微软大佬的校招面试总结
- 分享两篇文章 - PMs in Microsoft
- Java“彭于晏,区块链技术与应用
- java 点阵打印机_Linux上点阵打印机的Java打印质量
- 通过Apache PDFBox将pdf转换为word