Go语言实现常用排序算法

1,223 阅读6分钟

​背景

人类的发展中,我们学会了计数,比如知道小明今天打猎的兔子的数量是多少。另外一方面,我们也需要判断,今天哪个人打猎打得多,我们需要比较。

所以,排序这个很自然的需求就出来了。比如小明打了5只兔子,小王打了8只,还有部落其他一百多个人也打了。我们要论功行赏,谁打得多,谁就奖赏大一点。如何排序呢,怎么在最快的时间内,找到打兔子最多的人呢,这是一个很朴素的问题。

排序算法介绍

经过很多年的研究,出现了很多的排序算法,有快的有慢的。比如:

  1. 插入类排序有:直接插入排序和希尔排序

  2. 选择类排序有:直接选择排序和堆排序

  3. 交换类排序有:冒泡排序和快速排序

它们的复杂度如下:

稳定性概念

定义:能保证两个相等的数,经过排序之后,其在序列的前后位置顺序不变。(A1=A2,排序前A1在A2前面,排序后A1还在A2前面)

意义:稳定性本质是维持具有相同属性的数据的插入顺序,如果后面需要使用该插入顺序排序,则稳定性排序可以避免这次排序。

我们把冒泡排序直接选择排序直接插入排序认为是初级的排序算法,其中直接插入排序的性能是综合最好的,一般来说,当排序数组规模 n 较小时,直接插入排序可能比任何排序算法都要快,建议只在小规模排序中使用。

希尔排序:

是对直接插入排序的改进版本,比直接选择排序和直接插入排序快,且随着规模的递增,这种性能提升越明显。因为算法容易理解,在排序数组中等规模下,我们可以使用它。在非常大的规模下,它的性能也不那么糟糕,但大规模排序还是建议使用以下的高级排序算法。

快速排序归并排序堆排序是比较高级的排序算法。

目前被认为综合最好的高级排序算法是快速排序,快速排序的平均用时最短,大多数的编程库内置的排序算法都是它。

堆排序也是一种很快的排序算法,通过维持一棵二叉树,树的根节点总是最大或最小从而可实现排序。

归并排序和快速排序一样使用分治法,递归地先使每个子序列有序,再将两个有序的序列进行合并成一个有序的序列。

冒泡排序

冒泡排序是一种极其简单的排序算法,也是我所学的第一个排序算法。它重复地走访过要排序的元素,依次比较相邻两个元素,如果他们的顺序错误就把他们调换过来,直到没有元素再需要交换,排序完成。这个算法的名字由来是因为越小(或越大)的元素会经由交换慢慢“浮”到数列的顶端。

在面试中,也是问的最多的一种,有时候还要求手写排序代码。

冒泡排序属于交换类的排序算法。

冒泡排序算法的运作如下:

  1. 比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。

  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

  3. 针对所有的元素重复以上的步骤,除了最后一个。

  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

由于它的简洁,冒泡排序通常被用来对于程序设计入门的学生介绍算法的概念。其排序动图如下:

冒泡排序的实现代码如下:

// 冒泡排序,从大到小排序func BubbleSort(arr []int) {  for i := 0; i < len(arr) - 1; i++ {    for j := 0; j < len(arr)-1-i; j++ {      if arr[j] < arr[j+1] {        arr[j], arr[j+1] = arr[j+1], arr[j]      }    }  }}

选择排序

选择排序也是一种简单直观的排序算法。它的工作原理很容易理解:初始时在序列中找到最小(大)元素,放到序列的起始位置作为已排序序列;然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

注意选择排序与冒泡排序的区别:冒泡排序通过依次交换相邻两个顺序不合法的元素位置,从而将当前最小(大)元素放到合适的位置;而选择排序每遍历一次都记住了当前最小(大)元素的位置,最后仅需一次交换操作即可将其放到合适的位置。

选择排序属于选择类排序算法。

我打扑克牌的时候,会习惯性地从左到右扫描,然后将最小的牌放在最左边,然后从第二张牌开始继续从左到右扫描第二小的牌,放在最小的牌右边,以此反复。选择排序和我玩扑克时的排序特别相似。

其动图如下所示:

代码实现如下:

