Go语言实现十大排序算法,讲解超详细

大家好,我是王不错。(欢迎关注我的个人公众号:程序员王不错

今天为大家介绍排序算法。排序算法是算法领域的基础,可以说是每个程序员都必须熟练掌握的知识。

本文将最常见的十大排序算法进行了完整的总结,相信看完这篇文章,你对排序算法的掌握将会更进一步。

要说明的是,本文的算法实现都是由Go语言实现的,你可以根据自己的习惯用不同语言实现。

01 冒泡排序

冒泡排序是一种比较简单的排序算法,它的原理是:

从左到右遍历数组,相邻元素两两进行比较。每次比较完一轮,就会找到数组中最大的一个或最小的一个。这个数就会从序列的最右边冒出来。

以从小到大排序为例,第一轮比较后,所有数中最大的那个数就会浮到最右边;第二轮比较后,所有数中第二大的那个数就会浮到倒数第二个位置……就这样一轮一轮地比较,最后实现从小到大排序。

由于这个排序算法较为简单,就不需要再赘述了。

直接看代码:

func bubbleSort(nums []int) []int {
  numsLen := len(nums)
  if numsLen <= 1 {
    return nums
  } 
  for i := 0; i < numsLen; i++ {
    for j := 0; j < numsLen-i-1; j++ {
      if nums[j] > nums[j+1] {
        nums[j], nums[j+1] = nums[j+1], nums[j]
      }
    }
  }
  return nums
}

这个算法的时间复杂度是O(n^2),空间复杂度是O(1)。

02 选择排序

选择排序的原理是,首先在开始的时候遍历整个数组,找到序列中的最小元素,然后将这个元素与第一个元素交换,这样最小的元素就放到了它的最终位置上了。

然后,从第二个元素开始遍历,找到剩下的n-1个元素中的最小元素,再与第二个元素进行交换。

以此类推,直到最后一个元素放到了最终位置。

同样,选择排序也不难理解,还是直接看代码:


func selectSort(nums []int) []int {
  numsLen := len(nums)
  if numsLen < 2 {
    return nums
  }
  for i := 0; i < numsLen; i++ {
    min := i
    for j := i+1; j < numsLen; j++ {
      if nums[j] < nums[min] {
        min = j
      }
    }
    nums[min], nums[i] = nums[i], nums[min]
  }
  return nums
}

这个算法的时间复杂度也是O(n^2)。

03 插入排序

插入排序,也叫直接插入排序。

它的原理是,将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。

在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面的有序表进行待插入位置查找,并进行移动。

插入排序的代码如下:

func insertSort(nums []int) []int {
  numsLen := len(nums)
  if numsLen < 2 {
    return nums
  }
  for i := 1; i < numsLen; i++ {
        // 当前值的前一个元素的下标
    preIndex := i - 1
        // 当前值
    nowNums := nums[i]
    for nums[preIndex] > nowNums {
      nums[preIndex+1] = nums[preIndex]
      if preIndex == 0 {
        preIndex--
        break
      }
      preIndex--
    }
    nums[preIndex+1] = nowNums
  }
  return nums
}

这个算法的时间复杂度仍是O(n^2),空间复杂度是O(1)。

下面介绍几个加快排序的算法。

04 快速排序

快速排序要求我们选择一个基准,根据这个基准为原数组分组,比基准大的一组,比基准小的一组,再重复递归地进行快速排序,重新合并每组排序后的序列,即为排好序的序列。

快速排序需要我们先理解递归的思想,代码如下:


func quickSort(nums []int) []int {
  numsLen := len(nums)
  // base case
  if numsLen < 2 {
    return nums
  }
  pivot := nums[0]
  var low, mid, high []int
  for i := 1; i < numsLen; i++ {
    if nums[i] < pivot {
      low = append(low, nums[i])
    } else if nums[i] == pivot {
      mid = append(mid, nums[i])
    } else {
      high = append(high, nums[i])
    }
  }

  low, high = quickSort(low), quickSort(high)
  res := append(append(low, mid...), high...)
  return res
}

这个算法的时间复杂度是O(nlogn),空间复杂度为O(logn)。

05 归并排序

归并排序是采用分治法的一个典型的排序算法,将已有序的子序列合并为一个完全有序的序列。

归并排序的过程是:首先将序列拆分成子序列,然后对子序列进行排序,最后将排序好的子序列合并,就得到了一个有序的序列。


func mergeSort(nums []int) []int {
  // base case
  if len(nums) < 2 {
    return nums
  }
  // 1、拆分
  mid := len(nums) / 2
  leftArray, rightArray := mergeSort(nums[:mid]), mergeSort(nums[mid:])
  // 2、合并
  return merge(leftArray, rightArray)
}

func merge(list1, list2 []int) []int {
  i, j := 0, 0
  var tmp []int
  for i < len(list1) && j < len(list2) {
    if list1[i] <= list2[j] {
      tmp = append(tmp, list1[i])
      i++
    } else {
      tmp = append(tmp, list2[j])
      j++
    }
  }
  if len(list1) == i {
    tmp = append(tmp, list2[j:]...)
  } else {
    tmp = append(tmp, list1[i:]...)
  }
  return tmp
}

时间复杂度是O(nlogn),空间复杂度是O(1)。

06 堆排序

先来介绍一下什么是

堆是一种近似完全二叉树的数据结构,可以分为大根堆,小根堆。

大根堆:每个节点的值都大于或等于他左右孩子节点的值。

小根堆:每个节点的值都小于或等于他左右孩子节点的值。

堆排序就是基于这种结构而产生的一种排序算法。

接着我们来看一下堆排序的过程:

假设我们想得到一个升序序列,那么就需要借助大根堆这种数据结构。

1.将待排序序列构造成一个大根堆,此时,整个数组的最大值就是堆结构顶端的根节点。

2.将堆根节点的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序序列个数为n-1。

3.将剩余的n-1个数再构造成一个大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组。

最后,我们还需要了解如何构造一个大根堆?

构造大根堆,要从堆的最后一个叶子结点起,从后往前调整,每次调整,将叶子结点和根节点中的最大值作为根节点,调整到最后,便构造成了一个大根堆。

下面看上述过程的代码。

// 堆化
func heapify(nums []int, n int, i int) {
  /* 
    n: 数组长度
    i: 待维护节点的下标,即不满足大根堆定义的节点下标
  */
  largest := i
  lson := i * 2 + 1;
  rson := i * 2 + 2;
  if lson < n && nums[lson] > nums[largest] {
    largest = lson
  }
  if rson < n && nums[rson] > nums[largest] {
    largest = rson
  }
  // 需要调整
  if largest != i {
    nums[largest], nums[i] = nums[i], nums[largest]
    // 继续递归维护下一个节点
    heapify(nums, n, largest)
  }
}

func heapSort(nums []int) []int {
  n := len(nums)
  // 1、构造堆
  // 从最后一个叶子结点起,从后往前调整构造堆
  for i := (n-1-1)/2; i >= 0; i-- {
    heapify(nums, n, i)
  }
  // 2、交换堆顶元素和最后一个元素, 堆长减一,再对堆顶元素维护堆结构
  for i := n-1; i > 0; i-- {
    nums[i], nums[0] = nums[0], nums[i]
    heapify(nums, i, 0)
  }
  return nums
}

维护堆性质(heapify)的时间复杂度是O(logN),因为一个二叉树的高度为O(logN),建堆过程的时间复杂度是O(N)。

综上,算法的时间复杂度为O(NlogN)。

07 希尔排序

希尔排序是插入排序的一种,也叫缩小增量排序,是直接插入排序算法的一种更高效的改进版本。

希尔排序的原理是:希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

希尔排序的过程如下:

增量表示整个数组中的元素以它为跨度被分在一组中,增量为几数组就会被分成几组。

来看代码:


func shellSort(nums []int) []int {
  for gap := len(nums) / 2; gap > 0; gap /= 2 {
    // 直接插入排序
    for i := gap; i < len(nums); i++ {
      key := nums[i]
      j := i
      for ; j >= gap && key < nums[j-gap]; j-=gap {
        nums[j] = nums[j-gap]
      }  
      nums[j] = key
    }
  }
  return nums
}

希尔排序的时间复杂度有点不同。它不像其他时间复杂度为O(nlogn)的排序算法那么快,但是要比选择排序和插入排序这种时间复杂度为O(n^2)的排序算法快得多。

08 计数排序

计数排序是一种通过计数而不是比较进行排序的算法,适用于范围较小的整数序列。

它的优势在于在对一定范围内的整数排序时,时间复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。

当然这是一种牺牲空间换取时间的做法。

下面通过排序过程了解计数排序的原理:

计数排序代码:


func countSort(nums []int) []int {
  n := len(nums)
  if n < 2 {
    return nums
  }
  // 寻找数组最大值,为其分配一个长度为max+1值均为0的数组存储计数
  max := nums[0]
  for i := 1; i < n; i++ {
    if nums[i] > max {
      max = nums[i]
    }
  }
  count := make([]int, max+1)
  // 计数
  for i := 0; i < n; i++ {
    count[nums[i]]++
  }
  // 新数组(最终结果存在这)
  // 这里声明数组一定要指定长度为0,不然会直接分配n个空间存储0,追加会在后面追加,结果有问题
  output := make([]int, 0, n)
  for k, v := range count {
    for ; v > 0; v-- {
      output = append(output, k)
    }
  }
  return output
}

这个算法的时间复杂度为O(n+m),空间复杂度为O(m),m为数组最大值。

09 桶排序

桶排序类似于计数排序,其思想近乎彻底的分治思想。

原理是将数组按照元素所属范围分到有限数量的桶里,再单独对每个桶排序(可以使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来记得到有序序列。

代码如下:


func BucketSort(nums []int, bucketNum int) []int {
  // bucketNum:桶数量
  numsLen := len(nums)
  // 确定数组元素范围
  min, max := nums[0], nums[0]
  for i := 0; i < numsLen; i++ {
    if min > nums[i] {
      min = nums[i]
    }
    if max < nums[i] {
      max = nums[i]
    }
  }
  bucket := make([][]int, bucketNum)
  for j := 0; j < numsLen; j++ {
    // 分桶:这里比较复杂,重点是地板除
    n := int(math.Floor(float64(nums[j] - min) / (float64(max - min + 1) / float64(bucketNum))))
    bucket[n] = append(bucket[n], nums[j])
    k := len(bucket[n]) - 2
    for k >= 0 && nums[j] < bucket[n][k] {
      bucket[n][k+1] = bucket[n][k]
      k--
    }
    bucket[n][k+1] = nums[j]
  }
  i := 0
  for p, q := range bucket {
    for t := 0; t < len(q); t++ {
      nums[i] = bucket[p][t]
      i++
    }
  }
  return nums
}

桶排序的时间复杂度为O(n),是一种线性复杂度的排序算法。

10 基数排序

先来举个例子看一下该算法的排序思路。

假设待排序数组为:

1、我们按照个位将待排序数组分组。

然后按照索引大小取出得到新的数组。

2、我们按照十位将待排序数组分组。

然后按照索引大小取出得到新的数组。

3、我们按照百位将待排序数组分组。

然后按照索引大小取出得到新的数组。

总结一下,基数排序的原理是:依次按照个位、十位、百位...的顺序对待排序数组分组,然后将每一次的分组按照索引大小重新组成新的数组。最后一轮的新数组即为排好序的数组。

基数排序需要注意的是,

1)要先知道数组最大值的位数,假设为n,那么就需要n次分组;

2)下面的算法只能对全为正整数的数组排序。


