当前位置:   article > 正文

golang 排序_算法手撕-八大排序Go版本(上)

golang手撕堆排

前言

八大排序包含:冒泡排序、选择排序、插入排序、快速排序(快排)、归并排序、基数排序、希尔排序、堆排序。

本文主要讲述前5种,其中,建议冒泡排序选择排序插入排序可以放在一起对比;快速排序归并排序可以放在一起对比。

注:本文的排序采用Golang实现。

排序基础

1.时间复杂度、空间复杂度以及大O表示法,这里不再赘述

2.线性时间和非线性时间:线性时间可理解为线性函数,指数为1,比如O(n); 非线性时间可理解为非线性函数,指数不为1,比如O(n2)

3.稳定排序和非稳定排序:

  • 3.1 稳定排序:当a=b,a在b之前,排序后a仍然在b之前
  • 3.2 非稳定排序:当a=b,a在b之前,排序后a可能在b之后

4.原地排序和非原地排序:原地排序就是指不申请多余的空间来进行的排序,只在原来的排序数据中比较和交换。非原地排序则相反。

排序算法比较

排序算法时间复杂度(平均)时间复杂度(最好)时间复杂度(最坏)是否稳定是否原地排序
冒泡排序O(n2)O(n)O(n2)稳定原地
选择排序O(n2)O(n2)O(n2)非稳定原地
插入排序O(n2)O(n)O(n2)稳定原地
快速排序O(nlogn)O(nlogn)O(n2)非稳定原地
归并排序O(nlogn)O(nlogn)O(nlogn)稳定非原地

冒泡排序

冒泡排序用一句话来总结就是:从左至右,对数组中相邻的两个元素进行比较,将较大的放到后面。

假设有一个数组:[5, 4, 3, 2, 1],使用冒泡排序的话,会先将第一个数(5)与第二个数比较(4),由于 5>4,所以会互换位置;接着第二个数(5)与第三个数(3)比较,依次类推,从而最大的数会一直移动到最右边。

这里列举下冒泡排序的两种实现方式,这两种方式的最坏和最优的时间复杂度不同

冒泡实现一

  1. func bubbleSort1(arr []int) []int {
  2. // 代码中会大量用到len(arr),为了减少重复计算,可以用一个变量来存储
  3. n := len(arr)
  4. for i := 0; i < n-1; i++ {
  5. for k := 0; k < n-1-i; k++ {
  6. if arr[k] > arr[k+1] {
  7. arr[k], arr[k+1] = arr[k+1], arr[k]
  8. }
  9. }
  10. }
  11. return arr
  12. }

总结:这种是比较常见的冒泡排序,最坏和最优时间复杂度都为O(n2)。

冒泡实现二

  1. func bubbleSort2(arr []int) []int {
  2. n := len(arr)
  3. for i := 0; i < n-1; i++ {
  4. flag := 0
  5. for k := 0; k < n-1-i; k++ {
  6. if arr[k] > arr[k+1] {
  7. arr[k], arr[k+1] = arr[k+1], arr[k]
  8. flag += 1
  9. }
  10. }
  11. // 当遍历了一遍,flag仍然为0,说明原数组本身有序,所以不用再遍历,复杂度为O(n)
  12. if flag == 0 {
  13. break
  14. }
  15. }
  16. return arr
  17. }

总结:这种是改良过的,最坏时间复杂度为O(n2),当数组本身有序的情况下,最优时间复杂度为O(n)

选择排序

选择排序用一句话来总结就是:从第一个位置开始,与后面的数进行比较,找出最小的,并和第一个位置互换(从而最小的数都会在最左边)。

假设有一个数组:[5, 4, 3, 2, 1],使用选择排序的话,会将数字5分别和数字4,3,2,1进行比较,第一轮结束后,数组变为:[1, 5, 4, 3, 2]

代码实现如下:

  1. func selectSort(arr []int) []int {
  2. n := len(arr)
  3. for i := 0; i < n; i++ {
  4. // k=i+1,表示下一个数
  5. for k := i + 1; k < n; k++ {
  6. if arr[i] > arr[k] {
  7. arr[i], arr[k] = arr[k], arr[i]
  8. }
  9. }
  10. }
  11. return arr
  12. }

