剑指offerⅡ(Java 持续更新...)

目录

DAY6

10-Ⅰ:斐波那契数列

10-Ⅱ:青蛙跳台阶

63:股票的最大利润

DAY7

42:连续子数组的最大和(有点转不过来)

47:礼物的最大价值

18:删除链表的节点

22:列表中倒数第k个节点

25:合并两个排序的链表

DAY8

21:调整数组顺序使奇数位于偶数前面

52:两个链表的第一个公共节点(好妙 浪漫相遇)

57:和为s的两个数字

DAY9

58-Ⅰ:反转单词顺序

12:矩阵中的路径(这题好难)

13:机器人的运动范围

DAY10

34:二叉树中和为某一值的路径

36:二叉搜索树与双向链表

54:二叉搜索树的第k大节点


DAY6

10-Ⅰ:斐波那契数列

思路:经典动态规划。第一个数为0,第二个数为1,动态记录遍历过数据的和。

class Solution {
    public int fib(int n) {
        int a = 0, b = 1, sum;
        for(int i = 0; i < n; i++){
            sum = (a + b) % 1000000007;
            a = b;
            b = sum;
        }
        return a;
    }
}

10-Ⅱ:青蛙跳台阶

思路:同上一题。最后一次跳只有两种办法

  • 跨一阶:所以次数就是上n-1阶的方法数
  • 跨两阶:次数就是上n-2阶的方法数

f(n) = f(n-1)+f(n-2)

class Solution {
    public int numWays(int n) {
        if(n == 0 || n == 1) return 1;
        int a = 0, b = 0, f = 1;
        for(int i = 0; i < n; i++){
            a = b;
            b = f;
            f = (a + b) % 1000000007;
        }
        return f;
    }
}

63:股票的最大利润

思路1:动态规划。记录历史最低价格,当前价格减去历史最低价格记录当前最大利润。

class Solution {
    public int maxProfit(int[] prices) {
        int minprice = Integer.MAX_VALUE;
        int maxprofit = 0;
        for(int i = 0; i < prices.length; i++){
            if(prices[i] < minprice) minprice = prices[i];
            else if(prices[i] - minprice > maxprofit) maxprofit = prices[i] - minprice;
        }
        return maxprofit;
    }
}

DAY7

42:连续子数组的最大和(有点转不过来)

思路:动态规划。用nums[i]直接存储当前最大和。

  • 当nums[i-1] > 0时,是正反馈,所以直接修改nums[i] += nums[i-1] 
  • 当nums[i-1] < 0时,是负反馈,选择不加。

res存放全局最大

class Solution {
    public int maxSubArray(int[] nums) {
        int res = nums[0];
        for(int i = 1; i < nums.length; i++){
            nums[i] += Math.max(nums[i - 1], 0);
            res = Math.max(res, nums[i]);
        }
        return res;
    }
}

47:礼物的最大价值

思路:动态规划。类同上一题(但是感觉这个比上个好理解)。

  • 第一行的只能是左边跳来的
  • 第一列的只能是上边跳来的
  • 非第一行第一列的可能是从上也可能是从左跳来的
class Solution {
    public int maxValue(int[][] grid) {
        int row = grid.length, col = grid[0].length;
        for(int i = 0; i < row; i++){
            for(int j = 0; j < col; j++){
                if(i == 0 && j == 0) continue;
                if(i == 0) grid[i][j] += grid[i][j-1];
                else if(j == 0) grid[i][j] += grid[i-1][j];
                else grid[i][j] += Math.max(grid[i][j-1], grid[i-1][j]);
            }
        }
        return grid[row - 1][col - 1];
    }
}

动态规划好像都是倒着思考的。

18:删除链表的节点

思路:双指针。

  • 若删除的是头节点,则直接输出head.next
  • 若当前节点cur不是null且不是删除目标则进行遍历
  • 若cur为删除目标且cur不为null则进行删除操作,最后输出head即可
class Solution {
    public ListNode deleteNode(ListNode head, int val) {
        if(head.val == val) return head.next;
        ListNode pre = head, cur = head.next;
        while(cur != null && cur.val != val){
            pre = cur;
            cur = cur.next;
        }
        if(cur != null) pre.next = cur.next;
        return head;
    }
}

22:列表中倒数第k个节点

思路:双指针。初始时start和end都指向head,然后将end向后移动k个位置,此时start和end相差k个节点。然后同时移动start和end,当end移动到null时start正好处于倒数第k个节点,输出即可。

太妙了我为啥就想不到啊!

