Go语言数据结构与算法—常用排序算法

Posted 小圣.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go语言数据结构与算法—常用排序算法相关的知识,希望对你有一定的参考价值。

概述

所谓的排序算法,就是通过特定的算法因式将一组或多组数据按照既定模式进行重新排序。这种新序列遵循着一定的规则,体现出一定的规律。因此,经过处理后的数据便于筛选和计算,大大提升了计算效率。

分类

插入排序

  • 思路:每次将一个元素,按其关键字值的大小,插入前面已经排序的子序列,以此重复,直达插入全部元素。

  • 包含的算法:直接插入排序、二分插入排序、希尔排序

交换排序

  • 思路:比较两个元素大小,如果反序,则交换

  • 包含的算法:冒泡排序、快速排序

选择排序

  • 思路:每趟选择序列的最大/最小值,采取贪心选择策略
  • 包含的算法:直接选择排序、堆排序

归并排序

  • 思路:归并排序算法将相邻的两个排序子序列合并成一个排序子序列,分治策略

算法特性

  • 直接插入排序:时间复杂度为O(n)~O(n^2),数据序列的初始排列越接近有序,时间效率越高。空间复杂度为O(1),是一个稳定排序算法。
  • 希尔排序:时间复杂度为O(n^1.3)~O(n^2),实际所需的时间取决于具体的增量序列,空间复杂度为O(1),是一个不稳定的算法。
  • 冒泡排序:时间复杂度为O(n)~O(n^2),数据序列越接近有序,时间效率越高。空间复杂度为O(1),是一个稳定算法。
  • 快速排序:时间复杂度为O(nlogn)~O(n^2),当n较大且数据序列随机排列时,时间效率越高。空间复杂度为O(logn)~O(n),是一个不稳定算法。
  • 直接选择排序:时间复杂度为O(n^2)。空间复杂度为O(1),是一个不稳定算法。
  • 堆排序:时间复杂度为O(nlogn),是改进的直接选择排序算法。空间复杂度为O(1),是一个不稳定的算法。
  • 归并排序:时间复杂度为O(nlogn),空间复杂度为O(n),是一个稳定的算法。

冒泡排序

冒泡排序(Bubble Sort),从第一个元素开始,依次比较两个相邻的元素。如果顺序错误,就把它们进行交换。直到比较到了指定位置才停止比较。然后不断循环这个过程,就可以完成排序了。因为使用这个算法进行排序,最大或者最小的元素会经由交换,慢慢的替换到数列的顶端或者尾端。排序过程十分像气泡。所以被称为冒泡排序

排序流程分析

func Test01(arr []int) {
	// arr := []int{3, 9, -1, 10, -2}
	
	temp := 0
	// 冒牌排序要排序的次数: 假设有N个元素,那么需要进行N-1次排序
	//----------------------第一次比较---------------
	for i := 0; i < len(arr)-1; i++ {
		if arr[i] > arr[i+1] {
			temp = arr[i]
			arr[i] = arr[i+1]
			arr[i+1] = temp
		}
	}
	fmt.Println(arr) // [3 -1 9 -2 10]

	/*
	   分析:
	   	原数组:[3, 9, -1, 10, -2]
	   	第一次比较后的数组:[3 -1 9 -2 10]
	   	比较第一组相邻的元素[3, 9],查看是否交换,3小于9,不需要交换,此时数组为[3, 9, -1, 10, -2]
	   	比较第二组相邻的元素[9, -1],查看是否需要交换,9大于-1,交换,此时数组变为[3, -1, 9, 10, -2]
	  	比较第三组相邻的元素[9, 10],查看是否交换,9小于10,不需要交换,此时数组为[3, -1, 9, 10, -2]
	   	比较第三组相邻的元素[10, -2],查看是否需要交换,-2小于10,需要交换,此时数组变为[3, -1, 9, -2, 10]
	*/

	//----------------------第二次比较---------------
	// 循环条件-2:因为最后一个元素已经确定为最大的了,不需要再进行比较,第一次比较减1,所以这一次减2
	for i := 0; i < len(arr)-2; i++ {
		if arr[i] > arr[i+1] {
			temp = arr[i]
			arr[i] = arr[i+1]
			arr[i+1] = temp
		}
	}
	fmt.Println(arr) // [-1 3 -2 9 10]
	/*
		分析:
			第一次比较后的数组:[3 -1 9 -2 10]
			第二次比较后的数组:[-1 3 -2 9 10]
			比较第一组相邻的元素[-1, 3],查看是否交换,3大于-1,需要交换,此时数组为[-1 3 9 -2 10]
			比较第二组相邻的元素[3, 9],查看是否需要交换,3小于9,不需要交换,此时数组变为[-1 3 9 -2 10]
			比较第三组相邻的元素[9, -2],查看是否交换,-2小于9,需要交换,此时数组变为[-1 3 -2 9 10]
			因为最后一个元素已经为最大了,不需要再比较
	*/

	//----------------------第三次比较---------------
	for i := 0; i < len(arr)-3; i++ {
		if arr[i] > arr[i+1] {
			temp = arr[i]
			arr[i] = arr[i+1]
			arr[i+1] = temp
		}
	}
	fmt.Println(arr) // [-1 -2 3 9 10]

	/*
		分析:
			第二次比较后的数组:[-1 3 -2 9 10]
			第三次比较后的数组:[-1 -2 3 9 10]
			比较第一组相邻的元素[-1, 3],查看是否交换,-1大于3,不需要交换,此时数组为[-1 3 9 -2 10]
			比较第二组相邻的元素[3, -2],查看是否需要交换,3大于-2,需要交换,此时数组变为[-1 -2 3 9 10]
			因为最后两个个元素已经为最大了,不需要再比较
	*/

	// 此时数组已经有序,不需要再进行比较了

}

