文章目录
思维导图
直接插入排序
动图演示:
在待排序的元素中,假设前n-1个元素已有序,现将第n个元素插入到前面已经排好的序列中,使得前n个元素有序。按照此法对所有元素进行插入,直到整个序列有序。
但我们并不能确定待排元素中究竟哪一部分是有序的,所以我们一开始只能认为第一个元素是有序的,依次将其后面的元素插入到这个有序序列中来,直到整个序列有序为止。
代码:
//插入排序
//最坏的情况O(N^2) 逆序
//最好的情况O(N) 顺序有序或者接近顺序有序
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; ++i)
{
// [0,end] 插入 end+1 [0, end+1]有序
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
--end;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
时间复杂度:O(N^2) 空间复杂度:O(1)
希尔排序
动图演示:
希尔排序,又称缩小增量法。其基本思想是:
1.先选定一个小于N的整数gap作为第一增量,然后将所有距离为gap的元素分在同一组,并对每一组的元素进行直接插入排序。然后再取一个比第一增量小的整数作为第二增量,重复上述操作…
2.当增量的大小减到1时,就相当于整个序列被分到一组,进行一次直接插入排序,排序完成。
问题:为什么要让gap由大到小呢?
answer:gap越大,数据挪动得越快;gap越小,数据挪动得越慢。前期让gap较大,可以让数据更快得移动到自己对应的位置附近,减少挪动次数。
注:一般情况下,取序列的一半作为增量,然后依次减半,直到增量为1(也可自己设置)。
举个例子分析一下:
现在我们用希尔排序对该序列进行排序。
我们用序列长度的一半作为第一次排序时gap的值,此时相隔距离为4的元素被分为一组,然后分别对每一组进行直接插入排序。
gap的值折半,此时相隔距离为2的元素被分为一组,然后再分别对每一组进行直接插入排序。
gap的值再次减半,此时gap减为1,即整个序列被分为一组,进行一次直接插入排序。
该题中,前两趟就是希尔排序的预排序,最后一趟就是希尔排序的直接插入排序。
代码:
//希尔排序
//1.预排序 -> 间隔为gap的数据分为一组
//2.插入排序
//O(N^1.3)
void ShellSort(int* a, int n)
{
int gap = n;
//gap越大 大的数据越快到后面 小的数据越快到前面 gap越小 跳得越慢 但是越接近有序
// gap > 1 预排 gap = 1 快排
while (gap > 1)
{
//gap = gap / 3 + 1;//为了出现1 完成快排
gap = gap / 2;//也可以
for (int j = 0; j < gap; ++j)
{
for (int i = j; i < n - gap; i += gap)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
}
平均时间复杂度:O(N^1.3) 空间复杂度:O(1)
选择排序
动图演示:
选择排序,即每次从待排序列中选出一个最小值,然后放在序列的起始位置,直到全部待排数据排完即可。
代码:
//选择排序(一次选一个数)
void SelectSort(int* a, int n)
{
int i = 0;
for (i = 0; i < n; i++)//i代表参与该趟选择排序的第一个元素的下标
{
int start = i;
int min = start;//记录最小元素的下标
while (start < n)
{
if (a[start] < a[min])
min = start;//最小值的下标更新
start++;
}
Swap(&a[i], &a[min]);//最小值与参与该趟选择排序的第一个元素交换位置
}
}
时间复杂度:O(N^2) 空间复杂度:O(1)
优化:实际上,我们可以一趟选出两个值,一个最大值一个最小值,然后将其放在序列开头和末尾,这样可以使选择排序的效率快一倍。
//最坏时间复杂度 O(N^2)
//最好时间复杂度 O(N^2)
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
//选出最小的放到begin位置
//选出最大的放大end位置
int mini = begin, maxi = end;
for (int i = begin; i <= end; ++i)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[begin], &a[mini]);
//修正maxi
if (maxi == begin)
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
++begin;
--end;
}
}
时间复杂度:O(N^2) 空间复杂度:O(1)
堆排序
要学习堆排序,首先要学习堆的向下调整算法,因为要用堆排序,你首先得建堆,而建堆需要执行多次堆的向下调整算法。
堆的向下调整算法(使用前提):
若想将其调整为小堆,那么根结点的左右子树必须都为小堆。
若想将其调整为大堆,那么根结点的左右子树必须都为大堆。
向下调整算法的基本思想(以建大堆为例):
1.从根结点处开始,选出左右孩子中值较大的孩子。
2.让大的孩子与其父亲进行比较。
若大的孩子比父亲还大,则该孩子与其父亲的位置进行交换。并将原来大的孩子的位置当成父亲继续向下进行调整,直到调整到叶子结点为止。
若大的孩子比父亲小,则不需处理了,调整完成,整个树已经是大堆了。
堆的向下调整算法代码:
//堆的向下调整算法
void AdjustDown(int* a, int n, int root)
{
int parent = root;
int child = 2 * parent + 1;//假设左孩子较大
while (child < n)
{
if (child + 1 < n&&a[child + 1] > a[child])//右孩子存在,并且比左孩子大
{
child++;//左右孩子的较大值
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = 2 * parent + 1;
}
else//已成堆
{
break;
}
}
}
使用堆的向下调整算法,最坏的情况下(即一直需要交换结点),需要循环的次数为:h - 1次(h为树的高度)。而h = Log2(N+1)(N为树的总结点数)。所以堆的向下调整算法的时间复杂度为:O(logN) 。
上面说到,使用堆的向下调整算法需要满足其根结点的左右子树均为大堆或是小堆才行,那么如何才能将一个任意树调整为堆呢?
答案很简单,我们只需要从倒数第一个非叶子结点开始,从后往前,按下标,依次作为根去向下调整即可。
建堆代码:
//建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(php->a, php->size, i);
}
总结:
堆的向下调整算法时间复杂度:O(N)
堆的向下调整算法的时间复杂度为O(log2N)
那么堆建好后,如何进行堆排序呢?
步骤如下:
1、将堆顶数据与堆的最后一个数据交换,然后对根位置进行一次堆的向下调整,但是调整时被交换到最后的那个最大的数不参与向下调整。
2、完成步骤1后,这棵树除最后一个数之外,其余数又成一个大堆,然后又将堆顶数据与堆的最后一个数据交换,这样一来,第二大的数就被放到了倒数第二个位置上,然后该数又不参与堆的向下调整…反复执行下去,直到堆中只有一个数据时便结束。此时该序列就是一个升序。
堆排序的代码:
//堆排序
void HeapSort(int* a, int n)
{
//排升序,建大堆
//从第一个非叶子结点开始向下调整,一直到根
int i = 0;
for (i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;//记录堆的最后一个数据的下标
while (end)
{
Swap(&a[0], &a[end]);//将堆顶的数据和堆的最后一个数据交换
AdjustDown(a, end, 0);//对根进行一次向下调整
end--;//堆的最后一个数据的下标减一
}
}
时间复杂度:O(N*log2N) 空间复杂度:O(1)
冒泡排序
动图演示:
代码:
//冒泡排序
//最坏情况 O(N^2)
//最好情况 O(N)
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n - 1; ++j)//趟数
{
int exchange = 0;
for (int i = 1; i < n - j; ++i)//单次
{
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年提出的一种二叉树结构的交换排序算法,其基本思想为:
任取待排序元素序列中的某元素作为基准值,按照该基准值将待排序列分为两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右序列重复该过程,直到所有元素都排列在相应位置上为止。
对于如何按照基准值将待排序列分为两子序列,常见的方式有:
1、Hoare版本
2、挖坑法
3、前后指针法
单趟排序
单趟排序的意义:
一次单趟排序完成代表在某个数组中排第j的元素就放至了数组中第j-1的位置,然后将这个数组一分为二,继续完成单趟排序。
下面用hoare法画图举例:
Hoare版本
单趟动图演示方法:
Hoare版本的单趟排序的基本步骤如下:
1、选出一个key,一般是最左边或是最右边的。
2、定义一个L和一个R,L从左向右走,R从右向左走。(需要注意的是:若选择最左边的数据作为key,则需要R先走;若选择最右边的数据作为key,则需要L先走)。
3、在走的过程中,若R遇到小于key的数,则停下,L开始走,直到L遇到一个大于key的数时,将L和R的内容交换,R再次开始走,如此进行下去,直到L和R最终相遇,此时将相遇点的内容与key交换即可。(选取最左边的值作为key)
经过一次单趟排序,最终使得key左边的数据全部都小于key,key右边的数据全部都大于key。
然后我们在将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作,因为这种序列可以认为是有序的。
那么大家会有疑问,为什么最终相遇的地方一定比key小呢?
R先走会保证上面这个疑问:
Answer:
1.假设R找到了停下,L开始走,直到相遇,则该位置就是比key小的值、
2.假设L找到了停下(与上面不同的是先完成交换),R开始走,直到相遇,此时,相遇的值比key小(因为已经完成了交换)
//单趟排序的价值:
//key已经到了它的最终位置,这个数已经排好了
//分割出两个子区间。如果子区间有序,整体就有序了,子区间如何有序?->递归
//hoare方法
int PartSort1(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
//右边先走 R找小 相等的一定要过滤掉
while (a[right] >= a[keyi] && left < right) //不加=这个条件又可能会死循环
{
--right;
}
//左边后走 L找大
while (a[left] <= a[keyi] && left < right)
{
++left;
}
if (left < right)
{
Swap(&a[left], &a[right]);
}
}
int meeti = left;
//相遇了以后换一下
Swap(&a[meeti], &a[keyi]);
return meeti;
}
挖坑法
动图演示:
挖坑法的单趟排序的基本步骤如下:
1、选出一个数据(一般是最左边或是最右边的)存放在key变量中,在该数据位置形成一个坑。
2、还是定义一个L和一个R,L从左向右走,R从右向左走。(若在最左边挖坑,则需要R先走;若在最右边挖坑,则需要L先走)。
3、在走的过程中,若R遇到小于key的数,则将该数抛入坑位,并在此处形成一个坑位,这时L再向后走,若遇到大于key的数,则将其抛入坑位,又形成一个坑位,如此循环下去,直到最终L和R相遇,这时将key抛入坑位即可。(选取最左边的作为坑位)
经过一次单趟排序,最终也使得key左边的数据全部都小于key,key右边的数据全部都大于key。
然后也是将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作。
代码:
//挖坑方法
int PartSort2(int* a, int left, int right)
{
int key = a[left];
int hole = left;
while (left < right)
{
//右边找小,填到左边的坑
while (left < right && a[right] >= key)
{
--right;
}
a[hole] = a[right];
hole = right;
//左边找大, 填到右边的坑
while (left < right && a[left] <= key)
{
++left;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
前后指针法
前后指针法的单趟排序的基本步骤如下:
1、选出一个key,一般是最左边或是最右边的。
2、起始时,prev指针指向序列开头,cur指针指向prev+1。
3、若cur指向的内容小于key,则prev先向后移动一位,然后交换prev和cur指针指向的内容,然后cur指针++;若cur指向的内容大于key,则cur指针直接++。如此进行下去,直到cur指针越界,此时将key和prev指针指向的内容交换即可。
经过一次单趟排序,最终也能使得key左边的数据全部都小于key,key右边的数据全部都大于key。
然后也还是将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作。
代码:
//前后指针方法
int PartSort3(int* a, int left, int right)
{
int keyi = left;
int prev, cur;
prev = cur = left;
cur = prev + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
++cur;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
递归实现
完成一次单趟排序,返回相遇的位置(key排到了自己该排的位置),然后以相遇位置,划分左右界限,继续完成单趟排序。
代码:
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
//三种单趟排序算法任一种即可
//int keyi = PartSort1(a, begin, end);
//int keyi = PartSort2(a, begin, end);
int keyi = PartSort3(a, begin, end);
//[begin, keyi-1],keyi,[keyi+1, right]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
非递归实现
当我们需要将一个用递归实现的算法改为非递归时,一般需要借用一个数据结构,那就是栈。将Hoare版本、挖坑法以及前后指针法的快速排序改为非递归版本,其实主体思想一致,只是调用的单趟排序的算法不同而已。
于是我们可以先将Hoare版本、挖坑法和前后指针法的单趟排序单独封装起来。然后写一个非递归的快速排序,在函数内部调用单趟排序的函数即可。
代码:
void QuickSortNonR(int* a, int begin, int end)
{
Stack st;
StackInit(&st);
StackPush(&st, begin);
StackPush(&st, end);
while (!StackEmpty(&st))
{
int right = StackTop(&st);//取出栈顶的元素
StackPop(&st);//弹出栈
int left = StackTop(&st);
StackPop(&st);
//1.第一种方法
if (left >= right)
{
continue;
}
int keyi = PartSort3(a, left, right);
//[left, keyi-1] keyi [keyi+1, right]
StackPush(&st, keyi + 1);
StackPush(&st, right);
StackPush(&st, left);
StackPush(&st, keyi - 1);
//第二种方法
/*int keyi = PartSort3(a, left, right);
if (keyi + 1 < right)
{
StackPush(&st, keyi + 1);
StackPush(&st, right);
}
if (left < keyi - 1)
{
StackPush(&st, left);
StackPush(&st, keyi - 1);
}*/
}
StackDestroy(&st);
}
快速排序的两个优化
三数取中
为什么要进行三数取中呢?接下来画图解释:
快速排序的时间复杂度是O(NlogN),是我们在理想情况下计算的结果。在理想情况下,我们每次进行完单趟排序后,key的左序列与右序列的长度都相同:
若每趟排序所选的key都正好是该序列的中间值,即单趟排序结束后key位于序列正中间,那么快速排序的时间复杂度就是O(NlogN)。
可是谁能保证你每次选取的key都是正中间的那个数呢?当待排序列本就是一个有序的序列时,我们若是依然每次都选取最左边或是最右边的数作为key,那么快速排序的效率将达到最低:
可以看到,这种情况下,快速排序的时间复杂度退化为O(N2)。其实,对快速排序效率影响最大的就是选取的key,若选取的key越接近中间位置,则则效率越高。
为了避免这种极端情况的发生,于是出现了三数取中:
三数取中,当中的三数指的是:最左边的数、最右边的数以及中间位置的数。三数取中就是取这三个数当中,值的大小居中的那个数作为该趟排序的key。这就确保了我们所选取的数不会是序列中的最大或是最小值了。就能让后续的排序过程变成类似数的结构,避免称为类似冒泡排序类的算法。
代码:
int GetMidIndex(int* a, int left, int right)
{
int mid = (right + left) / 2;
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 right;
}
else
{
return left;
}
}
}
将三数取中放至单趟排序中:
//hoare方法
int PartSort1(int* a, int left, int right)
{
//三数取中
int mid = GetMidIndex(a, left, right);
Swap(&a[left], &a[mid]);
int keyi = left;
while (left < right)
{
//右边先走 R找小 相等的一定要过滤掉
while (a[right] >= a[keyi] && left < right) //不加=这个条件又可能会死循环
{
--right;
}
//左边后走 L找大
while (a[left] <= a[keyi] && left < right)
{
++left;
}
if (left < right)
{
Swap(&a[left], &a[right]);
}
}
int meeti = left;
//相遇了以后换一下
Swap(&a[meeti], &a[keyi]);
return meeti;
}
另外两种单趟排序类似,先获取中间数,然后和left交换,交换后,同样选择left为key,完成单趟排序的流程。
//挖坑方法
int PartSort2(int* a, int left, int right)
{
//三数取中
int mid = GetMidIndex(a, left, right);
Swap(&a[left], &a[mid]);
int key = a[left];
int hole = left;
while (left < right)
{
//右边找小,填到左边的坑
while (left < right && a[right] >= key)
{
--right;
}
a[hole] = a[right];
hole = right;
//左边找大, 填到右边的坑
while (left < right && a[left] <= key)
{
++left;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
//前后指针方法
int PartSort3(int* a, int left, int right)
{
//三数取中
int mid = GetMidIndex(a, left, right);
Swap(&a[left], &a[mid]);
int keyi = left;
int prev, cur;
prev = cur = left;
cur = prev + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
++cur;
}
/*while (cur < right - left + 1)
{
while (a[cur] < a[keyi] && cur < right - left + 1 && ++prev != cur)
{
++prev;
Swap(&a[prev], &a[cur]);
}
++cur;
}*/
Swap(&a[prev], &a[keyi]);
return prev;
}
小区间优化
我们可以看到,就算是上面理想状态下的快速排序,也不能避免随着递归的深入,每一层的递归次数会以2倍的形式快速增长。
为了减少递归树的最后几层递归,我们可以设置一个判断语句,当序列的长度小于某个数的时候就不再进行快速排序,转而使用其他种类的排序。小区间优化若是使用得当的话,会在一定程度上加快快速排序的效率,而且待排序列的长度越长,该效果越明显。
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
if (end - begin >= 8)
{
InsertSort(a + begin, end - begin + 1);
}
else
{
//三种单趟排序算法任一种即可
//int keyi = PartSort1(a, begin, end);
//int keyi = PartSort2(a, begin, end);
int keyi = PartSort3(a, begin, end);
//[begin, keyi-1],keyi,[keyi+1, right]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
}
归并排序
动图演示:
归并排序是采用分治法的一个非常典型的应用。其基本思想是:将已有序的子序合并,从而得到完全有序的序列,即先使每个子序有序,再使子序列段间有序。
递归实现
归并排序,从其思想上看就很适合使用递归来实现,并且用递归实现也比较简单。其间我们需要申请一个与待排序列大小相同的数组用于合并过程两个有序的子序列,合并完毕后再将数据拷贝回原数组。
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (end + begin) / 2;
// [begin, mid] [mid+1, end]
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
// 归并 取小的尾插
// [begin, mid] [mid+1, end]
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
// 拷贝回原数组 -- 归并哪部分就拷贝哪部分回去
memcpy(a + begin, tmp + begin, (end - begin + 1)*sizeof(int));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
时间复杂度:O(N*LogN) 空间复杂度:O(N)
非递归实现
归并排序的非递归算法并不需要借助栈来完成,我们只需要控制每次参与合并的元素个数即可,最终便能使序列变为有序:
当然,以上例子是一个待排序列长度比较特殊的例子,我们若是想写出一个广泛适用的程序,必定需要考虑到某些极端情况:
情况一(end2越界):
当最后一个小组进行合并时,第二个小区间存在,但是该区间元素个数不够gap个,这时我们需要在合并序列时,对第二个小区间的边界进行控制。
情况二(begin2和end2都越界):
当最后一个小组进行合并时,第二个小区间不存在,此时便不需要对该小组进行合并。
情况三(end1越界):
当最后一个小组进行合并时,第二个小区间不存在,并且第一个小区间的元素个数不够gap个,此时也不需要对该小组进行合并。(可与情况二归为一类)
综上:可以总结为一下三种越界情况
代码:
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
int gap = 1;
while (gap < n)
{
// gap个数据 gap个数据归并
for (int j = 0; j < n; j += 2 * gap)
{
// 归并 取小的尾插
int begin1 = j, end1 = j + gap - 1;
int begin2 = j + gap, end2 = j + 2 * gap - 1;
// 第一组越界
if (end1 >= n)
{
printf("[%d,%d]", begin1, n - 1);
break;
}
// 第二组全部越界
if (begin2 >= n)
{
printf("[%d,%d]", begin1, end1);
break;
}
// 第二组部分越界
if (end2 >= n)
{
// 修正一下end2,继续归并
end2 = n - 1;
}
printf("[%d,%d][%d,%d] ", begin1, end1, begin2, end2);
int i = j;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
// 拷贝回原数组 -- 归并哪部分就拷贝哪部分回去
memcpy(a + j, tmp + j, (end2 - j + 1)*sizeof(int));
}
gap *= 2;
printf("\n");
}
free(tmp);
tmp = NULL;
}
时间复杂度:O(N*LogN) 空间复杂度:O(N)
计数排序
计数排序,又叫非比较排序。顾名思义,该算法不是通过比较数据的大小来进行排序的,而是通过统计数组中相同元素出现的次数,然后通过统计的结果将序列回收到原来的序列中。
举个例子:
上列中的映射方法称为绝对映射,即arr数组中的元素是几就在count数组中下标为几的位置++,但这样会造成空间浪费。例如,我们要将数组:1020,1021,1018,进行排序,难道我们要开辟1022个整型空间吗?
若是使用计数排序,我们应该使用相对映射,简单来说,数组中的最小值就相对于count数组中的0下标,数组中的最大值就相对于count数组中的最后一个下标。这样,对于数组:1020,1021,1018,我们就只需要开辟用于储存4个整型的空间大小了,此时count数组中下标为i的位置记录的实际上是1018+i这个数出现的次数。
绝对映射:count数组中下标为i的位置记录的是arr数组中数字i出现的次数。
相对映射:count数组中下标为i的位置记录的是arr数组中数字min+i出现的次数。
代码:
//计数排序
void CountSort(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;//min和max之间的自然数个数(包括min和max本身)
int* count = (int*)calloc(range, sizeof(int));//开辟可储存range个整型的内存空间,并将内存空间置0
if (count == NULL)
{
printf("malloc fail\n");
exit(-1);
}
//统计相同元素出现次数(相对映射)
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
int i = 0;
//根据统计结果将序列回收到原来的序列中
for (int j = 0; j < range; j++)
{
while (count[j]--)
{
a[i++] = j + min;
}
}
free(count);//释放空间
}
时间复杂度:O(N+range) 空间复杂度:O(range)