【剑指Offer第2版】算法题目及答案集合

整理不易留个小心心呗🥰
如果有更好的或者是我有错的地方还请各位大佬指出哦
有些是copy的还望不要介意

剑指Offer(第2版)

03.数组中重复的数字

问题描述:

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

法一:将数组排序,然后查看i和i+1是否相等。时间复杂度O(nlogn)

法二:查看数组是否在哈希表中,若不在则存入,若在就返回。时间复杂度O(n)

法三(最优解):

思考:题中有一个0~n-1还未用过

利用下标法:通过不停交换元素,使得元素和它所对应的下标相等,当发现下标位已有元素说明重复
  • 代码实现
class Solution {
    public int findRepeatNumber(int[] nums) {
        for(int i=0;i<nums.length;i++){
            while(i != nums[i]){
                if(nums[i] == nums[nums[i]]){
                    return nums[i];
                }
                int temp = nums[nums[i]];
                nums[nums[i]] = nums[i];
                nums[i] = temp;
            }
        }
        return -1;
    }
}

04.二维数组中的查找

问题描述:

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

法一:二分查找,每行二分查找,时间复杂度O(nlogm)

法二:利用坐标轴左下角开始,比他小,枢轴往上走(+1),比他大,横轴往右走(+1)。时间复杂度O(n+m)

  • 代码实现
class Solution {
    public boolean findNumberIn2DArray(int[][] matrix, int target) {
        if(matrix == null || matrix.length<=0 || matrix[0].length<=0){
            return false;
        }
        
        int cols = matrix.length;
        int rows = matrix[0].length;
        //左下角位置
        int col = cols - 1;
        int row = 0;

        while(col >= 0 && row < rows){
            if(target>matrix[col][row]){
                row++;
            }else if(target< matrix[col][row]){
                col--;
            }else{
                return true;
            }
        }
        return false;
    }
}

05.替换空格

问题描述:

请实现一个函数,把字符串 `s` 中的每个空格替换成"%20"。
输入:s = "We are happy."
输出:"We%20are%20happy."

法一:java解法

利用StringBuilder,便利数组,遇到空格就加入%20
  • 代码实现
class Solution {
    public String replaceSpace(String s) {
        StringBuilder builder = new StringBuilder();
        for(int i=0;i<s.length();i++){
            if(s.charAt(i) == ' '){
                builder.append("%20");
            }else{
                builder.append(s.charAt(i));
            }
        }
        return builder.toString();
    }
}

法二:C++的思想,实现原地扩容,但java不支持原地扩容,此处只是模拟

扩容长度为原长度加上空格数*2

从后往前遍历

在这里插入图片描述

  • 代码实现
public String replaceSpace(String s) {
        // 统计有多少空格
        int count = 0;
        for(int i = 0; i < s.length(); i++){
            if(s.charAt(i) == ' '){
                count ++;
            }
        }

        char[] res = new char[s.length() + count * 2];
        int k = res.length - 1;
        // 从右往左遍历
        for(int i = s.length() - 1; i >= 0; i--){
            // 从右往左移动字符与替换字符
            if(s.charAt(i) == ' '){
                res[k--] = '0';
                res[k--] = '2';
                res[k--] = '%';
            } else {
                res[k--] = s.charAt(i);
            }
        }
        return new String(res);
}

06.从尾到头打印链表

问题描述:

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
输入:head = [1,3,2]
输出:[2,3,1]

法一:利用栈,先进后出特性,空间和时间复杂度都是O(n)

法二:反转链表

利用递归来反转,但递归法也需利用栈,所以时间和空间复杂度都是O(n)

原地反转,时间复杂度为O(n),空间O(1)

法三:从右往左填充数组

  • 代码实现
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public int[] reversePrint(ListNode head) {
        if(head == null)
            return new int[0];
        
        //统计链表数量,方便创建数组
        int count = 0;
        ListNode temp = head;
        while(temp!=null){
            count++;
            temp = temp.next;
        }

        int[] res = new int[count];
        //从右往左填充数据
        int k = count-1;
        while(head != null){
            res[k--] = head.val;
            head = head.next;
        }
        return res;
    }
}

07.重建二叉树

问题描述:

输入某二叉树的前序遍历和中序遍历的结果,请构建该二叉树并返回其根节点。

假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

在这里插入图片描述

由于前序遍历的第一个结点就是根结点,找到它在中序遍历的位置,左边为左子树,右边为右子树,分别进行递归求解

在这里插入图片描述

  • 代码实现
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    Map<Integer,Integer> map = new HashMap();
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        if(preorder.length<=0 || preorder == null){
            return null;
        }
        //做映射存入中序遍历的数组,为了快速求出某个元素的下标
        for(int i=0;i<inorder.length;i++){
            map.put(inorder[i],i);
        }

        TreeNode root = f(preorder,0,preorder.length-1,inorder,0,inorder.length-1);
        return root;
    }

    TreeNode f(int[] preorder,int l1,int r1,int[] inorder,int l2,int r2){
        // 前序遍历或者中序遍历为空时,表示这棵树不存在,直接返回 null
        if(l1>r1 || l2>r2){
            return null;
        }

        //根节点
        TreeNode root = new TreeNode(preorder[l1]);
        //根节点在中序遍历的下标
        int i = map.get(preorder[l1]);
        //递归求解
        root.left = f(preorder,l1+1,l1 + (i - l2),inorder,l2,i-1);
        root.right = f(preorder,l1+(i - l2)+1,r1,inorder,i+1,r2);

        return root;
    }
}

09.用两个栈实现队列

问题描述:

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

栈是先进后出,队列是先进先出

所以当要删除元素时,需要把栈1元素放入栈2后,删除栈2栈顶元素,才是队列的顺序

  • 代码实现
class CQueue {

    Stack <Integer> stack1;
    Stack <Integer> stack2;
    //构造栈
    public CQueue() {
        stack1 = new Stack<>();
        stack2 = new Stack<>();
    }
    
    public void appendTail(int value) {
        stack1.push(value);
    }
    
    public int deleteHead() {
        if(!stack2.isEmpty()){
            //栈2不为空则直接返回栈顶元素
            return stack2.pop();
        }
        //当栈2为空时,需要循环把栈1的元素放入栈2,然后返回栈2栈顶元素
        if(!stack1.isEmpty()){
            while(!stack1.isEmpty()){
                stack2.push(stack1.pop());
            }
            return stack2.pop();
        }

        return -1;
    }
}

/**
 * Your CQueue object will be instantiated and called as such:
 * CQueue obj = new CQueue();
 * obj.appendTail(value);
 * int param_2 = obj.deleteHead();
 */

10-Ⅰ.斐波那契数列

问题描述:

