Go语言学习笔记【16】 排序算法之计数排序、算法总结

【声明】

非完全原创,部分内容来自于学习其他人的理论。如果有侵权,请联系我,可以立即删除掉。

一、计数排序

1、方法和复杂度

1.1、核心思想和方法

计数排序是一个非基于比较的排序算法,对输入的数据有附加的限制条件:
1、输入的线性表的元素属于有限偏序集S;
2、设输入的线性表的长度为n,|S|=k(表示集合S中元素的总数目为k),则k=O(n)。
在这两个条件下,计数排序的复杂性为O(n)。

计数排序的基本思想是对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数(此处并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定)。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。

1.2、稳定性

排序算法是稳定的,因为在确定最终有序序列而遍历原序列时,遍历的方式是自右往左,从而导致后面的元素一定是排在后面的(代码中会给出验证)

1.3、时间复杂度

时间复杂度为 O ( n + k ) O(n+k) O(n+k),其中n为要排序的元素个数,k为data中最大的整数加1

1.4、空间复杂度

空间复杂度为 O ( n + k ) O(n+k) O(n+k),其中n为要排序的元素个数,k为data中最大的整数加1

2、排序的过程

2.1、基于队列数组的方式

(1)初始化一个计数数组(若输入序列中的元素是整数,则数组的每一个元素存放的是序列中元素的计数值;若输入序列中的元素是其他类型,则数组的每一个元素存放的是一个队列),计数数组大小是输入序列的所有元素待排序字段中最大的数
(2)遍历输入序列,遇到一个元素,若输入序列是整数,则根据数值计数数组对应的位置上加一;若输入序列是其他类型,就根据元素待排序字段的值在计数数组中找到其索引,将元素放入到队列中
(3)依次从计数数组中取出元素,填充到数组中

如下图中,结构体元素的第一个字段为待排序字段(字段最大值是3),因此计数数组的长度为4(索引分别为0、1、2、3,由于2没有用到因此未画出来),每一个元素均为队列
Go语言学习笔记【16】 排序算法之计数排序、算法总结_第1张图片

2.2、基于偏移地址的方式

见:排序算法的C语言实现(下 线性时间排序:计数排序与基数排序)

3、算法实现

package main

import "fmt"

const size = 10

type Data struct {
	No   int
	Name string
}

func Count(arr []*Data) {
	var index int = 0
	//记录输入序列中待排序字段的最大值
	var tmp_len int = 0
	for _, data := range arr {
		if tmp_len < data.No {
			tmp_len = data.No
		}
	}
	tmp_len++
	//创建数组,go不支持动态创建数组,因此用切片
	//队列用[]Data切片来表示,因为可以根据索引做到先进先出
	tmp_arr := make([][]*Data, tmp_len)
	for _, data := range arr {
		tmp_arr[data.No] = append(tmp_arr[data.No], data)
	}
	//按照数组的顺序取元素
	for i := 0; i < tmp_len; i++ {
		if tmp_arr[i] != nil {
			for j := 0; j < len(tmp_arr[i]); j++ {
				arr[index] = tmp_arr[i][j]
				index++
			}
		}
	}
	fmt.Println("【Count】排序, 最终结果: ")
	for i, data := range arr {
		fmt.Printf("i = %d, (%p)%v\n", i, data, *data)
	}
}

func main() {
	arr := [size]*Data{
		{3, "zw"},
		{1, "zs"},
		{0, "ls"},
		{3, "zy"},
		{1, "zl"},
		{4, "hu"},
		{6, "jk"},
		{4, "kl"},
		{5, "gh"},
		{4, "dj"},
	}
	fmt.Println("原始序列: ")
	for i, data := range arr {
		fmt.Printf("i = %d, (%p)%v\n", i, data, *data)
	}
	Count(arr[0:])
}

4、运行结果

原始序列: 
i = 0, (0xc000004078){3 zw}
i = 1, (0xc000004090){1 zs}
i = 2, (0xc0000040a8){0 ls}
i = 3, (0xc0000040c0){3 zy}
i = 4, (0xc0000040d8){1 zl}
i = 5, (0xc0000040f0){4 hu}
i = 6, (0xc000004108){6 jk}
i = 7, (0xc000004120){4 kl}
i = 8, (0xc000004138){5 gh}
i = 9, (0xc000004150){4 dj}
【Count】排序, 最终结果: 
i = 0, (0xc0000040a8){0 ls}
i = 1, (0xc000004090){1 zs}
i = 2, (0xc0000040d8){1 zl}
i = 3, (0xc000004078){3 zw}
i = 4, (0xc0000040c0){3 zy}
i = 5, (0xc0000040f0){4 hu}
i = 6, (0xc000004120){4 kl}
i = 7, (0xc000004150){4 dj}
i = 8, (0xc000004138){5 gh}
i = 9, (0xc000004108){6 jk}

