概览
序号 | 名称 | 时间复杂度 | 空间复杂度 | 稳定性 | 备注 | 应用范围 |
---|---|---|---|---|---|---|
1 | 冒泡排序 | O(N^2) | O(1) | 是 | 适用于小规模数据排序,由于效率低,很少在实际应用中使用。 | |
2 | 选择排序 | O(N^2) | O(1) | 否 | 适用于数据量小或内存空间非常紧张时的简单排序。 | |
3 | 插入排序 | O(N^2) | O(1) | 否 | 适合小数据量或近乎有序的数据,常用于复杂排序算法(如快速排序)的小规模数据优化。 | |
4 | 希尔排序 | O(N*logN) | O(1) | 否 | 适合中等规模数据的排序。常用于对缓存友好的排序场景。 | |
5 | 快速排序 | O(N*logN) | O(logN) | 否 | 适合大规模数据的排序,因其在大多数情况下效率较高,是实际应用中常用的排序算法之一。 | |
6 | 归并排序 | O(N*logN) | O(N) | 是 | 适合对大规模数据进行外部排序(如磁盘文件排序)以及需要稳定排序的情况。 | |
7 | 堆排序 | O(N*logN) | O(1) | 否 | 适合对数据量较大的数组进行排序,不需要额外空间且有较好性能,但不适合需要稳定性的场景。 | |
8 | 基数排序 | O(d⋅(n+k)) | O(n+k) | 是 | 其中 d 是位数,k 是位的取值范围(如十进制 k = 10) | 适合整数排序或特定位数数据的排序,常用于数据量较大且范围有限的情况(如账户 ID、电话号码排序)。 |
示例代码
1. 冒泡排序
冒泡排序通过多次比较相邻元素,将较大的元素逐步“冒泡”到数组的右边,最终使得整个数组有序。具体过程是从头到尾遍历数组,每次相邻两个元素比较并交换位置,使较大的元素向右移动。经过
n−1 轮遍历,数组会逐渐变得有序。冒泡排序是稳定的,因为相同元素之间不会改变相对位置。
/**
* 冒泡排序
*/
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2)
return;
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
}
}
}
}
2.选择排序
选择排序每一轮从未排序部分中找到最小的元素,然后将其放到已排序部分的末尾。第一轮找到最小元素,放到第一个位置,第二轮找到次小的元素,放到第二个位置,以此类推。这个过程直到所有元素都排序完毕。由于每次交换操作直接定位元素,选择排序是不稳定的。
/**
* 选择排序
*/
public static void selectionSort(int[] arr) {
if (arr == null || arr.length < 2)
return;
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(arr, i, minIndex);
}
}
3.插入排序
插入排序从第二个元素开始,将每个元素插入到前面已排序部分的正确位置,逐步构建有序数组。每次选择一个新元素,从后向前与已排序元素比较,找到合适位置插入。在数据量较少或接近有序的情况下,插入排序效率较高,因为其移动次数少。插入排序是稳定的,因为在插入时不会改变相同元素的相对位置。
/**
* 插入排序
*/
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2)
return;
for (int i = 1; i < arr.length; i++) {
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
swap(arr, j, j + 1);
}
}
}
4.希尔排序
希尔排序是插入排序的改进版,它通过分组排序来减少数据移动次数。希尔排序先将数组分为多个间隔较大的组,对每组进行插入排序。随着排序进行,逐渐减小分组间隔,直到最后一轮间隔为 1 时,完成插入排序。这样做可以快速减少“较大”元素的移动次数,最终减少排序时间。希尔排序是不稳定的。
/**
* 希尔排序
*/
public static void shellSort(int[] arr) {
if (arr == null || arr.length < 2)
return;
int gap = arr.length / 2;
while (gap > 0) {
for (int i = gap; i < arr.length; i++) {
int temp = arr[i];
int j = i - gap;
for (; j >= 0 && arr[j] > temp; j -= gap) {
arr[j + gap] = arr[j];
}
arr[j + gap] = temp;
}
gap /= 2;
}
}
5.快速排序
快速排序通过选择一个基准元素(pivot),将数组分为小于基准和大于基准的两部分,对这两部分分别递归排序,最终形成有序数组。具体步骤是:选择基准后,左右扫描,左边找到比基准大的元素,右边找到比基准小的元素并交换,直到左右指针相遇。快速排序是效率较高的不稳定排序算法,因为元素交换会改变相同元素的相对位置。
/**
* 快速排序
*/
public static void quickSort(int[] arr,int L,int R){
if(L >= R) return ;
int pivotIndex = partition(arr,L,R);
quickSort(arr,L,pivotIndex - 1);
quickSort(arr,pivotIndex + 1,R);
}
public static int partition(int[] arr,int L,int R){
if(L >= R) return L;
int less = L - 1;
int more = R;
int index = L;
while(index < more){
if(arr[index] < arr[R]){
swap(arr,++less,index);
}else if(arr[index] > arr[R]){
swap(arr,--more,index);
}
index++;
}
swap(arr,more,R);
return more;
}
6.归并排序
归并排序采用分治法,递归地将数组分成两部分,分别对两部分排序,然后将它们合并成一个有序数组。这个过程直到分解到每个子数组只有一个元素(单元素数组本身有序)为止。然后从底层向上合并,逐步构建有序数组。归并排序是稳定的,因为合并时保持相同元素的相对顺序。
/**
* 归并排序
*/
public static void mergeSort(int[] arr, int L, int R) {
if (L >= R)
return;
int mid = L + ((R - L) >> 1);
mergeSort(arr, L, mid);
mergeSort(arr, mid + 1, R);
merge(arr, L, mid, R);
}
public static void merge(int[] arr, int L, int mid, int R) {
int[] h = new int[R - L + 1];
int p1 = L;
int p2 = mid + 1;
int i = 0;
while (p1 <= mid && p2 <= R) {
h[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= mid) {
h[i++] = arr[p1++];
}
while (p2 <= R) {
h[i++] = arr[p2++];
}
for (i = 0; i < h.length; i++) {
arr[L + i] = h[i];
}
}
7. 堆排序
堆排序首先将数组构造成一个大顶堆(最大元素在堆顶),然后取出堆顶元素并与末尾元素交换,将剩下的元素重新调整为大顶堆,重复该过程直到数组完全有序。堆排序是一种利用二叉堆结构的选择排序,不需要额外的空间但不稳定。堆排序适合需要较少内存的场景。
/**
* 堆排序
*/
public static void heapSort(int[] arr){
if(arr == null || arr.length < 2) return;
int heapSize = arr.length;
buildHeap(arr,heapSize);
while(heapSize >0){
swap(arr,0,--heapSize);
heapify(arr,0,heapSize);
}
}
public static void buildHeap(int[] arr,int heapSize){
for(int i = (heapSize - 1)/ 2;i >= 0;i--){
heapify(arr,i,heapSize);
}
}
public static void heapify(int[] arr,int index,int heapSize){
int left = index * 2 + 1;
int right = index * 2 + 2;
int largeset = index;
if(left < heapSize && arr[left] > arr[largeset]){
largeset = left;
}
if(right < heapSize && arr[right] > arr[largeset]){
largeset = right;
}
if(largeset != index){
swap(arr,index,largeset);
heapify(arr,largeset,heapSize);
}
}
8.基数排序
基数排序适用于整数或字符串的排序,按照每个位数逐步排序。它从最低有效位(如个位)开始,使用计数排序对每个位进行排序,然后依次对更高位排序,直到最高位为止。每一位排序保持前一位排序结果的顺序,保证最终数组有序。基数排序是稳定的,适合特定场景下的数据(如 ID、手机号)排序。
/**
* 基数排序
*/
public static void radixSort(int[] arr) {
if (arr == null || arr.length < 2)
return;
int max = getMax(arr);
for (int exp = 1; max / exp > 0; exp *= 10) {
countingSortByDigit(arr, exp);
}
}
public static int getMax(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
max = Math.max(max, arr[i]);
}
return max;
}
public static void countingSortByDigit(int[] arr, int exp) {
int n = arr.length;
int[] output = new int[n]; // 存储排序结果的临时数组
int[] count = new int[10]; // 计数数组,十进制所以长度为10
// 记录当前位(个位、十位、百位等)的出现次数
for (int i = 0; i < n; i++) {
count[(arr[i] / exp) % 10]++;
}
// 将计数数组转换为位置数组
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]--;
}
// 将排序后的结果复制回原数组
for (int i = 0; i < n; i++) {
arr[i] = output[i];
}
}