目录
搜索与回溯算法
1 求1+2+…+n
剑指 Offer 64. 求1+2+…+nhttps://leetcode-cn.com/problems/qiu-12n-lcof/
求 1+2+...+n
,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)
1. 递归 (使用了if pass)
package jzof.Day19;
/**
* @author ahan
* @create_time 2021-11-26-3:44 下午
*/
public class _64 {
public static void main(String[] args) {
System.out.println(new _64().sumNums(4));
}
public int sumNums(int n) {
if(n == 1 || n == 0) return 1;
else return sumNums(n-1)+n;
}
}
本题在简单问题上做了许多限制,需要使用排除法一步步导向答案。
1+2+...+(n-1)+n1+2+...+(n−1)+n 的计算方法主要有三种:平均计算、迭代、递归。
作者:jyd
2.1 平均计算
问题: 此计算必须使用 乘除法 ,因此本方法不可取,直接排除。
public int sumNums(int n) {
return (1 + n) * n / 2;
}
2.2 迭代
问题: 循环必须使用 while 或 for ,因此本方法不可取,直接排除。
public int sumNums(int n) {
int res = 0;
for(int i = 1; i <= n; i++)
res += i;
return res;
}
2.3 递归
问题: 终止条件需要使用 if ,因此本方法不可取。
思考: 除了 if 和 switch 等判断语句外,是否有其他方法可用来终止递归?
public int sumNums(int n) {
if(n == 1) return 1;
n += sumNums(n - 1);
return n;
}
逻辑运算符的短路效应:
常见的逻辑运算符有三种,即 “与 && ”,“或 ∣∣ ”,“非 ! ” ;而其有重要的短路效应,如下所示:
if(A && B) // 若 A 为 false ,则 B 的判断不会执行(即短路),直接判定 A && B 为 false
if(A || B) // 若 A 为 true ,则 B 的判断不会执行(即短路),直接判定 A || B 为 true
本题需要实现 “当 n = 1 时终止递归” 的需求,可通过短路效应实现
n > 1 && sumNums(n - 1) // 当 n = 1 时 n > 1 不成立 ,此时 “短路” ,终止后续递归
class Solution {
int res = 0;
public int sumNums(int n) {
boolean x = n > 1 && sumNums(n - 1) > 0;
res += n;
return res;
}
}
//或者这样不借助res变量
class Solution {
public int sumNums(int n) {
boolean x = n > 1 && (n += sumNums(n - 1)) > 0;
return n;
}
}
复杂度分析:
- 时间复杂度 O(n) : 计算 n + (n-1) + ... + 2 + 1 需要开启 n 个递归函数。
- 空间复杂度 O(n) : 递归深度达到 n ,系统使用 O(n) 大小的额外空间。
2 二叉搜索树的最近公共祖先
1. 迭代
- 循环搜索: 当节点 root 为空时跳出;
- 当 p, q 都在 root 的 右子树 中,则遍历至 root.right ;
- 否则,当 p, q 都在 root 的 左子树 中,则遍历至 root.left ;
- 否则,说明找到了 最近公共祖先 ,跳出。
- 返回值: 最近公共祖先 root 。
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
while(root != null){
if(root.val < p.val && root.val < q.val)
root = root.right;
if(root.val > p.val && root.val > q.val)
root = root.left;
else
break;
}
return root;
}
复杂度分析:
- 时间复杂度 O(N) : 其中 N 为二叉树节点数;每循环一轮排除一层,二叉搜索树的层数最小为 logN (满二叉树),最大为 N(退化为链表)。
- 空间复杂度 O(1) : 使用常数大小的额外空间。
1.1 优化
优化:若可保证 p.val < q.val ,则在循环中可减少判断条件。
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(p.val > q.val) { // 保证 p.val < q.val
TreeNode tmp = p;
p = q;
q = tmp;
}
while(root != null) {
if(root.val < p.val) // p,q 都在 root 的右子树中
root = root.right; // 遍历至右子节点
else if(root.val > q.val) // p,q 都在 root 的左子树中
root = root.left; // 遍历至左子节点
else break;
}
return root;
}
}
2. 递归
- 递推工作:
- 当 p, q 都在 root 的 右子树 中,则开启递归 root.right 并返回;
- 否则,当 p, q 都在 root 的 左子树 中,则开启递归 root.left 并返回;
- 返回值: 最近公共祖先 root 。
package jzof.Day19;
import jzof.TreeNode;
import java.util.LinkedList;
public class _68_1 {
public static void main(String[] args) {
TreeNode root = new TreeNode(6);
TreeNode node1 = new TreeNode(2);
TreeNode node2 = new TreeNode(8);
TreeNode node3 = new TreeNode(0);
TreeNode node4 = new TreeNode(4);
TreeNode node5 = new TreeNode(7);
TreeNode node6 = new TreeNode(9);
TreeNode node7 = new TreeNode(3);
TreeNode node8 = new TreeNode(5);
root.left = node1;
root.right = node2;
node1.left = node3;
node1.right = node4;
node2.left = node5;
node2.right = node6;
node4.left = node7;
node4.left = node8;
System.out.println(new _68_1().lowestCommonAncestor(root, node1, node2).val);
}
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right, p, q);
if(root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left, p, q);
return root;
}
}
复杂度分析:
- 时间复杂度 O(N) : 其中 N 为二叉树节点数;每循环一轮排除一层,二叉搜索树的层数最小为 logN (满二叉树),最大为 N (退化为链表)。
- 空间复杂度 O(N) : 最差情况下,即树退化为链表时,递归深度达到树的层数 N 。
3 二叉树的最近公共祖先
1. 递归
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || p == root || q == root) {
return root;
}
TreeNode l = lowestCommonAncestor(root.left, p, q);
TreeNode r = lowestCommonAncestor(root.right, p, q);
return l == null ? r : (r == null ? l : root);
}
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if(left == null) return right;
if(right == null) return left;
return root;
}
}
//情况 1. , 2. , 3. , 4. 的展开写法如下。
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null) return null; // 如果树为空,直接返回null
if(root == p || root == q) return root; // 如果 p和q中有等于 root的,那么它们的最近公共祖先即为root(一个节点也可以是它自己的祖先)
TreeNode left = lowestCommonAncestor(root.left, p, q); // 递归遍历左子树,只要在左子树中找到了p或q,则先找到谁就返回谁
TreeNode right = lowestCommonAncestor(root.right, p, q); // 递归遍历右子树,只要在右子树中找到了p或q,则先找到谁就返回谁
if(left == null) return right; // 如果在左子树中 p和 q都找不到,则 p和 q一定都在右子树中,右子树中先遍历到的那个就是最近公共祖先(一个节点也可以是它自己的祖先)
else if(right == null) return left; // 否则,如果 left不为空,在左子树中有找到节点(p或q),这时候要再判断一下右子树中的情况,如果在右子树中,p和q都找不到,则 p和q一定都在左子树中,左子树中先遍历到的那个就是最近公共祖先(一个节点也可以是它自己的祖先)
else return root; //否则,当 left和 right均不为空时,说明 p、q节点分别在 root异侧, 最近公共祖先即为 root
}
}
2. 存储父节点
思路
用哈希表存储所有节点的父节点,然后利用节点的父节点信息从 p 结点开始不断往上跳,并记录已经访问过的节点visited,再从 q 节点开始不断往上跳,如果碰到已经访问过的节点,那么这个节点就是要找的最近公共祖先。
算法
- 从根节点开始遍历整棵二叉树,用哈希表记录每个节点的父节点指针。
- 从 p 节点开始不断往它的祖先移动,并用数据结构记录已经访问过的祖先节点。
- 同样,再从 q 节点开始不断往它的祖先移动,如果有祖先已经被访问过,即意味着这是 p 和 q 的深度最深的公共祖先,即 LCA 节点。
class Solution {
Map<Integer, TreeNode> parent = new HashMap<Integer, TreeNode>();
Set<Integer> visited = new HashSet<Integer>();
public void dfs(TreeNode root) {
if (root.left != null) {
parent.put(root.left.val, root);
dfs(root.left);
}
if (root.right != null) {
parent.put(root.right.val, root);
dfs(root.right);
}
}
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
dfs(root);
while (p != null) {
visited.add(p.val);
p = parent.get(p.val);
}
while (q != null) {
if (visited.contains(q.val)) {
return q;
}
q = parent.get(q.val);
}
return null;
}
}
复杂度分析
-
时间复杂度:O(N),其中 N 是二叉树的节点数。二叉树的所有节点有且只会被访问一次,从
p
和q
节点往上跳经过的祖先节点个数不会超过 N ,因此总的时间复杂度为 O(N) 。 -
空间复杂度:O(N) ,其中 N 是二叉树的节点数。递归调用的栈深度取决于二叉树的高度,二叉树最坏情况下为一条链,此时高度为 N ,因此空间复杂度为 O(N) ,哈希表存储每个节点的父节点也需要 O(N) 的空间复杂度,因此最后总的空间复杂度为 O(N) 。