排序算法模板函数:
public class Util {
/**
* 交换元素
*/
public static void swap(int[] arr, int x, int y) {
arr[x] = arr[x] ^ arr[y];
arr[y] = arr[x] ^ arr[y];
arr[x] = arr[x] ^ arr[y];
}
}
冒泡排序(超时)
基本思想:
两两比较相邻记录的关键字,如果是反序则交换,直到没有反序的记录为止。
演示:
代码:
public static void BubbleSort(int[] arr) {
boolean flag = true; // 定义一个flag,如果当前比较轮次没有发生交换,则说明排序完成
for (int i = 0; i < arr.length && flag; i++) {
flag = false; // 重置flag为false
for (int j = arr.length - 2; j >= i; j--) { // 从后往前比较,每轮比较次数减一
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1); // 交换元素
flag = true; // 当前轮次发生交换
}
}
}
}
选择排序(1504 ms)
基本思想:
通过 n - i 次关键字间的比较,从 n - i + 1 个记录中选出关键字最小的记录,并和第 i(1<= i <=n)个记录交换之。
演示:
代码:
public static void SelectSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int min = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min])
min = j; // 遍历数组元素,找出最小值索引
}
if (min != i)
swap(arr, min, i); // 交换元素
}
}
插入排序(2540 ms)
基本思想:
将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
演示:
代码:
public static void InsertSort(int[] arr) {
for (int i = 1; i < arr.length; i++)
for (int j = i; j > 0 && arr[j] < arr[j - 1]; j--)
swap(arr, j, j - 1); // 交换元素
}
希尔排序(12 ms)
基本思想:
将相距某个 “增量” 的记录组成一个子序列,保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。
演示:
代码:
public static void ShellSort(int[] arr) {
int N = arr.length;
int h = 1;
while (h < N / 3)
h = 3 * h + 1; // 根据数组长度定义步长
while (h >= 1) {
for (int i = h; i < N; i++)
for (int j = i; j >= h && arr[j] < arr[j - h]; j -= h)
swap(arr, j, j - h); // 交换元素
h = h / 3; // 缩短步长
}
}
归并排序(7 ms)
基本思想:
假设初始序列含有 n 个记录,则可以看成是 n 个有序的子序列,每个子序列的长度为1,然后两两归并,得到 {an / 2}( {x} 表示不小于 x 的最小整数)个长度为 2 或 1 的有序子序列;再两两归并,…,如此重复,直至得到一个长度为 n 的有序序列为止。
演示:
代码:
自顶向下
public static int[] MergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2; // 找出当前序列的中间位置
arr = MergeSort(arr, left, mid);
arr = MergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
return arr;
}
private static void merge(int[] arr, int left, int mid, int right) {
int[] tempArr = new int[right - left + 1];
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right)
tempArr[k++] = arr[i] >= arr[j] ? arr[j++] : arr[i++];
while (i <= mid)
tempArr[k++] = arr[i++];
while (j <= right)
tempArr[k++] = arr[j++];
for (i = 0; i < k; i++)
arr[left++] = tempArr[i];
}
自底向上
public static int[] MergeSort(int[] arr) {
int n = arr.length;
for (int i = 1; i < n; i += i) {
int left = 0, mid = left + i - 1, right = mid + i;
while (right < n) {
merge(arr, left, mid, right);
left = right + 1;
mid = left + i - 1;
right = mid + i;
}
if (left < n && mid < n)
merge(arr, left, mid, n - 1);
}
return arr;
}
快速排序(6 ms)
基本思想:
通过一趟排序将待记录分割成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可以分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
演示:
代码:
public static void QuickSort(int[] arr, int left, int right) {
if (left < right) {
int pivot = partition(arr, left, right);
QuickSort(arr, left, pivot - 1); // 递归排序在基准值前的元素
QuickSort(arr, pivot + 1, right); // 递归排序在基准值后的元素
}
}
public static int partition(int[] arr, int left, int right) {
// 获取数组首位、中位、末尾元素中的中位数,作为“基准值”,并将其交换至首位
int i = left, j = right;
int mid = left + (right - left) / 2;
if (arr[i] > arr[j])
swap(arr, i, j);
if (arr[mid] > arr[j])
swap(arr, mid, j);
if (arr[mid] > arr[i])
swap(arr, mid, i);
while (i < j) {
while (i < j && arr[i] <= arr[j])
j--; // 如果尾指针元素大于头指针元素,尾指针向前移动
if (i < j)
swap(arr, i++, j); // 交换元素
while (i < j && arr[i] <= arr[j])
i++; // 如果头指针元素大于尾指针元素,头指针向后移动
if (i < j)
swap(arr, i, j--); // 交换元素
}
return i; // 返回基准值指针
}
堆排序(8 ms)
基本思想:
将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的 n - 1 个序列重新构造成一个堆,这样就会得到 n 个元素中的次大小值。如此循环反复执行,便能得到一个有序序列了。
演示:
代码:
public static void HeapSort(int[] arr) {
int n = arr.length; // 获取数组长度
for (int i = (n - 2) / 2; i >= 0; i--) // 从下往上依次获取非叶子结点
heapAdjust(arr, i, n - 1); // 调整堆
for (int i = n - 1; i >= 1; i--) {
swap(arr, 0, i); // 将堆顶的元素值和尾部元素交换
heapAdjust(arr, 0, i - 1); // 调整根结点及其调整后影响的子结点
}
}
public static void heapAdjust(int[] arr, int parent, int n) {
int temp = arr[parent]; // 取出当前结点值
int child = 2 * parent + 1; // child指向当前结点的左子结点
while (child <= n) { // 判断结点是否存在
if (child + 1 <= n && arr[child] < arr[child + 1])
child++; // 如果当前结点有右子结点且右子结点值大于左子结点值,则指针指向右子结点
if (arr[child] <= temp)
break; // 当前结点值大于子结点值,不进行交换
arr[parent] = arr[child]; // 在原数组中替换子结点值
parent = child; // 记录子结点指针
child = 2 * parent + 1; // child重新指向子结点的左子结点
}
arr[parent] = temp; // 将当前结点值放到最终的位置
}
计数排序(2 ms)
基本思想:
把数组元素作为数组的下标,然后用一个临时数组统计该元素出现的次数,例如 temp[i] = m,表示元素 i 一共出现了 m 次。最后再把临时数组统计的数据从小到大汇总起来,此时汇总起来的数据就是有序的。
演示:
代码:
public static void CountSort(int[] arr) {
int min = arr[0]; // 记录数组元素中的最小值
int max = arr[0]; // 记录数组元素中的最大值
for (int i = 1; i < arr.length; i++)
if (arr[i] > max)
max = arr[i];
else if (arr[i] < min)
min = arr[i];
int[] tempArr = new int[max - min + 1]; // 创建临时数组
for (int i : arr)
tempArr[i - min]++; // 统计每个数组元素出现的次数
int index = 0;
for (int i = 0; i < tempArr.length; i++)
for (int j = 0; j < tempArr[i]; j++)
arr[index++] = min + i; // 将临时数组中的元素重新赋值到原数组中
}
桶排序(10 ms)
基本思想:
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量。
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中。
演示:
元素分布在桶中:
然后,元素在每个桶中排序:
代码:
public static void BucketSort(int[] arr, int bucketSize){
int min = arr[0]; // 记录数组元素中的最小值
int max = arr[0]; // 记录数组元素中的最大值
for (int i = 1; i < arr.length; i++)
if (arr[i] > max)
max = arr[i];
else if (arr[i] < min)
min = arr[i];
int bucketCount = (max - min) / bucketSize + 1; // 桶数量为桶大小的倍数
ArrayList<LinkedList<Integer>> buckets = new ArrayList<>(bucketCount);
for (int i = 0; i < bucketCount; i++)
buckets.add(new LinkedList<>()); // 初始化桶
for (int item : arr)
buckets.get((item - min) / bucketSize).add(item); // 根据计算值放入对应的桶中
int index = 0;
for (LinkedList<Integer> bucket : buckets) {
Collections.sort(bucket); // 对每个桶进行排序
for (int value : bucket)
arr[index++] = value; // 合并数据
}
}
基数排序(9 ms)
基本思想:
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
演示:
代码:
public static void RadixSort(int[] arr) {
int minVal = arr[0]; // 记录数组元素中的最小值
int maxVal = arr[0]; // 记录数组元素中的最大值
for (int i = 1; i < arr.length; i++)
if (arr[i] < minVal)
minVal = arr[i];
if (minVal < 0)
for (int i = 0; i < arr.length; i++)
arr[i] -= minVal; // 如果最小值是负数,则对数组所有元素减去最小值
for (int i = 1; i < arr.length; i++)
if (arr[i] > maxVal)
maxVal = arr[i];
int maxLen = (maxVal + "").length(); // 记录最大值位数
int[][] bucket = new int[10][arr.length]; // 初始化10个桶
int[] eleCount = new int[10]; // 记录每个桶中的元素个数
for (int i = 0, n = 1; i < maxLen; i++, n *= 10) {
for (int value : arr) {
int digit = value / n % 10; // 从个位数开始获取元素位数
bucket[digit][eleCount[digit]] = value; // 根据元素位数将其放入对应的桶中
eleCount[digit]++; // 当前桶记录数加一
}
int index = 0;
for (int k = 0; k < eleCount.length; k++) {
for (int l = 0; l < eleCount[k]; l++)
arr[index++] = bucket[k][l]; // 依次从桶中获取元素
eleCount[k] = 0; // 桶记录数清零
}
}
if (minVal < 0)
for (int i = 0; i < arr.length; i++)
arr[i] += minVal; // 如果最小值是负数,则对数组所有元素加上最小值
}
十种排序算法总结
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 排序方式 | 稳定性 |
冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | In-place | 稳定 |
选择排序 | O(n²) | O(n) | O(n²) | O(1) | In-place | 不稳定 |
插入排序 | O(n²) | O(n) | O(n²) | O(1) | In-place | 稳定 |
希尔排序 | O(n^1.3) | O(n) | O(n²) | O(1) | In-place | 不稳定 |
归并排序 | O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(n) | Out-place | 稳定 |
快速排序 | O(nlog₂n) | O(nlog₂n) | O(n²) | O(nlog₂n ~ n) | In-place | 不稳定 |
堆排序 | O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(1) | In-place | 不稳定 |
计数排序 | O(n + k) | O(n + k) | O(n + k) | O(k) | Out-place | 稳定 |
桶排序 | O(n + k) | O(n + k) | O(n²) | O(n + k) | Out-place | 稳定 |
基数排序 | O(n × k) | O(n × k) | O(n × k) | O(n + k) | Out-place | 稳定 |