背景
人类的发展中,我们学会了计数,比如知道小明今天打猎的兔子的数量是多少。另外一方面,我们也需要判断,今天哪个人打猎打得多,我们需要比较。
所以,排序这个很自然的需求就出来了。比如小明打了5只兔子,小王打了8只,还有部落其他一百多个人也打了。我们要论功行赏,谁打得多,谁就奖赏大一点。如何排序呢,怎么在最快的时间内,找到打兔子最多的人呢,这是一个很朴素的问题。
排序算法介绍
经过很多年的研究,出现了很多的排序算法,有快的有慢的。比如:
-
插入类排序有:直接插入排序和希尔排序
-
选择类排序有:直接选择排序和堆排序
-
交换类排序有:冒泡排序和快速排序
它们的复杂度如下:
稳定性概念
定义:能保证两个相等的数,经过排序之后,其在序列的前后位置顺序不变。(A1=A2,排序前A1在A2前面,排序后A1还在A2前面)
意义:稳定性本质是维持具有相同属性的数据的插入顺序,如果后面需要使用该插入顺序排序,则稳定性排序可以避免这次排序。
我们把冒泡排序,直接选择排序,直接插入排序认为是初级的排序算法,其中直接插入排序的性能是综合最好的,一般来说,当排序数组规模 n 较小时,直接插入排序可能比任何排序算法都要快,建议只在小规模排序中使用。
希尔排序:
是对直接插入排序的改进版本,比直接选择排序和直接插入排序快,且随着规模的递增,这种性能提升越明显。因为算法容易理解,在排序数组中等规模下,我们可以使用它。在非常大的规模下,它的性能也不那么糟糕,但大规模排序还是建议使用以下的高级排序算法。
快速排序,归并排序和堆排序是比较高级的排序算法。
目前被认为综合最好的高级排序算法是快速排序,快速排序的平均用时最短,大多数的编程库内置的排序算法都是它。
堆排序也是一种很快的排序算法,通过维持一棵二叉树,树的根节点总是最大或最小从而可实现排序。
归并排序和快速排序一样使用分治法,递归地先使每个子序列有序,再将两个有序的序列进行合并成一个有序的序列。
冒泡排序
冒泡排序是一种极其简单的排序算法,也是我所学的第一个排序算法。它重复地走访过要排序的元素,依次比较相邻两个元素,如果他们的顺序错误就把他们调换过来,直到没有元素再需要交换,排序完成。这个算法的名字由来是因为越小(或越大)的元素会经由交换慢慢“浮”到数列的顶端。
在面试中,也是问的最多的一种,有时候还要求手写排序代码。
冒泡排序属于交换类的排序算法。
冒泡排序算法的运作如下:
-
比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。
-
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
-
针对所有的元素重复以上的步骤,除了最后一个。
-
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
由于它的简洁,冒泡排序通常被用来对于程序设计入门的学生介绍算法的概念。其排序动图如下:
冒泡排序的实现代码如下:
// 冒泡排序,从大到小排序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] } }}
插入排序
插入排序,一般我们指的是简单插入排序,也可以叫直接插入排序。就是说,每次把一个数插到已经排好序的数列里面形成新的排好序的数列,以此反复。
插入排序属于插入类排序算法。
除了我以外,有些人打扑克时习惯从第二张牌开始,和第一张牌比较,第二张牌如果比第一张牌小那么插入到第一张牌前面,这样前两张牌都排好序了,接着从第三张牌开始,将它插入到已排好序的前两张牌里,形成三张排好序的牌,后面第四张牌继续插入到前面已排好序的三张牌里,直至排序完。
具体算法描述如下:
-
从第一个元素开始,该元素可以认为已经被排序
-
取出下一个元素,在已经排序的元素序列中从后向前扫描
-
如果该元素(已排序)大于新元素,将该元素移到下一位置
-
重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
-
将新元素插入到该位置后
-
重复步骤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)操作。归并操作指的是将两个已经排序的序列合并成一个序列的操作,归并操作步骤如下:
-
申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
-
设定两个指针,最初位置分别为两个已经排序序列的起始位置
-
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
-
重复步骤3直到某一指针到达序列尾
-
将另一序列剩下的所有元素直接复制到合并序列尾
动态图如下:
实现代码如下:
// 归并排序,从大到小排序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)来把一个序列分为两个子序列。算法实现步骤为:
-
从序列中挑出一个元素,作为"基准"(pivot).
-
把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
-
对每个分区递归地进行步骤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语言实现排序算法的介绍,完整示例代码地址:
感谢您的阅读,如想了解更多,欢迎关注“Go键盘侠”公众号,一起交流学习哈!如果你觉得不错的话,请点个赞呗^_^。
推荐文章: