力扣记录:剑指offer(7)——JZ59-68

JZ59-I 滑动窗口的最大值

  • 使用双端队列(尾入头出)实现单调队列(出队队首为最大值,可自定义类实现),根据窗口长度将数组中数字分别入队出队:入队时如果队尾元素小于当前元素,则弹出队尾元素再将当前元素入队(队尾元素的位置在当前元素之前,一定先出队);出队时如果当前元素等于队首元素,则弹出队首元素,否则不处理(队列中较小值且下标在前的元素已经弹出)
    • 时间复杂度O(n),空间复杂度O(k)
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums.length == 0) return new int[0];
        //使用双端队列(尾入头出)实现单调队列(出队队首为最大值,可自定义类实现)
        Deque<Integer> deque = new LinkedList<>();
        int[] res = new int[nums.length - k + 1];
        int index = 0;
        //初始化
        for(int i = 0; i < k; i++){
            //入队时如果队尾元素小于当前元素,则弹出队尾元素再将当前元素入队
            //(队尾元素的位置在当前元素之前,一定先出队)
            while(!deque.isEmpty() && deque.peekLast() < nums[i]) deque.pollLast();
            deque.offer(nums[i]);
        }
        res[index++] = deque.peek();
        //滑动窗口移动
        for(int i = k; i < nums.length; i++){
            //出队时如果当前元素等于队首元素,则弹出队首元素,否则不处理
            //(队列中较小值且下标在前的元素已经弹出)
            if(nums[i - k] == deque.peek()) deque.poll();
            while(!deque.isEmpty() && deque.peekLast() < nums[i]) deque.pollLast();
            deque.offer(nums[i]);
            res[index++] = deque.peek();
        }
        return res;
    }
}

JZ59-II 队列的最大值

  • 使用两个双端队列(尾入头出)实现单调队列(出队队首为最大值),入队时如果队尾元素小于当前元素,则弹出队尾元素再将当前元素入队(队尾元素的位置在当前元素之前,一定先出队),出队列正常入队;出队时如果出队列不为空,且出队列队首元素等于队列队首元素,则弹队列队首元素(队列中较小值且下标在前的元素已经弹出),出队列正常出队
    • 时间复杂度O(1),空间复杂度O(n)
class MaxQueue {
    Deque<Integer> deque;
    Deque<Integer> dequeOut;
    public MaxQueue() {
        //使用双端队列(尾入头出)实现单调队列(出队队首为最大值)
        deque = new LinkedList<>();
        dequeOut = new LinkedList<>();
    }
    
    public int max_value() {
        if(!deque.isEmpty()) return deque.peek();
        return -1;
    }
    //入队时如果队尾元素小于当前元素,则弹出队尾元素再将当前元素入队
    //(队尾元素的位置在当前元素之前,一定先出队)
    //出队列正常入队
    public void push_back(int value) {
        while(!deque.isEmpty() && deque.peekLast() < value) deque.pollLast();
        deque.offer(value);
        dequeOut.offer(value);
    }
    //出队时如果出队列不为空,且出队列队首元素等于队列队首元素,则弹队列队首元素
    //出队列正常出队
    public int pop_front() {
        if(!dequeOut.isEmpty()){
            int out = dequeOut.peek();  //两个Integer不能直接用==比较,需要手动拆箱
            if(out == deque.peek()) deque.poll();
            return dequeOut.poll();
        }
        return -1;
    }
}

JZ60 n个骰子的点数

  • 动态规划,定义二维dp数组表示投掷完i个骰子后,点数j的出现次数;投掷完第i个骰子后,点数j的出现次数可以由投掷完i-1个骰子后点数j-1,j-2,……,j-6推出(第i个骰子可能为1-6);注意点数应该 > 0,可以使用一维数组优化
    • 时间复杂度O(n^2),空间复杂度O(n)
class Solution {
    public double[] dicesProbability(int n) {
        //动态规划
        //定义二维dp数组表示投掷完i个骰子后,点数j的出现次数为dp[i][j]
        int[][] dp = new int[n + 1][n * 6 + 1];
        //初始化
        for(int i = 1; i <= 6; i++){
            dp[1][i] = 1;
        }
        //投掷完第i个骰子后,点数j的出现次数可以由投掷完i-1个骰子后点数j-1,j-2,……,j-6推出
        //(第i个骰子可能为1-6)
        for(int i = 2; i <= n; i++){
            for(int j = i; j <= 6 * i; j++){
                for(int k = 1; k <= 6; k++){
                    if(j <= k) break;   //注意点数应该 > 0
                    dp[i][j] += dp[i - 1][j - k];
                }
            }
        }
        double[] res = new double[5 * n + 1];
        int index = 0;
        double sum = Math.pow(6, n);
        for(int i = n; i <= 6 * n; i++){
            res[index++] = dp[n][i] / sum;
        }
        return res;
    }
}
//一维数组优化
class Solution {
    public double[] dicesProbability(int n) {
        //动态规划
        //定义dp数组表示投掷完i个骰子后,点数j的出现次数为dp[j]
        int[] dp = new int[n * 6 + 1];
        //初始化
        for(int i = 1; i <= 6; i++){
            dp[i] = 1;
        }
        //投掷完第i个骰子后,点数j的出现次数可以由投掷完i-1个骰子后点数j-1,j-2,……,j-6推出
        //(第i个骰子可能为1-6)
        for(int i = 2; i <= n; i++){
            for(int j = 6 * i; j >= i; j--){
                dp[j] = 0;  //需要进行初始化,防止多出上一次的和
                for(int k = 1; k <= 6; k++){
                    if(j - k < i - 1) break;   //注意点数应该 > 0,且不能超过上一次和的下限
                    dp[j] += dp[j - k];
                }
            }
        }
        double[] res = new double[5 * n + 1];
        int index = 0;
        double sum = Math.pow(6, n);
        for(int i = n; i <= 6 * n; i++){
            res[index++] = dp[i] / sum;
        }
        return res;
    }
}

JZ61 扑克牌中的顺子

  • 先将数组排序,从头开始遍历,记录0的数量,如果前后相差大于1则用0去填补,最后通过0的数量判断是否顺子,注意:当出现两张一样的不为0的数字时必不为顺子
    • 时间复杂度O(nlogn),空间复杂度O(1)
class Solution {
    public boolean isStraight(int[] nums) {
        //先将数组排序,从头开始遍历,记录0的数量
        Arrays.sort(nums);
        int count = 0;
        for(int i = 0; i < 5; i++){
            if(nums[i] == 0){
                count++;
            }else{
                if(i > 0 && nums[i - 1] != 0){
                    //当出现两张一样的不为0的数字时必不为顺子
                    if(nums[i] == nums[i - 1]) return false;
                    //如果前后相差大于1则用0去填补
                    if(nums[i] - nums[i - 1] > 1){
                        count -= nums[i] - nums[i - 1] - 1;
                    }
                }
            }
        }
        //最后通过0的数量判断是否顺子
        return count >= 0;
    }
}
  • 使用set,遍历数组,将除了0的数字加入set中(加入前先判断是否重复),记录最大和最小值,若差值小于5则为顺子
    • 时间复杂度O(n),空间复杂度O(1)
class Solution {
    public boolean isStraight(int[] nums) {
        //使用set
        Set<Integer> set = new HashSet<>();
        //记录最大和最小值,若差值小于5则为顺子
        int max = 0;
        int min = 14;
        //遍历数组,将除了0的数字加入set中
        for(int num : nums){
            if(num == 0) continue;
            if(set.contains(num)) return false; //加入前先判断是否重复
            set.add(num);
            if(num > max) max = num;
            if(num < min) min = num;
        }
        return max - min < 5;
    }
}

JZ62 圆圈中最后剩下的数字

  • 反推解决约瑟夫环问题,找规律:首先正推,数组第一位(每次删除后的下一位)的下标变化规律为+m后对当时数组长度取模,直到数组剩最后一位(下标为0);反推得到数组最后下标为0在开始时的下标,从0开始每次+3后对当时数组长度(从2开始)取模,直到回到最开始的数组
    • 时间复杂度O(n),空间复杂度O(1)
