算法之剑指offer题目总结

简介

剑指offer中的题目是面试中考察的重点,本篇文章对这些题目进行总结,方便日后阅读。

题目

题目顺序参考 leetcode 中出现的顺序。

1. 数组中重复的数字
2. 二维数组中的查找
3. 替换空格
4. 从尾到头打印链表
5. 重建二叉树
6. 用两个栈实现队列
7. 斐波那契数列
8. 青蛙跳台阶问题
9. 旋转数组的最小数字
10. 矩阵中的路径
11. 机器人的运动范围
12. 剪绳子
13. 剪绳子 II
14. 二进制中1的个数
15. 数值的整数次方
16. 打印从1到最大的n位数
17. 删除链表的节点
18. 正则表达式匹配
19. 表示数值的字符串
20. 调整数组顺序使奇数位于偶数前面
21. 链表中倒数第k个节点
22. 反转链表
23. 合并两个排序的链表
24. 树的子结构
25. 二叉树的镜像
26. 对称的二叉树
27. 顺时针打印矩阵
28. 包含min函数的栈
29. 栈的压入、弹出序列
30. 从上到下打印二叉树
31. 从上到下打印二叉树 II
32. 从上到下打印二叉树 III
33. 二叉搜索树的后序遍历序列
34. 二叉树中和为某一值的路径
35. 复杂链表的复制
36. 二叉搜索树与双向链表
37. 序列化二叉树
38. 字符串的排列
39. 数组中出现次数超过一半的数字
40. 最小的k个数
41. 数据流中的中位数
42. 连续子数组的最大和
43. 1~n 整数中 1 出现的次数
44. 数字序列中某一位的数字
45. 把数组排成最小的数
46. 把数字翻译成字符串
47. 礼物的最大价值
48. 最长不含重复字符的子字符串
49. 丑数
50. 第一个只出现一次的字符
51. 数组中的逆序对
52. 两个链表的第一个公共节点
53. 在排序数组中查找数字 I
54. 0~n-1中缺失的数字
55. 二叉搜索树的第k大节点
56. 二叉树的深度
57. 平衡二叉树
58. 数组中数字出现的次数
59. 数组中数字出现的次数 II
60. 和为s的两个数字
61. 和为s的连续正数序列
62. 翻转单词顺序
63. 左旋转字符串
64. 滑动窗口的最大值
65. 队列的最大值
66. n个骰子的点数
67. 扑克牌中的顺子
68. 圆圈中最后剩下的数字
69. 股票的最大利润
70. 求1+2+…+n
71. 不用加减乘除做加法
72. 构建乘积数组
73. 把字符串转换成整数
74. 二叉搜索树的最近公共祖先
75. 二叉树的最近公共祖先

解析

1. 数组中重复的数字

题目描述

找出数组中重复的数字。

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

示例 1:

输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3

分析
  1. 哈希表。记录遍历过的数字,发现重复的就返回。时空复杂度都为O(n).
  2. 原地交换。由于题目告诉数字范围为0~n-1,因此遍历时交换数字位置使得 n u m s [ i ] = i nums[i] = i nums[i]=i,当交换时发现 n u m s [ i ] = i nums[i] = i nums[i]=i 时说明这个位置已经被占用,是重复数字。时间复杂度O(n),空间复杂度O(1).
代码
  • 方法一
class Solution {
    public int findRepeatNumber(int[] nums) {
        int n = nums.length;
        int[] record = new int[n];
        for (int num : nums) {
            if (record[num] != 0) {
                return num;
            }
            record[num] = 1;
        }
        return -1;
    }
}
  • 方法二
class Solution {
    public int findRepeatNumber(int[] nums) {
        int i = 0, n = nums.length;
        while (i < n) {
            if (nums[i] == i) {
                i++;
                continue;
            }
            if (nums[nums[i]] == nums[i]) {
                return nums[i];
            }
            int tmp = nums[nums[i]];
            nums[nums[i]] = nums[i];
            nums[i] = tmp;
        }
        return -1;
    }
}

2. 二维数组中的查找

题目描述

在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

示例:

现有矩阵 matrix 如下:

[
     [  1,   4,    7, 11,  15],
     [  2,   5,    8, 12,  19],
     [  3,   6,    9, 16,  22],
     [10, 13,  14, 17,  24],
     [18, 21,  23, 26,  30]
]

给定 target = 5,返回 true。

给定 target = 20,返回 false。

分析
  1. 暴力。遍历二维数组中所有的元素。时间复杂度O(mn).
  2. 线性遍历。从二维数组的右上元素 (i, j) 开始遍历,当小于target时,j++;当大于target时,i–。时间复杂度O(m+n)
    正确性分析:将二维数组逆时针旋转45°,便类似一个“二叉搜索树”,左分支更小,右分支更大。参考文章.
    在这里插入图片描述
代码
public boolean findNumberIn2DArray(int[][] matrix, int target) {
    if (matrix.length == 0 || matrix[0].length == 0) {
        return false;
    }
    int m = matrix.length, n = matrix[0].length;
    int i = 0, j = n - 1;
    while (i >= 0 && i < m && j >= 0 && j < n) {
        if (matrix[i][j] > target) {
            j--;
        } else if (matrix[i][j] < target) {
            i++;
        } else {
            return true;
        }
    }
    return false;
}

3. 替换空格

题目描述

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

示例 1:

输入:s = “We are happy.”
输出:“We%20are%20happy.”

分析
  1. 使用可变字符串存储修改后的字符串。
代码
public String replaceSpace(String s) {
    StringBuilder builder = new StringBuilder();
    for (char c : s.toCharArray()) {
        if (c == ' ') {
            builder.append("%20");
        } else {
            builder.append(c);
        }
    }
    return builder.toString();
}

4. 从尾到头打印链表

题目描述

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。

示例 1:

输入:head = [1,3,2]
输出:[2,3,1]

分析
  1. 栈。由于需要返回链表的逆序,栈是理想的存储选择。
  2. 两次遍历。第一次遍历得到链表的长度,第二次遍历将元素放入答案的集合中。
代码
public int[] reversePrint(ListNode head) {
    int n = 0;
    ListNode first = head;
    while (first != null) {
        n++;
        first = first.next;
    }
    int[] res = new int[n];
    int i = n - 1;
    while (head != null) {
        res[i--] = head.val;
        head = head.next;
    }
    return res;
}

5. 重建二叉树

题目描述

输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

例如,给出

前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]

返回如下的二叉树:

   3
  /  \
9   20
     /   \
   15   7

分析
  1. 递归。
    由于前序遍历先遍历根节点,对于preorder中每一个元素,找到inorder中的位置index,inorder[:index] 和 inorder[index+1:] 分别是左右子树的元素。
代码
class Solution {
    private Map<Integer, Integer> numIndex;
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        numIndex = new HashMap<>();
        int n = inorder.length;
        for (int i = 0; i < n; i++) {
            numIndex.put(inorder[i], i);
        }
        return build(preorder, 0, n - 1, inorder, 0, n - 1);
    }

    private TreeNode build(int[] preorder, int startPre, int endPre, int[] inorder, int startIn, int endIn) {
        if (startPre > endPre) {
            return null;
        }
        int val = preorder[startPre];
        TreeNode node = new TreeNode(val);
        int index = numIndex.get(val);
        int lenLeft = index - startIn;
        node.left = build(preorder, startPre+1, startPre + lenLeft, inorder, startIn, index - 1);
        node.right = build(preorder, startPre + lenLeft + 1, endPre, inorder, index + 1, endIn);
        return node;
    }
}

6. 用两个栈实现队列

题目描述

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

示例 1:

输入:
[“CQueue”,“appendTail”,“deleteHead”,“deleteHead”]
[[],[3],[],[]]
输出:[null,null,3,-1]

分析
  1. stack1 用来放push的数,stack2 用来pop,当 stack2 为空时,将 stack1 中的数弹出并压入 stack2.
代码
class CQueue {

    private LinkedList<Integer> stack1;
    private LinkedList<Integer> stack2;
    public CQueue() {
        stack1 = new LinkedList<>();
        stack2 = new LinkedList<>();
    }

    public void appendTail(int value) {
        stack1.add(value);
    }

    public int deleteHead() {
        if (stack2.isEmpty()) {
            while (!stack1.isEmpty()) {
                stack2.add(stack1.poll());
            }
        }
        return stack2.isEmpty() ? -1 : stack2.poll();
    }
}

7. 斐波那契数列

题目描述

写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:

F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.

斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:

输入:n = 2
输出:1

分析
  1. dp.
    经典动规问题。记录前面两项,不断向后滑动。
代码
class Solution {
    public int fib(int n) {
        if (n < 2) {
            return n;
        }
        int mod = 1000000007;
        int a = 0, b = 1, t;
        for (int i = 2; i <= n; i++) {
            t = a;
            a = b;
            b = (t + a) % mod;
        }
        return b;
    }
}

8. 青蛙跳台阶问题

题目描述

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:

输入:n = 2
输出:2

