⚡ 排序算法大全:从冒泡到快排,面试必考!

42 阅读8分钟

"排序就像整理书架,方法不同,效率大不同!" 📚


📊 排序算法全景图

排序算法家族:

比较排序(O(n log n)及以上):
├─ 冒泡排序 🐌 O(n²)
├─ 选择排序 🐌 O(n²)
├─ 插入排序 🐌 O(n²)
├─ 希尔排序 🚗 O(n^1.3)
├─ 归并排序 ⚡ O(n log n)
├─ 快速排序 ⚡ O(n log n)
└─ 堆排序   ⚡ O(n log n)

非比较排序(线性时间):
├─ 计数排序 ⚡ O(n+k)
├─ 桶排序   ⚡ O(n+k)
└─ 基数排序 ⚡ O(n×k)

🐌 冒泡排序(Bubble Sort)

原理

像气泡一样,大的往上冒!

1轮:比较相邻元素,大的往后移
[5, 2, 8, 1, 9]
 ↓  ↓
[2, 5, 8, 1, 9]  (25比较,交换)
    ↓  ↓
[2, 5, 8, 1, 9]  (58比较,不交换)
       ↓  ↓
[2, 5, 1, 8, 9]  (81比较,交换)
          ↓  ↓
[2, 5, 1, 8, 9]  (89比较,不交换)

第1轮结果:最大的9"冒"到了最后!✅

代码实现

public void bubbleSort(int[] arr) {
    int n = arr.length;
    
    for (int i = 0; i < n - 1; i++) {
        boolean swapped = false;
        
        // 每轮把最大的"冒泡"到后面
        for (int j = 0; j < n - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                // 交换
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                swapped = true;
            }
        }
        
        // 如果一轮没有交换,说明已经有序
        if (!swapped) break;
    }
}

时间复杂度

  • 最好:O(n) - 已经有序
  • 平均/最坏:O(n²)

空间复杂度:O(1)

稳定性:✅ 稳定


🎯 选择排序(Selection Sort)

原理

每次选出最小的,放到前面!

[5, 2, 8, 1, 9]
 ↑找最小的→ 1

[1, 2, 8, 5, 9]
    ↑找最小的→ 2

[1, 2, 8, 5, 9]
       ↑找最小的→ 5

[1, 2, 5, 8, 9]
          ↑找最小的→ 8

[1, 2, 5, 8, 9] ✅

代码实现

public void selectionSort(int[] arr) {
    int n = arr.length;
    
    for (int i = 0; i < n - 1; i++) {
        int minIndex = i;
        
        // 找到最小元素的索引
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        
        // 交换
        int temp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }
}

时间复杂度:O(n²)

空间复杂度:O(1)

稳定性:❌ 不稳定


📖 插入排序(Insertion Sort)

原理

像打扑克牌,新牌插入到合适位置!

初始:[5, 2, 8, 1, 9]
       ↑
     已排序

步骤1:插入2
[5, 2, 8, 1, 9]
 ↓  ↑
[2, 5, 8, 1, 9]

步骤2:插入8
[2, 5, 8, 1, 9]  (8已经在正确位置)

步骤3:插入1
[2, 5, 8, 1, 9]
 ←←←  ↑
[1, 2, 5, 8, 9]

步骤4:插入9
[1, 2, 5, 8, 9]

代码实现

public void insertionSort(int[] arr) {
    int n = arr.length;
    
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;
        
        // 把大于key的元素往后移
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        
        arr[j + 1] = key;
    }
}

时间复杂度

  • 最好:O(n) - 已经有序
  • 平均/最坏:O(n²)

空间复杂度:O(1)

稳定性:✅ 稳定

适用场景:小规模数据或基本有序的数据


⚡ 快速排序(Quick Sort)⭐⭐⭐⭐⭐

原理

分而治之!选一个基准,左边比它小,右边比它大!

[5, 2, 8, 1, 9, 3]
 ↑基准(pivot)

分区:
[2, 1, 3] < 5 < [8, 9]

递归排序:
[1, 2, 3]    5    [8, 9]

结果:
[1, 2, 3, 5, 8, 9]

代码实现

