分治
算法入门
分
:递归地将原问题分解为两个或多个子问题,直至到达最小子问题时终止。
治
:从已知解的最小子问题开始,从底至顶地将子问题的解进行合并,从而构建出原问题
的解。
搜索策略
基于分治实现二分查找
给定一个长度为 𝑛 的有序数组 nums ,其中所有元素都是唯一的,请查找元素 target 。
分治过程:
声明一个递归函数来求解:
/* 二分查找:问题 f(start, end) */
int recursiveSearch(int[] nums, int target, int start, int end) {
// 如果区间无效,代表未找到目标元素,返回 -1
if (start > end) {
return -1;
}
// 计算中间索引 mid
int mid = (start + end) / 2;
if (nums[mid] < target) {
// 递归搜索右半部分 f(mid + 1, end)
return recursiveSearch(nums, target, mid + 1, end);
} else if (nums[mid] > target) {
// 递归搜索左半部分 f(start, mid - 1)
return recursiveSearch(nums, target, start, mid - 1);
} else {
// 找到目标元素,返回它的索引
return mid;
}
}
/* 二分查找主函数 */
int binarySearch(int[] nums, int target) {
int length = nums.length;
// 从整个数组范围 f(0, length - 1) 开始求解
return recursiveSearch(nums, target, 0, length - 1);
}
构建二叉树问题
给定一棵二叉树的前序遍历 preorder 和中序遍历 inorder ,请从中构建二叉树,返回二叉树的根节点。假设二叉树中没有值重复的节点。
分析:
前序遍历的首元素 3 是根节点的值。
查找根节点 3 在
inorder
中的索引,利用该索引可将
inorder
划分为
[ 9 | 3 | 1 2 7 ]
。
根据
inorder
的划分结果,易得左子树和右子树的节点数量分别为 1 和 3 ,从而可将
preorder
划分为 [ 3 | 9 | 2 1 7 ] 。
即可得到根节点、左子树、右子树在
preorder
和
inorder
中的索引区间
。
将当前树的根节点在
preorder
中的索引记为
𝑖
。
将当前树的根节点在
inorder
中的索引记为
𝑚
。
将当前树在 inorder 中的索引区间记为
[𝑙, 𝑟]
。

得到:

代码实现:
/* 构建二叉树:分治法 */
TreeNode buildSubTree(int[] preorder, Map<Integer, Integer> inorderIndexMap, int preorderIndex, int left, int right) {
// 子树区间无效时终止
if (right < left) {
return null;
}
// 创建当前根节点
TreeNode currentRoot = new TreeNode(preorder[preorderIndex]);
// 获取当前根节点在中序数组中的索引 m,从而划分左右子树
int inorderIndex = inorderIndexMap.get(preorder[preorderIndex]);
// 子问题:构建左子树
currentRoot.left = buildSubTree(preorder, inorderIndexMap, preorderIndex + 1, left, inorderIndex - 1);
// 子问题:构建右子树
currentRoot.right = buildSubTree(preorder, inorderIndexMap, preorderIndex + 1 + inorderIndex - left, inorderIndex + 1, right);
// 返回当前根节点
return currentRoot;
}
/* 构建二叉树的主函数 */
TreeNode buildTree(int[] preorder, int[] inorder) {
// 初始化哈希表,存储中序数组元素到索引的映射
Map<Integer, Integer> inorderIndexMap = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
inorderIndexMap.put(inorder[i], i);
}
// 从预序列数组和中序映射开始构建树
TreeNode root = buildSubTree(preorder, inorderIndexMap, 0, 0, inorder.length - 1);
return root;
}
汉诺塔问题
给定三根柱子,记为 A 、 B 和 C 。起始状态下,柱子 A 上套着 𝑛 个圆盘,它们从上到下按照从小到大的顺序排列。我们的任务是要把这 𝑛 个圆盘移到柱子 C 上,并保持它们的原有顺序不变(如图 12‑10 所示)。在移动圆盘的过程中,需要遵守以下规则。1. 圆盘只能从一根柱子顶部拿出,从另一根柱子顶部放入。2. 每次只能移动一个圆盘。3. 小圆盘必须时刻位于大圆盘之上。
将原问题
𝑓(𝑛)
划分为两个子问题
𝑓(𝑛−1) 和一个子问题 𝑓(1)
,并按照以下顺序解决这三个子问题。