分析
  1. dp.
    和上题除了初始条件不一样外,其他都一样。
代码
class Solution {
    public int numWays(int n) {
        if (n < 2) {
            return 1;
        }
        int mod = 1000000007;
        int a = 1, b = 1;
        for (int i = 2; i <= n; i++) {
            int tmp = a;
            a = b;
            b = (a + tmp) % mod;
        }
        return b;
    }
}

9. 旋转数组的最小数字

题目描述

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。

示例 1:

输入:[3,4,5,1,2]
输出:1

示例 2:

输入:[2,2,2,0,1]
输出:0

分析
  1. 二分。
    旋转数组经典问题,参考另一篇文章「算法之旋转数组的查找问题整理(附二分查找模板)」。
代码
public int minArray(int[] numbers) {
    int n = numbers.length;
    int left = 0, right = n - 1, mid;
    while (left < right) {
        mid = left + (right - left) / 2;
        if (numbers[mid] < numbers[right]) {
            right = mid;
        } else if (numbers[mid] > numbers[right]) {
            left = mid + 1;
        } else {
            right--;
        }
    }
    return numbers[left];
}

10. 矩阵中的路径

题目描述

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

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

例如,在下面的 3×4 的矩阵中包含单词 “ABCCED”(单词中的字母已标出)。

在这里插入图片描述

示例 1:

输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true

分析
  1. 回溯搜索。在原二维数组中记录搜索过的位置,避免走“回头路”。
代码
class Solution {
    int[] dx = {-1, 0, 1, 0};
    int[] dy = {0, -1, 0, 1};
    char[][] board;
    int m, n;
    String word;
    boolean isFound;
    public boolean exist(char[][] board, String word) {
        this.board = board;
        this.word = word;
        m = board.length;
        n = board[0].length;
        isFound = false;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (board[i][j] == word.charAt(0)) {
                    char t = board[i][j];
                    board[i][j] = '-';
                    if (dfs(i, j, 1)) {
                        return true;
                    }
                    board[i][j] = t;
                }
            }
        }
        return false;
    }

    private boolean dfs(int i, int j, int c) {
        if (c == word.length()) {
            return true;
        }
        
        for (int k = 0; k < 4; k++) {
            int x = i + dx[k], y = j + dy[k];
            if (x >= 0 && x < m && y >= 0 && y < n && board[x][y] == word.charAt(c)) {
                char t = board[x][y];
                board[x][y] = '-';
                if (dfs(x, y, c + 1)) {
                    return true;
                }
                board[x][y] = t;
            }
        }
        return false;
    }
}

11. 机器人的运动范围

题目描述

地上有一个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。请问该机器人能够到达多少个格子?

示例 1:

输入:m = 2, n = 3, k = 1
输出:3

分析
  1. DFS
代码
class Solution {
    boolean[][] record;
    int[] dx = {-1, 0, 1, 0};
    int[] dy = {0, -1, 0, 1};
    int res;
    int m, n, k;
    public int movingCount(int m, int n, int k) {
        record = new boolean[m][n];
        this.m = m;
        this.n = n;
        this.k = k;
        res = 0;
        if (k < 0) {
            return res;
        }
        record[0][0] = true;
        res++;
        dfs(0, 0);
        return res;
    }

    private void dfs(int i, int j) {

        for (int k = 0; k < 4; k++) {
            int x = i + dx[k], y = j + dy[k];
            if (x >= 0 && x < m && y >= 0 && y < n && !record[x][y] && calSum(x) + calSum(y) <= this.k) {
                res++;
                record[x][y] = true;
                dfs(x, y);
            }
        }
    }

    private int calSum(int x) {
        int res = 0;
        while (x != 0) {
            res += x % 10;
            x /= 10;
        }
        return res;
    }
}

12. 剪绳子

题目描述

给你一根长度为 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。

示例 1:

输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1

分析
  1. 数学分析。
    求最佳切分长度 x x x.
    因此求 x n x x^{\frac{n}{x}} xxn 的最大值。设 y = x n x y = x^{\frac{n}{x}} y=xxn,等式两边同时取对数 l n y = n l n x x ln y = n\frac{lnx}{x} lny=nxlnx,因为 n n n 是个常数,所以只需求 l n x x \frac{lnx}{x} xlnx的最大值。
    f ( x ) = l n x x f(x) = \frac{lnx}{x} f(x)=xlnx f ′ ( x ) = 1 − l n x x 2 f^{'}(x) = \frac{1 - lnx}{x^2} f(x)=x21lnx,令 f ′ ( x ) = 0 f^{'}(x) = 0 f(x)=0,得 x = e x = e x=e
    易知 x < e , f ′ ( x ) > 0 ; x > e , f ′ ( x ) < 0 x < e, f^{'}(x) > 0; x > e, f^{'}(x) < 0 x<e,f(x)>0;x>e,f(x)<0,所以 f ( x ) f(x) f(x) 先增后减, x = e x = e x=e 为最大值点。
    e ≈ 2.7 e ≈ 2.7 e2.7,离得最近的整数是2和3。那到底去2还是3呢?
    2 1 2 2^{\frac{1}{2}} 221 3 1 3 3^{\frac{1}{3}} 331
    ( 2 1 2 ) 6 (2^{\frac{1}{2}})^{6} (221)6 ( 3 1 3 ) 6 (3^{\frac{1}{3}})^6 (331)6
    2 3 = 8 2^3 = 8 23=8 3 2 = 9 3^2 = 9 32=9
    因此最优切分长度为3.
代码
public int cuttingRope(int n) {
    if (n < 4) {
        return n - 1;
    }
    int a = n / 3, b = n % 3;
    if (b == 0) return (int)Math.pow(3, a);
    else if (b == 1) return (int) Math.pow(3, a - 1) * 4;
    else return (int) Math.pow(3, a) * 2;
}

13. 剪绳子 II

题目描述

给你一根长度为 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。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:

输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1

分析
  1. 快速幂。和上一题的区别在于n的范围扩大了,求幂的结果可能超过了int的范围,而不能直接使用Math.pow进行计算。
    有一种快速求取幂次方的方法,称为快速幂。
    快速幂模板:
public long qmi(long a, long b) {
    long res = 1;
    while (b > 0) {
        if ((b & 1) == 1) {
            res *= a;
        }
        a *= a;
        b >>= 1;
    }
    return res;
}
代码
class Solution {
    private int mod;
    public int cuttingRope(int n) {
        mod = 1000000007;
        if (n < 4) {
            return n - 1;
        }
        long a = n / 3, b = n % 3;
        if (b == 0) return (int) qmi(3, a);
        else if (b == 1) return (int) (qmi(3, a - 1) * 4 % mod);
        else return (int) (qmi(3, a) * 2 % mod);
    }

    private long qmi(long a, long b) {
        long res = 1;
        while (b != 0) {
            if ((b & 1) == 1) {
                res = res * a % mod;
            }
            a = a * a % mod;
            b >>= 1;
        }
        return res;
    }
}

14. 二进制中1的个数

题目描述

请实现一个函数,输入一个整数(以二进制串形式),输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。

示例 1:

输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 ‘1’。

分析
  1. 逐位判断。判断二进制数的每一位是否为1。java中需要使用无符号右移。时间复杂度 O ( l o g 2 n ) O(log_2 n) O(log2n)
  2. n&(n-1)。时间复杂度O(M) ,M为1的个数。
代码
  • 方法一
public int hammingWeight(int n) {
    int res = 0;
    while (n != 0) {
        if ((n & 1) == 1) {
            res += 1;
        }
        n >>>= 1;
    }
    return res;
}
  • 方法二
public int hammingWeight(int n) {
    int res = 0;
    while (n > 0) {
        res += 1;
        n &= (n - 1);
    }
    return res;
}

15. 数值的整数次方

题目描述

实现 pow(x, n) ,即计算 x 的 n 次幂函数(即, x n x^n xn)。不得使用库函数,同时不需要考虑大数问题。

示例 1:

输入:x = 2.00000, n = 10
输出:1024.00000

提示:

  • -100.0 < x < 100.0
  • − 2 31 -2^{31} 231 <= n <= 2 31 − 1 2^{31}-1 2311
  • − 1 0 4 -10^4 104 <= x n x^n xn <= 1 0 4 10^4 104
分析
  1. 快速幂。
代码
class Solution {
    public double myPow(double x, int n) {
        int sign = 1;
        long a = n;
        if (a < 0) {
            sign = -1;
            a = -a;
        }
        double res = qmi(x, a);
        return sign == 1 ? res : 1 / res;
    }

    private double qmi(double a, long b) {
        double res = 1;
        while (b > 0) {
            if ((b & 1) == 1) {
                res *= a;
            }
            a *= a;
            b >>= 1;
        }
        return res;
    }
}

注意:int负数转正数防止越界,应该用long的正数去接收int负数转来的正数。因为int的取值范围为 − 2 31 -2^{31} 231 ~ 2 31 − 1 2^{31}-1 2311.

