力扣每日练习-java版(三)

208. 实现 Trie (前缀树)

208. 实现 Trie (前缀树)
在这里插入图片描述

思路

Trie 树又叫字典树、前缀树、单词查找树,是一种二叉树衍生出来的高级数据结构,主要应用场景是处理字符串前缀相关的操作。
其每个节点包含以下字段:

  • 指向子节点的指针数组children。对于本题而言,数组长度为 226,即小写英文字母的数量。此时children[0] 对应小写字母 a,children[1] 对应小写字母 b,…,children[25] 对应小写字母 z。
  • 布尔字段isEnd,表示该节点是否为字符串的结尾。

    插入字符串
    我们从字典树的根开始,插入字符串。对于当前字符对应的子节点,有两种情况:
    • 子节点存在。沿着指针移动到子节点,继续处理下一个字符。
    • 子节点不存在。创建一个新的子节点,记录在children 数组的对应位置上,然后沿着指针移动到子节点,继续搜索下一个字符。
      重复以上步骤,直到处理字符串的最后一个字符,然后将当前节点标记为字符串的结尾。
      查找前缀
      我们从字典树的根开始,查找前缀。对于当前字符对应的子节点,有两种情况:
    • 子节点存在。沿着指针移动到子节点,继续搜索下一个字符。
    • 子节点不存在。说明字典树中不包含该前缀,返回空指针。
      重复以上步骤,直到返回空指针或搜索完前缀的最后一个字符。
      若搜索到了前缀的末尾,就说明字典树中存在该前缀。此外,若前缀末尾对应节点的isEnd 为真,则说明字典树中存在该字符串。

代码

class Trie {
    private Trie[] children;
    private boolean isEnd;

    public Trie() {
        children = new Trie[26];
        isEnd = false;
    }
    
    public void insert(String word) {
        Trie node = this;
        for (int i = 0; i < word.length(); i++) {
            char ch = word.charAt(i);
            int index = ch - 'a';
            if (node.children[index] == null) {
                node.children[index] = new Trie();
            }
            node = node.children[index];
        }
        node.isEnd = true;
    }
    
    public boolean search(String word) {
        Trie node = searchPrefix(word);
        return node != null && node.isEnd;
    }
    
    public boolean startsWith(String prefix) {
        return searchPrefix(prefix) != null;
    }

    private Trie searchPrefix(String prefix) {
        Trie node = this;
        for (int i = 0; i < prefix.length(); i++) {
            char ch = prefix.charAt(i);
            int index = ch - 'a';
            if (node.children[index] == null) {
                return null;
            }
            node = node.children[index];
        }
        return node;
    }
}

时空复杂度

在这里插入图片描述

备注

198. 打家劫舍

198. 打家劫舍
在这里插入图片描述

思路

动态规划。

  1. 自顶向下
    状态: 当前房子索引。
    选择: 抢or不抢。抢就不能再选相邻房间,要从下下个房间开始选择;不抢就放弃当前,从下一个房子开始选择。
    base case: 走过了最后一间房。
    优化重复子问题: 使用备忘录记录抢过的房间。
  2. 自底向上
    状态: 当前房子索引。
    选择: 抢or不抢。抢就不能再选相邻房间,要从下下个房间开始选择;不抢就放弃当前,从下一个房子开始选择。
    base case: 从结果往回推,走过了第一间房结束。
    优化重复子问题: 只维护走一步、走两步的最大值

代码

  1. 自顶向下
class Solution {
    // 备忘录
    private int[] memo;
    // 主函数
    public int rob(int[] nums) {
        // 初始化备忘录
        memo = new int[nums.length];
        Arrays.fill(memo, -1);
        // 强盗从第 0 间房子开始抢劫
        return dp(nums, 0);
    }

    // 返回 dp[start..] 能抢到的最大值
    private int dp(int[] nums, int start) {
        if (start >= nums.length) {
            return 0;
        }
        // 避免重复计算
        if (memo[start] != -1) return memo[start];

        int res = Math.max(dp(nums, start + 1),
                nums[start] + dp(nums, start + 2));
        // 记入备忘录
        memo[start] = res;
        return res;
    }
}
  1. 自底向上
class Solution {
    public int rob(int[] nums) {
         int n = nums.length;
        // 记录 dp[i+1] 和 dp[i+2]
        int dp_i_1 = 0, dp_i_2 = 0;
        // 记录 dp[i]
        int dp_i = 0; 
        for (int i = n - 1; i >= 0; i--) {
            dp_i = Math.max(dp_i_1, nums[i] + dp_i_2);
            dp_i_2 = dp_i_1;
            dp_i_1 = dp_i;
        }
        return dp_i;
    }
}

时空复杂度

1.时间复杂度
O(n)
2.空间复杂度
​ 方法一:O(n) 备忘录需要空间 。
方法二:O(1)

备注

  1. Arrays.fill(memo, -1); 将数组memo 所有位置的值赋值为-1
  2. Math.max(a,b) 求a,b中最大值
  3. Math.min(a,b) 求a,b中最小值

213. 打家劫舍 II

213. 打家劫舍 II
在这里插入图片描述

思路

首尾房间不能同时被抢,那么只可能有三种不同情况:要么都不被抢;要么第一间房子被抢最后一间不抢;要么最后一间房子被抢第一间不抢。事实上,只要比较情况二和情况三就行了,因为这两种情况对于房子的选择余地比情况一大呀,房子里的钱数都是非负数,能选择的房子多,最优决策结果肯定不会小。

代码

