青训营X豆包MarsCode 技术训练营第三课 | 经典排序 豆包MarsCode AI 刷题

82 阅读6分钟

经典排序算法学习笔记

排序算法是计算机科学中非常基础和重要的算法之一。无论你是在做数据分析,还是在开发某个程序,排序算法都会频繁出现在你的工作中。所以,掌握一些经典的排序算法是非常必要的。

这篇笔记会从一些常见的排序算法入手,结合一些自己的理解,帮助自己更好地掌握排序算法的思想、实现方式,以及它们的优缺点。

一、冒泡排序(Bubble Sort)

冒泡排序是最基础、最直观的排序算法之一。它的核心思想是通过相邻元素的比较和交换,把最大的元素“冒泡”到序列的末尾,逐渐缩小排序的范围。

原理:

  1. 比较相邻的两个元素。如果前一个比后一个大,就交换它们。
  2. 每一次遍历结束后,最大的元素会被“冒泡”到最后。
  3. 继续进行第二轮、第三轮,直到没有需要交换的元素。

示例:

假设我们有一个数组 [5, 2, 9, 1, 5, 6],我们通过冒泡排序来排序它。

  • 第一轮遍历:比较相邻的元素,交换 52,得到 [2, 5, 9, 1, 5, 6]。继续交换,直到最大的元素 9 被移到最后。
  • 第二轮遍历:不再考虑已经排好序的 9,继续比较和交换剩下的元素。
  • 以此类推,直到数组完全排序。
func bubbleSort(arr []int) {
    n := len(arr)
    for i := 0; i < n-1; i++ {
        for j := 0; j < n-i-1; j++ {
            if arr[j] > arr[j+1] {
                arr[j], arr[j+1] = arr[j+1], arr[j]
            }
        }
    }
}

时间复杂度:

  • 最坏情况:O(n²)
  • 最好情况(已经排好序的情况下):O(n)

思考:

冒泡排序的效率不高,尤其是当数据量较大时,时间复杂度是 O(n²),这对于实际应用来说是不太理想的。不过,它的实现非常简单,适合用作初学者的入门算法。


二、选择排序(Selection Sort)

选择排序的思想是每次从未排序部分中选出最小(或最大)元素,将其放到已排序部分的末尾。这样做的好处是它通过交换来排序,不需要像冒泡排序那样做多次的元素交换。

原理:

  1. 从未排序的部分选择最小的元素,与未排序部分的第一个元素交换。
  2. 然后缩小未排序部分,重复这个过程,直到整个数组排序完成。

示例:

给定数组 [5, 2, 9, 1, 5, 6],选择排序的过程如下:

  • 第一轮:选择最小元素 1,交换 51,得到 [1, 2, 9, 5, 5, 6]
  • 第二轮:选择最小的元素 2,它已经在正确的位置,继续。
  • 以此类推,直到整个数组排好序。
func selectionSort(arr []int) {
    n := len(arr)
    for i := 0; i < n-1; i++ {
        minIdx := i
        for j := i + 1; j < n; j++ {
            if arr[j] < arr[minIdx] {
                minIdx = j
            }
        }
        arr[i], arr[minIdx] = arr[minIdx], arr[i]
    }
}

时间复杂度:

  • 最坏情况:O(n²)
  • 最好情况:O(n²)

思考:

选择排序的优势是每次交换的次数很少,最多交换 n-1 次。但它的时间复杂度仍然是 O(n²),和冒泡排序一样,所以在数据量大的时候,它的效率也是不理想的。


三、插入排序(Insertion Sort)

插入排序的思想跟打扑克牌类似,就是把每一张牌插入到已经排序好的部分中,直到整个数组排序完成。它比冒泡排序和选择排序要高效一些,特别是在数据部分已经有序的情况下。

原理:

  1. 从第二个元素开始,把当前元素与前面的已排序部分逐一比较,直到找到合适的位置插入。
  2. 每插入一个元素,已排序部分的序列会向右扩展一位。

示例:

给定数组 [5, 2, 9, 1, 5, 6],插入排序的过程如下:

  • 第一轮:2 插入到 [5] 前面,得到 [2, 5, 9, 1, 5, 6]
  • 第二轮:9 已经在正确的位置,不需要交换。
  • 第三轮:1 插入到 [2] 前面,得到 [1, 2, 5, 9, 5, 6]
  • 以此类推,直到整个数组排好序。
func insertionSort(arr []int) {
    n := len(arr)
    for i := 1; i < n; i++ {
        key := arr[i]
        j := i - 1
        for j >= 0 && arr[j] > key {
            arr[j+1] = arr[j]
            j--
        }
        arr[j+1] = key
    }
}

时间复杂度:

  • 最坏情况:O(n²)
  • 最好情况(已经排好序的情况下):O(n)

思考:

插入排序特别适用于数据量小或者数据已经部分排序的情况。在这些场景下,它能高效地工作。但在大规模数据时,O(n²)的时间复杂度仍然限制了它的应用。


四、快速排序(Quick Sort)

快速排序是分治法的经典应用。它通过一个“基准元素”将数组分成左右两个部分,然后递归地对这两个部分进行排序。快速排序的时间复杂度最优情况下是 O(n log n),在大多数情况下是非常高效的。

原理:

  1. 选择一个基准元素,将数组划分为两个子数组:左边是小于基准元素的元素,右边是大于基准元素的元素。
  2. 对左右两个子数组递归地进行排序。
  3. 最终合并结果,整个数组排好序。

示例:

给定数组 [5, 2, 9, 1, 5, 6],快速排序的过程如下:

  • 选择 5 作为基准,分成两部分 [2, 1][9, 5, 6],递归对这两部分进行排序。
  • 重复分治过程,直到每部分的长度为 1。
func quickSort(arr []int) {
    if len(arr) <= 1 {
        return
    }
    pivot := arr[len(arr)/2]
    left := []int{}
    right := []int{}
    for _, v := range arr {
        if v < pivot {
            left = append(left, v)
        } else if v > pivot {
            right = append(right, v)
        }
    }
    quickSort(left)
    quickSort(right)
    arr = append(append(left, pivot), right...)
}

时间复杂度:

  • 最坏情况:O(n²)(当基准选择不好时)
  • 最好情况:O(n log n)

思考:

快速排序在大多数情况下都非常高效,尤其是对于大规模数据集。但是,如果基准元素选择不当,可能会导致效率降低至 O(n²)。因此,选择合适的基准元素对于快速排序的性能至关重要。


五、总结

通过深入学习这些基础的排序算法,我们能够更深刻地理解排序问题。每种算法都有其独特的实现机制和适用场景。例如,冒泡排序和选择排序虽然简单但效率较低,适合作为学习排序概念的工具;而快速排序和插入排序在实际应用中往往更为高效,尤其是在处理大规模数据时。

我认为,理解这些排序算法的精髓比单纯记忆它们的代码更为重要。每种算法都有其优势和局限,能够根据实际情况选择最合适的排序算法,才是编程中的上策。