16. 打印从1到最大的n位数

题目描述

输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。

示例 1:

输入: n = 1
输出: [1,2,3,4,5,6,7,8,9]

分析
  1. 暴力
代码
class Solution {
    public int[] printNumbers(int n) {
        int[] res = new int[(int) Math.pow(10, n) - 1];
        for (int i = 1; i <= res.length; i++) {
            res[i-1] = i;
        }
        return res;
    }
}

17. 删除链表的节点

题目描述

给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。

返回删除后的链表的头节点。

注意:此题对比原题有改动

示例 1:

输入: head = [4,5,1,9], val = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.

分析
  1. 创建虚拟节点。删除节点时需要将前一个节点的next指针指向下一个节点。
代码
public ListNode deleteNode(ListNode head, int val) {
    ListNode dummy = new ListNode(-1), first = dummy;
    dummy.next = head;
    while (head != null && head.val != val) {
        dummy = dummy.next;
        head = head.next;
    }
    if (head != null) {
        dummy.next = head.next;
    }
    return first.next;
}

18. 正则表达式匹配

题目描述

请实现一个函数用来匹配包含’. ‘和’‘的正则表达式。模式中的字符’.‘表示任意一个字符,而’'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但与"aa.a"和"ab*a"均不匹配。

示例 1:

输入:
s = “aa”
p = “a”
输出: false
解释: “a” 无法匹配 “aa” 整个字符串。

分析
  1. dp.
    定义dp[i][j]表示s[:i]和p[:j]是否匹配。
    转移方程:

{ d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] , s [ i ] = = p [ j ] d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] , p [ j ] = = " . " d p [ i ] [ j ] = d p [ i ] [ j − 2 ] , p [ j ] = = " ∗ " d p [ i ] [ j ] = d p [ i − 1 ] [ j ] , p [ j ] = = " ∗ "   & &   ( s [ i ] = = p [ j − 1 ]   ∣ ∣   p [ j − 1 ] = = " . " ) \begin{cases} dp[i][j] = dp[i-1][j-1], & s[i] == p[j]\\ dp[i][j] = dp[i-1][j-1], & p[j] == "."\\ dp[i][j] = dp[i][j-2], & p[j] == "*"\\ dp[i][j] = dp[i - 1][j], & p[j] == "*"\ \&\&\ (s[i] == p[j-1]\ ||\ p[j-1] == ".") \end{cases} dp[i][j]=dp[i1][j1],dp[i][j]=dp[i1][j1],dp[i][j]=dp[i][j2],dp[i][j]=dp[i1][j],s[i]==p[j]p[j]=="."p[j]==""p[j]=="" && (s[i]==p[j1]  p[j1]==".")
初始状态:
{ d p [ 0 ] [ 0 ] = t r u e d p [ 0 ] [ j ] = d p [ 0 ] [ j − 2 ] , p [ j ] = = ∗ d p [ i ] [ 0 ] = f a l s e \begin{cases} dp[0][0] = true\\ dp[0][j] = dp[0][j-2], &p[j] == *\\ dp[i][0] = false\\ \end{cases} dp[0][0]=truedp[0][j]=dp[0][j2],dp[i][0]=falsep[j]==

代码
class Solution {
    public boolean isMatch(String s, String p) {
        int m = s.length(), n = p.length();
        boolean[][] dp = new boolean[m+1][n+1];
        dp[0][0] = true;
        for (int i = 1; i <= n; i++) {
            if (p.charAt(i - 1) == '*' && i > 1) {
                dp[0][i] = dp[0][i-2];
            }
        }
        
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (s.charAt(i - 1) == p.charAt(j - 1) || p.charAt(j - 1) == '.') {
                    dp[i][j] = dp[i-1][j-1];
                } 
                if (p.charAt(j - 1) == '*') {
                    // 0次
                    if (j > 1) {
                        dp[i][j] = dp[i][j-2];
                    }
                    // 多次
                    if (j > 1 && (s.charAt(i - 1) == p.charAt(j - 2) || p.charAt(j - 2) == '.')) {
                        dp[i][j] |= dp[i-1][j];
                    }
                }
            }
        }
        return dp[m][n];
    }
}

19. 表示数值的字符串

题目描述

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。

数值(按顺序)可以分成以下几个部分:

  1. 若干空格
  2. 一个 小数 或者 整数
  3. (可选)一个 ‘e’ 或 ‘E’ ,后面跟着一个 整数
  4. 若干空格

小数(按顺序)可以分成以下几个部分:

  1. (可选)一个符号字符(’+’ 或 ‘-’)
  2. 下述格式之一:
    a. 至少一位数字,后面跟着一个点 ‘.’
    b. 至少一位数字,后面跟着一个点 ‘.’ ,后面再跟着至少一位数字
    c. 一个点 ‘.’ ,后面跟着至少一位数字

整数(按顺序)可以分成以下几个部分:

  1. (可选)一个符号字符(’+’ 或 ‘-’)
  2. 至少一位数字

部分数值列举如下:

  • ["+100", “5e2”, “-123”, “3.1416”, “-1E-16”, “0123”]

部分非数值列举如下:

  • [“12e”, “1a3.14”, “1.2.3”, “±5”, “12e+5.4”]

示例 1:

输入:s = “0”
输出:true

示例 2:

输入:s = “e”
输出:false

分析
  1. 有限状态机
    定义所有合法的转移状态。
代码
def isNumber(self, s: str) -> bool:
    """
    s: +-
    d: 0-9
    e: e
    .: .
    _: _
    ?: unknown
    ["+100", "5e2", "-123", "3.1416", "-1E-16", "0123"]
    """
    states = [
        {' ': 0, 's': 1, 'd': 2, '.': 4},  # 0 开始的空格
        {'d': 2, '.': 4},                  # 1 e之前的符号
        {'d': 2, '.': 3, 'e': 5, ' ': 8},  # 2 e之前的数字
        {'d': 3, 'e': 5, ' ': 8},          # 3 e之前的小数点及数字
        {'d': 3},                          # 4 当小数点前为空格时,小数点及数字
        {'s': 6, 'd': 7},                  # 5 e
        {'d': 7},                          # 6 e之后的符号
        {'d': 7, ' ': 8},                  # 7 e之后的数字
        {' ': 8}                           # 8 末尾的空格
    ]

    p = 0
    for c in s:
        if c == ' ' or c == '.' or c == 'e':
            pass
        elif c == 'E':
            c = 'e'
        elif '0' <= c <= '9':
            c = 'd'
        elif c == '+' or c == '-':
            c = 's'
        else:
            c = '?'
        if c not in states[p]:
            return False
        p = states[p][c]
    return p in {2, 3, 7, 8}

20. 调整数组顺序使奇数位于偶数前面

题目描述

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。

示例:

输入:nums = [1,2,3,4]
输出:[1,3,2,4]
注:[3,1,2,4] 也是正确的答案之一。

分析
  1. 双指针。慢指针指向第一个偶数。不保证相对顺序。
  2. 前后双指针,利用O(n)的空间,保证相对顺序。
    前后两个指针 head 和 tail,head寻找奇数,放在结果数组前面位置;tail寻找偶数,放在结果集后面位置。
代码
  • 方法一
class Solution {
    public int[] exchange(int[] nums) {
        int p1 = 0, n = nums.length;
        for (int i = 0; i < n; i++) {
            if (nums[i] % 2 == 1) {
                swap(nums, i, p1);
                p1++;
            }
        }
       return nums;
    }
    
    private void swap(int[] nums, int i, int j) {
        int t = nums[i];
        nums[i] = nums[j];
        nums[j] = t;
    }
}
  • 方法二
public int[] reOrderArray (int[] array) {
    // write code here
    int n = array.length;
    int[] res = new int[n];
    int head = 0, tail = n - 1;
    int i = 0, j = n - 1;
    while (head < n && tail >= 0) {
        if (array[head] % 2 == 1) {
            res[i++] = array[head];
        }
        head++;
        if (array[tail] % 2 == 0) {
            res[j--] = array[tail];
        }
        tail--;
        if (i > j) {
            break;
        }
    }
    return res;
}

21. 链表中倒数第k个节点

题目描述

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。

示例:

给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.

分析
  1. 快慢指针。快指针先走k步,然后慢指针和快指针一起走,当快指针到达链表末尾时,慢指针正好是倒数第k个节点。
代码
class Solution {
    public ListNode getKthFromEnd(ListNode head, int k) {
        // 1 2 3 4 5
        if (head == null) {
            return null;
        }
        ListNode fast = head, slow = head;
        for (int i = 0; i < k; i++) {
            fast = fast.next;
        }
        while (fast != null) {
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }
}

22. 反转链表

题目描述

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

分析
  1. 递归。
  2. 修改指针。
  3. 移动节点。

方法详细介绍参看另一篇文章「反转链表系列问题总结(多种解法多种题型)

代码
  • 方法一
class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode p = reverseList(head.next);
        head.next.next = head;
        head.next = null;
        return p;
    }
}
  • 方法二
