一、什么是排序算法?
生活中经常有需要我们排序的栗子,比如说成绩排名,按照分数从高到底排序,对绝大多数程序员来说,排序算法可能是学过的第一种算法,但是不同的排序算法是存在差异的,有的算法花费时间很长,有的算法花费空间很多,就好比我们的代码质量,不同的算法得到的程序性能是不一样的。因此我们要学习下优秀的算法,用一种更合适的方式去处理问题。
二、如何分析排序算法的好坏?
主要是有以下几类指标。
2.1 排序算法的执行效率
2.1.1 最好情况
指的是在一个算法在最好的情况下的执行效率,比如说要排序的数组刚好是已经有序的,要吃饭的时候刚好外卖到了。当然这是理想状态下,不可强求。
2.1.2 最坏情况
指的是在最糟糕的情况下,执行这段代码的时间复杂度,比如要排序的数组刚好都是反序的,赚到钱的时候,没m花了。
2.2.3 平均情况时间复杂度
数据在上述两种状况下的概率并不高,为表示平均情况下的时间复杂度,引入了平均时间复杂度的概念。
2.2.4 时间复杂度的系数、常数 、低阶
因为时间复杂度反映的是数据规模 n 很大的时候的一个增长趋势,所以它表示的时候会忽略系数、常数、低阶。
但是实际的软件开发中,我们排序的规模可能很小,在这种情况下,这些因素所造成的影响比较大,所以,在对同一阶时间复杂度的排序算法性能对比的时候,我们就要把系数、常数、低阶也考虑进来
2.2.5 比较次数和交换(或移动)次数
一般在基于比较的排序算法的执行过程,主要会涉及两种操作,一种是元素比较大小,另一种是元素交换或移动。所以,如果我们在分析排序算法的执行效率的时候,应该把比较次数和交换(或移动)次数也考虑进去,因为这些操作也会影响一定的性能。
因为时间复杂度这块并不是本篇文章的重点,可以移步百度自行深入了解。
2.2 排序算法的内存消耗
主要是指空间复杂度,对于同一个目标,不同的算法所占用的内存是不同的,占用的内存越多,空间复杂度越高,程序的性价比就越不高,原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。
2.3 排序算法的稳定性
如果说待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变,那我们就说这排序算法是稳定的排序算法。
例如说,数组 [1,5,7,5],经过排序后得到的结果[1,5,5,7] , 第一个5和第二个5 的先后顺序还是不变,那就是稳定的。
三、常见的排序算法有哪些?
最常用的算法主要有:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序
这几种算法的区别主要是
算法类型 | 时间复杂度 | 是否基于比较 |
---|---|---|
冒泡、插入、排序 | O(n^2) | 是 |
快排、归并 | O(nlogn) | 是 |
桶、计数、基数 | O(n) | 否 |
四、如何用 Go 实现排序算法
这里先实现 冒泡、插入、排序这三种比较简单的算法。(其实是其他的我还没写好)
话不多说。上代码。
4.1 冒泡排序
算法:每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换,一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。
show code time
func bubblingSort(arr [8]int) [8]int {
count := len(arr)
for i := 0; i < len(arr); i++ {
for j := 0; j < count-1; j++ {
if arr[j] > arr[j+1] { // 判断大小决定是否替换
temp := arr[j+1]
arr[j+1] = arr[j]
arr[j] = temp
}
}
count-- //这是最关键的,每次处理后,最大的元素都会在最右边,因此后续要处理的数组长度均减一
}
return arr
}
4.2 插入排序
算法:插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束
func insertionSort(arr [8]int) [8]int {
//[3, 6, 7], [5, 1, 2, 10, 4]
//[3, 5, 6, 7],[1, 2, 10, 4]
//[1,3, 5, 6, 7],[2, 10, 4]
// 假设第一个元素已经是已排序区间的第一个值
hasSortNum := 1 // arr[hasSortNum] 就是新的要处理的元素, hasSortNum 刚好是未排序区间的第一个元素下标
for hasSortNum < len(arr) { // 说明都排完了
// 遍历一下已排序区间,插入到合适的位置,并且搬运数据
for k, _ := range arr[:hasSortNum] {
// 找到要插入的位置
if arr[k] > arr[hasSortNum] { // 此时 arr[k]就是要插入的位置
// 搬运数据,需要将 k 位置给插入的元素,其他元素都后移,直到arr[hasSortNum]元素
// k=2 hasSortNum=3 需要搬运2个位置
// k=1 hasSortNum=4,需要搬运4个位置 (hasSortNum-k+1)
// 先保留要被代替的元素
temp := arr[hasSortNum]
for i := hasSortNum; i > k; i-- { // k+1 到 hasSortNum 位置都要动
arr[i] = arr[i-1]
}
arr[k] = temp
}
}
hasSortNum++
}
return arr
}
4.3 选择排序
算法:选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
func selectionSort(arr [8]int) [8]int {
//[3, 6, 7, 5, 1, 2, 10, 4]
//[1, 6, 7, 5 , 3, 2, 10, 4]
hasSortNum := 0 //一开始已排区间为空
for hasSortNum < len(arr) {
minValue := arr[hasSortNum] // 取未排第一个元素作为比较值
minValueIndex := hasSortNum
for i := hasSortNum + 1; i < len(arr); i++ {
// 遍历未排序区间,找出最小值
if arr[i] < minValue {
minValue = arr[i]
minValueIndex = i
}
}
// 替换两个的位置
temp := arr[hasSortNum]
arr[hasSortNum] = minValue
arr[minValueIndex] = temp
hasSortNum++
}
return arr
}
五、小结
本篇文章主要讲述了如何分析排序算法的好坏,以及几种常见排序算法(冒泡,插入、排序)的Go写法,排序算法是算法的基础,在日常的工作中,很多框架的工具包虽然已经封装过,但是了解它才能正确的使用它,这三种算法实际上在生产中比较少用,因为复杂度相对较高,后面我们再讲下性能更高的快排和归并排序。