循环排序

func Test02(arr []int) {
	temp := 0

	// 外层循环控制进行n-1趟扫描
	for i := 0; i < len(arr)-1; i++ {
		// 内层循环进行比较和交换
		for j := 0; j < len(arr)-1-i; j++ {
			if arr[j] > arr[j+1] {
				temp = arr[j]
				arr[j] = arr[j+1]
				arr[j+1] = temp
			}
		}
	}

	fmt.Println(arr)
}

排序优化

有的时候,不一定非要进行N-1次排序,只要数组的数组已经有序了,那么我们可以直接退出循环。那么怎么知道数组中的数据已经有序了呢?我们可以设置一个标志来控制循环。

func Test03(arr []int) {
	temp := 0

	// 控制是否继续循环
	flag := false
	// 外层循环控制进行n-1趟扫描
	for i := 0; i < len(arr)-1; i++ {
		// 内层循环进行比较和交换
		for j := 0; j < len(arr)-1-i; j++ {
			if arr[j] > arr[j+1] {
				temp = arr[j]
				arr[j] = arr[j+1]
				arr[j+1] = temp

				flag = true
			}
		}
		// 没有进行交换,说明数组已有序
		if !flag {
			break
		}
	}

	fmt.Println(arr)
}

选择排序

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。

排序流程分析

func SelectSort1(arr *[5]int) {

	// 选出第一大的元素:假设第0个位置的元素是最大的,然后和后面的元素进行比较,
	//  如果后面的元素比第0个位置的元素大,就替换
	max := arr[0]
	maxIndex := 0

	// 比较
	for i := 1; i < len(arr); i++ {
		if max < arr[i] {
			max = arr[i]
			maxIndex = i
		}
	}

	// 替换
	if maxIndex != 0 {
		arr[0], arr[maxIndex] = arr[maxIndex], arr[0]
	}

	// 选出第二大元素
	max = arr[1]
	maxIndex = 1
	for i := 2; i < len(arr); i++ {
		if max < arr[i] {
			max = arr[i]
			maxIndex = i
		}
	}

	if maxIndex != 1 {
		arr[1], arr[maxIndex] = arr[maxIndex], arr[1]
	}

	// 选出第三大元素
	max = arr[2]
	maxIndex = 2
	for i := 3; i < len(arr); i++ {
		if max < arr[i] {
			max = arr[i]
			maxIndex = i
		}
	}

	if maxIndex != 2 {
		arr[2], arr[maxIndex] = arr[maxIndex], arr[2]
	}

	// 选出第四大元素
	max = arr[3]
	maxIndex = 3

	for i := 4; i < len(arr); i++ {
		if max < arr[i] {
			max = arr[i]
			maxIndex = i
		}
	}

	if maxIndex != 3 {
		arr[3], arr[maxIndex] = arr[maxIndex], arr[3]
	}

	// 元素排序完成

	fmt.Println(arr)
}

循环排序

func SelectSort2(arr *[5]int) {

	for i := 0; i < len(arr); i++ {
		// 假设第i个位置的元素是最大的
		max := arr[i]
		maxIndex := i
		// 与第i个位置的元素进行比较
		for j := i; j < len(arr); j++ {
			if max < arr[j] {
				max = arr[j]
				maxIndex = j
			}
		}

		// 替换
		if maxIndex != i {
			arr[i], arr[maxIndex] = arr[maxIndex], arr[i]
		}
	}

	fmt.Println(arr)
}

