深度剖析八大排序算法
欢迎并且感谢大家指出我的问题,由于本人水平有限,有些内容写的不是很全面,只是把比较实用的东西给写下来,如果有写的不对的地方,还希望各路大牛多多指教!谢谢大家!🥰
在计算机科学领域,排序算法是基础且重要的算法类别,它能够将一组无序的数据按照特定的顺序(如升序或降序)重新排列。接下来,我们将详细探讨冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、堆排序和计数排序这八大排序算法。
冒泡排序
基本概念
冒泡排序是一种简单的比较排序算法,它通过多次比较相邻元素并交换位置,将最大(或最小)的元素逐步 “冒泡” 到数组的末尾。
核心思想
重复地走访要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
算法步骤
- 比较相邻的元素。如果第一个比第二个大(升序),就交换它们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
代码实现(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]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
}
优缺点及适用场景
- 优点:实现简单,代码量少,比较稳定(相同元素的相对顺序在排序前后不会改变)。
- 缺点:时间复杂度较高,为 O (n²),在数据量较大时效率较低。
- 适用场景:适用于数据量较小且对稳定性有要求的场景。
选择排序
基本概念
选择排序是一种简单直观的排序算法,它在每一趟选择未排序序列中最小(或最大)的元素,存放到已排序序列的末尾。
核心思想
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
算法步骤
- 初始状态:无序区为整个数组,有序区为空。
- 第 1 趟排序:在无序区中选出最小(大)的元素,将它与无序区的第 1 个元素交换,此时有序区增加 1 个元素,无序区减少 1 个元素。
- 重复第 2 步,直到无序区为空。
代码实现(Java)
public class SelectionSort {
public static void selectionSort(int[] arr) {
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;
}
}
}
优缺点及适用场景
- 优点:简单直观,无论什么数据进去都是 O (n²) 的时间复杂度,所以如果数据规模较小,可以考虑使用。
- 缺点:时间复杂度高,同样为 O (n²),且不稳定。
- 适用场景:适用于数据量较小,对时间复杂度要求不高,且对稳定性没有要求的场景。
插入排序
基本概念
插入排序是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据。
核心思想
把 n 个待排序的元素看成一个有序表和一个无序表。开始时有序表中只包含 1 个元素,无序表中包含 n - 1 个元素;排序过程中每次从无序表中取出第一个元素,将它插入到有序表中的适当位置,使之成为新的有序表,重复 n - 1 次可完成排序过程。
算法步骤
- 从第一个元素开始,该元素可以认为已经被排序。
- 取出下一个元素,在已经排序的元素序列中从后向前扫描。
- 如果已排序元素大于新元素,将已排序元素移到下一位置。
- 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置。
- 将新元素插入到该位置后。
- 重复步骤 2~5。
代码实现(Java)
public class InsertionSort {
public static void insertionSort(int[] arr) {
int n = arr.length;
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = key;
}
}
}
优缺点及适用场景
- 优点:在数据量较小或者部分有序的情况下,效率较高,并且是稳定的排序算法。
- 缺点:时间复杂度在最坏情况下为 O (n²),不适用于大规模数据排序。
- 适用场景:适用于数据量较小、部分有序的数据排序,以及对稳定性有要求的场景。
希尔排序
基本概念
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。它通过将原始数据分成多个子序列,对每个子序列进行插入排序,逐步减少增量,最终使整个序列有序。
核心思想
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录 “基本有序” 时,再对全体记录进行依次直接插入排序。
算法步骤
- 选择一个增量序列 t1,t2,…,tk,其中 ti > tj, tk = 1。
- 按增量序列个数 k,对序列进行 k 趟排序。
- 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
代码实现(Java)
public class ShellSort {
public static void shellSort(int[] arr) {
int n = arr.length;
for (int gap = n / 2; gap > 0; gap /= 2) {
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j;
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
arr[j] = arr[j - gap];
}
arr[j] = temp;
}
}
}
}
优缺点及适用场景
- 优点:希尔排序的时间复杂度比 O (n²) 要好很多,在中等规模数据下表现良好,并且实现相对简单。
- 缺点:希尔排序是不稳定的排序算法,并且其时间复杂度分析较为复杂。
- 适用场景:适用于中等规模数据的排序,对稳定性要求不高的场景。
快速排序
基本概念
快速排序是一种分治的排序算法,它通过选择一个基准元素,将数组分为两部分,使得左边部分的元素都小于基准元素,右边部分的元素都大于基准元素,然后递归地对左右两部分进行排序。
核心思想
- 从数列中挑出一个基准值(通常选择第一个元素或最后一个元素)。
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区操作。
- 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序。
算法步骤
- 选择基准值 pivot。
- 初始化两个指针 left 和 right,left 指向数组的起始位置,right 指向数组的末尾位置。
- 从 right 开始,向左移动 right 指针,直到找到一个小于 pivot 的元素;从 left 开始,向右移动 left 指针,直到找到一个大于 pivot 的元素。
- 如果 left < right,交换这两个元素,然后继续步骤 3。
- 当 left >= right 时,将 pivot 与 left 指向的元素交换,此时 pivot 左边的元素都小于它,右边的元素都大于它。
- 递归地对 pivot 左边和右边的子数组进行快速排序。
代码实现(Java)
public class QuickSort {
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 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;
}
}
优缺点及适用场景
- 优点:平均时间复杂度为 O (nlogn),在大多数情况下表现非常高效,并且是一种原地排序算法(不需要额外的大量辅助空间)。
- 缺点:在最坏情况下(如数组已经有序),时间复杂度会退化为 O (n²),并且快速排序是不稳定的排序算法。
- 适用场景:适用于大规模数据的排序,对稳定性没有要求的场景。
归并排序
基本概念
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。它将一个数组分成两个子数组,对每个子数组进行排序,然后将排序好的子数组合并成一个最终的有序数组。
核心思想
- 把长度为 n 的输入序列分成两个长度为 n/2 的子序列。
- 对这两个子序列分别采用归并排序。
- 将两个排序好的子序列合并成一个最终的有序序列。
算法步骤
- 分解:将数组不断地分成两个子数组,直到子数组的长度为 1。
- 合并:将两个有序的子数组合并成一个更大的有序数组。合并时,比较两个子数组的元素,将较小的元素依次放入一个临时数组中,直到其中一个子数组的元素全部放入临时数组,然后将另一个子数组的剩余元素也放入临时数组。
- 将临时数组中的元素复制回原数组,完成一次合并。
- 重复步骤 2 和 3,直到所有子数组都合并成一个有序数组。
代码实现(Java)
public class MergeSort {
public static void mergeSort(int[] arr) {
int n = arr.length;
if (n < 2) {
return;
}
int mid = n / 2;
int[] left = new int[mid];
int[] right = new int[n - mid];
System.arraycopy(arr, 0, left, 0, mid);
System.arraycopy(arr, mid, right, 0, n - mid);
mergeSort(left);
mergeSort(right);
merge(arr, left, right);
}
private static void merge(int[] arr, int[] left, int[] right) {
int i = 0, j = 0, k = 0;
while (i < left.length && j < right.length) {
if (left[i] < right[j]) {
arr[k] = left[i];
i++;
} else {
arr[k] = right[j];
j++;
}
k++;
}
while (i < left.length) {
arr[k] = left[i];
i++;
k++;
}
while (j < right.length) {
arr[k] = right[j];
j++;
k++;
}
}
}
优缺点及适用场景
- 优点:时间复杂度始终为 O (nlogn),并且是稳定的排序算法,适用于大规模数据的排序。
- 缺点:需要额外的空间来存储临时数组,空间复杂度为 O (n)。
- 适用场景:适用于对稳定性有要求,数据量较大的场景,如外部排序。
堆排序
基本概念
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
核心思想
- 首先将待排序的数组构建成一个最大堆(升序排序时)或最小堆(降序排序时)。
- 然后将堆顶元素与堆的最后一个元素交换,此时堆的最后一个元素就是最大(或最小)的元素,将其从堆中移除(实际上是缩小堆的范围)。
- 对剩余的元素重新调整为最大堆(或最小堆),重复步骤 2,直到堆中只剩下一个元素,此时数组已经有序。
算法步骤
- 初始化堆:将数组构建成一个最大堆(或最小堆)。从最后一个非叶子节点开始,依次对每个非叶子节点进行向下调整操作,使其满足堆的性质。
- 交换与调整:将堆顶元素与堆的最后一个元素交换,然后对堆顶元素进行向下调整操作,使堆重新满足堆的性质。
- 重复步骤 2,直到堆中只剩下一个元素。
代码实现(Java)
public class HeapSort {
public static void heapSort(int[] arr) {
int n = arr.length;
// 构建最大堆
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 一个个取出元素
for (int i = n - 1; i > 0; i--) {
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 调整堆
heapify(arr, i, 0);
}
}
private static void heapify(int[] arr, int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
if (largest!= i) {
int swap = arr[i];
arr[i] = arr[largest];
arr[largest] = swap;
// 递归调整受影响的子树
heapify(arr, n, largest);
}
}
}
优缺点及适用场景
- 优点:时间复杂度为 O (nlogn),并且是原地排序算法,不需要额外的大量辅助空间。
- 缺点:堆排序是不稳定的排序算法,并且实现相对复杂。
- 适用场景:适用于大规模数据的排序,对稳定性没有要求,并且空间有限的场景。
计数排序
基本概念
计数排序是一种非比较排序算法,它通过统计每个元素在数组中出现的次数,然后根据统计结果将元素按照顺序输出,从而实现排序。
核心思想
1.找出待排序数组中的最大值和最小值,确定计数数组的范围。
2.统计每个元素在数组中出现的次数,将统计结果存储在计数数组中。
3.根据计数数组,将每个元素按照出现的次数依次输出到结果数组中。
算法步骤
- 确定范围:遍历待排序数组,找出其中的最大值max和最小值min,计算出计数数组的长度countLength = max - min + 1。
- 初始化计数数组:创建一个长度为countLength的计数数组count,并将所有元素初始化为 0。
- 统计元素出现次数:再次遍历待排序数组,对于数组中的每个元素num,将count[num - min]的值加 1,表示该元素出现的次数。
- 计算前缀和:对计数数组进行处理,计算每个元素的前缀和。即从计数数组的第二个元素开始,每个元素都加上前一个元素的值。这一步的目的是让count[i]表示小于等于i + min的元素个数。
- 填充结果数组:创建一个与待排序数组长度相同的结果数组result。从待排序数组的末尾开始遍历,对于每个元素num,根据count[num - min]的值确定其在结果数组中的位置,然后将count[num - min]减 1。重复此步骤,直到所有元素都放入结果数组中。
代码实现
-
public class CountingSort { public static void countingSort(int[] arr) { if (arr == null || arr.length == 0) { return; } int max = arr[0]; int min = arr[0]; // 找出最大值和最小值 for (int num : arr) { if (num > max) { max = num; } if (num < min) { min = num; } } int countLength = max - min + 1; int[] count = new int[countLength]; // 统计每个元素出现的次数 for (int num : arr) { count[num - min]++; } // 计算前缀和 for (int i = 1; i < countLength; 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); } }
优缺点及适用场景
- 优点:时间复杂度为 O (n + k),其中 n 是待排序数组的长度,k 是数据的范围。在数据范围不大且数据量较大时,效率非常高。并且计数排序是稳定的排序算法。
- 缺点:对数据的要求比较苛刻,只能用于数据范围较小且数据为整数的情况。同时,需要额外的空间来存储计数数组,空间复杂度为 O (k) 。
- 适用场景:适用于数据范围有限,且对稳定性有要求的场景,比如对学生成绩(分数范围固定)进行排序等。