跳到主要内容9 种常用排序算法总结 | 极客日志Javajava算法
9 种常用排序算法总结
总结了 9 种常用排序算法,包括直接插入、折半插入、希尔、冒泡、快速、选择、堆、归并及基数排序。每种算法均提供了 Java 代码实现,并分析了时间复杂度和空间复杂度。内容涵盖基本思想、排序过程及性能对比,适合算法学习与面试准备。
禅心26K 浏览 一、插入排序
基本思想:每一趟将一个待排序的记录,按其关键字的大小插入到已经排序好的一组记录的适当位置上,直到全部待排序记录全部插入为止。
1.1 直接插入排序
排序过程:
- 将待排序数组 arr[1...n] 看作两个集合,arr[1] 为有序集合中元素,arr[2...n] 为无序集合中元素,a[0] 用来临时存放当前待排序记录
- 外层循环每次从无序集合中选择一个待插入元素(n-1 次),每次使用顺序查找法,内层循环查找 arr[i] 在有序集合中的位置(将有序集合中大于待插入元素的记录后移一位)
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 key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
}
时间复杂度:
- 最好情况下(待排序记录递增有序),总的比较次数为 n-1 次,记录不需要移动。
- 最坏情况下(待排序记录递减有序),总的比较次数和移动次数均为 n^2/2。
- 平均情况下,比较次数和移动次数都是 n^2/4。
空间复杂度:
只需要一个辅助空间 arr[0],因此空间复杂度为 O(1)
1.2 折半插入排序
基本思想同直接查找插入排序,不同点在于,在有序集合中搜索插入位置时折半插入采用二分搜索,可以有效的减少比较次数。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
public static void binaryInsertionSortDetailed(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
for (int i = 1; i < arr.length; i++) {
int key = arr[i];
int insertPos = binarySearch(arr, 0, i - 1, key);
for (int j = i - 1; j > insertPos; j--) {
arr[j + 1] = arr[j];
}
arr[insertPos] = key;
}
}
- 该算法比较次数要小于直接插入排序,平均性能要优于直接插入排序,时间复杂度为 O(n^2)
- 该算法比较次数与待排序列初始排序列无关,依赖于有序序列的元素个数,插入第 i 个元素时比较次数为 logi。折半插入排序的对象移动次数与直接排序相同,依赖对象的初始排列。
只需要一个辅助空间 arr[0],因此空间复杂度为 O(1)。
1.3 希尔排序
通过分析直接插入排序可以得出,待排序记录个数越少、待排序记录中逆序对越少,直接插入排序算法的效率越高。希尔排序正是通过将待排序记录分组来减少记录数量,通过对分组后的每个小组进行直接插入排序来减少逆序对的数量。
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 = i;
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
}
}
}
- 最坏情况,步长序列由 n/2^k 计算得出,为 O(n^2)
- 最好情况,步长序列由下列公式计算得出,为 O(n^(4/3))
仅用 arr[0] 作为辅助空间,因此空间复杂度为 O(1)
二、交换排序
基本思想:两两比较待排序记录关键字,当两个关键字不满足次序要求时进行交换,直到整个序列满足要求为止。
2.1 冒泡排序
两两比较关键字,如逆序则交换顺序,较大关键字逐渐一端移动,直到序列有序。
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 - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
}
- 最好情况(初始序列正序),只需进行一次排序,在排序过程中进行 n-1 次关键字的比较,不移动记录
- 最坏情况(初始序列逆序),进行 n-1 次排序,总的关键字比较次数为 n^2 / 2,记录移动次数为(3n^2)/ 2
- 平均情况,比较次数和记录移动次数分别为 n^2 / 2 ,3n^2 / 4, 时间复杂度为 O(n^2)
仅需 arr[0] 作为交换辅助空间,故空间复杂度为 O(1)
2.2 快速排序
由冒泡排序改进得到,冒泡排序只对相邻两个记录进行比较,因此每次只能消除一个逆序,而快速排序一次交换可消除多个逆序,从而提高排序性能。
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++;
swap(arr, i, j);
}
}
swap(arr, i + 1, high);
return i + 1;
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
- 最好情况(每次排序后序列被分成两个大小大致相等的子表),定位枢轴所需的时间为 O(n),总的排序时间为 O(n log₂n)
- 最坏情况(待排序列有序),递归树成为单支树,关键字的比较次数为 n^2 / 2,这种情况下快速排序的速度已经退化为简单排序的水平。枢轴记录的合理选择可以避免最坏情况的出现,例如,可在待排序列中随机选择枢轴,并将枢轴交换到第一个位置。
- 平均情况,时间复杂度为 O(n log₂n)
快速排序时递归的,最大递归调用次数与递归树的深度一致,因此最好情况为 O(log₂n),最坏情况为 O(n)
三、选择排序
基本思想:每一趟排序从待排序的记录中选出关键字最小的记录,按顺序放在已排序的记录中,直到全部排完为止。
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;
}
}
if (minIndex != i) {
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
}
- 最好情况(正序),记录不需要移动。
- 最坏情况(逆序),移动 3(n-1) 次
无论记录的初始状态如何,所进行的关键字之间的比较次数相同,均为 n^2 / 2,因此时间复杂度为 O(n^2)
仅用 arr[0] 作为交换辅助空间,因此空间复杂度为 O(1)
四、堆排序
每次将堆顶元素取出,与末尾元素交换,调整前 n-1 个元素,使其仍然成堆,重复上述过程,直到剩余元素为 1 时为止,即可得到非递减序列
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 temp = arr[i];
arr[i] = arr[largest];
arr[largest] = temp;
heapify(arr, n, largest);
}
}
}
堆排序平均性能接近于最坏性能,时间复杂度为 O(n log₂n)
仅用 arr[0] 作为交换辅助空间,空间复杂度为 O(1)
五、归并排序
基本思想:假设初始序列含有 n 个记录,则可看成是 n 个有序的子序列,每个子序列的长度为 1,然后两两并归,得到 n/2 个长度为 2 或 1 的有序子序列;如此重复,直至得到一个长度为 n 的有序序列为止。
public class MergeSort {
public static void mergeSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
int[] temp = new int[arr.length];
mergeSort(arr, temp, 0, arr.length - 1);
}
private static void mergeSort(int[] arr, int[] temp, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSort(arr, temp, left, mid);
mergeSort(arr, temp, mid + 1, right);
merge(arr, temp, left, mid, right);
}
}
private static void merge(int[] arr, int[] temp, int left, int mid, int right) {
for (int i = left; i <= right; i++) {
temp[i] = arr[i];
}
int i = left;
int j = mid + 1;
int k = left;
while (i <= mid && j <= right) {
if (temp[i] <= temp[j]) {
arr[k] = temp[i];
i++;
} else {
arr[k] = temp[j];
j++;
}
k++;
}
while (i <= mid) {
arr[k] = temp[i];
i++;
k++;
}
}
public static void main(String[] args) {
int[] arr = {12, 11, 13, 5, 6, 7};
System.out.println("排序前的数组:");
for (int num : arr) {
System.out.print(num + " ");
}
mergeSort(arr);
System.out.println("\n排序后的数组:");
for (int num : arr) {
System.out.print(num + " ");
}
}
}
n 个记录需要进行 log₂n 趟归并排序,每一趟归并,其关键字比较次数不超过 n,元素移动次数都是 n,因此时间复杂度为 O(n log₂n)
用顺序表实现递归时,需要和待排序记录相等的辅助存储空间,所以空间复杂度为 O(n)
六、基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。排序过程是将所有待比较数值统一为同样的数位长度,数位较短的数前面补零,然后从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
import java.util.Arrays;
public class RadixSort {
private static int getMax(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
private static void countingSort(int[] arr, int exp) {
int n = arr.length;
int[] output = new int[n];
int[] count = new int[10];
Arrays.fill(count, 0);
for (int i = 0; i < n; i++) {
int digit = (arr[i] / exp) % 10;
count[digit]++;
}
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
for (int i = n - 1; i >= 0; i--) {
int digit = (arr[i] / exp) % 10;
output[count[digit] - 1] = arr[i];
count[digit]--;
}
System.arraycopy(output, 0, arr, 0, n);
}
public static void radixSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
int max = getMax(arr);
for (int exp = 1; max / exp > 0; exp *= 10) {
countingSort(arr, exp);
}
}
}