总结: 选择排序的最坏、最优复杂度都是O(n2),因为遍历一次只能知道哪个数最小,无法节省遍历次数,所以最坏最优都是O(n2)。

冒泡排序 VS 选择排序

之前的我,对冒泡排序和选择排序有点傻傻分不清,经过最近的整理,总结出它们的几点区别:

1.比较方式不同:冒泡排序是相邻比较,选择排序是第一个数与其他数进行比较,具体如下:

34fa22db6a8fcbe0ff5c890d61afbd5c.png

2.时间复杂度不同:在特殊的情况下,冒泡排序可以达到O(n)的复杂度;而选择排序的复杂度永远为O(n2)

3.冒泡排序是稳定排序,选择排序是非稳定排序。以一个数组 [4, 6, 4, 3, 3]为例,在第一轮比较的时候,我们知道第一个4会和第一个3交换位置,从而原数组中2个4的前后顺序就被破坏了,所以选择排序不是稳定排序算法。

插入排序

插入排序有点像我们斗地主时,一般会对手中的牌进行排序。

还是以数组 [5, 4, 3, 2, 1] 为例,可以将其看做5张牌:

  • 1.一开始拿到数字5,没有人和它比较,所以它放在最左边(最左边我们放最小的)
  • 2.接着拿到数字4,和5做比较,发现比5小,所以手中的牌变成 [4, 5]
  • 3.接着拿到数字3,和5比较后交换,然后和4比较,也交换,所以手中的牌变成 [3, 4, 5]
  • 4.接着拿到数字2和数字1,以此类推

可以看到,插入排序是将一个值插入到一个已经排好序的数组,并一直持续到所有值插入完毕,最终得到一个新的有序数组。

这里也列举插入排序的两种实现方式,最坏最优时间复杂度也不同。

插入排序实现一

  1. func insertSort1(arr []int) []int {
  2. n := len(arr)
  3. for i := 1; i < n; i++ {
  4. idx := i
  5. for idx > 0 {
  6. if arr[idx] < arr[idx-1] {
  7. arr[idx], arr[idx-1] = arr[idx-1], arr[idx]
  8. }
  9. // 通过这里,我们可以看到插入排序是往后比较的
  10. idx -= 1
  11. }
  12. }
  13. return arr
  14. }

总结:插入排序的常规写法,最优复杂度和最坏复杂度为O(n2)

插入排序实现二

  1. func insertSort2(arr []int) []int {
  2. n := len(arr)
  3. for i := 1; i < n; i++ {
  4. idx := i
  5. for idx > 0 {
  6. if arr[idx] < arr[idx-1] {
  7. arr[idx], arr[idx-1] = arr[idx-1], arr[idx]
  8. idx -= 1
  9. // 当要插入的值,比排好序的数组的最后一个值大,则不用再比较
  10. } else {
  11. break
  12. }
  13. }
  14. }
  15. return arr
  16. }

总结:插入排序的一种优化实现,最坏复杂度为O(n2),当数组本身有序的情况下,最优复杂度为O(n), 有点类似冒泡排序。

选择排序 VS 插入排序

1.比较方式不同:个人对比后觉得可以将选择排序理解为往前比较,而插入排序是往后比较

2.时间复杂度不同:在特殊的情况下,插入排序可以达到O(n)的复杂度;而选择排序的复杂度永远为O(n2)

3.插入排序是稳定排序,选择排序是不稳定排序

快速排序

快速排序,又称分区交换排序(partition-exchange sort),其实个人觉得用分区交换排序更贴近这种排序的思想。

关于快速排序这个命名,个人猜测是因为这句话 "快速排序 O(nlog n) 通常明显比其他算法更快",所以才有了这个名字。

快速排序有以下三个步骤:

  • 1.挑选基准值
  • 2.对数组进行划分:所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)
  • 3.递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。

快速排序有两种版本,一种是原地排序的,但是不太好理解;一种不是原地排序的,但是相对好理解。