写一个函数,输入 `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。

法一:递归优化

由于递归会出现重复子问题,所以可以用map或数组来保存结果

map<key,value> = map(n,f(n))

但是map是对象消耗内存

数组的话,若没用过则为-1

时间和空间都是O(n)
  • 代码实现
class Solution {
    
    int[] arr;
    public int fib(int n) {
        arr = new int[n+1];
        for(int i=0;i<=n;i++){
            arr[i] = -1;
        }
        return f(n);
    }

    int f(int n){
        if(n<= 1){
            return n;
        }
        if(arr[n] != -1){
            return arr[n];
        }
        //没计算过就计算
        int sum = (f(n-1) + f(n-2)) % 1000000007;
        arr[n] = sum;
        return sum;
    }
}

法二:自底向上

在这里插入图片描述

a原本指向的值不会再用到,所以让a保存b,b保存c,之后重复计算,循环
  • 代码实现
class Solution {

    public int fib(int n) {

        if(n<=1){
            return n;
        }

        int a = 0;
        int b = 1;
        int c = 0;

        for(int i = 2;i <= n;i++){
            //题目要求取余
            c = (a + b) % 1000000007;
            a = b;
            b = c;
        }
        return c;
    }
}

10-Ⅱ.青蛙跳台阶问题

问题描述:

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

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

在这里插入图片描述

f(3)=2,f(4)=3,f(5)=f(3)+f(4)

  • 代码实现
class Solution {
    public int numWays(int n) {
        if(n<=1){
            return 1;
        }

        int a = 1;
        int b = 1;
        int c = 0;

        for(int i = 2;i<=n;i++){
            c = (a+b) % 1000000007;
            a = b;
            b = c;
        }
        return c;
    }
}

11.旋转数组的最小数字

问题描述:

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。

给你一个可能存在 重复 元素值的数组 numbers ,它原来是一个升序排列的数组,
并按上述情形进行了一次旋转。请返回旋转数组的最小元素。
例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一次旋转,该数组的最小值为 1。  

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
输入:numbers = [3,4,5,1,2]
输出:1

法一:暴力解法

遍历数组,当nums[i]>nums[i+1]时出现旋转点,nums[i+1]为最小值

法二:二分法

根据题意,符合元素有序,顺序存储,所以利用二分法

数组头为l,尾为r,当l元素大于r元素,则利用中间元素进行查找。时间复杂度O(logN),空间O(1)

若小于,则出现极端情况,因为题目说明可旋转n次,也就是有可能最终结果为原顺序数组,012345

则直接返回l元素,就为最小值。极端情况下时间复杂度O(n)
  • 代码实现
class Solution {
    public int minArray(int[] numbers) {
        if(numbers.length == 1){
            return numbers[0];
        }
    
        int l = 0;
        int r = numbers.length-1;
        while(l<r){
            if(numbers[l]<numbers[r]){
                return numbers[l];
            }

            int mid = (l+r)/2;
            if(numbers[l]<numbers[mid]){
                l = mid + 1;
            }else if(numbers[l]>numbers[mid]){
                r = mid;
            }else{
                l++;
            }
        }
        return numbers[l];
    }
}

12.矩阵中的路径

问题描述:

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

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

在这里插入图片描述

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

思路:

深度搜索,向上下左右

若已经搜索过则标记,回溯回去的就要标记为没有用过

时间复杂度:mn * 3^len (两个循环加三个方向的搜索)

空间复杂度:m*n

  • 代码实现
class Solution {
    int n;
    int m;
    int len;
    boolean[][] visited;
    public boolean exist(char[][] board, String word) {
        
            this.n = board.length;
            this.m = board[0].length;
            this.len = word.length();
            visited = new boolean[n][m];

            for(int i=0;i<n;i++){
                for(int j=0;j<m;j++){
                    if(dsf(board,i,j,word,0)){
                        return true;
                    }
                }
            }
            return false;
    }

    boolean dsf(char[][]board,int i,int j,String word,int k){
            //判断是否越界,已经访问过或者不匹配
            if(i<0 || i>=n || j<0 ||j>=m || board[i][j] != word.charAt(k) 
                // ||visited[i][j]
            ){
                return false;
            }
            if(k == len - 1){
                return true;
            }
            //标记此格已经被访问过
            //visited[i][j] = true;
            //把该位置标记为特殊符号,便免除加多了空间复杂度
            board[i][j] = '\0';

            //四个方向都搜索
            boolean res = dsf(board,i,j+1,word,k+1) ||
                        dsf(board,i+1,j,word,k+1) ||
                        dsf(board,i,j-1,word,k+1) ||
                        dsf(board,i-1,j,word,k+1);

            //visited[i][j] = false;
            board[i][j] = word.charAt(k);
            return res;
    }
}

13.机器人的运动范围

问题描述:

地上有一个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。请问该机器人能够到达多少个格子?
输入:m = 2, n = 3, k = 1
输出:3

法一:暴力搜索,深度搜索

往一个方向进行搜索,当遇到障碍物或尽头时,则进行回溯(之前的格子的其他方向进行搜索),
并且记录每个格子是否有搜索。需要上下左右搜索。时间复杂度O(m*n)
  • 代码实现
class Solution {
    int m;
    int n;
    int k;
    boolean [][] visited;
    public int movingCount(int m, int n, int k) {
        this.m = m;
        this.n = n;
        this.k = k;
        //默认值是false
        visited = new boolean[m][n];

        return dfs(0,0);
    }

    int dfs(int i,int j){
        //是否在方格内或是否访问过或是否障碍物
        if(i<0 || j<0 || i>= m || j>=n || visited[i][j] || k < sum(i) + sum(j)){
            return 0;
        }
        visited[i][j] = true;
        //已经访问一个格子,然后向上下左右方向走
        return 1 + dfs(i+1,j) + dfs(i,j+1) + dfs (i-1,j) + dfs(i,j-1);

    }

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

法二:只向下和向右搜索

理解题,发现只向下和右搜索便可。

时间复杂度O(m*n),比上题好的地方是不会过多的查看是否有搜索过

在这里插入图片描述
在这里插入图片描述

代码实现:将暴力搜索的向上和向左递归删除即可

14-Ⅰ.剪绳子

问题描述:

给你一根长度为 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。
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
提示:2 <= n <= 58

解题思路:

当分成相近大小时,乘积为最大值。eq.3*3

假设把绳子分成两段,拿其中一段来分析

  为1、2、3、4时,就算把他们剪掉也与原本结果一样

  当为5时,可以把他剪成2、3,2*3=6>5

也就是说可以把绳子剪成的长度可以为2、3、4(1没有意义)

  但是2*4≤3*3,也就是说若同时存在2和4时,可以把他们剪成3和3

  所以不可同时存在2和4,同样也不可存在多个2(合并成4和2)和多个4(拆成2和4)

所以最好的是剪为3

可以把绳子剪成几段:res = n/3。没有余数则3^res

若有余数mod = n%3

  当等于1时:3^(res-1)*4

  当等于2时:3^res *2
  • 代码实现
class Solution {
    public int cuttingRope(int n) {
        if(n<=2){
            return 1;
        }
        if(n==3){
            return 2;
        }
        if(n==4){
            return 4;
        }

        int res = n/3;
        int mod = n%3;

        if(mod == 0){
            //Math返回值为double类型,所以强转为int
            return (int)Math.pow(3,res);
        }else if(mod == 1){
            return (int)Math.pow(3,res-1) * 4;
        }else{
            return (int)Math.pow(3,res) * 2;
        }
    }
}

14-Ⅱ.剪绳子Ⅱ

问题描述:

给你一根长度为 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。
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
提示:2 <= n <= 1000

在这里插入图片描述

根据提示:2≤n≤1000,所以上图二式成立

在这里插入图片描述

  • 代码实现
class Solution {
    public int cuttingRope(int n) {
        if(n<=2){
            return 1;
        }
        if(n==3){
            return 2;
        }
        if(n==4){
            return 4;
        }

        int res = n/3;
        int mod = n%3;
        int p = 1000000007;

        if(mod == 0){
            //Math返回值为double类型,所以强转为int
            return (int)pow(3,res);
        }else if(mod == 1){
            //long->int
            return (int)(pow(3,res-1) * 4 % p);
        }else{
            return (int)(pow(3,res) * 2 % p);
        }
    }

    //求a^n %p
    long pow(int a,int n){
        long res = 1;
        int p = 1000000007;
        for(int i=1;i<=n;i++){
            res = (res * a) % p;
        }
        return res;
    }
}

15.二进制中1的个数

问题描述:

编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为汉明重量。

提示:

请注意,在某些语言(如 Java)中,没有无符号整数类型。
在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,
因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。

在 Java 中,编译器使用 二进制补码 记法来表示有符号整数。因此,在上面的 示例 3 中,输入表示有符号整数 -3。

输入:n = 4294967293 (控制台输入 11111111111111111111111111111101,部分语言中 n = -3)
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。

法一:除以2

不断除以2直到为0,判断是否为1

法二:n与(n-1)相与

n与(n-1)相与能消掉1
eq:
n = 1000100
n-1=1000011
相与=1000000
时间 O(logn),空间 O(1)
  • 代码实现
public class Solution {
    // you need to treat n as an unsigned value
    public int hammingWeight(int n) {
        int sum = 0;
        while(n!=0){
            //相与能消除n最右边的一个1
            n = n & (n-1);
            sum ++;
        }
        return sum;
    }
}

16.数值的整数次方*

问题描述:

实现 pow(x,n) ,即计算 x 的 n 次幂函数(即,xn)。不得使用库函数,同时不需要考虑大数问题。
输入:x = 2.00000, n = -2
输出:0.25000
解释:2-2 = 1/22 = 1/4 = 0.25

eq:n=13 =>1101

x^8 * x^4 * x^1

此方法只针对正数使用,所以负数要先转为正数

本题利用快速幂算法

时间复杂度O(log2n),空间O(1)

  • 代码实现
class Solution {
    public double myPow(double x, int n) {
        double res = 1;
        //不能用int会保存不下,最小负数转正数
        long y = n;
        if(n < 0){
            //转为正数来计算
            y = -y;
            x = 1/x;
        }
        while(y > 0){
            //奇数时
            if(y%2 == 1){
                res = res * x;
            }

            x = x * x;
            y = y / 2;
        }
        return res;
    }
}

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

问题描述:

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

说明:

- 用返回一个整数列表来代替打印
- n 为正整数
输入: n = 1
输出: [1,2,3,4,5,6,7,8,9]

此问题不考虑大数问题

  • 代码实现
class Solution {
    public int[] printNumbers(int n) {
        //不考虑大数问题,所以用int即可
        int max = (int)Math.pow(10,n);
        int[] res = new int[max - 1];
        for(int i=1;i<=max-1;i++){
            //0不能存
            res[i - 1] = i;
        }
        return res;
    }
}

若考虑大数问题,可用方法有:全排列、大数相加处理

18.删除链表的节点

问题描述:

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

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

**说明:**

- 题目保证链表中节点的值互不相同
- 若使用 C 或 C++ 语言,你不需要 `free` 或 `delete` 被删除的节点
输入: head = [4,5,1,9], val = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.

  • 代码实现
/**
 * 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 == null){
            return null;
        }
        if(head.val == val){
            return head.next;
        }
        ListNode temp = head.next;
        ListNode pre = head;

        while(temp != null){
            if(temp.val == val){
                pre.next = temp.next;
                return head;
            }
            temp = temp.next;
            pre = pre.next;
        }
        return head;

    }
}

19.正则表达式匹配*

问题描述:

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

- 例子
  - `s` 可能为空,且只包含从 `a-z` 的小写字母。
  - `p` 可能为空,且只包含从 `a-z` 的小写字母以及字符 `.` 和 `*`,无连续的 `'*'`。
输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
输入:
s = "aa"
p = "a*"
输出: true
解释: 因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。

输入:
s = "ab"
p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。
输入:
s = "aab"
p = "c*a*b"
输出: true
解释: 因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。

输入:
s = "mississippi"
p = "mis*is*p*."
输出: false

分析:

假设dp[i][j]表示字符串长度为i,j时,s与p是否匹配(s[i],p[j])

1. 当p[j] = ' . '或s[i]时

    dp[i][j] = dp[i-1][j-1](循环表达式)

    否则dp[i][j] = false
2. 当p[j] = *
    1. p[j-1] ≠ s[i]

        dp[i][j] = dp[i][j-2]
    2. p[j-1] = s[i]
        1. 当*匹配0个 => dp[i][j] = dp[i][j-2]
        2. 当*匹配1个 => dp[i][j] = dp[i][j-1]
        3. 当*匹配多个 => dp[i][j] =  dp[i-1][j] (相当于拿其中一个和s[i]匹配消掉了)

寻找初始值:

  dp[0][0]=true,dp[0][1]=false,

  dp[0][j] j≥2 

    当p[j]=* : dp[0][j] = dp[0][j-2] (不等于*为false)
  • 代码实现
class Solution {
    public boolean isMatch(String s, String p) {
        if(s == null || p == null){
            return true;
        }
        int n = s.length();
        int m = p.length();
        boolean dp[][] = new boolean[n+1][m+1];
        
        //初始化
        dp[0][0] = true;
        //dp[0][1]=false; java初始值为false
        for(int j = 2;j<=m;j++){
            if(p.charAt(j - 1) == '*'){
                dp[0][j] = dp[0][j-2];
            }
        }

        for(int i = 1;i<=n;i++){
            for(int j=1;j<=m;j++){
                //当j不为*
                if(p.charAt(j-1) != '*'){
                    //如果不匹配
                    if(p.charAt(j-1) == '.' || p.charAt(j-1) == s.charAt(i-1)){
                        dp[i][j] = dp[i-1][j-1];
                    }
                //第一个字符不可能为*,所以j=1不会出现溢出,不会进入下面的else中
                }else{
                    //第j-1个字符不匹配
                    if(p.charAt(j-2) != s.charAt(i-1) && p.charAt(j-2) != '.' ){
                        dp[i][j] = dp[i][j-2];
                    }else{
                        dp[i][j] = dp[i][j-2] || dp[i][j-1] || dp[i-1][j];
                    }
                }
            }
        }
        return dp[n][m];
    }
}

20.表示数值的字符串

问题描述:

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

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

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

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

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

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

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

部分**数值**列举如下:

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

部分**非数值**列举如下:

  ["12e", "1a3.14", "1.2.3", "+-5", "12e+5.4"]
输入:s = "0"
输出:true
输入:s = "e"
输出:false

思路:

将该字符串转成char数组,根据情况判断

前一个或现在位置是什么值可以设置boolean类型
  • 代码实现
class Solution {
    public boolean isNumber(String s) {
        //有限状态机
        //2.小数点 3.E/e 4.数字字符 5.小数点
        if(s == null || s.length() < 0){
            return false;
        }
        //将空格去掉,并转为char数组
        char[] res = s.trim().toCharArray();
        if(res.length <= 0) return false;
        
        int n = res.length;

        boolean is_num = false;//该位置是否为数字
        boolean is_dot = false;//该位置是否为小数点
        boolean is_e_or_E = false;//该位置是否为e或E
        for(int i = 0;i<n;i++){
            if(res[i] >= '0' && res[i] <= '9'){
                is_num = true;
            }else if(res[i] == '.'){
                //前面不能有重复的小数点,也不能出现e/E
                if(is_dot || is_e_or_E){
                    return false;
                }
                //该位置是小数点,所以把状态置为true
                is_dot = true;
            }else if(res[i] == 'e' || res[i] == 'E'){
                //前面必须要有数字 || 前面不能出现重复的e/E
                if(is_e_or_E || !is_num){
                    return false;
                }
                is_e_or_E = true;
                is_num = false; // 5e+ 5e5 5e
            }else if(res[i] == '-' || res[i] == '+'){
                if(i!=0 && res[i-1] != 'e' && res[i-1] != 'E'){
                    return false;
                }
            }else{
                return false;
            }
        }

        return is_num;
    }
}

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

问题描述:

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数在数组的前半部分,
所有偶数在数组的后半部分。
输入:nums = [1,2,3,4]
输出:[1,3,2,4]  (稳定调整)
注:[3,1,2,4] 也是正确的答案之一。  (非稳定调整)

思路:

此题和排序差不多,把大的数放前面相当于把奇数放前面

所以此题可以用快速排序
  • 代码实现
class Solution {
    public int[] exchange(int[] nums) {
        if(nums == null || nums.length == 0){
            return nums;
        }

        int left = 0;
        int right = nums.length - 1;
        while(left < right){
            while(left<right && nums[left] %2 != 0) left++;
            while(left<right && nums[right] %2 == 0) right--;

            int temp = nums[left];
            nums[left] = nums[right];
            nums[right] = temp;
        }
        return nums;
    }
}

22.链表中倒数第k个节

问题描述:

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

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。
这个链表的倒数第 3 个节点是值为 4 的节点。
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.

思路:

此题用双指针(快慢指针)

先让快指针走k步,然后快慢指针同时走动,当fast等于null时,slow位置便为倒数k位
  • 代码实现
/**
 * 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) {
        if(head == null){
            return null;
        }

        ListNode fast = head, slow = head;
        //先让快指针走k步
        for(int i=0; i < k; i++){
            //防止越界
            if(fast == null){
                return null;
            }
            fast = fast.next;
        }

        //然后快慢指针同时走,当fast走到null时slow位置就是k位
        while(fast!= null){
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }
}

24.反转链表

问题描述:

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

法一:递归

时间和空间复杂度都是n,递归需要调用到最后一个节点,所以需要调用n层
  • 代码实现
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        if(head == null || head.next == null){
            return head;
        }

        ListNode temp = reverseList(head.next);
        head.next.next = head;
        head.next = null;

        return temp;
    }
}

法二:原地排序

设立一个当前节点和前驱节点,并且后面的节点要保存起来防止丢失,直到保存的节点为null

空间是1,时间是n
  • 代码实现
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        if(head == null || head.next == null){
            return head;
        }

        ListNode cur = head, pre = null;

        while(cur!=null){
            ListNode temp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = temp;
        }

        return pre;
    }
}

25.合并两个排序的链表

问题描述:

输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

思路:

比较两个指针的值,小的存入新链表,若有一个链表已经遍历完成,则把另一个链表剩下的存入

设定新链表时,可以加多一个辅助节点,可减少判断
  • 代码实现
/**
 * 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) {
        //定义一个新的链表,并给与一个辅助节点
        ListNode merge = new ListNode(0);
        ListNode temp = merge;

        while(l1!= null && l2 != null){
            if(l1.val > l2.val){
                //若没有给与辅助节点,第一个是空指针,.next会报错
                //此处便要加多一个判断
                temp.next = l2;
                l2 = l2.next;
            }else{
                temp.next = l1;
                l1 = l1.next;
            }
            temp = temp.next;
        }

        //若其中一个以及到达null,则把另一个链表的值存入
        temp.next = l1 == null ? l2 :l1;

        //因为第一个是辅助节点没有值,所以要返回下一个
        return merge.next;
    }
}

26.树的子结构

问题描述:

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

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

例如:

给定的树 A:

     3
    / \
   4   5
  / \
 1   2

给定的树 B:

    4 
   /
  1

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

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

思路:

**二叉树大部分都使用递归方式**

此题先判断B是否以A节点为根节点的一个子树

然后再判断B是否是A左右子树的一个子结构
  • 代码实现
/**
 * 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;
        }
        //判断B是否以A节点为根节点的一个子树
        if(isSubTree(A,B)){
            return true;
        }
        //判断B是否是A左右子树的一个子结构
        if(isSubStructure(A.left,B) || isSubStructure(A.right,B)){
            return true;
        }
        return false;
    }

    public boolean isSubTree(TreeNode Ta,TreeNode Tb){
        //B树已经遍历完
        if(Tb == null){
            return true;
        }
        //A树已经遍历完
        if(Ta == null){
            return false;
        }
        //当前节点是否相等,若不相等也不可能是他的子树
        if(Ta.val != Tb.val){
            return false;
        }
        //判断左右子树是否相等
        return isSubTree(Ta.left,Tb.left) && 
            isSubTree(Ta.right,Tb.right);
    }
}

27.二叉树的镜像

问题描述:

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

例如输入:

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

镜像输出:

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

思路:

使用递归

分别对左右子树进行镜像,类似于反转链表
  • 代码实现
/**
 * 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 || (root.left == null && root.right == null)){
            return root;
        }

        //各自镜像操作左右子树
        TreeNode left = mirrorTree(root.left);
        TreeNode right = mirrorTree(root.right);

        //镜像操作
        root.left = right;
        root.right = left;

        return root;
    }
}

28.对称的二叉树

问题描述:

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

例如,二叉树 [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
输入:root = [1,2,2,3,4,4,3]
输出:true
  • 代码实现
/**
 * 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) {
        //若左右子树都为空,则此树也是对称的
        if(root == null || (root.left == null && root.right == null)){
            return true;
        }

        return f(root.left,root.right);
    }
    public boolean f(TreeNode A,TreeNode B){
        if(A == null && B == null){
            return true;
        }
        //若左右子树其中有一个是空则不是对称的
        if(A == null || B == null){
            return false;
        }
        //判断根节点是否相等,若不等也不是对称的
        if(A.val != B.val){
            return false;
        }

        return f(A.left,B.right) && f(A.right,B.left);
    }
}

29.顺时针打印矩阵

问题描述:

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
输入: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 == null || matrix.length == 0 || matrix[0].length == 0){
            return new int[0];
        }
        //定义上下左右遍历t、b、l、r
        int t = 0,b = matrix.length-1,l = 0,r = matrix[0].length-1;
        int[] res = new int[(r+1) * (b+1)];
        int k = 0;
        while(true){
            //从左往右
            for(int i=t,j=l;j <= r;j++){
                res[k++] = matrix[i][j];
            }
            t++;
            if(t > b) break;
            
            //从上往下
            for(int i=t,j=r;i <= b;i++){
                res[k++] = matrix[i][j];
            }
            r--;
            if(l > r) break;

            //从右往左
            for(int i=b,j=r;j >= l;j--){
                res[k++] = matrix[i][j];
            }
            b--;
            if(t > b) break;

            //从下往上
            for(int i=b,j=l;i >= t;i--){
                res[k++] = matrix[i][j];
            }
            l++;
            if(l > r) break;

        }
        return res;
    }
}

30.包含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.

思路:

定义多一个辅助栈,用于存储最小值

时间复杂度O(1),空间复杂度O(n),最差也是2n
  • 代码实现
class MinStack {
    Stack<Integer> stack1;
    Stack<Integer> stack2;

    /** initialize your data structure here. */
    public MinStack() {
        this.stack1 = new Stack();
        this.stack2 = new Stack();
    }
    
    //stack2.peek()返回栈顶元素但不移除它
    public void push(int x) {
        stack1.push(x);
        if(stack2.isEmpty() || x<= stack2.peek().intValue()){
            stack2.push(x);
        }
    }

    //若移除的元素和栈2元素相等也要移除
    public void pop() {
        if(!stack1.isEmpty()){
            //Integer,当数值大于127时比较的是对象,小于才是数值,所以要加上intValue()
            if(stack1.peek().intValue() == stack2.peek().intValue()){
                stack2.pop();
            }
            stack1.pop();
        }
    }
    
    public int top() {
        return stack1.peek();
    }
    
    //栈2的栈顶元素永远是最小的
    public int min() {
        return stack2.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();
 */

31.栈的压入、弹出序列

问题描述:

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。
假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,
序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。
输入: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

思路:

按压入顺序push,当遇到栈顶元素于弹出序列的值相同时则弹出

当栈最后不是空时,代表弹出顺序错误

此题还需明白,当压入和弹出顺序给定时,执行顺序也是唯一的
  • 代码实现
class Solution {
    public boolean validateStackSequences(int[] pushed, int[] popped) {
        //根据给出的提示,两个是相等的,所以只需判断一个
        if(pushed == null || pushed.length <= 0){
            return true;
        }

        int k = 0;
        Stack<Integer> stack = new Stack();
        for(int i = 0;i<pushed.length;i++){
            stack.push(pushed[i]);
            while(!stack.isEmpty() && stack.peek().intValue() == popped[k]){
                stack.pop();
                k++;
            }
        }
        return stack.isEmpty();
    }
}

32-Ⅰ.从上到下打印二叉树

问题描述:

从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
例如:
给定二叉树: [3,9,20,null,null,15,7],
    3
   / \
  9  20
    /  \
   15   7
返回:
[3,9,20,15,7]

思路:

**遍历二叉树通常使用队列**

先把根节点存入队列

然后返回队首元素并删除

并把该节点的左右节点存入队列

如此循环上述步骤
  • 代码实现
/**
 * 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];
        }

        //定义一个队列来读取元素
        Queue<TreeNode> queue = new LinkedList<>();
        //列表存放元素
        List<Integer> res = new ArrayList<>();

        queue.add(root);
        //queue元素拿出
        //把它的左右节点放入队列
        while(!queue.isEmpty()){
            //返回队首元素并删除
            TreeNode t = queue.poll();
            res.add(t.val);
            if(t.left != null) queue.add(t.left);
            if(t.right != null) queue.add(t.right);
        }

        int[] arr = new int[res.size()];
        for(int i=0;i<res.size();i++){
            arr[i] = res.get(i);
        }
        return arr;
    }
}

32-Ⅱ.从上到下打印二叉树(分层打印)

问题描述:

从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
例如:
给定二叉树: [3,9,20,null,null,15,7],
    3
   / \
  9  20
    /  \
   15   7
返回其层次遍历结果
[
  [3],
  [9,20],
  [15,7]
]

思路:

跟上题相同,只是在此基础上加多一个列表,用于存放该层的元素

这个列表大小就是队列的大小,因为每层读取时都会同时删除,所以一层遍历完队列剩下的是下一层的元素
  • 代码实现
/**
 * 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) {
        if(root == null){
            return new ArrayList<>();
        }

        Queue<TreeNode> queue = new LinkedList<>();
        List<List<Integer>> res = new ArrayList<>();

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

32-Ⅲ.从上到下打印二叉树(蛇形打印)

问题描述:

请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,
第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。
给定二叉树: [3,9,20,null,null,15,7],
    3
   / \
  9  20
    /  \
   15   7
返回其层次遍历结果:
[
  [3],
  [20,9],
  [15,7]
]

思路;

用一个变量记录该行是奇数还是偶数行

偶数行从前插入链表

此题用链表是因为它从前和从后插入的时间复杂度都是O(1)
  • 代码实现
/**
 * 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) {
        if(root == null){
            return new ArrayList<>();
        }

        Queue<TreeNode> queue = new LinkedList<>();
        List<List<Integer>> res = new ArrayList<>();
        //用于判断该行是偶数还是奇数行
        int sum = 1;
        queue.add(root);
        while(!queue.isEmpty()){
            int k = queue.size();
            //用链表的话,从前面和后面插入的时间复杂度都是O(1)
            LinkedList<Integer> temp = new LinkedList<>();
            for(int i = 0;i<k;i++){
                TreeNode t = queue.poll();
                if(sum % 2 == 1){
                    //奇数行正常插入
                    temp.add(t.val);
                }else{
                    //偶数行从前面开始插入
                    temp.addFirst(t.val);
                }
                
                if(t.left != null) queue.add(t.left);
                if(t.right != null) queue.add(t.right);
            }
            res.add(temp);
            sum++;
        }
        return res;
    }
}

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

问题描述:

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。
如果是则返回 `true`,否则返回 `false`。假设输入的数组的任意两个数字都互不相同。
     5
    / \
   2   6
  / \
 1   3
输入: [1,3,2,6,5]
输出: true

思路:

二叉树的特点:左节点比根节点小,右节点比根节点大

后序遍历的最后一个必然是根节点

以数组中第一个比根节点大的数为分隔,左边便是左子树,右边便是右子树

此题要判断是不是后序遍历的结果,只需判断右子树中是否有比跟节点小的数
  • 代码实现
class Solution {
    public boolean verifyPostorder(int[] postorder) {
        if(postorder == null || postorder.length <=0){
            return true;
        }

        return f(postorder,0,postorder.length -1);
    }

    boolean f(int[] postorder , int i,int j){
        if(i>=j){
            return true;
        }
        //根节点必然是数组的最后一个
        int root = postorder[j];
        int p=i;
        //找到第一个比root大的位置,该位置的右边便是右子树,左边是左子树
        while(postorder[p] < root) p++;
        //判断左子树部分中是否还存在比root小的
        for(int k = p; k<j;k++){
            if(postorder[k] < root){
                return false;
            }
        }
        return f(postorder,i,p-1) && f(postorder,p,j-1);
    }
}

leetcode单调栈题解:leetcode单调栈题解

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

问题描述:

给你二叉树的根节点 `root` 和一个整数目标和 `targetSum` ,找出所有 **从根节点到叶子节点** 路径总和等于给定目标和的路径

在这里插入图片描述

输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]

思路:

深度搜索,一直搜索,每遇到一个则加入存储路径的列表,并目标相减,
遇到叶子节点后判断目标值是否为0,不为0则回溯
  • 代码实现
/**
 * 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 {
    List<List<Integer>> res;
    List<Integer> temp;//保存路径防止丢失
    public List<List<Integer>> pathSum(TreeNode root, int target) {
        res = new ArrayList<>();
        temp = new ArrayList<>();
        dsf(root,target);
        return res;
    }
     void dsf(TreeNode root,int target){
         if(root == null){
             return;
         }

         temp.add(root.val);
         target = target - root.val;
         
         if(root.left == null && root.right == null && target == 0){
            res.add(new ArrayList<>(temp));
         }
         dsf(root.left,target);
         dsf(root.right,target);

         temp.remove(temp.size() - 1);
     }
}

35.复杂链表的复制

问题描述:

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

在这里插入图片描述

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

思路:

复制节点和随机节点,然后拆分

把 A->A1->B->B1->C->C1拆分成 A->B->C和A1->B1->C1
  • 代码实现
/*
// 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;
        }
        //复制节点 A->A1->B->B1->C->C1
        Node cur = head;
        while(cur != null){
            Node next = cur.next; //B
            cur.next = new Node(cur.val); //A1
            cur.next.next =next;
            cur = next;
        }

        //复制随机节点
        cur = head;
        while(cur != null){
            Node curNew = cur.next;//A1
            //A->A1->B->B1->C->C1 比如A.random->c 让A1.random->c.next(C1)
            curNew.random = cur.random == null ? null : cur.random.next;
            cur = cur.next.next;//B
        }

        //拆分,比如把 A->A1->B->B1->C->C1拆分成 A->B->C和A1->B1->C1
        Node headNew = head.next;
        cur = head;
        Node curNew = head.next;
        while(cur != null){
            cur.next = cur.next.next;
            cur = cur.next;
            curNew.next = cur == null ? null : cur.next;
            curNew = curNew.next;
        }
        return headNew;
    }
}

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

问题描述:

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

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

在这里插入图片描述

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

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

在这里插入图片描述

特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,
树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。

思路:

仔细观察会发现链表的排序实际上是二叉树的中序遍历的结果

所以首先将二叉树进行中序遍历,可以用队列来存储结果
  • 代码实现
/*
// 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 {
    public Node treeToDoublyList(Node root) {
        if(root == null){
            return null;
        }
        
        //定义一个队列来存放排好序的二叉树
        Queue<Node> queue = new LinkedList<>();
        //将二叉树进行中序遍历
        inOrder(root,queue);
        Node head = queue.poll();
        Node pre = head;

        while(!queue.isEmpty()){
            Node cur = queue.poll();
            pre.right = cur;
            cur.left = pre;
            pre = cur;
        }
        //将头尾节点互联
        pre.right = head;
        head.left = pre;

        return head;
    }
    //二叉树转化的链表排序实际上是二叉树中序遍历的结果
    void inOrder(Node root,Queue queue){
        if(root == null){
            return;
        }
        inOrder(root.left,queue);
        queue.add(root);
        inOrder(root.right,queue);
    }

}

37.序列化二叉树

问题描述:

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

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

提示:输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。

在这里插入图片描述

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

思路:

序列化:遍历二叉树,然后利用append构建字符串

反序列化:将字符串根据逗号分隔成字符串数组,然后存进队列,字符串顺序便是根节点、左结点、右节点顺序,按照此来构建二叉树

遍历:将根节点存入队列,然后出队,并将左右节点存入队列
  • 代码实现
/**
 * 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) {
        //序列化成字符串1,2,3,nulll,...
        if(root == null){
            return "";
        }
        StringBuilder builder = new StringBuilder();
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);

        while(!queue.isEmpty()){
            TreeNode t = queue.poll();
            if(t != null){
                //此处与之前的遍历做法有所不同,此处左右节点为null也需要存入队列
                queue.add(t.left);
                queue.add(t.right);
                builder.append(t.val + ",");
            }else{
                builder.append("null,");
            }
        }
        return builder.toString();
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        if(data == null || data.length() <= 0){
            return null;
        }
        //根据逗号将字符串切割成字符串数组
        String[] s = data.split(",");
        Queue<TreeNode> queue = new LinkedList<>();
        //此处需要注意转化为整数
        TreeNode root = new TreeNode(Integer.parseInt(s[0]));
        queue.add(root);
        int i = 1;
        while(!queue.isEmpty()){
            TreeNode t = queue.poll();
            //构建左节点
            if(!s[i].equals("null")){
                TreeNode left = new TreeNode(Integer.parseInt(s[i]));
                t.left = left;
                queue.add(left);
            }
            //为null时不需要构建,因为默认值为null
            i++;

            //构建右节点
            if(!s[i].equals("null")){
                TreeNode right = new TreeNode(Integer.parseInt(s[i]));
                t.right = right;
                queue.add(right);
            }
            i++;
        }
        return root;
    }
}

// Your Codec object will be instantiated and called as such:
// Codec codec = new Codec();
// codec.deserialize(codec.serialize(root));

38.*字符串的排列

问题描述:

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

你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]

思路:

此题相当于进行全排列,构建一个n叉树

结果不能有重复的,相当于要给n叉树剪枝

先给字符串排序成字符串数组,然后判断该元素是否是第一个元素

若不是则判断前一个元素与该元素是否相邻,且相等,并且是否是第一次访问

若不是第一次访问则说明是同一条路径,若是则说明这两个元素相邻,这两条路径重复

假设arr={a,a,b}

在这里插入图片描述

时间复杂度O(n*n!) 全排列n!

 空间N,N,递归调用最大深度也是N,也就是3n,O(n)
  • 代码实现
class Solution {
    List<Character> path;//记录访问路径
    List<String> res;//记录结果集
    boolean[] visited;//记录是否被访问过

    public String[] permutation(String s) {
        this.path = new ArrayList<>();
        this.res = new ArrayList<>();
        this.visited = new boolean[s.length()];

        char[] arr = s.toCharArray();
        Arrays.sort(arr);
        //全排列并剪枝
        dfs(arr,0);

        String[] ss = new String[res.size()];
        for(int i = 0;i<res.size();i++){
            ss[i] = res.get(i);
        }
        return ss;
    }
    
    void dfs(char[] arr,int k){
        //已经遍历完一条路径
        if(arr.length == k){
            res.add(listToString(path));
            return;
        }

        //进行N叉树搜索
        for(int i=0;i<arr.length;i++){
            //剪枝,先判断是否是第一个元素,
            //然后判断他们是否是相邻字符相等,且前一个字符是否被访问过
            //若没有访问过说明是同一层,也就说明这一条路径是重复的
            if(i>0 && arr[i] == arr[i-1] && visited[i-1] == false){
                continue;
            }
            if(visited[i] == false){
                //递归访问
                visited[i] = true;
                path.add(arr[i]);
                dfs(arr,k+1);
                path.remove(path.size() - 1);
                visited[i] = false;
            }
        }
    }

    String listToString(List list){
        StringBuilder b = new StringBuilder();
        for(int i = 0;i<list.size();i++){
            b.append(list.get(i));
        }
        return b.toString();
    }
}

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

问题描述:

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

你可以假设数组是非空的,并且给定的数组总是存在多数元素。
输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2

思路:

法一:哈希表法:

  遍历数组并记录出现次数,然后查找数值超过n/2的数

  时间复杂度和空间复杂度都是O(n)

法二:排序法:

  将数组排序,中位数便是众数

  时间O(nlogn) 空间O(1)

法三:摩尔投票法:

  众数票数为sum,其他数字的票数为y,并且由于sum>y,所以两两抵消结果还是众数的多

  先假设第一个数为众数,sum=1,若下一个数跟众数不相等,则他们两个的票数都减1,也就是抵消,去掉了数组的前两个数,此时sum=0,重新设置第三个数为众数

  若和第四个数相等,也就是同党,票数加1

  最后留下有票数的值便是众数

  时间O(n) 空间O(1)
  • 代码实现
class Solution {
    public int majorityElement(int[] nums) {
        if(nums.length <= 2){
            return nums[0];
        }

        //先设第一个数是众数的数值
        int x = nums[0];
        //计算众数票数
        int sum = 1;

        for(int i = 1;i<nums.length;i++){
            //票数抵消完毕,重新设定众数数值
            if(sum == 0){
                x = nums[i];
                sum = 1;
            }//判断是否为同党
            else{
                if(x == nums[i]){
                    sum++;
                }//不是同党抵消票数
                else{
                    sum--;
                }
            }
        }
        return x;
    }
}

40.最小的k个数

问题描述:

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

思路:

法一:堆排序

  先设一个大小为k的大根堆(会把堆中数最大的值放在顶部)

  然后遍历数组,若遇到比大根堆最大值小的数,则把大根堆替换出去,然后把现有数中最大的值放在顶部,以此类推,最终大根堆里的数便是结果

  java中可用优先队列(默认是小根堆)

  时间O(nlogk)

法二:快速排序

  先定义数组最左边的数为pivot,进行第一次排序,找到pivot正确位置的下标

  若该位置的下标+1等于k,则表示该数组到第k位的便是结果

  若该位置的下标+1大于k,则表示在pivot左边范围,再对左边进行快排

  若该位置的下标+1小于k,则表示在pivot右边范围,再对右边进行快排

  时间复杂度有关于切割位置,也就是pivot的位置,大约是O(2n)
  • 代码实现
class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if(arr == null || arr.length == 0 || k==0){
            return new int[0];
        }

        return quickFind(arr ,0,arr.length-1,k);
    }

    int[] quickFind(int[] arr,int left ,int right , int k){
        int i = partition(arr,left,right);
        // 之所以需要 i+1,是因为下标从 0 开始,0~i之间一共有 i+1个数
        if(i+1 == k){
            return Arrays.copyOf(arr,k);
        }
        //在pivot左边范围内
        if(i+1 > k){
            return quickFind(arr,0,i-1,k);
        }//还要扩大到pivot右边
        else{
            return quickFind(arr,i+1,right,k);
        }
    }
    // 找出pivot的下标以及使小于等于pivot在左边,大于等于的在右边
    int partition(int[] arr ,int left , int right){
        int pivot = arr[left];
        int i = left+1;
        int j = right;

        while(i<j){
            while(i <= j && arr[i] <= pivot) i++;
            while(i <= j && arr[j] >= pivot) j--;
            if(i>=j){
                break;
            }

            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
        arr[left] = arr[j];
        arr[j] = pivot;
        return j;
    }
}

41.数据流中的中位数

问题描述:

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

例如,

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

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

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

void addNum(int num) - 从数据流中添加一个整数到数据结构中。

double findMedian() - 返回目前所有元素的中位数。

输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]

思路:

定义两个堆,一个大根堆一个小根堆

大根堆存放较小元素,eq.1,2,3 小根堆存放较大元素,eq.4,5,6

若为偶数时取大根堆堆顶元素和小根堆堆顶元素相加再除

若为奇数时,取大根堆的堆顶元素,此处设定大根堆比小根堆多一位数

添加元素进堆时,注意要实现动态维护,也就是每次存入元素后都能得到结果

若此时堆为偶数时,先把元素存入小根堆,再将堆顶元素存入大根堆

若此时堆为奇数时,先把元素存入大根堆,再将堆顶元素存入小根堆

大根堆和小根堆都用优先队列实现,默认为小根堆,大根堆只需改变参数即可

addnum的时间复杂度:

  优先队列的poll和add操作都是logn的时间复杂度

  所以addnum方法的时间复杂度时logn

空间复杂度:用了两个堆,总元素为n,所以空间为O(n)
  • 代码实现
class MedianFinder {
    //大根堆和小根堆都是用优先队列来实现
    Queue<Integer> min,max;
    /** initialize your data structure here. */
    public MedianFinder() {
        //优先队列默认是小根堆
        min = new PriorityQueue<>();//小根堆,存放较大的元素
        //重写参数变成大根堆
        max = new PriorityQueue<>((x,y) -> (y-x));//大根堆,存放较小的元素

    }
    
    //需要实现动态维护,也就是插入元素要根据情况返回结果
    public void addNum(int num) {
        //如果此时是偶数
        if(min.size() == max.size()){
            //先存入小根堆,再把最小的元素存入大根堆
            min.add(num);
            max.add(min.poll());
        }else{
            //此时是奇数,先存入大根堆,再把最大元素存入小根堆
            //规定奇数时,大根堆的数比小根堆多一位
            max.add(num);
            min.add(max.poll());
        }

    }
    
    public double findMedian() {
        //如果此时是偶数
        if(min.size() == max.size()){
            //两个堆的堆顶元素相加后除
            return (min.peek() + max.peek()) / 2.0;
        }else{
            //返回大根堆的堆顶元素
            return max.peek() * 1.0;
        }
    }
}

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

42.连续子数组的最大和

问题描述:

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

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

思路:

**一般求最值用动态规划做**

第一步定义dp数组含义:

  dp[i]:以元素nums[i]为结尾的连续子数组最大的和

第二步找关系式:dp[i] = max(nums[i] , dp[i-1]+nums[i])

  数组前一部分加不加取决于加了之后数是否有变大,所以此处取最大的值

第三步定义初始值:dp[0] = nums[0]

但此方法的空间复杂度为n,可以优化:

  关系式是用于刷新dp的,但刷新前dp就为dp[i-1]的值,所以没必要浪费空间来定义多一个数组

  优化后空间为O(1)
  • 代码实现
class Solution {
    public int maxSubArray(int[] nums) {
        // int[] dp = new int[nums.length];
        // dp[0] = nums[0];
        //优化:
        int dp = nums[0];
        //刷新最大值
        int max = nums[0];

        //因为dp刷新前是dp[i-1],刷新后是dp[i],所以没必要浪费空间定义一个数组
        for(int i = 1; i<nums.length;i++){
            //dp[i] = Math.max(dp[i-1] + nums[i] , nums[i]);
            dp = Math.max(dp + nums[i] , nums[i]);
            max = Math.max(max,dp);
        }
        return max;
    }
}

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

问题描述:

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

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

思路:

  • 代码实现
class Solution {
    public int countDigitOne(int n) {
        //记录此时计算几位数1的个数,个十百千
        long bit = 1;
        long sum = 0;
        while(bit <= n){
            //第几位数上的数值
            long cur = (n/bit)%10;
            //第几位右边的数,低数
            long low = n%bit;
            //第几位左边的数,高数
            long high = n/bit/10;

            if(cur >1){
                sum += (high +1) * bit;
            }else if(cur == 1){
                sum += (high * bit) + (1 + low);
            }else if(cur == 0){
                //high不能加1,会超过n
                sum += high * bit;
            }
            bit = bit * 10;
        }
        return (int)sum;
    }
}

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

问题描述:

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

请写一个函数,求任意第n位对应的数字。
输入:n = 3
输出:3
输入:n = 11
输出:0

  • 代码实现
class Solution {
    public int findNthDigit(int n) {
        if(n == 0){
            return 0;
        }
        //表示几位,个十百千,1,10,100,1000
        long bit = 1;
        //表示第几位,个十百千,1,2,3,4
        int i = 1;
        //表示几位上有多少个字符,个位有9个
        long count = 9;

        //确定在哪个区间
        while(count < n){
            n = (int)(n - count);
            bit = bit * 10;
            i++;
            count = bit * i * 9;
        }
        // 确定是在这个区间的哪个数
        long num = bit + (n - 1) / i;
        // 确定在 Num 的那个字符
        int index = (n - 1) % i + 1;
        int res = (int)(num / Math.pow(10, i - index)) % 10;

        return res;
    }
}

45.把数组排成最小的数

问题描述:

输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个
输入: [10,2]
输出: "102"

思路:

[3,30,24,5,9]

比较相邻的数 eq.比较 330和 303

可以用冒泡排序,3和30 30和24 ... 但比较时留意具有传递性 x>y y>z x>z 时间复杂度为O(n^2)

此题用快速排序,时间为nlogn 空间为n

比较x +y 和y+x
  • 代码实现
class Solution {
    public String minNumber(int[] nums) {
        String[] strs = new String[nums.length];
        //转成字符串数组
        for(int i = 0;i < nums.length;i++){
            strs[i] = String.valueOf(nums[i]);
        }

        //快速排序
        quickShort(strs,0,strs.length - 1);

        StringBuilder sb = new StringBuilder();
        for(int i=0;i<strs.length;i++){
            sb.append(strs[i]);
        }
        return sb.toString();
    }

    //时间复杂度nlogn 空间On
    //快速排序
    private void quickShort(String[] arr,int left,int right){
        if(left > right){
            return;
        }
        int i = partition(arr,left,right);
        quickShort(arr,left,i -1);
        quickShort(arr,i + 1,right);
    }

    int partition(String[] arr,int left,int right){
        String pivot = arr[left];
        int i = left;
        int j = right;
        //3 30 24 5 9
        while(i < j){
            //找到arr[i] + pivot 比pivot + arr[i]大的 y+x > x+y
            //3=3 303<330 243<324 53>35
            //arr[i] + pivot <= pivot + arr[i]
            while(i <= j && (arr[i] + pivot).compareTo(pivot + arr[i]) <= 0){
                i++;
            }
            //找到arr[i] + pivot 比pivot + arr[i]小的 z+x < x+z
            //93>39 53>35 243<324
            //arr[j] + pivot >= pivot + arr[j]
            while(i <= j && (arr[j] + pivot).compareTo(pivot + arr[j]) >= 0){
                j--;
            }
            if(i >= j){
                break;
            }
            String temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
        arr[left] = arr[j];
        arr[j] = pivot;

        return j;
    }

}

46.把数字翻译成字符串

问题描述:

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法
输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi"

思路:

类似于青蛙跳台,相邻可以组合,但必须大于等于10小于等于25
  • 代码实现
class Solution {
    public int translateNum(int num) {
        if(num <= 9){
            return 1;
        }
        //转成字符数组
        char[] arr = String.valueOf(num).toCharArray();
        int n = arr.length;
        //int[] ap = new int[n+1];
        //f(n) = f(n-1) + f(n-2)
        int a = 1;
        int b = 1;
        int c = 1;
        //优化abc
        for(int i = 2;i <= n;i++){
            //计算第i和第i-1个字符的组合
            int temp = 10 * (arr[i-2] -'0') + (arr[i-1] -'0');
            if(temp >= 10 && temp <= 25){
                c = a+b;
            }else{
                c = b;
            }
            //迭代
            a = b;
            b = c;
        }
        return c;
    }
}

47.礼物的最大价值

问题描述:

在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
输入: 
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物

思路:

求最值一般用动态规划

第一步:确定意义

第二步:关系式:该值的左边值或上面值加上该值

  dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]) + grid[i][j];

第三步:给定初始值

  dp[0][0] = grid[0][0];

  第一列和第一行的初始值是前一个值加该值
  • 代码实现
class Solution {
    //二维版本
    public int maxValue(int[][] grid) {
        int n = grid.length;
        int m = grid[0].length;

        int[][] dp = new int[n][m];
        //给定初始值
        dp[0][0] = grid[0][0];
        //第一列的初始值
        for(int i = 1;i<n;i++){
            dp[i][0] = dp[i-1][0] + grid[i][0];
        }
        //第一行的初始值
        for(int j = 1;j<m;j++){
            dp[0][j] = dp[0][j-1] + grid[0][j];
        }

        //每个值累加前面的值
        for(int i= 1;i<n;i++){
            for(int j = 1;j<m;j++){
                dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]) + grid[i][j];
            }
        }
        //dp结果:
        //[
        //    [1,4,5],
        //    [2,9,10],
        //    [6,11,12]
        //]
        return dp[n-1][m-1];
    }
}

优化思路:因为每次累加都只需看上一行的值,所以不需要用到二维数组,一维便可

  • 优化代码
class Solution {
    //二维版本
    public int maxValue(int[][] grid) {
        int n = grid.length;
        int m = grid[0].length;

        //int[][] dp = new int[n][m];
        int[] dp = new int[m];
        //给定初始值
        //dp[0][0] = grid[0][0];
        dp[0] = grid[0][0];
        //第一列的初始值
        // for(int i = 1;i<n;i++){
        //     dp[i][0] = dp[i-1][0] + grid[i][0];
        // }
        //第一行的初始值
        for(int j = 1;j<m;j++){
            //dp[0][j] = dp[0][j-1] + grid[0][j];
            dp[j] = dp[j-1] + grid[0][j];
        }

        //每个值累加前面的值
        for(int i= 1;i<n;i++){
            //
            dp[0] = dp[0] + grid[i][0];
            for(int j = 1;j<m;j++){
                //dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]) + grid[i][j];
                dp[j] = Math.max(dp[j],dp[j-1]) + grid[i][j];
            }
        }
        //dp结果:
        //[
        //    [1,4,5],
        //    [2,9,10],
        //    [6,11,12]
        //]
        //return dp[n-1][m-1];

        return dp[m-1];
    }
}

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

问题描述:

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

思路:

时间和空间都是n

在这里插入图片描述

  • 代码实现
class Solution {
    public int lengthOfLongestSubstring(String s) {
        if(s == null || s.length() <= 0){
            return 0;
        }
        //优化 =》单个变量
        //存放每个字符出现的最近的位置(索引)
        Map map = new HashMap<>();
        //int[] dp = new int[s.length()];
        //dp[0] = 1;
        int a = 1;
        map.put(s.charAt(0),0);
        //记录最长的值
        int res = 1;

        for(int i =1;i<s.length();i++){
            if(!map.containsKey(s.charAt(i))){
                //dp[i] = dp[i-1] + 1;
                a = a + 1 ;
            }//出现重复值
            else{
                int k =(int) map.get(s.charAt(i));
                //重复值是否出现在此时的无重复子串范围中
                //区间之外无影响,继续子串长度加1
                //区间之内则停止在该字符前面的子串
                //dp[i] = i - k <= dp[i-1] ? i - k : dp[i-1] + 1;
                a = i-k <= a ? i - k : a + 1;
            }
            //res = Math.max(res,dp[i]);
            res = Math.max(res,a);
            map.put(s.charAt(i),i);
        }
        return res;
    }
}

49.丑数

问题描述:

我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。

思路:

要求最大的丑数,需要找出每个数乘以质因子的最小值

时间和空间都是n
  • 代码实现
class Solution {
    public int nthUglyNumber(int n) {
        int a = 1, b = 1,c = 1;
        int[] dp = new int[n+1];
        dp[1] = 1;

        for(int i = 2;i<=n;i++){
            dp[i] = Math.min(Math.min(dp[a] * 2,dp[b] * 3),dp[c] * 5);
            if(dp[i] == dp[a] * 2) a++;
            if(dp[i] == dp[b] * 3) b++;
            if(dp[i] == dp[c] * 5) c++;
        }

        return dp[n];
    }
}

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

问题描述:

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母
输入:s = "abaccdeff"
输出:'b'
  • 代码实现
class Solution {
    public char firstUniqChar(String s) {
        if(s == null || s.length() <=0){
            return ' ';
        }
        //一个字母出现第二次便标记为false,不符合要求
        //有序哈希表
        Map<Character,Boolean> map = new LinkedHashMap<>();
        for(int i = 0;i<s.length();i++){
            //若已经存在便为false值,直接取反
            map.put(s.charAt(i), !map.containsKey(s.charAt(i)));
        }

        //二次遍历查找true的值
        for(Map.Entry<Character,Boolean> m : map.entrySet()){
            if(m.getValue()){
                return m.getKey();
            }
        }

        return ' ';
    }
}

51.数组中的逆序对

问题描述:

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

思路:

法一:冒泡排序

  对数组进行冒泡排序,统计交换次数,便是逆序对的总数

  时间n^2 空间1

法二:归并排序

  将数组分成两部分,x1和x2,以及全部x3,分别对x1和x2进行排序操作计算逆序对

  在对x1和x2合并前计算x3的逆序对数量

  若x1的数比x2的数大,则存在逆序对,个数是L2-i

  x1边界L1和r1,x2类似,i和j指向x1和x2此时的值

  时间nlogn 空间n
  • 代码实现
class Solution {
    public int reversePairs(int[] nums) {
        if(nums == null || nums.length <=1){
            return 0;
        }

        return mergeSort(nums,0,nums.length - 1);
    }

    int mergeSort(int[] nums,int left,int right){
        if(left >= right){
            return 0;
        }

        int mid = (right - left) / 2 + left;
        int x1 = mergeSort(nums, left, mid);
        int x2 = mergeSort(nums,mid + 1,right);
        int x3 = merge(nums, left, mid, mid + 1, right);

        return x1 + x2 + x3;
    }

    int merge(int[] nums,int l1,int r1,int l2,int r2){
        //定义一个临时数组存放合并的结果
        int[] temp = new int[r2 - l1 + 1];
        int count = 0;
        int i = l1,j = l2,k = 0;
        while(i <= r1 && j <= r2){
            if(nums[i] > nums[j]){
                //存在逆序对
                count = count + (l2 - i);
                temp[k++] = nums[j++];
            }else{
                temp[k++] = nums[i++];
            }
        }
        while(i <= r1) temp[k++] = nums[i++];
        while(j <= r2) temp[k++] = nums[j++];

        //把临时数组复制会原数组
        k = 0;
        for(i = l1;i <= r2;i++){
            nums[i] = temp[k++];
        }

        return count;
    }
}

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

问题描述:

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

如下面的两个链表:

在这里插入图片描述

在节点 c1 开始相交。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这里插入图片描述

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], 
      skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。
        从各自的表头开始算起,链表 A 为 [4,1,8,4,5],
        链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;
        在 B 中,相交节点前有 3 个节点。

思路:

在这里插入图片描述

a和b同时出发,若到达null还未相遇,则走另一个链表的节点

时间a+b 空间1
  • 代码实现
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
class Solution {
    ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if(headA == null || headB == null){
            return null;
        }

        ListNode A = headA;
        ListNode B = headB;

        while(A != B){
            //若节点指向null时指向另一个链表
            A = A == null ? headB : A.next;
            B = B == null ? headA : B.next;
        }

        return A;
    }
}

53-Ⅰ.在排序数组中查找数字

问题描述:

统计一个数字在排序数组中出现的次数。
输入: nums = [5,7,7,8,8,10], target = 8
输出: 2

思路:

进行二分查找,寻找第一个和最后一个等于 target 的数,索引相减
  • 代码实现
class Solution {
    public int search(int[] nums, int target) {
         //常见题型:
        // 1.寻找第一个大于等于 target 的数   2.寻找第一个等于 target 的数
        // 3.寻找最后一个大于等于 target 的数 4.寻找最后一个等于 target 的数
        //PS:其实这里是不需要写两个查找函数的,可以把代码优化放在同一个方法里滴
        int left = search2(nums,target);
        int right = search4(nums,target);

        //查找target值的边界(二分查找),然后计算
        if(left < 0 || right < 0) return 0;
        return right - left + 1;
    }
    //2.寻找第一个等于 target 的数
    int search2(int[] nums , int target){
        if(nums == null || nums.length <= 0){
            return -1;
        }
        int left = 0,right = nums.length- 1;
        while(left < right){
            //向下取整
            int mid = (right - left) / 2 +left;
            if(nums[mid] >= target){
                right = mid;
            }else{
                left = mid + 1;
            }
        }
        //判断该数是否存在
        if(nums[left] != target) return -1;
        return left;
    }
    //4.寻找最后一个等于 target 的数
    int search4(int[] nums, int target){
        if(nums == null || nums.length <= 0){
            return -1;
        }
        int left = 0, right = nums.length - 1;
        while(left < right){
            //向上取整
            int mid = (right - left + 1) / 2 + left;
            if(nums[mid] <= target){
                left = mid;
            }else{
                right = mid - 1;
            }
        }
        // 判断该数是否存在
        if(nums[left] != target) return -1;
        return left;
    }
}

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

问题描述:

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

思路:

数组的数是有序的,也就是代表索引值和数值是相等的,若不相等,则说明缺失数字

使用二分查找,若中间值的索引值和数值相等,则说明缺失值在右边

此外,还需注意,可能出现整个数组是正确的,也就是缺失第n个数
  • 代码实现
class Solution {
    public int missingNumber(int[] nums) {
        int l =  0,r = nums.length - 1;
        while(l < r){
            int mid = (r - l) / 2 + l;
            //若下标值和数值相等,则说明缺少的在右侧
            if(nums[mid] == mid) l = mid + 1;
            else r = mid;
        }

        //有可能整个数组都是正确的,缺少的是第n个数字
        return nums[l] == l ? l+1 : l;
    }
}

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

问题描述:

给定一棵二叉搜索树,请找出其中第 `k` 大的节点的值。
输入: root = [3,1,4,null,2], k = 1
   3
  / \
 1   4
  \
   2
输出: 4

思路:

有序二叉树的中序遍历便是从小到大排序的结果,但此题需要找出第k大的节点,所以可以调转遍历顺序:右 根 左,得到的结果便是从大到小,每遍历一个k-1,直到k=0
  • 代码实现
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    //每查找一个便减1,若k=0则表示该数为答案
    int k = 0;
    int target = 0;
    public int kthLargest(TreeNode root, int k) {
        //中序遍历,有序的序列,结果便是从小到大排序
        //但此题需要找到第k大的,所以可以调换顺序 右 根 左
        this.k = k;
        right_root_left(root);
        return target;
    }

    void right_root_left(TreeNode root){
        if(root == null || k<=0){
            return;
        }

        right_root_left(root.right);
        k--;
        if(k == 0){
            target = root.val;
        }
        right_root_left(root.left);
    }
}

55-Ⅰ.二叉树的深度

问题描述:

输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。
给定二叉树 [3,9,20,null,null,15,7],
    3
   / \
  9  20
    /  \
   15   7
返回它的最大深度 3 。

思路:

利用递归,选择左右子树中最深的值加1(根节点)
  • 代码实现
/**
 * 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;
        int left = maxDepth(root.left);
        int right = maxDepth(root.right);

        return Math.max(left,right) + 1;
    }
}

55-Ⅱ.平衡二叉树

问题描述:

输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。
给定二叉树 [3,9,20,null,null,15,7]
    3
   / \
  9  20
    /  \
   15   7
返回 true 

思路;

计算左右子树的深度,绝对值相减小于等于1的满足,并计算左右子树是否也满足

时间nlogn 空间n
  • 代码实现
/**
 * 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(root == null) return true;
        int l = maxDepth(root.left);
        int r = maxDepth(root.right);

        return Math.abs(l - r) <= 1 && isBalanced(root.left) && isBalanced(root.right);
    }

    public int maxDepth(TreeNode root) {
        if(root == null) return 0;
        int left = maxDepth(root.left);
        int right = maxDepth(root.right);

        return Math.max(left,right) + 1;
    }
}
但是上述代码实现存在多处重复操作,已经查找完深度之后判断的,到了左右子树再判断,所以可以直接在查找深度时便判断,若不满足则直接返回

下述进行剪枝操作,时间n空间n
  • 代码实现
/**
 * 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(root == null) return true;
        if(maxDepth(root) == -1) return false;

        return true;
    }

    //直接在查找深度时便判断,若不满足则直接返回
    public int maxDepth(TreeNode root) {
        if(root == null) return 0;
        int left = maxDepth(root.left);
        if(left == -1) return -1;
        int right = maxDepth(root.right);
        if(right == -1) return -1;

        if(Math.abs(left - right) > 1) return -1;

        return Math.max(left,right) + 1;
    }
}

56-Ⅰ.数组中数字出现的次数

问题描述:

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

思路:

统计出现次数可以用哈希表做,但此题要求空间复杂度为1,所以可以用位运算

若一个数组中,只有一个数字只出现一次,其他都出现两此,那么所有数字异或,便会只剩下那个数,因为相同数异或等于0

在这里插入图片描述

  • 代码实现
class Solution {
    public int[] singleNumbers(int[] nums) {
        int z = 0;
        //先对所有数进行相与操作
        for(int i = 0;i<nums.length;i++){
            z = z ^ nums[i];
        }

        //找到二进制中最低位相同的数(也就是每个位与z进行相与操作)
        int m = 1;
        //直到等于1,说明该位相等
        while((m & z) == 0){
            m = m << 1; //向左移动一位
        }

        int x = 0,y = 0;

        for(int i = 0;i< nums.length;i++){
            if((nums[i] & m) == 0){
                //相与等于0的子数组,一边异或找出只出现一次的x
                x = x ^ nums[i];
            }else{
                //相与等于1的子数组,一边异或找出只出现一次的y
                y = y ^ nums[i];
            }
        }
        return new int[]{x,y};
    }
}

56-Ⅱ.数组中数字出现的次数Ⅱ

问题描述:

在一个数组 `nums` 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。
输入:nums = [3,4,3,3]
输出:4

思路:

计算每个二进制位上数组中所有数字的数值为1的个数,除余3(对于出现三次的数字,各 **二进制位** 出现的次数都是 33 的倍数),便是出现一次的数字在该二进制位的值

时间n,空间1(32)

在这里插入图片描述

  • 代码实现
class Solution {
    public int singleNumber(int[] nums) {
        //因为int有32位,所以给予32个
        int[] res = new int[32];
        //二进制的第几位
        int m = 1;
        //已经计算的二进制位相加,最后结果便是出现一次的数字
        int sum = 0;

        for(int i = 0;i<32 ;i++){
            for(int j = 0;j<nums.length;j++){
                //数值在给位的二进制是1还是0
                //是1则累加,统计所有数值在该位上1的个数
                if((nums[j] & m) != 0){
                    //数组默认值为0,所以不用给予初始值
                    res[i]++;
                }
            }

            //该位上的所有数值的二进制为1的个数除余3便是只出现一次的数字该位的二进制值
            res[i] = res[i] % 3;
            sum = sum + res[i] * m;
            m = m << 1;
        }
        return sum;
    }
}

57.和为target的两个数

问题描述:

输入一个递增排序的数组和一个数字target,在数组中查找两个数,使得它们的和正好是target。
如果有多对数字的和等于target,则输出任意一对即可。
输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]

思路:

利用双指针,若相加大于target,则右指针左移
  • 代码实现
class Solution {
    public int[] twoSum(int[] nums, int target) {
        if(nums == null || nums.length < 2){
            return new int[0];
        }

        int i = 0,j = nums.length - 1;
        while(i < j){
            if(nums[i] + nums[j] > target){
                j--;
            }else if(nums[i] + nums[j] < target){
                i++;
            }else{
                return new int[]{nums[i],nums[j]};
            }
        }
        return new int[0];
    }
}

57-Ⅱ.和为s的连续正数序列

问题描述:

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

序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。
输入:target = 9
输出:[[2,3,4],[4,5]]

思路:

利用滑动窗口,初始时左右边界都为1,若此时滑动窗口的值小于target则往右扩大,大于则缩小左边界

小小优化:等于target值时存好窗口值后缩小左边界,此时的值必定小于taget,所以可以即可扩大右边界,少一步判断

注意左边界的值不得超过taget除于2,因为题目表明至少含有两个数
  • 代码实现
class Solution {
    public int[][] findContinuousSequence(int target) {
        List<int[]> res = new ArrayList<>();
        //滑动窗口的左右边界
        int i = 1,j = 1;
        int sum = 1;
        //因为题目表明至少含有两个数
        while(i <= target/2){
            if(sum < target){
                //扩大右边界
                j++;
                sum = sum + j;
            }else if(sum > target){
                //缩小左边界
                sum = sum - i;
                i++;
            }else{
                int[] temp = new int[j - i +1];
                int index = 0;
                for(int k = i;k <= j;k++){
                    temp[index++] = k;
                }
                //缩小左边界,向右滑动
                sum = sum - i;
                i++;
                //此时sum值必定小于target,所以可以直接扩大右边界,减少一步判断
                j++;
                sum = sum + j;
                res.add(temp);
            }
        }
        return res.toArray(new int[res.size()][]);
    }
}

58-Ⅰ.翻转单词顺序

问题描述:

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

说明:

  无空格字符构成一个单词。

输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

  • 示例
输入: "the sky is blue"
输出: "blue is sky the"
输入: "  hello world!  "
输出: "world! hello"
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。

输入: "a good   example"
输出: "example good a"
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

时间n空间n(用来StringBuilder)
  • 代码实现
class Solution {
    public String reverseWords(String s) {
        //可以用split库函数做,但不建议

        if(s == null || s.length()<=0){
            return s;
        }
        //删除两端的空格
        s = s.trim();
        StringBuilder build = new StringBuilder();
        //从后往前
        int i = s.length() - 1,j=i;

        while(j >= 0){
            while(i >= 0 && s.charAt(i) != ' ') i--;
            //[i+1,j] 
            //substring()左闭右开
            build.append(s.substring(i+1,j+1) + " ");
            //消除单词间多余的空格
            while(i >= 0 && s.charAt(i) == ' ') i--;
            j = i;
        }
        //最后一个单词后面会有多余空格
        return build.toString().trim();
    }
}

58-Ⅱ.左旋转字符串

问题描述:

字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。
请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
输入: s = "abcdefg", k = 2
输出: "cdefgab"
因为java中字符串是无法改变的,所以一般没有什么骚操作

时间n,空间n
  • 代码实现
class Solution {
    public String reverseLeftWords(String s, int n) {
        StringBuilder build = new StringBuilder();
        int len = s.length();

        //先拼接前面部分
        for(int i = n;i<len;i++){
            build.append(s.charAt(i));
        }
        //再拼接后半部分
        for(int i = 0;i<n;i++){
            build.append(s.charAt(i));
        }

        //另一种做法:
        // for(int i = n;i<len + n;i++){
        //      build.append(s.charAt(i % len));
        //  }
        
        return build.toString();
    }
}

59-Ⅰ.滑动窗口的最大值

问题描述:

给定一个数组 `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

