阿翰 剑指offer 之 Day 19 搜索与回溯算法 中等

目录

搜索与回溯算法

1 求1+2+…+n

1. 递归 (使用了if pass)

2.1 平均计算

2.2 迭代

2.3 递归

2 二叉搜索树的最近公共祖先

1. 迭代

1.1 优化

2. 递归

3 二叉树的最近公共祖先

1. 递归

2. 存储父节点


搜索与回溯算法

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 二叉搜索树的最近公共祖先

剑指 Offer 68 - I. 二叉搜索树的最近公共祖先https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-zui-jin-gong-gong-zu-xian-lcof/

1. 迭代

  1. 循环搜索: 当节点 root 为空时跳出;
    1. 当 p, q 都在 root 的 右子树 中,则遍历至 root.right ;
    2. 否则,当 p, q 都在 root 的 左子树 中,则遍历至 root.left ;
    3. 否则,说明找到了 最近公共祖先 ,跳出。
  2. 返回值: 最近公共祖先 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. 递归

  1. 递推工作:
    1. 当 p, q 都在 root 的 右子树 中,则开启递归 root.right 并返回;
    2. 否则,当 p, q 都在 root 的 左子树 中,则开启递归 root.left 并返回;
  2. 返回值: 最近公共祖先 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 二叉树的最近公共祖先

剑指 Offer 68 - II. 二叉树的最近公共祖先https://leetcode-cn.com/problems/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof/

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 节点开始不断往上跳,如果碰到已经访问过的节点,那么这个节点就是要找的最近公共祖先。

算法

  1. 从根节点开始遍历整棵二叉树,用哈希表记录每个节点的父节点指针。
  2. 从 p 节点开始不断往它的祖先移动,并用数据结构记录已经访问过的祖先节点。
  3. 同样,再从 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) 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值