【Go实现】七种常见排序算法丨快速排序丨归并排序丨堆排序

284 阅读4分钟

1 全文摘要

本文介绍了七种常见的排序算法,包括原理解释、时间/空间复杂度分析、稳定性等,并基于Go语言给出了代码实现。

2 算法介绍

image.png

2.1 总体描述

2.1.1性能指标定义

时间复杂度是指执行算法所需要的计算工作量

空间复杂度是指执行这个算法所需要的内存空间。

排序算法稳定性是指假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,则称这种是稳定的;否则称为不稳定的。注意:改变相同元素的相对位置是一种不稳定的表现,因为在某些应用场景下,我们需要保持相同元素的相对位置不变。

2.1.2 各排序算法的性能分析

  1. 插入排序
    • 时间复杂度:最好情况下为 O(n),最坏情况下为 O(n^2),平均情况下为 O(n^2)。
    • 空间复杂度:O(1)。
    • 稳定性:稳定。
  2. 希尔排序
    • 时间复杂度:最好情况下为 O(n),最坏情况下为 O(n^2),平均情况下为 O(nlogn) 或 O(n^(3/2)),具体取决于间隔序列的选择。
    • 空间复杂度:O(1)。
    • 稳定性:不稳定。
  3. 选择排序
    • 时间复杂度:最好情况下为 O(n^2),最坏情况下为 O(n^2),平均情况下为 O(n^2)。
    • 空间复杂度:O(1)。
    • 稳定性:不稳定。
  4. 堆排序
    • 时间复杂度:最好情况下为 O(nlogn),最坏情况下为 O(nlogn),平均情况下为 O(nlogn)。
    • 空间复杂度:O(1)。
    • 稳定性:不稳定。
  5. 冒泡排序
    • 时间复杂度:最好情况下为 O(n),最坏情况下为 O(n^2),平均情况下为 O(n^2)。
    • 空间复杂度:O(1)。
    • 稳定性:稳定。
  6. 快速排序
    • 时间复杂度:最好情况下为 O(nlogn),最坏情况下为 O(n^2),平均情况下为 O(nlogn)。
    • 空间复杂度:最好情况下为 O(logn),最坏情况下为 O(n),平均情况下为 O(logn)。
    • 稳定性:不稳定。
  7. 归并排序
    • 时间复杂度:最好情况下为 O(nlogn),最坏情况下为 O(nlogn),平均情况下为 O(nlogn)。
    • 空间复杂度:最好情况下为 O(n),最坏情况下为 O(n),平均情况下为 O(n)。
    • 稳定性:稳定。

2.2 具体介绍

2.2.1 插入排序

2.2.1.1 直接插入排序

基本思想:遍历待检查的数据,一个一个地插入合适的位置。

可以具象化理解为:扑克牌一张一张地取新牌并不断插入到已排好的序列中的合适位置

结合代码实现,可以很好理解:

func insertionSort(arr []int) {  
    n := len(arr) //插入排序:可理解为用插入方法整理扑克牌  
    for i := 1; i < n; i++ {  
        key := arr[i] //每次新拿一张牌  
        j := i - 1 //在j及其之前有序(已排好),找合适的位置插入,从当后往前检查  
        for j >= 0 && arr[j] > key {  
            arr[j+1] = arr[j] //把大于key的值往后移  
            j-- //从后往前检查  
        }  
        arr[j+1] = key //把key插到合适的位置  
    }  
}

2.2.1.2 希尔排序

希尔排序是基于插入排序的一种改进版本,又称缩小增量法

基本思想:选定一个整数gap,把所有距离为gap的值分在同一组内,并对每一组内的数据进行插入排序。然后改变 gap大小 ,重复上述分组和排序的工作。当 gap 到达 1 时,所有记录在同一组内排好序。

实现图解:

image.png

代码实现:

func shellSort(arr []int) {  
    n := len(arr)  
    gap := n / 2  
    for gap > 0 {  
        for i := gap; i < n; i++ {  
            key := arr[i]  
            j := i - gap  
            for j >= 0 && arr[j] > key {  
                arr[j+gap] = arr[j]  
                j -= gap  
            }  
            arr[j+gap] = key  
        }  
    gap /= 2  
    }  
}

2.2.2 选择排序

2.2.2.1 选择排序

2.2.2.2 堆排序

2.2.3 交换排序

2.2.3.1 冒泡排序

d79cfca6000a704d4d89b87b3abefdaa.gif

基本流程:每轮从序列的前面开始向后遍历(冒泡),相邻两两比较,大的置后,则较大值会一直往后“沉”,直至遇上更大的值,显然每一轮的操作都能使一个当前最大值“沉底”,沉底后位置即确定,可不参与下一轮的排序。

依照上述的流程分析,显然有:

  1. 每轮能确保1个当前最大元素“沉底”。
    • 那么,对于n个元素的序列,只需“沉底”n-1轮即可实现排序(最后一个元素只能沉在原地)。
  2. “沉底后”无需再参与下次排序
    • 每轮需要参与排序的范围不断缩小。(每轮减一)

代码实现:

func bubbleSort(arr []int) {  
    n := len(arr)  
    for i := 0; i < n-1; i++ { //每一轮沉底一个元素,需要n-1轮(最后一个元素只能沉在原地)。  
        for j := 0; j < n-i-1; j++ { //需要排序的范围不断缩小(沉底数在增多)  
            if arr[j] > arr[j+1] { //较大的元素往后沉  
                arr[j], arr[j+1] = arr[j+1], arr[j]  
            }  
        }  
    }  
}

2.2.3.2 快速排序

代码实现:

func quickSort(arr []int) {  
    if len(arr) <= 1 {//递归的停止条件,一定要写!  
        return  
    }  
    left, right := 0, len(arr)-1  
    pivot := arr[left] //用于比较的轴  
    //保证:小于轴的都放轴左边,大于轴的都放轴右边(但不管两边内部的顺序如何)  
    for left < right { //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 //此时left游历到靠中部(轴的位置)  
    quickSort(arr[:left])  
    quickSort(arr[left+1:])  
}

2.2.4 归并排序