分治的经典应用
汉诺塔如何用分治求解?
- 问题: 把 个圆盘从 A 柱经 B 柱移到 C 柱, 每次只能移动一个且大盘不能压小盘;
- 分: 把顶部 个圆盘看作整体;
- 三步: ① 从 A 经 C 移到 B; ② 剩下 1 个从 A 移到 C; ③ 从 B 经 A 移到 C;
- 递推式: , 终止条件为只剩 1 个圆盘时直接移动;
const hanota = (A, B, C) => {
const dfs = (n, from, buffer, to) => {
if (n === 1) {
to.push(from.pop());
return;
}
dfs(n - 1, from, to, buffer); // n-1 从 from 经 to 移到 buffer
to.push(from.pop()); // 最大盘直接移入 to
dfs(n - 1, buffer, from, to); // n-1 从 buffer 经 from 移到 to
};
dfs(A.length, A, B, C);
};
- 关键: 三次移动中, 出发柱、缓冲柱、目标柱的角色随递归层交换;
合并 k 个有序链表如何用分治求解?
- 等价: 把归并排序中"按中点划分单链表"换成"按下标划分链表数组";
- 分: 把链表数组从中间切成两半;
- 治: 两个有序链表按双指针归并;
- 优势: 相比逐个归并的 , 分治两两归并只需 ;
const mergeKLists = (lists) => {
const merge = (l1, l2) => {
const dummy = new ListNode(-1);
let cur = dummy;
while (l1 && l2) {
if (l1.val < l2.val) {
cur.next = l1;
l1 = l1.next;
} else {
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
cur.next = l1 ?? l2;
return dummy.next;
};
const mergeK = (left, right) => {
if (left >= right) return lists[left];
const mid = left + ((right - left) >> 1);
return merge(mergeK(left, mid), mergeK(mid + 1, right));
};
return lists.length === 0 ? null : mergeK(0, lists.length - 1);
};
快速幂如何用分治把复杂度降到对数级?
- 目标: 计算 ;
- 分: 由指数的奇偶性拆成同底数的更小幂;
- 为偶数: ;
- 为奇数: ;
- 治: 每层只算一个子问题, 所以不是标准二分而是"减半";
- 边界: 时先取倒数并取反指数, 时返回 1;
const myPow = (x, n) => {
if (n < 0) return myPow(1 / x, -n);
if (n === 0) return 1;
const half = myPow(x * x, Math.floor(n / 2));
return n % 2 === 0 ? half : x * half;
};
- 复杂度: 时间 , 空间 ;
验证二叉搜索树的后序序列如何分治?
- 依据: 后序遍历顺序为"左 → 右 → 根", 数组最右元素是当前子树的根;
- 划分: 从左向右找到第一个大于根的元素, 其左侧是左子树、右侧是右子树;
- 校验: 右子树内必须全部大于根; 满足则递归校验两个子数组;
const verifyTreeOrder = (postorder) => {
const dfs = (left, right) => {
if (left >= right) return true;
let index = left;
while (postorder[index] < postorder[right]) index++; // 左子树边界
const mid = index;
while (postorder[index] > postorder[right]) index++; // 跳过右子树
return index === right && dfs(left, mid - 1) && dfs(mid, right - 1);
};
return dfs(0, postorder.length - 1);
};
- 复杂度: 时间 (退化为链时), 空间 ;
- 边界: 空区间与单元素区间直接返回 true, 对应
left >= right;
寻找两个正序数组的中位数如何用分治求解?
- 目标: 在两个有序数组中取第 小元素, ;
- 分: 设 nums1 取前 个、nums2 取前 个, 在 上二分 ;
- 判定:
nums1[a] < nums2[k - a - 1]说明前 小还缺 nums1 的元素, 令left = mid + 1; 否则right = mid; - 合并: 取两个候选左侧元素的最大值; 总长度为偶数时再与右侧最小值取平均;
- 前提: 先交换数组, 保证
nums1更短, 这样 的范围最小且不会越界;
const findMedianSortedArrays = (nums1, nums2) => {
if (nums1.length > nums2.length) return findMedianSortedArrays(nums2, nums1);
const k = Math.floor((nums1.length + nums2.length + 1) / 2);
let left = 0;
let right = nums1.length;
while (left < right) {
const mid = Math.floor(left + (right - left) / 2);
if (nums1[mid] < nums2[k - mid - 1]) left = mid + 1;
else right = mid;
}
const m = k - left;
const v1 = Math.max(left <= 0 ? -Infinity : nums1[left - 1], m <= 0 ? -Infinity : nums2[m - 1]);
if ((nums1.length + nums2.length) % 2 === 1) return v1;
const v2 = Math.min(
left >= nums1.length ? Infinity : nums1[left],
m >= nums2.length ? Infinity : nums2[m],
);
return (v1 + v2) / 2;
};
- 复杂度: 时间 , 空间 ;
- 边界: 某个数组取 0 个元素时, 对应候选值视为 / ;