排序算法不但是算法考试的重点,也是面试中经常会问到的问题。对于常见的算法,不但要能熟练写出,还要明白其时间复杂度、空间复杂度、稳定性以及适用的场景。
1.快速排序
-
算法思想:选择一个主元,比主元小的放在一边,比主元大的放在另外一边。让后对两边分别递归使用快速排序。
-
时间复杂度:
- 平均时间复杂度 O(nlog2n)
- 最好时间复杂度 O(nlog2n)
- 最坏时间复杂度 O(n2)
-
空间复杂度:O(nlog2n)
-
稳定性:不稳定
-
适用场景:寻找数组中的第 K 大数。Partition得到 pos 后,若 pos == K - 1,则得到了答案,不用继续寻找。若 pos > K - 1 就到[left, pos - 1] 中寻找,若 pos < K - 1 就到 [pos + 1, right] 中寻找。
-
不适用场景:元素有序时,退化成冒泡排序,主元不能把当前区间划分成两个长度接近的子区间。
- 使用随机选择主元可以解决。
- 三数取中解决:在left、mid、right中选择中间大小的数。
private static void quickSort(int[] nums, int left, int right) {
if (left < right) {
int pos = Partition(nums, left, right);
quickSort(nums, left, pos - 1);
quickSort(nums, pos + 1, right);
}
}
private static int Partition(int[] nums, int left, int right) {
int pos = new Random().nextInt(right - left + 1) + left;
int pivot = nums[pos];
nums[pos] = nums[left];
while (left < right) {
while (left < right && nums[right] > pivot) right--;
nums[left] = nums[right];
while (left < right && nums[left] <= pivot) left++;
nums[right] = nums[left];
}
nums[left] = pivot;
return left;
}
2.归并排序
-
算法思想:将初始序列的n个记录看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到 n / 2 个长度为2的有序子序列。在此基础上,再对长度为2的有序子序列进行两两归并,得到 n / 4个长度为4的有序子序列。以此类推,直到得到一个长度为n的有序序列。
-
时间复杂度:
- 平均时间复杂度 O(nlog2n)
- 最好时间复杂度 O(nlog2n)
- 最坏时间复杂度 O(nlog2n)
-
空间复杂度:O(n)
-
稳定性:稳定
-
适用场景:
- n 较大且要求是稳定的排序。
- 对链表排序。
-
不适用场景:要求原地排序。
// 递归版本
public void mergeSort(int[] nums, int left, int right) {
if (left < right) {
// >>>1相当于对(left+right)/2,但不会溢出
int mid = (left + right) >>> 1;
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
merge(nums, left, mid, mid + 1, right);
}
}
// 非递归版本
public void mergeSort2(int[] nums) {
int n = nums.length;
for (int step = 2; step / 2 <= n; step <<= 1) {
for (int i = 0; i < n; i+=step) {
int mid = i + step / 2 - 1;
if (mid + 1 < n) {
merge(nums, i, mid, mid + 1, Math.min(i + step - 1, n - 1));
}
}
}
}
public void merge(int[] nums, int L1, int R1, int L2, int R2) {
int i = L1, j = L2;
int[] temp = new int[R2 - L1 + 1];
int idx = 0;
while (i <= R1 && j <= R2) {
if (nums[i] <= nums[j]) temp[idx++] = nums[i++];
else temp[idx++] = nums[j++];
}
while (i <= R1) temp[idx++] = nums[i++];
while (j <= R2) temp[idx++] = nums[j++];
for (i = 0; i < idx; i++) {
nums[i + L1] = temp[i];
}
}
3.堆排序
-
算法思想:堆是一棵完全二叉树,树中的每个结点的值都不小于(或不大于)其左右孩子结点的值。最大元素在对堆顶是大顶堆,反之是小顶堆。一般用优先队列实现。
-
时间复杂度:
- 平均时间复杂度 O(nlog2n)
- 最好时间复杂度 O(nlog2n)
- 最坏时间复杂度 O(nlog2n)
-
空间复杂度:O(1)
-
稳定性:不稳定
-
适用场景:
- 寻找 topN。建立一个小顶堆,遍历数组,大于堆顶元素才插入,插入后如果堆的 size > N 那么弹出堆顶元素。最终堆里面存储的就是 topN 的元素。时间复杂度为M*log(N)
- 维护数据流中的中位数。建立一个大顶堆存较小元素,小顶堆存较大元素。保持大顶堆和小顶堆 size 相等或者大顶堆的size 等于小顶堆 size + 1。大顶堆堆顶元素或者两个堆堆顶元素就是中位数。
// 方法一:用有序队列实现(推荐)
import java.util.Comparator;
import java.util.PriorityQueue;
public class Main {
public static void main(String[] args) {
int[] nums = new int[]{1, 5, 6, 6, 3, 1, 2};
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
for (int num : nums) {
minHeap.offer(num);
maxHeap.offer(num);
}
while (!minHeap.isEmpty()) {
System.out.print(minHeap.poll() + " ");
}
System.out.println();
while (!maxHeap.isEmpty()) {
System.out.print(maxHeap.poll() + " ");
}
}
}
// 方法二:用数组实现
public class Main {
private static int maxN; // 堆中最多能容纳多少个元素(数组的大小)
private static int n; // 元素个数
private static int[] heap; // 堆
/* 向下调整,时间复杂度O(logn)
* 将当前节点与左右孩子比较,与值更大的交换
*/
public static void downAdjust(int low, int high) {
int i = low, j = i * 2; // i 为当前节点,j 是其左孩子
while (j <= high) {
// 如果有孩子存在且大于左孩子,那么移动到右孩子
if (j + 1 <= high && heap[j + 1] > heap[j]) {
j = j + 1;
}
// 交换并向下移动
if (heap[j] > heap[i]) {
swap(i, j);
i = j;
j = i * 2;
} else break; // 孩子的权值均比欲调整节点 i 小,调整结束
}
}
/* 建堆。从 n/2个节点开始倒着枚举。
* 时间复杂度O(n)
*/
public static void createHeap() {
for (int i = n / 2; i >= 1; i--) {
downAdjust(i, n);
}
}
/* 删除堆顶元素:用最后一个元素覆盖堆顶元素并向下调整。
* 时间复杂度O(logn)
*/
public static void deleteTop() {
heap[1] = heap[n--];
downAdjust(1, n);
}
/* 添加元素:放到数组最后,然后向上调整。
* 时间复杂度 O(logn)
*/
public static void upAdjust(int low, int high) {
int i = high, j = i / 2;
while (j >= low) {
if (heap[j] < heap[i]) {
swap(i, j);
i = j;
j = i / 2;
} else break;
}
}
public static void insert(int x) {
heap[++n] = x;
upAdjust(1, n);
}
/* 堆排序 */
public static void heapSort() {
createHeap();
for (int i = n; i > 1; i--) { // 倒着枚举,不断减小堆的元素个数
swap(i, 1); // 将堆顶元素放到最后一个位置
downAdjust(1, i - 1); // 重新调整堆
}
}
public static void swap(int i, int j) {
int temp = heap[i];
heap[i] = heap[j];
heap[j] = temp;
}
public static void main(String[] args) {
maxN = 100;
heap = new int[maxN];
int[] input = new int[]{5, 6, 1, 3, 6, 2, 1};
n = input.length;
for (int i = 1; i <= n; i++) {
heap[i] = input[i - 1];
}
heapSort();
for (int i = 1; i <= n; i++) {
System.out.print(heap[i] + " ");
}
}
}
4.直接插入排序
-
算法思想:对于 nums[1]~nums[n],从 i = 2 开始枚举,每次开始,nums[1]~nums[i - 1] 已经有序,[i, n] 无序。每趟排序从 [1, i - 1] 中找到一个位置 j,使得 nums[i] 放到 j,原本的 nums[j]~nums[i - 1] 后移一位之后,[1, i]变成有序。
-
时间复杂度:
- 平均时间复杂度 O(n2)
- 最好时间复杂度 O(n)
- 最坏时间复杂度 O(n2)
-
空间复杂度:O(1)
-
稳定性:稳定
-
适用场景:基本有序时使用。最好时间复杂度为 O(n)。
-
不适用场景:数据量庞大的时候,插入后的数据移动很多。
public void insertSort(int[] nums) {
for (int i = 1; i < n; i++){ // 进行 n-1 趟排序
int temp = nums[i], j = i;
while (j > 0 && temp < nums[j - 1]) {
nums[j] = nums[j - 1];
j--;
}
nums[j] = temp;
}
}
5.冒泡排序
-
算法思想:本质是交换,每次把当前剩余的所有元素中最大的元素移到最后。剩余元素为 0 时排序结束。
-
时间复杂度:
- 平均时间复杂度 O(n2)
- 最好时间复杂度 O(n)
- 最坏时间复杂度 O(n2)
-
空间复杂度:O(1)
-
稳定性:稳定
-
适用场景:基本有序时使用。设一个标志位,没有发生移动就提前结束排序。
public void sortArray(int[] nums) {
int len = nums.length;
for (int i = len - 1; i >= 0; i--) {
// 先默认数组是有序的,只要发生一次交换,就必须进行下一轮比较,
// 如果在内层循环中,都没有执行一次交换操作,说明此时数组已经是升序数组
boolean sorted = true;
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
sorted = false;
}
}
if (sorted) break;
}
}
6.简单选择排序
-
算法思想:每次从 nums[i]~nums[n] 中选择最小的与待排序部分的第一个元素 nums[i] 交换,这样有序区间从 [1, i - 1] 变成了 [1, i]。重复这样的操作直到全部有序。
-
时间复杂度:
- 平均时间复杂度 O(n2)
- 最好时间复杂度 O(n2)
- 最坏时间复杂度 O(n2)
-
空间复杂度:O(1)
-
稳定性:不稳定
public void selectSort(int[] nums) {
for (int i = 0; i < n; i++) { // 进行 n 趟操作
int k = i;
for (int j = i; j < n; j++) { // 选择[i, n)中的最小值
if (nums[j] < nums[k]) k = j;
}
int temp = nums[k];
nums[k] = nums[i];
nums[i] = temp;
}
}
欢迎关注公众号!