Leetcode剑指offer刷题笔记20210421

方法

  • 滑动窗口
  • 动态规划
  • 贪心算法
  • 二叉树
  • 深度遍历BFS
  • 广度优先DFS
  • 位运算
  • 递归
  • 回溯
  • 数组
  • 链表
  • 字符串
  • 其他

  1. 用两个栈实现队列 E
    用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )

一个栈A用来模拟添加元素到队列尾,另一个B存A中元素倒叙,用来取出队列头

class CQueue {

    LinkedList<Integer> A, B;
    public CQueue() {
        A = new LinkedList<>();//A用来加入数据
        B = new LinkedList<>();//B用于存放A中倒叙元素,取出队列头
    }
    
    public void appendTail(int value) {
        A.addLast(value);
    }
    
    public int deleteHead() {
        if(!B.isEmpty()) return B.removeLast();
        if(A.isEmpty()) return -1;
        else {
            while(!A.isEmpty()){
                B.addLast(A.removeLast());
            }
            return B.removeLast();
        }
    }
}

/**
 * Your CQueue object will be instantiated and called as such:
 * CQueue obj = new CQueue();
 * obj.appendTail(value);
 * int param_2 = obj.deleteHead();
 */
  1. 包含min函数的栈
    定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
class MinStack {

    /** initialize your data structure here. */
    Stack<Integer> A, B;
    public MinStack() {
        A = new Stack<>();
        B = new Stack<>();
    }
    
    public void push(int x) {
        A.add(x);
        if(B.isEmpty() || x <= B.peek()){//用<=防止重复的最小值只记录了一次
            B.add(x);
        }
    }
    
    public void pop() {
        if(A.pop().equals(B.peek())){
            B.pop();
        }
    }
    
    public int top() {
        return A.peek();
    }
    
    public int min() {
        return B.peek();
    }
}

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack obj = new MinStack();
 * obj.push(x);
 * obj.pop();
 * int param_3 = obj.top();
 * int param_4 = obj.min();
 */

59 - II. 队列的最大值 M
请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。
若队列为空,pop_front 和 max_value 需要返回 -1

class MaxQueue {

    Queue<Integer> queue;//队列
    Deque<Integer> deque;//双端队列
    public MaxQueue() {
        queue = new LinkedList<>();
        deque = new LinkedList<>();
    }
    
    public int max_value() {
        if(!deque.isEmpty()){
            return deque.peekFirst();
        }
        return -1;
    }
    
    public void push_back(int value) {
        queue.offer(value);
        while(!deque.isEmpty() && deque.peekLast() < value){
            deque.pollLast();
        }
        deque.offerLast(value);
    }
    
    public int pop_front() {
        if(queue.isEmpty()) return -1;
        if(queue.peek().equals(deque.peekFirst())){
            deque.pollFirst();
        }
        return queue.poll();
    }
}

/**
 * Your MaxQueue object will be instantiated and called as such:
 * MaxQueue obj = new MaxQueue();
 * int param_1 = obj.max_value();
 * obj.push_back(value);
 * int param_3 = obj.pop_front();
 */
  1. 从尾到头打印链表 E
    输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public int[] reversePrint(ListNode head) {
        Stack<Integer> stack = new Stack<>();
        while(head != null){
            stack.push(head.val);
            head = head.next;
        }
        int res[] = new int[stack.size()];
        for(int i = 0; i < res.length; i++){
            res[i] = stack.pop();
        }
        return res;
    }
}

  1. 数据流中的中位数 H
    如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

例如,

[2,3,4] 的中位数是 3

[2,3] 的中位数是 (2 + 3) / 2 = 2.5

Leetcode剑指offer刷题笔记20210421_第1张图片

class MedianFinder {

    Queue<Integer> A,B;
    /** initialize your data structure here. */
    public MedianFinder() {
        A = new PriorityQueue<>(); // 小顶堆,保存较大的一半,堆底->顶,小->大,优先级队列会自动排序
        B = new PriorityQueue<>((x, y) -> (y - x)); // 大顶堆,保存较小的一半,堆底->顶,大->小
    }
    
    public void addNum(int num) {//AB中数目不等,向B添加,方式:先加入A,再将A顶加入B
        if(A.size() != B.size()){
            A.add(num);
            B.add(A.poll());
        }else{
            B.add(num);
            A.add(B.poll());
        }
    }
    
    //A中放的元素多,B中少,若AB中数目不等则中位数为A堆顶元素,相等则为AB堆顶和的1/2
    public double findMedian() {
        return A.size() == B.size() ? (A.peek()+B.peek())/2.0 : A.peek();
    }
}

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder obj = new MedianFinder();
 * obj.addNum(num);
 * double param_2 = obj.findMedian();
 */

滑动窗口

滑动窗口的重要性质是:窗口的左边界和右边界永远只能向右移动,而不能向左移动。这是为了保证滑动窗口的时间复杂度是 O(n)。如果左右边界向左移动的话,这叫做“回溯”,算法的时间复杂度就可能不止 O(n)。

  • 57.和为s的连续正数序列:输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。

序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

示例 1:

输入:target = 9
输出:[[2,3,4],[4,5]]
Leetcode剑指offer刷题笔记20210421_第2张图片

class Solution {
    public int[][] findContinuousSequence(int target) {
        int i = 1, j = 2, sum = 3;
        List<int[]> res = new ArrayList<>();

        while(i < j){
            if(sum == target){
                int[] arr = new int[j-i+1];
                for(int k = i; k <= j; k++){//i从1开始,既是数组下标,又是数组内容
                    arr[k-i] = k;
                }
                res.add(arr);
            }
            if(sum > target){//左边界右移,先减再移动
                    sum -= i;
                    i++;
            }else{//右边界右移,先移再加
                j++;
                sum += j;
            }    
        }
        return res.toArray(new int[0][]);
    }
}
  1. 最长不含重复字符的子字符串 M
    请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

输入: “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。

Leetcode剑指offer刷题笔记20210421_第3张图片

