关注微信公众号 “程序员小胖” 每日技术干货,第一时间送达!
引言
排序算法是计算机科学中的基础知识,用于将一组数据按照特定顺序重新排列。
冒泡排序(Bubble Sort)
冒泡排序(Bubble Sort)是一种简单的排序算法,它通过重复遍历待排序的数列,比较相邻的两个元素,并在必要时交换它们的位置。算法的执行过程如下:
- 从第一个元素开始,比较相邻的两个元素。
- 如果第一个元素比第二个元素大,就交换它们的位置。
- 移动到下一对相邻元素,重复步骤2,直到数列的末尾。
- 一轮比较结束后,最大的元素会“冒泡”到数列的最后位置。
- 重复步骤1到4,直到整个数列有序。
冒泡排序的时间复杂度为O(n^2),在最坏的情况下(数列完全逆序)需要进行n-1轮比较和交换。在最好的情况下(数列已经有序),时间复杂度为O(n),因为一轮遍历后没有发生交换,算法会提前结束。
java代码示例
public class BubbleSort {
public static void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交换 arr[j] 和 arr[j+1]
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
public static void main(String[] args) {
int[] arr = {64, 34, 25, 12, 22, 11, 90};
bubbleSort(arr);
System.out.println("Sorted array: ");
for (int i : arr) {
System.out.print(i + " ");
}
}
}
bubbleSort方法接受一个整数数组arr作为参数,并对其进行原地排序。main方法中创建了一个示例数组,并调用bubbleSort方法进行排序,然后打印排序后的数组。
冒泡排序虽然在实际应用中效率不高,但由于其算法的简单性,常被用于教学和学习算法的基本概念。在理解了冒泡排序的原理后,可以尝试实现其他更高效的排序算法,如快速排序、归并排序等。
选择排序(Selection Sort)
选择排序(Selection Sort)是一种简单直观的排序算法。它的工作原理是每次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。选择排序是不稳定的排序方法(即相等的元素在排序后可能会改变原来的相对位置)。
算法步骤:
- 在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置。
- 从剩余未排序元素中继续寻找最小(或最大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
时间复杂度:
- 最好情况:O(n^2)
- 最坏情况:O(n^2)
- 平均情况:O(n^2)
尽管选择排序的时间复杂度在最好、最坏和平均情况下都是O(n2),但由于交换操作的次数较少,它在某些情况下可能比其他O(n2)算法更快。
java代码示例
public class SelectionSort {
public static void selectionSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
// 寻找[i, n-1]区间里的最小值的索引
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j; // 将最小值的索引更新为j
}
}
// 将找到的最小值和第i位置所在的值交换
if (minIndex != i) {
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
}
public static void main(String[] args) {
int[] arr = {64, 34, 25, 12, 22, 11, 90};
selectionSort(arr);
System.out.println("Sorted array: ");
for (int value : arr) {
System.out.print(value + " ");
}
}
}
selectionSort方法接受一个整数数组arr作为参数,并对其进行原地排序。main方法中创建了一个示例数组,并调用selectionSort方法进行排序,然后打印排序后的数组。
选择排序的一个特点是无论数组的初始顺序如何,它都将进行完整的n^2次比较。因此,它不适合于大型数据集的排序。在实际应用中,更高效的排序算法(如快速排序、归并排序)通常是更好的选择。
插入排序(Insertion Sort)
插入排序(Insertion Sort)是一种简单直观的排序算法,适用于小型数据集或基本有序的数据集。
算法步骤
- 从数组的第二个元素开始(即索引为1的元素),将当前元素保存到一个变量中(称为“key”)。
- 与当前元素左边的元素进行比较,如果左边的元素大于当前元素(key),则将左边的元素向右移动一个位置。
- 重复步骤2,直到找到一个小于或等于当前元素(key)的元素,或者到达数组的开头。
- 将当前元素(key)插入到找到的位置。
- 重复步骤1到4,直到整个数组排序完成。
时间复杂度
平均和最坏情况下的时间复杂度为 O(n^2),其中 n 是数组的长度。最好情况下,如果数组已经是有序的,时间复杂度为 O(n)。
空间复杂度
插入排序是原地排序算法,不需要额外的存储空间,因此空间复杂度为 O(1)。
稳定性
插入排序是稳定的排序算法,相同元素的相对顺序在排序后保持不变。
使用场景
插入排序适用于数据量较小或者数据近乎有序的情况。在实际应用中,插入排序由于其实现简单,通常用作其他复杂排序算法的辅助排序方法。
java代码示例
public class InsertionSort {
public static void main(String[] args) {
int[] array = {5, 2, 9, 1, 5, 6};
insertionSort(array);
System.out.println("Sorted array:");
for (int value : array) {
System.out.print(value + " ");
}
}
public static void insertionSort(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--;
}
// 插入key到正确的位置
arr[j + 1] = key;
}
}
}
快速排序(Quick Sort)
快速排序(Quick Sort)是一种高效的排序算法,采用分治法(Divide and Conquer)的策略来对一个数组进行排序。它的核心思想是选择一个基准元素(pivot),然后将数组分为两个子数组:一个包含所有小于基准元素的值,另一个包含所有大于基准元素的值。这个过程称为分区(partitioning)。之后,递归地对这两个子数组进行快速排序。
算法步骤
- 选择基准元素,可以选择数组中的最后一个元素作为基准。
- 进行分区操作,将数组中的元素根据与基准元素的大小关系分为两个子数组。
- 递归地对这两个子数组进行快速排序。
- 合并两个已排序的子数组。由于是原地排序,这一步在快速排序中通常不需要实际合并操作。
时间复杂度
平均情况下的时间复杂度为 O(n log n),最坏情况下为 O(n^2),但这种情况很少发生。通过随机选择基准或使用三数取中法可以避免最坏情况的发生。
空间复杂度
快速排序是原地排序算法,空间复杂度主要取决于递归调用的深度,最好情况下为 O(log n),最坏情况下为 O(n)。
稳定性
快速排序不是稳定的排序算法,因为相同元素的相对顺序可能会在分区过程中改变。
使用场景
快速排序适用于大型数据集,因为它的平均时间复杂度较低,且在实际应用中表现良好。由于其分治法的特性,快速排序通常比其他 O(n log n) 算法更快。然而,当输入数组中包含大量重复元素时,快速排序的性能可能会下降。在这种情况下,可以考虑使用其他排序算法,如归并排序。
java代码示例
public class QuickSort {
public static void main(String[] args) {
int[] array = {10, 7, 8, 9, 1, 5};
quickSort(array, 0, array.length - 1);
System.out.println("Sorted array:");
for (int value : array) {
System.out.print(value + " ");
}
}
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);
}
}
// 选择数组最后一个元素作为基准进行分区
public 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++;
// 交换 arr[i] 和 arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 交换 arr[i+1] 和 arr[high](基准元素)
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return i + 1;
}
}
总结
在本文中,我们一起探索了多种经典的排序算法,每一种都有其独特的魅力和适用场景。我们了解到,选择合适的排序算法不仅要考虑时间复杂度和空间复杂度,还要考虑算法的稳定性和实际应用需求。通过深入理解这些算法,我们能够更加高效地处理和分析数据,为解决现实世界中的复杂问题提供强有力的工具。希望本文能激发大家对算法学习的热情,并在未来的学习和工作中,将这些知识应用到实践中去。让我们一起期待下一篇关于算法的探索之旅吧!