1. 将
𝑛 − 1
个圆盘借助
C
从
A
移至
B
。
2. 将剩余
1
个圆盘从
A
直接移至
C
。
3. 将
𝑛 − 1
个圆盘借助
A
从
B
移至
C
。
对于这两个子问题
𝑓(𝑛 − 1)
,
可以通过相同的方式进行递归划分
,直至达到最小子问题
𝑓(1)
。
代码实现:
/* 移动一个圆盘 */
void transferDisk(List<Integer> source, List<Integer> target) {
// 从 source 顶部拿出一个圆盘
Integer disk = source.remove(source.size() - 1);
// 将圆盘放入 target 顶部
target.add(disk);
}
/* 求解汉诺塔问题 f(n) */
void solveHanoi(int n, List<Integer> source, List<Integer> auxiliary, List<Integer> target) {
// 若 source 只剩下一个圆盘,则直接将其移到 target
if (n == 1) {
transferDisk(source, target);
return;
}
// 子问题 f(n-1):将 source 顶部 n-1 个圆盘借助 target 移到 auxiliary
solveHanoi(n - 1, source, target, auxiliary);
// 子问题 f(1):将 source 剩余一个圆盘移到 target
transferDisk(source, target);
// 子问题 f(n-1):将 auxiliary 顶部 n-1 个圆盘借助 source 移到 target
solveHanoi(n - 1, auxiliary, source, target);
}
/* 求解汉诺塔问题主函数 */
void solveHanoiMain(List<Integer> A, List<Integer> B, List<Integer> C) {
int numDisks = A.size();
// 将 A 顶部 numDisks 个圆盘借助 B 移到 C
solveHanoi(numDisks, A, B, C);
}
相关例题:
leetcode226.翻转二叉树
226. 翻转二叉树https://leetcode.cn/problems/invert-binary-tree/
法一:递归
public class Method01 {
/**
* 反转一棵二叉树。
*
* @param root 二叉树的根节点
* @return 返回反转后的二叉树的根节点
*/
public TreeNode invertTree(TreeNode root) {
// 如果当前节点为空,返回null
if (root == null) {
return null;
}
// 递归反转左子树,并将结果保存在left中
TreeNode left = invertTree(root.left);
// 递归反转右子树,并将右子树赋值给当前节点的左子树
root.left = invertTree(root.right);
// 将之前保存的左子树赋值给当前节点的右子树
root.right = left;
// 返回反转后的根节点
return root;
}
}
leetcode101.对称二叉树
101. 对称二叉树https://leetcode.cn/problems/symmetric-tree/
法一:递归
public class Method01 {
/**
* 判断一棵二叉树是否是对称的。
*
* @param root 二叉树的根节点
* @return 如果二叉树是对称的,返回 true;否则返回 false
*/
public boolean isSymmetric(TreeNode root) {
// 调用辅助方法检查根节点的左子树和右子树是否对称
return cheakSymmetric(root.left, root.right);
}
/**
* 辅助方法,检查两个子树是否对称。
*
* @param left 左子树的根节点
* @param right 右子树的根节点
* @return 如果两个子树是对称的,返回 true;否则返回 false
*/
private boolean cheakSymmetric(TreeNode left, TreeNode right) {
// 如果两个子树都为空,返回 true
if (left == null && right == null) {
return true;
}
// 如果一个子树为空而另一个不为空,返回 false
if (left == null || right == null) {
return false;
}
// 检查当前两个节点的值是否相等,并递归检查子树的对称性
return left.val == right.val
&& cheakSymmetric(left.left, right.right) // 递归检查左子树的左节点与右子树的右节点
&& cheakSymmetric(left.right, right.left); // 递归检查左子树的右节点与右子树的左节点
}
}
法二:迭代
public class Method02 {
/**
* 判断一棵二叉树是否是对称的。
*
* @param root 二叉树的根节点
* @return 如果二叉树是对称的,返回 true;否则返回 false
*/
public boolean isSymmetric(TreeNode root) {
// 调用辅助方法检查树的左右子树是否对称
return checkSymmetric(root, root);
}
/**
* 辅助方法,通过队列实现广度优先搜索,检查两棵子树是否对称。
*
* @param root1 第一棵子树的根节点
* @param root2 第二棵子树的根节点
* @return 如果两棵子树是对称的,返回 true;否则返回 false
*/
private boolean checkSymmetric(TreeNode root1, TreeNode root2) {
// 使用队列进行广度优先遍历
Queue<TreeNode> queue = new LinkedList<>();
// 将两棵子树的根节点加入队列
queue.offer(root1);
queue.offer(root2);
// 当队列不为空时循环处理
while (!queue.isEmpty()) {
// 从队列中取出两个节点进行比较
TreeNode node1 = queue.poll();
TreeNode node2 = queue.poll();
// 如果两个节点都为 null,继续下一轮循环
if (node1 == null && node2 == null) {
continue;
}
// 如果其中一个节点为 null,另一个不为 null,则树不对称
if (node1 == null || node2 == null) {
return false;
}
// 如果两个节点的值不相等,则树不对称
if (node1.val != node2.val) {
return false;
}
// 将左子树和右子树的节点以相反的顺序加入队列
queue.offer(node1.left);
queue.offer(node2.right);
queue.offer(node1.right);
queue.offer(node2.left);
}
// 如果遍历完成且没有发现不对称的情况,则树是对称的
return true;
}
}
leetcode50. Pow(x, n)
50. Pow(x, n)https://leetcode.cn/problems/powx-n/
法一:快速幂+递归
public class Method01 {
public double myPow(double x, int n) {
return n >= 0 ? quickMul(x, n) : 1.0 / quickMul(x, -n);
}
private double quickMul(double x, int n) {
if (n == 0) {
return 1; // 任何数的0次幂都是1
}
double y = quickMul(x, n / 2); // 递归计算x的n/2次幂
return n % 2 == 0 ? y * y : y * y * x; // 判定n是偶数还是奇数
}
}
法二:快速幂+迭代
public class Method02 {
public double myPow(double x, int n) {
long N = n;
return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N);
}
public double quickMul(double x, long N) {
double ans = 1.0;
// 贡献的初始值为 x
double x_contribute = x;
// 在对 N 进行二进制拆分的同时计算答案
while (N > 0) {
if (N % 2 == 1) {
// 如果 N 二进制表示的最低位为 1,那么需要计入贡献
ans *= x_contribute;
}
// 将贡献不断地平方
x_contribute *= x_contribute;
// 舍弃 N 二进制表示的最低位,这样我们每次只要判断最低位即可
N /= 2;
}
return ans;
}
}
leetcode110.平衡二叉树
110. 平衡二叉树https://leetcode.cn/problems/balanced-binary-tree/
法一:自顶向下递归
public class Method01 {
// 判断一棵树是否是平衡二叉树
public boolean isBalanced(TreeNode root) {
// 如果根节点为空,返回true(空树是平衡的)
if (root == null) {
return true;
}
// 计算左子树和右子树的深度差
// 如果深度差小于等于1,并且左子树和右子树均是平衡的,则返回true
return Math.abs(getDepth(root.left) - getDepth(root.right)) <= 1
&& isBalanced(root.left)
&& isBalanced(root.right);
}
// 获取树的深度
private int getDepth(TreeNode root) {
// 如果节点为空,深度为0
if (root == null) {
return 0;
}
// 递归计算左右子树的深度,返回当前节点的深度(1 + 左右子树深度的最大值)
return 1 + Math.max(getDepth(root.left), getDepth(root.right));
}
}
法二:自底向上递归
public class Method02 {
// 判断一棵树是否是平衡二叉树
public boolean isBalanced(TreeNode root) {
// 如果树的高度大于等于0,说明是平衡的,否则返回false
return height(root) >= 0;
}
// 计算树的高度,并判断树是否平衡
public int height(TreeNode root) {
// 如果当前节点为空,返回0,表示空树的高度
if (root == null) {
return 0;
}
// 递归计算左子树和右子树的高度
int leftHeight = height(root.left);
int rightHeight = height(root.right);
// 如果左子树或右子树不平衡,返回-1
// 或者左右子树的高度差大于1,返回-1
if (leftHeight == -1 || rightHeight == -1 || Math.abs(leftHeight - rightHeight) > 1) {
return -1; // 表示不平衡
} else {
// 返回当前节点的高度
return Math.max(leftHeight, rightHeight) + 1; // 高度为左右子树的最大高度加1
}
}
}
文章记录了学习Krahets的《Hello 算法》的轨迹,代码均使用Java语言,原书支持 Python、C++、Java、C#、Go、Swift、JavaScript、TypeScript、Dart、 Rust、C 和 Zig 等语言。