class Solution {
    public int lengthOfLongestSubstring(String s) {
        int l = 0;//滑动窗口的左边界
        int max = 0;//记录最大长度
        for(int i = 0; i < s.length(); i++){//外循环i遍历字符串
            for(int j = l; j < i; j++){//内循环j遍历滑动窗口
                if(s.charAt(i) == s.charAt(j)){
                    l = j + 1;
                    break;
                }
            }
            max = Math.max(max, i-l+1);
        }
        return max;
    }
}

动态规划

  • 49.丑数
    我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。

示例:

输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。

下一个丑数是通过上一个丑数x2或x3或x5得到,所以用动态规划
Leetcode剑指offer刷题笔记20210421_第4张图片

class Solution {
    public int nthUglyNumber(int n) {
        int a = 0, b = 0, c = 0;
        int[] arr = new int[n];//需要的结果是第n-1个,所以要全部记录下来
        arr[0] = 1;
        for(int i = 1; i < n; i++){
            int n1 = arr[a]*2, n2 = arr[b]*3, n3 = arr[c]*5;//下一个丑数必定是上一个丑数乘2、3、5后
            arr[i] = Math.min(Math.min(n1,n2),n3);//其中最小的一个
            if(arr[i] == n1) a++;//若下一个是通过上一个乘2得到,则a索引向后走一步
            if(arr[i] == n2) b++;//若下一个数=乘2或乘3,则a、b都要向后走一步
            if(arr[i] == n3) c++;
        }
        return arr[n-1];
    }
}
  • 62.圆圈中最后剩下的数字
    0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

示例 1:

输入: n = 5, m = 3
输出: 3

约瑟夫环问题,关键公式:dp[i]=(dp[i−1]+m)%i
Leetcode剑指offer刷题笔记20210421_第5张图片

class Solution {
    public int lastRemaining(int n, int m) {
        int ans = 0;//一个数字的话,结果必然是下标0
        for(int i = 2; i <= n; i++){//从两个数字开始
            ans = (ans + m) % i;
        }
        return ans;
    }
}
  • 46.把数字翻译成字符串
    给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

示例 1:
输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", “bwfi”, “bczi”, “mcfi"和"mzi”
Leetcode剑指offer刷题笔记20210421_第6张图片
规律:0个数和1个数时,都是1种方法,
当有2个数字以上:1.若最后两数字组合能翻译,则dp[i] = dp[i-1] +dp[i-2]
2.否则dp[i] = dp[i-1],就是说只加一个数字和不加方法数量一样

class Solution {
    public int translateNum(int num) {
        String s = String.valueOf(num);//转换成字符串才好切片
        int b = 1, a = 1;//b是0个数的总方法,a是1个数的总方法,都是1种
        for(int i = 2; i <= s.length(); i++){//从2个数开始
            String tmp = s.substring(i-2,i);//subString取值范围,左闭右开
            int c = tmp.compareTo("10")>=0 && tmp.compareTo("25")<=0 ? a+b : a;
            //compareTo方法,将tmp和“10”、“25”的ASCII码值相减           
            b = a;
            a = c;
        }
        return a;
    }
}
    1. 正则表达式匹配 H
      请实现一个函数用来匹配包含’. ‘和’‘的正则表达式。模式中的字符’.‘表示任意一个字符,而’'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但与"aa.a"和"ab*a"均不匹配。

f[i][j] 代表 A 的前 i个和 B 的前 j个能否匹配

对于前面两个情况,可以合并成一种情况 f[i][j] = f[i-1][j-1]f[i][j]=f[i−1][j−1]

对于第三种情况,对于 c∗ 分为看和不看两种情况

不看:直接砍掉正则串的后面两个, f[i][j] = f[i][j-2]f[i][j]=f[i][j−2]
看:正则串不动,主串前移一个,f[i][j] = f[i-1][j]f[i][j]=f[i−1][j]

class Solution {
    public boolean isMatch(String s, String p) {
        int m = s.length();
        int n = p.length();
        boolean[][] res = new boolean[m+1][n+1];
        for(int i = 0; i <= m; i++){
            for(int j = 0; j <= n; j++){
                if(j == 0){
                    res[i][j] = (i == 0);//若p为空,则根据s是否为空决定
                }
                else{
                    if(p.charAt(j-1) != '*'){//若p的最后一个不是*,只能比较该位是否匹配,或该位是.
                        if(i > 0 && (s.charAt(i-1) == p.charAt(j-1) || p.charAt(j -1) == '.')){
                            res[i][j] = res[i-1][j-1];
                        }
                    }else{//若p的最后一个是*
                        if(j >= 2){
                            res[i][j] = res[i][j-2];//判断能否直接把p的最后两位舍去
                        }
                        if(i >= 1 && j >= 2 && (s.charAt(i-1) == p.charAt(j-2) || p.charAt(j-2) == '.')){
                            res[i][j] |= res[i-1][j];//若不能舍去且s最后一位能匹配上,则s向前移动一位继续匹配
                        }
                    }
                }
            }
        }
        return res[m][n];
    }
}
  1. 连续子数组的最大和 E
    输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
    要求时间复杂度为O(n)。
    输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
    输出: 6
    解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
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(nums[i], res);
        }
        return res;
    }
}
  1. 礼物的最大价值 M
    在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
    Leetcode剑指offer刷题笔记20210421_第7张图片
    Leetcode剑指offer刷题笔记20210421_第8张图片
class Solution {
    public int maxValue(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                if(i == 0 && j == 0) continue;
                if(i == 0 && j != 0){//在第一行,后面只可能从左边加
                    grid[i][j] += grid[i][j-1];
                }
                if(i != 0 && j == 0){//在第一行,后面只可能从上边加
                    grid[i][j] += grid[i-1][j];
                }
                if(i != 0 && j != 0){
                    grid[i][j] += Math.max(grid[i-1][j], grid[i][j-1]);
                }
            }
        }
        return grid[m-1][n-1];
    }
}
  1. 股票的最大利润 M
    假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
    输入: [7,1,5,3,6,4]
    输出: 5
    解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
    注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

动态规划最重要的就是找到转移方程
Leetcode剑指offer刷题笔记20210421_第9张图片

class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length == 0) return 0;
        int res = 0, minVal = prices[0];
        for(int i = 1; i < prices.length; i++){
            minVal = Math.min(minVal, prices[i-1]);
            res = Math.max(res, prices[i] - minVal);
        }
        return res;
    }
}

