排序算法

本文内容参考 sorting-algorithms,《算法》,《算法导论》。(强迫症,最后还是把描述性的文字删了,只贴代码

验证算法的正确性,可以使用:912. 排序数组148. 排序链表164. 最大间距

简单概述

理想的排序算法

  • 稳定性:相同键不会重新排序。
  • 原地排序:不使用额外空间存储排序数据。
  • 最坏情况 \(O(n\log{n})\) 次比较,\(O(n)\) 次交换。
  • 适应性:当数据接近排序或唯一键很少时,时间复杂度为 \(O(n)\)。

如何看待常数因子的改进方案

摘抄自《算法》的一段内容:在每一节中,我们会将书中的每个算法都看做某种应用的关键。但在整体上,我们希望学习的是为每种应用找到最合适的算法。我们并不是在推荐读者一定要实现所有提到的改进方法,而是提醒大家不要对算法初始实现的性能盖棺定论。研究一个新问题时,最好的方法是先实现一个你能想到的最简单的程序,当它成为瓶颈的时候再继续改进它。实现那些只能把运行时间缩短某个常数因子的改进措施可能并不值得。你需要用实验来检验一项改进,正如本书练习所演示的那样。

冒泡排序

  • 稳定性:稳定。
  • 时间复杂度:\(O(n^{2})\)。
  • 空间复杂度:\(O(1)\)。
  • 数据接近排序时,时间复杂度为 \(O(n)\)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static void bubbleSort(int[] nums) {
int n = nums.length, pos;
for (int i = n - 1; i > 0; i = pos) {
pos = 0;
for (int j = 0; j < i; j++) {
// 稳定性的关键
if (nums[j] > nums[j + 1]) {
swap(nums, j, j + 1);
// 将最后交换的位置,作为下次冒泡的上界
pos = j;
}
}
}
}

选择排序

  • 稳定性:不稳定。
  • 时间复杂度:\(O(n^{2})\)。元素的交换次数最少的排序算法。
  • 空间复杂度:\(O(1)\)。
1
2
3
4
5
6
7
8
9
10
11
12
private static void selectionSort(int[] nums) {
int n = nums.length;
for (int i = 0; i < n; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[minIndex]) {
minIndex = j;
}
}
if (minIndex != i) swap(nums, minIndex, i);
}
}

插入排序

  • 稳定性:稳定。
  • 时间复杂度:\(O(n^{2})\)。元素的交换次数等于逆序数。
  • 空间复杂度:\(O(1)\)。
  • 数据接近排序时,时间复杂度为 \(O(n)\)。
1
2
3
4
5
6
7
8
9
10
11
private static void insertionSort(int[] nums) {
int n = nums.length;
for (int i = 1; i < n; i++) {
int j = i - 1, t = nums[i];
// 稳定性的关键
for (; j >= 0 && nums[j] > t; j--) {
nums[j + 1] = nums[j];
}
nums[j + 1] = t;
}
}

希尔排序

  • 稳定性:不稳定。
  • 时间复杂度:使用 Knuth 增量序列 \(O(n^{\frac{3}{2}})\),。Knuth 增量序列:\(1,4,13,40,\dots,\frac{3^{k}-1}{2}\),相当于首项为 \(1\),公比为 \(3\) 的数列的前缀和。
  • 空间复杂度:\(O(1)\)。
  • 数据接近排序时,时间复杂度为 \(O(n\log{n})\)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static void insertionSort(int[] nums) {
int n = nums.length, h = 1;
while (h < n / 3) h = 3 * h + 1;
while (h >= 1) {
for (int i = h; i < n; i++) {
int j = i - h, t = nums[i];
for (; j >= 0 && nums[j] > t; j -= h) {
nums[j + h] = nums[j];
}
nums[j + h] = t;
}
h /= 3;
}
}

归并排序

  • 稳定性:稳定的。
  • 时间复杂度:\(O(n\log{n})\)。
  • 空间复杂度:排序数组 \(O(n)\);自顶向下排序链表 \(O(\log{n})\)(递归的空间),自底向上排序链表 \(O(1)\)。
  • 顺序访问数据,缓存友好。

实现一:自顶向下(数组)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static void mergeSort(int[] nums) {
int n = nums.length;
int[] aux = new int[n];
mergeSort(nums, aux, 0, n - 1);
}

private static void mergeSort(int[] nums, int[] aux, int lo, int hi) {
if (lo >= hi) return;
int mid = lo + (hi - lo) / 2;
mergeSort(nums, aux, lo, mid);
mergeSort(nums, aux, mid + 1, hi);
merge(nums, aux, lo, mid, hi);
}

