常见的排序算法

一、快速排序

1.快速排序算法说明

快速排序的核心思想是分治法,主要分为以下步骤:

  1. 选择基准元素:这里选择数组最右侧的元素作为基准 (pivot)
  2. 分区操作:将数组分为两部分,左侧元素都小于等于基准,右侧元素都大于基准
  3. 递归排序:对基准元素左右两侧的子数组分别进行递归排序

2.代码特点

  • 时间复杂度 :平均情况 O (nlogn),最坏情况 O (n²),通过随机选择基准元素可以避免最坏情况
  • 空间复杂度:O (logn),主要是递归调用栈的空间
  • 不稳定性:快速排序是不稳定的排序算法
  • 原地排序:不需要额外的数组空间,只需要常数级的临时变量
java 复制代码
import java.util.Arrays;

public class QuickSort {
    // 快速排序入口方法
    public static void quickSort(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }
        sort(arr, 0, arr.length - 1);
    }
    
    // 递归排序方法
    private static void sort(int[] arr, int left, int right) {
        // 递归终止条件:左右指针相遇或交叉
        if (left >= right) {
            return;
        }
        
        // 进行分区操作,获取基准元素的正确位置
        int pivotIndex = partition(arr, left, right);
        
        // 递归排序基准元素左侧子数组
        sort(arr, left, pivotIndex - 1);
        // 递归排序基准元素右侧子数组
        sort(arr, pivotIndex + 1, right);
    }
    
    // 分区操作
    private static int partition(int[] arr, int left, int right) {
        // 选择最右侧元素作为基准
        int pivot = arr[right];
        
        // 记录小于等于基准元素的区域边界
        int i = left - 1;
        
        // 遍历数组,将小于等于基准的元素放到左侧区域
        for (int j = left; j < right; j++) {
            if (arr[j] <= pivot) {
                i++;
                // 交换元素到左侧区域
                swap(arr, i, j);
            }
        }
        
        // 将基准元素放到正确位置(左侧区域的下一个位置)
        swap(arr, i + 1, right);
        
        // 返回基准元素的索引
        return i + 1;
    }
    
    // 交换数组中两个元素的位置
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    
    // 测试示例
    public static void main(String[] args) {
        int[] arr = {3, 6, 8, 10, 1, 2, 1};
        System.out.println("排序前: " + Arrays.toString(arr));
        
        quickSort(arr);
        
        System.out.println("排序后: " + Arrays.toString(arr));
    }
}
    

二、选择排序

选择排序的基本步骤

  1. 从数组的第一个元素开始,将其视为当前最小值
  2. 遍历剩余的未排序元素,寻找比当前最小值更小的元素,更新最小值的位置
  3. 将找到的最小值与未排序部分的第一个元素交换位置
  4. 移动边界,将已排序部分的长度增加 1
  5. 重复步骤 1-4,直到整个数组排序完成

选择排序的特点

  • 时间复杂度 :无论最好、最坏还是平均情况,都是 O (n²),因为总是需要完整遍历未排序部分
  • 空间复杂度:O (1),只需要常数级的额外空间
  • 不稳定性:当存在相等元素时,可能会改变它们的相对顺序
  • 原地排序:不需要额外的数组空间
java 复制代码
import java.util.Arrays;

public class SelectionSort {
    // 选择排序实现
    public static void selectionSort(int[] arr) {
        // 边界检查
        if (arr == null || arr.length <= 1) {
            return;
        }
        
        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;  // 更新最小值索引
                }
            }
            
            // 将找到的最小值与未排序部分的第一个元素交换
            if (minIndex != i) {
                int temp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = temp;
            }
        }
    }
    
    // 测试示例
    public static void main(String[] args) {
        int[] arr = {64, 25, 12, 22, 11};
        System.out.println("排序前: " + Arrays.toString(arr));
        
        selectionSort(arr);
        
        System.out.println("排序后: " + Arrays.toString(arr));
    }
}
    

三、插入排序

------将数组分为 "已排序" 和 "未排序" 两部分,每次从无序部分取一个元素,插入到有序部分的合适位置,直到所有元素都被插入完毕。

插入排序的基本步骤

  1. 将数组的第一个元素视为初始的 "已排序部分"(只有一个元素,天然有序)
  2. 从第二个元素开始,依次处理 "未排序部分" 的每个元素:
    • 暂存当前要插入的元素(称为 "待插入元素")
    • 与 "已排序部分" 的元素从后往前比较
    • 如果已排序元素大于待插入元素,则将其向后移动一位
    • 找到合适位置后,将待插入元素放入该位置
  3. 重复步骤 2,直到所有元素都被插入到有序部分