贪心算法

  • 14- I.剪绳子
    给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
    核心思路是:尽可能把绳子分成长度为3的小段,这样乘积最大
    1.如果 n = 2,返回1,如果 n = 3,返回2,两个可以合并成n小于4的时候返回n - 1
    2.如果 n = 4,返回4
    3.如果 n > 4,分成尽可能多的长度为3的小段,每次循环长度n减去3,乘积res乘以3;最后返回时乘以小于等于4的最后一小段;每次乘法操作后记得取余就行
    以上2和3可以合并
class Solution {
    public int cuttingRope(int n) {
        if(n < 4){//小于4的绳子,最大乘积都是n-1
            return n - 1;
        }
        int res = 1;
        while(n > 4){
            res *= 3;
            n -= 3;
        }
        return res * n;
    }
}
  • 14- II. 剪绳子 II
    与前一题相同,不过答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
 class Solution {
    public int cuttingRope(int n) {
        if(n < 4) return n-1;
        long res = 1;
        while(n > 4){
            res = res * 3 % 1000000007;
            n-=3;
        }
        return (int)(res * n % 1000000007);
    }
} 

二叉树

    1. 重建二叉树 M
      输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
      Leetcode剑指offer刷题笔记20210421_第10张图片
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    int[] preorder;
    HashMap<Integer,Integer> map = new HashMap<>();
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        this.preorder = preorder;
        for(int i = 0; i < inorder.length; i++){
            map.put(inorder[i], i);
        }
        return recur(0,0,inorder.length-1);
    }

    TreeNode recur(int pre_root, int in_left, int in_right){
        if(in_left > in_right) return null;
        TreeNode root = new TreeNode(preorder[pre_root]);
        int i = map.get(preorder[pre_root]);//获取前序遍历中根节点在中序遍历的下标
        //1.左子树的根节点是前序遍历根节点+1
        //2.左边界是中序遍历的左边界
        //3.右边界是中序遍历根节点左边一位
        root.left = recur(pre_root+1, in_left, i-1);
        //1.右子树根节点是前序遍历根节点+左子树长度+1
        //2.左边界是中序遍历根节点右边一位
        //3.右边界是中序遍历右边界
        root.right = recur(pre_root+i-in_left+1, i+1, in_right);
        return root;
    }
}

55 - I. 二叉树的深度 E
Leetcode剑指offer刷题笔记20210421_第11张图片

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public int maxDepth(TreeNode root) {
        if(root == null) return 0;
        return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
    }
}

68 - I. 二叉搜索树的最近公共祖先 E
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        while(root != null){
            if(root.val > p.val && root.val > q.val){//root的值大于p,q,说明p,q都在左子树
                root = root.left;//到左子树遍历
            }else if(root.val < p.val && root.val < q.val){
                    root = root.right;
                }else{
                    break;
                }
        }
        return root;
    }
}

68 - II. 二叉树的最近公共祖先 E
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
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;//不同时在左或右,说明分布在不同子树,根节点为最近祖先
    }
}

深度遍历BFS

  1. 二叉搜索树的第k大节点 E
    给定一棵二叉搜索树,请找出其中第k大的节点。
    性质:二叉搜索树的中序遍历为 递增序列
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    int res, count;
    public int kthLargest(TreeNode root, int k) {
        this.count = k;
        dfs(root);
        return res;
    }

    public void dfs(TreeNode root){
        if(root == null || count == 0) return;//若root为空或k=0即已经找到,则返回
        dfs(root.right);
        if(--count == 0){
            res = root.val;
            return;
        }
        dfs(root.left);
    }
}

55 - II. 平衡二叉树 E
输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。
此树的深度 等于 左子树的深度 与 右子树的深度 中的 最大值 +1
Leetcode剑指offer刷题笔记20210421_第12张图片

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public boolean isBalanced(TreeNode root) {
        if(dfs(root) == -1){
            return false;
        }
        return true;
    }

    public int dfs(TreeNode root){
        if(root == null) return 0;
        int left = dfs(root.left);
        if(left == -1) return -1;//出现-1,直接返回
        int right = dfs(root.right);
        if(right == -1) return -1;
        if(Math.abs(left - right) < 2){//左右子树高度差<1就是平衡二叉树
            return Math.max(left,right) + 1;
        }
        return -1;
    }
}

广度优先DFS

    1. 矩阵中的路径
      给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

一定是用深度优先搜索(DFS)+ 剪枝关键是各种条件的判断

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 (bfs(board, words, i, j, 0)) return true;
            }
        }
        return false;
    }

    public boolean bfs(char[][] board, char[] words, int i, int j, int k){
        if(i >= board.length || j >= board[0].length || i < 0 || j < 0 || board[i][j] != words[k]) return false;
        if(k == words.length - 1) return true;
        board[i][j] = '\0';//将访问过的标记,防止重复访问
        boolean res = (bfs(board, words, i-1, j, k+1) || bfs(board, words, i+1, j, k+1) ||
                       bfs(board, words, i, j-1, k+1) || bfs(board, words, i, j+1, k+1));
        board[i][j] = words[k];//将标记过的还原,因为若某次失败回溯后可能还要访问他
        return res;
    }
}
  • 机器人的运动范围
    地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

深度优先搜索: 可以理解为暴力法模拟机器人在矩阵中的所有路径。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。
剪枝: 在搜索中,遇到数位和超出目标值、此元素已访问,则应立即返回,称之为 可行性剪枝

递归参数: 当前元素在矩阵中的行列索引 i 和 j ,两者的数位和 si, sj 。
终止条件: 当 ① 行列索引越界 或 ② 数位和超出目标值 k 或 ③ 当前元素已访问过 时,返回 00 ,代表不计入可达解。
递推工作:
标记当前单元格 :将索引 (i, j) 存入 Set visited 中,代表此单元格已被访问过。
搜索下一单元格: 计算当前元素的 下、右 两个方向元素的数位和,并开启下层递归 。
回溯返回值: 返回 1 + 右方搜索的可达解总数 + 下方搜索的可达解总数,代表从本单元格递归搜索的可达解总数。

