1106 字
6 分钟
分治思想(归并排序)
2025-08-25
2025-09-26
统计加载中...

分治思想(归并排序)#

0. 引入#

  • 分治(Divide and Conquer)= 分 → 治 → 合
    • 分:把大问题拆成规模更小、形式相同的子问题;
    • 治:递归地解决每个子问题;
    • 合:把子问题的答案合并,得到原问题答案。
  • 归并排序是分治思想的经典案例:它的“合”步骤是把两个有序段“拉链式”合并。

直观例子(升序):

  • 把区间 [l..r] 二分成 [l..mid][mid+1..r]
  • 分别排好左段、右段;
  • 用双指针合并这两个有序段。

归并排序性质一览:

  • 时间复杂度:O(nlogn)O(n log n)
  • 额外空间:O(n)O(n)
  • 具有稳定性

1. 从“合并两个有序段”开始#

“拉链式合并”

// 合并两个有序数组 A, B 到 C(都升序)
vector<int> merge_two(const vector<int>& A, const vector<int>& B) {
vector<int> C;
C.reserve(A.size() + B.size());
size_t i = 0, j = 0;
while (i < A.size() && j < B.size()) {
if (A[i] <= B[j]) C.push_back(A[i++]); // 用 <= 保证稳定性
else C.push_back(B[j++]);
}
while (i < A.size()) C.push_back(A[i++]);
while (j < B.size()) C.push_back(B[j++]);
return C;
}

2. 归并排序1#

void merge_sort(vector<int>& a, int l, int r, vector<int>& tmp) {
if (l >= r) return;
int mid = l + (r - l) / 2;
merge_sort(a, l, mid, tmp);
merge_sort(a, mid + 1, r, tmp);
// 小优化:如果两个段已经衔接有序,跳过合并
if (a[mid] <= a[mid + 1]) return;
int i = l, j = mid + 1, k = l;
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 x = l; x <= r; ++x) a[x] = tmp[x];
}

3. 归并排序2#

void merge_sort_iter(vector<int>& a) {
int n = (int)a.size();
vector<int> tmp(n);
for (int len = 1; len < n; len <<= 1) {
for (int l = 0; l < n; l += (len << 1)) {
int mid = min(l + len - 1, n - 1);
int r = min(l + (len << 1) - 1, n - 1);
if (mid >= r) continue; // 右段不存在
int i = l, j = mid + 1, k = l;
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 x = l; x <= r; ++x) a[x] = tmp[x];
}
}
}

4. 复杂度#

  • 时间复杂度:T(n)=2T(n2)+O(n)O(nlogn)T(n) = 2T(\frac{n}{2}) + O(n) → O(n log n)
    • 递归树每层都处理 n 个元素,共约 lognlogn 层。
  • 空间复杂度:O(n)O(n) 额外空间存放临时数组;递归版另有 O(logn)O(log n) 栈。

5. 应用一:逆序对计数(分治在“合并阶段统计”)#

题意(如:洛谷 P1908、POJ 2299)

  • 逆序对:i < j 且 a[i] > a[j] 的有序对个数。

6. 应用二:小和问题#

定义

  • 对每个 a[i],统计它右侧有多少元素 ≥ a[i],贡献 a[i] × 这个数量;求总贡献。

合并思路

  • 当 a[i] <= a[j] 时,a[i] 对右段当前 j..r 的所有元素都满足 ≥ a[i],一次性贡献 (rj+1)×a[i](r - j + 1) \times a[i]

7. 应用三:最大子段和(分治合并信息)#

设计四个量

  • sum:区间总和;
  • lmax:以左端点开头的最大前缀和;
  • rmax:以右端点结尾的最大后缀和;
  • best:区间内的最大子段和。

合并法则

  • sum = L.sum + R.sum
  • lmax = max(L.lmax, L.sum + R.lmax)
  • rmax = max(R.rmax, R.sum + L.rmax)
  • best = max(L.best, R.best, L.rmax + R.lmax)

8. 链表归并排序#

为什么链表适合归并

  • 归并只需要顺序前进指针,不依赖随机访问;
  • 快排/堆排对链表并不友好。

代码

struct ListNode {
int val;
ListNode* next;
ListNode(int v): val(v), next(nullptr) {}
};
ListNode* merge_lists(ListNode* a, ListNode* b) {
ListNode dummy(0);
ListNode* tail = &dummy;
while (a && b) {
if (a->val <= b->val) { tail->next = a; a = a->next; }
else { tail->next = b; b = b->next; }
tail = tail->next;
}
tail->next = a ? a : b;
return dummy.next;
}
ListNode* split_mid(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head->next;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
ListNode* right = slow->next;
slow->next = nullptr;
return right;
}
ListNode* merge_sort_list(ListNode* head) {
if (!head || !head->next) return head;
ListNode* right = split_mid(head);
ListNode* L = merge_sort_list(head);
ListNode* R = merge_sort_list(right);
return merge_lists(L, R);
}

9. 多路归并与外部排序概念#

  • k 路归并:有 k 个有序序列,合并成一个有序序列。
  • 做法:小根堆维护每个序列的当前最小元素,弹出后推进该序列下一个。
  • 复杂度:O(nlogk)O(n log k)
struct Item {
int val, i, j; // val 值;来自第 i 段的第 j 个
bool operator>(const Item& other) const { return val > other.val; }
};
vector<int> k_merge(const vector<vector<int>>& arrs) {
priority_queue<Item, vector<Item>, greater<Item>> pq;
for (int i = 0; i < (int)arrs.size(); ++i)
if (!arrs[i].empty()) pq.push({arrs[i][0], i, 0});
vector<int> res;
while (!pq.empty()) {
auto [v, i, j] = pq.top(); pq.pop();
res.push_back(v);
if (j + 1 < (int)arrs[i].size()) pq.push({arrs[i][j + 1], i, j + 1});
}
return res;
}
分治思想(归并排序)
https://lansganbs.cn/posts/算法竞赛/分治思想归并排序/
作者
LANSGANBS
发布于
2025-08-25
许可协议
CC BY-NC-SA 4.0