10种排序:

  • 冒泡排序(Bubble Sort)
  • 选择排序(Selection Sort)
  • 插入排序(Insertion Sort)
  • 希尔排序(Shell Sort)
  • 快速排序(Quick Sort)
  • 归并排序(Merge Sort)
  • 计数排序(Counting Sort)
  • 基数排序(Radix Sort)
  • 堆排序(Heap Sort)
  • 桶排序(Bucket Sort)

冒泡排序(Bubble Sort)

// BubbleSort
// 冒泡排序
// 时间复杂度: 平均O(N^2) 最好O(N) 最坏O(N^2)
// 空间复杂度: O(1)
// 稳定性:稳定
// 基本步骤:顾名思义,每次排序都会遍历数组找一个最大值(最小值)上升(冒泡)到右边(左边),这个最大值的位置就确定了。
// 然后对剩余未确定的部分重复冒泡操作。
func BubbleSort(arr []int){
	n := len(arr)
	for i:=0;i<n;i++{
		for j:=0;j<n-i-1;j++{
			if arr[j]>arr[j+1]{
				arr[j],arr[j+1] = arr[j+1],arr[j]
			}
		}
	}
}

选择排序(Selection Sort)

// SelectionSort
// 选择排序
// https://zh.wikipedia.org/zh-cn/选择排序
// 时间复杂度: 平均O(N^2) 最好O(N^2) 最坏O(N^2)
// 空间复杂度: O(1)
// 稳定性:不稳定
// 基本步骤:
// 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,
// 然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
// 以此类推,直到所有元素均排序完毕。
// 交换次数比冒泡排序较少,由于交换所需CPU时间比比较所需的CPU时间多,
// n值较小时,选择排序比冒泡排序快。
// 原地操作几乎是选择排序的唯一优点,当空间复杂度要求较高时,可以考虑选择排序;实际适用的场合非常罕见。
func SelectionSort(arr []int)  {
	var i,j,min int
	for i=0;i<len(arr);i++{
		min = i
		//  找到最小值
		for j = i+1;j<len(arr);j++{
			if arr[min] > arr[j]{
				min = j
			}
		}
		if min != i{
			arr[min],arr[i] = arr[i],arr[min]
		}
	}
}

插入排序(Insertion Sort)

// InsertionSort
// 插入排序
// https://zh.wikipedia.org/zh-cn/插入排序
// 时间复杂度: 平均O(N^2) 最好O(N) 最坏O(N^2)
// 空间复杂度: O(1)
// 稳定性:稳定
// 对于少量元素的排序,它是一个有效的算法。
// 基本步骤:
// 插入排序:把元素分为已排序的和未排序的。每次从未排序的元素取出第一个保存到临时变量a,
// 然后在,已排序中的元素中从右往左依次把比a大的元素往后移动一个位置,最后把a填入移动后留下的空位。
func InsertionSort(arr []int){
	var i,j,key int

	// 首先,把数组第一个元素当作是一个已排序的数组,然后从剩余未未排序数组中的第一个元素插入到已排序数组
	for i=1;i<len(arr);i++{
		key = arr[i]
		j = i-1
		// 比key大的元素往后移动一位
		// 这里采用移动法,还可以使用类似冒泡排序中的交换法
		for j >= 0 && arr[j] > key{
			arr[j+1] = arr[j]
			j--
		}
		arr[j+1] = key
	}
}

希尔排序(Shell Sort)