class Solution {
    public int lastRemaining(int n, int m) {
        //定义最后留下的数下标
        int k = 0;
        //反推
        for(int i = 2; i <= n; i++){
            k = (k + m) % i;
        }
        return k;
    }
}

JZ63 股票的最大利润

  • 动态规划,定义dp数组表示第i天卖出股票(在0到i-1天买入)的最大利润为dp[i]
    • 时间复杂度O(n),空间复杂度O(n)
class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length == 0) return 0;
        //动态规划
        int[] dp = new int[prices.length];
        int max = 0;
        //初始化
        dp[0] = 0;
        for(int i = 1; i < prices.length; i++){
            dp[i] = Math.max(dp[i - 1] + prices[i] - prices[i - 1], 0);
            max = Math.max(max, dp[i]);
        }
        return max;
    }
}
  • 贪心,收集最小低的买入价和最高的利润
    • 时间复杂度O(n),空间复杂度O(1)
class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length == 0) return 0;
        //贪心
        int low = prices[0];
        int profit = 0;
        for(int i = 1; i < prices.length; i++){
            low = Math.min(low, prices[i]);
            profit = Math.max(profit, prices[i] - low);
        }
        return profit;
    }
}

JZ64 求1+2+…+n

  • 递归,不适用if,使用与或非终止递归(n = 1时)
    • 时间复杂度O(n),空间复杂度O(n)
class Solution {
    int res = 0;
    public int sumNums(int n) {
        //递归
        boolean flag = n > 0 && sumNums(n - 1) > 0;
        res += n;
        return res;
    }
    //简化
    public int sumNums(int n) {
        //递归
        boolean flag = n > 0 && (n += sumNums(n - 1)) > 0;
        return n;
    }
}

JZ65 不用加减乘除做加法

  • 使用异或运算(无进位)和与运算(有进位),注意计算进位时左移一位(进的是下一位),和为无进位+进位(再次循环)
    • 时间复杂度O(1),空间复杂度O(1)
class Solution {
    public int add(int a, int b) {
        //使用异或运算(无进位)和与运算(有进位)
        while(b != 0){
            int c = (a & b) << 1; //计算进位左移一位(进的是下一位),与运算优先级较低
            a ^= b;  //计算非进位
            b = c;  //当进位不为0时循环计算a+b(非进位+进位)
        }
        return a;
    }
}

JZ66 构建乘积数组

  • 定义两个dp数组,分别存储i左边和i右边的乘积,可以不用dp数组直接遍历计算存储结果,优化空间
    • 时间复杂度O(n),空间复杂度O(n),优化为O(1)
class Solution {
    public int[] constructArr(int[] a) {
        int leng = a.length;
        if(leng == 0) return new int[0];
        //定义两个dp数组表示数组a下标i(不包括i)之前、之后的乘积分别为dpL[i]、dpR[i]
        int[] dpL = new int[leng];
        int[] dpR = new int[leng];
        //初始化
        dpL[0] = 1;
        dpR[leng - 1] = 1;
        for(int i = 1; i < leng; i++){
            dpL[i] = dpL[i - 1] * a[i - 1];
        }
        for(int j = leng - 2; j >= 0; j--){
            dpR[j] = dpR[j + 1] * a[j + 1];
        }
        //左右相乘得到结果
        int[] b = new int[leng];
        for(int i = 0; i < leng; i++){
            b[i] = dpL[i] * dpR[i];
        }
        return b;
    }
    //优化
    public int[] constructArr(int[] a) {
        int leng = a.length;
        if(leng == 0) return new int[0];
        //空间优化
        int[] b = new int[leng];
        b[0] = 1;
        //计算i左边乘积
        for(int i = 1; i < leng; i++){
            b[i] = b[i - 1] * a[i - 1];
        }
        int temp = 1; //计算右边乘积直接乘到b[i]上
        //计算i右边乘积
        for(int j = leng - 2; j >= 0; j--){
            temp *= a[j + 1];   //右边乘积
            b[j] *= temp;   //直接乘到b[i]
        }
        return b;
    }
}

