排序是最最基本的算法之一,一般的排序算法有以下几种:
- 插入排序
- 选择排序
- 冒泡排序
- 快速排序
- 堆排序
- 希尔排序
- 归并排序
- 计数排序
- 桶排序
- 基数排序
插入排序
从数组第一个开始,根据当前需要排序的位置的值,找到在它之前的一个有序数组里面,它应该在的位置,然后将这个位置之后的值一次后移一位,再将这个值放入这个位置。
时间复杂度$O(n^2)$
空间复杂度$O(1)$
public class InsertSort {
public static void sort(int[] data) {
for (int i = 0; i < data.length; i++) {
int j = i;
int target = data[j];
// 顺位后移
while (j > 0 && data[j - 1] > target) {
data[j] = data[j - 1];
j --;
}
// 插入
data[j] = target;
}
}
}
选择排序
从数组第一个开始,从要排序的位置之后找到一个最小的值,将那个值与当前位置的值互换位置,然后开始找下一位。
时间复杂度$O(n^2)$
空间复杂度$O(1)$
public class SelectSort {
public static void sort(int[] data) {
for (int i = 0; i < data.length; i++) {
int min = i;
// 找到最小的值的位置
for (int j = i + 1; j < data.length; j++) {
if (data[min] > data[j]) {
min = j;
}
}
// 与当前位置互换
if (min != i) {
int temp = data[i];
data[i] = data[min];
data[min] = temp;
}
}
}
}
冒泡排序
从数组的第一个开始,每次从数组的最后一位往前数,数到这一位,每次比较相邻的两位的大小,并将小的放到前面。
时间复杂度$O(n^2)$
空间复杂度$O(1)$
public class BubbleSort {
public static void sort(int[] data) {
for (int i = 0; i < data.length; i++) {
for (int j = data.length - 1; j > i ; j--) {
// 如果靠后的值小于靠前的值,互换
if (data[j] < data[j - 1]) {
int temp = data[j];
data[j] = data[j - 1];
data[j - 1] = temp;
}
}
}
}
}
快速排序
二分法原理,通过取一个值作为基准值,将所有大于这个值和小于这个值的数分别放在右边和左边,完成一半的排序,即每次找到一个中间数,然后将这两半各自再完成其一半的排序,直到排序完成。
时间复杂度$O(nlgn)$
空间复杂度$O(1)$
public class QuickSort {
public static void sort(int[] data) {
int left = 0, right = data.length - 1;
quickSort(data, left, right);
}
private static void quickSort(int[] data, int left, int right) {
if (left >= right) {
return;
}
int pivotPos = partition(data, left, right);
quickSort(data, left, pivotPos - 1);
quickSort(data, pivotPos + 1, right);
}
// 常规版
private static int partition(int[] data, int left, int right) {
int pivotKey = data[left];
int pivotPointer = left;
while (left < right) {
while (left < right && data[right] >= pivotKey) {
right --;
}
while (left < right && data[left] <= pivotKey) {
left ++;
}
swap(data, left, right);
}
swap(data, pivotPointer, left);
return left;
}
// 改进版
private static int partition2(int[] data, int left, int right) {
int pivotKey = data[left];
while (left < right) {
while (left < right && data[right] >= pivotKey) {
right --;
}
data[left] = data[right];
while (left < right && data[left] <= pivotKey) {
left ++;
}
data[right] = data[left];
}
data[left] = pivotKey;
return left;
}
private static void swap(int[] data, int l, int r) {
int temp = data[l];
data[l] = data[r];
data[r] = temp;
}
}
堆排序
借助堆实现的选择排序,利用堆的特性,每次将当前组中最大的一个值放到堆顶然后取走,接着将剩余的组中最大的一个值放到堆顶,直到只剩下一个数。
时间复杂度$O(nlgn)$
空间复杂度$O(1)$
public class HeapSort {
public static void sort(int[] data) {
for (int i = data.length / 2; i >= 0; i --) {
heapAdjust(data, i, data.length - 1);
}
for (int i = data.length - 1; i >= 0; i--) {
swap(data, 0, i);
heapAdjust(data, 0, i - 1);
}
}
private static void heapAdjust(int[] data, int start, int end) {
int temp = data[start];
for (int i = 2 * start + 1; i <= end; i = 2 * i + 1) {
if (i < end && data[i] < data[i + 1]) {
i ++;
}
if (temp >= data[i]) {
break;
}
data[start] = data[i];
start = i;
}
data[start] = temp;
}
private static void swap(int[] data, int f, int t) {
int temp = data[f];
data[f] = data[t];
data[t] = temp;
}
}
希尔排序
插入排序的一种高效实现,也叫缩小增量排序,即先将数组变成大致有序,然后进行一遍插入排序,将数组变得大致有序的方法,就是「缩小增量排序」,即每次只排序数组的一部分,然后逐渐增加这个「一部分」,直到这个「一部分」等于整个数组,就变成了插入排序。
时间复杂度$O(n^{1.3})$
空间复杂度$O(1)$
public class ShellSort {
public static void sort(int[] data) {
int d = data.length / 2;
while (d >= 1) {
shellInsert(data, d);
d /= 2;
}
}
private static void shellInsert(int[] data, int d) {
for (int i = d; i < data.length; i++) {
int j = i - d;
int temp = data[i];
while (j >= 0 && data[j] > temp) {
data[j + d] = data[j];
j -= d;
}
data[j + d] = temp;
}
}
}
归并排序
递归分治的思想,将一个数组不断二分,分到最小之后,排序,然后合起来。
时间复杂度$O(nlgn)$
空间复杂度$O(n)$
public class MergeSort {
public static void sort(int[] data) {
mergeSort(data, 0, data.length - 1);
}
private static void mergeSort(int[] data, int left, int right) {
if (left < right) {
// 递归的思想
int mid = (left + right) / 2;
mergeSort(data, left, mid);
mergeSort(data, mid + 1, right);
merge(data, left, mid, right);
}
}
private static void merge(int[] data, int left, int mid, int right) {
int[] temp = new int[right - left + 1];
int i = left;
int j = mid + 1;
int k = 0;
// 直接选择然后复制过去
while (i <= mid && j <= right) {
if (data[i] <= data[j]) {
temp[k ++] = data[i ++];
}else {
temp[k ++] = data[j ++];
}
}
// 左边没有复制完的
while (i <= mid) {
temp[k ++] = data[i ++];
}
// 右边没有复制完的
while (j <= right) {
temp[k ++] = data[j ++];
}
// 将临时数组复制到真正的数组里面
System.arraycopy(temp, 0, data, left, temp.length);
}
}
计数排序
如果需要排序的数组的值的范围不是很大的时候,就可以通过下标计数的方法完成排序。
时间复杂度$O(n)$
空间复杂度 根据数组的值的范围而定
public class CountSort {
public static void sort(int[] data) {
int[] a = min(data);
int max = a[1];
int min = a[0];
int size = max - min;
// 构建计数数组
int[] count = new int[size + 1];
Arrays.fill(count, 0);
// 遍历一遍带排序数组,得到计数数组
for (int d : data) {
count[d - min]++;
}
// 对计数数组进行重整,得到排序后数组
int k = 0;
for (int i = 0; i <= size; i++) {
for (int j = 0; j < count[i]; j++) {
data[k ++] = i + min;
}
}
}
// 确定计数数组的范围
private static int[] min(int[] data) {
int min = data[0];
int max = data[0];
for (int i = 1; i < data.length; i++) {
if (data[i] < min) {
min = data[i];
}else if (data[i] > max) {
max = data[i];
}
}
return new int[]{min, max};
}
}
桶排序
桶排序的原理是,先将数组划分为多个桶,同时桶与桶之间是按序排列的大小关系,然后分别对每个桶中的数据进行排序,最后将桶中的数据还原到数组中。
这中间要明确两个概念,桶的数量 m 与 数组的大小 n
时间复杂度$O(n + n * (logn - logm))$
空间复杂度$O(n + m)$
public class BucketSort {
public static void sort(int[] data) {
// 桶数量
int bucketNum = 10;
// 桶索引
List<List<Integer>> buckets = new ArrayList<>();
for (int i = 0; i < bucketNum; i++) {
buckets.add(new LinkedList<>());
}
// 将数组的数据依次加入桶
for (int aData : data) {
buckets.get(f(aData)).add(aData);
}
// 对每个桶排序
for (List<Integer> bucket : buckets) {
if (!bucket.isEmpty()) {
Collections.sort(bucket);
}
}
// 将桶中的数据还原到数组中
int k = 0;
for (List<Integer> l : buckets) {
for (int i : l) {
data[k ++] = i;
}
}
}
private static int f(int x) {
return x / 10;
}
}
基数排序
基数排序比较有趣,他通过多对数组的遍历完成排序,按照数组中的最大值的位数,如 123, 3 位,就需要遍历 3 次,从这个数的最低位,开始,先完成数组中所有数据的个位数的排序(通过一个链表保存然后恢复,与桶排序类似原理),然后依次完成十位数、百位数排序,当最高位的排序完成的时候,因为之前的个位数与十位数已经是有序的了,那么一旦百位数的排序完成,那么这个数组就肯定也是一个有序的数组了。
时间复杂度$O(d(n + rd))$
空间复杂度$O(rd)$
public class RadixSort {
public static void sort(int[] data) {
int max = getMaxBit(data);
for (int i = 1; i <= max; i++) {
List<List<Integer>> list = distribute(data, i);
collect(data, list);
}
}
// 对当前数组中按某一位进行排序
private static List<List<Integer>> distribute(int[] data, int bit) {
List<List<Integer>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(new LinkedList<>());
}
for (int aData : data) {
list.get(getBit(aData, bit)).add(aData);
}
return list;
}
// 将链表中保存的数据还原到数组
private static void collect(int[] data, List<List<Integer>> buf) {
int k = 0;
for (List<Integer> l : buf) {
for (int i : l) {
data[k ++] = i;
}
}
}
// 获取数组中最大数的位数(以确定遍历次数)
private static int getMaxBit(int[] data) {
int num = 0;
for (int i : data) {
int l = (i + "").length();
if (l > num) {
num = l;
}
}
return num;
}
// 获取某个数某位的值(没有则用 0 代替)
private static int getBit(int x, int n) {
String xs = x + "";
if (xs.length() < n) {
return 0;
}else {
return xs.charAt(xs.length() - n) - '0';
}
}
}
总结
比较
| 排序方法 | 平均时间 | 最坏情况 | 辅助存储 |
|---|---|---|---|
| 简单排序 | $O(n^2)$ | $O(n^2)$ | $O(1)$ |
| 快速排序 | $O(nlgn)$ | $O(n^2)$ | $O(logn)$ |
| 堆排序 | $O(nlogn)$ | $O(nlogn)$ | $O(1)$ |
| 归并排序 | $O(nlogn)$ | $O(nlogn)$ | $O(n)$ |
| 基数排序 | $O(d(n+rd))$ | $O(d(n+rd))$ | $O(rd)$ |
稳定性
- 稳定的
- 基数排序
- 冒泡排序
- 插入排序
- 归并排序
- 不稳定的
- 快速排序
- 堆排序
- 希尔排序
- 选择排序