非原地排序版本

  1. func quickSort(arr []int) []int {
  2. n := len(arr)
  3. if n <= 1 {
  4. return arr
  5. }
  6. left, right := []int{}, []int{}
  7. // 挑选基准值,这里以第一个元素为基准值
  8. base := arr[0]
  9. arr = arr[1:]
  10. // 对数组进行分割
  11. for _, num := range arr {
  12. if num < base {
  13. left = append(left, num)
  14. }else{
  15. right = append(right, num)
  16. }
  17. }
  18. // 这两行代码等价于 quickSort(left) + [base] + quickSort(right)
  19. result := append(quickSort(left), base)
  20. result = append(result, quickSort(right)...)
  21. return result
  22. }

总结:这种实现会申请多余的空间(left、right两个数组),所以不是原地排序

原地排序版本

  1. // 对数组进行划分,并返回下次划分的基准值
  2. func partition(arr []int, startIdx, endIdx int) int {
  3. // 基准值
  4. base := arr[endIdx]
  5. i := startIdx - 1
  6. // 这一步把小于基准值的元素都挪到前面
  7. for j := startIdx; j < endIdx; j++ {
  8. if arr[j] < base {
  9. i++
  10. arr[j], arr[i] = arr[i], arr[j]
  11. }
  12. }
  13. // 这一步把基准值放到比它小的元素的后面
  14. arr[i+1], arr[endIdx] = arr[endIdx], arr[i+1]
  15. return i + 1
  16. }
  17. func quickSort2(arr []int, startIdx, endIdx int) {
  18. // 递归截止条件
  19. if startIdx >= endIdx {
  20. return
  21. }
  22. p := partition(arr, startIdx, endIdx)
  23. quickSort2(arr, startIdx, p-1)
  24. quickSort2(arr, p+1, endIdx)
  25. }
  26. // 如果对partition函数不理解的,建议使用下面的数组调试一遍,应该会更理解一些
  27. func main() {
  28. arr := []int{3, 2, 1, 6, 8, 5, 4}
  29. quickSort2(arr, 0, len(arr) - 1)
  30. fmt.Println(arr)
  31. }

总结:这种实现并不会申请多余的空间,所以是原地排序。

最后,当基准值(base)取中间数时,复杂度最优,为O(log2n),当基准值一直为最小/最大时,复杂度最坏, 为O(n2)。(注意这里的中间数,以数组[5, 4, 1, 3, 2]为例,中间数指的是3,而不是1)

归并排序

如果你对快速排序有一定了解,初看归并排序的时候会觉得这两个排序算法非常像,这也是我为什么建议大家可以将两者进行对比。

归并排序的思想是将一个数组划分为两部分,然后对两部分进行排序,接着将排序后的两部分进行合并,合并的时候需要新建一个临时数组保存合并的结果。

  1. // 合并
  2. func merge(left []int, right []int) []int {
  3. leftIdx, rightIdx := 0, 0
  4. result := []int{}
  5. for leftIdx < len(left) && rightIdx < len(right) {
  6. if left[leftIdx] <= right[rightIdx] {
  7. result = append(result, left[leftIdx])
  8. leftIdx += 1
  9. } else {
  10. result = append(result, right[rightIdx])
  11. rightIdx += 1
  12. }
  13. }
  14. // 上述for循环有两种情况:
  15. // 1.left数组和right数组长度一致
  16. // 2.left数组和right数组长度不一致,则必定有一方有多余元素,需要把这些多余元素复制到队尾
  17. result = append(result, left[leftIdx:]...)
  18. result = append(result, right[rightIdx:]...)
  19. return result
  20. }
  21. func mergeSort(arr []int) []int {
  22. n := len(arr)
  23. if n <= 1 {
  24. return arr
  25. }
  26. split := n / 2
  27. left := mergeSort(arr[:split])
  28. right := mergeSort(arr[split:])
  29. return merge(left, right)
  30. }

总结:归并排序划分部分时间复杂度为O(logn),合并部分时间复杂度为O(n),所以总时间复杂度为O(nlogn)

归并排序 VS 快速排序

1.基准值:归并排序并不需要基准值,而快速排序需要一个基准值

2.稳定性:归并排序是稳定排序,但不是原地排序;快速排序是非稳定排序,但可以实现原地排序。

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号