JZ67 把字符串转换成整数

  • 直接遍历,如果第一位为非符号非字符直接返回0,如果第一位为符号位则存储符号位(方便后面返回),存储数字拼接的结果(每遍历一位结果乘10),注意:每次拼接前进行判断是否越界。也可以使用状态机(相似题目:JZ20、JZ56-II)
    • 时间复杂度O(n),空间复杂度O(n),不使用trim()则为O(1)
class Solution {
    public int strToInt(String str) {
        //直接遍历
        //使用trim()去除首尾空格
        str = str.trim();
        if(str.length() == 0) return 0;
        char[] strArr = str.toCharArray();  //转为字符数组
        int border = Integer.MAX_VALUE / 10;    //防止越界
        int i = 1;
        int flag = 1;   //符号位,+为1,-为-1
        if(strArr[0] == '-'){
            flag = -1;
        }else if(strArr[0] != '+'){
            i = 0;  //无符号位从0开始
        }
        int res = 0;
        for(int j = i; j < strArr.length; j++){
            if(strArr[j] < '0' || strArr[j] > '9') break;   //到非字符时终止
            //判断是否越界
            if(res > border || (res == border && strArr[j] > '7')) return flag == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
            res = res * 10 + (strArr[j] - '0'); //拼接
        }
        return flag * res;
    }
}
//不适用trim(),不转为字符数组,直接在原字符串上遍历
class Solution {
    public int strToInt(String str) {
        //直接遍历
        int border = Integer.MAX_VALUE / 10;    //防止越界
        int i = 0;
        //手动跳过空格
        while(i < str.length()){
            if(str.charAt(i) != ' ') break;
            i++;
        }
        if(i == str.length()) return 0; //字符串为空
        int flag = 1;   //符号位,+为1,-为-1
        if(str.charAt(i) == '-'){
            flag = -1;
            i++;
        }else if(str.charAt(i) == '+'){
            i++;  //无符号位从i + 1开始
        }
        int res = 0;
        for(int j = i; j < str.length(); j++){
            if(str.charAt(j) < '0' || str.charAt(j) > '9') break;   //到非字符时终止
            //判断是否越界
            if(res > border || (res == border && str.charAt(j) > '7')) return flag == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
            res = res * 10 + (str.charAt(j) - '0'); //拼接
        }
        return flag * res;
    }
}

JZ68-I 二叉搜索树的最近公共祖先

  • 后序遍历,从底向上递归回溯,如果当前节点为要查找的节点或为空则直接返回当前节点,如果递归左(右)子树返回节点不为空,说明有一个目标节点在左(右)子树:若当前节点左右子树均不为空,则返回当前节点(当前节点为最近公共祖先),若当前节点只有一个子树不为空,则返回该子树,否则返回空。
    • 时间复杂度O(n),空间复杂度O(n)
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        //从底向上递归回溯
        //如果当前节点为要查找的节点或为空则直接返回当前节点
        if(root == p || root == q || root == null) return root;
        //后序遍历
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        //如果递归左(右)子树返回节点不为空,说明有一个目标节点在左(右)子树
        //若当前节点左右子树均不为空,则返回当前节点(当前节点为最近公共祖先)
        if(left != null && right != null){
            return root;
        }else if(left != null && right == null){//若当前节点只有一个子树不为空,则返回该子树
            return left;
        }else if(left == null && right != null){
            return right;
        }
        //否则返回空
        return null;
    }
}

JZ68-II 二叉树的最近公共祖先

  • 迭代,对于二叉搜索树(中序遍历为递增序列,左小右大),从上往下遍历,只要遍历时发现某个节点值在目标节点值之间,当前节点即为最近公共祖先
    • 时间复杂度O(n),空间复杂度O(1)(递归为O(n))
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        //迭代,对于二叉搜索树(中序遍历为递增序列,左小右大)
        //从上往下遍历,只要遍历时发现某个节点值在目标节点值之间,当前节点即为最近公共祖先
        while(root != null){
            if(root.val > p.val && root.val > q.val){
                root = root.left;
            }else if(root.val < p.val && root.val < q.val){
                root = root.right;
            }else{
                return root;
            }
        }
        return null;    //没找到
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值