1106 字
6 分钟
分治思想(归并排序)
分治思想(归并排序)
0. 引入
- 分治(Divide and Conquer)= 分 → 治 → 合
- 分:把大问题拆成规模更小、形式相同的子问题;
- 治:递归地解决每个子问题;
- 合:把子问题的答案合并,得到原问题答案。
- 归并排序是分治思想的经典案例:它的“合”步骤是把两个有序段“拉链式”合并。
直观例子(升序):
- 把区间
[l..r]二分成[l..mid]和[mid+1..r]; - 分别排好左段、右段;
- 用双指针合并这两个有序段。
归并排序性质一览:
- 时间复杂度:
- 额外空间:
- 具有稳定性
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. 复杂度
- 时间复杂度:
- 递归树每层都处理 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],一次性贡献 。
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 个有序序列,合并成一个有序序列。
- 做法:小根堆维护每个序列的当前最小元素,弹出后推进该序列下一个。
- 复杂度:。
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/算法竞赛/分治思想归并排序/