二、算法总结

算法名 时间复杂度 空间复杂度 稳定性 原地排序 算法思路 注意点
冒泡排序 O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1) 每一趟循环都通过相邻元素的比较将较大元素往后调 可设置标志位减少趟数;
可设置索引记录最后比较元素的位置以减少比较次数
选择排序 O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1) 可以稳定 每一个位置上选择当前最小的元素 可一趟同时选择最小和最大的元素;
可设置交换规则让算法稳定
插入排序 O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1) 假设第一个元素组成的区间有序,后面的元素依次插入到有序区间 二分查找插入可以减少有序区间查找的效率
希尔排序 O ( n 1.3 − 2 ) O(n^{1.3 -2}) O(n1.32) O ( 1 ) O(1) O(1) 按照增量将序列分为多个小组,组内插入排序;
减少增量,重复该过程,直到增量为0;
一般增量初始值设为长度的一半,逐渐除以2
小组内元素间隔增量个元素,插入排序只能保证小组内是稳定的,但从整个序列来看不稳定
快速排序 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( l o g 2 n ) O(log_2n) O(log2n) 选取中枢元素,以此为基准,小于它的元素放在左边区间,大于它的元素放在右边区间;
继续选择左、右区间的中枢元素,重复该过程,直到整个数组有序;
中枢可选待排序区间第一个元素,也可以选中位数;
挖坑法:选取中枢是挖坑;初始时坑在左边,右指针找到小于中枢的元素填坑,此时右边空出坑;左指针找到大于中枢的元素填坑;重复上面过程,指针重合时用中枢填坑;
前后指针法:区间第一个元素为中枢;右指针找到小于中枢的元素,左指针找到大于中枢的元素,左右指针的元素交换;重复上述过程,指针重合时和中枢交换值;
就地快速排序需要递归,因此每次递归需要保存常数个数据,空间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n)
中枢为区间第一个元素时左指针先走可能会导致排序失败
归并排序 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n ) O(n) O(n) 将区间不断地一分为二,然后将有序的两个区间合并为一个,直到递归到整个数组有序 合并两个有序的区间时,需要额外申请空间,空间复杂度 O ( n ) O(n) O(n),因此不属于原地排序
堆排序 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( 1 ) O(1) O(1) 先将数组初始化为大顶堆,然后堆顶元素交换堆中最后一个元素;
除去最后一个元素的数组继续构建大顶堆,堆顶元素继续交换,直到堆中只剩下一个元素
大顶堆得到从小到大的数组;
建堆时需要遍历len/2-1 ~ 0的父结点,每一个父结点需要从上往下调整大顶堆;
筛选法是先填满完全二叉树再调整为堆,插入法是假设二叉树只有根结点然后将数组其他元素按照堆的规则逐个插入
桶排序 取决于算法 O ( m + n ) O(m+n) O(m+n) 取决于算法 将序列按照某种规则分到有限的桶中,桶内排序后依次输出 桶内元素均匀分布时,时间 O ( n l o g 2 n m ) O(nlog_2 \frac{n}{m}) O(nlog2mn),当桶元素个数接近于数组元素个数,时间接近于 O ( n ) O(n) O(n)
算法依赖于输入数据的规律,数据需要扩容且不符合原规律时算法要重写;
时间复杂度低时,耗费的空间巨大;
输入数据不均匀时需要增加桶个数,效率会降低;
非比较排序,有些情况不适合,如小数
基数排序 O ( n l o g r n ) O(nlog_rn) O(nlogrn) O ( n ) O(n) O(n) 找到输入序列中最大元素,计算其在基数r下的最高位(即外层循环次数);
根据元素个位上的值key将元素按照队列的方式分到桶key中;
按照key的大小遍历桶,元素按照队列的方式输出到数组中;
根据元素十位数的值分配桶,重复上面操作
基数排序中每个桶都是一个队列,因此算法是稳定的
计数排序 O ( n + k ) O(n+k) O(n+k) O ( n + k ) O(n+k) O(n+k) 初始化一个计数数组,不同索引下分别统计输入序列中的元素;根据数组的索引从小到大依次取出元素 若输入序列是整数,则计数数组统计的是元素的个数;
若输入序列是其他类型,计数数组的每一个元素存放的是一个队列,用于放置输入序列中的元素,遵从先进先出的原则

你可能感兴趣的:(go语言学习之路,排序算法,排序算法,算法,学习,golang,后端)