直接插入排序

直接插入排序的基本作是将一个元素插入到已经排好序的有序表中,从而得到一个新的、元素数增1的有序表。对于少量元素的排序,它是一个有效的算法。

排序流程分析

func InsertSort1(arr [5]int) {
	// 从后面选择一个元素,然后在排序过后的数组中,从后向前比较,找到合适的位置就插入

	// 先确定插入的位置和要插入的值
	insertVal := arr[1]
	// 待插入元素位置的前一个位置
	insertIndex := 1 - 1

	// 开始循环比较
	// 满足这些条件才循环
	for insertIndex >= 0 && arr[insertIndex] < insertVal {
		// 数据后移一位
		arr[insertIndex+1] = arr[insertIndex]
		insertIndex--
	}

	// 找到了合适的位置,进行插入
	// 判断是否在最后
	if insertIndex+1 != 1 {
		arr[insertIndex+1] = insertVal
	}

	// 进行第二次插入排序,排第三个元素
	insertVal = arr[2]
	insertIndex = 2 - 1

	for insertIndex >= 0 && arr[insertIndex] < insertVal {
		arr[insertIndex+1] = arr[insertIndex]
		insertIndex--
	}

	if insertIndex+1 != 2 {
		arr[insertIndex+1] = insertVal
	}

	// 进行第三次插入排序,排第四个元素
	insertVal = arr[3]
	insertIndex = 3 - 1

	for insertIndex >= 0 && arr[insertIndex] < insertVal {
		arr[insertIndex+1] = arr[insertIndex]
		insertIndex--
	}

	if insertIndex+1 != 3 {
		arr[insertIndex+1] = insertVal
	}

	// 进行第四次插入排序,排第五个元素
	insertVal = arr[4]
	insertIndex = 4 - 1

	for insertIndex >= 0 && arr[insertIndex] < insertVal {
		arr[insertIndex+1] = arr[insertIndex]
		insertIndex--
	}

	if insertIndex+1 != 4 {
		arr[insertIndex+1] = insertVal
	}
	fmt.Println(arr)
}

循环排序

func InsertSort2(arr [5]int) {
	for i := 1; i < len(arr); i++ {
		insertVal := arr[i]
		insertIndex := i - 1
		for insertIndex >= 0 && arr[insertIndex] < insertVal {
			arr[insertIndex+1] = arr[insertIndex]
			insertIndex--
		}
		if insertIndex != i {
			arr[insertIndex+1] = insertVal
		}
	}

	fmt.Println(arr)
}

快速排序

快速排序(quickSort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分别进行快速排序,整个排序过程可以递归进行,依此达到整个数据变成有序序列。

/*
	left: 数组左边的下标
	right:所在右边的下标
	array:要排序的数组
*/
func QuickSort(left int, right int, array *[6]int) {
	l := left
	r := right
	// 找到中间的数
	pivot := array[(left+right)/2]
	temp := 0
	// for循环的目的是将比pivot小的数放到左边,比pivot大的说放到右边
	for l < r {
		// 从pivot的左边找到大于等于pivot的值
		for array[l] < pivot {
			l++
		}
		// 从pivot的右边找到小于等于pivot的值
		for array[r] > pivot {
			r--
		}
		// 到中间位置了,本次循环完成
		if l >= r {
			break
		}
		// 交换
		temp = array[l]
		array[l] = array[r]
		array[r] = temp
		// 优化,相等的情况不需要交换
		if array[l] == pivot {
			r--
		}
		if array[r] == pivot {
			l++
		}
	}

	// 如果 l == r,再移动下
	if l == r {
		l++
		r--
	}

	// 向后递归,继续排序
	// 向左递归
	if left < r {
		QuickSort(left, r, array)
	}
	// 向右递归
	if right > l {
		QuickSort(l, right, array)
	}
}

最后

完整源码在这里:https://github.com/bigzoro/go_algorithm/tree/main/sort

以上是关于Go语言数据结构与算法—常用排序算法的主要内容,如果未能解决你的问题,请参考以下文章

Go语言 排序与搜索切片

go语言实现排序算法

打造 Go 语言最快的排序算法

希尔排序ShellSort算法详解Java/Go/Python/JS/C不同语言实现

go-数据结构与算法 复习

Go语言之冒泡排序算法和二分查找算法