插入排序的特点

  • 时间复杂度
    • 最好情况(数组已完全有序):O (n),只需遍历一次,无需移动元素
    • 最坏情况(数组完全逆序):O (n²)
    • 平均情况:O (n²)
  • 空间复杂度:O (1),只需要常数级额外空间
  • 稳定性:稳定排序,相等元素的相对顺序不会改变
  • 原地排序:不需要额外数组,直接在原数组上操作

插入排序特别适合小规模数据部分有序的数据(如接近排序的数组),因为此时移动元素的操作会大幅减少。

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

public class InsertionSort {
    // 插入排序实现
    public static void insertionSort(int[] arr) {
        // 边界检查
        if (arr == null || arr.length <= 1) {
            return;
        }
        
        int n = arr.length;
        
        // 外层循环:从第二个元素开始处理未排序部分
        for (int i = 1; i < n; i++) {
            int current = arr[i]; // 待插入的元素
            int j = i - 1;       // 已排序部分的最后一个元素索引
            
            // 内层循环:在已排序部分中找到合适位置
            // 将大于current的元素向后移动
            while (j >= 0 && arr[j] > current) {
                arr[j + 1] = arr[j]; // 元素后移
                j--;
            }
            
            // 将待插入元素放到正确位置
            arr[j + 1] = current;
        }
    }
    
    // 测试示例
    public static void main(String[] args) {
        int[] arr = {12, 11, 13, 5, 6};
        System.out.println("排序前: " + Arrays.toString(arr));
        
        insertionSort(arr);
        
        System.out.println("排序后: " + Arrays.toString(arr));
    }
}
    

四、希尔排序

希尔排序(Shell Sort)是插入排序的一种改进版本,也称为 "缩小增量排序"。它通过引入 "增量" 概念,将原数组分割成多个子数组分别进行插入排序,逐步缩小增量直至为 1,最终完成整个数组的排序。

希尔排序的核心思想

  1. 分组排序:选择一个增量序列(如 n/2, n/4, ..., 1),按照增量将数组分为若干子数组(每个子数组由间隔为增量的元素组成)
  2. 子数组插入排序:对每个子数组分别执行插入排序
  3. 缩小增量:逐步减小增量,重复分组和排序操作
  4. 最终排序:当增量减为 1 时,整个数组被视为一个子数组,执行最后一次插入排序(此时数组已基本有序,插入排序效率极高)

增量序列的影响

希尔排序的性能很大程度上依赖于增量序列的选择:

  • 经典序列(n/2, n/4...):实现简单但性能一般
  • Hibbard 序列(2^k-1):性能更好,时间复杂度约为 O (n^1.5)
  • Sedgewick 序列:更优的序列,时间复杂度可接近 O (n^1.3)

希尔排序的特点

  • 时间复杂度 :取决于增量序列的选择,通常在 O (n^1.3) 到 O (n²) 之间
  • 空间复杂度:O (1),原地排序,只需常数级额外空间
  • 不稳定性:由于分组排序可能改变相等元素的相对位置
  • 适用性:对中等规模数据的排序效率较好,优于简单的 O (n²) 排序算法
java 复制代码
import java.util.Arrays;

public class ShellSort {
    // 希尔排序实现
    public static void shellSort(int[] arr) {
        // 边界检查
        if (arr == null || arr.length <= 1) {
            return;
        }
        
        int n = arr.length;
        
        // 初始增量为数组长度的一半,逐渐减小增量
        for (int gap = n / 2; gap > 0; gap /= 2) {
            // 对每个子数组执行插入排序
            for (int i = gap; i < n; i++) {
                int current = arr[i]; // 待插入元素
                int j = i;
                
                // 在当前子数组中寻找合适位置
                while (j >= gap && arr[j - gap] > current) {
                    arr[j] = arr[j - gap]; // 元素后移
                    j -= gap;
                }
                
                // 插入元素
                arr[j] = current;
            }
        }
    }
    
    // 测试示例
    public static void main(String[] args) {
        int[] arr = {12, 34, 54, 2, 3};
        System.out.println("排序前: " + Arrays.toString(arr));
        
        shellSort(arr);
        
        System.out.println("排序后: " + Arrays.toString(arr));
    }
}
    

代码说明

  1. 增量序列:代码中使用的是最经典的增量序列(n/2, n/4, ..., 1),每次将增量减半
  2. 分组处理 :对于每个增量gap,数组被分为gap个独立的子数组
  3. 子数组排序 :对每个子数组采用插入排序的变体,比较和移动元素时的步长为gap
  4. 效率关键:通过前期的大增量排序,使数组快速变得 "基本有序",当增量为 1 时,只需少量移动即可完成最终排序

五、归并排序

归并排序(Merge Sort)是一种基于分治思想的高效排序算法,其核心思路是将数组不断分割为更小的子数组,直到每个子数组只包含一个元素(天然有序),然后逐步将这些有序子数组合并成更大的有序数组,最终得到完整的排序结果。