思路:

用一个队列存放滑动窗口中可能为最大值元素的索引

存放进去的数若大于队尾元素,则队尾元素剔除,直到队首元素为此时的最大值

当(队尾索引-k=队首索引)队首元素便是在滑动窗口之外,将此数剔除

时间n(while只用了k)  空间k
  • 代码实现
class Solution {
    //队列先进先出,从队尾进队首出
    public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums == null || nums.length <= 1){
            return nums;
        }

        //用队列存放滑动窗口内可能成为最大值元素的下标
        LinkedList<Integer> queue = new LinkedList<>();
        //存放最大值
        int[] res = new int[nums.length - k + 1];
        int index = 0;

        for(int i = 0;i < nums.length;i++){
            //队尾元素小于此时的数,则队尾元素剔除
            while(!queue.isEmpty() && nums[queue.peekLast()] <= nums[i]){
                queue.pollLast();
            }

            queue.add(i);

            //判断此时队首元素是否还在滑动窗口内
            //队尾索引-k = 队首索引,队首便是在滑动窗口外的元素
            if(queue.peekLast() - k == queue.peek()){
                queue.poll();
            }
            //队首元素永远是最大的
            if(i + 1 >= k){
                res[index++] = nums[queue.peek()];
            }
        }
        return res;
        
    }
}