class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode dummy = new ListNode(-1), cur = head, next;
        dummy.next = cur;
        while (cur != null) {
            next = cur.next;
            cur.next = dummy;
            dummy = cur;
            cur = next;
        }
        head.next = null;
        return dummy;
    }
}
  • 方法三
class Solution {
    public ListNode reverseList(ListNode head) {
        // -1 1 2 3 4 5
        if (head == null || head.next == null) {
            return head;
        }
        ListNode dummy = new ListNode(-1), n1, n2;
        dummy.next = head;
        while (head.next != null) {
            n1 = dummy.next;
            n2 = head.next;
            head.next = n2.next;
            dummy.next = n2;
            n2.next = n1;
        }
        return dummy.next;
    }
}

23. 合并两个排序的链表

题目描述

输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。

示例1:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

分析
  1. 归并排序中的合并两个链表的思想。
代码
class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(-1);
        ListNode first = dummy;

        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                dummy.next = l1;
                l1 = l1.next;
            } else {
                dummy.next = l2;
                l2 = l2.next;
            }
            dummy = dummy.next;
        }
        dummy.next = l1 != null ? l1 : l2;
        return first.next;
    }
}

24. 树的子结构

题目描述

输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)

B是A的子结构, 即 A中有出现和B相同的结构和节点值。

例如:
给定的树 A:

     3
     / \
   4   5
  /  \
1    2

给定的树 B:

   4
  /
1

返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。

示例 1:

输入:A = [1,2,3], B = [3,1]
输出:false

分析
  1. 递归。
代码
class Solution {
    public boolean isSubStructure(TreeNode A, TreeNode B) {
        if (B == null || A == null) {
            return false;
        }
        if (A.val == B.val && isSub(A, B)) {
            return true;
        }
        return isSubStructure(A.left, B) || isSubStructure(A.right, B);
    }
    
    private boolean isSub(TreeNode A, TreeNode B) {
        if (B == null) {
            return true;
        }
        if (A == null || A.val != B.val) {
            return false;
        }
        
        return isSub(A.left, B.left) && isSub(A.right, B.right);
    }
}

25. 二叉树的镜像

题目分析

请完成一个函数,输入一个二叉树,该函数输出它的镜像。

例如输入:

       4
     /   \
   2     7
  /  \   /  \
1   3 6   9

镜像输出:

      4
     /  \
    7   2
  /  \   /  \
9   6 3   1

示例 1:

输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]

分析
  1. 递归
代码
class Solution {
    public TreeNode mirrorTree(TreeNode root) {
        if (root == null) return null;
        TreeNode left = mirrorTree(root.left);
        TreeNode right = mirrorTree(root.right);
        root.left = right;
        root.right = left;
        return root;
    }
}

26. 对称的二叉树

题目描述

请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。

例如,二叉树 [1,2,2,3,4,4,3] 是对称的。

       1
      /  \
    2    2
  /   \  /   \
3    4 4   3

但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:

  1
  / \
2   2
  \    \
   3   3

示例 1:

输入:root = [1,2,2,3,4,4,3]
输出:true

分析
  1. 递归。
代码
class Solution {
    public boolean isSymmetric(TreeNode root) {
        if (root == null) {
            return true;
        }
        return isSym(root.left, root.right);
    }
    
    private boolean isSym(TreeNode p, TreeNode q) {
        if (p == null && q == null) {
            return true;
        }
        if (p == null || q == null || p.val != q.val) {
            return false;
        }
        return isSym(p.left, q.right) && isSym(p.right, q.left);
        
    }
}

27. 顺时针打印矩阵

题目描述

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。

示例 1:

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]

示例 2:

输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]

分析
  1. 模拟。记录遍历的方向和已经遍历过的位置。
代码
class Solution {
    public int[] spiralOrder(int[][] matrix) {
        if (matrix.length == 0 || matrix[0].length == 0) {
            return new int[0];
        }
        int[] dx = {0, 1, 0, -1};
        int[] dy = {1, 0, -1, 0};
        int orient = 0, i = 0, x = 0, y = 0;
        int m = matrix.length, n = matrix[0].length;
        int[] res = new int[m * n];
        boolean[][] record = new boolean[m][n];
        while (i < m * n) {
            res[i++] = matrix[x][y];
            record[x][y] = true;
            int x1 = x + dx[orient], y1 = y + dy[orient];
            if (x1 >= 0 && x1 < m && y1 >= 0 && y1 < n && !record[x1][y1]) {
                x = x1;
                y = y1;
            } else {
                orient = (orient + 1) % 4;
                x += dx[orient];
                y += dy[orient];
            }
        }
        return res;
    }
}

28. 包含min函数的栈

题目描述

定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。

示例:

MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.min(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.min(); --> 返回 -2.

分析
  1. 维护两个栈,一个正常用来存放压入和弹出的数据,另一个单调递减栈,栈顶始终是当前的最小元素。
代码
class MinStack {

    LinkedList<Integer> stack;
    LinkedList<Integer> minStack;

    /** initialize your data structure here. */
    public MinStack() {
        stack = new LinkedList<>();
        minStack = new LinkedList<>();
    }

    public void push(int x) {
        stack.push(x);
        if (minStack.isEmpty() || x <= minStack.peek()) {
            minStack.push(x);
        }
    }

    public void pop() {
        if (stack.isEmpty()) {
            return;
        }
        int val = stack.poll();
        if (!minStack.isEmpty() && minStack.peek() == val) {
            minStack.poll();
        }
    }

    public int top() {
        if (stack.isEmpty()) {
            return -1;
        }
        return stack.peek();
    }

    public int min() {
        if (minStack.isEmpty()) {
            return -1;
        }
        return minStack.peek();
    }
}

29. 栈的压入、弹出序列

题目描述

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {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

分析
  1. 模拟。
    模拟栈的压入、弹出操作
    一个指针记录弹出栈的位置,压入后,当栈顶元素与弹出栈元素相等时,则弹出,弹出栈位置加一。最后看栈是否为空。
代码
class Solution {
    public boolean validateStackSequences(int[] pushed, int[] popped) {
        LinkedList<Integer> stack = new LinkedList<>();
        int i = 0;
        for (int val : pushed) {
            stack.push(val);
            while (!stack.isEmpty() && stack.peek() == popped[i]) {
                stack.poll();
                i++;
            }
        }
        return stack.size() == 0;
    }
}

30. 从上到下打印二叉树

题目描述

从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。

例如:
给定二叉树: [3,9,20,null,null,15,7],

   3
  /  \
9   20
     /   \
   15   7

返回:

[3,9,20,15,7]

分析
  1. 层序遍历。借助队列。
代码
class Solution {
    public int[] levelOrder(TreeNode root) {
        if (root == null) {
            return new int[0];
        }
        List<Integer> res = new ArrayList<>();
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            res.add(node.val);
            if (node.left != null) {
                queue.add(node.left);
            }
            if (node.right != null) {
                queue.add(node.right);
            }
        }
        int[] r = new int[res.size()];
        for (int i = 0; i < res.size(); i++) {
            r[i] = res.get(i);
        }
        return r;
    }
}

31. 从上到下打印二叉树 II

题目描述

从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。

例如:
给定二叉树: [3,9,20,null,null,15,7],

   3
  /  \
9   20
     /   \
  15    7

返回其层次遍历结果:

[
    [3],
    [9, 20],
    [15, 7]
]

分析
  1. 层序遍历。每层的结果单独保存。
代码
class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        if (root == null) {
            return new ArrayList<>();
        }
        List<List<Integer>> res = new ArrayList<>();
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);

        while (!queue.isEmpty()) {
            List<Integer> tmp = new ArrayList<>();
            int qSize = queue.size();
            for (int i = 0; i < qSize; i++) {
                TreeNode node = queue.poll();
                tmp.add(node.val);
                if (node.left != null) {
                    queue.add(node.left);
                }
                if (node.right != null) {
                    queue.add(node.right);
                }
            }
            res.add(tmp);
        }
        return res;
    }
}

32. 从上到下打印二叉树 III

题目描述

请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。

例如:
给定二叉树: [3,9,20,null,null,15,7],

   3
  /  \
9   20
     /   \
  15    7

返回其层次遍历结果:

[
     [3],
     [20, 9],
     [15, 7]
]

分析
  1. 层序遍历。遇到偶数层就反转列表保存到结果集。
代码
class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        if (root == null) {
            return new ArrayList<>();
        }
        List<List<Integer>> res = new ArrayList<>();
        Queue<TreeNode> queue = new LinkedList<>();
        int layer = 0;
        queue.add(root);
        while (!queue.isEmpty()) {
            List<Integer> tmp = new ArrayList<>();
            int qsize = queue.size();
            for (int i = 0; i < qsize; i++) {
                TreeNode node = queue.poll();
                tmp.add(node.val);
                if (node.left != null) {
                    queue.add(node.left);
                }
                if (node.right != null) {
                    queue.add(node.right);
                }
            }
            layer++;
            if ((layer & 1) == 0) {
                Collections.reverse(tmp);
            }
            res.add(tmp);
        }
        return res;
    }
}

