如何理解计数排序和基数排序?

先说二者的联系,计数排序是基数排序的依托,基数排序是计数排序的改良!

0x01.何谓计数排序?
从实现功能上看,它是将一串数据按一定顺序排出,每个数字出现多少次,就排多少遍,如下图:
image

绘图工具:draw.io

什么时候用?
当数据重复出现次数较多,且数据极差较小时采用。

0x02.如何实现计数排序?
1.实现查找最大值的函数,这里就是编程中常用的先定后变法(我起的名字):
就是说要找最值(最大值,最小值),先让第一个数组当最大值/最小值,后序遍历如果有比他更大/小的,就令新的元素来充当这个最值。

点击查看代码
int toFindMax(int arr[],int n){
	int max = arr[0];
	for(int i = 0;i < n;++i){
		if(arr[i] > max){
			max = arr[i];
		}
	}
	return max;
}

2.核心功能的实现:先统计每个数字出现的次数,再直接回填。
可能有读者不认识memset,简单来说就是给某一连续的内存地址统一赋值的,数组内存是连续的所以可以用。语法是:
memset(待赋值连续内存的首地址,赋值数据,缓冲区大小(该连续内存有多长))。
但是注意它是stdlib.h头文件里的函数。

点击查看代码
void countingSort(int arr[],int n){
	//取最大值
	int max = toFindMax(arr,n);
	
	//计数
	int count[40];
	memset(count,0,sizeof(int)* n);
	for(int i = 0;i < n;++i){
		count[arr[i]]++;
	}
	
	//把数据回填到arr里
	int index = 0;		//记录arr回填的最新位置
	for(int num = 0;num <= max;++num){   //从小到大找arr中存放的值
		while(count[num] > 0){			//每个值都回填应有的次数
			arr[index++] = num;
			count[num]--;
		}
	}
}

0x03.何谓基数排序
对于数据极差很大的一套数据,计数排序会很浪费内存,因为他会把从最小值到最大值都记录一遍,太浪费空间!
如果我们把数字按一位一位的排呢?每次只需要排0-9 9个数字,采用递归形式把arr中存放的每个数字都用计数排序来遍历,
不就很好地规避直接计数排序带来的空间开销大的风险吗?这就是基数排序的思想。

0x04.怎么实现基数排序?
如图:
image
看起来很麻烦,但其实很简单:首先每一位的处理操作都是相同的,所以我们采用递归的思想来看待此实现方法,我们可以写一个递归函数,
传入arr[],缓冲区大小n,再传入一个参数exp表示位数,实现在该位下的计数排序。

问:如何用一个参数表示取哪一位?
答:以123举例,如果我们要取个位3,123/100=123,123%10=3。取十位:123/101=12,12%10=2.取百位:123/10^3=1,1/10=1。
它的原理是我们要取哪一位,就让它的后面所有位数全部消失,它成为了新数字中的个位,然后直接/10求新数的个位即可。
这里唯一变换的是10的次方数,我们直接让它等于exp即可。

点击查看代码
void countSortByDigit(int arr[],int n,int exp){
	int temp[40];//临时储存
	int count[10] = {0};//存储0-9
	
	//统计0-9出现次数
	for(int i = 0;i < n;++i){
		count[(arr[i]/exp) % 10]++; //exp:10的次方数,这里(arr[i]/exp) % 10就是某一位上存储的数字
	}
	
	//使用前缀和思想保证排序稳定
	for(int i = 1;i < 10;++i){
		count[i] += count[i-1];  //注:从此count表示的不是0-9的一个数字出现了多少次,而是应存在的位置
	}
	
	//采用倒序插入法将arr的元素填入临时数组temp中
	//解释:由于为了排序稳定性,我们要确保指定位数上数字相同的元素相对位置不变,
	//count由大到小说明temp是倒着插入的,所以arr[i]要从高地址开始存,保证同时倒插。
	for(int i = n-1;i >= 0;--i){
		int digit = (arr[i] / exp) % 10; //取arr中第i个元素指定位的数字
		temp[count[digit] - 1] = arr[i];//存入临时数组中
		--count[digit];
	}
	//回填(在原数组上排序)
	for(int i = 0;i < n;++i){
		arr[i] = temp[i];
	}
}

上述代码用到了前缀和思想。那么,什么是前缀和呢?

前缀和就是:从第一个数开始,一路加到当前位置,得到的 “累计值”。

在该思想的指导下,count实现了从服务于一个数字的计数到服务于确定该数字最终位置的转变。
以只有个位的一组数字为例,看看count是怎样实现功能的:

image
有人说,不对呀,为什么不存在的元素也有count值?
int digit = (arr[i] / exp) % 10; //取arr中第i个元素指定位的数字
temp[count[digit] - 1] = arr[i];//存入临时数组中
不妨多品味一下这两行代码,digit从arr中取,根据digit对应的count值确定该数字应存的位置然后插进去。不存在的数在第一步就pass掉了。
这里第二个2用红底标注用于判断排序的稳定性,显然稳定(两个2相对位置不变)

最后总函数使用遍历将每一位都这样处理,就大功告成!

点击查看代码
//需要调用上面的toFindMax()
void radixSort(int arr[],int n){
	int max = toFindMax(arr,n);
	for(int exp = 1;max /exp > 0;exp *= 10){	//从个位到最大位,注意exp从1开始,说明我们先排个位
		countSortByDigit(arr,n,exp);
	}
}
为什么先排各位?从最高位开始不行嘛? 比如31,13,先排最高位则是13,31,再排最低位则是31,13 。❌️ 再比如31,21,13,先排最低位31,21,23,再排最高位21,23,31 ✅️ 显然位越高影响越大,先排最高位容易导致效果被最低位覆盖掉。 所以先排最低位

0x05总结复盘

计数排序从整体出发,直接计数并排序,当数据极值很大时浪费空间;
基数排序则从每一位出发,在每一位进行计数排序,最终完成排序。

学习到的关键思想:前缀和思想

posted on 2026-03-20 16:04  richu  阅读(90)  评论(0)    收藏  举报