前置知识:无

基于比较的排序 VS. 不基于比较的排序

  • 基于比较的排序:只需要定义好两个对象之间怎么比较即可,很通用
  • 不基于比较的排序:不进行比较,对于对象的数据特征有要求,并不通用

计数排序

不基于比较的排序,对于对象的数据特征有要求:数值范围不能太大

如:一个数组中的每个数在 [0, 60) 范围内,对这个数组进行排序

遍历一遍数组,记录每个数值出现的次数,再直接填充即可

package main
 
import (
  "fmt"
  "math/rand"
  "sort"
)
 
const (
  MIN        = 0
  MAX        = 60
  N          = 10000
  TEST_TIMES = 10000
)
 
func main() {
  for i := 0; i < TEST_TIMES; i++ {
    arr := generateArr()
    copiedArr := copyArr(arr)
    sort.Ints(arr)
    countSort(copiedArr)
    n := len(arr)
    for j := 0; j < n; j++ {
      if arr[j] != copiedArr[j] {
        panic("error")
      }
    }
  }
  fmt.Println("success")
}
 
func countSort(arr []int) {
  counts := [MAX - MIN]int{}
  for _, num := range arr {
    counts[num]++
  }
  idx := 0
  for num, count := range counts {
    for ; count > 0; count-- {
      arr[idx] = num
      idx++
    }
  }
}
 
func generateArr() []int {
  n := rand.Intn(N)
  arr := make([]int, n)
  for i := range arr {
    arr[i] = rand.Intn(MAX-MIN) + MIN
  }
  return arr
}
 
func copyArr(arr []int) []int {
  n := len(arr)
  dist := make([]int, n)
  copy(dist, arr)
  return dist
}
  • 时间复杂度:
  • 空间复杂度: 为数值范围

计数排序非常简单、非常高效,但是数值范围比较大就不行了

基数排序

不基于比较的排序,对于对象的数据特征有要求:样本是 m 进制的非负整数(这里以 10 进制为例)

元素依次以个位、十位、百位……入桶,十位入桶时,个位已有序,所以十位相同的元素会按从小到大的顺序依次入相同的十位桶

有负数怎么办?所有元素都加同一个数,加成非负数,如果可能溢出,就将 int 改成 uint

两个技巧之一:前缀数量分区

如:有数组 [0, 1, 3, 3, 2, 0, 0, 1]

统计每个数出现的次数:

0: 3 次
1: 2 次
2: 1 次
3: 2 次

计算前缀和,含义变成 <= x 的数有几个:

<= 0: 3 个
<= 1: 5 个 (3 + 2)
<= 2: 6 个 (5 + 1)
<= 3: 8 个 (6 + 2)

从后往前遍历原数组:

[0, 1, 3, 3, 2, 0, 0, 1]
                      |
 
遍历到最后一个1,<= 1 的数有 5 个,所以这个 1 放到第 5 个(索引为 4)
因为这个 1 已经放置好了,所以 <= 1 的数要减一
以此类推,继续往前遍历数组

为什么要从后往前遍历?这样能保证 稳定性(最后一个 1 仍然再所有 1 的最后一位)

这个技巧可以很优雅快速地使得某一位(如:个位)置入桶中正确的位置

两个技巧之二:数字提取某一位

有一个数字,如何提取它的个位、十位、百位……?

offset = 1,个位:num / offset % 10,之后 offset *= 10 继续提取接下来的位数

测试链接

代码

package main
 
import (
	"bufio"
	"os"
	"strconv"
)
 
const (
	MAXN = 501 // 数组最大长度
	BASE = 10  // 进制
)
 
var (
	n      int
	minn   int // 数组中的最小值,看看有没有负数,有负数的话需要所有元素都加成非负数
	maxx   int // 非负数数组中的最大值,用于计算最高是几位
	arr    = [MAXN]int{}
	bucket = [MAXN]int{}
	counts = [BASE]int{}
)
 
func main() {
	in := bufio.NewScanner(os.Stdin)
	in.Split(bufio.ScanWords)
	out := bufio.NewWriterSize(os.Stdout, 4096)
	for in.Scan() {
		n, _ = strconv.Atoi(in.Text())
		if n == 0 {
			continue
		}
		minn = 0
		maxx = 0
		for i := 0; i < n; i++ {
			in.Scan()
			arr[i], _ = strconv.Atoi(in.Text())
			if arr[i] < minn {
				minn = arr[i]
			}
			if arr[i] > maxx {
				maxx = arr[i]
			}
		}
		if minn < 0 { // 有负数
			for i := 0; i < n; i++ {
				arr[i] -= minn
			}
			maxx -= minn
		}
		radixSort(getBitCount(maxx))
		out.WriteString(strconv.Itoa(arr[0] - minn))
		for i := 1; i < n; i++ {
			out.WriteByte(' ')
			out.WriteString(strconv.Itoa(arr[i] - minn))
		}
		out.WriteByte('\n')
	}
	out.Flush()
}
 
func radixSort(bitCount int) {
	offset := 1
	for ; bitCount > 0; bitCount-- {
		for i := range counts {
			counts[i] = 0
		}
		for i := 0; i < n; i++ {
			counts[arr[i]/offset%BASE]++
		}
		for i := 1; i < BASE; i++ {
			counts[i] += counts[i-1]
		}
		for i := n - 1; i >= 0; i-- {
			counts[arr[i]/offset%BASE]--
			bucket[counts[arr[i]/offset%BASE]] = arr[i]
		}
		copy(arr[:n], bucket[:n])
		offset *= BASE
	}
}
 
// 返回 num 在 BASE 进制下有几位
func getBitCount(num int) int {
	ans := 0
	for num > 0 {
		ans++
		num /= BASE
	}
	return ans
}
  • 时间复杂度:
  • 额外空间复杂度:
    • 为进制数
    • 需要辅助空间做类似桶的作用,来不停的装入、弹出数字

不基于比较的排序总结

一般来讲,计数排序要求,样本是整数,且范围比较窄

一般来讲,基数排序要求,样本是 m 进制的非负整数

如果不符合要求就需要转化,代码里可以设置任何进制来进行基数排序

一旦比较的对象不再是常规数字,那么改写代价的增加是显而易见的,所以不基于比较的排序并不通用