选择排序
- 时间复杂度: N 2 N^2 N2
- 空间复杂度: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;
}
}
public class Selection<T extends Comparable<T>> extends Sort<T> {
@Override
public void sort(T[] nums) {
int N = nums.length;
for (int i = 0; i < N - 1; i++) {
int min = i;
for (int j = i + 1; j < N; j++) {
if (less(nums[j], nums[min])) {
min = j;
}
}
swap(nums, i, min);
}
}
}
冒泡排序
- 时间复杂度: N 2 N^2 N2
- 空间复杂度:1
- 实现逻辑:从左向右两两比较相邻的值,如果左边大于右边,则进行交换,第一遍交换完成后,最大的值在最右侧,第二遍遍历截止的元素则为倒数第二个元素,结束的标志为:1.遍历截止的值为第一个时,可停止;2.一遍便利后,没有值交换,则已经排序完成,可退出
- 代码:
public class Bubble<T extends Comparable<T>> extends Sort<T> {
@Override
public void sort(T[] nums) {
int N = nums.length;
boolean hasSorted = false;
for (int i = N - 1; i > 0 && !hasSorted; i--) {
hasSorted = true;
for (int j = 0; j < i; j++) {
if (less(nums[j + 1], nums[j])) {
hasSorted = false;
swap(nums, j, j + 1);
}
}
}
}
}
插入排序
- 时间复杂度: N − N 2 N-N^2 N−N2
- 空间复杂度:1
- 实现逻辑:从左向右扩大比较数组,第一遍为两个元素的数组,左边大于右边则进行交换,第二遍为三个元素的数组,先比较第二个和第三个元素,需要交换则交换,并且比较第一个和第二个元素,若不需要交换,则进入第三遍4个元素,直至遍历到最后一个元素
- 代码:
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);
}
}
}
}
希尔排序
- 时间复杂度: N 3 / 2 N^{3/2} N3/2
- 空间复杂度:1
- 实现逻辑:是插入排序的升级版,插入排序每次都比较相邻的元素,故每次逆序数量只能减少1,而希尔排序第一遍比较为数组长度/3后的值作为间隔,比较此间隔的数组,并按照插入排序进行比较,第二遍为第一遍间隔/3的值作为间隔,再进行插入排序,直至间隔为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; // 1, 4, 13, 40, ...
}
while (h >= 1) {
for (int i = h; i < N; i++) {
for (int j = i; j >= h && less(nums[j], nums[j - h]); j -= h) {
swap(nums, j, j - h);
}
}
h = h / 3;
}
}
}
归并排序
- 时间复杂度:NlogN
- 空间复杂度:N
- 实现逻辑:将数组通过递归的方式不断将数组拆分成两个数组,直到数组只有一个元素就直接返回,拆分成的两个数组已经通过递归排序完成,再调用合并方法,合并方法通过一个备份的数组,依次比较前半段和后半段的数组,第一遍比较前半段第一个元素和后半段第一个元素,若前半段第一个元素小,则前半段第一个元素复制给原有数组的最左侧,并前半段的位置向右移动一位,后半段第一个元素小亦然,第二遍比较前半段第二个元素和后半段第一个元素,直至所有元素赋值到原有的数组
- 自顶向下归并排序:通过将数组不断二分递归的方式进行
- 自底向上归并排序:通过循环的方式,先对一个元素的两个数组排序,在对两个元素的两个数组排序,直至待排序的数组元素个数和初始数组长度相同
- 代码:
public abstract class MergeSort<T extends Comparable<T>> extends Sort<T> {
protected T[] aux;
protected void merge(T[] nums, int l, int m, int h) {
int i = l, j = m + 1;
for (int k = l; k <= h; k++) {
aux[k] = nums[k]; // 将数据复制到辅助数组
}
for (int k = l; k <= h; k++) {
if (i > m) {
nums[k] = aux[j++];
} else if (j > h) {
nums[k] = aux[i++];
} else if (aux[i].compareTo(aux[j]) <= 0) {
nums[k] = aux[i++]; // 先进行这一步,保证稳定性
} else {
nums[k] = aux[j++];
}
}
}
}
// 自上而下
public class Up2DownMergeSort<T extends Comparable<T>> extends MergeSort<T> {
@Override
public void sort(T[] nums) {
aux = (T[]) new Comparable[nums.length];
sort(nums, 0, nums.length - 1);
}
private void sort(T[] nums, int l, int h) {
if (h <= l) {
return;
}
int mid = l + (h - l) / 2;
sort(nums, l, mid);
sort(nums, mid + 1, h);
merge(nums, l, mid, h);
}
}
// 自下而上
public class Down2UpMergeSort<T extends Comparable<T>> extends MergeSort<T> {
@Override
public void sort(T[] nums) {
int N = nums.length;
aux = (T[]) new Comparable[N];
for (int sz = 1; sz < N; sz += sz) {
for (int lo = 0; lo < N - sz; lo += sz + sz) {
merge(nums, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1));
}
}
}
}
快速排序
- 时间复杂度:NlogN
- 空间复杂度logN:
- 实现逻辑:
- 先对数组进行切分,切分的过程是取数组最左侧的值,作为切分元素,从左向右选出第一个比它大的元素,再从右向左选出第一个比起小的元素,进行交换,直至两个指针相遇后,切分元素和从右边开始的指针进行交换,返回右边指针的位置
- 再对切分的数组进行左半部分和右半部分的递归调用
- 代码:
public class QuickSort<T extends Comparable<T>> extends Sort<T> {
@Override
public void sort(T[] nums) {
shuffle(nums);
sort(nums, 0, nums.length - 1);
}
private void sort(T[] nums, int l, int h) {
if (h <= l)
return;
int j = partition(nums, l, h);
sort(nums, l, j - 1);
sort(nums, j + 1, h);
}
private void shuffle(T[] nums) {
List<Comparable> list = Arrays.asList(nums);
Collections.shuffle(list);
list.toArray(nums);
}
private int partition(T[] nums, int l, int h) {
int i = l, j = h + 1;
T v = nums[l];
while (true) {
while (less(nums[++i], v) && i != h) ;
while (less(v, nums[--j]) && j != l) ;
if (i >= j)
break;
swap(nums, i, j);
}
swap(nums, l, j);
return j;
}
}
三向切分快速排序
- 时间复杂度:N~NlogN
- 空间复杂度:logN
- 实现逻辑:
- 先对数组进行切分,切分的方式有所不同,第一个指针指向起始元素,第二个指针从第二个元素开始向右移动,第一个指针和第二指针进行比较,若第一个大于第二个,则两则进行交换,两个指针均向右移动一位,若第二个大于第一个,则第二个指针和后边的第三个指针进行交换,后边的指针想左移动一位,直至第二个指针和后边的第三个指针相遇以后,结束
- 再对切分的数组进行左半部分和右半部分的递归调用
- 代码:
public class ThreeWayQuickSort<T extends Comparable<T>> extends QuickSort<T> {
@Override
protected void sort(T[] nums, int l, int h) {
if (h <= l) {
return;
}
int lt = l, i = l + 1, gt = h;
T v = nums[l];
while (i <= gt) {
int cmp = nums[i].compareTo(v);
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, h);
}
}
堆排序
-
时间复杂度:NlogN
-
空间复杂度:1
-
实现逻辑:
- 堆排序是将数组看成一个堆,而堆是一个完全二叉树,而二叉树可以很好地存储在数组中,位置k的父节点是k/2,其两个子节点是2k和2k+1,数组索引为0的位置不使用
- 第一步通过下沉的方式,将最大值调整到每个堆的堆顶,也就是父节点不小于其子节点,形成最大堆,该方式只需从叶子节点的上一级n/2向上遍历即可
- 第二步替换顶级元素和最尾部的元素,并且堆长度减1
- 第三步通过从顶部下沉的方式,将第二大的值放到堆顶,再往复进行第二步和第三步,直至堆长度为1
ps: 其中下沉的方式为,比较子元素中较大的值,然后父节点再和较大的值比较,若父节点更小,则与子节点较大值替换
堆排序详解 -
代码:
public class HeapSort<T extends Comparable<T>> extends Sort<T> {
/**
* 数组第 0 个位置不能有元素
*/
@Override
public void sort(T[] nums) {
int N = nums.length - 1;
for (int k = N / 2; k >= 1; k--)
sink(nums, k, N);
while (N > 1) {
swap(nums, 1, N--);
sink(nums, 1, N);
}
}
private void sink(T[] nums, int k, int N) {
while (2 * k <= N) {
int j = 2 * k;
if (j < N && less(nums, j, j + 1))
j++;
if (!less(nums, k, j))
break;
swap(nums, k, j);
k = j;
}
}
private boolean less(T[] nums, int i, int j) {
return nums[i].compareTo(nums[j]) < 0;
}
}