func BaseSort(nums []int) []int {
  numsLen := len(nums)
  if numsLen < 2 {
    return nums
  }
  // 找最大值
  max := nums[0]
  for i := 1; i < numsLen; i++ {
    if max < nums[i] {
      max = nums[i]
    }
  }
  // 计算最大值的位数
  maxDigit := 0
  for max > 0 {
    max /= 10
    maxDigit++
  }
  // 定义一个桶,分别存储每一位对应的数组中的数
  bucket := [10][]int{}
  for i := 0; i < 10; i++ {
    // 为了防止每一位都一样所以将每个桶的长度设为最大,与原数组大小相同
    bucket[i] = make([]int, numsLen)
  }
  // 定义一个计数数组,计算当前每一位对应有多少个数
  count := [10]int{0}
  // 定义一个除数
  divisor := 1
  // 定义一个位数
  var digit int

  for i := 1; i <= maxDigit; i++ {
    // 分组
    for j := 0; j < numsLen; j++ {
      temp := nums[j]
      digit = (temp / divisor) % 10
      bucket[digit][count[digit]] = temp
      count[digit]++
    }
    // 重排数组
    k := 0
    for m := 0; m < 10; m++ {
      if count[m] == 0 {
        continue
      }
      for n := 0; n < count[m]; n++ {
        nums[k] = bucket[m][n]
        k++
      }
      count[m] = 0
    }
    divisor *= 10
  }
  return nums
}

该算法的时间复杂度为O(m*n),m为数组长度,n为数组最大值位数。

以上就是对十大常见的排序算法的总结。

除了仔细阅读本文,最重要的是,一边看文章,一边也要动手把代码敲起来;如果你对排序算法不是那么熟悉,最好每天都拿出一点时间敲一遍,毕竟这也算是程序员的一件趁手兵器。

好啦,今天的分享就到这里。

我是程序员王不错,如果文章内容有帮助到你,就点个“关注”吧~

#Go##算法##排序算法#
全部评论
堆排序 在右节点更新哪里写成了左节点的索引
1 回复 分享
发布于 2023-03-13 17:46 广西

相关推荐

牛客717484937号:双飞硕没实习挺要命的
点赞 评论 收藏
分享
拒绝无效加班的小师弟很中意你:求职意向没有,年龄、课程冗余信息可以删掉,需要提升项目经历。排版需要修改。
点赞 评论 收藏
分享
2 7 评论
分享
牛客网
牛客企业服务