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

前言

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

本文主要讲述前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)比较,依次类推,从而最大的数会一直移动到最右边。

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

冒泡实现一

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

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

冒泡实现二

func bubbleSort2(arr []int) []int {
 n := len(arr)
 for i := 0; i < n-1; i++ {
  flag := 0
  for k := 0; k < n-1-i; k++ {
   if arr[k] > arr[k+1] {
    arr[k], arr[k+1] = arr[k+1], arr[k]
    flag += 1
   }
  }
  // 当遍历了一遍,flag仍然为0,说明原数组本身有序,所以不用再遍历,复杂度为O(n)
  if flag == 0 {
   break
  }
 }
 return arr
}

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

选择排序

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

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

代码实现如下:

func selectSort(arr []int) []int {
 n := len(arr)
 for i := 0; i < n; i++ {
  // k=i+1,表示下一个数
  for k := i + 1; k < n; k++ {
   if arr[i] > arr[k] {
    arr[i], arr[k] = arr[k], arr[i]
   }
  }
 }
 return arr
}

总结: 选择排序的最坏、最优复杂度都是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,以此类推

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

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

插入排序实现一

func insertSort1(arr []int) []int {
 n := len(arr)
 for i := 1; i < n; i++ {
  idx := i
  for idx > 0 {
   if arr[idx] < arr[idx-1] {
    arr[idx], arr[idx-1] = arr[idx-1], arr[idx]
   }
   // 通过这里,我们可以看到插入排序是往后比较的
   idx -= 1
  }
 }
 return arr
}

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

插入排序实现二

func insertSort2(arr []int) []int {
 n := len(arr)
 for i := 1; i < n; i++ {
  idx := i
  for idx > 0 {
   if arr[idx] < arr[idx-1] {
    arr[idx], arr[idx-1] = arr[idx-1], arr[idx]
    idx -= 1
    // 当要插入的值,比排好序的数组的最后一个值大,则不用再比较
   } else {
    break
   }
  }
 }
 return arr
}

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

选择排序 VS 插入排序

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

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

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

快速排序

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

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

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

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

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

非原地排序版本

func quickSort(arr []int) []int {
 n := len(arr)
 if n <= 1 {
  return arr
 }
 left, right := []int{}, []int{}
 // 挑选基准值,这里以第一个元素为基准值
 base := arr[0]
 arr = arr[1:]
 // 对数组进行分割
 for _, num := range arr {
  if num < base {
   left = append(left, num)
  }else{
   right = append(right, num)
  }
 }
 // 这两行代码等价于 quickSort(left) + [base] + quickSort(right)
 result := append(quickSort(left), base)
 result = append(result, quickSort(right)...)
 return result
}

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

原地排序版本

// 对数组进行划分,并返回下次划分的基准值
func partition(arr []int, startIdx, endIdx int) int {
 // 基准值
 base := arr[endIdx]
 i := startIdx - 1
 // 这一步把小于基准值的元素都挪到前面
 for j := startIdx; j < endIdx; j++ {
  if arr[j] < base {
   i++
   arr[j], arr[i] = arr[i], arr[j]
  }
 }
 // 这一步把基准值放到比它小的元素的后面
 arr[i+1], arr[endIdx] = arr[endIdx], arr[i+1]
 return i + 1
}

func quickSort2(arr []int, startIdx, endIdx int) {
 // 递归截止条件
 if startIdx >= endIdx {
  return
 }
 p := partition(arr, startIdx, endIdx)
 quickSort2(arr, startIdx, p-1)
 quickSort2(arr, p+1, endIdx)
}

// 如果对partition函数不理解的,建议使用下面的数组调试一遍,应该会更理解一些
func main() {
 arr := []int{3, 2, 1, 6, 8, 5, 4}
 quickSort2(arr, 0, len(arr) - 1)
 fmt.Println(arr)
}

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

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

归并排序

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

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

// 合并
func merge(left []int, right []int) []int {
 leftIdx, rightIdx := 0, 0
 result := []int{}
 for leftIdx < len(left) && rightIdx < len(right) {
  if left[leftIdx] <= right[rightIdx] {
   result = append(result, left[leftIdx])
   leftIdx += 1
  } else {
   result = append(result, right[rightIdx])
   rightIdx += 1
  }
 }
 // 上述for循环有两种情况:
 // 1.left数组和right数组长度一致
 // 2.left数组和right数组长度不一致,则必定有一方有多余元素,需要把这些多余元素复制到队尾
 result = append(result, left[leftIdx:]...)
 result = append(result, right[rightIdx:]...)
 return result
}

func mergeSort(arr []int) []int {
 n := len(arr)
 if n <= 1 {
  return arr
 }

 split := n / 2
 left := mergeSort(arr[:split])
 right := mergeSort(arr[split:])
 return merge(left, right)
}

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

归并排序 VS 快速排序

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

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

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值