冒泡排序
冒泡排序是一种基础的排序算法,它的思想是通过相邻元素的比较和交换,将较大的元素逐步"冒泡"到数组末尾。
冒泡排序的步骤
- 从数组的第一个元素开始,逐一比较相邻的两个元素。
- 如果前一个元素大于后一个元素,就交换它们的位置。
- 对每一轮操作后,最大的元素会被"冒泡"到数组的最后。
- 重复上述操作,直到数组完全有序。
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;
// 外层循环控制排序轮数
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;
}
}
}
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));
}
}
复杂度分析
时间复杂度:
- 最坏情况:𝑂(𝑛2) (完全逆序时,每次都需要比较和交换)。
- 最好情况:𝑂(𝑛)(数组本身已排序,第一轮检测到未发生交换即可退出)。
- 平均情况:𝑂(𝑛2)。
空间复杂度:
- 𝑂(1)(原地排序,无需额外空间)。
稳定性分析
冒泡排序是一个 稳定的排序算法,因为相同的元素在比较时不会发生位置交换,保持了它们的相对顺序。
选择排序
选择排序是一种简单直观的排序算法,它的基本思想是:每次从未排序部分中选出最小(或最大)的元素,放到已排序部分的末尾。
选择排序的步骤
- 从数组的第一个元素开始。
- 在未排序部分中找到最小的元素,将它与当前未排序部分的第一个元素交换。
- 将指针向后移动到下一个元素,重复步骤 2,直到所有元素都排序完成。
java
import java.util.Arrays;
public class SelectionSort {
public static void selectionSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return; // 如果数组为空或只有一个元素,无需排序
}
// 遍历数组
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i; // 假设当前索引 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;
}
}
}
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),因为总是需要两层嵌套循环。
空间复杂度:
- 𝑂(1)(原地排序,无需额外空间)。
稳定性分析
选择排序是一种 不稳定的排序算法,因为在交换操作时,可能会破坏相同元素的相对顺序。
示例: 数组 4_𝐴,4_𝐵,3,在第一次选择过程中,3 会与 4_A 交换位置,导致相同的 4_A 和 4_B 的相对顺序被打破。
插入排序
插入排序是一种简单直观的排序算法,适用于小规模数据的排序。它的工作原理是:从第二个元素开始,逐步将元素插入到前面已经排好序的部分中。
插入排序的步骤
- 从数组的第二个元素开始(假设第一个元素已经有序)。
- 将当前元素与前面的元素比较,如果当前元素较小,就将前面的元素后移。
- 重复上述步骤,直到找到合适的位置,将当前元素插入。
- 对数组中剩余的元素重复此过程,直到整个数组有序。
java
import java.util.Arrays;
public class InsertionSort {
public static void insertionSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return; // 空数组或只有一个元素,不需要排序
}
// 从第二个元素开始
for (int i = 1; i < arr.length; i++) {
int current = arr[i]; // 当前待插入的元素
int j = i - 1;
// 比较并将前面较大的元素后移
while (j >= 0 && arr[j] > current) {
arr[j + 1] = arr[j];
j--;
}
// 找到插入位置
arr[j + 1] = current;
}
}
public static void main(String[] args) {
// 测试用例
int[] arr = {5, 2, 9, 1, 5, 6};
System.out.println("排序前: " + Arrays.toString(arr));
insertionSort(arr);
System.out.println("排序后: " + Arrays.toString(arr));
}
}
复杂度分析
时间复杂度:
- 最坏情况:𝑂(𝑛2)(当输入数组是倒序时)。
- 最好情况:𝑂(𝑛)(当输入数组本身是有序时)。
- 平均情况:𝑂(𝑛2)。
空间复杂度:
- 𝑂(1)(原地排序,无需额外空间)。
稳定性分析
插入排序是一种稳定的排序算法。它的稳定性来源于它总是在不打破已排序部分元素的相对顺序的前提下,插入新元素。
归并排序
归并排序是一种分治算法,将数组分成两个子数组分别排序,然后将它们合并成一个有序数组。它的时间复杂度为 𝑂(𝑛log𝑛),空间复杂度为
𝑂(𝑛),是一个稳定的排序算法。
归并排序的步骤
- 分割:将数组递归地分成两半,直到每个子数组只有一个元素。
- 合并:从最小的子数组开始,合并两个有序子数组,形成一个新的有序数组。
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]; // 辅助数组,用于合并时的临时存储
mergeSort(arr, 0, arr.length - 1, temp);
}
// 递归拆分数组并排序
private static void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left >= right) {
return; // 递归结束条件
}
int mid = left + (right - left) / 2; // 中间索引,避免整型溢出
// 分治法:递归对左右两部分进行排序
mergeSort(arr, left, mid, temp); // 排序左半部分
mergeSort(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 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++];
}
// 将辅助数组中的排序结果复制回原数组
System.arraycopy(temp, 0, arr, left, k);
}
public static void main(String[] args) {
// 测试用例
int[] arr = {38, 27, 43, 3, 9, 82, 10};
System.out.println("排序前: " + Arrays.toString(arr));
mergeSort(arr);
System.out.println("排序后: " + Arrays.toString(arr));
}
}
复杂度分析
时间复杂度:
- 每次划分数组需要 𝑂(log𝑛) 次递归。
- 每次合并操作需要 𝑂(𝑛)。
- 总时间复杂度为 𝑂(𝑛log𝑛)。
空间复杂度:
- 需要辅助数组存储合并时的中间结果,因此空间复杂度为 𝑂(𝑛)。
稳定性分析
归并排序是稳定的排序算法。在合并过程中,相同元素的相对顺序保持不变,因为左子数组的元素先被加入辅助数组。
快速排序
快速排序是通过选取一个基准值(pivot),将数组分成小于基准值和大于基准值的两部分,并递归地对这两部分进行排序。
快速排序的步骤
- 选择基准值(Pivot) :
- 通常选取数组的第一个元素、最后一个元素、中间元素,或随机选取一个元素作为基准值。
- 分区(Partition) :
- 将数组中小于基准值的元素移到左边,大于基准值的元素移到右边。
- 递归排序 :
- 分别对基准值左边和右边的子数组进行快速排序,直到子数组的大小为 1 或 0。
java
import java.util.Arrays;
public class QuickSort {
// 主函数:调用快速排序
public static void quickSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return; // 空数组或单元素数组无需排序
}
quickSort(arr, 0, arr.length - 1);
}
// 快速排序的递归实现
private static 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 static int partition(int[] arr, int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为基准值
int i = low - 1; // i 用于追踪小于基准值的区域
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) { // 如果当前元素小于等于基准值
i++;
// 交换元素
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; // 返回基准值的位置
}
public static void main(String[] args) {
// 测试用例
int[] arr = {10, 80, 30, 90, 40, 50, 70};
System.out.println("排序前: " + Arrays.toString(arr));
quickSort(arr);
System.out.println("排序后: " + Arrays.toString(arr));
}
}
复杂度分析
时间复杂度:
- 最坏情况:𝑂(𝑛2)(当数组已经有序,且每次选择的基准值是最大或最小值时)。
- 平均情况:𝑂(𝑛log𝑛)。
- 最好情况:𝑂(𝑛log𝑛)(每次均匀分割数组时)。
空间复杂度:
- 递归调用栈的空间复杂度:𝑂(log𝑛)(平均情况)。
稳定性分析
快速排序是不稳定的排序算法。因为在交换元素时,可能会改变相同元素的相对顺序。
随机快排
随机快速排序的关键在于随机选择基准值(pivot),以减少最坏情况下(例如数组已经有序时)的时间复杂度。通过随机选择基准值,可以有效地避免划分不平衡的情况,从而提高排序效率。
java
import java.util.Arrays;
import java.util.Random;
public class RandomizedQuickSort {
// 主函数:调用快速排序
public static void quickSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return; // 空数组或单元素数组无需排序
}
quickSort(arr, 0, arr.length - 1);
}
// 快速排序的递归实现
private static void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 分区操作,返回基准值的正确位置
int pivotIndex = randomizedPartition(arr, low, high);
// 对基准值左边的子数组进行快速排序
quickSort(arr, low, pivotIndex - 1);
// 对基准值右边的子数组进行快速排序
quickSort(arr, pivotIndex + 1, high);
}
}
// 随机选择基准值并分区
private static int randomizedPartition(int[] arr, int low, int high) {
// 随机选择一个索引作为基准值
Random random = new Random();
int randomIndex = low + random.nextInt(high - low + 1);
// 将随机选中的基准值与最后一个元素交换位置
int temp = arr[randomIndex];
arr[randomIndex] = arr[high];
arr[high] = temp;
// 使用普通分区函数进行分区
return partition(arr, low, high);
}
// 分区函数:将数组按基准值分为两部分
private static int partition(int[] arr, int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为基准值
int i = low - 1; // i 用于追踪小于基准值的区域
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) { // 如果当前元素小于等于基准值
i++;
// 交换元素
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; // 返回基准值的位置
}
public static void main(String[] args) {
// 测试用例
int[] arr = {10, 80, 30, 90, 40, 50, 70};
System.out.println("排序前: " + Arrays.toString(arr));
quickSort(arr);
System.out.println("排序后: " + Arrays.toString(arr));
}
}
随机快排的优点
- 避免最坏情况 :
- 通过随机化选择基准值,均匀分布划分的概率增大,避免最坏时间复杂度 𝑂(𝑛2) 的情况。
- 时间复杂度 :
- 平均情况:𝑂(𝑛log𝑛)。
- 最坏情况:在随机化后几乎不可能出现,但理论上仍是 𝑂(𝑛2)。
- 额外开销 :
- 随机化的开销是微小的,影响可以忽略不计。
堆排序
堆排序的主要思想是利用堆(大顶堆或小顶堆)来对数组进行排序。
java
import java.util.Arrays;
public class HeapSort {
public static void heapSort(int[] arr) {
int n = arr.length;
// Step 1: Build a max heap
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// Step 2: Extract elements from heap one by one
for (int i = n - 1; i > 0; i--) {
// Move current root to the end
swap(arr, 0, i);
// Call heapify on the reduced heap
heapify(arr, i, 0);
}
}
// Heapify a subtree rooted at node i
private static void heapify(int[] arr, int n, int i) {
int largest = i; // Initialize largest as root
int left = 2 * i + 1; // Left child
int right = 2 * i + 2; // Right child
// If left child is larger than root
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
// If right child is larger than largest so far
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
// If largest is not root
if (largest != i) {
swap(arr, i, largest);
// Recursively heapify the affected subtree
heapify(arr, n, largest);
}
}
// Helper method to swap two elements in the array
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// Main method to test the heap sort implementation
public static void main(String[] args) {
int[] arr = {12, 11, 13, 5, 6, 7};
System.out.println("Original array: " + Arrays.toString(arr));
heapSort(arr);
System.out.println("Sorted array: " + Arrays.toString(arr));
}
}
复杂度分析
时间复杂度:
- 最坏情况:𝑂(𝑛log𝑛)。
- 平均情况:𝑂(𝑛log𝑛)。
- 最好情况:𝑂(𝑛log𝑛)。
空间复杂度: 𝑂(1)
- 堆排序是一种原地排序算法,在排序过程中只需要常数级别的额外空间用于交换元素。
稳定性分析
- 堆排序不是稳定排序算法。
- 在堆化过程中,两个相等的元素可能会因为交换位置而改变它们的相对顺序。
基数排序和分布式排序
这些算法适用于特定数据类型,通常基于非比较的方式。
基数排序(Radix Sort)
- 思想: 按位(如个位、十位)对数字进行多轮排序。
- 时间复杂度: 𝑂(𝑛𝑘),其中 𝑘 是数字位数
- 空间复杂度: 𝑂(𝑛+𝑘)
- 稳定性: 稳定
java
import java.util.*;
// 适合对整数进行排序
public class RadixSort {
public static void radixSort(int[] arr) {
if (arr == null || arr.length == 0) return;
// 找到最大值,确定最大位数
int max = Arrays.stream(arr).max().orElse(0);
int maxDigit = String.valueOf(max).length();
int mod = 10, div = 1;
List<List<Integer>> buckets = new ArrayList<>();
// 初始化桶
for (int i = 0; i < 10; i++) {
buckets.add(new ArrayList<>());
}
for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
// 分配到桶中
for (int num : arr) {
int bucketIndex = (num % mod) / div;
buckets.get(bucketIndex).add(num);
}
// 收集桶中的元素回到数组
int index = 0;
for (List<Integer> bucket : buckets) {
for (int num : bucket) {
arr[index++] = num;
}
bucket.clear();
}
}
}
public static void main(String[] args) {
int[] arr = {170, 45, 75, 90, 802, 24, 2, 66};
radixSort(arr);
System.out.println(Arrays.toString(arr)); // 输出 [2, 24, 45, 66, 75, 90, 170, 802]
}
}
计数排序(Counting Sort)
- 思想: 统计每个元素出现的次数,根据统计结果直接放置元素。
- 时间复杂度: 𝑂(𝑛+𝑘),其中 𝑘 是数据范围
- 空间复杂度: 𝑂(𝑛+𝑘)
- 稳定性: 稳定
java
import java.util.Arrays;
// 适合数据范围有限且非负的整数
public class CountingSort {
public static void countingSort(int[] arr) {
if (arr == null || arr.length == 0) return;
// 找到数组中的最大值和最小值
int max = Arrays.stream(arr).max().orElse(0);
int min = Arrays.stream(arr).min().orElse(0);
int range = max - min + 1;
// 创建计数数组
int[] count = new int[range];
for (int num : arr) {
count[num - min]++;
}
// 累加计数数组
for (int i = 1; i < count.length; i++) {
count[i] += count[i - 1];
}
// 临时结果数组
int[] result = new int[arr.length];
for (int i = arr.length - 1; i >= 0; i--) {
result[count[arr[i] - min] - 1] = arr[i];
count[arr[i] - min]--;
}
// 将结果拷贝回原数组
System.arraycopy(result, 0, arr, 0, arr.length);
}
public static void main(String[] args) {
int[] arr = {4, 2, 2, 8, 3, 3, 1};
countingSort(arr);
System.out.println(Arrays.toString(arr)); // 输出 [1, 2, 2, 3, 3, 4, 8]
}
}
桶排序(Bucket Sort)
- 思想: 将数据分到多个桶中,每个桶内单独排序,再合并结果。
- 时间复杂度: 最优 𝑂(𝑛),最坏 𝑂(𝑛2)
- 空间复杂度: 𝑂(𝑛+𝑘)
- 稳定性: 稳定
java
import java.util.*;
// 适合小数或均匀分布的数值。
public class BucketSort {
public static void bucketSort(double[] arr) {
if (arr == null || arr.length == 0) return;
int n = arr.length;
List<List<Double>> buckets = new ArrayList<>();
// 初始化桶
for (int i = 0; i < n; i++) {
buckets.add(new ArrayList<>());
}
// 分配元素到对应桶
for (double num : arr) {
int bucketIndex = (int) (num * n); // 桶索引计算
buckets.get(bucketIndex).add(num);
}
// 每个桶内排序
for (List<Double> bucket : buckets) {
Collections.sort(bucket);
}
// 收集桶内元素回到数组
int index = 0;
for (List<Double> bucket : buckets) {
for (double num : bucket) {
arr[index++] = num;
}
}
}
public static void main(String[] args) {
double[] arr = {0.42, 0.32, 0.23, 0.52, 0.25, 0.47, 0.51};
bucketSort(arr);
System.out.println(Arrays.toString(arr)); // 输出 [0.23, 0.25, 0.32, 0.42, 0.47, 0.51, 0.52]
}
}
特殊排序算法
希尔排序(Shell Sort)
- 思想: 是插入排序的优化版,通过逐步减少的"增量"分组实现高效排序。
- 时间复杂度: 𝑂(𝑛3/2) 或 𝑂(𝑛log2𝑛)(具体取决于增量序列)
- 空间复杂度: 𝑂(1)
- 稳定性: 不稳定
java
import java.util.Arrays;
public class ShellSort {
public static void shellSort(int[] arr) {
int n = arr.length;
// 初始间隔 gap 设置为数组长度的一半
for (int gap = n / 2; gap > 0; gap /= 2) {
// 从 gap 开始,对每组进行插入排序
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j = i;
// 对间隔为 gap 的元素进行插入排序
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
}
}
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));
}
}
外部排序
适用于无法将所有数据一次性加载到内存的场景,常用归并排序的变种。
例如:多路归并排序、磁盘排序。