public void quickSort(int[] arr, int low, int high) {
    if (low < high) {
        int pivotIndex = partition(arr, low, high);
        quickSort(arr, low, pivotIndex - 1);   // 排序左半部分
        quickSort(arr, pivotIndex + 1, high);  // 排序右半部分
    }
}

private int partition(int[] arr, int low, int high) {
    int pivot = arr[high];  // 选最后一个元素作为基准
    int i = low - 1;
    
    for (int j = low; j < high; j++) {
        if (arr[j] < pivot) {
            i++;
            // 交换arr[i]和arr[j]
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    
    // 把基准放到正确位置
    int temp = arr[i + 1];
    arr[i + 1] = arr[high];
    arr[high] = temp;
    
    return i + 1;
}

// 调用
quickSort(arr, 0, arr.length - 1);

优化:三数取中法

// 选择low、mid、high的中位数作为基准
private int medianOfThree(int[] arr, int low, int high) {
    int mid = low + (high - low) / 2;
    
    if (arr[low] > arr[mid]) swap(arr, low, mid);
    if (arr[low] > arr[high]) swap(arr, low, high);
    if (arr[mid] > arr[high]) swap(arr, mid, high);
    
    // 现在arr[mid]是三个数的中位数
    return arr[mid];
}

时间复杂度

  • 最好/平均:O(n log n) ⚡
  • 最坏:O(n²) - 每次选到最小/最大值(已排序数据)

空间复杂度:O(log n) - 递归栈

稳定性:❌ 不稳定

应用:Java的Arrays.sort()对基本类型使用双轴快排


🔀 归并排序(Merge Sort)⭐⭐⭐⭐⭐

原理

分而治之!先拆分,再合并!

[5, 2, 8, 1, 9, 3]

拆分:
[5, 2, 8]        [1, 9, 3]
  ↓                ↓
[5] [2, 8]      [1] [9, 3]
     ↓               ↓
   [2] [8]         [9] [3]

合并:
[2, 8]          [3, 9]
  ↓               ↓
[2, 5, 8]      [1, 3, 9]
  ↓_______________↓
[1, 2, 3, 5, 8, 9]

代码实现

public void mergeSort(int[] arr, int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;
        
        // 递归排序左半部分
        mergeSort(arr, left, mid);
        // 递归排序右半部分
        mergeSort(arr, mid + 1, right);
        // 合并
        merge(arr, left, mid, right);
    }
}

private void merge(int[] arr, int left, int mid, int right) {
    // 创建临时数组
    int[] temp = new int[right - left + 1];
    int i = left;      // 左半部分指针
    int j = mid + 1;   // 右半部分指针
    int k = 0;         // 临时数组指针
    
    // 合并两个有序数组
    while (i <= mid && j <= right) {
        if (arr[i] <= arr[j]) {
            temp[k++] = arr[i++];
        } else {
            temp[k++] = arr[j++];
        }
    }
    
    // 复制剩余元素
    while (i <= mid) {
        temp[k++] = arr[i++];
    }
    while (j <= right) {
        temp[k++] = arr[j++];
    }
    
    // 复制回原数组
    for (int p = 0; p < temp.length; p++) {
        arr[left + p] = temp[p];
    }
}

// 调用
mergeSort(arr, 0, arr.length - 1);

时间复杂度:O(n log n) - 任何情况都是!✅

空间复杂度:O(n) - 需要临时数组

稳定性:✅ 稳定

应用:Java的Arrays.sort()对对象使用归并排序(实际是TimSort)


🏔️ 堆排序(Heap Sort)⭐⭐⭐

原理

利用堆的性质:最大堆的根节点是最大值!

步骤1:建立最大堆
[5, 2, 8, 1, 9, 3]9
       / \
      5   8
     / \ /
    1  2 3

步骤2:交换根和最后元素,重新堆化
93交换 → 堆化 → 8成为新的根
重复...

最终:[1, 2, 3, 5, 8, 9]

代码实现

public void heapSort(int[] arr) {
    int n = arr.length;
    
    // 1. 建立最大堆
    for (int i = n / 2 - 1; i >= 0; i--) {
        heapify(arr, n, i);
    }
    
    // 2. 一个个取出堆顶元素
    for (int i = n - 1; i > 0; i--) {
        // 交换堆顶和末尾元素
        int temp = arr[0];
        arr[0] = arr[i];
        arr[i] = temp;
        
        // 重新堆化
        heapify(arr, i, 0);
    }
}

// 堆化(下沉)
private void heapify(int[] arr, int n, int i) {
    int largest = i;
    int left = 2 * i + 1;   // 左子节点
    int 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) {
        int temp = arr[i];
        arr[i] = arr[largest];
        arr[largest] = temp;
        
        heapify(arr, n, largest);
    }
}

