十大经典排序算法
冒泡排序(Bubble Sort)是基于交换的排序,每次遍历需要排序的元素,依次比较相邻的两个元素的大小,如果前一个元素大于后一个元素则两者交换,保证最后一个数字一定是最大的(假设按照从小到大排序),即最后一个元素已经排好序,下一轮只需要保证前面 n-1 个元素的顺序即可。
冒泡排序算法是稳定的,它不会出现两个相等的元素,后面的元素被交换到前面去的情况.
package main
import "fmt"
func main() {
fmt.Println("开始排序")
arr := []int{2,7,3,1}
res := sort(arr)
fmt.Println(res)
}
func sort(arr []int) []int {
l := len(arr)
for i:= 0; i < l-1 ; i++ {
for j:=0; j < l - i - 1; j++ {
if arr[j] > arr[j+1] {
arr[j],arr[j+1] = arr[j+1],arr[j]
}
}
}
return arr
}
选择排序就是每次选择剩下的元素中最小的那个元素,与当前索引位置的元素交换,直到所有的索引位置都选择完成。
由于选择排序只会选择最小的元素进行交换,如果我们可以保证我们每次选择到的最小元素是第一次出现的(就算后面出现大小相等的元素我们也不会选择后面的),那么就可以保证它的稳定性,所以选择排序是可以做到稳定的。
package main
import "fmt"
func main() {
fmt.Println("开始排序")
arr := []int{2,7,3,1}
res := sort(arr)
fmt.Println(res)
}
func sort(arr []int) []int {
l := len(arr)
for i:=0 ; i < l - 1 ; i++ {
min := i
for j:=i+1 ; j < l ; j++ {
if arr[j] < arr[min] {
min = j
}
}
arr[i],arr[min] = arr[min],arr[i]
}
return arr
}
插入排序是依次选择一个元素,插入到前面已经排好序的数组中间,确保它处于正确的位置,当然,这是需要已经排好的顺序数组不断移动。
个人感觉插入还是有点容易迷糊的,只要记住就是当在当前要插入的数据的地方之前的数组已经排好序了,这样就容易理解了。
由于插入排序只会选择元素插入到适合的位置,只要我们按照原来的顺序遍历,即使相等的两个元素最后排完顺序之后,也会保持原有的相对顺序,所以插入排序是稳定的。
package main
import "fmt"
func main() {
fmt.Println("开始排序")
arr := []int{2,7,3,1}
res := sort(arr)
fmt.Println(res)
}
func sort(arr []int) []int {
l := len(arr)
for i:=1; i < l; i++ {
index := i - 1
temp := arr[i]
for index >= 0 && temp < arr[index] {
arr[index+1] = arr[index]
index--
}
arr[index+1] = temp
}
return arr
}
希尔排序(Shell's Sort)又称“缩小增量排序”(Diminishing Increment Sort),是插入排序的一种更高效的改进版本,同时该算法是首次冲破 O(n^2n2) 的算法之一。
插入排序的痛点在于不管是否是大部分有序,都会对元素进行比较,如果最小数在数组末尾,想要把它移动到数组的头部是比较费劲的。希尔排序是在数组中采用跳跃式分组,按照某个增量 gap 进行分组,分为若干组,每一组分别进行插入排序。再逐步将增量 gap 缩小,再每一组进行插入排序,循环这个过程,直到增量为 1。
其实希尔排序也就是在插入排序的基础上进行了修改。由于希尔排序,在分组的时候,会将后面的元素间隔性的调动到前面,所以会打乱原本两个相等的数之间的相对顺序,所以希尔排序是不稳定的排序算法。
package main
import "fmt"
func main() {
fmt.Println("开始排序")
arr := []int{2,7,3,1}
res := sort(arr)
fmt.Println(res)
}
func sort(arr []int) []int {
l := len(arr)
for i := l/2; i > 0 ; i/=2 { //这里是分组
for j := i ; j < l; j++ {
m := j
temp := arr[m]
if arr[m] < arr[m-i] {
for m-i >= 0 && temp < arr[m-i] {
arr[m] = arr[m-i]
m -=i
}
arr[m] = temp
}
}
}
return arr
}
快速排序选择数组的一个数作为基准数,一趟排序,将数组分割成为两部分,一部分均小于/等于基准数,另外一部分大于/等于基准数。然后分别对基准数的左右两部分继续排序,直到数组有序。这体现了分而治之的思想,其中还应用到挖坑填数的策略。
最差的情况是 O(n^2n2),平均时间复杂度为 nlogn,空间复杂度,虽然快排本身没有申请额外的空间,但是递归需要使用栈空间,递归数的深度是 log2n,空间复杂度也就是 log2n。
由于快速排序会将一个数大间隔的移动到一边,大的数放在右边,小的数放在左边,所以会破坏两个相等的元素的相对顺序,所以它是不稳定的排序算法。
func getMid(arr []int,left int,right int) int {
pivot := arr[left]
for left < right {
for left < right && arr[right] >= pivot {
right--
}
arr[left] = arr[right]
for left < right && arr[left] <= pivot {
left++
}
arr[right] = arr[left]
}
arr[left] = pivot
return left
}
func QuickSort(arr []int,left int,right int) {
mid := getMid(arr,left,right)
if left < mid - 1 {
QuickSort(arr,left,mid - 1)
}
if right > mid + 1 {
QuickSort(arr,mid + 1,right)
}
}
归并排序
归并的总体思想是先将数组分割,再分割 ... 分割到一个元素,然后再两两归并排序,做到局部有序,不断地归并,直到数组又被全部合起来。
最后的排序复杂度为 nlog2n,不存在好坏的情况,因为每一层的合并,都是遍历完整个数组,也就是 n,一共有 log2n 层,但是代价就是需要申请额外的空间,申请空间的大小最大为 n,所以空间复杂度为 O(n)。由于归并排序只会在相邻的子数组做合并操作,而且是严格按照从左到右的顺序,不会出现跳跃交换的情况,所以归并算法是稳定的排序算法。
//归
func mergeSort(arr []int) []int {
if len(arr) < 2 {
return arr
}
mid := len(arr) / 2
left := mergeSort(arr[:mid])
right := mergeSort(arr[mid:])
result := merge(left, right)
return result
}
//并
func merge(left, right []int) []int {
fmt.Println(left, right)
temp := make([]int, 0)
i, j := 0, 0
for i < len(left) && j < len(right) {
if left[i] <= right[j] {
temp = append(temp, left[i])
i++
} else {
temp = append(temp, right[j])
j++
}
}
if i < len(left) {
temp = append(temp, left[i:]...)
}
if j < len(right) {
temp = append(temp, right[j:]...)
}
return temp
}