归并排序的基本步骤

  1. 分解(Divide):将当前数组从中间分割为两个等长(或近似等长)的子数组
  2. 递归排序(Conquer):对两个子数组分别递归执行归并排序
  3. 合并(Merge):将两个已排序的子数组合并成一个更大的有序数组

归并排序的特点

  • 时间复杂度 :最好、最坏和平均情况均为 O (nlogn),性能稳定
  • 空间复杂度:O (n),需要额外空间存储合并过程中的临时数组
  • 稳定性:稳定排序,相等元素的相对顺序不会改变
  • 适用性:适合处理大规模数据,尤其适合外排序(数据不适合全部加载到内存的场景)
java 复制代码
import java.util.Arrays;

public class MergeSort {
    // 归并排序入口
    public static void mergeSort(int[] arr) {
        // 边界检查
        if (arr == null || arr.length <= 1) {
            return;
        }
        // 创建临时数组,避免递归中频繁创建
        int[] temp = new int[arr.length];
        sort(arr, 0, arr.length - 1, temp);
    }
    
    // 递归分割数组
    private static void sort(int[] arr, int left, int right, int[] temp) {
        if (left < right) {
            // 找到中间位置
            int mid = left + (right - left) / 2;
            
            // 递归排序左半部分
            sort(arr, left, mid, temp);
            // 递归排序右半部分
            sort(arr, mid + 1, right, temp);
            
            // 合并两个有序子数组
            merge(arr, left, mid, right, temp);
        }
    }
    
    // 合并两个有序子数组
    private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
        int i = left;       // 左子数组的起始索引
        int j = mid + 1;    // 右子数组的起始索引
        int t = 0;          // 临时数组的起始索引
        
        // 比较两个子数组的元素,将较小的放入临时数组
        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j]) {
                temp[t++] = arr[i++];
            } else {
                temp[t++] = arr[j++];
            }
        }
        
        // 将左子数组剩余元素放入临时数组
        while (i <= mid) {
            temp[t++] = arr[i++];
        }
        
        // 将右子数组剩余元素放入临时数组
        while (j <= right) {
            temp[t++] = arr[j++];
        }
        
        // 将临时数组中的元素复制回原数组
        t = 0;
        while (left <= right) {
            arr[left++] = temp[t++];
        }
    }
    
    // 测试示例
    public static void main(String[] args) {
        int[] arr = {14, 33, 27, 35, 10};
        System.out.println("排序前: " + Arrays.toString(arr));
        
        mergeSort(arr);
        
        System.out.println("排序后: " + Arrays.toString(arr));
    }
}
    

六、冒泡排序

冒泡排序(Bubble Sort)是一种简单直观的排序算法,其核心思想是重复遍历数组,每次比较相邻的两个元素,如果它们的顺序错误就交换位置。由于较小的元素会像 "气泡" 一样逐渐上浮到数组的顶端,因此得名冒泡排序。

冒泡排序的基本步骤

  1. 从数组的第一个元素开始,依次比较相邻的两个元素
  2. 如果前一个元素大于后一个元素,则交换它们的位置
  3. 完成一轮遍历后,最大的元素会 "浮" 到数组的末尾(已排序部分)
  4. 缩小遍历范围(不再包含已排序的末尾元素),重复步骤 1-3
  5. 直到没有元素需要交换,排序完成

冒泡排序的特点

  • 时间复杂度
    • 最好情况(数组已完全有序):O (n)(需优化版本)
    • 最坏情况(数组完全逆序):O (n²)
    • 平均情况:O (n²)
  • 空间复杂度:O (1),原地排序,只需常数级额外空间
  • 稳定性:稳定排序,相等元素的相对顺序不会改变
  • 适用性:仅适合小规模数据排序,实际应用中较少使用
java 复制代码
import java.util.Arrays;

public class BubbleSort {
    // 冒泡排序实现(优化版)
    public static void bubbleSort(int[] arr) {
        // 边界检查
        if (arr == null || arr.length <= 1) {
            return;
        }
        
        int n = arr.length;
        boolean swapped; // 标记本轮是否发生交换
        
        // 外层循环控制排序轮次
        for (int i = 0; i < n - 1; i++) {
            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;
            }
        }
    }
    
    // 测试示例
    public static void main(String[] args) {
        int[] arr = {64, 34, 25, 12, 22, 11, 90};
        System.out.println("排序前: " + Arrays.toString(arr));
        
        bubbleSort(arr);
        
        System.out.println("排序后: " + Arrays.toString(arr));
    }
}
    

