冒泡排序(Bubble Sort)
冒泡排序是一种简单的排序算法,它重复地走访过要排序的数组,一次比较两个相邻的元素,如果它们的顺序错误就把它们交换过来。走访数组的工作是重复地进行直到没有再需要交换,也就是说该数组已经排序完成。
实现代码
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};
bubbleSort(arr);
System.out.println("排序后的数组:");
for (int num : arr) {
System.out.print(num + " ");
}
}
}
复杂度分析
- 时间复杂度:最坏情况和平均情况都是 O (n²),最好情况(数组已经有序)是 O (n)。
- 空间复杂度:O (1),只需要一个临时变量用于交换元素。
适用场景
冒泡排序适用于数据量较小的情况,或者作为入门级的排序算法来理解排序的基本思想。由于其时间复杂度较高,在处理大规模数据时效率较低,不建议使用。
选择排序(Selection Sort)
选择排序的基本思想是:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
实现代码
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;
}
}
// 交换最小元素和未排序部分的第一个元素
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
public static void main(String[] args) {
int[] arr = {64, 25, 12, 22, 11};
selectionSort(arr);
System.out.println("排序后的数组:");
for (int num : arr) {
System.out.print(num + " ");
}
}
}
复杂度分析
- 时间复杂度:无论什么情况,时间复杂度都是 O (n²),因为它需要进行 n-1 轮选择,每轮选择都要遍历剩余的元素。
- 空间复杂度:O (1),只需要一个临时变量用于交换元素。
适用场景
选择排序和冒泡排序类似,适用于数据量较小的场景。与冒泡排序相比,选择排序的交换次数更少,在某些情况下性能会略好一些,但总体来说效率仍然不高。
插入排序(Insertion Sort)
插入排序的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
实现代码
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 key = arr[i];
int j = i - 1;
// 将大于key的元素向后移动一位
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
public static void main(String[] args) {
int[] arr = {12, 11, 13, 5, 6};
insertionSort(arr);
System.out.println("排序后的数组:");
for (int num : arr) {
System.out.print(num + " ");
}
}
}
复杂度分析
- 时间复杂度:最坏情况(数组逆序)是 O (n²),平均情况是 O (n²),最好情况(数组已经有序)是 O (n)。
- 空间复杂度:O (1),只需要一个临时变量存储当前要插入的元素。
适用场景
插入排序在处理接近有序的数据时效率很高,因为此时需要移动的元素很少。它也适用于数据量较小的情况,在实际开发中,对于一些小规模的数据集,插入排序是一个不错的选择。此外,插入排序还是一些高级排序算法(如归并排序、快速排序)的基础。
快速排序(Quick Sort)
快速排序是一种分治法的排序算法。它的基本思想是:选择一个元素作为 “基准”(pivot),然后将数组分为两部分,一部分所有元素都小于基准,另一部分所有元素都大于基准,然后递归地对这两部分进行排序。
实现代码
public class QuickSort {
public 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;
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, 7, 8, 9, 1, 5};
int n = arr.length;
quickSort(arr, 0, n - 1);
System.out.println("排序后的数组:");
for (int num : arr) {
System.out.print(num + " ");
}
}
}
复杂度分析
- 时间复杂度:平均情况是 O (nlogn),最坏情况(数组已经有序或逆序,且选择最左或最右元素作为基准)是 O (n²)。在实际应用中,通过合理选择基准元素(如随机选择、三数取中),可以避免最坏情况的发生,使得时间复杂度接近 O (nlogn)。
- 空间复杂度:主要取决于递归调用的栈深度,平均情况是 O (logn),最坏情况是 O (n)。
适用场景
快速排序是一种高效的排序算法,适用于处理大规模数据。它在大多数情况下性能都很好,是实际开发中常用的排序算法之一。不过,由于其递归实现,在处理极大规模数据时可能会出现栈溢出的问题,此时可以考虑使用非递归实现或者其他排序算法。
归并排序(Merge Sort)
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。归并排序的基本思想是:将待排序序列分成两个长度相等的子序列,对这两个子序列分别进行归并排序,然后将排序好的子序列合并成一个最终的排序序列。
实现代码
public class MergeSort {
public 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 n1 = mid - left + 1;
int n2 = right - mid;
// 创建临时数组
int[] L = new int[n1];
int[] R = new int[n2];
// 将数据复制到临时数组
for (int i = 0; i < n1; ++i) {
L[i] = arr[left + i];
}
for (int j = 0; j < n2; ++j) {
R[j] = arr[mid + 1 + j];
}
// 合并临时数组到原数组
int i = 0, j = 0;
int k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}
// 复制剩余的元素
while (i < n1) {
arr[k] = L[i];
i++;
k++;
}
while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
}
public static void main(String[] args) {
int[] arr = {12, 11, 13, 5, 6, 7};
int n = arr.length;
mergeSort(arr, 0, n - 1);
System.out.println("排序后的数组:");
for (int num : arr) {
System.out.print(num + " ");
}
}
}
复杂度分析
- 时间复杂度:无论什么情况,时间复杂度都是 O (nlogn),因为它总是将数组分成两个子数组,然后进行合并,合并的时间复杂度是 O (n)。
- 空间复杂度:O (n),需要额外的空间来存储临时数组。
适用场景
归并排序是一种稳定的排序算法,适用于处理大规模数据,尤其是在需要稳定排序的场景中。不过,由于其空间复杂度较高,在对内存使用有严格限制的情况下,可能不是最佳选择。
总结
以上介绍了 Java 中几种常见的排序方法,每种排序方法都有其特点和适用场景:
- 冒泡排序、选择排序和插入排序属于简单排序算法,时间复杂度较高,适用于数据量较小的情况。
- 快速排序和归并排序属于高级排序算法,时间复杂度较低,适用于处理大规模数据。其中,快速排序在大多数情况下性能更好,但不稳定;归并排序稳定,但空间复杂度较高。
在实际开发中,应根据数据的规模、是否需要稳定排序以及对内存的要求等因素来选择合适的排序方法。此外,Java 的Arrays类中也提供了sort()方法,该方法在不同的 JDK 版本中可能采用不同的排序算法(如快速排序、归并排序、TimSort 等),在大多数情况下,使用Arrays.sort()方法是一个方便且高效的选择。