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

2,238 阅读7分钟

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

今天为大家介绍排序算法

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

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

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

01 冒泡排序

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

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

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

就这样一轮一轮地比较,最后实现从小到大排序。由于这个排序算法较为简单,就不需要再赘述了。

直接看代码:

image.png

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

02 选择排序

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

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

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

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

image.png

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

03 插入排序

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

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

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

插入排序的代码如下:

image.png

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

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

04 快速排序

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

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

image.png

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

05 归并排序

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

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

image.png

image.png

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

06 堆排序

先来介绍一下什么是堆?堆是一种近似完全二叉树的数据结构,可以分为大根堆,小根堆。

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

图片

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

图片

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

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

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

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

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

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

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

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

下面看上述过程的代码。

image.png

image.png

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

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

07 希尔排序

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

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

希尔排序的过程如下:

图片

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

来看代码:

image.png

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

08 计数排序

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

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

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

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

图片

计数排序代码:

image.png

这个算法的时间复杂度为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为数组最大值位数。

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

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

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

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