八大排序算法及其Go代码实现

16 篇文章 1 订阅

1.选择排序

原理

每次选择数组中的最小元素,排在第一个位置

算法步骤

[38,65,97,76,13,27,49]
13 [65 97 76 38 27 49]
13 27 [97 76 38 65 49]
13 27 38 [76 97 65 49]
13 27 38 49 [97 65 76]
13 27 38 49 65 [97 76]
13 27 38 49 65 76 [97]
13 27 38 49 65 76 97

代码实现

func SelectSort(data []int) {
	length := len(data)

	for i := 0; i < length; i++ {
		// 记录第一个数据
		min := data[i]
		index := i

		// 获取到最小的值以及对应的索引
		for j := i + 1; j < length; j++ {
			if min > data[j] {
				min = data[j]
				index = j
			}
		}
		// 如果最小的值不是当前序列的第一个值
		// 那么需要进行交换
		if index != i {
			// 交换最小值
			data[index], data[i] = data[i], min
		}
	}
}

2.插入排序

原理

假设之前的数组都是一个有序序列,其余的记录为无序序列,从这些无序序列中不断选取数据,插入到前面已经排序好的有序序列中

算法步骤

[38] 65 97 76 13 27 49
[38 65] 97 76 13 27 49
[38 65 97] 76 13 27 49
[38 65 76 97] 13 27 49
[13 38 65 76 97] 27 49
[13 27 38 65 76 97] 49
[13 27 38 49 65 76 97]

代码实现

func InsertSort(data []int) {
	for i := 1; i < len(data); i++ {
		tmp, j := data[i], i
		// 如果第j-1个元素比第i个元素大
		// 不满足递增的条件,第i个需要移动到前面
		if data[j-1] > tmp {
			// 将第i个元素移动到前面的适当位置
			// 也就是将元素不断的向右边移动,直到找到合适的位置摆放
			for j >= 1 && data[j-1] > tmp {
				data[j] = data[j-1]
				j--
			}
		}
		// 将当前遍历到的值填充到对应的位置
		data[j] = tmp
	}
}

3.冒泡排序

原理

基本思路: 从第一个元素开始一次对相邻的记录进行比较, 当前面的记录大于后面的记录,交换其位置,进行一轮比较和换位之后,n个记录中的最大记录将位于第n位

算法步骤

{38 65 97 76 13 27 49}
38 65 76 13 27 49 [97]
38 65 13 27 49 [76 97]
38 13 27 49 [65 76 97]
13 27 38 [49 65 76 97]
13 27 [38 49 65 76 97]
13 [27 38 49 65 76 97]
[13 27 38 49 65 76 97]

代码实现

func BubbleSort(nums []int) {
	flag := false
	n := len(nums)
	// 这里采用的是不断将最大的移动到数组的最后面
	// 当然也可以将最小的移动到最前面来
	for i := 0; i < n ; i ++{
		for j := 0; j < n-1 -i; j ++{
			if nums[j] > nums[j+1]{
				nums[j], nums[j+1] = nums[j+1], nums[j]
				flag = true
			}
		}
		if !flag{
			fmt.Println("一轮遍历中没有进行交换,说明数组已经有序")
			break
		}
		fmt.Println(nums)
	}
}

4.归并排序

原理

将一个数组拆成两份,每一份进行递归操作之后都是有序的,然后进行合并,最终是有序的。

算法步骤

{38 65 97 76 13 27 49}
[38 65 97 76] [13 27 49] 
[38 65] [97 76] [13 27] [49]
[38] [65] [97] [76] [13] [27] [49]  // 全部分裂
[38 65] [76 97] [13 27] [49] // 开始进行合并
[38 65 76 97] [13 27 49]
[13 27 38 49 65 76 97]

代码实现

func MergerSort(nums []int) {
	mergeSort(nums, 0, len(nums)-1)
}

func mergeSort(nums []int, left, right int) {
	if left >= right {
		return
	}
	mid := (right-left)/2 + left
	mergeSort(nums, left, mid)
	mergeSort(nums, mid+1, right)
	merge(nums, left, mid, right)
}

// 合并两个数组,分别是从 left -> mid , mid -> right 索引开始
func merge(nums []int, left, mid, right int) {
	// 辅助数据
	tmp := make([]int, right-left+1)

	for i := left; i <= right; i++ {
		tmp[i-left] = nums[i]
	}
	i, j := 0, mid-left+1
	k := left
	for k <= right && i <= mid-left && j <= right-left {
		if tmp[i] > tmp[j] {
			nums[k] = tmp[j]
			k++
			j++
		} else {
			nums[k] = tmp[i]
			i++
			k++
		}
	}
	for i <= mid-left {
		nums[k] = tmp[i]
		i++
		k++
	}

	for j <= right-left {
		nums[k] = tmp[j]
		j++
		k++
	}
}

5.快速排序

快速排序是一种非常高效的排序算法,它采用分而治之的思想,把大的拆分成小的,小的再拆分成更小的。

原理

