数据结构与算法-排序(八)计数排序(Counting Sort)

664 阅读2分钟

这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战

摘要

计数排序本质就是统计不同元素出现的次数,然后将元素依次从小到大放置,每个元素看统计的次数,就紧挨着放置几个同样的元素。

看似简单的处理,在算法中,会依据统计的元素次数推算出每个元素的索引位置,这就是算法的魅力。

逻辑

统计每个整数在序列中出现的次数,进而推导出每个整数在有序序列中的索引。

这是通过空间换取时间的方式。

流程

  1. 获取序列中的最大值
  2. 统计每个元素出现的次数
  3. 按照顺序进行赋值

实现

根据获取到的最大值,创建序列统计序列中的元素出现的次数。创建的序列大小是 max+1,让最大值也可以放在统计序列中。

	// 找出最大值
	int max = array[0];
	for (int i = 1; i < array.length; i++) {
		if (array[i] > max) {
			max = array[i];
		}
	}
	// 开辟内存空间,存储每个整数出现的次数
	int[] counts = new int[1 + max];

counts 数组的索引就是序列中的元素,存放的是序列中元素出现的次数。

	// 统计每个整数出现的次数
	for (int i = 0; i < array.length; i++) {
		counts[array[i]]++;
	}

现在就通过遍历 counts 数组排序序列中的元素,这里设置一个 index 变量,用于多次出现的元素,每放置一个元素,index 就减 1,直到 index 为 0 时,再遍历下一个索引位置。

		
		
	// 根据整数出现次数,对整数进行排序
	int index = 0;
	for (int i = 0; i < counts.length; i++) {
		while (counts[i]-- > 0) {
			array[index++] = i;
		}
	}

缺点

  • 无法对负整数进行排序,这里只能排序 0 到 max 元素的序列
  • 极其浪费内存空间
  • 是不稳定的排序

进阶

看上面实现计数排序的缺点,这里就去更改它的缺点。

可以对负整数排序,获取序列中的 min 和 max,创建计数数组的长度为 max-min。 在计数数组中,当前位置 count 加上上一个位置的 count。那么当前元素在序列中的索引就是计数数组中的 count - 1。

比较难理解的是计数数组的 count 和 array 元素的索引的对应关系。

这块可以理解,计数数组中存放着该元素的起始位置和有几个相同的元素。即类似于 range(index, count) 的情况

首先是找到序列中的 minmax

	// 找出最大值
	int max = array[0];
	int min = array[0];
	for (int i = 1; i < array.length; i++) {
		if (array[i] > max) {
			max = array[i];
		}
		if (array[i] < min) {
			min = array[i];
		}
	}

接下来就是计数排序的重点了。这里做了两次处理,第一次统计序列中元素出现的次数。第二次将统计数组中每个索引位置的数字加上前一个索引位置的数字。这就可以计算出每个元素在序列中的位置就是 array[i]-min

	// 开辟内存空间,存储次数
	int[] counts = new int[max-min+1];
	// 统计每个整数出现的次数
	for (int i = 0; i < array.length; i++) {
		counts[array[i]-min]++;
	}
	// 累加次数
	for (int i = 1; i < counts.length; i++) {
		counts[i] += counts[i-1];
	}

最后将序列从后往前遍历,通过计算出的索引,依次排序。从最后开始遍历就是增加序列的稳定性

	// 从后往前遍历数组,放在有序数组中的位置
	int[] newArray = new int[array.length];
	for (int i = array.length - 1; i >= 0; i--) {
		newArray[--counts[array[i]-min]] = array[i];
	}
	// 将有序数组覆盖到 array
	for (int i = 0; i < newArray.length; i++) {
		array[i] = newArray[i];
	}

技巧点/注意点

  • 计数数组中的索引是序列元素-min
  • 从后往前遍历数组,是保证数组的稳定性,如果从头开始,相同元素在原来数组中的相对位置会发生变化
  • 计数数组中的 count 在赋值一次后要进行 -- 操作。

计数排序的精妙点就在通过两次的统计,就可以计算出每个元素在序列中的位置

时间和空间复杂度

  • 最好、最坏、平均时间复杂度:O(n+k)
  • 空间复杂度:O(n+k)
  • 属于稳定排序

k 是整数的取值范围