// 选择排序,从大到小排序func SelectSort(arr []int) {  for i := 0; i < len(arr); i++ {    maxVal := arr[i]    maxIndex := i    for j := i + 1; j < len(arr); j++ {      if maxVal < arr[j] {        maxVal = arr[j]        maxIndex = j      }    }    if maxIndex != i {      arr[i], arr[maxIndex] = arr[maxIndex], arr[i]    }  }}

插入排序

插入排序,一般我们指的是简单插入排序,也可以叫直接插入排序。就是说,每次把一个数插到已经排好序的数列里面形成新的排好序的数列,以此反复。

插入排序属于插入类排序算法。

除了我以外,有些人打扑克时习惯从第二张牌开始,和第一张牌比较,第二张牌如果比第一张牌小那么插入到第一张牌前面,这样前两张牌都排好序了,接着从第三张牌开始,将它插入到已排好序的前两张牌里,形成三张排好序的牌,后面第四张牌继续插入到前面已排好序的三张牌里,直至排序完。

具体算法描述如下

  1. 从第一个元素开始,该元素可以认为已经被排序

  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描

  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置

  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置

  5. 将新元素插入到该位置后

  6. 重复步骤2~5

代码实现如下:

func InsertSort(arr []int) {  for i := 1; i < len(arr); i++ {    insertVal := arr[i]    insertIndex := i - 1    for insertIndex >= 0 && insertVal > arr[insertIndex] {      arr[insertIndex+1] = arr[insertIndex]      insertIndex--    }    if insertIndex+1 != i {      arr[insertIndex+1] = insertVal    }  }}

归并排序

归并排序是创建在归并操作上的一种有效的排序算法。

归并排序的实现分为递归实现与非递归(迭代)实现。递归实现的归并排序是算法设计中分治策略的典型应用,我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。非递归(迭代)实现的归并排序首先进行是两两归并,然后四四归并,然后是八八归并,一直下去直到归并了整个数组。

归并排序算法主要依赖归并(Merge)操作。归并操作指的是将两个已经排序的序列合并成一个序列的操作,归并操作步骤如下:

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列

  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置

  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置

  4. 重复步骤3直到某一指针到达序列尾

  5. 将另一序列剩下的所有元素直接复制到合并序列尾

动态图如下:

实现代码如下:

// 归并排序,从大到小排序func MergeSort(arr []int, left, right int) {  if right - left <= 1 {    return  }  middle := (left + right)/2  MergeSort(arr, left, middle)  MergeSort(arr, middle, right)​  arrLeft := make([]int, middle - left)  arrRight := make([]int, right - middle)  copy(arrLeft, arr[left:middle])  copy(arrRight, arr[middle:right])​  i := 0  j := 0  for k := left; k < right; k++ {    if i >= middle - left {      arr[k] = arrRight[j]      j++    }else if j >= right - middle {      arr[k] = arrLeft[i]      i++    }else if arrLeft[i] > arrRight[j] {      arr[k] = arrLeft[i]      i++    }else {      arr[k] = arrRight[j]      j++    }  }}

快速并排

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序n个元素要O(nlogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他O(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。

快速排序使用分治策略(Divide and Conquer)来把一个序列分为两个子序列。算法实现步骤为:

  1. 从序列中挑出一个元素,作为"基准"(pivot).

  2. 把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。

  3. 对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。

动态图如下:

实现代码如下:

// 快速排序,从大到小排序func QuickSort(arr []int, left, right int) {  l := left  r := right  pivot := arr[(left+right)/2]​  for l < r {    for arr[l] > pivot {      l++    }    for arr[r] < pivot {      r--    }​    if l >= r {      break    }    arr[l], arr[r] = arr[r], arr[l]​    if arr[l] == pivot {      r--    }    if arr[r] == pivot {      l++    }  }​  if l == r {    l++    r--  }  if left < r {    QuickSort(arr, left, r)  }  if right > l {    QuickSort(arr, l, right)  }}

以上是关于Go语言实现排序算法的介绍,完整示例代码地址:

github.com/Scoefield/g…

感谢您的阅读,如想了解更多,欢迎关注“Go键盘侠”公众号,一起交流学习哈!如果你觉得不错的话,请点个赞呗^_^。

推荐文章:

Go语言之网络编程(干货!)