对于一组给定的记录,通过一趟排序之后,将原来的序列分成两部分,其中一部分的所有记录均比后一部分的所有记录小,然后再依次对前后两部分的记录进行快速排序,递归改过程,直到序列中的所有记录均有序为止

可以直接认为是找到了一个分界点,比如如果需要实现递增,那么左边都是小于该数,右边都是大于该数

算法步骤

  1. 分解:将输入的序列array[m...n] 划分成两个非空子序列array[m...k]array[k+1...n],使得array[k+1...n]中的任意一个元素都不小于array[m...k]中的元素
  2. 递归求解:通过递归调用快速排序算法分别对array[m...k]array[k+1...n]进行排序
  3. 合并:由于对分解出来的两个子序列的排序是就地进行的,所以在array[m...k]array[k+1...n]都排好序后不需要执行任何计算array[m...n]

代码实现

func sort(arr []int, left, right int) {
	if left >= right {
		return
	}

	i := left
	j := right

	// 选定一个分界点
    // 如果直接下面这种方式取的话,如果数据是有序的,那么时间复杂度相比较会比较高
	index := arr[i]
    // 也可以随机挑选一个
    
   	// rand.Seed(time.Now().UnixNano())
	// var random int 
	// random = rand.Int() % (right-left+1) + left
    // 将随机选中的移动到最左边
	// arr[i], arr[random] = arr[random], arr[i]
    // 这里只是为了更新上面的index,如果直接写的话,可以去掉上的赋值,直接使用index:= arr[i]
	// index = arr[i]

	for i < j {
		// 右边界不断向左移动,如果碰到比index小的数据
		// 则需要将左边的i对应的数据赋值为对应的值
		for i < j && arr[j] >= index {
			j--
		}
		if i < j {
			arr[i] = arr[j]
			i++
		}
		// 左边界不断往右移动,如果碰到比index小的数据
		// 则将右边的这个数据赋值索引j对应的数据
		for i < j && arr[i] <= index {
			i++
		}

		if i < j {
			arr[j] = arr[i]
			j--
		}


	}
    // 索引i表示的就是
    arr[i] = index

	// 排序左边子数组
	sort(arr, left, i-1)
	// 排序右边
	sort(arr, i+1, right)
}

func QuickSort(arr []int) {
	sort(arr, 0, len(arr)-1)
}
基准关键字的选取

常用的基准关键字的选取有以下方式:

  1. 选取首尾,中间位置上的中值作为基准关键字
  2. 选取随机数

6.希尔排序

原理

类似插入排序,但是不是相邻的进行,而是会跳过几个位置

算法步骤

{38, 65, 97, 76, 13, 27, 49}
step为: 3   [38 65 97 76 13 27 49]  // 索引 3 和 0 进行比较并交换
step为: 3   [38 13 97 76 65 27 49]  // 索引 4 和 1 进行比较并交换
step为: 3   [38 13 27 76 65 97 49]  // 索引 5 和 2 进行比较并交换
step为: 3   [38 13 27 49 65 97 76]  // 索引 6 和 3 进行比较并交换
step为: 1   [13 38 27 49 65 97 76]  // 索引 1 和 0 进行比较并交换
step为: 1   [13 27 38 49 65 97 76]  // 索引 2 和 1 进行比较并交换
step为: 1   [13 27 38 49 65 97 76]  // 索引 3 和 2 进行比较并交换
step为: 1   [13 27 38 49 65 97 76]  // 索引 4 和 3 进行比较并交换
step为: 1   [13 27 38 49 65 97 76]  // 索引 5 和 4 进行比较并交换
step为: 1   [13 27 38 49 65 76 97]  // 索引 6 和 5 进行比较并交换

代码实现

func ShellSort(nums []int) {
	// 步长
	for step := len(nums) / 2; step > 0; step /= 2 {
		for i := step ; i < len(nums); i++ {
			// 直接插入排序
			if nums[i] < nums[i-step] {
				j, tmp := i, nums[i]
				// 找出同一组中比tmp小的值,往后面移动step个位置
				for j >= step && tmp < nums[j-step] {
					nums[j] = nums[j-step]
					j -= step
				}
				nums[j] = tmp
			}
		}
	}
}

7.堆排序

原理

这里说一下递增排序,使用大根堆

大根堆有一个特点,那就是堆的顶部是整个数组中最大的元素,一个比较简单的想法就是,我把这个元素拿出来,然后插入到其他的数组中,堆中 pop 出堆顶的元素,依次进行,便可以获取到一个排序序列,不过这种思路需要多余的一个数组,其实没有必要,我们来自己看看删除元素的过程。

首先,将堆顶的元素和最后一个元素进行交换,然后 size--,进行向下冒泡 down,删除掉最后一个元素。

func pop(arr *[]int) int {
	nums := *arr
	n := len(nums) - 1
    // 交换
	nums[0], nums[n] = nums[n], nums[0]
	down(nums, 0, n)
	x := nums[n]
	// 删除最后一个元素
	*arr = (*arr)[:n]
	return x
}