33. 二叉搜索树的后序遍历序列

题目描述

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。

参考以下这颗二叉搜索树:

      5
     /   \
   2     6
  /   \
1     3

示例 1:

输入: [1,6,3,2,5]
输出: false

分析
  1. 递归。先看以最后一个节点为根的树,再看左树[0, m - 1],再看右树[m, j]。
代码
class Solution {
    public boolean verifyPostorder(int[] postorder) {
        return verify(postorder, 0, postorder.length - 1);
    }
    
    private boolean verify(int[] postorder, int start, int end) {
        if (start >= end) {
            return true;
        }
        int root = postorder[end];
        int i = start, splitIndex;
        for (; i < end; i++) {
            if (postorder[i] > root) {
                break;
            }
        }
        splitIndex = i;
        for (; i< end; i++) {
            if (postorder[i] < root) {
                return false;
            }
        }
        return verify(postorder, start, splitIndex - 1) && verify(postorder, splitIndex, end - 1);
    }
}

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

题目描述

输入一棵二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。

示例:
给定如下二叉树,以及目标和 target = 22,

          5
         / \
        4   8
       /   / \
      11  13  4
     /  \    / \
    7    2  5   1

返回:

[
    [5,4,11,2],
    [5,8,4,5]
]

分析
  1. 回溯。
代码
class Solution {
    List<List<Integer>> res;
    int target;
    List<Integer> tmp;
    public List<List<Integer>> pathSum(TreeNode root, int target) {
        res = new ArrayList<>();
        this.target = target;
        tmp = new ArrayList<>();
        if (root == null) {
            return res;
        }
        tmp.add(root.val);
        helper(root, root.val);
        tmp.remove(tmp.size() - 1);
        return res;
    }

    private void helper(TreeNode root, int total) {
        if (root == null) return;
        if (total == target && root.left == null && root.right == null) {
            res.add(new ArrayList<>(tmp));
            return;
        }
        if (root.left != null) {
            tmp.add(root.left.val);
            helper(root.left, total + root.left.val);
            tmp.remove(tmp.size() - 1);
        }
        if (root.right != null) {
            tmp.add(root.right.val);
            helper(root.right, total + root.right.val);
            tmp.remove(tmp.size() - 1);
        }
    }
}

35. 复杂链表的复制

题目描述

请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。

在这里插入图片描述

示例 1:

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

分析
  1. HashMap。存储每个节点到新节点的映射,然后建立next、random指针。
  2. 三步走。1.复制;2.建立随机指针;3.断开
代码
  • 方法一
class Solution {
    public Node copyRandomList(Node head) {
        Map<Node, Node> map = new HashMap<>();
        Node cur = head;
        while (cur != null) {
            if (!map.containsKey(cur)) {
                map.put(cur, new Node(cur.val));
            }
            cur = cur.next;
        }

        cur = head;
        while (cur != null) {
            Node node = map.get(cur);
            if (cur.next != null) {
                node.next = map.get(cur.next);
            }
            if (cur.random != null) {
                node.random = map.get(cur.random);
            }
            cur = cur.next;
        }
        return map.get(head);
    }
}
  • 方法二
class Solution {
    public Node copyRandomList(Node head) {
        Node cur = head;
        // 1 复制节点
        while (cur != null) {
            Node t = new Node(cur.val);
            Node next = cur.next;
            cur.next = t;
            t.next = next;
            cur = next;
        }
        
        cur = head;
        // 2 建立随机指针
        while (cur != null) {
            if (cur.random != null) {
                cur.next.random = cur.random.next;
            }
            cur = cur.next.next;
        }
        
        cur = new Node(-1);
        Node p = cur, next;
        // 3 断开新旧节点
        while (head != null) {
            next = head.next.next;
            cur.next = head.next;
            head.next = next;
            cur = cur.next;
            head = next;
        }
        return p.next;
    }
}

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

题目描述

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。

为了让您更好地理解问题,以下面的二叉搜索树为例:

在这里插入图片描述

我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。

下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点。

在这里插入图片描述

分析
  1. 中序遍历。这道题本质上就是二叉树的中序遍历,记录前一个遍历节点即可。
代码
class Solution {
    Node pre, head;
    public Node treeToDoublyList(Node root) {
        if (root == null) {
            return null;
        }
        pre = null;
        head = null;
        LinkedList<Node> stack = new LinkedList<>();
        while (!stack.isEmpty() || root != null) {
            while (root != null) {
                stack.push(root);
                root = root.left;
            }
            root = stack.poll();
            root.left = pre;
            if (pre == null) {
                head = root;
            } else {
                pre.right = root;
            }
            pre = root;
            root = root.right;
        }
        pre.right = head;
        head.left = pre;
        return head;
    }
}

37. 序列化二叉树

题目描述

请实现两个函数,分别用来序列化和反序列化二叉树。

示例:

你可以将以下二叉树:

   1
  /  \
2    3
     /   \
   4     5

序列化为 “[1,2,3,null,null,4,5]”

分析
  1. 层序遍历。记录每个节点的左右空节点。
代码
public class Codec {

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        if (root == null) return "";
        StringBuilder builder = new StringBuilder();
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            if (node == null) {
                builder.append("n,");
                continue;
            }
            builder.append(node.val).append(",");
            queue.add(node.left);
            queue.add(node.right);
        }
        builder.deleteCharAt(builder.length() - 1);
        return builder.toString();
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        if (data.length() == 0) return null;
        String[] vals = data.split(",");
        TreeNode root = new TreeNode(Integer.parseInt(vals[0]));
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        int index = 1;
        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            if (!"n".equals(vals[index])) {
                TreeNode left = new TreeNode(Integer.parseInt(vals[index++]));
                node.left = left;
                queue.add(left);
            } else {
                index++;
            }
            if (!"n".equals(vals[index])) {
                TreeNode right = new TreeNode(Integer.parseInt(vals[index++]));
                node.right = right;
                queue.add(right);
            } else {
                index++;
            }
        }
        return root;
    }
}

38. 字符串的排列

题目描述

输入一个字符串,打印出该字符串中字符的所有排列。

你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

示例:

输入:s = “abc”
输出:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]

分析
  1. 回溯。
代码
class Solution {
    char[] chars;
    List<String> res;
    public String[] permutation(String s) {
        chars = s.toCharArray();
        res = new ArrayList<>();
        per(0);
        return res.toArray(new String[0]);
    }

    private void per(int index) {
        if (index == chars.length - 1) {
            res.add(String.valueOf(chars));
            return;
        }

        Set<Character> record = new HashSet<>();
        for (int i = index; i < chars.length; i++) {
            if (record.contains(chars[i])) {
                continue;
            }
            record.add(chars[i]);
            swap(i, index);
            per(index + 1);
            swap(i, index);
        }
    }

    private void swap(int i, int j) {
        char t = chars[i];
        chars[i] = chars[j];
        chars[j] = t;
    }
}

39. 数组中出现次数超过一半的数字

题目描述

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2

分析
  1. 投票法。
代码
class Solution {
    public int majorityElement(int[] nums) {
        if (nums.length == 0) {
            return -1;
        }
        int num = nums[0], count = 1;
        for (int n : nums) {
            if (n == num) {
                count++;
            } else {
                if (count == 1) {
                    num = n;
                } else {
                    count--;
                }
            }
        }
        return num;
    }
}

40. 最小的k个数

题目描述

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

示例 1:

输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]

分析
  1. 大顶堆。维护大小为k的大顶堆,遍历元素,小于堆顶元素时入堆,并弹出堆顶元素,最终堆中k个数为最小k个数。时间复杂度O(nlogk)
  2. 快排。根据快排思想,每次遍历长度减少一半,时间复杂度为O(n)
代码
class Solution {
    int k;
    public int[] getLeastNumbers(int[] arr, int k) {
        this.k = k;
        quickSort(arr, 0, arr.length - 1);
        int[] res = new int[k];
        for (int i = 0; i < k; i++) {
            res[i] = arr[i];
        }
        return res;
    }
    
    private void quickSort(int[] arr, int start, int end) {
        if (start >= end) {
            return;
        }
        int base = arr[start];
        int i = start, j = end;
        while (i < j) {
            while (i < j && arr[j] >= base) {
                j--;
            }
            if (i < j) {
                arr[i] = arr[j];
                i++;
            }
            while (i < j && arr[i] <= base) {
                i++;
            }
            if (i < j) {
                arr[j] = arr[i];
                j--;
            }
        }
        arr[i] = base;
        if (i + 1 == k) {
            return;
        } else if (i + 1 > k) {
            quickSort(arr, start, i - 1);
        } else {
            quickSort(arr, i + 1, end);
        }
    }
}

41. 数据流中的中位数

题目描述

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

例如,

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

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

设计一个支持以下两种操作的数据结构:

void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。
示例 1:

输入:

