一、快速排序
1.快速排序算法说明
快速排序的核心思想是分治法,主要分为以下步骤:
- 选择基准元素:这里选择数组最右侧的元素作为基准 (pivot)
- 分区操作:将数组分为两部分,左侧元素都小于等于基准,右侧元素都大于基准
- 递归排序:对基准元素左右两侧的子数组分别进行递归排序
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
- 重复步骤 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));
}
}
三、插入排序
------将数组分为 "已排序" 和 "未排序" 两部分,每次从无序部分取一个元素,插入到有序部分的合适位置,直到所有元素都被插入完毕。
插入排序的基本步骤
- 将数组的第一个元素视为初始的 "已排序部分"(只有一个元素,天然有序)
- 从第二个元素开始,依次处理 "未排序部分" 的每个元素:
- 暂存当前要插入的元素(称为 "待插入元素")
- 与 "已排序部分" 的元素从后往前比较
- 如果已排序元素大于待插入元素,则将其向后移动一位
- 找到合适位置后,将待插入元素放入该位置
- 重复步骤 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,最终完成整个数组的排序。
希尔排序的核心思想
- 分组排序:选择一个增量序列(如 n/2, n/4, ..., 1),按照增量将数组分为若干子数组(每个子数组由间隔为增量的元素组成)
- 子数组插入排序:对每个子数组分别执行插入排序
- 缩小增量:逐步减小增量,重复分组和排序操作
- 最终排序:当增量减为 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));
}
}
代码说明
- 增量序列:代码中使用的是最经典的增量序列(n/2, n/4, ..., 1),每次将增量减半
- 分组处理 :对于每个增量
gap
,数组被分为gap
个独立的子数组 - 子数组排序 :对每个子数组采用插入排序的变体,比较和移动元素时的步长为
gap
- 效率关键:通过前期的大增量排序,使数组快速变得 "基本有序",当增量为 1 时,只需少量移动即可完成最终排序
五、归并排序
归并排序(Merge Sort)是一种基于分治思想的高效排序算法,其核心思路是将数组不断分割为更小的子数组,直到每个子数组只包含一个元素(天然有序),然后逐步将这些有序子数组合并成更大的有序数组,最终得到完整的排序结果。
归并排序的基本步骤
- 分解(Divide):将当前数组从中间分割为两个等长(或近似等长)的子数组
- 递归排序(Conquer):对两个子数组分别递归执行归并排序
- 合并(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-3
- 直到没有元素需要交换,排序完成
冒泡排序的特点
- 时间复杂度 :
- 最好情况(数组已完全有序):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));
}
}
代码说明
- 外层循环:控制排序的轮次,最多需要 n-1 轮(每轮至少确定一个元素的最终位置)
- 内层循环 :负责相邻元素的比较和交换,每轮的遍历范围会随着已排序元素的增加而缩小(
n-1-i
) - 优化机制 :通过
swapped
变量判断本轮是否发生交换,若未发生交换,说明数组已完全有序,可直接退出循环,避免不必要的遍历
七、堆排序
堆排序(Heap Sort)是一种基于堆数据结构的高效排序算法,其核心思想是利用堆的特性(大顶堆或小顶堆),反复提取堆顶元素(最大或最小元素),并调整剩余元素维持堆结构,最终得到有序数组。
堆排序的基本步骤
- 构建堆:将待排序数组转换为大顶堆(父节点大于子节点)
- 提取堆顶元素:将堆顶元素(最大值)与堆尾元素交换,此时最大值已放到正确位置
- 调整堆:将剩余元素重新调整为大顶堆
- 重复操作:不断重复步骤 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));适合处理海量数据。
- 局限:不是稳定排序;元素访问模式对缓存不友好(跳跃式访问);常数因子较大,实际性能略逊于快速排序。