Leetcode剑指offer刷题笔记20210421_第13张图片

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//i或j越过数组边界
        || sum(i)+sum(j) > k//i+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 sum(int n){
        int sum = 0;
        while(n > 0){
            sum += n % 10;//求个位
            n /= 10;//十位
        }
        return sum;
    }
}
    1. 序列化二叉树 H
      请实现两个函数,分别用来序列化和反序列化二叉树。

你需要设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Codec {

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        if(root == null) return "[]";
        StringBuilder res = new StringBuilder("[");
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);

        while(!queue.isEmpty()){
            TreeNode node = queue.poll();
            if(node != null){
                res.append(node.val + ",");
                queue.add(node.left);
                queue.add(node.right);
            }else{
                res.append("null,");
            }
        }
        res.deleteCharAt(res.length()-1);
        res.append("]");
        return res.toString();
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        if(data.equals("[]")) return null;
        String[] res = data.substring(1,data.length()-1).split(",");
        TreeNode root = new TreeNode(Integer.parseInt(res[0]));
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);

        int i = 1;
        while(!queue.isEmpty()){
            TreeNode node = queue.poll();
            if(!res[i].equals("null")){
                node.left = new TreeNode(Integer.parseInt(res[i]));
                queue.add(node.left);
            }
            i++;
            if(!res[i].equals("null")){
                node.right = new TreeNode(Integer.parseInt(res[i]));
                queue.add(node.right);
            }
            i++;
        }
        return root;
    }
}

// Your Codec object will be instantiated and called as such:
// Codec codec = new Codec();
// codec.deserialize(codec.serialize(root));
  • 32 - I. 从上到下打印二叉树 M
    从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。

这题和上一题思路一样
题目要求的二叉树的 从上至下 打印(即按层打印),又称为二叉树的 广度优先搜索(BFS)。
BFS 通常借助 队列 的先入先出特性来实现。

Leetcode剑指offer刷题笔记20210421_第14张图片

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public int[] levelOrder(TreeNode root) {
        if(root == null) return new int[0];
        ArrayList<Integer> arr = new ArrayList<>();
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);

        while(!queue.isEmpty()){
            TreeNode node = queue.poll();
            arr.add(node.val);
            if(node.left != null) queue.add(node.left);
            if(node.right != null) queue.add(node.right);
        }

        int[] res = new int[arr.size()];
        for(int i = 0; i < arr.size(); i++){
            res[i] = arr.get(i);
        }
        return res;
    }
}
  • 32 - II. 从上到下打印二叉树 II E
    I. 按层打印: 题目要求的二叉树的 从上至下 打印(即按层打印),又称为二叉树的 广度优先搜索(BFS)。BFS 通常借助 队列 的先入先出特性来实现。
    II. 每层打印到一行: 将本层全部节点打印到一行,并将下一层全部节点加入队列,以此类推,即可分为多行打印。

    Leetcode剑指offer刷题笔记20210421_第15张图片
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        Queue<TreeNode> queue = new LinkedList<>();
        List<List<Integer>> res = new ArrayList<>();
        if(root != null) queue.add(root);

        while(!queue.isEmpty()){
            List<Integer> arr = new ArrayList<>();
            for(int i = queue.size(); i > 0; i--){
                TreeNode node = queue.poll();
                arr.add(node.val);
                if(node.left != null) queue.add(node.left);
                if(node.right != null) queue.add(node.right);
            }
            res.add(arr);
        }
        return res;
    }
}
  • 32 - III. 从上到下打印二叉树 III E
    请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。

Leetcode剑指offer刷题笔记20210421_第16张图片

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        Queue<TreeNode> queue = new LinkedList<>();
        List<List<Integer>> res = new ArrayList<>();
        if(root != null) queue.add(root);

        boolean flag = true;
        while(!queue.isEmpty()){
            LinkedList<Integer> arr = new LinkedList<>();
            for(int i = queue.size(); i > 0; i--){
                TreeNode node = queue.poll();
                if(flag) arr.addLast(node.val);
                else{
                    arr.addFirst(node.val);
                }
                if(node.left != null) queue.add(node.left);
                if(node.right != null) queue.add(node.right);
            }
            flag = !flag;
            res.add(arr);
        } 
        return res;
    }
}

位运算

    1. 二进制中1的个数
      请实现一个函数,输入一个整数(以二进制串形式),输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。
public class Solution {
    // you need to treat n as an unsigned value
    public int hammingWeight(int n) {
        int res = 0;
        while(n != 0){
            res += n&1;
            n >>>= 1;  
        } 
        return res;
    }
}
  • 16.数值的整数次方
    实现 pow(x, n) ,即计算 x 的 n 次幂函数(即,xn)。不得使用库函数,同时不需要考虑大数问题。
    Leetcode剑指offer刷题笔记20210421_第17张图片
class Solution {
    public double myPow(double x, int n) {
        if(x ==0) return 0;
        long b = n;
        double res = 1.0;
        if(b < 0){
            x = 1/x;
            b = -b;
        }
        while(b > 0){
            if((b&1) == 1) res *= x;//判断末位是否为1,是1才乘x,是0直接跳过该位
            x *= x;
            b >>= 1;//不断右移,将前面的数移到末位
        }
        return res;
    }
}
  • 56 - I. 数组中数字出现的次数
    一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
    示例 1:
    输入:nums = [4,1,4,6]
    输出:[1,6] 或 [6,1]
    Leetcode剑指offer刷题笔记20210421_第18张图片
    Leetcode剑指offer刷题笔记20210421_第19张图片
class Solution {
    public int[] singleNumbers(int[] nums) {
        int x = 0, y = 0, n = 0, m = 1;
        for(int num : nums){//循环按位异或遍历,不同为1,相同为0
            n ^= num;//最后剩下x^y
        }
        while((n&m) == 0){//x,y不同,则至少有一位不同,该位异或后位为1 & 1 = 1
            m <<= 1;
        }
        for(int num : nums){//按照不同的这一位将nums分为两组
            if((num&m) == 0) x ^= num;
            else y ^= num;
        }
        int[] res = {x,y};
        return res;
    }
}
  • 56 - II. 数组中数字出现的次数 II
    在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。
    Leetcode剑指offer刷题笔记20210421_第20张图片