class Solution {
    public ListNode getKthFromEnd(ListNode head, int k) {
        ListNode start = head, end = head;
        for(int i = 0; i < k ; i++){
            end = end.next;
        }
        while(end != null){
            end = end.next;
            start = start.next;
        }
        return start;
    }
}

25:合并两个排序的链表

思路:两个待合并的链表都是升序排列。创建一个新的链表,将0作为伪头节点,开始遍历比较l1和l2值的大小,选择小的添加到新创建的链表里,循环结束条件为其中一个链表空了。此时将未空链表剩下的节点直接接在新链表的尾部即可。

难点(个人认为):让指针动起来,这个是我的弱项,思路都懂但是写的时候总是不知道怎么写才能让指针遍历下去o(╥﹏╥)o。

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode head = new ListNode(0), cur = head;
        while(l1 != null && l2 != null){
            if(l1.val <= l2.val){
                cur.next = l1;
                l1 = l1.next;
            }
            else{
                cur.next = l2;
                l2 = l2.next;
            }
            cur = cur.next; //让指针动起来!!
        }
        cur.next = l1 != null ? l1 : l2;  //若是l1空了则cur.next等于l2,反之等于l1
        return head.next; //此时head为0,所以从head.next开始输出
    }
}

DAY8

21:调整数组顺序使奇数位于偶数前面

思路:双指针。两个指针分别指向数组的头和尾

  • 左指针若遇到奇数则向右移一个
  • 右指针若遇到偶数则向左移一个
  • 左指针遇到偶数或右指针遇到奇数停下等待对方遇到不属于自己的元素,然后进行交换。

二进制的末位为 0表示偶数,末位为 1表是奇数。用&1比%2快很多

class Solution {
    public int[] exchange(int[] nums) {
        int l = 0, r = nums.length - 1, temp;
        while(l < r){
            while(nums[l] % 2 == 1 && l < r) l++; //l < r && (nums[l] & 1) == 1
            while(nums[r] % 2 == 0 && l < r) r--; //l < r && (nums[l] & 1) == 0
            temp = nums[l];
            nums[l] = nums[r];
            nums[r] = temp;
        }
        return nums;
    }
}

52:两个链表的第一个公共节点(好妙 浪漫相遇)

思路:双指针,设公共尾部节点个数为c

  • 指针A遍历完headA再开始遍历headB,走到公共节点一共走了a+(b-c)
  • 指针B遍历完headB再遍历headB,走到公共节点一共b+(a-c)
  • 所以若有(c>0)公共节点A,B同时指向的节点即为公共的,若无(c=0)公共节点则同时指向null
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode A = headA, B = headB;
        while(A != B){
            A = A != null ? A.next : headB;
            B = B != null ? B.next : headA;
        }
        return A;
    }
}

57:和为s的两个数字

思路:双指针,一头一尾,数组内为升序排列。

  • 当sum大于target则需要减小加数,所以有指针向左移动
  • 反之左指针向右移动以增大sum
class Solution {
    public int[] twoSum(int[] nums, int target) {
        int l = 0, r = nums.length - 1;
        while(l != r){
            int sum = nums[l] + nums[r];
            if(sum > target) r--;
            else if(sum < target) l++;
            else return new int[]{nums[l], nums[r]};
        }
        return new int[]{0,0};
    }
}

DAY9

58-Ⅰ:反转单词顺序

思路:双指针,初始时双指针均处于字符串尾端。

  • 指针i开始遍历,遇到空格跳出小循环,即一个单词遍历结束,将单词添加至新的字符串里。
  • 然后开始遍历空格,当遇到字符时跳出小循环2,将指针j移动到当前i的位置,再开始遍历锁定下一个单词。重复小循环1。
  • 要删除首尾空格,trim()函数不删除字符串中间的空格
  • substring(index,index)里的索引,含头不含尾。
  • charAt(index):返回指定索引的字符
class Solution {
    public String reverseWords(String s) {
        s = s.trim(); //删除首尾空格
        int j = s.length() - 1, i = j;
        StringBuilder res = new StringBuilder();
        while(i >= 0){
            while(i >= 0 && s.charAt(i) != ' ') i--;
            res.append(s.substring(i + 1, j + 1) + " "); //含头不含尾
            while(i >= 0 && s.charAt(i) == ' ') i--;
            j = i;
        }
        return res.toString().trim();
    }
}

12:矩阵中的路径(这题好难)

思路:DFS+剪枝

递归终止条件:

  • 返回false:1)越界;2)当前矩阵元素与目标不匹配;3)该元素已访问过(因为将访问过的元素修改为"\0"所以该条件与2合并)
  • 返回true:字符串全部匹配完毕,即k = word.length - 1

