【数据结构】深入理解排序算法:从基础原理到高级应用

目录

前言

一、排序基本概念与分类

[1.1 排序的定义与重要性](#1.1 排序的定义与重要性)

[1.2 关键概念解析](#1.2 关键概念解析)

稳定性(Stability)

[内部排序 vs 外部排序](#内部排序 vs 外部排序)

[1.3 排序算法的应用场景](#1.3 排序算法的应用场景)

二、基于比较的七大排序算法详解

[2.1 插入排序类](#2.1 插入排序类)

[2.1.1 直接插入排序(Insertion Sort)](#2.1.1 直接插入排序(Insertion Sort))

[2.1.2 希尔排序(Shell Sort)](#2.1.2 希尔排序(Shell Sort))

[2.2 选择排序类](#2.2 选择排序类)

[2.2.1 直接选择排序(Selection Sort)](#2.2.1 直接选择排序(Selection Sort))

[2.2.2 堆排序(Heap Sort)](#2.2.2 堆排序(Heap Sort))

[2.3 交换排序类](#2.3 交换排序类)

[2.3.1 冒泡排序(Bubble Sort)](#2.3.1 冒泡排序(Bubble Sort))

[2.3.2 快速排序(Quick Sort)](#2.3.2 快速排序(Quick Sort))

[2.4 归并排序(Merge Sort)](#2.4 归并排序(Merge Sort))

三、排序算法性能对比分析

[3.1 时间复杂度对比](#3.1 时间复杂度对比)

[3.2 排序算法选择指南](#3.2 排序算法选择指南)

四、非基于比较的排序算法

[4.1 计数排序(Counting Sort)](#4.1 计数排序(Counting Sort))

[4.2 基数排序(Radix Sort)](#4.2 基数排序(Radix Sort))

[4.3 桶排序(Bucket Sort)](#4.3 桶排序(Bucket Sort))

五、实战应用与优化技巧

[5.1 Java中的排序工具](#5.1 Java中的排序工具)

[5.2 排序算法优化策略](#5.2 排序算法优化策略)


前言

排序是计算机科学中最基础、最核心的算法之一。从简单的电话簿排序到复杂的数据库查询优化,排序算法无处不在。想象一下,如果没有排序,我们在电商网站搜索商品、在音乐库中查找歌曲、甚至在查看联系人列表时都会变得极其困难。

排序不仅仅是让数据有序排列------高效的排序算法能够显著提升系统性能,影响用户体验。本文将带你深入理解七大经典排序算法,掌握它们的实现原理、性能特点以及应用场景。


一、排序基本概念与分类

1.1 排序的定义与重要性

排序(Sorting)是将一组记录按照某个或某些关键字的大小,以递增或递减的顺序重新排列的操作。排序算法的选择直接影响程序的效率和资源消耗。

1.2 关键概念解析

稳定性(Stability)
  • 稳定排序:如果两个相等元素的相对位置在排序前后保持不变

  • 不稳定排序:相等元素的相对位置可能发生变化

示例:学生成绩表中,先按姓名排序,再按成绩排序。如果排序算法稳定,同分的学生仍会保持姓名的字典序。

  • 稳定排序示例:冒泡排序
  • 不稳定排序示例:快速排序
内部排序 vs 外部排序
类型 特点 适用场景
内部排序 所有数据都在内存中进行排序 数据量较小,内存充足
外部排序 数据量太大,无法全部加载到内存 大数据处理,如数据库排序

1.3 排序算法的应用场景

  • 电商平台:商品按价格、销量、评分排序

  • 社交网络:动态按时间、热度排序

  • 操作系统:进程调度优先级排序

  • 数据库系统:索引构建和查询优化

二、基于比较的七大排序算法详解

2.1 插入排序类

2.1.1 直接插入排序(Insertion Sort)

核心思想:将待排序元素插入到已排序序列的适当位置,类似于整理扑克牌。

算法步骤

  1. 从第一个元素开始,该元素可视为已排序

  2. 取出下一个元素,在已排序序列中从后向前扫描

  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置

  4. 重复步骤3,直到找到已排序元素小于或等于新元素的位置

  5. 将新元素插入到该位置后

  6. 重复步骤2~5

    public class InsertionSort {
    public static void sort(int[] arr) {
    for (int i = 1; i < arr.length; 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(n²)

  • 空间复杂度:O(1)

  • 稳定性:稳定

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

2.1.2 希尔排序(Shell Sort)

核心思想:将原始数组分割成若干子序列进行插入排序,逐步缩小增量,最终完成整体排序。

算法步骤

  1. 选择一个增量序列(如:n/2, n/4, ..., 1)

  2. 按增量序列个数k,对数组进行k趟排序

  3. 每趟排序,根据对应的增量,将待排序序列分割成若干子序列

  4. 分别对各子序列进行直接插入排序

  5. 当增量减至1时,整个序列作为一个表来处理

    public class ShellSort {
    public static void sort(int[] arr) {
    int n = arr.length;

    复制代码
         // 使用Knuth增量序列
         int gap = 1;
         while (gap < n / 3) {
             gap = gap * 3 + 1;
         }
         
         while (gap > 0) {
             // 对每个子序列进行插入排序
             for (int i = gap; i < n; i++) {
                 int temp = arr[i];
                 int j = i;
                 
                 while (j >= gap && arr[j - gap] > temp) {
                     arr[j] = arr[j - gap];
                     j -= gap;
                 }
                 arr[j] = temp;
             }
             gap = (gap - 1) / 3;  // 缩小增量
         }
     }

    }

性能分析

  • 时间复杂度:O(n^(1.3~2)),具体取决于增量序列

  • 空间复杂度:O(1)

  • 稳定性:不稳定

  • 特点:希尔排序是插入排序的高效改进版

2.2 选择排序类

2.2.1 直接选择排序(Selection Sort)

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

算法步骤

  1. 初始状态:有序区为空,无序区为整个数组

  2. 第i趟排序:从无序区选出最小元素,与无序区第一个元素交换

  3. 重复n-1趟,直到排序完成

    public class SelectionSort {
    public static void sort(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++) {
    int minIndex = i;

    复制代码
             // 在未排序部分寻找最小元素
             for (int j = i + 1; j < arr.length; j++) {
                 if (arr[j] < arr[minIndex]) {
                     minIndex = j;
                 }
             }
             
             // 将最小元素交换到已排序部分末尾
             if (minIndex != i) {
                 int temp = arr[i];
                 arr[i] = arr[minIndex];
                 arr[minIndex] = temp;
             }
         }
     }

    }

性能分析

  • 时间复杂度:O(n²)

  • 空间复杂度:O(1)

  • 稳定性:不稳定

  • 特点:简单直观,但效率较低

2.2.2 堆排序(Heap Sort)

核心思想:利用堆数据结构设计的排序算法,是一种改进的选择排序。

算法步骤

  1. 将无序数组构建成一个大顶堆(升序排序)

  2. 将堆顶元素(最大值)与末尾元素交换

  3. 调整堆结构,使其满足堆定义

  4. 重复步骤2-3,直到堆的大小为1

java 复制代码
public class HeapSort {
    public static void sort(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 static 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 swap = arr[i];
            arr[i] = arr[largest];
            arr[largest] = swap;
            
            // 递归调整受影响的子树
            heapify(arr, n, largest);
        }
    }
}

性能分析

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

  • 空间复杂度:O(1)

  • 稳定性:不稳定

  • 特点:原地排序,适合大规模数据

2.3 交换排序类

2.3.1 冒泡排序(Bubble Sort)

核心思想:重复遍历数组,比较相邻元素,如果顺序错误就交换它们。

算法步骤

  1. 比较相邻元素,如果第一个比第二个大,就交换它们

  2. 对每一对相邻元素做同样工作,从开始第一对到结尾最后一对

  3. 针对所有元素重复以上步骤,除了最后一个

  4. 重复步骤1-3,直到排序完成

java 复制代码
public class BubbleSort {
    public static void sort(int[] arr) {
        int n = arr.length;
        boolean swapped;
        
        for (int i = 0; i < n - 1; i++) {
            swapped = false;
            
            // 每次遍历将最大元素"冒泡"到末尾
            for (int j = 0; j < n - i - 1; 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)

  • 稳定性:稳定

  • 特点:简单易懂,但效率较低

2.3.2 快速排序(Quick Sort)

核心思想:采用分治策略,选取一个基准元素,将数组分成两部分,左边小于基准,右边大于基准,然后递归排序。

三种分区方法

1. Hoare分区法

java 复制代码
private static int partitionHoare(int[] arr, int low, int high) {
    int pivot = arr[low];
    int i = low - 1;
    int j = high + 1;
    
    while (true) {
        do {
            i++;
        } while (arr[i] < pivot);
        
        do {
            j--;
        } while (arr[j] > pivot);
        
        if (i >= j) return j;
        
        // 交换arr[i]和arr[j]
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

2. 挖坑法

java 复制代码
private static int partitionDigging(int[] arr, int low, int high) {
    int pivot = arr[low];  // 挖坑,保存基准值
    int i = low, j = high;
    
    while (i < j) {
        // 从右向左找第一个小于pivot的元素
        while (i < j && arr[j] >= pivot) {
            j--;
        }
        if (i < j) {
            arr[i] = arr[j];  // 填坑
            i++;
        }
        
        // 从左向右找第一个大于等于pivot的元素
        while (i < j && arr[i] < pivot) {
            i++;
        }
        if (i < j) {
            arr[j] = arr[i];  // 填坑
            j--;
        }
    }
    
    arr[i] = pivot;  // 基准值放入最终位置
    return i;
}

3. 前后指针法

java 复制代码
private static int partitionTwoPointers(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;
}

完整快速排序实现

java 复制代码
public class QuickSort {
    // 快速排序主函数
    public static void sort(int[] arr) {
        quickSort(arr, 0, arr.length - 1);
    }
    
    // 递归实现快速排序
    private static void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            // 优化1:小区间使用插入排序
            if (high - low + 1 <= 10) {
                insertionSort(arr, low, high);
                return;
            }
            
            // 优化2:三数取中法选择基准
            int pivotIndex = medianOfThree(arr, low, high);
            swap(arr, pivotIndex, low);  // 将基准放到开头
            
            // 分区操作
            int pi = partitionHoare(arr, low, high);
            
            // 递归排序左右两部分
            quickSort(arr, low, pi);
            quickSort(arr, pi + 1, high);
        }
    }
    
    // 三数取中法
    private static 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);
        
        return mid;  // 返回中间值索引
    }
    
    // 插入排序(用于小区间优化)
    private static void insertionSort(int[] arr, int low, int high) {
        for (int i = low + 1; i <= high; i++) {
            int key = arr[i];
            int j = i - 1;
            
            while (j >= low && arr[j] > key) {
                arr[j + 1] = arr[j];
                j--;
            }
            arr[j + 1] = key;
        }
    }
    
    // 交换辅助函数
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

快速排序的非递归实现

java 复制代码
public class QuickSortIterative {
    public static void sort(int[] arr) {
        // 使用栈模拟递归调用
        Stack<Integer> stack = new Stack<>();
        stack.push(0);
        stack.push(arr.length - 1);
        
        while (!stack.isEmpty()) {
            int high = stack.pop();
            int low = stack.pop();
            
            if (low < high) {
                int pi = partitionHoare(arr, low, high);
                
                // 压入左子数组边界
                stack.push(low);
                stack.push(pi);
                
                // 压入右子数组边界
                stack.push(pi + 1);
                stack.push(high);
            }
        }
    }
}

性能分析

  • 时间复杂度

    • 最好情况:O(n log n)

    • 最坏情况:O(n²)(当数组已排序或逆序时)

    • 平均情况:O(n log n)

  • 空间复杂度

    • 递归实现:O(log n)(调用栈深度)

    • 非递归实现:O(log n)(显式栈)

  • 稳定性:不稳定

  • 特点:平均性能最好,是实际应用中最常用的排序算法

2.4 归并排序(Merge Sort)

核心思想:采用分治法,将数组分成两半,分别排序,然后合并。

算法步骤

  1. 分割:将数组递归地分成两半,直到每个子数组只有一个元素

  2. 合并:将两个已排序的子数组合并成一个有序数组

java 复制代码
public class MergeSort {
    // 归并排序主函数
    public static void sort(int[] arr) {
        mergeSort(arr, 0, arr.length - 1);
    }
    
    // 递归分割和合并
    private static 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 static 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 (i = left, k = 0; i <= right; i++, k++) {
            arr[i] = temp[k];
        }
    }
    
    // 归并排序的非递归实现
    public static void sortIterative(int[] arr) {
        int n = arr.length;
        int currSize;  // 当前子数组大小
        
        // 从1开始,每次合并的子数组大小翻倍
        for (currSize = 1; currSize <= n - 1; currSize = 2 * currSize) {
            // 选择起始位置
            for (int leftStart = 0; leftStart < n - 1; leftStart += 2 * currSize) {
                int mid = Math.min(leftStart + currSize - 1, n - 1);
                int rightEnd = Math.min(leftStart + 2 * currSize - 1, n - 1);
                
                // 合并arr[leftStart...mid]和arr[mid+1...rightEnd]
                merge(arr, leftStart, mid, rightEnd);
            }
        }
    }
}

性能分析

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

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

  • 稳定性:稳定

  • 特点:稳定排序,适合外部排序

三、排序算法性能对比分析

3.1 时间复杂度对比

排序算法 最好情况 平均情况 最坏情况 空间复杂度 稳定性
冒泡排序 O(n) O(n²) O(n²) O(1) 稳定
选择排序 O(n²) O(n²) O(n²) O(1) 不稳定
插入排序 O(n) O(n²) O(n²) O(1) 稳定
希尔排序 O(n log n) O(n^(1.3~2)) O(n²) O(1) 不稳定
堆排序 O(n log n) O(n log n) O(n log n) O(1) 不稳定
快速排序 O(n log n) O(n log n) O(n²) O(log n) 不稳定
归并排序 O(n log n) O(n log n) O(n log n) O(n) 稳定

3.2 排序算法选择指南

  1. 小规模数据(n ≤ 10):插入排序

  2. 基本有序数据:插入排序或冒泡排序

  3. 大规模数据,要求稳定排序:归并排序

  4. 大规模数据,对稳定性无要求:快速排序或堆排序

  5. 数据范围已知且较小:计数排序

  6. 外部排序(数据无法全部加载到内存):归并排序

四、非基于比较的排序算法

4.1 计数排序(Counting Sort)

核心思想:统计每个元素出现的次数,然后按顺序输出。

适用条件:元素范围已知且不大(如0-100的整数)

java 复制代码
public class CountingSort {
    public static void sort(int[] arr) {
        if (arr.length == 0) return;
        
        // 1. 找出最大值和最小值
        int max = arr[0], min = arr[0];
        for (int num : arr) {
            if (num > max) max = num;
            if (num < min) min = num;
        }
        
        // 2. 创建计数数组
        int range = max - min + 1;
        int[] count = new int[range];
        
        // 3. 统计每个元素出现的次数
        for (int num : arr) {
            count[num - min]++;
        }
        
        // 4. 累加计数(确定每个元素的位置)
        for (int i = 1; i < range; i++) {
            count[i] += count[i - 1];
        }
        
        // 5. 构建排序结果
        int[] output = new int[arr.length];
        for (int i = arr.length - 1; i >= 0; i--) {
            int num = arr[i];
            int pos = count[num - min] - 1;
            output[pos] = num;
            count[num - min]--;
        }
        
        // 6. 复制回原数组
        System.arraycopy(output, 0, arr, 0, arr.length);
    }
}

性能分析

  • 时间复杂度:O(n + k),k为数据范围

  • 空间复杂度:O(n + k)

  • 稳定性:稳定

4.2 基数排序(Radix Sort)

核心思想:按位排序,从最低位到最高位依次进行稳定排序。

java 复制代码
public class RadixSort {
    public static void sort(int[] arr) {
        if (arr.length == 0) return;
        
        // 找出最大值,确定最大位数
        int max = arr[0];
        for (int num : arr) {
            if (num > max) max = num;
        }
        
        // 从个位开始,对每一位进行计数排序
        for (int exp = 1; max / exp > 0; exp *= 10) {
            countingSortByDigit(arr, exp);
        }
    }
    
    private static void countingSortByDigit(int[] arr, int exp) {
        int n = arr.length;
        int[] output = new int[n];
        int[] count = new int[10];  // 0-9十个数字
        
        // 统计当前位的数字出现次数
        for (int num : arr) {
            int digit = (num / exp) % 10;
            count[digit]++;
        }
        
        // 累加计数
        for (int i = 1; i < 10; i++) {
            count[i] += count[i - 1];
        }
        
        // 构建输出数组(从后往前保持稳定性)
        for (int i = n - 1; i >= 0; i--) {
            int digit = (arr[i] / exp) % 10;
            output[count[digit] - 1] = arr[i];
            count[digit]--;
        }
        
        // 复制回原数组
        System.arraycopy(output, 0, arr, 0, n);
    }
}

4.3 桶排序(Bucket Sort)

核心思想:将数据分到有限数量的桶中,每个桶分别排序,然后合并。

java 复制代码
public class BucketSort {
    public static void sort(int[] arr) {
        if (arr.length == 0) return;
        
        // 1. 确定桶的数量和范围
        int max = arr[0], min = arr[0];
        for (int num : arr) {
            if (num > max) max = num;
            if (num < min) min = num;
        }
        
        int bucketCount = 5;  // 假设分为5个桶
        List<List<Integer>> buckets = new ArrayList<>(bucketCount);
        
        for (int i = 0; i < bucketCount; i++) {
            buckets.add(new ArrayList<>());
        }
        
        // 2. 将元素分配到桶中
        int range = max - min + 1;
        for (int num : arr) {
            int bucketIndex = (num - min) * bucketCount / range;
            if (bucketIndex >= bucketCount) bucketIndex = bucketCount - 1;
            buckets.get(bucketIndex).add(num);
        }
        
        // 3. 对每个桶进行排序
        int index = 0;
        for (List<Integer> bucket : buckets) {
            // 使用插入排序对每个桶排序
            Collections.sort(bucket);
            
            // 4. 将排序后的桶合并
            for (int num : bucket) {
                arr[index++] = num;
            }
        }
    }
}

五、实战应用与优化技巧

5.1 Java中的排序工具

java 复制代码
import java.util.Arrays;
import java.util.Collections;

public class JavaSortExample {
    public static void main(String[] args) {
        // 1. 基本类型数组排序
        int[] arr = {5, 2, 8, 1, 9};
        Arrays.sort(arr);  // 快速排序实现
        
        // 2. 对象数组排序(需要实现Comparable接口)
        Integer[] arr2 = {5, 2, 8, 1, 9};
        Arrays.sort(arr2);
        
        // 3. 降序排序
        Arrays.sort(arr2, Collections.reverseOrder());
        
        // 4. 自定义比较器
        Arrays.sort(arr2, (a, b) -> b - a);
        
        // 5. 部分排序
        Arrays.sort(arr, 0, 3);  // 只排序前3个元素
    }
}

5.2 排序算法优化策略

  1. 混合排序:结合多种排序算法的优点

  2. 多线程并行排序:利用多核CPU加速

  3. 内存优化:原地排序 vs 非原地排序

  4. 缓存友好:考虑CPU缓存局部性原理

相关推荐
sin_hielo1 小时前
leetcode 3314(位运算,lowbit)
数据结构·算法·leetcode
bybitq1 小时前
Leetcode-124-二叉树最大路径和-Python
算法·leetcode·深度优先
wanzhong23331 小时前
开发日记13-响应式变量
开发语言·前端·javascript·vue
indexsunny1 小时前
互联网大厂Java求职面试实战:Spring Boot微服务与Kafka消息队列场景解析
java·spring boot·面试·kafka·microservices·interview·distributed systems
qq_12498707531 小时前
基于Spring Boot的心理咨询预约微信小程序(源码+论文+部署+安装)
java·spring boot·后端·spring·微信小程序·小程序·毕业设计
AlenTech1 小时前
1683. 无效的推文 - 力扣(LeetCode)
leetcode
jiayong231 小时前
Tomcat Servlet容器与生命周期管理面试题
java·servlet·tomcat
鱼跃鹰飞2 小时前
Leetcode会员专享题:426.将二叉搜索树转换为排序的双向链表
数据结构·算法·leetcode·链表·深度优先
漫随流水2 小时前
leetcode回溯算法(39.组合总和)
数据结构·算法·leetcode·回溯算法