// ShellSort
// 希尔排序
// 时间复杂度: 平均O(NlogN) 最好O(NlogN) 最坏O(N^2)
// 空间复杂度: O(1)
// 稳定性:不稳定
// 希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),
// 是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。
// 该方法因 D.L.Shell 于 1959 年提出而得名。
// 希尔排序中对于增量序列的选择十分重要,直接影响到希尔排序的性能。
// 选择的增量序列{n/2,(n/2)/2...1}(希尔增量),其最坏时间复杂度为O(n2),
// 可以对增量序列进行优化,例如Hibbard可使得最坏时间复杂度为O(n3/2)
func ShellSort(arr []int)  {
	n := len(arr)
	// 选择增量,最简单的是按照数组长度不断除2
	for gap := n/2;gap > 0;gap/=2{
		// 与增量成倍数关系的元素为一组,例如增量gap=5时,分为5组:
		// 下标(0,0+5,0+5+5)为一组,下标(1,1+5,1+5+5)为一组,以此类推。
		// 对每个组进行插入排序
		for i:=0;i<gap;i++{
			// 这里可以把gap=1来进行逻辑处理,实际上就是插入排序的逻辑
			for j:=i+gap;j<n;j+=gap{
				key := arr[j]
				k := j - gap
				for k >=0 && arr[k]>key{
					arr[k+gap] = arr[k]
					k -= gap
				}
				arr[k+gap] = key
			}
		}
	}
}

快速排序(Quick Sort)

// QuickSort
// 快速排序
// https://zh.wikipedia.org/wiki/快速排序
// 时间复杂度: 平均O(NlogN) 最好O(NlogN) 最坏O(N^2)退化为冒泡排序
// 空间复杂度: O(logN)
// 稳定性:不稳定
// 基本步骤:
// 挑选基准值:从数组中挑出一个元素,称为基准(pivot),(选择策略:第一个元素/最后一个元素/中间元素/随机选择)
// 分割:重新排序数组,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基基准面
//(与基准值相等的数可以到任何一变,注意这里破坏来稳定性)。在这个分割点束之后,对基准值的排序就已经完成,
// 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。
// 递归到最底部的判断条件是数组的大小是零或一,此时该数列显然已经有序。(这里可以优化,例如:子序列长度小于7时用选择排序)
// 选取基准值有数种具体方法,此选取方法对排序的时间性能有决定性影响。
func QuickSortRecursive(arr []int,left ,right int)  {
	if left < right{
		i := left
		j := right
		key := arr[i]
		for i < j{
			// 因为pivot选择在左边第一个,一定要从右边开始。
			// 如果pivot不是选择在左边第一个,可以通过交换令其成为第一个。
			// 然后pivot值在左边和右边反复横跳:
			// 首先pivot选择数组第一个值(左边),那我要从右边(顺序从右往左)找一个比我小的值,
			// 然后交换,此时pivot在右边了。然后它又要跳回左边,于是在左边(从左往右)找一个比pivot大的值,
			// 然后交换,于是pivot跳到左边了,然后又要跳到右边。。。直到两边都没坑位跳了。
			// 此时pivot值所在的数组下标就是i,pivot是已经排序的来,位置不会再改变
			// 因此递归的时候不再包含pivot,递归拆分为(left,i-1)和(i+1,right)两部分。
			for i < j && arr[j] >= key{
				j--
			}
			// 通过上面的循环后,为何还需要判断arr[j] < key ?
			// 因为可能 i==j,此时如果进行i++会导致错误。
			if arr[j] < key{
				arr[i],arr[j] = arr[j],arr[i]
				i++
			}

			for i < j && arr[i] <= key{
				i++
			}
			if arr[i] > key{
				arr[i],arr[j] = arr[j],arr[i]
				j--
			}
		}
		QuickSortRecursive(arr,left,i - 1)
		QuickSortRecursive(arr,i + 1,right)
	}
}

