归并排序(O(nlogn)、O(n)):使用的是分而治之的思想,将大问题分解成小的子问题来解决:首先把数组从中间分成前后两部分,然后对它们分别排序,再将排好序的两部分合并在一起,这样整个数组就有序了。
public void mergeSort(int[] a) {
int n = a.length;
if (n <= 1) {
return;
}
merge(a, 0, n - 1);
}
private void merge(int[] a, int l, int r) {
if (l >= r) {
return;
}
int mid = l + ((r - l) >> 1);
merge(a, l, mid);
merge(a, mid + 1, r);
mergeTwo(a, l, mid, r);
}
private void mergeTwo(int[] a, int l, int mid, int r) {
int[] tmp = new int[r - l + 1];
int i = l;
int j = mid + 1;
int k = 0;
while (i <= mid && j <= r) {
if (a[i] <= a[j]) {
tmp[k++] = a[i++];
} else {
tmp[k++] = a[j++];
}
}
while (i <= mid) {
tmp[k++] = a[i++];
}
while (j <= r) {
tmp[k++] = a[j++];
}
for (int m : tmp) {
a[l++] = m;
}
}
- 任何情况下时间复杂度都是O(nlogn),求解过程是:首先假设对n个元素进行归并排序所需要的时间是T(n),那分解成两个子数组的排序时间就都是 T(n/2),合并这两个子数组的时间复杂度为 O(n),所以得到等式 T(n) = 2 * T(n/2) + n,进一步推导可以得到 T(n) = 2k * T(n/2k) + k * n,当划分到只有一个元素时,也就是 n/2k = 1 ,此时 k = logn ,所以最后 T(n) = Cn + nlogn ,所以时间复杂度为 O(nlogn)。
- 在合并两个有序数组为一个有序数组时,需要借助额外的存储空间,n/2,n/4,…,1 ,所以空间复杂度为 O(n)。
- 稳定。
快速排序(O(nlogn)、O(1)):使用的是分而治之的思想,将大问题分解成小的子问题来解决。首先选择一个基准值,然后遍历数组,将小于基准值的放在左边,大于基准值的放在右边,基准值放在中间,这样数组就被分成了三部分,然后再用递归的思想对小于基准值的部分和大于基准值的部分再进行一次比较划分,每一次可以确定其中一个元素的最终位置。
- 在理想情况下,每次分区操作都正好把数组分为大小相等的两个子区间,那快排的时间复杂度的求解公式和归并排序的一样,结果也为 O(nlogn)。而在极端情况下,比如数组已经有序时,每次分区都是不均等的,需要进行 n 次分区,这种情况下快排的时间复杂度退化为 O(n2)
- 空间复杂度为 O(1) 。
- 不稳定
public class QuickSort {
public void quickSort(int[] a) {
int n = a.length;
if (n <= 1) {
return;
}
qSort(a, 0, n - 1);
}
public void qSort(int[] a, int l, int r) {
if (l >= r) {
return;
}
int i = l;
int j = r;
int index = a[l]; //以第一个数为基准值
while (i != j) {
while (i < j && a[j] >= index) {
j--;
}
if (i < j) {
a[i++] = a[j];
}
while (i < j && a[i] < index) {
i++;
}
if (i < j) {
a[j--] = a[i];
}
}
a[i] = index;
qSort(a, l, i - 1);
qSort(a, i + 1, r);
}
}
堆排序:任何情况下时间复杂度都为 O(nlogn),空间复杂度为 O(1)。不稳定。堆总是一棵完全二叉树。
思想:
- 首先将待排序序列构成一个堆,假设构成一个大根堆,大根堆的性质就是每个节点的值都大于或等于它左右孩子节点的值;
- 然后将堆顶元素与末尾元素交换,交换后末尾元素就是这个序列的最大值
- 然后将剩余 n-1 个元素重新调整为大根堆,再次将堆顶元素与末尾元素交换,交换后末尾元素就是这个序列第二大的值了
- 反复执行,最后就得到了一个递增的有序序列。
- 排序重建堆的过程是每次将堆顶元素与末尾元素交换,然后重新调整剩余的 n - 1 个元素为大根堆,每次调整的时间为 logn,所以排序重建堆的时间复杂度为 nlogn。
- 建堆时间(初始化堆):每个节点在进行堆化时,需要比较和交换的节点个数,跟这个节点的高度成正比,也就是第一层节点个数为1,高度为h;第二层节点个数为2,高度为 h-1,以此类推,最后一层节点个数为 2 的 h-1 次方,高度为 1。然后将每个节点的高度求和,得到的就是建堆时间,公式为:S = 2^0 * h + 2^1 *(h-1) + 2^2 *(h-2) +…+ 2^(h-1) * 1 ,将公式左右都乘以 2,然后 2S - S,最后可以得到 S = 2^(h-1) - h - 2 。因为二叉树中的高度 h = logn,代入公式,就可以得到 S = 2n - logn - 2,所以建堆的时间复杂度为 O(n)。
- 综上所述,整体堆排序的时间复杂度为 O(nlogn)
public class HeapSort {
public void heapSort(int[] arr) {
//构建大根堆,从最后一个非叶子节点开始调整,左右孩子节点中较大的交换到父节点中
//数组排成二叉树,此二叉树最后一个非叶子节点的位置就是数组中下标为 arr.length/2 - 1 的位置
for (int i = arr.length / 2 - 1; i >= 0; i--) {
sort(arr, i, arr.length);
}
for (int j = arr.length - 1; j >= 1; j--) {
//将根节点与堆尾节点交换
int temp = arr[0];
arr[0] = arr[j];
arr[j] = temp;
//将剩余n-1个元素重新调整为大根堆
sort(arr, 0, j);
}
}
private void sort(int[] arr, int i, int len) {
int temp = arr[i];
//i节点的左结点下标:2*i+1
int k = 2 * i + 1;
while (k < len) {
if (k + 1 < len && arr[k] < arr[k + 1]) {
k++;
}
if (arr[k] > temp) {
arr[i] = arr[k];
i = k;
k = 2 * i + 1;
} else {
break;
}
}
arr[i] = temp;
}
}