代码说明

  1. 外层循环:控制排序的轮次,最多需要 n-1 轮(每轮至少确定一个元素的最终位置)
  2. 内层循环 :负责相邻元素的比较和交换,每轮的遍历范围会随着已排序元素的增加而缩小(n-1-i
  3. 优化机制 :通过swapped变量判断本轮是否发生交换,若未发生交换,说明数组已完全有序,可直接退出循环,避免不必要的遍历

七、堆排序

堆排序(Heap Sort)是一种基于堆数据结构的高效排序算法,其核心思想是利用堆的特性(大顶堆或小顶堆),反复提取堆顶元素(最大或最小元素),并调整剩余元素维持堆结构,最终得到有序数组。

堆排序的基本步骤

  1. 构建堆:将待排序数组转换为大顶堆(父节点大于子节点)
  2. 提取堆顶元素:将堆顶元素(最大值)与堆尾元素交换,此时最大值已放到正确位置
  3. 调整堆:将剩余元素重新调整为大顶堆
  4. 重复操作:不断重复步骤 2 和 3,直到所有元素都被排序

堆的核心概念

  • 大顶堆 :对于任意节点 i,其父节点的值大于等于子节点的值(arr[parent] >= arr[i]
  • 堆的索引关系 :对于索引为 i 的节点,左子节点索引为2i+1,右子节点索引为2i+2,父节点索引为(i-1)/2
  • 堆的高度:堆是完全二叉树,高度为 O (logn),这决定了堆操作的时间效率

堆排序的特点

  • 时间复杂度 :建堆 O (n),每次调整 O (logn),整体 O (nlogn)(最好、最坏、平均情况一致)
  • 空间复杂度:O (1),原地排序,无需额外数组
  • 不稳定性:交换操作可能改变相等元素的相对顺序
  • 适用性:适合大规模数据排序,对缓存不友好(元素访问跳跃性大)
java 复制代码
import java.util.Arrays;

public class HeapSort {
    // 堆排序入口
    public static void heapSort(int[] arr) {
        if (arr == null || arr.length <= 1) {
            return;
        }
        
        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;
            
            // 对剩余元素重新调整为大顶堆(堆大小减1)
            heapify(arr, i, 0);
        }
    }
    
    // 堆调整函数:将以i为根的子树调整为大顶堆
    private static void heapify(int[] arr, int heapSize, int i) {
        int largest = i;          // 初始化最大值为根节点
        int left = 2 * i + 1;     // 左子节点索引
        int right = 2 * i + 2;    // 右子节点索引
        
        // 如果左子节点大于根节点,更新最大值索引
        if (left < heapSize && arr[left] > arr[largest]) {
            largest = left;
        }
        
        // 如果右子节点大于当前最大值,更新最大值索引
        if (right < heapSize && arr[right] > arr[largest]) {
            largest = right;
        }
        
        // 如果最大值不是根节点,交换并递归调整受影响的子树
        if (largest != i) {
            int temp = arr[i];
            arr[i] = arr[largest];
            arr[largest] = temp;
            
            // 递归调整以largest为根的子树
            heapify(arr, heapSize, largest);
        }
    }
    
    // 测试示例
    public static void main(String[] args) {
        int[] arr = {12, 11, 13, 5, 6, 7};
        System.out.println("排序前: " + Arrays.toString(arr));
        
        heapSort(arr);
        
        System.out.println("排序后: " + Arrays.toString(arr));
    }
}
    

堆排序的优势与局限

  • 优势:时间复杂度稳定为 O (nlogn),不受数据分布影响;空间效率高(O (1));适合处理海量数据。
  • 局限:不是稳定排序;元素访问模式对缓存不友好(跳跃式访问);常数因子较大,实际性能略逊于快速排序。
相关推荐
周杰伦_Jay3 小时前
【图文详解】强化学习核心框架、数学基础、分类、应用场景
人工智能·科技·算法·机器学习·计算机视觉·分类·数据挖掘
violet-lz3 小时前
Linux静态库与共享库(动态库)全面详解:从创建到应用
算法
贝塔实验室3 小时前
ADMM 算法的基本概念
算法·数学建模·设计模式·矩阵·动态规划·软件构建·傅立叶分析
235164 小时前
【LeetCode】3. 无重复字符的最长子串
java·后端·算法·leetcode·职场和发展
JasmineX-14 小时前
数据结构——静态链表(c语言笔记)
c语言·数据结构·链表
微笑尅乐4 小时前
神奇的位运算——力扣136.只出现一次的数字
java·算法·leetcode·职场和发展
吃着火锅x唱着歌5 小时前
LeetCode 3105.最长的严格递增或递减子数组
算法·leetcode·职场和发展
小卡皮巴拉5 小时前
【笔试强训】Day1
开发语言·数据结构·c++·算法
初圣魔门首席弟子5 小时前
switch缺少break出现bug
c++·算法·bug