注意这里删除最后一个元素,我们可以把这个元素保留在这里,但是让 down 的时候不接触到这个元素不就行了?这个很好实现,因为一般我们都是使用元素的长度作为 down 的一个 size 参数,我们指定一下就好,主要的逻辑代码如下:

func heapSort(nums []int) {
	size := len(nums)
	// 首先建堆
	buildHeap(nums)
	// 然后这个时候最大的元素在第一个
	for size > 1 {
		// 第一个元素和最后一个进行交换
		nums[0], nums[size-1] = nums[size-1], nums[0]
		size--
		down(nums, 0, size)
	}
}

代码实现

// 建大根堆
func buildHeap(nums []int) {
	size := len(nums)
	for i := size / 2; i >= 0; i-- {
		down(nums, i, size)
	}
}

// 插入元素的时候需要使用
func up(nums []int, i int) {
	for {
		// 父结点计算公式!!
		parent := (i - 1) / 2
		// 如果父结点大于子节点了,说明不需要调换,已经满足条件了
		// 如果 i == parent 说明 i = 0 了,没有父结点,也不需要调换
		if i == parent || nums[parent] > nums[i] {
			break
		}
		// 父结点没有子节点i的值大,需要调换
		nums[parent], nums[i] = nums[i], nums[parent]
	}
}

// 初始化的时候需要建堆
// 删除元素的时候需要down:
// 将第一个元素和最后一个元素进行调换, 然后重新 down(nums, 0, size)
func down(nums []int, i int, size int) {
	left, right := 2*i+1, 2*i+2
	j := i

	// 找到最大的元素的下标
	if left < size && nums[left] > nums[j] {
		j = left
	}
	if right < size && nums[right] > nums[j] {
		j = right
	}

	if j != i {
		nums[i], nums[j] = nums[j], nums[i]
		down(nums, j, size)
	}
}

// 不用递归也可以
func down2(nums []int, i, size int) {
	for {
		left, right := i*2+1, i*2+2
		// 说明i是叶子节点
		if left >= size {
			break
		}
		// 找到最大的节点
		largest := i
		if left < size && nums[left] > nums[largest] {
			largest = left
		}
		if right < size && nums[right] > nums[largest] {
			largest = right
		}

		if largest != i {
			nums[i], nums[largest] = nums[largest], nums[i]
			i = largest
		} else {
			// 如果最大的节点就是本身,那么后面也不需要进行操作了
			break
		}
	}
}

// 从堆中弹出一个数
// 首先将堆顶的元素和最后一个元素进行调换,然后进行down操作
func pop(arr *[]int) int {
	nums := *arr
	n := len(nums) - 1
	nums[0], nums[n] = nums[n], nums[0]
	down(nums, 0, n)
	x := nums[n]

	*arr = (*arr)[:n]
	return x
}

func HeapSort(nums []int) {
	size := len(nums)
	// 首先建堆
	buildHeap(nums)
	// 然后这个时候最大的元素在第一个
	for size > 1 {
		// 第一个元素和最后一个进行交换
		nums[0], nums[size-1] = nums[size-1], nums[0]
		size--
		down(nums, 0, size)
	}
}

8.计数排序

原理

设置一个长度为元素最大值最小值的空间,将元素一个一个对应一个索引,如果这个索引中已经存在元素,那么对应的值+1,说明这个索引处对应多个元素,最后遍历一遍这个索引空间,值为0的不需要添加,值为多个的一次添加到原数组中

计数排序适合于数组中的值在一定范围内的数据,比如说年龄,数据可能需要进行整理,比如 [10000, 20000] 的数据,我们可以首先整理为 [0, 10000],然后再转换回去。

代码实现

// 比如我们需要排序年龄,那么我们可以估算年龄在[0,200]中
func CountingSort(nums []int) {
    count := make([]int, 200)
    for _, num := range nums {
        count[num] ++ 
    }
    index := 0 
    for i :=0; i < len(count); i ++ {
        // 出现了多少次,排序后添加到原数组多少次
        for j:= 0; j < count[i]; j++ {
            nums[index] = i 
            index ++ 
        }
    }
}

9.总结

下表给出了每种排序算法的稳定性和效率的比较:

排序方法最好时间平均时间最坏时间辅助储存稳定性备注
简单选择排序 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)不稳定n小时较好
直接插入排序 O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)稳定大部分有序时较好
冒泡排序 O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)稳定n小时较好
希尔排序 O ( n ) O(n) O(n) O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( s n )   1 < s < 2 O(sn)\ 1<s<2 O(sn) 1<s<2 O ( 1 ) O(1) O(1)不稳定s是所选分组
快速排序 O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n 2 ) O(n^2) O(n2) O ( log ⁡ n ) O(\log n) O(logn)不稳定n大时较好
堆排序 O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( 1 ) O(1) O(1)不稳定n大时较好
归并排序 O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n ) O(n) O(n)稳定n大时较好

如果想要更直观的看排序过程,推荐使用 Data Structure Visualizations 这个网站进行理解。

具体的实现代码可从微信公众号推文中获取~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值