时间复杂度:O(n log n)

空间复杂度:O(1)

稳定性:❌ 不稳定


🪣 计数排序(Counting Sort)

原理

统计每个数字出现的次数,然后按顺序输出!

数组:[3, 1, 2, 1, 3, 2, 1]
范围:1-3

统计:
count[1] = 3  (1出现3次)
count[2] = 2  (2出现2次)
count[3] = 2  (3出现2次)

输出:[1, 1, 1, 2, 2, 3, 3]

代码实现

public void countingSort(int[] arr) {
    if (arr.length == 0) return;
    
    // 找出最大值
    int max = arr[0];
    for (int num : arr) {
        if (num > max) max = num;
    }
    
    // 统计每个数字的出现次数
    int[] count = new int[max + 1];
    for (int num : arr) {
        count[num]++;
    }
    
    // 按顺序输出
    int index = 0;
    for (int i = 0; i <= max; i++) {
        while (count[i] > 0) {
            arr[index++] = i;
            count[i]--;
        }
    }
}

时间复杂度:O(n + k) - k是数据范围

空间复杂度:O(k)

稳定性:✅ 稳定

适用场景:数据范围不大(k较小)


📊 排序算法总结

排序算法平均时间最坏时间空间稳定性说明
冒泡排序O(n²)O(n²)O(1)基础,慢
选择排序O(n²)O(n²)O(1)简单,慢
插入排序O(n²)O(n²)O(1)小数据好
希尔排序O(n^1.3)O(n²)O(1)插入排序的改进
快速排序O(n log n)O(n²)O(log n)🏆最常用
归并排序O(n log n)O(n log n)O(n)稳定,空间大
堆排序O(n log n)O(n log n)O(1)不稳定
计数排序O(n+k)O(n+k)O(k)范围小时快
桶排序O(n+k)O(n²)O(n+k)数据分布均匀
基数排序O(n×k)O(n×k)O(n+k)整数排序

🎯 如何选择排序算法?

数据规模小(<50)?
  ├─ 是 → 插入排序
  └─ 否 → 继续

需要稳定排序?
  ├─ 是 → 归并排序
  └─ 否 → 快速排序

数据范围小?
  ├─ 是 → 计数排序
  └─ 否 → 快速排序 / 归并排序

内存有限?
  └─ 堆排序(O(1)空间)

🏆 Java中的排序

Arrays.sort()

// 基本类型:双轴快速排序(Dual-Pivot QuickSort)
int[] arr = {5, 2, 8, 1, 9};
Arrays.sort(arr);

// 对象类型:TimSort(归并排序+插入排序)
Integer[] arr = {5, 2, 8, 1, 9};
Arrays.sort(arr);

Collections.sort()

List<Integer> list = Arrays.asList(5, 2, 8, 1, 9);
Collections.sort(list);  // 底层也是TimSort

📝 总结

🎓 记忆口诀

冒泡选择和插入,
时间都是O(n²)。
快速归并和堆排,
O(n log n)称霸。
计数桶排和基数,
线性时间也疯狂。
Java快排用双轴,
对象排序用TimSort。
稳定首选归并法,
空间受限用堆排!

核心要点

  • 快速排序:最常用,平均最快,但不稳定
  • 归并排序:稳定,时间稳定,但空间大
  • 堆排序:空间O(1),但不稳定
  • 插入排序:小数据或基本有序时很快
  • 计数排序:数据范围小时超快

恭喜你!🎉 你已经掌握了所有重要的排序算法!

记住:没有最好的排序算法,只有最合适的! 💪


📌 面试重点:快速排序、归并排序、堆排序

🤔 思考题:为什么Java对基本类型用快排,对对象用归并排序?

(答案:基本类型不需要稳定性,快排更快;对象需要稳定性,所以用归并排序!)