class Solution{
	public int rob(int[] nums) {
    int n = nums.length;
    if (n == 1) return nums[0];
    return Math.max(robRange(nums, 0, n - 2), 
                    robRange(nums, 1, n - 1));
	}
// 仅计算闭区间 [start,end] 的最优结果
	int robRange(int[] nums, int start, int end) {
	    int n = nums.length;
	    int dp_i_1 = 0, dp_i_2 = 0;
	    int dp_i = 0;
	    for (int i = end; i >= start; i--) {
	        dp_i = Math.max(dp_i_1, nums[i] + dp_i_2);
	        dp_i_2 = dp_i_1;
	        dp_i_1 = dp_i;
	    }
	    return dp_i;
	}
}

时空复杂度

1.时间复杂度
O(n),其中 n 是数组长度。需要对数组遍历两次,计算可以偷窃到的最高总金额。
2.空间复杂度
​O(1)

备注

337. 打家劫舍 III

337. 打家劫舍 III
在这里插入图片描述

思路

动态规划。
选择:
返回一个大小为 2 的数组 arr
arr[0] 表示不抢 root 的话,得到的最大钱数
arr[1] 表示抢 root 的话,得到的最大钱数
分别递归左右子树得到结果。

代码

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public int rob(TreeNode root) {
        int[] res = dp(root);
        return Math.max(res[0], res[1]);
    }
    /* 返回一个大小为 2 的数组 arr
    arr[0] 表示不抢 root 的话,得到的最大钱数
    arr[1] 表示抢 root 的话,得到的最大钱数 */
    public int[] dp(TreeNode root) {
        if (root == null) return new int[]{0, 0};
        int[] left = dp(root.left);
        int[] right = dp(root.right);
        // 抢,下家就不能抢了
        int rob = root.val + left[0] + right[0];
        // 不抢,下家可抢可不抢,取决于收益大小
        int not_rob = Math.max(left[0],left[1]) + Math.max(right[0],right[1]);
        return new int[]{not_rob, rob};
    }
}

时空复杂度

1.时间复杂度
O(N)
2.空间复杂度
​空间复杂度只有递归函数堆栈所需的空间 O(N)

备注

  1. int[] res = new int[]{not_rob, rob}; 声明,初始化数组

112. 路径总和

112. 路径总和
在这里插入图片描述

思路

递归:输入一个根节点,返回该根节点到叶子节点是否存在一条和为 targetSum 的路径。每次向下走一个节点,将targetSum减去之前节点的val,直到到达根节点。

代码

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public boolean hasPathSum(TreeNode root, int targetSum) {
        if(root==null) return false;
        if(root.left==null && root.right ==null) {
            return targetSum == root.val;
        }
        return hasPathSum(root.left,targetSum-root.val) || hasPathSum(root.right,targetSum-root.val);
    }
}

时空复杂度

1.时间复杂度
O(N),其中 N 是树的节点数。对每个节点访问一次。
2.空间复杂度
O(H),其中 H是树的高度。空间复杂度主要取决于递归时栈空间的开销,最坏情况下,树呈现链状,空间复杂度为O(N)。平均情况下树的高度与节点数的对数正相关,空间复杂度为O(logN)。

备注

69. x 的平方根

69. x 的平方根
在这里插入图片描述

思路

二分查找,由于 xx平方根的整数部分ans 是满足 k^2 ≤x 的最大k值,因此我们可以对 k进行二分查找,从而得到答案。

代码

class Solution {
    public int mySqrt(int x) {
        int l = 0, r = x, ans = -1;
        while (l <= r) {
            int mid = l + (r - l)/2;
            if ((long) mid * mid <= x) {
                ans = mid;
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        return ans;
    }
}

时空复杂度

1.时间复杂度
O(logx),即为二分查找需要的次数。
2.空间复杂度
​O(1)

备注

64. 最小路径和

64. 最小路径和
在这里插入图片描述

思路

动态规划:在二维矩阵中求最优化问题(最大值或者最小值),肯定需要递归(不断选择不同路径) + 备忘录(解决重叠子问题,防止重复访问)。

代码

class Solution {
    int[][] memo;
    public int minPathSum(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
         // 构造备忘录,初始值全部设为 -1,后续的值更新为到达当前下标的最小路径和
        memo = new int[m][n];
        for(int[] row:memo){
            Arrays.fill(row,-1);
        }
        //从右下角往回反推,到右下角的最小路径和 = min(到上边位置的最小路径和,到左边位置的最小路径和)+ 右下角的值。
        return dp(grid,m-1,n-1);
    }
    //grid数组,当前所在下标
    public int dp(int[][] grid,int x,int y) {
        //base case
        if(x==0&&y==0) return grid[0][0];
        //越界处理:如果索引出界,返回一个很大的值,保证在取 min 的时候不会被取到
        if(x<0||y<0) return Integer.MAX_VALUE;
        // 避免重复计算
        if (memo[x][y] != -1) {
            return memo[x][y];
        }
        // 将计算结果记入备忘录
        memo[x][y] = Math.min(
            dp(grid, x - 1, y),
            dp(grid, x, y - 1)
        ) + grid[x][y];

        return memo[x][y];
    }
}

时空复杂度

1.时间复杂度
O(MN)
2.空间复杂度
O(MN)

备注

  1. 整数最大值 Integer.MAX_VALUE
  2. 整数最小值 Integer.MIN_VALUE
  3. 关于备忘录:
    1)构造备忘录,并初始化默认值
    2)递归过程中,先查询备忘录,有直接返回,没有则将结果记录到备忘录。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值