[“MedianFinder”,“addNum”,“addNum”,“findMedian”,“addNum”,“findMedian”]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]

分析
  1. 两个堆。一个大顶堆,存储前一半小的数;一个小顶堆,存储后一半大的数。
    时间复杂度:添加一个数:O(log n),求中位数:O(1)
代码
class MedianFinder {

    PriorityQueue<Integer> queue1;
    PriorityQueue<Integer> queue2;

    /** initialize your data structure here. */
    public MedianFinder() {
        queue1 = new PriorityQueue<>((x, y) -> y - x);
        queue2 = new PriorityQueue<>();
    }

    public void addNum(int num) {
        if (queue1.size() == queue2.size()) {
            queue2.add(num);
            queue1.add(queue2.poll());
        } else {
            queue1.add(num);
            queue2.add(queue1.poll());
        }
    }

    public double findMedian() {
        if (queue1.size() == queue2.size()) {
            return (queue1.peek() + queue2.peek()) / 2.0;
        } else {
            return queue1.peek();
        }
    }
}

42. 连续子数组的最大和

题目描述

输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

要求时间复杂度为O(n)。

示例1:

输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

分析
  1. dp。当前状态仅依赖前一个状态。
    dp[n] = max(dp[n - 1], 0) + nums[n]
代码
class Solution {
    public int maxSubArray(int[] nums) {
        int n = nums.length;
        if (n <= 0) {
            return -1;
        }
        int total = nums[0], max = total;
        for (int i = 1; i < n; i++) {
            int num = nums[i];
            if (total < 0) {
                total = num;
            } else {
                total += num;
            }
            max = Math.max(max, total);
        }
        return max;
    }
}

43. 1~n 整数中 1 出现的次数

题目描述

输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。

例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。

示例 1:

输入:n = 12
输出:5

分析
  1. 数学分析。

计算n中每一位为1时的数字个数之和。

定义高位hight,低位low,位数digit

分三种情况:

  • 该位为0时
    以2407为例,十位为1的数字个数为:
    0010~2319
    000~239
    总共为240个,即24*10
    h i g h ∗ d i g i t high*digit highdigit

  • 该位为1时
    以2417为例,十位为1的数字个数为:
    0010~2417
    000~247
    总共为248个,即24*10+7+1
    h i g h ∗ d i g i t + l o w + 1 high*digit+low+1 highdigit+low+1

  • 该位为其他数时
    以2427为例,十位为1的数字个数为:
    0010~2419
    000~249
    总共为250个,即(24 + 1)*10
    ( h i g h + 1 ) ∗ d i g i t (high+1)*digit (high+1)digit

代码
class Solution {
    public int countDigitOne(int n) {
        int high = n / 10, bit = 1, low = 0, val = n % 10, count = 0;
        while (high != 0 || val != 0) {
            if (val == 0) {
                count += (high * bit);
            } else if (val == 1) {
                count += high * bit + low + 1;
            } else {
                count += (high + 1) * bit;
            }
            
            low += val * bit;
            val = high % 10;
            high /= 10;
            bit *= 10;
            
        }
        return count;
    }
}

44. 数字序列中某一位的数字

题目描述

数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从下标0开始计数)是5,第13位是1,第19位是4,等等。

请写一个函数,求任意第n位对应的数字。

示例 1:

输入:n = 3
输出:3

分析
  1. 三步走。1.确定区间;2.确定哪个数;3.确定哪一位
代码
class Solution {
    public int findNthDigit(int n) {
        // 1.确定区间 2.确定第几个 3.确定第几位
        int bit = 1;
        long start = 1, count = 9;
        // 1.确定区间
        while (n > count) {
            n -= count;
            start *= 10;
            bit++;
            count = 9 * start * bit;
        }
        // 2.确定第几个
        long num = (n - 1) / bit + start;  // 减1是因为从0开始
        // 3.确定第几位
        int b = (n - 1) % bit;
        return Long.toString(num).charAt(b) - '0';
    }
}

45. 把数组排成最小的数

题目描述

输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

示例 1:

输入: [10,2]
输出: “102”

分析
  1. 排序。自定义排序标准。用快排实现。
代码
class Solution {
    public String minNumber(int[] nums) {
        int n = nums.length;
        String[] s = new String[n];
        for (int i = 0; i < n; i++) {
            s[i] = String.valueOf(nums[i]);
        }
        quickSort(s, 0, n - 1);
        return String.join("", s);
    }

    private void quickSort(String[] s, int start, int end) {
        if (start >= end) {
            return;
        }
        String base = s[start];
        int i = start, j = end;
        while (i < j) {
            while (i < j && (s[j] + base).compareTo(base + s[j]) > 0) {
                j--;
            }
            if (i < j) {
                s[i] = s[j];
                i++;
            }
            while (i < j && (s[i] + base).compareTo(base + s[i]) < 0) {
                i++;
            }
            if (i < j) {
                s[j] = s[i];
                j--;
            }
        }
        s[i] = base;
        quickSort(s, start, i - 1);
        quickSort(s, i + 1, end);
    }
}

46. 把数字翻译成字符串

题目描述

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

示例 1:

输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", “bwfi”, “bczi”, “mcfi"和"mzi”

分析
  1. dp。判断相邻两个数的组合是否合法。
代码
class Solution {
    public int translateNum(int num) {
        String s = String.valueOf(num);
        int n = s.length();
        int[] dp = new int[n + 1];
        dp[1] = 1;
        dp[0] = 1;
        for (int i = 1; i < n; i++) {
            if (isValid(s.charAt(i - 1), s.charAt(i))) {
                dp[i + 1] = dp[i] + dp[i-1]; 
            } else {
                dp[i + 1] = dp[i];
            }
        }
        return dp[n];

    }

    private boolean isValid(char c1, char c2) {
        if (c1 > '2' || c1 == '0') {
            return false;
        }
        if (c1 == '2' && c2 > '5') {
            return false;
        }
        return true;
    }
}

47. 礼物的最大价值

题目描述

在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

示例 1:

输入:

[
   [1,3,1],
   [1,5,1],
   [4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物

分析
  1. dp。由于当前状态 dp[i][j] 只依赖于 dp[i-1][j] 和 dp[i][j-1],如果两维 dp 可以优化为一维。
代码
class Solution {
    public int maxValue(int[][] grid) {
        if (grid.length == 0 || grid[0].length == 0) {
            return 0;
        }
        int m = grid.length, n = grid[0].length;
        int[] dp = new int[n + 1];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                dp[j] = Math.max(dp[j-1], dp[j]) + grid[i-1][j-1];
            }
        }
        return dp[n];
    }
}

48. 最长不含重复字符的子字符串

题目描述

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

示例 1:

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

分析
  1. HashMap。存储每个字符的位置,如果当前字符已经出现过,则判断当前长度是否包含前一个当前字符。
代码
class Solution {
    public int lengthOfLongestSubstring(String s) {
        Map<Character, Integer> map = new HashMap<>();
        int n = s.length();
        if (n == 0) {
            return 0;
        }
        int maxLen = 0, cur = 0;
        for (int i = 0; i < n; i++) {
            char c = s.charAt(i);
            if (!map.containsKey(c)) {
                cur++;
            } else {
                if (i - map.get(c) > cur) {
                    cur++;
                } else {
                    cur = i - map.get(c);
                }
            }
            map.put(c, i);
            maxLen = Math.max(maxLen, cur);
        }
        return maxLen;
    }
}

49. 丑数

题目描述

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

示例:

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

分析
  1. dp。用三个指针分别记录下一个乘以2,3,5的数。
代码
class Solution {
    public int nthUglyNumber(int n) {
        int[] res = new int[n];
        res[0] = 1;
        int p2 = 0, p3 = 0, p5 = 0, i = 1;
        while (i < n) {
            int v1 = res[p2] * 2;
            int v2 = res[p3] * 3;
            int v3 = res[p5] * 5;
            res[i] = Math.min(Math.min(v1, v2), v3);
            if (res[i] == v1) p2++;
            if (res[i] == v2) p3++;
            if (res[i] == v3) p5++;
            i++;
        }
        return res[n - 1];
    }
}

50. 第一个只出现一次的字符

题目描述

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。

示例:

s = “abaccdeff”
返回 “b”

分析
  1. HashMap+两次遍历。
代码
class Solution {
    public char firstUniqChar(String s) {
        Map<Character, Integer> map = new HashMap<>();
        int n = s.length();
        for (int i = 0; i < n; i++) {
            char c = s.charAt(i);
            map.put(c, map.getOrDefault(c, 0) + 1);
        }
        for (int i = 0; i < n; i++) {
            if (map.get(s.charAt(i)) == 1) {
                return s.charAt(i);
            }
        }
        return ' ';
    }
}

51. 数组中的逆序对

题目描述

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

示例 1:

输入: [7,5,6,4]
输出: 5

分析
  1. 归并排序
    left: [1,3,5,6,9] right [2,3,4,7]
    res: [1,2,3,3,4,5] 当指针到5时,right中2,3,4和5就是逆序对