60.n个骰子的点数

问题描述:

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

你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率
输入: 2
输出: [0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778]
 
限制:
1 <= n <= 11

思路:

动态规划

dp[i][j]:当骰子个数为i,点数为j时,一定有dp[i][j]种组合

[剑指 Offer 60. n个骰子的点数 - 力扣(LeetCode)](https://leetcode.cn/problems/nge-tou-zi-de-dian-shu-lcof/)
  • 代码实现
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=1;j <= 6*i;j++){
                for(int k=1;k<=6;k++){
                    if(j < k) break;
                    dp[i][j] += dp[i-1][j-k];
                }
            }
        }

        double[] res = new double[5*n +1];
        int index = 0;
        double sum = Math.pow(6,n);

        for(int i=n;i<=6*n;i++){
            res[index++] = dp[n][i] / sum;
        }

        return res;
    }
}

61.扑克牌中的顺子

问题描述:

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

数组长度为 5 

数组的数取值为 [0, 13] .
输入: [0,0,1,2,5]
输出: True

思路:

如果要成为一个顺子,则需要 满足两个条件:

   1、不存在有重复的数,大小王除外

   2、最大值 - 最小值 < 5

时间n 空间n
  • 代码实现
class Solution {
    public boolean isStraight(int[] nums) {
        //法一:排序法nlogn
        //法二:集合 时间n 空间n
        // 如果要成为一个顺子,则需要 满足两个条件
        // 1、不存在有重复的数,大小王除外
        // 2、最大值 - 最小值 < 5,大小王除外
        Set<Integer> set = new HashSet<>();
        int max = -1,min = 20;
        for(int i = 0;i<nums.length;i++){
            if(nums[i]==0) continue;
            //出现重复值
            if(set.contains(nums[i])) return false;
            set.add(nums[i]);
            max = Math.max(nums[i],max);
            min = Math.min(nums[i],min);
        }
        
        return max - min < 5;
    }
}

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