递推过程:

  • 标记当前字符防止后续重复搜索
  • 搜索下一单元格,按照下、上、右、左的顺序开启下层递归,只找到一条路径即可,所以  || 连接,结果记录到res
  • 还原当前矩阵元素(修改元素是为了进行递归的时候防止重复搜索到)
class Solution {
    public boolean exist(char[][] board, String word) {
        char[] words = word.toCharArray();
        for(int i = 0; i < board.length; i++){
            for(int j = 0; j < board[0].length; j++){
                if(dfs(board, words, i, j, 0)) return true;
            }
        }
        return false;
    }
    boolean dfs(char[][] board, char[] word, int i, int j, int k){
        if(i >= board.length || i < 0 || j >= board[0].length 
           || j < 0 || board[i][j] != word[k]) return false;
        if(k == word.length - 1) return true;
        board[i][j] = '\0'; //标记已遍历过的元素防止重复遍历
        //判断顺序:下 上 右 左
        boolean res = dfs(board, word, i + 1, j, k + 1) || dfs(board, word, i - 1, j, k + 1) 
        || dfs(board, word, i, j + 1, k + 1) || dfs(board, word, i, j - 1, k + 1);
        board[i][j] = word[k];
        return res;
    }
}

13:机器人的运动范围

思路:递归,类同上一题,copy了一个大佬的代码

  • 标记访问过的元素
  • 越界和数位和大于k时机器人不可到达
  • 向右,下递归搜索
  • 返回值为从本单元格递归搜索的可达解总数(返回值要+1)
class Solution {
    public int movingCount(int m, int n, int k) {
        //标记当前格子是否被访问过
        boolean[][] visited = new boolean[m][n];
        return dfs(visited, m, n, k, 0, 0);
    }
    private int dfs(boolean[][] visited, int m, int n, int k, int i, int j){
        if(i >= m || j >= n || sums(i) + sums(j) > k || visited[i][j]) return 0;
        visited[i][j] = true;
        return 1 + dfs(visited, m, n, k, i + 1, j) + dfs(visited, m, n, k, i, j +1);
    }
    private int sums(int x){
        int s = 0;
        while(x != 0){
            s += x % 10;
            x = x / 10;
        }
        return s;
    }

}

DAY10

34:二叉树中和为某一值的路径

思路:递归,先序遍历。

  • 列表path存储先序遍历的当前路径,判断该路径和是否等于target,是的话存入res,不是的话清空
  • 每遍历一个节点就用target-val,当到了叶结点刚好target等于0则说明该路径节点值和为target
class Solution {
    LinkedList<List<Integer>> res = new LinkedList<>();
    LinkedList<Integer> path = new LinkedList<Integer>();
    public List<List<Integer>> pathSum(TreeNode root, int target) {
        recur(root, target);
        return res;
    }

    void recur (TreeNode root, int target){
        if(root == null) return; 
        path.add(root.val);
        target -= root.val;
        if(target == 0 && root.left == null && root.right == null) //叶结点
        res.add(new LinkedList(path));
        recur(root.left, target);
        recur(root.right, target);
        path.removeLast();
    }
}

36:二叉搜索树与双向链表

思路:递归,中序遍历。

  • pre为前驱结点,双向链表即cur.left = pre,pre.right = cur,循环链表即head.left = tail,tail.right = head。
  • 递归左子树
  • 当pre为空时记为head;当pre不为空时,修改成双向节点;更新
  • 递归右子树
class Solution {
    Node pre, head;
    public Node treeToDoublyList(Node root) {
        if(root == null) return null;
        dfs(root); //中序遍历,构建双向链表
        //连接头尾构建循环链表
        head.left = pre;  
        pre.right = head;
        return head;
    }

    void dfs(Node cur){
        if(cur == null) return;
        dfs(cur.left);
        if(pre != null) pre.right = cur;
        else head = cur;  //pre=null时cur是头节点 所以将head指向cur
        cur.left = pre;
        pre = cur;
        dfs(cur.right);
    }
}

54:二叉搜索树的第k大节点

思路:递归,倒序中序遍历(因为是二叉树是递增的)。

  • 遍历右子树->根结点->左子树,得到的就是从大到小的排列
  • 遍历一个节点计数器count加1,当count=k时即为目标节点
class Solution {
    int target;
    int count = 0;
    public int kthLargest(TreeNode root, int k) {
        dfs(root, k);
        return target;
    }

    void dfs(TreeNode root, int k){
        if(root == null) return;
        dfs(root.right, k);
        count++;
        if(k == count) target = root.val;
        dfs(root.left, k);
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值