// 非递归实现的快速排序
// 首先要理解递归和非递归的本质区别,递归方法调用实际上就是一个压栈的过程,
// 一般来说,栈是有大小限制的(例如2K),递归深度过大会导致栈溢出。因此
// 递归转为非递归,就是要手动实现压栈过程。此时的“栈”是在堆上,因此不会导致栈溢出。
// 模拟压栈,就是把每次递归调用的参数压栈(left,right)
func QuickSort(arr []int, left, right int) {
	// 这里简单用slice实现栈
	// 最好情况下的栈大小log2(N),最坏情况要N
	size := 2
	l := len(arr)
	for l > 0 {
		size += 2 // 每次压栈有2个参数
		l = l >> 1
	}
	stack := make([]int, 0, size)

	// 入栈
	stack = append(stack, left)
	stack = append(stack, right)

	// 模拟递归调用
	for len(stack) > 0 {
		// 出栈
		i := stack[len(stack)-2]
		j := stack[len(stack)-1]
		stack = stack[0 : len(stack)-2]
		pivot := Partition(arr, i, j)
		if pivot-1 > i {
			// 入栈
			stack = append(stack, i)
			stack = append(stack, pivot-1)
		}
		if pivot+1 < j {
			stack = append(stack, pivot+1)
			stack = append(stack, j)
		}
	}
}
func Partition(arr []int, left int, right int) int {
	mid := left + (right-left)/2
	i := left
	j := right
	pivot := arr[mid]
	for i < j {
		for i < j && arr[i] < pivot {
			i++
		}
		for j > i && arr[j] > pivot {
			j--
		}
		arr[i], arr[j] = arr[j], arr[i]
	}
	return i
}

归并排序(Merge Sort)

// MergeSort
// 归并排序
// https://zh.wikipedia.org/wiki/归并排序
// 时间复杂度: 平均O(NlogN) 最好O(NlogN) 最坏O(NlogN)
// 空间复杂度: O(N)
// 稳定性:稳定
// 该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。
// 采用分治法:
// 分割:递归地把当前序列平均分割成两半。
// 整合:在保持元素顺序的同时将上一步得到的子序列整合到一起(归并)。
// 递归法(Top-down)
// 1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
// 2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
// 3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
// 4. 重复步骤3直到某一指针到达序列尾
// 5. 将另一序列剩下的所有元素直接复制到合并序列尾
// 迭代法(Bottom-up)
// 原理如下(假设序列共有n个元素):
// 1. 将序列每相邻两个数字进行归并操作,形成ceil(n/2)个序列,排序后每个序列包含两/一个元素
// 2. 若此时序列数不是1个则将上述序列再次归并,形成ceil(n/4)个序列,每个序列包含四/三个元素
// 3. 重复步骤2,直到所有元素排序完毕,即序列数为1
func MergeSort(arr []int){
	l := len(arr)
	temp := make([]int,l)
	// 分段为1,2,4,8...
	for seg:=1;seg < l;seg += seg{
		for start:=0;start < l;start += seg*2{
			mid := start + seg - 1
			end := start + seg * 2 - 1
			if mid >= l{
				mid = l-1
			}
			if end >= l{
				end = l-1
			}
			start1 := start
			end1 := mid
			start2 := mid + 1
			end2 := end
			k := start
			for start1 <= end1 && start2 <= end2{
				if arr[start1] < arr[start2]{
					temp[k] = arr[start1]
					start1++
				}else{
					temp[k] = arr[start2]
					start2++
				}
				k++
			}
			for start1 <= end1{
				temp[k] = arr[start1]
				start1++
				k++
			}
			for start2 <= end2{
				temp[k] = arr[start2]
				start2++
				k++
			}
			for i:=start;i <= end;i++{
				arr[i] = temp[i]
			}
		}
	}
}
// MergeSortRecursive
// MergeSort的递归版本
func MergeSortRecursive(arr []int){
	var temp = make([]int,len(arr))
	DoMergeSortRecursive(arr,temp,0,len(arr)-1)
}
func DoMergeSortRecursive(arr,temp []int,start,end int){
	if start >= end{
		return
	}
	mid := start + ((end - start) >> 1)
	start1 := start
	end1 := mid
	start2 := mid+1
	end2 := end
	DoMergeSortRecursive(arr,temp,start1,end1)
	DoMergeSortRecursive(arr,temp,start2,end2)

	// 合并
	k := start
	for start1 <= end1 && start2 <= end2{
		if arr[start1] < arr[start2]{
			temp[k] = arr[start1]
			start1++
		}else{
			temp[k] = arr[start2]
			start2++
		}
		k++
	}
	for start1 <= end1{
		temp[k] = arr[start1]
		start1++
		k++
	}
	for start2 <= end2{
		temp[k] = arr[start2]
		start2++
		k++
	}
	// 临时排序数据拷贝到原数组
	for i:=start;i <= end;i++{
		arr[i] = temp[i]
	}
}