class Solution {
    public int singleNumber(int[] nums) {
        int[] count = new int[32];//int类型32位
        for(int num : nums){
            for(int j = 0; j < 32; j++){//每个数有32位
                count[j] += num & 1;//统计每个位上1的个数
                num >>>= 1;
            }
        }
        int res = 0;
        for(int i = 0; i < 32; i++){
            res <<= 1;
            res |= count[31 - i] % 3;//从高位开始还原这个数
        }
        return res;
    }
}

递归

    1. 树的子结构 M
      Leetcode剑指offer刷题笔记20210421_第21张图片
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public boolean isSubStructure(TreeNode A, TreeNode B) {
        if(A == null || B == null) return false;
        boolean r1 = recure(A,B);//B与A相等
        boolean r2 = isSubStructure(A.left,B);//B是A左子树的一部分
        boolean r3 = isSubStructure(A.right,B);//B是A右子树的一部分
        return r1 || r2 || r3;
    }

    public boolean recure(TreeNode A, TreeNode B){
        if(B == null) return true;//这里B为空说明B已经全匹配上了
        if(A == null || A.val != B.val) return false;//说明A已经越过叶子节点,B还没匹配完,或者A,B已经有值不一样
        //若一某个点为根节点匹配上了,则他们的左右子节点
        //也要完全匹配
        return recure(A.left,B.left) && recure(A.right,B.right);
    }
}
    1. 二叉树的镜像 E
      Leetcode剑指offer刷题笔记20210421_第22张图片
      递归解析:
      终止条件: 当节点 root为空时(即越过叶节点),则返回 null ;
      递推工作:
      初始化节点 tmp ,用于暂存 root的左子节点;
      开启递归 右子节点 mirrorTree(root.right) ,并将返回值作为 root的 左子节点 。
      开启递归 左子节点 mirrorTree(tmp) ,并将返回值作为 root 的 右子节点 。
      返回值: 返回当前节点 root ;
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public TreeNode mirrorTree(TreeNode root) {
        if(root == null) return null;
        TreeNode tmp = root.left;
        root.left = mirrorTree(root.right);
        root.right = mirrorTree(tmp);
        return root;
    }
}
    1. 对称的二叉树 E
      Leetcode剑指offer刷题笔记20210421_第23张图片
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public boolean isSymmetric(TreeNode root) {
        return (root == null) ? true : recur(root.left,root.right);
    }

    public boolean recur(TreeNode l,TreeNode r){
        if(l == null && r == null) return true;
        if(l == null || r == null || l.val != r.val) return false;
        return recur(l.left,r.right) && recur(l.right,r.left);
    }
}

二叉搜索树定义: 左子树中所有节点的值 << 根节点的值;右子树中所有节点的值 >> 根节点的值;其左、右子树也分别为二叉搜索树。

  • 36.二叉搜索树与双向链表 M
    输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
    Leetcode剑指offer刷题笔记20210421_第24张图片
    性质:二叉搜索树的中序遍历为 递增序列
/*
// Definition for a Node.
class Node {
    public int val;
    public Node left;
    public Node right;

    public Node() {}

    public Node(int _val) {
        val = _val;
    }

    public Node(int _val,Node _left,Node _right) {
        val = _val;
        left = _left;
        right = _right;
    }
};
*/
class Solution {
    Node per, head;
    public Node treeToDoublyList(Node root) {
        if(root == null) return null;
        dfs(root);//二叉搜索树的中序遍历即为排序
        per.right = head;
        head.left = per;//首尾相连
        return head;
    }

    public void dfs(Node cur){
        if(cur == null) return;
        dfs(cur.left);
        if(per == null) head = cur;//per为空,说明当前节点是头节点
        else per.right = cur;//建立双向链表
        cur.left = per;//无论per是否为空,当前节点都指向per

        per = cur;//将当前cur置为per
        dfs(cur.right);
    }
}
  • 33.二叉搜索树的后序遍历序列 M
    输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。

最后一个数一定是根节点的值,要找到以一个大于改值的数,将数组划分为左右子树
Leetcode剑指offer刷题笔记20210421_第25张图片

class Solution {
    public boolean verifyPostorder(int[] postorder) {
        return recur(postorder, 0, postorder.length - 1);
    }

    public boolean recur(int[] postorder, int l, int r){
        if(l >= r) return true;
        int point = l;//定义一个指针遍历数组
        while(postorder[point] < postorder[r]) point++;//后序遍历,最后一个必定是root节点
        int m = point;//现在要找到第一个大于root的节点,划分左右子树,现在m左边全都小于root,即为大左子树
        while(postorder[point] > postorder[r]) point++;//m右边需要全部大于root,若达成该条件则point=r
        return point == r && recur(postorder, l, m-1) && recur(postorder, m, r-1);
    }
}
  1. 反转链表 E
    定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode cur = head, per = null;
        while(cur != null){
            ListNode tmp = cur.next; // 暂存后继节点 cur.next
            cur.next = pre;          // 修改 next 引用指向
            pre = cur;               // pre 暂存 cur
            cur = tmp;               // cur 访问下一节点
        }
        return per;
    }
}

回溯

  1. 二叉树中和为某一值的路径 M
    Leetcode剑指offer刷题笔记20210421_第26张图片
    Leetcode剑指offer刷题笔记20210421_第27张图片