private static void merge(int[] nums, int[] aux, int lo, int mid, int hi) {
for (int i = lo; i <= hi; i++) {
aux[i] = nums[i];
}
int i = lo, j = mid + 1;
for (int k = lo; k <= hi; k++) {
if (i > mid) nums[k] = aux[j++];
else if (j > hi) nums[k] = aux[i++];
// 稳定性的关键
else if (aux[i] <= aux[j]) nums[k] = aux[i++];
else nums[k] = aux[j++];
}
}

实现二:自底向上(数组)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static void mergeSort(int[] nums) {
int n = nums.length;
int[] aux = new int[n];
for (int len = 1; len < n; len += len) {
for (int i = 0; i + len < n; i += len << 1) {
merge(nums, aux, i, i + len - 1, Math.min(n - 1, i + (len << 1) - 1));
}
}
}

private static void merge(int[] nums, int[] aux, int lo, int mid, int hi) {
for (int i = lo; i <= hi; i++) {
aux[i] = nums[i];
}
int i = lo, j = mid + 1;
for (int k = lo; k <= hi; k++) {
if (i > mid) nums[k] = aux[j++];
else if (j > hi) nums[k] = aux[i++];
// 稳定性的关键
else if (aux[i] <= aux[j]) nums[k] = aux[i++];
else nums[k] = aux[j++];
}
}

实现三:自顶向下(链表)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private static ListNode mergeSort(ListNode head) {
if (head == null || head.next == null) {
return head;
}

// 分为两个子链表
ListNode mid = middleNode(head);
ListNode next = mid.next;
mid.next = null;

head = mergeSort(head);
next = mergeSort(next);
return merge(head, next);
}

// 返回合并链表的头节点
private static ListNode merge(ListNode head1, ListNode head2) {
ListNode dummy = new ListNode(), curr = dummy;
while (head1 != null && head2 != null) {
// 稳定性的关键
if (head1.val <= head2.val) {
curr.next = head1;
head1 = head1.next;
} else {
curr.next = head2;
head2 = head2.next;
}
curr = curr.next;
}
curr.next = head1 != null ? head1 : head2;
return dummy.next;
}

// 调用保证 head != null,并且如果有两个中间节点,则返回前一个(不然会无限递归)
// 使用 fast = head.next 可以保证返回前一个中间节点
// 使用 fast = head 可以保证返回后一个中间节点
private static ListNode middleNode(ListNode head) {
if (head == null) return head;
ListNode slow = head, fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}

实现四:自底向上(链表)

下面这个是没有提前计算链表长度,没有断开链表的解法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private static ListNode mergeSort(ListNode head) {
if (head == null || head.next == null) return head;

int len = 1;
ListNode dummy = new ListNode(-1, head);

while (true) {
// 两个子链表的头节点的前一个节点
ListNode prev1, prev2;
prev1 = prev2 = dummy;
while (true) {
for (int i = 0; i < len && prev2 != null; i++) {
prev2 = prev2.next;
}
if (prev2 == null || prev2.next == null) break;

prev1 = prev2 = merge(prev1, prev2, len);
}
// 如果该轮没有进行合并操作,则排序完成
if (prev1 == dummy) break;
len += len;
}
return dummy.next;
}

// 返回合并链表的尾节点
private static ListNode merge(ListNode prev1, ListNode prev2, int len) {
// 判断条件:第一个链表未结束 && 第二个链表未结束(考虑长度不足的情况)
while (prev1 != prev2 && len != 0 && prev2.next != null) {
// 稳定性的关键
if (prev1.next.val > prev2.next.val) {
// 将 prev2.next 插入到 prev1 之后
ListNode temp = prev2.next;
prev2.next = prev2.next.next;
temp.next = prev1.next;
prev1.next = temp;
len--;
}
prev1 = prev1.next;
}
// 使 prev2 指向合并链表的尾节点
while (len-- != 0 && prev2.next != null) prev2 = prev2.next;
return prev2;
}

快速排序

  • 稳定性:不稳定。
  • 时间复杂度:期望 \(O(n\log{n})\)。
  • 空间复杂度:期望 \(O(\log{n})\)。
  • 如果使用三路快排,数据包含大量重复元素时,时间复杂度为 \(O(n)\)。

实现一:二路快排

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private static void quickSort(int[] nums) {
int n = nums.length;
quickSort(nums, 0, n - 1);
}

private static void quickSort(int[] nums, int lo, int hi) {
if (lo >= hi) return;
int index = partition(nums, lo, hi);
quickSort(nums, lo, index - 1);
quickSort(nums, index + 1, hi);
}

private static int partition(int[] nums, int lo, int hi) {
// 三数取中
int mid = median3(nums, lo, lo + (hi - lo) / 2, hi);
swap(nums, lo, mid);

int key = nums[lo];
int i = lo, j = hi + 1;
while (true) {
// 和切分元素相等的元素也会停顿并交换
while (nums[++i] < key && i != hi);
while (nums[--j] > key);
if (i >= j) break;
swap(nums, i, j);
}
swap(nums, lo, j);
return j;
}