计数排序(Counting Sort)

// CountingSort
// 计数排序
// https://zh.wikipedia.org/wiki/计数排序
// 时间复杂度: 平均O(N+k) 最好O(N+k) 最坏O(N+k)
// 空间复杂度: O(max) 比排序元素数量还要大
// 稳定性:稳定
// 由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),
// 这使得计数排序对于数据范围很大的数组,需要大量时间和内存。
// 计数排序一般用来配合基数排序。
// 算法步骤:
// 1. 找出待排序的数组中最大和最小的元素
// 2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
// 3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
// 4. 反向填充目标数组:将每个元素i放在新数组的第C[i]项,每放一个元素就将C[i]减一。(确保稳定性)
func CountingSort(arr []int)  {
	// 只能对n及以内的正整数进行排序
	n := len(arr)
	c := make([]int,n)
	temp := make([]int,n)

	// 将计数的数组初始化为0
	for i:=0;i<n;i++{
		c[i] = 0
	}

	// 直接将值当作数组下标进行计数
	for i:=0;i<n;i++{
		c[arr[i]]++
	}
	for i:=1;i<n;i++{
		// 计数求和,这样c[i]就是i在arr的所处下标
		c[i] = c[i] + c[i-1]
	}

	// 反向填充目标数组,为什么?
	// 因为计数数组c保存了对应值实际的下标,如果出现多个重复的值i,则c[i]>1,我们需要对其进行减一
	// 来确保重复值能够按照原来的顺序保存(排序稳定),因此下标是递减的,所以必须反向填充目标数组。
	for j:=n-1;j>=0;j--{
		temp[c[arr[j]] - 1] = arr[j]
		c[arr[j]]--
	}
	for i:=0;i<n;i++{
		arr[i] = temp[i]
	}
}

基数排序(Radix Sort)

// RadixSort
// 基数排序
// https://zh.wikipedia.org/wiki/基数排序
// 时间复杂度: 平均O(N+k) 最好O(N+k) 最坏O(N+k)
// 空间复杂度: O(max)
// 稳定性:稳定
// 基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。
// 由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
// 基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),
// LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
// 基数排序一般要快过基于比较的排序,比如快速排序。
// 算法步骤:
// 1 找出最大的元素(或者已知),计算该数有多少个数字组成(十进制),例如789有3个数字。
func RadixSort(arr []int)  {
	max := 0
	for _,v := range arr{
		if max < v{
			max = v
		}
	}
	// 对每个位进行排序
	for place := 1;max/place > 0;place *=10{
		countingSortOnDigit(arr,place)
	}
}

// 对基数进行计数排序,例如对个位/十位/百位/千位进行排序
// place = 1,10,100,1000,...
func countingSortOnDigit(arr []int,place int)  {
	if len(arr) == 0{
		return
	}
	// 计数排序,首先找出该位上的最大数字(0-9),例如1738的个位为8,十位为3
	max := (arr[0]/place)%10
	for i:=1;i<len(arr);i++{
		if max < ((arr[i]/place)%10){
			max = (arr[i]/place)%10
		}
	}

	// max可以确定用于计数的数组大小
	// 如果最大值是8,因为下标是从0开始,因此需要9个计数,因此max+1
	count := make([]int,max+1)

	// 进行计数
	index := 0
	for i:=0;i<len(arr);i++{
		index = (arr[i]/place)%10
		count[index]++
	}

	// 将count数组求和,注意这里遍历最大值是max
	for i:=1;i<=max;i++{
		count[i] = count[i] + count[i-1]
	}

	var temp = make([]int,len(arr)+1)
	// 反向填充数组
	for i:=len(arr)-1;i>=0;i--{
		temp[count[(arr[i]/place)%10]-1] = arr[i]
		count[(arr[i]/place)%10]--
	}
	for i:=0;i<len(arr);i++{
		arr[i] = temp[i]
	}
}

