十大经典排序算法详解与实现

排序算法是计算机科学中一个非常重要的主题,它不仅在实际应用中广泛使用,也是面试中经常考察的内容。本文将详细介绍十种经典的排序算法,包括冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序、计数排序、桶排序和基数排序。每种算法都会从核心思想、算法步骤、性能分析和代码实现四个方面进行讲解。

1. 冒泡排序

核心思想

冒泡排序是一种简单的排序算法,它通过重复地遍历待排序的数列,比较相邻的两个元素,如果顺序错误就交换它们。这个过程会不断重复,直到没有需要交换的元素为止。

算法步骤

  1. 从数组的第一个元素开始,依次比较相邻的两个元素。
  2. 如果前一个元素大于后一个元素,则交换它们的位置。
  3. 每次遍历结束后,最大的元素会被移到数组的末尾。
  4. 重复上述过程,直到整个数组有序。

性能分析

  • 时间复杂度:O(n²)
  • 空间复杂度:O(1)
  • 稳定性:稳定

代码实现

package main
import "fmt"

func bubbleSort(arr []int) []int {
    for i := 0; i < len(arr)-1; i++ {
        for j := 0; j < len(arr)-i-1; j++ {
            if arr[j] > arr[j+1] {
                arr[j], arr[j+1] = arr[j+1], arr[j]
            }
        }
    }
    return arr
}

func main() {
    var a []int = []int{1, 7, 3, 4, 5}
    fmt.Println(bubbleSort(a))
}

2. 选择排序

核心思想

选择排序的核心思想是每次从未排序的部分中找到最小(或最大)的元素,将其放到已排序部分的末尾。

算法步骤

  1. 将数组分为已排序部分和未排序部分。
  2. 在未排序部分中找到最小的元素。
  3. 将找到的最小元素与未排序部分的第一个元素交换。
  4. 重复上述过程,直到整个数组有序。

性能分析

  • 时间复杂度:O(n²)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

代码实现

func selectionSort(arr []int) []int {
    n := len(arr)
    for i := 0; i < n-1; i++ {
        minIndex := i
        for j := i + 1; j < n; j++ {
            if arr[minIndex] > arr[j] {
                minIndex = j
            }
        }
        if minIndex != i {
            arr[i], arr[minIndex] = arr[minIndex], arr[i]
        }
    }
    return arr
}

3. 插入排序

核心思想

插入排序的工作方式类似于整理扑克牌。每次从未排序部分取出一个元素,插入到前面有序部分的正确位置。

算法步骤

  1. 假设数组的第一个元素已经有序。
  2. 从第二个元素开始,依次将每个元素插入到前面有序部分的正确位置。
  3. 每次插入时,从后向前比较,找到合适的位置后插入。

性能分析

  • 时间复杂度:O(n²)
  • 空间复杂度:O(1)
  • 稳定性:稳定

代码实现

func insertionSort(arr []int) []int {
    n := len(arr)
    for i := 1; i < n; i++ {
        current := arr[i]
        j := i - 1
        for j >= 0 && arr[j] > current {
            arr[j+1] = arr[j]
            j--
        }
        arr[j+1] = current
    }
    return arr
}

4. 希尔排序

核心思想

希尔排序是插入排序的改进版,通过引入“增量”来减少元素的移动次数。它将数组分成多个子数组,分别进行插入排序,然后逐渐减小增量,直到增量为1时完成排序。

算法步骤

  1. 选择一个增量序列,通常从数组长度的一半开始。
  2. 按照增量将数组分成多个子数组。
  3. 对每个子数组进行插入排序。
  4. 逐渐减小增量,重复上述过程,直到增量为1。

性能分析

  • 时间复杂度:O(n²)(最坏情况),O(n log n)(最好情况)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

代码实现

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

5. 归并排序

核心思想

归并排序是一种分治算法,它将数组分成两部分,分别排序,然后将两个有序部分合并成一个有序数组。

算法步骤

  1. 将数组分成两部分。
  2. 递归地对两部分分别进行归并排序。
  3. 将两个有序子数组合并成一个有序数组。

性能分析

  • 时间复杂度:O(n log n)
  • 空间复杂度:O(n)
  • 稳定性:稳定

代码实现

func mergeSort(arr []int) []int {
    if len(arr) <= 1 {
        return arr
    }
    mid := len(arr) / 2
    left := mergeSort(arr[:mid])
    right := mergeSort(arr[mid:])
    return merge(left, right)
}

func merge(left, right []int) []int {
    result := make([]int, 0, len(left)+len(right))
    i, j := 0, 0
    for i < len(left) && j < len(right) {
        if left[i] < right[j] {
            result = append(result, left[i])
            i++
        } else {
            result = append(result, right[j])
            j++
        }
    }
    result = append(result, left[i:]...)
    result = append(result, right[j:]...)
    return result
}

6. 快速排序

核心思想

快速排序也是一种分治算法,它通过选择一个基准值,将数组分为两部分:小于基准值的部分和大于基准值的部分,然后递归地对这两部分进行排序。

算法步骤

  1. 选择一个基准值。
  2. 将数组分为两部分:小于基准值的部分和大于基准值的部分。
  3. 递归地对两部分进行快速排序。

性能分析

  • 时间复杂度:O(n log n)(平均情况),O(n²)(最坏情况)
  • 空间复杂度:O(log n)
  • 稳定性:不稳定