/**
 * 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 {
    LinkedList<List<Integer>> res = new LinkedList<>();
    LinkedList<Integer> path = new LinkedList<>();
    public List<List<Integer>> pathSum(TreeNode root, int target) {
        recur(root, target);
        return res;
    }

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

数组

  1. 数组中重复的数字 E
    在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
    输入:[2, 3, 1, 0, 2, 5, 3]
    输出:2 或 3
class Solution {
    public int findRepeatNumber(int[] nums) {
        Set<Integer> set = new HashSet<>();
        for(int num : nums){
            if(set.contains(num)){
                return num;
            }else{
                set.add(num);
            }
        }
        return -1;
    }
}
  1. 二维数组中的查找 M
    Leetcode剑指offer刷题笔记20210421_第28张图片
class Solution {
    public boolean findNumberIn2DArray(int[][] matrix, int target) {
        int i = matrix.length - 1, j = 0;//从左下角开始判断
        while(i >= 0 && j < matrix[0].length){
            if(target < matrix[i][j]){
                i--;
            }else{
                if(target > matrix[i][j]){
                    j++;
                }else{
                    return true; 
                }
            }
        }
        return false;
    }
}
  1. 旋转数组的最小数字 M
    把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。
    输入:[3,4,5,1,2]
    输出:1
class Solution {
    public int minArray(int[] numbers) {
        int i = 0 , j = numbers.length - 1;
        while(i < j){
            int m = (i+j)/2;
            if(numbers[m] > numbers[j]){//如果中间数大于末尾,说明m在左排序数组,旋转点在(m,j)之间
                i = m + 1;
            }else{
                if(numbers[m] < numbers[j]){//中间数小于末尾,说明m在右排序数组,旋转点在(i,m)之间
                    j = m;
                }else{
                    j--;
                }
            }
        }
        return numbers[i];
    }
}
  1. 顺时针打印矩阵 E
    输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
    输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
    输出:[1,2,3,6,9,8,7,4,5]
class Solution {
    public int[] spiralOrder(int[][] matrix) {
        if(matrix.length == 0) return new int[0];
        int l = 0, r = matrix[0].length-1, t = 0, b = matrix.length-1;
        int x = 0;
        int[] res = new int[(r+1)*(b+1)];
        while(true){
            for(int i = l; i <= r; i++){
                res[x++] = matrix[t][i];
            }
            if(++t > b) break;
            for(int i = t; i <= b; i++){
                res[x++] = matrix[i][r];
            }
            if(--r < l) break;
            for(int i = r; i >= l; i--){
                res[x++] = matrix[b][i];
            }
            if(--b < t) break;
            for(int i = b; i >= t; i--){
                res[x++] = matrix[i][l];
            }
            if(++l > r) break;
        }
        return res;
    }
}
  1. 数组中出现次数超过一半的数字 E
    数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
    先排序,超过数组一半的数字必定会出现在中间
class Solution {
    public int majorityElement(int[] nums) {
        Arrays.sort(nums);
        return nums[nums.length/2];
    }
}
  1. 数组中的逆序对 H
    在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
    输入: [7,5,6,4]
    输出: 5
    使用归并排序,在合并阶段统计逆序数对
class Solution {
    int[] nums, tmp;
    int res = 0;
    public int reversePairs(int[] nums) {
        this.nums = nums;
        tmp = new int[nums.length];
        mergeSort(0, nums.length - 1);
        return res;
    }

    private void mergeSort(int left, int right){
        if(left >= right) return;
        int mid = (left + right) / 2;
        mergeSort(left, mid);
        mergeSort(mid + 1, right);
        merge(left, mid, right);
    }

    private void merge(int left, int mid, int right){
        int i = left;
        int j = mid + 1;
        int t = 0;
        while(i <= mid && j <= right){//在数组合并前统计
            if(nums[i] > nums[j]){//若左边数组最小的数都大于右边的数,则左边数组所有数都大于右边
                tmp[t++] = nums[j++];
                res += mid - i + 1;//统计逆序数对
            }else{
                tmp[t++] = nums[i++];
            }
        }

        while(i <= mid){//说明右边统计完了
            tmp[t++] = nums[i++];
        } 

        while(j <= right){//左边的统计完了
            tmp[t++] = nums[j++];
        } 
        t = 0;
        while(left <= right){
            nums[left++] = tmp[t++];
        }
    }
}

53 - I. 在排序数组中查找数字 I E
统计一个数字在排序数组中出现的次数。
输入: nums = [5,7,7,8,8,10], target = 8
输出: 2
二分法找到重复数字的左右边界

class Solution {
    public int search(int[] nums, int target) {
        int i = 0, j = nums.length - 1;
        while(i <= j){
            int mid = (i+j)/2;
            if(target >= nums[mid]) i = mid + 1;
            else j = mid - 1;
        }
        int right = i;//上面二分查找完毕后,target的有边界就是i
        if(j >= 0 && nums[j] != target) return 0;//说明数组中没有target

        i = 0;
        j = nums.length - 1;
        while(i <= j){
            int mid = (i+j)/2;
            if(target > nums[mid]) i = mid + 1;
            else j = mid - 1;
        }
        int left = j;
        return right - left - 1;
    }
}

53 - II. 0~n-1中缺失的数字 E
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
输入: [0,1,3]
输出: 2
排序数组中的搜索问题,首先想到 二分法 解决。

class Solution {
    public int missingNumber(int[] nums) {
        int i = 0, j = nums.length - 1;
        while(i <= j){
            int mid = (i+j)/2;
            if(mid == nums[mid]){//如果当前下标等于当前元素,说明mid左边没问题,向右找
                i = mid + 1;
            }else
            j = mid - 1;//如果当前下标不等于当前元素,说明缺失的元素在左边
        }
        return i;
    }
}

链表

  1. 合并两个排序的链表 E
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if(l1 == null) return l2;
        if(l2 == null) return l1;
        if(l1.val <= l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }
}
  1. 链表中倒数第k个节点 E
    输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
    例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。

使用双指针则可以不用统计链表长度。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode getKthFromEnd(ListNode head, int k) {
        ListNode former = head, latter = head;//因为不知道链表长度,所以设置双指针
        for(int i = 0; i < k ;i++){//循环结束后,快慢指针相差k
            former = former.next;
        }
        while(former != null){//快慢指针相差k,当快指针到达链表结尾时,慢指针距离结尾k,即为倒数第k个节点
            former = former.next;
            latter = latter.next;
        }
        return latter;
    }
}
  1. 删除链表的节点 E
    给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
    返回删除后的链表的头节点。
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
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;
    }
}
  1. 两个链表的第一个公共节点 E
    Leetcode剑指offer刷题笔记20210421_第29张图片
    两个链表长度分别为L1+C、L2+C, C为公共部分的长度,按照楼主的做法: 第一个人走了L1+C步后,回到第二个人起点走L2步;第2个人走了L2+C步后,回到第一个人起点走L1步。 当两个人走的步数都为L1+L2+C时就两个家伙就相爱了
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = 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;
    }
}
  1. 复杂链表的复制 M
    Leetcode剑指offer刷题笔记20210421_第30张图片
    Leetcode剑指offer刷题笔记20210421_第31张图片
/*
// Definition for a Node.
class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/
class Solution {
    public Node copyRandomList(Node head) {
        if(head == null) return null;
        Map<Node, Node> map = new HashMap<>();
        Node cur = head;
        while(cur != null){
            map.put(cur, new Node(cur.val));//先在map的val里复制每一个节点的值
            cur = cur.next;
        }
        cur = head;
        while(cur != null){
            map.get(cur).next = map.get(cur.next);
            map.get(cur).random = map.get(cur.random);
            cur = cur.next;
        }
        return map.get(head);
    }
}

字符串

  1. 替换空格 E
    请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
    输入:s = “We are happy.”
    输出:“We%20are%20happy.”
class Solution {
    public String replaceSpace(String s) {
        StringBuilder str = new StringBuilder();
        for(Character c : s.toCharArray()){
            if(c == ' ') str.append("%20");
            else str.append(c);
        }
        return str.toString();
    }
}
  1. 字符串的排列 M
    输入一个字符串,打印出该字符串中字符的所有排列。
    你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
    输入:s = “abc”
    输出:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]
    Leetcode剑指offer刷题笔记20210421_第32张图片
    Leetcode剑指offer刷题笔记20210421_第33张图片
class Solution {
    List<String> res = new ArrayList<>();
    char[] c;
    public String[] permutation(String s) {
        c = s.toCharArray();
        dfs(0);
        String[] str =  new String[res.size()];
        return res.toArray(str);
    }

    private void dfs(int x){
        if(x == c.length - 1){//如果到了倒数第一个,前面的都固定了,只剩下一种排法
            res.add(String.valueOf(c));
        }
        HashSet<Character> set = new HashSet<>();
        for(int i = x; i < c.length; i++){
            if(set.contains(c[i])){//出现重复的,直接跳过
                continue;
            }
            set.add(c[i]);
            swap(x, i);//将c[i]固定在第x位
            dfs(x+1);//固定剩下的位
            swap(x, i);//固定完后恢复
        }
    }

    private void swap(int a, int b){
        char tmp = c[a];
        c[a] = c[b];
        c[b] = tmp;
    }
}
  1. 第一个只出现一次的字符 E
    在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。

输入:s = “abaccdeff”
输出:‘b’

肯定是用键值对统计每个字母出现的次数

class Solution {
    public char firstUniqChar(String s) {
        HashMap<Character, Boolean> map = new HashMap<>();//用键值对存储
        char[] ch = s.toCharArray();
        for(Character c :ch){
            map.put(c, map.containsKey(c));//存入<字符,是否出现过>
        }
        for(Character c : ch){
            if(!map.get(c)){//如果某个键的值为否,说明该字母只出现了一次
                return c;
            }
        }
        return ' ';
    }
}

其他

  • 40.最小的k个数 E
    输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
    快速排序即可解决
class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        quickSort(arr, 0, arr.length - 1);
        return Arrays.copyOf(arr, k);
    }

    private void quickSort(int[] arr, int left, int right){
        if(left >= right) return;
        int i = left, j = right;
        while(i < j){
            while(i < j && arr[j] >= arr[left]) j--;
            while(i < j && arr[i] <= arr[left]) i++;
            swap(arr, i, j);
        }
        swap(arr, left, i);
        quickSort(arr, left, i-1);
        quickSort(arr, i+1, right);
    }

    private void swap(int[] arr, int i, int j){
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}
    1. 把数组排成最小的数 M
      输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
      示例 1:
      输入: [10,2]
      输出: “102”Leetcode剑指offer刷题笔记20210421_第34张图片
      按照上面的规律比较两个数,然后使用快排即可
class Solution {
    public String minNumber(int[] nums) {
        String[] arr = new String[nums.length];
        for(int i = 0; i < nums.length; i++){
            arr[i] = String.valueOf(nums[i]);
        }
        quickSort(arr, 0, nums.length-1);
        StringBuilder res = new StringBuilder();
        for(String s : arr){
            res.append(s);
        }
        return res.toString();
    }

    public void quickSort(String[] arr, int l, int r){
        if(l >= r) return;
        int i = l, j = r;
        String tmp = arr[i];
        while(i < j){
            while((arr[j]+arr[l]).compareTo(arr[l]+arr[j]) >= 0 && i < j) j--;
            while((arr[i]+arr[l]).compareTo(arr[l]+arr[l]) <= 0 && i < j) i++;
            tmp = arr[i];
            arr[i] = arr[j];
            arr[j] = tmp;
        }
        arr[i] = arr[l];
        arr[l] = tmp;
        quickSort(arr,l,i-1);
        quickSort(arr,i+1,r);
    } 
}
  • 58.左旋转字符串
    字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。

示例 1:

输入: s = “abcdefg”, k = 2
输出: “cdefgab”

法一:

class Solution {
    public String reverseLeftWords(String s, int n) {
        return s.substring(n,s.length()) + s.substring(0,n);
    }
}

法二:

class Solution {
    public String reverseLeftWords(String s, int n) {
        StringBuilder sb = new StringBuilder();
        for(int i = n; i < n+s.length(); i++){
            sb.append(s.charAt(i%s.length()));//取模,非常巧妙。i
                                             //i>length时,可以取到前半段
        }
        return sb.toString();
    }
}
  • 61.扑克牌中的顺子
    必须知道,满足以下条件,必然可行
    1.除大小王外,所有牌无重复
    2.设此 55 张牌中最大的牌为 max ,最小的牌为 min(大小王除外),满足:max - min < 5
    Leetcode剑指offer刷题笔记20210421_第35张图片
class Solution {
    public boolean isStraight(int[] nums) {
        Set<Integer> set = new HashSet<>();
        int max = 0, min = 14;//最大最小值是反过来设置的,这样他们才能更新
        for(int num : nums){
            if(num == 0) continue;//遇到0,跳过
            min = Math.min(num,min);
            max = Math.max(num,max);
            if(set.contains(num)) return false;//如果包含重复的数字,直接退出
            set.add(num);
        }
        return max - min < 5;//最大 - 最小 < 5,且无重复数字,必然可行
    }
}
  • 17.打印从1到最大的n位数
    输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。
    最大n位数为: 10的n次方-1
class Solution {
    public int[] printNumbers(int n) {
        int end = (int)Math.pow(10,n) - 1;//最大n位数为,10的n次方-1
        int[] res = new int[end];
        for(int i = 0; i < end; i++){
            res[i] = i + 1;
        }
        return res;
    }
}

本题若考虑大数则难度加大,之后再学

    1. 替换空格
      请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
      示例 1:
      输入:s = “We are happy.”
      输出:“We%20are%20happy.”
class Solution {
    public String replaceSpace(String s) {
        StringBuilder str = new StringBuilder();
        for(Character c : s.toCharArray()){
            if(c == ' ') str.append("%20");
            else str.append(c);
        }
        return str.toString();
    }
}
  • 21.调整数组顺序使奇数位于偶数前面
    输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
class Solution {
    public int[] exchange(int[] nums) {
        int i = 0, j = nums.length - 1, tmp;
        while(i < j){
            if(i<j && nums[i]%2==1) i++;
            if(i<j && nums[j]%2==0) j--;
            tmp = nums[i];
            nums[i] = nums[j];
            nums[j] = tmp;    
        }
        return nums;
    }
}
  • 44.数字序列中某一位的数字
    数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从下标0开始计数)是5,第13位是1,第19位是4,等等。
    请写一个函数,求任意第n位对应的数字。

1.确定 n 所在 数字 的 位数 ,记为 digit ;
2.确定 n 所在的 数字 ,记为 num;
3.确定 n 是 num 中的哪一数位,并返回结果。
Leetcode剑指offer刷题笔记20210421_第36张图片
Leetcode剑指offer刷题笔记20210421_第37张图片

class Solution {
    public int findNthDigit(int n) {
        int digit = 1;//几位数1,2,3...
        long start = 1;//几位数的最小一个1,10,100...可能超出int范围,所以用long
        long count = 9;//几个数位9,180,2700...
        while(n > count){//找到n是几位数
            n -= count;//减掉几位数的开头数字,n = 该位数段从0开始数的第几位
            digit += 1;
            start *= 10;
            count = 9 * digit * start;
        }
        long num = start + (n-1) / digit;//得到n所在的数字
        String s = Long.toString(num);//转换乘Sting数组,便于找到n的位置
        char c = s.charAt((n-1) % digit);//确定n在num的第几位
        return c - '0';//将char转换为int
    }
}
    1. 构建乘积数组
      给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。
      本质就是两个dp数组,分别维护 i 左侧、右侧的乘积和
class Solution {
    public int[] constructArr(int[] a) {
        int[] res = new int[a.length];
        for(int i = 0, p = 1; i < a.length; i++){//先遍历i左边的数,获得乘积
            res[i] = p;
            p *= a[i];
        }
        for(int i = a.length - 1, p = 1; i >=0; i--){//再遍历右边,相乘
            res[i] *= p;
            p *= a[i];
        }   
        return res;
    }
}
    1. 栈的压入、弹出序列
      输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。

示例 1:
输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1

class Solution {
    public boolean validateStackSequences(int[] pushed, int[] popped) {
        Stack<Integer> stack = new Stack<>();//创建一个模拟栈
        int i = 0;
        for(int num : pushed){
            stack.push(num);
            while(!stack.isEmpty() && stack.peek() == popped[i]){//判断栈顶元素是否与poped相等
                stack.pop();//相等则弹出
                i++;//继续比较poped中下一个,
            }//不等则继续入栈
        }
        return stack.isEmpty();//若stack为空,表示弹出顺序与poped一致
    }
}
    1. 求1+2+…+n
      求 1+2+…+n ,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
class Solution {
    int res = 0;
    public int sumNums(int n) {
        boolean x = n > 1 && sumNums(n -1) > 0;//n = 1 是终止递归的标志
        res += n;
        return res;
    }
}
    1. 表示数值的字符串
      请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。
      遍历字符串
      如果当前字符c是数字:将hasNum置为true,index往后移动一直到非数字或遍历到末尾位置;如果已遍历到末尾(index == n),结束循环
      如果当前字符c是’e’或’E’:如果e已经出现或者当前e之前没有出现过数字,返回fasle;否则令hasE = true,并且将其他3个flag全部置为false,因为要开始遍历e后面的新数字了
      如果当前字符c是+或-:如果已经出现过+或-或者已经出现过数字或者已经出现过’.’,返回flase;否则令hasSign = true
      如果当前字符c是’.’:如果已经出现过’.‘或者已经出现过’e’或’E’,返回false;否则令hasDot = true
      如果当前字符c是’ ':结束循环,因为可能是末尾的空格了,但也有可能是字符串中间的空格,在循环外继续处理
      如果当前字符c是除了上面5种情况以外的其他字符,直接返回false
class Solution {
    public boolean isNumber(String s) {
        int n = s.length();
        int index = 0;
        boolean hasNum = false, hasDot = false, hasSign = false, hasE = false;
        while(index < n && s.charAt(index) == ' '){//处理开头的空格
            index++;
        }
        while(index < n){
            while(index < n && s.charAt(index) >= '0' && s.charAt(index) <= '9'){//是数字跳过
                index++;
                hasNum = true;
            }
            if(index == n){//遍历到末尾则跳出循环
                break;
            }
            char c = s.charAt(index);
            if(c == 'e' || c == 'E'){//若e前面已经有e或e前没有数字则不能表示数值
                if(hasE || !hasNum){
                    return false;
                }
                hasE = true;
                hasNum = false; hasDot = false; hasSign = false;//e前面判断完成后,标志位置false,                                                                               //继续判断e后面是不是数字
            }else if(c == '.'){
                if(hasDot || hasE){//若小数点前面已经有小数点或小数点前没有数字则不能表示数值
                    return false;
                }
                hasDot = true;
            }else if(c == '+' || c == '-'){//若符号前面已经有符号或符号前没有数字或符号前面有数字则不能表示数值
                if(hasSign || hasDot || hasNum){
                    return false;
                }
                hasSign = true;
            }else if(c == ' '){
                break;
            }else {
                return false;
            }
            index++;
        }
        while(index < n && s.charAt(index) == ' '){//处理末尾的空格
            index++;
        }
        return hasNum && index == n;
    }
}

你可能感兴趣的:(数据结构与算法,算法,数据结构)