堆排序(Heap Sort)

// HeapSort
// 堆排序
// 注意:性能看起来很好,但要考虑硬件缓存。
// 时间复杂度: 平均O(NlogN) 最好O(NlogN) 最坏O(NlogN)
// 空间复杂度: O(1)
// 稳定性:不稳定
// 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,
// 堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。
// 将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,
// 这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
// 直观的看起来就是:从最大堆中一个一个元素出列,出列元素组成一个有序列表。
// 堆排序缺点:快速排序是顺着扫的,堆排序是跳着走的,根据局部性原理,堆排序CPU缓存命中率比快速排序低。
// 快速排序通常比基数排序更有效地使用硬件的缓存。
func HeapSort(arr []int)  {
	n := len(arr)

	// 首先构建一个最大堆,为了构建最大堆我们需要:
	// 从最底层的子树往上一层一层heapify子树直至根节点
	// 最后一个非叶子节点是n/2 - 1,因此所有比n/2 - 1大的节点都是叶子节点,
	// 叶子节点不需要heapify(因为他们没有子节点)
	// 这使得根节点到任意叶子节点的路径上的元素使有序的。
	for i:= n/2-1;i >=0;i--{
		heapify(arr,i,n)
	}

	for i:=n-1;i>=0;i--{
		arr[0],arr[i] = arr[i],arr[0]
		// 这里是如何确保heapify后root节点是最大值的?
		heapify(arr,0,i)
	}
}

// heapify
// 调整堆,使根节点要么比所有子节点大,要么成为叶子节点。
// root 堆顶下标
// length 数组arr长度
func heapify(arr []int,root,length int)  {
	max := root
	leftChild := root*2+1
	rightChild := root*2+2
	if leftChild < length && arr[max] < arr[leftChild]{
		max = leftChild
	}
	if rightChild < length && arr[max] < arr[rightChild]{
		max = rightChild
	}
	if max != root{
		arr[max],arr[root] = arr[root],arr[max]
		heapify(arr,max,length)
	}
}

桶排序(Bucket Sort)

// BucketSort
// 桶排序
// 时间复杂度: 平均O(N+k) 最好O(N) 最坏O(N^2)
// 空间复杂度: O(N+k)
// 稳定性:稳定 (一般桶中元素以数组或链表保存,针对桶中元素要使用稳定排序算法才能保证稳定性)
// 适合大量数据的排序,例如对10亿个32位随机整数进行排序
// 每个通可以针对性选择的桶排序或其它算法进行排序,例如快速排序
func BucketSort(arr []int)  {
	// 创建桶,依需要排序的数量而定
	// 针对桶还可以进行优化,例如使用bit
	n := len(arr)
	bucketSize := 1000
	var buckets = make([][]int,100)
	for k ,_ := range buckets{
		buckets[k] = []int{}
	}

	// 将每个元素分配到对应的桶中,对于不同的桶来说,是有序的,但是桶内的元素是无序的。
	for i:=0;i<n;i++{
		index := arr[i]/bucketSize
		buckets[index] = append(buckets[index],arr[i])
	}


	// 对每个桶内的元素使用快速排序(当然也可以根据需要选用不同的排序算法)
	j := 0
	for _,v := range buckets{
		QuickSort(v,0,len(v)-1)
		// 整合
		for _,vv := range v{
			arr[j] = vv
			j++
		}
	}
}