代码
class Solution {
    int count;
    public int reversePairs(int[] nums) {
        count = 0;
        int n = nums.length;
        mergeSort(nums, new int[n], 0, n - 1);
        return count;
    }

    private void mergeSort(int[] nums, int[] tmp, int start, int end) {
        if (start >= end) {
            return;
        }
        int mid = start + (end - start) / 2;
        mergeSort(nums, tmp, start, mid);
        mergeSort(nums, tmp, mid + 1, end);
        int p = start, q = mid + 1, i = start;
        while (p <= mid && q <= end) {
            if (nums[p] <= nums[q]) {
                tmp[i++] = nums[p++];
                count += (q - mid - 1);
            } else {
                tmp[i++] = nums[q++];
            }
        }
        for (int k = p; k <= mid; k++) {
            tmp[i++] = nums[k];
            count += (end - mid);
        }
        for (int k = q; k <= end; k++) {
            tmp[i++] = nums[k];
        }
        for (int k = start; k <= end; k++) {
            nums[k] = tmp[k];
        }
    }
}

52. 两个链表的第一个公共节点

题目描述

输入两个链表,找出它们的第一个公共节点。

如下面的两个链表:

在这里插入图片描述

在节点 c1 开始相交。

分析
  1. 双指针。两个指针A和B同时出发,当一个指针A到达终点时,指向B的起点继续走,当B到达终点时,指向A的起点,第一个相遇的点就是所求的节点。
代码
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode p1 = headA, p2 = headB;
        boolean f1 = false, f2 = false;
        while (p1 != null && p2 != null) {
            if (p1 == p2) {
                return p1;
            }
            p1 = p1.next;
            p2 = p2.next;
            if (p1 == null && !f1) {
                p1 = headB;
                f1 = true;
            } else if (p2 == null && !f2) {
                p2 = headA;
                f2 = true;
            }
        }
        return null;
    }
}

53. 在排序数组中查找数字 I

题目描述

统计一个数字在排序数组中出现的次数。

示例 1:

输入: nums = [5,7,7,8,8,10], target = 8
输出: 2

分析
  1. 二分查找。两次二分分别查找左右端点。
代码
class Solution {
    public int search(int[] nums, int target) {
        int n = nums.length;
        if (n == 0) {
            return 0;
        }
        int i = 0, j = n - 1, mid;
        while (i < j) {
            mid = i + (j - i) / 2;
            if (nums[mid] < target) {
                i = mid + 1;
            } else {
                j = mid;
            }
        }
        int left = i;
        if (nums[left] != target) {
            return 0;
        }
        j = n - 1;
        while (i < j) {
            mid = i + (j - i + 1) / 2;
            if (nums[mid] > target) {
                j = mid - 1;
            } else {
                i = mid;
            }
        }
        return i - left + 1;
    }
}

54. 0~n-1中缺失的数字

题目描述

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

示例 1:

输入: [0,1,3]
输出: 2

分析
  1. 二分查找。
代码
class Solution {
    public int missingNumber(int[] nums) {
        int n = nums.length;
        int i = 0, j = n, mid;
        while (i < j) {
            mid = i + (j - i) / 2;
            if (nums[mid] == mid) {
                i = mid + 1;
            } else {
                j = mid;
            }
        }
        return i;
    }
}

55. 二叉搜索树的第k大节点

题目描述

给定一棵二叉搜索树,请找出其中第k大的节点。

示例 1:

输入: root = [3,1,4,null,2], k = 1
    3
  /   \
1     4
  \
   2
输出: 4

分析
  1. 中序遍历。先右子树再左子树的中序遍历,记录遍历的节点数。
代码
class Solution {
    public int kthLargest(TreeNode root, int k) {
        int count = 0;
        LinkedList<TreeNode> stack = new LinkedList<>();
        stack.push(root);
        while (!stack.isEmpty() || root != null) {
            while (root != null) {
                stack.push(root);
                root = root.right;
            }
            //
            root = stack.poll();
            count += 1;
            if (count == k) {
                return root.val;
            }
            root = root.left;
        }
        return -1;
    }
}

56. 二叉树的深度

题目描述

输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。

例如:

给定二叉树 [3,9,20,null,null,15,7],

   3
  /   \
9    20
      /   \
   15    7

返回它的最大深度 3 。

分析
  1. 递归。
代码
class Solution {
    public int maxDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
    }
}

57. 平衡二叉树

题目描述

输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。

示例 1:

给定二叉树 [3,9,20,null,null,15,7]

   3
  /   \
9    20
      /   \
   15    7

返回 true 。

分析
  1. 递归。
代码
class Solution {
    public boolean isBalanced(TreeNode root) {
        if (root == null) return true;
        return isBalanced(root.left) && isBalanced(root.right) && Math.abs(deep(root.left) - deep(root.right)) <= 1;
    }

    private int deep(TreeNode root) {
        if (root == null) {
            return 0;
        }
        return 1 + Math.max(deep(root.left), deep(root.right));

    }
}

58. 数组中数字出现的次数

题目描述

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

示例 1:

输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]

分析
  1. 异或
    思路:要得到两个只出现一次的数a,b,即将数组分成两组,这a,b分别出现在两组中。
    1 遍历并异或整个数组,得到x;
    2 因为x不能为零,因此其二进制中总是存在某一位为1,找出为1的任意一位,得到div,如:1000;
    3 遍历整个数组,将每个元素与div相与,不为0的分为1组,其他分为1组,就得到了两组。同时遍历过程中进行异或运算,同时也得到了答案。
代码
class Solution {
    public int[] singleNumbers(int[] nums) {
        int a = 0;
        for (int num : nums) {
            a ^= num;
        }
        
        int b = 1;
        while ((a & b) == 0) {
            b <<= 1;
        }
        
        int x = 0, y = 0;
        for (int num : nums) {
            if ((num & b) != 0) {
                x ^= num;
            } else {
                y ^= num;
            }
        }
        return new int[]{x, y};
    }
}

59. 数组中数字出现的次数 II

题目描述

在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

示例 1:

输入:nums = [3,4,3,3]
输出:4

分析
  1. 统计。用一个长度为32的数组记录每位1的个数,然后对每位除3取余就得到出现一次的数在该位的值。
代码
class Solution {
    public int singleNumber(int[] nums) {
        int[] bitCount = new int[32];
        for (int num: nums) {
            for (int i = 0; i < 32; i++) {
                if (((1 << i) & num) != 0) {
                    bitCount[i]++;
                }
            }
        }
        int res = 0;
        for (int i = 0; i < 32; i++) {
            if (bitCount[i] % 3 != 0) {
                res += (1 << i);
            }
        }
        return res;
    }
}

60. 和为s的两个数字

题目描述

输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]

分析
  1. 双指针。
代码
class Solution {
    public int[] twoSum(int[] nums, int target) {
        int n = nums.length, i = 0, j = n - 1;
        while (i < j) {
            int total = nums[i] + nums[j];
            if (total < target) {
                i++;
            } else if (total > target) {
                j--;
            } else {
                return new int[]{nums[i], nums[j]};
            }
        }
        return new int[]{-1, -1};
    }
}

61. 和为s的连续正数序列

题目描述

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

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

示例 1:

输入:target = 9
输出:[[2,3,4],[4,5]]

分析
  1. 双指针。
    开始:left = 1, right = 2
    sum > target, left++;
    sum < target, right++;
    加入答案集合。
    终止条件为l==r。
代码
class Solution {
    public int[][] findContinuousSequence(int target) {
        List<int[]> res = new ArrayList<>();
        int i = 1, j = 2, total;
        while (i < j) {
            total = (i + j) * (j - i + 1) / 2;
            if (total == target) {
                res.add(generateArray(i, j));
                i++;
            } else if (total < target) {
                j++;
            } else {
                i++;
            }
        }
        int[][] result = new int[res.size()][];
        for (int k = 0; k < res.size(); k++) {
            result[k] = res.get(k);
        }
        return result;
    }

    private int[] generateArray(int i, int j) {
        int[] res = new int[j - i + 1];
        for (int k = i; k <= j; k++) {
            res[k - i] = k;
        }
        return res;
    }
}

62. 翻转单词顺序

题目描述

输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. “,则输出"student. a am I”。

示例 1:

输入: “the sky is blue”
输出: “blue is sky the”

分析
  1. 逐字符检查。
代码
class Solution {
    public String reverseWords(String s) {
        List<String> words = new ArrayList<>();
        int i = 0, n = s.length();
        while (i < n) {
            while (i < n && s.charAt(i) == ' ') i++;
            if (i == n) break;
            int start = i;
            while (i < n && s.charAt(i) != ' ') i++;
            words.add(s.substring(start, i));
        }
        Collections.reverse(words);
        return String.join(" ", words);
    }
}

63. 左旋转字符串

题目描述

字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。

示例 1:

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

分析
  1. substring
  2. 三次翻转。先翻转整个字符串s,再翻转s[:k],在翻转s[k+1:]