代码实现

func quickSort(arr []int) []int {
    if len(arr) <= 1 {
        return arr
    }
    pivot := arr[len(arr)-1]
    left, right := 0, len(arr)-1
    for i := range arr {
        if arr[i] < pivot {
            arr[left], arr[i] = arr[i], arr[left]
            left++
        }
    }
    arr[left], arr[right] = arr[right], arr[left]
    quickSort(arr[:left])
    quickSort(arr[left+1:])
    return arr
}

7. 堆排序

核心思想

堆排序利用堆的性质,将数组构造成一个最大堆,然后每次取出堆顶元素放到数组的末尾,逐渐缩小堆的范围。

算法步骤

  1. 将数组构造成一个最大堆。
  2. 将堆顶元素与最后一个元素交换。
  3. 调整堆,使其重新满足最大堆的性质。
  4. 重复上述过程,直到整个数组有序。

性能分析

  • 时间复杂度:O(n log n)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

代码实现

func heapSort(arr []int) []int {
    n := len(arr)
    for i := n/2 - 1; i >= 0; i-- {
        heapify(arr, n, i)
    }
    for i := n - 1; i > 0; i-- {
        arr[0], arr[i] = arr[i], arr[0]
        heapify(arr, i, 0)
    }
    return arr
}

func heapify(arr []int, n, i int) {
    largest := i
    left := 2*i + 1
    right := 2*i + 2
    if left < n && arr[left] > arr[largest] {
        largest = left
    }
    if right < n && arr[right] > arr[largest] {
        largest = right
    }
    if largest != i {
        arr[i], arr[largest] = arr[largest], arr[i]
        heapify(arr, n, largest)
    }
}

8. 计数排序

核心思想

计数排序是一种非比较排序算法,它通过统计每个元素的出现次数,然后根据计数结果重构有序数组。

算法步骤

  1. 找出数组中的最大值和最小值。
  2. 创建一个计数数组,统计每个元素的出现次数。
  3. 根据计数数组的值,重构有序数组。

性能分析

  • 时间复杂度:O(n + k)(k是元素的范围)
  • 空间复杂度:O(n + k)
  • 稳定性:稳定

代码实现

func countingSort(arr []int) []int {
    max, min := arr[0], arr[0]
    for _, num := range arr {
        if num > max {
            max = num
        }
        if num < min {
            min = num
        }
    }
    count := make([]int, max - min + 1)
    for _, num := range arr {
        count[num - min]++
    }
    index := 0
    for i, cnt := range count {
        for ; cnt > 0; cnt-- {
            arr[index] = i + min
            index++
        }
    }
    return arr
}

9. 桶排序

核心思想

桶排序将数组中的元素分配到多个桶中,每个桶内部使用其他排序算法(如插入排序)进行排序,最后将所有桶中的元素合并。

算法步骤

  1. 将数组中的元素均匀分配到多个桶中。
  2. 对每个桶内部的元素进行排序。
  3. 按顺序将所有桶中的元素合并。

性能分析

  • 时间复杂度:O(n + k)(k是桶的数量)
  • 空间复杂度:O(n + k)
  • 稳定性:稳定

代码实现

func bucketSort(arr []int, bucketSize int) []int {
    if len(arr) == 0 {
        return arr
    }
    min, max := arr[0], arr[0]
    for _, num := range arr {
        if num < min {
            min = num
        }
        if num > max {
            max = num
        }
    }
    bucketCount := (max - min) / bucketSize + 1
    buckets := make([][]int, bucketCount)
    for _, num := range arr {
        index := (num - min) / bucketSize
        buckets[index] = append(buckets[index], num)
    }
    result := make([]int, 0, len(arr))
    for _, bucket := range buckets {
        insertionSort(bucket)
        result = append(result, bucket...)
    }
    return result
}

10. 基数排序

核心思想

基数排序是一种非比较排序算法,它通过逐位比较元素的每一位数字,从最低位到最高位进行排序。

算法步骤

  1. 找出数组中的最大值,确定最大的位数。
  2. 从最低位开始,逐位进行计数排序。
  3. 每次计数排序后,将数组重新排列。

性能分析

  • 时间复杂度:O(n * k)(k是元素的位数)
  • 空间复杂度:O(n + k)
  • 稳定性:稳定

代码实现

func radixSort(arr []int) []int {
    max := arr[0]
    for _, num := range arr {
        if num > max {
            max = num
        }
    }
    exp := 1
    for max/exp > 0 {
        countingSortByDigit(arr, exp)
        exp *= 10
    }
    return arr
}

func countingSortByDigit(arr []int, exp int) {
    n := len(arr)
    output := make([]int, n)
    count := make([]int, 10)
    for i := 0; i < n; i++ {
        index := (arr[i] / exp) % 10
        count[index]++
    }
    for i := 1; i < 10; i++ {
        count[i] += count[i-1]
    }
    for i := n - 1; i >= 0; i-- {
        index := (arr[i] / exp) % 10
        output[count[index]-1] = arr[i]
        count[index]--
    }
    for i := 0; i < n; i++ {
        arr[i] = output[i]
    }
}

总结

以上就是十种经典的排序算法的详细讲解和实现。每种算法都有其适用的场景和优缺点,选择合适的排序算法需要根据具体的问题和数据特性来决定。希望本文能帮助你更好地理解和掌握这些排序算法!

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