排序
算法 | 稳定性 | 时间复杂度 | 空间复杂度 |
---|---|---|---|
选择排序 | N | (N^2) | 1 |
冒泡排序 | Y | (N^2) | 1 |
插入排序 | Y | (N~N^2) | 1 |
希尔排序 | N | (N^{(1.3-2)}) | 1 |
归并排序 | Y | (NlogN) | N |
快速排序 | N | (NlogN) | logN |
堆排序 | N | (NlogN) | 1 |
public abstract class Sort<T extends Comparable<T>> {
public abstract void sort(T[] nums);
protected boolean less(T v, T w) {
return v.compareTo(w) < 0;
}
protected void swap(T[] a, int i, int j) {
T t = a[i];
a[i] = a[j];
a[j] = t;
}
}
选择排序
从数组中 选择最小元素,将它与数组的第一个元素 交换 位置。再从数组剩下的元素中选择出最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。
- (N^2 /2) 次比较
- (N) 次交换
- 不稳定
- 运行时间与输入无关,即对一个已经排序的数组也需要这么多的比较和交换操作
public class Selection<T extends Comparable<T>> extends Sort<T> {
@Override
public void sort(T[] nums) {
int len = nums.length;
for (int i = 0; i < len - 1; i++) {
int min = i;
for (int j = i + 1; j < len; j++) {
if (less(nums[j], nums[min])) {
min = j;
}
}
swap(nums, i, min);
}
}
}
冒泡排序
从左到右不断 交换相邻逆序 的元素,在一轮的循环之后,可以让未排序的最大元素上浮到右侧。
优化:在一轮循环中,如果没有发生交换,那么说明数组已经是有序的。
- 最好 (N-1) 次比较 (0) 次交换
- 最差 (N(N-1)/2) 次比较 (N(N-1)/2) 次交换
- 稳定
public class Bubble<T extends Comparable<T>> extends Sort<T> {
@Override
public void sort(T[] nums) {
int n = nums.length;
boolean isSorted = false;
for (int i = n - 1; !isSorted && i > 0; i--) {
isSorted = true;
for (int j = 0; j < i; j++) {
if (less(nums[j + 1], nums[j])) {
swap(nums, j, j + 1);
isSorted = false;
}
}
}
}
}
插入排序
每次都将当前元素 插入到左侧已经排序 的数组中,使得左侧数组依然有序。
插入排序每次只能交换相邻元素,令逆序数量减少 1,因此插入排序需要交换的次数为逆序数量。
- 平均 (N^2/4) 次比较 (N^2/4) 次交换
- 最坏 (N^2/2) 次比较 (N^2/2) 次交换
- 最好 (N-1) 次比较 (0) 次交换
- 稳定
- 运行时间取决于数组的初始顺序
public class Insertion<T extends Comparable<T>> extends Sort<T> {
@Override
public void sort(T[] nums) {
int n = nums.length;
for (int i = 1; i < n; i++) {
for (int j = i; j > 0 && less(nums[j], nums[j - 1]); j--) {
swap(nums, j, j - 1);
}
}
}
public static void main(String[] args) {
Sort.test(new Insertion<Integer>());
}
}
希尔排序
希尔排序使用 插入排序对间隔 h 的序列 进行排序。通过不断减小 h,最后令 h=1,就可以使得整个数组是有序。
希尔排序通过交换不相邻的元素,每次可以将逆序数量减少大于 1。
插入排序每次只能交换相邻元素,令逆序数量减少 1,因此插入排序需要交换的次数为逆序数量。
- 不稳定
public class Shell<T extends Comparable<T>> extends Sort<T> {
@Override
public void sort(T[] nums) {
int n = nums.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 && less(nums[j], nums[j - h]); j = j - h) {
swap(nums, j, j - h);
}
}
h /= 3;
}
}
}
归并排序
分而治之 ,将问题分解成几个规模较小的问题,再合并子问题获得答案。
- 稳定
- 时间复杂度:(Ο(nlogn))
public class MergeSort<T extends Comparable<T>> extends Sort<T> {
private T[] tmp;
@Override
public void sort(T[] nums) {
int n = nums.length;
tmp = Arrays.copyOf(nums, n);
sortTopDown(nums, 0, n - 1);
// sortDownTop(nums, n);
}
private void sortTopDown(T[] nums, int l, int r) {
if (l >= r)
return;
int m = ((r - l) >> 1) + l;
sortTopDown(nums, l, m);
sortTopDown(nums, m + 1, r);
merge(nums, l, m, r);
}
private void sortDownTop(T[] nums, int n) {
for (int d = 1; d < n; d += d) {
for (int l = 0; l < n - d; l += d + d) {
merge(nums, l, l + d - 1, Math.min(l + d + d - 1, n - 1));
}
}
}
private void merge(T[] nums, int l, int m, int r) {
int i = l;
int j = m + 1;
int index = l;
while (i <= m && j <= r) {
if (!less(nums[j], nums[i])) {
tmp[index++] = nums[i++];
} else {
tmp[index++] = nums[j++];
}
}
while (i <= m) {
tmp[index++] = nums[i++];
}
while (j <= r) {
tmp[index++] = nums[j++];
}
for (index = l; index <= r; index++) {
nums[index] = tmp[index];
}
}
}
快速排序
快速排序通过一个 切分元素 将数组分为两个子数组,左子数组小于等于切分元素,右子数组大于等于切分元素,将这两个子数组排序也就将整个数组排序了。
public class QuickSort<T extends Comparable<T>> extends Sort<T> {
@Override
public void sort(T[] nums) {
sort(nums, 0, nums.length - 1);
}
private void sort(T[] nums, int l, int r) {
if (l >= r) {
return;
}
int index = partition(nums, l, r);
sort(nums, l, index - 1);
sort(nums, index + 1, r);
}
private int partition(T[] nums, int l, int r) {
T tmp = nums[r];
int i = l;
int j = r - 1;
while (true) {
// 相等时即停止移动指针
while (i < r && less(nums[i], tmp))
i++;
while (j >= l && less(tmp, nums[j]))
j--;
if (i >= j)
break;
swap(nums, i++, j--);
}
swap(nums, i, r);
return i;
}
}
注 :若指针指向的元素与参考值相等仍继续移动,假设极端情况:数组中所有元素都相等。那么i会移动到最后一个位置,导致数组切分失衡,复杂度变为 (O(n^2))。因此相等时就应停止移动,虽然多了许多交换操作,但能够保证切分均衡。
改进 :三向切分。lt左侧小于切分元素,gt右侧大于切分元素。
private void threeWayQuickSort(T[] nums, int l, int r) {
if (l >= r) {
return;
}
int lt = l, i = l + 1, gt = r;
T tmp = nums[l];
while (i <= gt) {
int cmp = nums[i].compareTo(tmp);
if (cmp < 0) {
swap(nums, lt++, i++);
} else if (cmp > 0) {
swap(nums, i, gt--);
} else {
i++;
}
}
sort(nums, l, lt - 1);
sort(nums, gt + 1, r);
}
堆排序
堆是 完全二叉树 ,可以用数组来表示。
位置 k 的节点的父节点位置为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。(不使用数组索引为 0 的位置)
- 堆的高度为 (logN),因此在堆中插入元素和删除最大元素的复杂度都为 (logN)。
- 对于堆排序,由于要对 N 个节点进行下沉操作,因此复杂度为 (NlogN)。
- 不稳定
- 正常来说,堆排序是 原地排序 ,这里为了实现简单,下标从1开始,使用了辅助数组。
public class HeapSort<T extends Comparable<T>> extends Sort<T> {
@Override
public void sort(T[] nums) {
int n = nums.length;
T[] tmp = (T[]) new Comparable[n + 1];
System.arraycopy(nums, 0, tmp, 1, n);
for (int i = n / 2; i >= 1; i--) {
sink(tmp, i, n);
}
for (int i = 1; i <= n; i++) {
swap(tmp, 1, n - i + 1);
sink(tmp, 1, n - i);
}
System.arraycopy(tmp, 1, nums, 0, n);
}
private void sink(T[] nums, int k, int n) {
while (k <= n / 2) {
int t = 2 * k;
if (t + 1 <= n && less(nums[t], nums[t + 1])) {
t++;
}
if (!less(nums[k], nums[t]))
break;
swap(nums, k, t);
k = t;
}
}
}