文章目录
一 排序的基本概念
排序啊,一个很有趣的话题,排序就是使得数据能够成为一种有序的操作。(并不严谨,可是这样理解就够了。)准确的定义,朋友们可以看看数据结构的书哈,我就不复制粘贴了哈。
- 为什么要有排序这个概念呢,它有啥用呢?
答:其实排序的主要目的就是为了提高查找效率,就比如说,二分查找算法,前提必须有有序的数据才可以使用,极大提高了查找效率。,在我的理解,我的理解上啊,其实从广义上讲:把数据结构分为四大块,线性表,非线性表,排序,查找。前面的三大板块都是为了查找做准备的,都是为了提高查找效率,怎么说呢?假如你在某个浏览器,输入你想要查找的东西,可是它半天都没显示出来,这就说明这个浏览器很菜,为什么菜,很大原因就是底层的代码,如数据结构是按,线性的,非线性的,使用的排序算法安排的不合理导致的呗。所以有必要学习以下排序,并且它非常有用哦。
- 排序的稳定与否判断
稳定:如果a没排序之前在b前面,且a = b,在排序之后a仍然在b的前面;
不稳定:如果a排序之前在b前面,且a=b,在排序之后a有可能会出现在b的后面;
- 内外排序是啥?
内排序:所有排序操作都在内存中完成;适合小数据的排序
外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
二 常见的基本排序
1. 插入排序
思想:插入排序,就是给你一组无序的数据,然后把数据分为 有序序列和无序序列 ,然后通过无序序列的数据和有序序列的数据进行比较,从而达到排序的目的。
但是我们思路插入排序的时候,首先是思考单趟插入排序是如何操作的,就比如玩扑克牌时候,你每插入一张牌之前,在你手中的排都是有序序列,当我们插入一张牌时候,就相当于对单趟插入排序进行了操作!
那我们就有办法了:先搞定单趟插入排序;
举个例子:
这个是 在 end >= 0的情况下找到的位置要插入值的情况
end<0情况,插入的值即是数组的第一个位置:
单趟排序的代码:
int end ; //假设我不知道下标是什么
int x ; //假设我不知道插入的值是什么
//插入单趟的逻辑
while (end >= 0)
{
if (x < a[end])
{
//把前面的往后面挪动
a[end + 1] = a[end];
//end往前走,更新指标
end--;
}
else //如果x >=a[end] 表示要插入到 end的后面,先跳出循环线,因为有两个逻辑可以用同一份代码处理
{
break;
}
}
//退出循环有两种情况:
//一:end<0时候:那么直接插入第一个位置
//二:x>=a[end]时候那么就插入end+1的位置
//三:x = a[end+1],即这一趟的比较是有序的,自己和自己换
a[end + 1] = x;
单趟插入排序写好了,那么就要写出给你一个无须的数组时候,你是如何使用单趟排序的思想了;
无非是从单趟的插入排序,变成多趟的插入排序,那么直接加个for循环就解决了,我们在单趟直接插入排序时候,是假设了数组本身就有序的;对于多趟的插入排序,我们也可以假设数组本身有序,但是实际是无序的,所以我们可以认为单个元素是有序的,然后,紧紧挨着有序数组后面的元素作为要插入的元素,用一个for来控制该段逻辑就可以;
插入排序的最终代码:
void InsertSort(int*a, int n)
{
for (int i = 0; i < n - 1; i++) //注意i的下标【0,n-2】
{
int end = i; //遍历end,从【0,n-2】
int x = a[i+1]; //把end+1后面的元素往前找位置,找到合适位置插入
//插入单趟的逻辑
while (end >= 0)
{
if (x < a[end])
{
//把前面的往后面挪动
a[end + 1] = a[end];
//end往前走,更新指标
end--;
}
else
{
break;
}
}
//退出循环有两种情况:
//一:end<0时候:那么直接插入第一个位置
//二:x>=a[end]时候那么就插入end+1的位置
//三:x = a[end+1],即这一趟的比较是有序的,自己和自己换
a[end + 1] = x;
}
}
细节:
上面的代码我们把单趟排序的下标end用 i 来控制,i 遍历往前走:保证每一趟的单趟排序,end都是指向每一趟有序数组的最后一个位置;
为什么 i 最多只能到 n -2 的位置呢?
因为i 到 n -2的位置是单趟排序end所在的位置,而我们end+1的位置,也是n -1的位置,已经被要插入的x所占用了,如果 end在 n-1的位置,那么x就会越界!
还有其版本的代码:供你们思路参考
void InsertSort (int *a,int length)
{
for (int i = 1; i< length;i++) // 控制比较次数
{
if(a[i] < a[i-1]) //升序排序
{
swap(a[i],a[i-1]); //交换两个数
//交换完两个数还不够,还要保证有序序列中也是有序的。
for (j = i-1;j > 0 && a[j] < a[j-1];j--)
{
swap(a[j],a[j-1]);
}
}
}
}
//交换函数
void swap(int &a,int &b)
{
int temp = a;
a = b;
b = temp;
}
另一种写法:
void InsertSort(int* a,int length)
{
for (int i = 1;i < length;i++) //控制比较次数
{
key = a[i]; //保存要插入的值,为了下面执行a[j+1] = a[j]时候,a[i]的值还在
for (j = i-1;j >= 0 && key < a[j];j--) //开始比较插入
a[j+1] = a[j];
//退出循环后
a[j+1] = key; //在执行a[j+1] = a[j]时候,
//a[i]也就是a[j+1]就被a[j]覆盖了,所以这句话的是把a[i]还会原来的位置
}
}
插入排序的时间复杂度
很简单:从原理思考:假设有n个数组;
第一次插入:挪动 1个位置;
第二次插入:挪动 2个位置;
第三次插入:挪动3个位置;
。
。
。
第 N 插入:挪动n个位置;
全部加起来,就是一个等差数列:时间复杂度就是O(n2)
最好时间复杂度是:O(n);也是从原理思考:假如数组本身有序的条件下:那么我们每一趟的单趟排序只需要比较一次,往end+1位置插入即可;
最坏时间复杂度就是:O(n2);也就是逆序对时候嘛!每次都需要挪动位置;
2. 希尔排序
思想:
希尔排序又叫增量缩小排序,就是把一组数据按一定的增量来分成多个小组来插入排序,直到增量缩小到1,则排序结束;
希尔排序怎么想出来的?首先我们知道直接插入排序在对接近有序的数组来说,直接插入排序还是相当快的,因为接近有序的数组使用直接插入排序几乎能达到O(n)的级别,这是什么概念,O(n)级别的排序是排序届的天花板啊;希尔这个人想:既然能够在接近有序的情况下我们的直接插入排序能够如此之快,那么是否可以让一个无序的数组,经过一些操作,然后就搞出接近有序的数组,再使用插入排序,那么就可以降低直接插入排序时间复杂度了吗?
举个例子解释以下希尔排序:
1.首先我们知道希尔排序是对直接插入排序的优化,那么我们需要做的事就是先对无序的数组预排序,排成接近有序的数组;
2.其次再使用直接插入排序,这样就可以完成希尔排序了!
我只画了排了一组的例子:其他的组你们可以画一画
按着这个思路:我们也不难写出单组的希尔排序;
int grap = 3; //假设grap = 3;
for (int i = 0; i < n - grap; i += grap) //注意细节:这里我们 i += grap
//这是控制end的移动的,
//为了控制单组排序完一个后,继续排单组的下一个数
{
int end = i;
int x = a[end + grap];
//排序单趟为grap分组的数据
while (end >= 0)
{
if (x < a[end])
{
a[end + grap] = a[end];
end -= grap;
}
else
{
break;
}
}
a[end + grap] = x;
}
那么我们就可以写出整趟的希尔排序排序;用一个循环控制即可!有多少组我们就用循环控制就行
int grap = 3; //假设grap = 3;
for(int j = 0;i <grap;j++) //用来控制每一组,比如这里grap等3那么我们控制3组,//每一组排序好再排下一组
{
for (int i = j; i < n - grap; i += grap) //注意细节:这里我们 i += grap
//这里的意思是控制每一组的排序
{
int end = i;
int x = a[end + grap];
//排序单趟为grap分组的数据
while (end >= 0)
{
if (x < a[end])
{
a[end + grap] = a[end];
end -= grap;
}
else
{
break;
}
}
a[end + grap] = x;
}
}
但是上面的写法我们并不认为很好:我们希望不要分每一组都排序,单独对每一组都排序,我们希望分好组了,但是我们却从左到右直接遍历,从每一组中都可以进行插入排序;
所以我们有这样的代码:
int grap = 3; //假设grap = 3;
for (int i = 0; i < n - grap; i++) //注意这里是i++,这个意思就是从左到右,每个分组都能遍历到,
//每个分组都一起排序
{
int end = i;
int x = a[end + grap];
//排序单趟为grap分组的数据
while (end >= 0)
{
if (x < a[end])
{
a[end + grap] = a[end];
end -= grap;
}
else
{
break;
}
}
a[end + grap] = x;
}
接下来我们最重要的是控制grap的增量了,grap到底等于多少好呢?我们一般认为grap每次预排序时候,都是1/2;这样比较好
最终代码:
//希尔排序
void ShellSort(int* a, int n)
{
//grap每一趟结束后都是预排序完,知道grap等于1就是直接插入排序,
int grap = n;
while (grap > 1) //控制grap的增量,刚开始是n/2,然后每一趟都继续缩减2倍,当grap缩到<=1就结束缩减了
{
grap /= 2;
//控制的是每趟grap/2分组的处理逻辑
//每一趟的grap/2下来,整个数组就越来越接近有序
//直到grap == 1时候,排完这一趟后,整个数组就会成为有序的数组
for (int i = 0; i < n - grap; i++)
{
int end = i;
int x = a[end + grap];
//排序单趟为grap分组的数据
while (end >= 0)
{
if (x < a[end])
{
a[end + grap] = a[end];
end -= grap;
}
else
{
break;
}
}
a[end + grap] = x;
}
}
}
还有其他版本的写法,供你参考以下:
void ShellSort(int* a, int length)
{
int j = 0;
for (int grap = length / 2; grap > 0; grap /= 2) //控制增量
{ //每一趟增量的插入排序
//每一趟都是排了一小组的一部分;
for (int i = grap; i < length; i++)
{
int key = a[i];//保存要插入的值
for ( j = i - grap; j >= 0 && key < a[j]; j -= grap)
a[j + grap] = a[j]; // 若后面的数 小于 前面, 把 前面的给后面的
//退出循环后,把前面的数插入要插入的值。
a[j + grap] = key;
}
}
}
对于 这个循环的解释 for (int i = grap;i > length;i++)
在我的理解:希尔排序也是插入排序的一种小变形方式;在插入排序中,增量就是为1的希尔排序;
希尔排序的时间复杂度分析
3. 选择排序
思想: 选择排序就是,给定一组无序数据,让第一个数据作为最小(最大)的,然后剩下的数据与其比较,找到新的最小(最大)的数,把新的替换旧的数,依次循环认为第二个数据最小…直到排完。
在这里插入图片描述
void SelectSort(int* a, int length)
{
for (int i = 0;i < length;i++)
{
int min = i; //先假设第一个为最小的
for(int j = i+1;j < length;j++) //第一个后面开始找新的最小的
{
if(a[j] < a[min])
min = j;
}
swap(a[min],a[i]);
}
}
//交换函数
void swap(int &a,int &b)
{
int temp = a;
a = b;
b = temp;
}
选择排序时间复杂度分析
从原理上分析:每次都是要找最小的下标;
第一次找:n
第二次找:n-1
第N次找:1
分析下来时间复杂度也是O(n2)
即使这个数组本身是有序的,也需要找n2次;
4.堆排序
思想:堆排序,也是叫做优先队列,使用一种“完全二叉树”的模式去存储数据,然而完全二叉树又可以用数组的形式的方式存储。而且,每一个父结点的值大于(小于)左右孩子的节点值,叫做大堆(小堆)。
完全二叉树的性质:
- 结点 下标 i 父结点的下标:i/2 -1;
- 结点下标 i 的左孩子的下标:i *2 +1;
- 结点下标 i 的有孩子的下标:i *2 +2;
推荐视频:堆排序(heapsort) 或者这个 排序算法:堆排序【图解+代码】,两个都讲得很清楚;
递归维护堆的性质
维护堆的性质是把不满足堆的排序方式的结点,维护成为堆的排序方式的结点。
// a为堆的数组,n为数组大小,i为要维护堆的下标。
//维护大堆的例子
void Heapify (int* a, int n,int i)
{
if (i >=n) //递归的结束条件
return;
// 先把维护的下标默认认为是最大值的下标
int maxIndex = i;
int left = 2*i+1;
int right = 2*i+2;
//开始找真实最大值的下标
if (left < n && a[left] > a[maxIndex]) //左大于父的话,就换新的maxIndex
maxIndex = left;
if (right < n && a[right] > a[maxIndex])
//右大于父(在左大于父不成立下)或者右大于左,
//在(左大于父的前提下),把新的maxIndex找出来
maxIndex= right ;
if (maxIndex != i) //说明左右结点其中一个大于父结点
{
swap(a[maxIndex],a[i]);
Heapify(a,n,maxIndex);
}
}
这段代码的意思就是,假如要从某个已知不成堆的结点 i 开始 heapify; 假如我要从任意结点开始呢?那可以从最后一个结点的父结点开始逐渐递减就可以 heapify 全部的节点了。这就是建立堆的过程。
这图就是从最后一个结点的父节点开始堆化:
非递归维护堆的性质----向下调整算法
void heapify(int *a,int n,int parent)
{
int left = 2*parent + 1;
while (left < n)
{ //先找出根左右最大值的下标
if (left + 1 < n && a[left+1] > a[left]) //交换前看看右结点的值是否大于根节点;
left++;
if (a[left] > a[parent]) //右结点的值不大于根节点;左结点和根结点交换
{ //右结点得值大于根节点,
swap(a[left],a[parent]);
//继续对下面的结点堆化
parent = left;
left = 2*parent + 1;
}
else //假如没有 左右结点比parent大的话就结束堆化;
break;
}
}
建立堆
建立堆:就是维护堆的性质,把任意结点 i 的位置定为 最后一个结点;对最后一个结点的父节点进行维护堆;
void BulidHeap(int *a,int n)
{
int lastNode = n - 1; //先找出最后一个结点
for (int i = lastNode/2 -1; i > 0;i--) // 从最后一个结点的父节点开始向上堆化构建,
//到根节点时候结束堆化
{
heapify(a,n,i);
}
}
堆排序
建立起的对我们结构我们还没开始排序呢。现在开始排序。
因为刚刚建立了大堆,所以我们可以知道根结点一定是堆树中最大的值,我们而要做就是
- 把根节点和最后一个结点做一次交换,然后取出交换后的最后一个结点,这样就可以把最大的值取出来了,取出来的目的也是为了排序,然后由于交换了就会破坏堆的结构。
- 然后对交换后的根结点再进行一次堆化就可以了。继续完成上诉操作,依次把堆树中最大的值取出来,这样就可以排序啦。
void HeapSort(int* a,int n)
{
int lastNode= n -1;
for (int i = lastNode; i >= 0; i--) //从最后一个结点开始和根交换
{
swap(a[0],a[i]);
//交换后,根又不满足堆的性质了,所以重新堆化
heapify(a,i,0); //对根堆化,根的下标为 0;数组大小每次会减一
}
}
堆排序的时间复杂度
建堆的时间复杂度
对于排序时候
所以总的时间复杂度是O(n logn)
5 归并排序
思想:归并排序就是将一个无序得序列,先分开,直到分到只有一个元素,然后再合并排序;
归并排序的操作就有点像二叉树的后序遍历一样:先分开左边数组,再分开右边数组,当两边数组都是有序时候,就可以处理了,做的处理就是归并起来!
void merge_sort(int a[], int temp[], int left, int right)
{
if (left >= right) // left > right 不用分了, left = right 就是分到一个元素了,也不用分了
return;
//先把数组分开 ,在 left < right 的前提下
int mid = (left + right) / 2;
//分为左边的数组
merge_sort(a, temp, left, mid );
// 分为右边的数组
merge_sort(a, temp, mid + 1, right);
//开始归并
// 定义一个 左边数组的第一个元素的下标
int l_pos = left;
//定义一个 右边数组的第一个元素的下标
int r_pos = mid + 1;
// 定义一个存放归并结果的数组下标,用来后面归并时候的操作
int t_pos = left;
//开始归并,到临时数组
//在满足左边数组的第一个元素下标<=最后一个元素的下标
//和右边数组第一个元素小于等于最后一个元素的下标前提下
while (l_pos <= mid && r_pos <= right)
{
if (a[l_pos]<a[r_pos])
temp[t_pos++] = a[l_pos++];
else
temp[t_pos++] = a[r_pos++];
}
//退出循环后
//假如左边数组还有元素,直接将左边数组的元素丢进去临时数组
while(l_pos <= mid)
{
temp[t_pos++] = a[l_pos++];
}
//假如右边数组还有元素,直接将右边数组的元素丢进去临时数组
while(r_pos <= right)
{
temp[t_pos++] = a[r_pos++];
}
// 将临时数组拷贝到 原数组
while (left <= right)
{
a[left] = temp[left];
left++;
}
}
void MergeSort(int a[], int n)
{ //给 归并的结果存放的数组开辟一个空间
int *temp = (int*)malloc(n*sizeof(int));
if (temp)
{ //开辟成功;
//开始归并排序,先分开再归并
merge_sort(a, temp, 0, n - 1);
free(temp);
}
}
6 快速排序
思想:就是任意选定一个数为 pivotKey(中心轴关键字),为了方便,通常这个pivotKet 为第一个元素,把剩余的元素和 pivotKey 比较,比它大放到右边,比它小放到左边;然后重复操作,直到分到左右的序列只有一个就排好序了。
举个例子:在单趟的快速排序中:假如排如下数组
开始单趟的快速排序:
注意细节:
假如我们选最左边的作为key值,那么我们就需要右边的right先走;这样可以保证,当到相遇点时候,相遇点的值肯定是比key要小的,此时交换就不会出错;
假如我们选最右边的最为key值,那么我嫩就需要左边的left先走;这样可以保证,当到相遇点时候,相遇点的值肯定是比key要大的,此时交换就不会出错;
基于上面的操作我们可以写出单趟快速排序的算法:
//快速排序的单趟排序
//选左边的第一个值做key,那么就需要右边的值先走
//需要的是:找左边的比key大,找右边比key小的,找到之后就交换两个位置
int Partion(int* a, int left, int right)//[left,right]
{
int keyI = left; //定左边的第一个值为关键字
while (left < right) //只要没相遇,就一直走下去
{
//右边的先走,因为我们选了左的为key值
while (a[right] >= a[keyI] && right > left) //而相等也作为条件的原因是:
//相等需要一直往前走,假如不走那么如果数组都是相等的值,
//就会陷入一直交换的死循环
//right > left 的原因防止:
//right一直都没有找到比key小的,会一直往前走直到越界
{
right--;
}
//右边走完左边再走
while (a[left] <= a[keyI] && left < right) //left < right的原因防止:
{ //left一直没有找到比key大的,会一直往右走直到越界
left++;
}
//当右边找到比key要小的,左边找到比key要大的时候,就交换
Swap(&a[left], &a[right]);
}
//当上面循环结束:表示到了相遇点,那么我们用相遇点和key交换
//这样就会完成左边的都比key的小,右边的都比key的大
Swap(&a[left], &a[keyI]);
//最后返回相遇点就行:相遇点就是新的keyI
return left;
}
单趟的快速排序有了,那么我们就可以写出整趟的快速排序:
我们只要继续递归key左边的数组:做单趟的快速排序;继续递归key右边的数组的单谈快速排序即可了;
void QuickSort(int* a, int left,int right)
{
if (left >= right) //递归出口
return;
//拿到中间的keyI值
int keyI = Partion(a,left,right);
//递归排序左边的
QuickSort(a, left, keyI-1);
//递归排序右边的
QuickSort(a, keyI+1, right);
}
快速排序的时间复杂度
我们从原理出发:
我们从left找大,right找小,走了两个步骤加起来走了n个步骤;
然后以key分区,左边的走n/2,右边走n/2;继续分下去,每一步都是除2;
但是我们从递归二叉树图看,它们尽管分开,但是每次递归都是走了N步骤;
而二叉树的高度是logn,每一层是n,总共有logn层,所以时间复杂度为O(n logn)
快速排序有什么缺陷吗?
对于有序的数组,那么快速排序的时间复杂度会直接降到O(n2)
从原理出发:
第一次:right往前找,都找不到比key小的值,那么right就会走n步;
第二次:right往前走,也找不到比key小的,right走了n-1步;
…
第n次:right走了1步,就可以确认是有序了;
此时时间复杂度直接到O(n2);
如何避免这个问题呢?
关键是key的选择,因为当数组有序的时候,我们key还选择最左边作为key值,那么就会使得时间复杂度降低;
我们可以使用三数取中法选出合理的key值,也就是选择最左边和最右边和中间的值,取它们三个数中第二大的数,这样作为key值,我们就可以避免right一直往左走走n步骤了;
//获取三个数中第二大的值的下标
int GetMidIndex(int*a, int left, int right)
{
//int mid = (left + right) / 2;
//int mid = left + ((right - left) / 2);
int mid = left + ((right - left) >>1);
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;
}
return right;
}
}
//快速排序的单趟排序
//选左边的第一个值做key,那么就需要右边的值先走
//需要的是:找左边的比key大,找右边比key小的,找到之后就交换两个位置
int Partion(int* a, int left, int right)//[left,right]
{
int mid = GetMidIndex(a,left.right);
Swap(&a[mid],&a[left]);
int keyI = left; //定左边的第一个值为关键字
while (left < right) //只要没相遇,就一直走下去
{
//右边的先走,因为我们选了左的为key值
while (a[right] >= a[keyI] && right > left)
{
right--;
}
//右边走完左边再走
while (a[left] <= a[keyI] && left < right)
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyI]);
return left;
}
快速排序–挖坑法
快速排序–非递归栈模拟实现
用栈模拟实现,也是模拟递归调用栈帧开辟的情况!
放入总区间,再取出来处理总区间,处理完,继续放左区间和右区间;
大致思路是把数组的,左右指针分别入栈;然后出栈处理,如何处理?以关键值得位置来分区左右处理;
//非递归版本
//相当于用栈模拟递归的实现
void QuickSortR(int* a, int left, int right)
{
ST st;
StackInit(&st);
StackPush(&st, left);
StackPush(&st, right);
while (!StackIsEmpty(&st))
{
//获取【left,right】区间处理
int end = StackTop(&st);
StackPop(&st);
int begin = StacTop(&st);
StackPop(&st);
//以keyI为基准分割左右区间
int keyI = Partion(a, begin, end);
//分开的区间【begin,keyi-1】keyi【keyi+1,end】
//入栈keyI的右边区间
if (keyI + 1 < end)//【keyi+1,end】至少有两个值才可以放入栈
{
StackPush(&st, keyI + 1);
StackPush(&st, end);
}
//入栈keyI的左边区间
if (begin < keyI - 1)
{
stackPush(&st, begin);
stackPush(&st, keyI - 1);
}
}
StackDestroy(&st);
}
7 冒泡排序
思想:冒泡排序就是相邻的两个元素比较,前面大于后面的就交换,重复这个步骤;
普通的冒泡排序相信大家都会,这里我提供一种优化版的;我们都知道假如你给的序列就是有序的,就不需要排序了,可是,在普通冒泡排序中,并不是这样,及时给你是有序的还是会一直找很多遍,优化版的只需要找一遍就可以;
void swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void bubble_sort(int arr[], int n)
{
bool flag = false;
for (int i = 0; i < n-1; i++)
{ //没交换保持flag = false;
flag = false;
for (int j = 0; j < n - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
flag = true;
swap(&arr[j], &arr[j + 1]);
}
}
//若一趟下来,都没有交换,说明 已经是有顺序的了,直接退出外循环
if (flag == false)
break;
}
}
8 计数排序
思想:计数排序就是统计一个无序序列的范围大小,把里面相同的元素个数统计出来,把每组相同元素的个数存放到一个数组中,用数组下标对应元素的值。然后再取出其中的值。
计数排序适合数据比较集中,范围不大的数排序!
类似一种哈希表的映射:用元素的值映射到数组下标。
void counting_sort(int a[], int n)
{
//统计元素的范围大小,先找出元素的最大和最小值
int min = a[0]; //假定最小值为第一个元素
int 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 *temp = (int*)malloc(range*sizeof(int));
assert(temp);
//初始化数组
memset(temp, 0, range*sizeof(int));
//往数组temp添加相同元素的个数,于此同时,下标对应着元素的映射值
for (int j = 0; j < n; j++)
temp[a[j] - min]++;
//开始把值赋给 a 数组
int i = 0; //存放数组 a 的下标
for (int j = 0; j < range; j++)
{
while (temp[j]--)
{
a[i++] = j + min;
}
}
free(temp);
}
注意:记得开辟内存时候大小别赋值错了,要不然改bug,改到你头疼;