问题描述:

0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字
(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字

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

思路:

法一:链表,当cur.next等于本身时便是结果,时间n*m

法二:递归:

  每次删除某一个士兵后,对这些士兵重新编号,然后难点在于找出删除前和删除后士兵编号的映射关系

  递归函数的定义:f(n,m):表示士兵个数为n时,存活的士兵编号为f(n,m)

  难点:找出f(n,m)与f(n-1,m)的关系

  (当前index + m) % 上一轮剩余数字的个数
  • 代码实现
class Solution {
    //约瑟夫环问题
    public int lastRemaining(int n, int m) {
        if(n == 0) return n;
        //时间n 空间n
        //return (lastRemaining(n-1,m) + m) % n;

        //优化,从下往上,迭代的方式
        //时间n,空间1
        int res = 0;
        for(int i=1;i<=n;i++){
            res = (res + m) % i;
        }
        return res;
    }
}

63.股票的最大利润

问题描述:

假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

思路:

遍历数组,若此时的值小于最小值,则尝试买入(保存此时的值为最小值),
遇到比最小值要大的值则卖出,计算差值,保存最大值
  • 代码实现
class Solution {
    public int maxProfit(int[] prices) {
        int min = Integer.MAX_VALUE;
        int max = 0;

        for(int i=0;i<prices.length;i++){
            if(prices[i] < min){
                //此时的值小于最小值则买入
                min = prices[i];
            }else{
                //此时的值大于最小值则卖出
                max = Math.max(max,prices[i] - min);
            }
        }
        return max;
    }
}

64.求1+2+…+n

问题描述:

求 `1+2+...+n` ,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)
输入: n = 3
输出: 6

思路:

采用与或门,实际上计算机底层逻辑也是运用与或门来判断是否继续执行
  • 代码实现
class Solution {
    int sum = 0;
    public int sumNums(int n) {
        //与或门 只有前面那个符合条件才会执行后面的
        //并且注意返回值为boolean,所以后面式子随便弄一个>1使之为true
        boolean flag = n>=1 && sumNums(n - 1) > 1;
        sum = sum + n;
        return sum;
    }
}

68-Ⅰ.二叉搜索树的最近公共祖先

问题描述:

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

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

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

在这里插入图片描述

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

输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。

思路:

二叉搜索树,比根节点大的在右边,小的在左边

情况一:两个节点都比根节点小,则可以把组先缩小到左结点

情况二:两个节点都比根节点大,则可以把组先缩小到右结点

情况三:左右节点分布在左右子树,则祖先便是根节点
  • 代码实现
/**
 * 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 = root.left;
            }else if(root.val < p.val && root.val < q.val){
                //两个节点都比根节点大,则可以把组先缩小到右结点
                root = root.right;
            }else{
                //左右节点分布在左右子树,则祖先便是根节点
                return root;
            }
        }
        //未找到返回null
        return null;
    }
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值