代码
class Solution {
    public String reverseLeftWords(String s, int n) {
        int l = s.length();
        return s.substring(n, l) + s.substring(0, n);
    }
}

64. 滑动窗口的最大值

题目描述

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

示例:

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:
滑动窗口的位置    最大值
---------------            -----
[1 3 -1] -3 5 3 6 7    3
1 [3 -1 -3] 5 3 6 7    3
1 3 [-1 -3 5] 3 6 7    5
1 3 -1 [-3 5 3] 6 7    5
1 3 -1 -3 [5 3 6] 7    6
1 3 -1 -3 5 [3 6 7]    7

分析
  1. 单调递减双向队列。
    对于这道题,需要记录滑动窗口内的最大值,则维护一个单调递减双向队列。遍历nums,如果当前num大于队列尾的元素,则弹出所有大于num的数;同时如果当前元素等于队列首的元素,则poll队列首的元素。
代码
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums.length == 0) {
            return new int[0];
        }
        Deque<Integer> deque = new LinkedList<>();
        for (int i = 0; i < k; i++) {
            while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
                deque.pollLast();
            }
            deque.addLast(i);
        }

        int n = nums.length;
        int[] res = new int[n - k + 1];
        res[0] = nums[deque.peekFirst()];

        for (int i = k; i < n; i++) {
            if (deque.peekFirst() == i - k) {
                deque.pollFirst();
            }
            while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
                deque.pollLast();
            }
            deque.addLast(i);
            res[i - k + 1] = nums[deque.peekFirst()];
        }
        return res;
    }
}

65. 队列的最大值

题目描述

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。

若队列为空,pop_front 和 max_value 需要返回 -1

示例 1:

输入:
[“MaxQueue”,“push_back”,“push_back”,“max_value”,“pop_front”,“max_value”]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]

分析
  1. 双向递减队列。和上题类似,维护一个双向单调递减队列。
代码
class MaxQueue {

    Deque<Integer> deque;
    Deque<Integer> maxDeque;
    public MaxQueue() {
        deque = new LinkedList<>();
        maxDeque = new LinkedList<>();
    }

    public int max_value() {
        if (maxDeque.isEmpty()) {
            return -1;
        }
        return maxDeque.peekFirst();
    }

    public void push_back(int value) {
        deque.addLast(value);
        while (!maxDeque.isEmpty() && maxDeque.peekLast() < value) {
            maxDeque.pollLast();
        }
        maxDeque.addLast(value);
    }

    public int pop_front() {
        if (deque.isEmpty()) {
            return -1;
        }
        int val = deque.pollFirst();
        if (!maxDeque.isEmpty() && maxDeque.peekFirst() == val) {
            maxDeque.pollFirst();
        }
        return val;
    }
}

66. n个骰子的点数

题目描述

把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。

你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。

示例 1:

输入: 1
输出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]

分析
  1. 动规
    总次数:6^n
    dp[i][j]:i个骰子点数为j的次数
    dp[i][j] = ∑dp[i-1][j-k]
代码
class Solution {
    public double[] dicesProbability(int n) {
        int[][] dp = new int[n + 1][6 * n + 1];
        for (int i = 1; i <=  6; i++) {
            dp[1][i] = 1;
        }
        for (int i = 2; i <= n; i++) {
            for (int j = i; j <= i * 6; j++) {
                for (int k = 1; k <= 6; k++) {
                    if (j - k >= i - 1) dp[i][j] += dp[i-1][j-k];
                }
            }
        }
        double base = Math.pow(6, n);
        double[] res = new double[6 * n - n + 1];
        for (int i = n; i <= 6*n; i++) {
            res[i - n] = dp[n][i] / base;
        }
        return res;
    }
}

67. 扑克牌中的顺子

题目描述

从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。

示例 1:

输入: [1,2,3,4,5]
输出: True

分析
  1. 计算剩余0个数。
    满足:
    1)0的数量大于等于补的数量
    2)无重复
代码
class Solution {
    public boolean isStraight(int[] nums) {
        Arrays.sort(nums);
        int zeros = 0;
        for (int i = 0; i < 5; i++) {
            if (nums[i] == 0) {
                zeros++;
            } else if (i > 0 && nums[i - 1] != 0 && nums[i] == nums[i - 1]) {
                return false;
            } else if (i > 0 && nums[i - 1] != 0) {
                zeros -= (nums[i] - nums[i - 1] - 1);
            }
        }
        return zeros >= 0;
    }
}

68. 圆圈中最后剩下的数字

题目描述

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

分析
  1. 递归。
    约瑟夫环
    0~n-1排成圆圈,删除第m个数,最后剩下的数字是f(n, m)
    n-1个数,删除第m个数最后剩下的是f(n-1, m) = x
    假设n个数,删除第m个数后剩下了n-1个数,因此从m+1个数开始删除第二个数。。。
    而f(n-1, m)是从第0个数开始删除的,所以 (x+m)%n 就得到了答案
代码
class Solution {
    public int lastRemaining(int n, int m) {
        if (n == 1) {
            return 0;
        }
        int p = lastRemaining(n - 1, m);
        return (p + m) % n;
    }
}

69. 股票的最大利润

题目描述

假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?

示例 1:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

分析
  1. 一次遍历。记录当前的最小值,更新最大收益。
代码
class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        if (n <= 1) {
            return 0;
        }
        int minPrice = prices[0], res = 0;
        for (int i = 1; i < n; i++) {
            minPrice = Math.min(minPrice, prices[i]);
            res = Math.max(res, prices[i] - minPrice);
        }
        return res;
    }
}

70. 求1+2+…+n

题目描述

求 1+2+…+n ,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

示例 1:

输入: n = 3
输出: 6

分析
  1. 短路效应。
代码
class Solution {
    public int sumNums(int n) {
        boolean f = n >= 1 && (n += sumNums(n - 1)) > 0; 
        return n;
    }
}

71. 不用加减乘除做加法

题目描述

写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。

示例:

输入: a = 1, b = 1
输出: 2

分析
  1. 位运算。
    a ^ b 得到非进位和
    a & b 得到进位和,由于是进位,因此需要左移。
代码
class Solution {
    public int add(int a, int b) {
        while (b != 0) {
            int c = (a & b) << 1;
            a ^= b;
            b = c;
        }
        return a;
    }
}

72. 构建乘积数组

题目描述

给定一个数组 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]。不能使用除法。

示例:

输入: [1,2,3,4,5]
输出: [120,60,40,30,24]

分析
  1. 前缀积和后缀积。
代码
class Solution {
    public int[] constructArr(int[] a) {
        int n = a.length;
        int[] res = new int[n];
        if (n == 0) {
            return res;
        }
        int total = 1;
        for (int i = 0; i < n; i++) {
            res[i] = total;
            total *= a[i];
        }
        total = 1;
        for (int i = n - 1; i >= 0; i--) {
            res[i] *= total;
            total *= a[i];
        }
        return res;
    }
}

73. 把字符串转换成整数

题目描述


写一个函数 StrToInt,实现把字符串转换成整数这个功能。不能使用 atoi 或者其他类似的库函数。

首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。

当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。

该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。

注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。

在任何情况下,若函数不能进行有效的转换时,请返回 0。

说明:

假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231, 231 − 1]。如果数值超过这个范围,请返回 INT_MAX (231 − 1) 或 INT_MIN (−231) 。

示例 1:

输入: “42”
输出: 42

分析
  1. 逐字符判断。
代码
class Solution {
    public int strToInt(String str) {
        int i = 0, n = str.length();
        // 去除前面空格
        while (i < n && str.charAt(i) == ' ') {
            i++;
        }
        int sign = 1;
        if (i < n) {
            if (str.charAt(i) == '-') {
                sign = -1;
                i++;
            } else if (str.charAt(i) == '+') {
                i++;
            }
        }
        int left = i;
        long val = 0;
        while (i < n) {
            char c = str.charAt(i);
            if (c < '0' || c > '9') {
                break;
            }
            val = val * 10 + (c - '0');
            i++;
            if (sign == 1 && val > Integer.MAX_VALUE) {
                return Integer.MAX_VALUE;
            } else if (sign == -1 && val - 1 > Integer.MAX_VALUE) {
                return Integer.MIN_VALUE;
            }
        }
        if (i - left == 0) {
            return 0;
        }
        return (int) (val * sign);
    }
}

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

题目描述

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

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

例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]

在这里插入图片描述
示例 1:

输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6
解释: 节点 2 和节点 8 的最近公共祖先是 6。

分析
  1. 递归查找。利用二叉搜索树的已知条件。
代码
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null) {
            return null;
        }
        if (root == p || root == q) {
            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) {
            return right;
        } else {
            return left;
        } 
    }
}

75. 二叉树的最近公共祖先

题目描述

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

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

例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]

在这里插入图片描述

示例 1:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。

分析
  1. 递归查找。
代码
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null) {
            return null;
        }
        if (root == p || root == q) {
            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) {
            return right;
        } else {
            return left;
        }
    }
}
  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值