冒泡排序
算法介绍
现在有一堆乱序的数,比如:5 9 1 6 8 14 6 49 25 4 6 3
。
第一轮迭代:从第一个数开始,依次比较相邻的两个数,如果前面一个数比后面一个数大,那么交换位置,直到处理到最后一个数,最后的这个数是最大的。
第二轮迭代:因为最后一个数已经是最大了,现在重复第一轮迭代的操作,但是只处理到倒数第二个数。
第三轮迭代:因为最后一个数已经是最大了,最后第二个数是次大的,现在重复第一轮迭代的操作,但是只处理到倒数第三个数。
第N轮迭代:….
经过交换,最后的结果为:1 3 4 5 6 6 6 8 9 14 25 49
,我们可以看到已经排好序了。
因为小的元素会慢慢地浮到顶端,很像碳酸饮料的汽泡,会冒上去,所以这就是冒泡排序取名的来源。
列如对 [4,2,9,1]排序:
[]表示排好序 {}表示比较后交换的结果
第一轮开始: 4 2 9 1 从第一个数开始,4 比 2 大,交换 4,2
第一轮: {2 4} 9 1 接着 4 比 9 小,不交换
第一轮: 2 {4 9} 1 接着 9 比 1 大,交换 9,1
第一轮: 2 4 {1 9} 已经到底,结束
第一轮结果: 2 4 1 [9]第二轮开始:2 4 1 [9] 从第一个数开始,2 比 4 小,不交换
第二轮: {2 4} 1 [9] 接着 4 比 1 大,交换 4,1
第二轮: 2 {1 4} [9] 已经到底,结束
第二轮结果: 2 1 [4 9]第三轮开始:2 1 [4 9] 从第一个数开始,2 比 1 大,交换 2,1
第三轮: (1 2} [4 9] 已经到底,结束
第三轮结果: 1 [2 4 9]结果: [1 2 4 9]
算法实现
package main
import "fmt"
func BubbleSort(list []int) {
n := len(list)
// 在一轮中有没有交换过
didSwap := false
// 进行 N-1 轮迭代
for i := n - 1; i > 0; i-- {
// 每次从第一位开始比较,比较到第 i 位就不比较了,因为前一轮该位已经有序了
for j := 0; j < i; j++ {
// 如果前面的数比后面的大,那么交换
if list[j] > list[j+1] {
list[j], list[j+1] = list[j+1], list[j]
didSwap = true
}
}
// 如果在一轮中没有交换过,那么已经排好序了,直接返回
if !didSwap {
return
}
}
}
func main() {
list := []int{5, 9, 1, 6, 8, 14, 6, 49, 25, 4, 6, 3}
BubbleSort(list)
fmt.Println(list)
}
时间复杂度
比较次数:1 + 2 + 3 + ... + (N-1) = (N^2 - N)/2
,是一个平方级别的时间复杂度,我们可以记为:O(n^2)
。
交换次数:如果数列在有序的状态下进行冒泡排序,也就是最好情况下,那么交换次数为0,而如果完全乱序,最坏情况下那么交换的次数和比较的次数一样多。
冒泡排序交换和比较的次数相加是一个和 N
有关的平方数,所以冒泡排序的最好和最差时间复杂度都是:O(n^2)
。
冒泡排序是稳定的
选择排序
算法介绍
选择排序,一般我们指的是简单选择排序,也可以叫直接选择排序,它不像冒泡排序一样相邻地交换元素,而是通过选择最小的元素,每轮迭代只需交换一次。虽然交换次数比冒泡少很多,但效率和冒泡排序一样的糟糕。
选择排序属于选择类排序算法。
我打扑克牌的时候,会习惯性地从左到右扫描,然后将最小的牌放在最左边,然后从第二张牌开始继续从左到右扫描第二小的牌,放在最小的牌右边,以此反复。选择排序和我玩扑克时的排序特别相似。
举个简单例子,选择排序一个 4 个元素的数列:4 2 9 1
:
[]表示排好序
起始: 4 2 9 1 未排序数列从左扫描最小的数是 1,与第一个元素 4 交换,交换 1,4
一轮: [1] 2 9 4 未排序数列从左扫描最小的数是 2,不需要交换
二轮: [1 2] 9 4 未排序数列从左扫描最小的数是 4,与第三个元素 9 交换,交换 4,9
三轮: [1 2 4] 9 未排序数列只有 1 个数,结束
结果: [1 2 4 9]
算法实现
package main
import "fmt"
func SelectSort(list []int) {
n := len(list)
// 进行 N-1 轮迭代
for i := 0; i < n-1; i++ {
// 每次从第 i 位开始,找到最小的元素
min := list[i] // 最小数
minIndex := i // 最小数的下标
for j := i + 1; j < n; j++ {
if list[j] < min {
// 如果找到的数比上次的还小,那么最小的数变为它
min = list[j]
minIndex = j
}
}
// 这一轮找到的最小数的下标不等于最开始的下标,交换元素
if i != minIndex {
list[i], list[minIndex] = list[minIndex], list[i]
}
}
}
func main() {
list := []int{5, 9, 1, 6, 8, 14, 6, 49, 25, 4, 6, 3}
SelectSort(list)
fmt.Println(list)
}
复杂度
比较的次数和冒泡排序一样多,因为扫描过程也是比较的过程,只不过交换的次数减少为每轮 1 次。最佳和最坏时间复杂度仍然是:O(n^2)
选择排序是不稳定排序
插入排序
算法介绍
插入排序,一般我们指的是简单插入排序,也可以叫直接插入排序。就是说,每次把一个数插到已经排好序的数列里面形成新的排好序的数列,以此反复。
插入排序属于插入类排序算法。
除了我以外,有些人打扑克时习惯从第二张牌开始,和第一张牌比较,第二张牌如果比第一张牌小那么插入到第一张牌前面,这样前两张牌都排好序了,接着从第三张牌开始,将它插入到已排好序的前两张牌里,形成三张排好序的牌,后面第四张牌继续插入到前面已排好序的三张牌里,直至排序完。
举例; [4,2,9,1]
[]表示排好序
第一轮: [4] 2 9 1 拿待排序的第二个数 2,插入到排好序的数列 [4]
与排好序的数列 [4] 比较
第一轮进行中:2 比 4 小,插入到 4 前
第二轮: [2 4] 9 1 拿待排序的第三个数 9,插入到排好序的数列 [2 4]
与排好序的数列 [2 4] 比较
第二轮进行中: 9 比 4 大,不变化
第三轮: [2 4 9] 1 拿待排序的第四个数 1,插入到排好序的数列 [2 4 9]
与排好序的数列 [2 4 9] 比较
第三轮进行中: 1 比 9 小,插入到 9 前
第三轮进行中: 1 比 4 小,插入到 4 前
第三轮进行中: 1 比 2 小,插入到 2 前
结果: [1 2 4 9]
算法实现
package main
import "fmt"
func InsertSort(nums []int) {
for i:=1;i<len(nums);i++{
j:=i-1
key:=nums[i] //待排序的数
for ;j>=0;j--{
if nums[j]>key{
nums[j+1]=nums[j] //比待排序的数大的就往后移
}else{
break
}
}
nums[j+1]=key
}
}
func main() {
list := []int{5, 9, 1, 6, 8, 14, 6, 49, 25, 4, 6, 3}
InsertSort(list)
fmt.Println(list)
}
复杂度
最好情况下,对一个已经排好序的数列进行插入排序,那么需要迭代 N-1
轮,并且因为每轮第一次比较,待排序的数就比它左边的数大,那么这一轮就结束了,不需要再比较了,也不需要交换,这样时间复杂度为:O(n)
。
最坏情况下,每一轮比较,待排序的数都比左边排好序的所有数小,那么需要交换 N-1
次,第一轮需要比较和交换一次,第二轮需要比较和交换两次,第三轮要三次,第四轮要四次,这样次数是:1 + 2 + 3 + 4 + ... + N-1
,时间复杂度和冒泡排序、选择排序一样,都是:O(n^2)
。
因为是从右到左,将一个个未排序的数,插入到左边已排好序的队列中,所以插入排序,相同的数在排序后顺序不会变化,这个排序算法是稳定的。
快速排序
算法介绍
快速排序通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
步骤:
- 先从数列中取出一个数作为基准数。一般取第一个数。
- 分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
- 再对左右区间重复第二步,直到各区间只有一个数。
举一个例子:5 9 1 6 8 14 6 49 25 4 6 3
一般取第一个数 5 作为基准,从它左边和最后一个数使用[]进行标志,
如果左边的数比基准数大,那么该数要往右边扔,也就是两个[]数交换,这样大于它的数就在右边了,然后右边[]数左移,否则左边[]数右移。
5 [9] 1 6 8 14 6 49 25 4 6 [3] 因为 9 > 5,两个[]交换位置后,右边[]左移
5 [3] 1 6 8 14 6 49 25 4 [6] 9 因为 3 !> 5,两个[]不需要交换,左边[]右移
5 3 [1] 6 8 14 6 49 25 4 [6] 9 因为 1 !> 5,两个[]不需要交换,左边[]右移
5 3 1 [6] 8 14 6 49 25 4 [6] 9 因为 6 > 5,两个[]交换位置后,右边[]左移
5 3 1 [6] 8 14 6 49 25 [4] 6 9 因为 6 > 5,两个[]交换位置后,右边[]左移
5 3 1 [4] 8 14 6 49 [25] 6 6 9 因为 4 !> 5,两个[]不需要交换,左边[]右移
5 3 1 4 [8] 14 6 49 [25] 6 6 9 因为 8 > 5,两个[]交换位置后,右边[]左移
5 3 1 4 [25] 14 6 [49] 8 6 6 9 因为 25 > 5,两个[]交换位置后,右边[]左移
5 3 1 4 [49] 14 [6] 25 8 6 6 9 因为 49 > 5,两个[]交换位置后,右边[]左移
5 3 1 4 [6] [14] 49 25 8 6 6 9 因为 6 > 5,两个[]交换位置后,右边[]左移
5 3 1 4 [14] 6 49 25 8 6 6 9 两个[]已经汇总,因为 14 > 5,所以 5 和[]之前的数 4 交换位置
第一轮切分结果:4 3 1 5 14 6 49 25 8 6 6 9现在第一轮快速排序已经将数列分成两个部分:
4 3 1 和 14 6 49 25 8 6 6 9
左边的数列都小于 5,右边的数列都大于 5。
使用递归分别对两个数列进行快速排序。
算法实现
package main
import "fmt"
// 普通快速排序
func QuickSort(array []int, begin, end int) {
if begin < end {
// 进行切分
loc := partition(array, begin, end)
// 对左部分进行快排
QuickSort(array, begin, loc-1)
// 对右部分进行快排
QuickSort(array, loc+1, end)
}
}
// 切分函数,并返回切分元素的下标
func partition(array []int, begin, end int) int {
i := begin + 1 // 将array[begin]作为基准数,因此从array[begin+1]开始与基准数比较!
j := end // array[end]是数组的最后一位
// 没重合之前
for i < j {
if array[i] > array[begin] {
array[i], array[j] = array[j], array[i] // 交换
j--
} else {
i++
}
}
/* 跳出while循环后,i = j。
* 此时数组被分割成两个部分 --> array[begin+1] ~ array[i-1] < array[begin]
* --> array[i+1] ~ array[end] > array[begin]
* 这个时候将数组array分成两个部分,再将array[i]与array[begin]进行比较,决定array[i]的位置。
* 最后将array[i]与array[begin]交换,进行两个分割部分的排序!以此类推,直到最后i = j不满足条件就退出!
*/
if array[i] >= array[begin] { // 这里必须要取等“>=”,否则数组元素由相同的值组成时,会出现错误!
i--
}
array[begin], array[i] = array[i], array[begin]
return i
}
func main() {
list := []int{5, 9, 1, 6, 8, 14, 6, 49, 25, 4, 6, 3}
QuickSort(list, 0, len(list)-1)
fmt.Println(list)
}
复杂度
快速排序主要靠基准数进行切分,将数列分成两部分,一部分比基准数都小,一部分比基准数都大。
在最好情况下,每一轮都能平均切分,这样遍历元素只要 n/2
次就可以把数列分成两部分,每一轮的时间复杂度都是:O(n)
。因为问题规模每次被折半,折半的数列继续递归进行切分,也就是总的时间复杂度计算公式为: T(n) = 2*T(n/2) + O(n)
。按照主定理公式计算,我们可以知道时间复杂度为:O(nlogn)
,当然我们可以来具体计算一下:
我们来分析最好情况,每次切分遍历元素的次数为 n/2
T(n) = 2*T(n/2) + n/2
T(n/2) = 2*T(n/4) + n/4
T(n/4) = 2*T(n/8) + n/8
T(n/8) = 2*T(n/16) + n/16
...
T(4) = 2*T(2) + 4
T(2) = 2*T(1) + 2
T(1) = 1进行合并也就是:
T(n) = 2*T(n/2) + n/2
= 2^2*T(n/4)+ n/2 + n/2
= 2^3*T(n/8) + n/2 + n/2 + n/2
= 2^4*T(n/16) + n/2 + n/2 + n/2 + n/2
= ...
= 2^logn*T(1) + logn * n/2
= 2^logn + 1/2*nlogn
= n + 1/2*nlogn因为当问题规模 n 趋于无穷大时 nlogn 比 n 大,所以 T(n) = O(nlogn)。
最好时间复杂度为:O(nlogn)。
最差的情况下,每次都不能平均地切分,每次切分都因为基准数是最大的或者最小的,不能分成两个数列,这样时间复杂度变为了 T(n) = T(n-1) + O(n)
,按照主定理计算可以知道时间复杂度为:O(n^2)
,我们可以来实际计算一下:
我们来分析最差情况,每次切分遍历元素的次数为 n
T(n) = T(n-1) + n
= T(n-2) + n-1 + n
= T(n-3) + n-2 + n-1 + n
= ...
= T(1) + 2 +3 + ... + n-2 + n-1 + n
= O(n^2)最差时间复杂度为:O(n^2)。
快速排序是不稳定的,因为切分过程中进行了交换,相同值的元素可能发生位置变化。