实现二:三路快排

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private static final Random RANDOM = new Random();

private static void quickSort(int[] nums) {
int n = nums.length;
quickSort(nums, 0, n - 1);
}

private static void quickSort(int[] nums, int lo, int hi) {
if (lo >= hi) return;

// 随机选择
int randomIndex = lo + RANDOM.nextInt(hi - lo + 1);
swap(nums, randomIndex, lo);

int key = nums[lo];
int lt = lo, gt = hi, i = lo + 1;
while (i <= gt) {
if (nums[i] < key) swap(nums, i++, lt++);
else if (nums[i] > key) swap(nums, i, gt--);
else i++;
}

quickSort(nums, lo, lt - 1);
quickSort(nums, gt + 1, hi);
}

堆排序

  • 稳定性:不稳定。
  • 时间复杂度:\(O(n\log{n})\)。
  • 空间复杂度:\(O(1)\)。
  • 随机访问数据,缓存不友好。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void heapSort(int[] nums) {
int n = nums.length;

// 建堆 - 时间复杂度 O(n)
for (int i = n / 2 - 1; i >= 0; i--) {
sink(nums, i, n - 1);
}

// 排序 - 时间复杂度 O(nlog(n))
for (int i = n - 1; i > 0; ) {
swap(nums, 0, i--);
sink(nums, 0, i);
}
}

private static void sink(int[] nums, int i, int n) {
int t = nums[i];
while (2 * i + 1 <= n) {
int j = 2 * i + 1;
if (j + 1 <= n && nums[j + 1] > nums[j]) {
j++;
}
if (t >= nums[j]) break;
nums[i] = nums[j];
i = j;
}
nums[i] = t;
}

计数排序

  • 稳定性:稳定。
  • 时间复杂度:\(O(n+k)\)。
  • 空间复杂度:\(O(n+k)\)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 元素取值范围 [0,k)
private static int[] countingSort(int[] nums, int k) {
int n = nums.length;
int[] cnt = new int[k];
int[] res = new int[n];

// 计数
for (int x : nums) {
cnt[x]++;
}

// 前缀和
for (int i = 1; i < k; i++) {
cnt[i] += cnt[i - 1];
}

// 排序(倒序遍历,保证稳定性)
for (int i = n - 1; i >= 0; i--) {
res[--cnt[nums[i]]] = nums[i];
}
return res;
}

基数排序

  • 稳定性:稳定。
  • 时间复杂度:\(O(d\cdot (n+k))\)。
  • 空间复杂度:\(O(n+k)\)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 元素最多有 d 位
private static int[] radixSort(int[] nums, int d) {
int n = nums.length;
int[] cnt = new int[10];
int[] aux = new int[n];
int divisor = 1;
for (int i = 0; i < d; i++) {
countingSort(nums, cnt, aux, divisor);

Arrays.fill(cnt, 0);
divisor *= 10;

int[] t = nums;
nums = aux;
aux = t;
}
return nums;
}

private static void countingSort(int[] nums, int[] cnt, int[] aux, int divisor) {
int n = nums.length;

// 计数
for (int x : nums) {
cnt[x / divisor % 10]++;
}

// 前缀和
for (int i = 1; i < 10; i++) {
cnt[i] += cnt[i - 1];
}

// 排序(倒序遍历,保证稳定性)
for (int i = n - 1; i >= 0; i--) {
aux[--cnt[nums[i] / divisor % 10]] = nums[i];
}
}

桶排序

  • 稳定性:取决于桶内的排序策略。
  • 时间复杂度:期望 \(O(n)\)。(输入数据均匀分布时)
  • 空间复杂度:\(O(n+m)\)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private static void bucketSort(int[] nums) {
int n = nums.length;

if (n <= 1) return;

int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (int x : nums) {
min = Math.min(min, x);
max = Math.max(max, x);
}

// 小心除以零
int inter = Math.max(1, (max - min) / (n - 1));
int bucketSize = (max - min) / inter + 1;

List<Integer>[] buckets = new List[bucketSize];
Arrays.setAll(buckets, k -> new ArrayList<>());

for (int x : nums) {
int i = (x - min) / inter;
buckets[i].add(x);

// 插入排序
int j = buckets[i].size() - 1;
for (; j > 0 && buckets[i].get(j - 1) > x; j--) {
buckets[i].set(j, buckets[i].get(j - 1));
}
buckets[i].set(j, x);
}

int idx = 0;
for (var bucket : buckets) {
for (int x : bucket) {
nums[idx++] = x;
}
}
}
作者

Ligh0x74

发布于

2023-10-26

更新于

2024-09-02

许可协议

评论