剑指OFFER

剑指 Offer 03. 数组中重复的数字

在这里插入图片描述

可以用map,set等方法,但是消耗大。时间空间都是O(n)
也可以先排序,再遍历.时间O(n*logn)

用辅助数组标记对应位置上出现的次数
因为限制了数字出现的范围,所以辅助数组的大小也可以确定
时间空间都是O(n)

class Solution {
    public int findRepeatNumber(int[] nums) {
//        辅助数组
        int[] arr = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
            arr[nums[i]]++;
            if (arr[nums[i]] > 1) {
                return nums[i];
            }
        }
        return -1;
    }
}

上面利用了一个辅助数组用于数字nums[i]和放入到arr[nums[i]]的位置上,进而判断arr数组索引为nums[i]的出现的次数来判断重复元素
也可以不借助辅助数组,因为可以利用原数组nums的索引:
逐步遍历元素,如果它的值没有出现再对应的索引上,则放到索引上
这样,就会把第一次出现的值全部和索引对应
如果再出现值已经出现再了对应索引上,说明重复了
在这里插入图片描述

在这里插入图片描述
一开始如图所示,上面的是初始数组,下面的是索引
i=0时,已经值和索引是对应的,就跳过
i=1时,不对应,但是值2对应的索引为2的值时5,说明没有重复,则把它放在索引为2的上面
在这里插入图片描述
此时到i=1,把nums[1]的5再放到nums[5]上
得到0 0 2 3 2 5
此时i=1,num[1]=1,它和对应索引上的值一样,说明已经出现过了,重复

class Solution {
    public int findRepeatNumber(int[] nums) {
        int i = 0;
        while (0 < nums.length) {
            //值和索引是本来就是有序的,跳过
            if (nums[i] == i) {
                i++;
                continue;
            }
            //出现别的索引上的值(没拍好的)和值对应的索引上的值(排好了的)一样,重复
            if (nums[i] == nums[nums[i]]) {
                return nums[i];
            }
            //没发生重复,也没有本来就排好,手动把它放到对应索引的位置上
            int temp = nums[i];
            nums[i] = nums[temp];
            nums[temp] = temp;
        }
        return -1;
    }
}

剑指 Offer 04. 二维数组中的查找

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

剑指 Offer 05. 替换空格

在这里插入图片描述

方法一:利用list或者StringBuffer遍历并逐个加入
时间复杂度可空间复杂度都是O(n)

class Solution {
    public String replaceSpace(String s) {
        StringBuilder sb = new StringBuilder();
        char[] c = s.toCharArray();
        for(char ch : c){
            if(ch!=' '){
                sb.append(ch);
            }else{
                sb.append("%20");
            }
        }
        return sb.toString();
    }
}

方法二:原地拓展数组,双指针处理。空间复杂度就变成了O(1)——只有C++可以原地拓展。但是JAVA也可以实现,不过空间复杂度还是O(n),不过不再是从前往后一个一个填充了,而是从后往前

在这里插入图片描述

class Solution {
    public String replaceSpace(String s) {
        //有多少个空格,扩充数组
        int count = 0;
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == ' ') {
                count++;
            }
        }
        char[] arr = new char[s.length() + 2 * count];

        //从后面开双指针替换
        for (int i = s.length() - 1, j = arr.length - 1; i >=0; i--, j--) {
            if (s.charAt(i) == ' ') {
                arr[j] = '0';
                arr[j - 1] = '2';
                arr[j - 2] = '%';
                j-=2;
            } else {
                arr[j] = s.charAt(i);
            } 
        }
        return new String(arr);
    }
}

剑指 Offer 06. 从尾到头打印链表

在这里插入图片描述

主要就是用LinkedList
很简单,就是学习以下List转为int[]

    public int[] reversePrint(ListNode head) {
        //直接插入到list的头部
        // LinkedList<Integer> list = new LinkedList<>();
        // while (head != null) {
        //     list.addFirst(head.val);
        //     head = head.next;
        // }
        //这个转换用的时间多,所以不如下面直接遍历来的块
        // int[] res = list.stream().mapToInt(Integer::intValue).toArray();
        // return res;

        //利用栈(其实和上面是一样的)
        LinkedList<Integer> stack = new LinkedList<Integer>();
        while(head != null) {
            stack.addLast(head.val);
            head = head.next;
        }
        int[] res = new int[stack.size()];
        for(int i = 0; i < res.length; i++)
            res[i] = stack.removeLast();
        return res;
    }

剑指 Offer 07. 重建二叉树

z

这个题已经可以独立做出来了

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public TreeNode buildTree(int[] preorder, int[] inorder) {
          //把中序遍历的值和位置存入map
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < inorder.length; i++) {
            map.put(inorder[i], i);
        }
        return buildHelp(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1,map);
    }

    private TreeNode buildHelp(int[] preorder, int pl, int pr, int[] inorder, int il, int ir,HashMap<Integer, Integer> map) {
        if (pl > pr || il > ir) {
            return null;
        }
        TreeNode root = new TreeNode(preorder[pl]);
        Integer index = map.get(root.val);
        int countLeft = index - il;
        root.left = buildHelp(preorder, pl + 1, pl + countLeft, inorder, il, index - 1, map);
        root.right = buildHelp(preorder, pl + countLeft + 1, pr, inorder, index + 1, ir, map);
        return root;
    }
}

剑指 Offer 09. 用两个栈实现队列

class CQueue {

    Deque<Integer> stack1;
    Deque<Integer> stack2;
    
    public CQueue() {
        stack1 = new ArrayDeque<>();
        stack2 = new ArrayDeque<>();
    }

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

    public int deleteHead() {
        if (stack2.isEmpty()) {
            if (stack1.isEmpty()) {
                return -1;
            }
            while (!stack1.isEmpty()) {
                stack2.push(stack1.poll());
            }
        }
        Integer poll = stack2.poll();
        return poll;
    }
}

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

剑指 Offer 10- I. 斐波那契数列

在这里插入图片描述

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

剑指 Offer 10- II. 青蛙跳台阶问题

在这里插入图片描述

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

剑指 Offer 11. 旋转数组的最小数字

在这里插入图片描述

因为是两部分有序数组——二分
数组的最后一个是第二个数组的最大值,也就是说
1.它<=第一个数组的所有数
2.它>=第二个数组所有的数
所以可以使用二分法和他比较,逐步缩小第二个数组第一个元素所在的区间

举例:345123
mid为5,5》3,所以所在区间一定再[mid+1,j]
mid为2,2<3,所以所在区间一定再[i,mid]注意这个包含mid
mid为1,小于2,所以所在区间一定再[i,mid]
此时i==j,退出循环,返回i的值

注意,有一种清空,当num[mid]==num[j]
比如0,1,1或者1,0,0
第一种清空,在mid的左区间[i,mid],第二种情况是[mid,j]。所以不确定该怎么办
这就应当执行j-1,因为即使把num[j]删除了,还有num[mid]保存了他的信息,防止num[j]就是最小值的情况

class Solution {
    public int minArray(int[] numbers) {
        int i = 0, j = numbers.length - 1;
        while (i < j) {
            int mid = i + (j - i)/2;
            if (numbers[mid] > numbers[j]) {
                i = mid + 1;
            } else if (numbers[mid] < numbers[j]) {
                j = mid;
            } else {
                j--;
            } 
        }
        return numbers[i];
    }
}

剑指 Offer 12. 矩阵中的路径

在这里插入图片描述

明显使用递归逐步判断是否符合
思路:遍历从二维数组从i,j开始和word的每一位位进行比较
1.比较不成功,直接false
2.比较成功,且比较到了最后一位,全部成功,返回true
3.成功,但没比较晚,递归i,j的上下左右和word的下一位进行比较
(1)如果新位置越界了,false
(2)每越界,但是已经访问过了的就不能再访问(访问辅助数组)

class Solution {
public boolean exist(char[][] board, String word) {
        int h = board.length;//高
        int w = board[0].length;//宽
        boolean[][] visited = new boolean[h][w];//记录i,j是否被访问过

        //遍历每一个ij,看从他们开始是否有符合的
        for (int i = 0; i < h; i++) {
            for (int j = 0; j < w; j++) {
                //判断从i,j开始,校验从0开始包括0之后的字符是否符合board
                boolean check = check(board, i, j, visited, word, 0);
                if (check) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 从i,j开始,校验从k开始包括k之后的字符是否符合board
     * @param board
     * @param i
     * @param j
     * @param visited
     * @param word
     * @param k 该校验的word的位置
     * @return
     */
    private boolean check(char[][] board, int i, int j, boolean[][] visited, String word, int k) {
        //1.比较i,j上的元素和k的元素匹配不成功:失败,从下一个i,开始匹配
        if (board[i][j] != word.charAt(k)) {
            return false;
        } else if (k == word.length() - 1) {//2.匹配成功,且匹配到了word的最后一个元素:成功
            return true;
        }

        //3.匹配成功,继续匹配i,j的上下左右是否和k+1匹配
        visited[i][j] = true;//记录当前的i,j访问过滤,防止之后的递归探测访问
        int[][] dirs = new int[][]{{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
        for (int[] dir : dirs) {
            int newi = i + dir[0];
            int newj = j + dir[1];
            //不越界说明新的方法可探测
            if (newi >= 0 && newi < board.length && newj >= 0 && newj < board[0].length) {
                //新坐标没有被探测过
                if (!visited[newi][newj]) {
                    boolean check = check(board, newi, newj, visited, word, k + 1);
                    //当前i,j开始不光k匹配成功,K+1之后的全部匹配成功
                    if (check) {
                        //恢复访问
                        visited[i][j] = false;
                        return true;
                    }
                }
            }
        }
        //i,j虽然和k匹配成功,但是之后的没有成功
        visited[i][j] = false;
        return false;
    }
}

面试题13. 机器人的运动范围

在这里插入图片描述

思路:广度遍历
1.因为从0,0开始走,所以只能走右边和下,所以只需要一直向这两个方向走就可以了
2.访问数组,用于记录走过的格子
3.把走过的格子放入队列,然后出队,进行向右、下两个方向走,如果新位置合理,则加入队列
4.判断新位置合理:
(1)位数和
(2)是否越界

class Solution {
public int movingCount(int m, int n, int k) {
        if (k == 0) {
            return 1;
        }
        boolean[][] visited = new boolean[m][n];
        int res = 1;
        //广度优先遍历需要的数组
        Queue<int[]> queue = new LinkedList<>();
        //向右和向下的方向数组
        //1,0和0,1是因为后边向右和向下走的时候好分开进行走
        //因为算法从0,0开始走,所以只有向下和向上就行
        int[] right = new int[]{1, 0};
        int[] down = new int[]{0, 1};
        //初始坐标进入
        queue.offer(new int[]{0, 0});
        visited[0][0] = true;
        while (!queue.isEmpty()) {
            int[] poll = queue.poll();
            //向右和向下走
            int x = poll[0];
            int y = poll[1];
            for (int i = 0; i < 2; i++) {
                int tx = right[i] + x;
                int ty = down[i] + y;
                //判断走后的新位置是否合适
                if (tx < 0 || tx >= m || ty < 0 || ty >= n || get(tx) + get(ty) > k || visited[tx][ty]) {
                    continue;
                }
                queue.offer(new int[]{tx, ty});
                visited[tx][ty] = true;
                res++;
            }
        }
        return res;
    }

    //传入一个数,得到他的位数和
    public int get(int x) {
        int res = 0;
        while (x > 0) {
            res += x % 10;
            x = x / 10;
        }
        return res;
    }
}

剑指 Offer 14- I. 剪绳子

在这里插入图片描述

(1)数学推导

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

public int cuttingRope(int n) {
        //1.n=2,1*1;n=3,1*2
        if (n <= 3) {
            return n - 1;
        }
        int a = n / 3;//能切分的长度为3的最大段数
        int b = n % 3;//切分之后,最后一段的长度
        //2.最后一段长度为1,把前面一个长度为3的分出来个1给它,变成2
        if (b == 1) {
            return (int) (Math.pow(3, a - 1) * 2 * 2);
        }
        //3.最后一段长度为2,直接计算
        if (b == 2) {
            return (int) (Math.pow(3, a) * 2);
        } else {
            //4.最后一段长度为3
            return (int) Math.pow(3, a);
        }
    }

(2)动态规划

长度i可以分解为i和i-j
dp[i]表示将正整数 ii 拆分成至少两个正整数的和之后,这些正整数的最大乘积。特别地,0 不是正整数,1 是最小的正整数,0 和 1 都不能拆分,因此 dp[0]=dp[1]=0。
在这里插入图片描述

    public int cuttingRope(int n) {
        //dp[i]存储i拆分后的最大成绩,即dp[n]为所求
        int[] dp = new int[n + 1];
        //从n=2开始拆
        for (int i = 2; i <= n; i++) {
            int max = 0;//n=i时,最大乘积,即dp[i]=max
            // i分成j和i-j
            for (int j = 1; j < i; j++) {
                max = Math.max(max, Math.max(j * (i-j), j * dp[i - j]));
            }
            dp[i] = max;
        }
        return dp[n];
    }

剑指 Offer 15. 二进制中1的个数

在这里插入图片描述

位运算
在这里插入图片描述

(1)位运算的方法

一个数字n,底层表示的是一串二进制。
所以当n&1进行运算时是n的二进制表示的最右位和1进行&运算
若为1,则得到的也是1
然后将n>>1做向右移一位的操作(右边第二位变成右边第一位)进行比较即可

    // you need to treat n as an unsigned value
    public int hammingWeight(int n) {
        int ret = 0;
        for (int i = 0; i < 32; i++) {
            if ((n & 1) == 1) {
                ret++;
            }
            n = n >> 1;
        }
        return ret;
    }

把整数右移一位和把整数除以2在数学上是等价的,那上面的代码中可以把右移运算换成除以2吗?

不能。
位移运算比除法效率低得多

(2)优化

在(1)中是让n不断的右移,但是这种方法在面对有符号数时的负数时会出现问题
负数的最高位永远是1
所以当右移的时候,最高位永远会补1,也就会陷入循环
为了避免死循环,我们可以不右移输入的数字i。首先把i和1做与运算,判断i的最低位是
不是为1。接着把1左移一位得到2,再和i做与运算,就能判断i的次低位是不是1……这样反复
左移,每次都能判断i的其中一位是不是1。

1<<0:得到1,1和n做&判断最右位是不是1
1<<1:得到10,10和n做&判断右边第二位是不是1
1<<2:得到100,。。。。。。。

    // you need to treat n as an unsigned value
    public int hammingWeight(int n) {
        int ret = 0;
        for (int i = 0; i < 32; i++) {
            if ((n & (1 << i)) != 0) {
                ret++;
            }
        }
        return ret;
    }

(3)利用位&计算的技巧

当用n-1,若最右位为1.则最右位变为0;若第m位为1,右边全为0,则变为m位为0,右边全为1
此时发现,n-1得到的是从最右侧的第一个1开始(m),m和其右侧的全变为取反
所以若n&(n-1)得到的是将n的最右侧的第一个1变为0(右边全部都是0)
即能做几次n&(n-1),就有几个1

    public int hammingWeight(int n) {
        int res = 0;
        while (n != 0) {
            n = n & (n - 1);
            res++;
        }
        return res;
    }

剑指 Offer 16. 数值的整数次方

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

    public double myPow(double x, int n) {
        if (n == 0) {
            return 1;
        }
        if (n == 1) {
            return x;
        }
        if (n == -1) {
            return 1 / x;
        }
        double half = myPow(x, n / 2);
        double mod = myPow(x, n % 2);
        return half * half * mod;
    }

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

在这里插入图片描述

这道题就是打印1-10^n-1,很简单
但是需要考虑大数(先没看)
此处先不考虑解决

class Solution {
    public int[] printNumbers(int n) {
        //最大的数
        int end = (int)Math.pow(10,n)-1;
        int[] res = new int[end];
        for(int i =0;i<end;i++){
            res[i]=i+1;
        }
        return res;
    }
}

剑指 Offer 18. 删除链表的节点

//创建空头节点
class Solution {
    public ListNode deleteNode(ListNode head, int val) {
        ListNode MyHead = new ListNode(-1);
        MyHead.next = head;
        ListNode pre = MyHead;
        ListNode p = head;
        while(p!=null){
            if(p.val==val){
                pre.next =p.next;
                break;
            }
            pre = p;
            p = p.next;
        }
        return MyHead.next;
    }
}
//先判断头节点,就不用创建空头节点了
class Solution {
    public ListNode deleteNode(ListNode head, int val) {
        if(head.val == val) return head.next;
        ListNode pre = head, cur = head.next;
        while(cur != null && cur.val != val) {
            pre = cur;
            cur = cur.next;
        }
        if(cur != null) pre.next = cur.next;
        return head;
    }
}


剑指 Offer 19. 正则表达式匹配

在这里插入图片描述

使用动态规划
dp[0][0]表示两个字符串都是空的时候是否匹配,自然是匹配的.设为1
dp[i][j]表示s的第i-1和p的j-1以及之前的是否匹配

初始化:
dp[0][0]为0
因为s为空,p必须是值*值*的这种形式
所以必须初始化dp第一行,当j=2,4等偶数时,必须为*
即dp[0][j] = dp[0][j - 2] && p[j - 1] = ‘*’
在这里插入图片描述

判别思路:
明确dp[i][j]对应的是字符s[i-1]和p[j-1]
在这里插入图片描述

class Solution {
    public boolean isMatch(String s, String p) {
        //定义dp
        int m = s.length() + 1;
        int n = p.length() + 1;
        boolean[][] dp = new boolean[m][n];

        //1.初始化dp
        //1.1空字符串匹配成功
        dp[0][0] = true;
        //1.2s为空时,p能否匹配成功(填充第一行)
        //从2开始即从p的第二个字符(p.charAt(1))是因为p的第一个肯定不匹配
        for (int j = 2; j < n; j += 2) {
            dp[0][j] = dp[0][j - 2] && p.charAt(j - 1) == '*';
        }

        //2.正式比较
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = p.charAt(j - 1) == '*' ?
                        dp[i][j - 2] || (dp[i - 1][j] && s.charAt(i - 1) == p.charAt(j - 2)) || (dp[i - 1][j] && p.charAt(j - 2) == '.') :
                        (dp[i - 1][j - 1] && s.charAt(i - 1) == p.charAt(j - 1)) || (dp[i - 1][j - 1] && p.charAt(j - 1) == '.');
            }
        }
        return dp[m - 1][n - 1];
    }
}

剑指 Offer 20. 表示数值的字符串

在这里插入图片描述

确定有限状态自动机
1.列出所有满足业务的内部状态,得到状态集合
2.每一个新的字符加入,看能否到达列出来的状态。如果可以,接着下一个字符;如果不行,直接false
3.如果遍历字符完毕,需要判断当前状态是否是接收状态:最终满足条件的状态,如果是,则结束,不然false

如何定义状态集合
根据每一步的操作,定义可能出现的状态
注意,只要满足业务的可能状态都要列举
注意:本题规定允许4.5、.6、4.这四个状态的小数出现
在这里插入图片描述

画出状态转换图
在这里插入图片描述

public boolean isNumber(String s) {
        Map[] states = {
                //数组0:从状态0可以到的别的状态为:0还是空格、2整数部分、1符号位、4左边无整数的小数点
                new HashMap<Character, Integer>() {{
                    put(' ', 0);
                    put('s', 1);
                    put('d', 2);
                    put('.', 4);
                }},
                //数组1:从状态1可以到的状态:2整数、4左边没有整数的小数点
                new HashMap<Character, Integer>() {{
                    put('d', 2);
                    put('.', 4);
                }},
                //数组2:从状态2可到的状态:2整数、3左侧有整数的小数点、6字符e、9空格末尾
                new HashMap<Character, Integer>() {{
                    put('d', 2);
                    put('.', 3);
                    put('e', 6);
                    put(' ', 9);
                }},
                //数组3:从状态3可到的状态:5小数部分、6字符e、9空格末尾
                new HashMap<Character, Integer>() {{
                    put('d', 5);
                    put('e', 6);
                    put(' ', 9);
                }},
                //数组4:从状态4可到的状态:5小数部分
                new HashMap<Character, Integer>() {{
                    put('d', 5);
                }},
                //数组5:从状态5可到的状态:5小数部分、6字符e、9空格末尾
                new HashMap<Character, Integer>() {{
                    put('d', 5);
                    put('e', 6);
                    put(' ', 9);
                }},
                //数组6:从状态6可到的状态:7指数符号位、8指数部分的整数部分
                new HashMap<Character, Integer>() {{
                    put('s', 7);
                    put('d', 8);
                }},
                //数组7:从状态7可到的状态:8指数部分的整数部分
                new HashMap<Character, Integer>() {{
                    put('d', 8);
                }},
                //数组8:从状态8可到的状态:8指数部分的整数部、9空格末尾
                new HashMap<Character, Integer>() {{
                    put('d', 8);
                    put(' ', 9);
                }},
                //数组9:从状态9可到的状态:9空格末尾
                new HashMap<Character, Integer>() {{
                    put(' ', 9);
                }}
        };

        int p = 0;//初始状态0
        char t;
        for (char c : s.toCharArray()) {
            if (c >= '0' && c <= '9') t = 'd';
            else if (c == '+' || c == '-') t = 's';
            else if (c == 'e' || c == 'E') t = 'e';
            else if (c == '.' || c == ' ') t = c;
            else t = '?';
            //如果当前状态下没有执行操作t可到达的状态,false
            if (!states[p].containsKey(t)) return false;
            //到达可到达的状态
            p = (int) states[p].get(t);
        }
        //最终状态是2,3,6,8,9处于接收状态
        return p == 2 || p == 3 || p == 5 || p == 8 || p == 9;
    }

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

在这里插入图片描述

双指针:
前后指针一起搜索,各找到属于对方的数,再一起交换

    public int[] exchange(int[] nums) {
        int i = 0, j = nums.length - 1;
        while (i < j) {
            while (i < j && nums[i] % 2 != 0) {
                i++;
            }
            while (i < j && nums[j] % 2 == 0) {
                j--;
            }
            swap(nums, i, j);
        }
        return nums;
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

剑指 Offer 22. 链表中倒数第k个节点

在这里插入图片描述

class Solution {
    public ListNode getKthFromEnd(ListNode head, int k) {
        ListNode q = head;
        ListNode p = head;
        while (k > 1) {
            q = q.next;
            k--;
        }
        while (q.next != null) {
            p = p.next;
            q = q.next;
        }
        return p;
    }
}

剑指 Offer 24. 反转链表

在这里插入图片描述

从前往后反转:
记录当前节点的下一个节点
当前节点指向前一个节点
前一个节点更新
当前节点更新

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode cur = head;
        ListNode pre = null;
        while (cur != null) {
            ListNode p = cur.next;
            cur.next = pre;
            pre = cur;
            cur = p;
        }
        return pre;
    }
}

递归
从后往前进行反转

class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode last = reverseList(head.next);
        head.next.next = head;
        head.next = null;
        return last;
    }
}

剑指 Offer 25. 合并两个排序的链表

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode head = new ListNode(-1);
        ListNode pre = head;
        ListNode p = l1;
        ListNode q = l2;
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                pre.next = l1;
                pre = l1;
                l1 = l1.next;
            } else {
                pre.next = l2;
                pre = l2;
                l2 = l2.next;
            }
        }
        if (l1 != null) {
            pre.next = l1;
        }
        if (l2 != null) {
            pre.next = l2;
        }
        return head.next;
    }
}

剑指 Offer 26. 树的子结构

在这里插入图片描述

先判断两个树的根节点是否一样

  • 如果一样,判断这两个树的子树是否一样(知道B为null)
  • 如果不一样,判断A.left和B的根节点是否一样
  • 再不一样,判断A.right和B的根节点是否一样
public boolean isSubStructure(TreeNode A, TreeNode B) {
        boolean res = false;
        if (A != null && B != null) {
            if (A.val == B.val) {
                //如果根节点一样,判断子树们是否一样
                res = leftAndRight(A, B);
            }
            if (!res) {
                //A的根几点改变
                res = isSubStructure(A.left, B);
            }
            if (!res) {
                res = isSubStructure(A.right, B);
            }
        }
        return res;
    }

    private boolean leftAndRight(TreeNode A, TreeNode B) {
        //成功条件
        if (B == null) {
            return true;
        }
        if (A == null) {
            return false;
        }
        if (A.val != B.val) {
            return false;
        }
        return leftAndRight(A.left, B.left) && leftAndRight(A.right, B.right);
    }

剑指 Offer 27. 二叉树的镜像

在这里插入图片描述

class Solution {
    public TreeNode mirrorTree(TreeNode root) {
        if (root == null) {
            return null;
        }
        mirrorTree(root.left);
        mirrorTree(root.right);
        TreeNode temp = root.left;
        root.left = root.right;
        root.right = temp;
        return root;
    }
}

剑指 Offer 28. 对称的二叉树

在这里插入图片描述

要注意,不是判断别左右子树是否一样, 而是对称!!!
所以就是判断当前节点的左子树和比对节点的右子树是否一样

    public boolean isSymmetric(TreeNode root) {
        return check(root, root);
    }

    private boolean check(TreeNode p,TreeNode q) {
        if (p == null && q == null) {
            return true;
        }
        if (p == null || q == null) {
            return false;
        }
        return p.val == q.val && check(p.left, q.right) && check(q.left, q.right);
    }

6.剑指 Offer 29. 顺时针打印矩阵

在这里插入图片描述

1.从左向右遍历第一行,在向下——》第一行排除
2.从上到下遍历最后一行,再向左——》最后一列排除
3.从右到左遍历最后一行,再向上——》最后一行排除
4.从下到上遍历第一列,再向右——》第一列排除
可以看出,周围一圈在不断缩小,所以指定四个遍历表示第一行、最后一列、最后一行、第一列,进行循环即可

    public int[] spiralOrder(int[][] matrix) {
        if (matrix.length == 0) {
            return new int[0];
        }
        //返回的数组
        int[] res = new int[matrix.length * matrix[0].length];
        //填充res的位置
        int i = 0;
        //最后一行
        int height = matrix.length-1;
        //最后一列
        int wide = matrix[0].length - 1;
        //第一行
        int x = 0;//行的位置
        //第一列
        int y = 0;//列的位置
        while (true) {
            //从左到右遍历行
            for (int j = y; j <= wide; j++) {
                res[i++] = matrix[x][j];
            }
            //向下移动一行
            if (++x > height) {
                break;
            }
            //从上到下遍历列
            for (int j = x; j <= height; j++) {
                res[i++] = matrix[j][wide];
            }
            //向左移动一列
            if (--wide < y) {
                break;
            }
            //从右向左移动
            for (int j = wide; j >=y ; j--) {
                res[i++] = matrix[height][j];
            }
            //向上移动一行
            if (--height < x) {
                break;
            }
            for (int j = height; j >=x; j--) {
                res[i++] = matrix[j][y];
            }
            //向右移动一行
            if (++y > wide) {
                break;
            }
        }
        return res;
    }

剑指 Offer 30. 包含min函数的栈

在这里插入图片描述

辅助栈来维护栈中的最小值
辅助站的栈顶元素永远是栈的最小值

  • push的x比辅助站顶元素小,加入
  • 否则继续加入栈顶元素
class MinStack {

       Deque<Integer> stack;
    Deque<Integer> helpStack;

    /**
     * initialize your data structure here.
     */
    public MinStack() {
        stack = new ArrayDeque<>();
        helpStack = new ArrayDeque<>();
        //放入一个最大值
        helpStack.push(Integer.MAX_VALUE);
    }

    public void push(int x) {
        stack.push(x);
        helpStack.push(Math.min(helpStack.peek(), x));
    }

    public void pop() {
        stack.poll();
        helpStack.poll();
    }

    public int top() {
        return stack.peek();
    }

    public int min() {
        return helpStack.peek();
    }
}

上面的方法是每次加入的数小于栈顶元素时候,才加入,否则加入栈顶元素进行占位,方便两个栈同时出栈。
也可以每次当x<=栈顶元素的时候就入栈,然而x>栈顶元素的时候就什么都不入,则这样在出栈的时候判断一下就可以了。两种都可以。注意x<=这个条件主要是为了针对0,1,0这种情况导致的空指针

class MinStack {
    Deque<Integer> stack1;
    Deque<Integer> stack2;
    /** initialize your data structure here. */
    public MinStack() {
        stack1 = new LinkedList<>();
        stack2 = new LinkedList<>();
    }
    
    public void push(int x) {
        stack1.push(x);
        if (stack2.isEmpty() || x <= stack2.peek()) {
            stack2.push(x);
        }
    }

    public void pop() {
        Integer pop = stack1.pop();
        if (stack2.peek().equals(pop)) {
            stack2.pop();
        }
    }

    public int top() {
        return stack1.peek();
    }

    public int min() {
        if(stack2.isEmpty()){
            return -1;
        }
        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();
 */

7.剑指 Offer 31. 栈的压入、弹出序列

在这里插入图片描述

    public boolean validateStackSequences(int[] pushed, int[] popped) {
        Deque<Integer> stack = new ArrayDeque<>();
        for (int i = 0, j = 0; i < pushed.length; i++) {
            stack.push(pushed[i]);
            while (!stack.isEmpty() && stack.peek() == popped[j]) {
                stack.pop();
                j++;
            }
        }
        return stack.isEmpty();
    }

8.剑指 Offer 32 - 从上到下打印二叉树

(1)普通的层次遍历打印

    public int[] levelOrder(TreeNode root) {
        if (root == null) {
            return new int[0];
        }
        Deque<TreeNode> queue = new ArrayDeque<>();
        ArrayList<Integer> list = new ArrayList<>();
        queue.addLast(root);
        while (!queue.isEmpty()) {
            TreeNode p = queue.removeFirst();
            list.add(p.val);
            if (p.left != null) {
                queue.addLast(p.left);
            }
            if (p.right != null) {
                queue.addLast(p.right);
            }
        }
        //把list给数组
        int[] res = new int[list.size()];
        for (int i = 0; i < list.size(); i++) {
            res[i] = list.get(i);
        }
        return res;
    }
}

(2)按每一行打印

在这里插入图片描述

就是需要在外层遍历的时候,记录队列内的数量,一次性把当前队列内的(这一层的)全部取出
关键在于:如果把这一层全部取出是按while (!queue.isEmpty())取,则新放入的下一层放到哪?
所以取的时候应当记录现有的个数,按while (size != 0)

public List<List<Integer>> levelOrder(TreeNode root) {
        if (root == null) {
            return new ArrayList<>();
        }
        List<List<Integer>> list = new ArrayList<>();
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            int size = queue.size();
            List<Integer> list1 = new ArrayList<>();
            while (size != 0) {
                TreeNode p = queue.poll();
                size--;
                list1.add(p.val);
                if (p.left != null) {
                    queue.offer(p.left);
                }
                if (p.right != null) {
                    queue.offer(p.right);
                }
            }
            list.add(list1);
        }
        return list;
    }

(3)不同层不同顺序打印

在这里插入图片描述

1.需要有1个遍历记录层数
2.内层的list采用双端队列,奇数层从后面插入,偶数层从前面插入。最后在转为list类型

public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> list = new ArrayList<>();
        if (root == null) {
            return list;
        }
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        int i = 1;
        while (!queue.isEmpty()) {
            Deque<Integer> list1 = new LinkedList<>();
            int size = queue.size();
            while (size != 0) {
                TreeNode p = queue.poll();
                size--;
                if (i % 2 != 0) {
                    list1.addLast(p.val);
                } else {
                    list1.addFirst(p.val);
                }
                if (p.left != null) {
                    queue.offer(p.left);
                }
                if (p.right != null) {
                    queue.offer(p.right);
                }
            }
            i++;
            list.add((List<Integer>) list1);
        }
        return list;
    }

9.剑指 Offer 33. 二叉搜索树的后序遍历序列

在这里插入图片描述

重点是,不需要你判读是不是后序遍历
就默认是后序,也默认是搜索树,判断是否满足即可

1.划分左右树
2.判断是否满足是搜索树

难点在于如何根据后序遍历划分左右子树,去递归的判断是否符合搜索树

  • 如何划分?
    • root为根的树若是搜索树,他的后序遍历一定是[左孩子的一堆值,有孩子的一堆值,root]所以从头遍历,当遇到第一个比root大的,就是右子树的最左下角,便可以划分左右子树
  • 划分之后如何判断是否是搜索树?
    • 搜索树后序遍历一定是[左孩子的一堆值,有孩子的一堆值,root],所以当按照前面的划分出左右子树之后,继续向后遍历,一直找到不大于root的数。若是搜索树,这个点一定就是他自己。
    public boolean verifyPostorder(int[] postorder) {
        boolean verify = verify(postorder, 0, postorder.length - 1);
        return verify;
    }


    /**
     * 验证局部树是否正确
     *
     * @param postorder 后根次序
     * @param left      左
     * @param right     右
     */
    private boolean verify(int[] postorder, int left, int right) {
        if (left >= right) {
            return true;
        }
        //1.划分左右子树
        int mid = left;
        while (postorder[mid] < postorder[right]) {
            mid++;//(1)mid是右子树的第一个;(2)mid是right
        }
        int p = mid;
        while (postorder[p] > postorder[right]) {
            p++;
        }
        //此时无论那种情况,只要是搜索树,都该是p==right
        boolean leftIs = verify(postorder, left, mid - 1);
        boolean rightIs = verify(postorder, mid, right - 1);
        //2.判断是否满足搜索二叉树
        return leftIs && rightIs && p == right;
    }

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

在这里插入图片描述

List<List<Integer>> res = new LinkedList<>();
    List<Integer> path = new LinkedList<>();
    
    public List<List<Integer>> pathSum(TreeNode root, int target) {
        getPath(root, target);
        return res;
    }

    private void getPath(TreeNode root, int target) {
        if (root == null) {
            return;
        }
        path.add(root.val);
        target -= root.val;
        if (root.left == null && root.right == null && target == 0) {
            // res.add(path);
            // 细节:为什么我要通过构造方法传入path,不能直接res.add(path)
            //      因为直接加入,加入的是引用(指向的堆中数据会变化),而此时的path是个全局变量,需要克隆一份加入
            res.add(new LinkedList<Integer>(path));
        }
        getPath(root.left, target);
        getPath(root.right, target);
        path.remove(path.size()-1);
    }

剑指 Offer 35. 复杂链表的复制

在这里插入图片描述

思路:
不可能利用循环或者递归,逐步的把当前节点的next和random创建和连接
因为这样会重复创建,所以需要map保存关系,那么保存什么关系

如果保存random关系?
问题:不可以先连接next,然后从map中取出来random节点进行连接
因为再进行next连接(第一次遍历复制)的时候,每一次循环,保存的都是原来链表的random关系(因为赋值链表还没有完全创建)
所以再第二次遍历的时候,想要给复制链表连接random关系是不行的
因为:通过原来链表的random关系,找到当前创建好的链表对应的random节点还要再遍历一次

在这里插入图片描述

所以,需要保存的是原来节点和当前节点
第一次遍历可以把新节点们都创建出来,并且map保存对应关系
这样第二次遍历就可以连接next和random,因为新节点的next和random的节点可以通过map.(对应旧节点.next/random)得到

/*
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) {
        Map<Node,Node> map = new HashMap<>();
        Node p = head;
        //存储新旧节点关系
        while (p != null) {
            map.put(p, new Node(p.val));
            p = p.next;
        }
        //第二次遍历,进行连接
        p = head;
        while (p != null) {
            map.get(p).next = map.get(p.next);
            map.get(p).random = map.get(p.random);
            p = p.next;
        }
        return map.get(head);
    }
}

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

在这里插入图片描述

问题1:如何连接左右指针?
中序遍历、使用pre记录中序遍历的前驱节点,逐步连接
问题2:如何把head指向最左节点?
设置前驱节点pre,初始伟null,指向当前节点的前一个节点,如果是最左节点,那么此时pre为null
问题3:头尾指针的关系怎么指向?
递归过程中,并不能解决最后的头为节点的连接,所以需要手动连接。因为最后一次递归,pre指向的是尾指针,就可以方便连接了

    //pre表示前驱节点,head表示头节点
    Node pre, head;

    public Node treeToDoublyList(Node root) {
        if (root == null) {
            return null;
        }
        inOrder(root);
        //配置头尾节点的关系
        head.left = pre;
        pre.right = head;
        return head;
    }


    /**
     * 中序遍历
     * 形成一个链表
     *
     * @param root
     */
    private void inOrder(Node root) {
        if (root == null) {
            return;
        }
        //中序遍历
        inOrder(root.left);
        //找到了最左节点,配置head节点
        if (pre == null) {
            head = root;
        } else {//不是最左的情况,配置pre
            pre.right = root;
            root.left = pre;
        }
        pre = root;
        inOrder(root.right);
    }

12.剑指 Offer 37. 序列化二叉树

在这里插入图片描述

注意点:
1.Node的left是一个成员变量,所以本来就会给一个初始化null。所以在执行queue.add(poll.left);的啥时候放入的是一个Node类型的null。一般的层序遍历要考虑为左右为NULL的时候不把子节点放入队列,是因为层序遍历不需要。而这个题需要放入NULL的情况。
2.如何反序列化?
如果遍历层序遍历数组,会不知道给哪个节点弄左右了。所以需要用队列保存父节点,把队列里的左右节点一个个补齐。null的就不用管了,会自动初始化(成员变量)。
3.如果用了层序遍历,逐个填充,如何知道填充的是数组的第几个?
用变量记录

public class Codec {

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        if (root == null) {
            return "[]";
        }
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("[");
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        while (!queue.isEmpty()) {
            TreeNode poll = queue.poll();
            if (poll == null) {
                stringBuilder.append("null" + ",");
            } else {
                stringBuilder.append(poll.val+",");
                queue.add(poll.left);
                queue.add(poll.right);
            }

        }
        stringBuilder.deleteCharAt(stringBuilder.length() - 1);
        stringBuilder.append("]");
        return stringBuilder.toString();
    }

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

    }
}

13.剑指 Offer 38. 字符串的排列

在这里插入图片描述

思想:
1.第一个位置确定的可能n种,第二个n-1种。。。。
2.先确定x位,然后递归的确定n+1位
3.可以用交换的方式实现不同情况的排序

简单逻辑模拟,abc为例子
1.最外层,确定第一位:a和a交换,a和b交换,a和c交换,三种情况
2.内层,同理的确定第2,3位
在第一位为a的情况,剩下bc,b和b交换,b和c交换
即可得到abc,acb

问题:
aab这种情况
确定第一个位置时候,a先和本身交互,再和第二个a交换,此时aab就出现了两次,所系在每次确定第x位置的时候,需要判断当前要交换的元素是否已经之前进行了交换

class Solution {
char[] c;
    List<String> res = new ArrayList<>();//结果集
    public String[] permutation(String s) {
        c = s.toCharArray();
        dfs(0);
        return res.toArray(new String[res.size()]);
    }

    /**
     * 确定第x位,并递归确定x以后的位
     * @param x
     */
    private void dfs(int x) {
        //有一种可能的结果确定了
        if (x == c.length - 1) {
            res.add(String.valueOf(c));
            return;
        }

        //防止有重复的字符交换
        HashSet<Character> set = new HashSet<>();
        //从x位开始和x,x+1...进行交换
        for (int i = x; i < c.length; i++) {
            //修剪操作,防止重复元素造成的冗余
            if (set.contains(c[i])) {
                continue;
            }
            set.add(c[i]);

            swap(x, i);//交换,交换完相当于已经确定了第x位
            dfs(x + 1);//递归的确定x+1位
/**
 *          此时第x位的第一种确定情况的所有情况已经得到
 *          eg:abc,acb已经得到了
 *          现在要恢复交换,以便x和新的i(x+1)进行交换
 *          即确定以b开头的
 */
            swap(x, i);
        }
    }

    private void swap(int x, int i) {
        char temp = c[x];
        c[x] = c[i];
        c[i] = temp;
    }
}

14.剑指 Offer 39. 数组中出现次数超过一半的数字

在这里插入图片描述

hash
排序
投票
这三个方法里,投票是最有效率的,一个循环+两个变量的维护
思想:
1.当票数0,暂时没有最多的数,当前数即最多的数
2.如果当前数
最多的数,票数+1,否则-1
摩尔投票法找的其实不是众数,而是占一半以上的数。当数组没有超过一半的数,则可能返回非众数,比如[1, 1, 2, 2, 2, 3, 3],最终返回3。
投票法简单来说就是不同则抵消,占半数以上的数字必然留到最后。

    public int majorityElement(int[] nums) {
        int x = 0;//所求的众数
        int v = 0;//票数
        for (int num : nums) {
            if (v == 0) {
                x = num;
            }
            v += x == num ? 1 : -1;
        }
        return x;
    }

15.剑指 Offer 40. 最小的k个数

在这里插入图片描述

这道题主要考研快排

class Solution {
public int[] getLeastNumbers(int[] arr, int 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 left, int right) {
        if (left < right) {
            int p = partition(arr, left, right);
            quickSort(arr, left, p - 1);
            quickSort(arr, p + 1, right);
        }
    }

    private int partition(int[] arr,int left, int right) {
        int t = arr[left];
        while (left < right) {
            while (left < right && arr[right] >= t) {
                right--;
            }
            arr[left] = arr[right];
            while (left < right && arr[left] <= t) {
                left++;
            }
            arr[right] = arr[left];
        }
        arr[left] = t;
        return left;
    }
}

上面是直接全部排序出来,但是题目只要前k个得到出来即可,并不要求有序。所以,只要快排的哨兵p>k时,则想要的前k个数,就继续快排左半部分即可。若p<k,则说明想要的在右边,快排右边即可。当p== k时,想要的就是左边+p,已经得到了,但此时不一定时有序的(不要求有序),如果要求有序,把==的情况时也继续左边快排即可。

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if (k >= arr.length) {
            return arr;
        }
        quickSort(arr, 0, arr.length - 1,k);
        int[] res = new int[k];
        for (int i = 0; i < k; i++) {
            res[i] = arr[i];
        }
        return res;
    }

    //快排
    private void quickSort(int[] arr, int left, int right,int k) {
        if (left < right) {
            int p = partition(arr, left, right);
            if (p > k ) {
                quickSort(arr, left, p - 1, k);
            } 
            if (p < k) {
                quickSort(arr, p+1, right, k);
            }
            //不需要有序,当等于的之后结束
            return;

        }
    }

    private int partition(int[] arr,int left, int right) {
        int t = arr[left];
        while (left < right) {
            while (left < right && arr[right] >= t) {
                right--;
            }
            arr[left] = arr[right];
            while (left < right && arr[left] <= t) {
                left++;
            }
            arr[right] = arr[left];
        }
        arr[left] = t;
        return left;
    }
}

16剑指 Offer 41. 数据流中的中位数

在这里插入图片描述

这道题主要就是需要在得到中位数的时候,把数据进行排序,但是每次得到的时候都排序,消耗就很大
所以利用优先队列在加入数据的时候就维护数据的顺序
但是一个优先队列在取中位数的时候不好取(消耗大)
就可以利用两个优先队列

思想:
q1维护小于等于中位数的从大到小的队列
q2维护大于中位数的从小到大的队列
当为奇数的时候,直接取出q1的头,当为偶数的时候取出两个的头的平均数即可

怎么在加入数据的时候维护两个队列?
当q1为空的时候,说明当前数就是中位数,所以加入q1
当q1的队头大于等于num的时候,说明比中位数小,所以加入q1,但是加入之后可能会存在,q2.size+1<q1.size的情况,此时q1的队头就不是中位数了,第二个数才是中位数,即把queue2.add(queue1.poll());
q2的维护也是同理

package com.example.concurrent;

public class MedianFinder {
    /**
     * initialize your data structure here.
     */
    PriorityQueue<Integer> queue1;//queue1是保存小于等于中位数的,队头是最大值
    PriorityQueue<Integer> queue2;//queue2是保存大于中位数的,队头是最小值

    public MedianFinder() {
        queue1 = new PriorityQueue<>((a, b) -> b - a);
        queue2 = new PriorityQueue<>((a, b) -> a - b);
    }

    public void addNum(int num) {
        if (queue1.isEmpty() || num <= queue1.peek()) {
            queue1.add(num);
            if (queue2.size() + 1 < queue1.size()) {
                queue2.add(queue1.poll());
            }
        } else {
            queue2.add(num);
            if (queue2.size() > queue1.size()) {
                queue1.add(queue2.poll());
            }
        }
    }

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

    public static void main(String[] args) {
        PriorityQueue<Integer> queue = new PriorityQueue<>();
        queue.add(5);
        queue.add(1);
        queue.add(8);
        queue.add(3);
        while (!queue.isEmpty()) {
            System.out.println(queue.poll());
        }
    }
}

剑指 Offer 42. 连续子数组的最大和

在这里插入图片描述

思考:
我首先想到了滑动窗口,看滑动窗口能否找到最大数组和的窗口
但是遇到问题:
如何扩大和缩小窗口?
一开始-2,遇到1的时候肯定是扩大,在遇到-3,是不懂还是继续扩大?
在一个-2肯定要缩小,那1什么时候缩?

所以滑动窗口不行
采用动态规划
dp[i]表示i之前包括i的最大值
这个例子中,dp数组就是:-2,-1,-3,4…
可以看出,求取dp[i]的时候,只要dp[i-1]<=0,说明前面的没有帮助,就不要了,dp[i]=nums[i];否则,dp[i]=dp[i-1]+nums[i]

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

在这里插入图片描述

想要得到1的个数,即找到所给范围内每个位上的1的个数之和
在这里插入图片描述

eg:12

  • 个位为2,把他设为1,则这个1出现的次数取决于他的高位十位。高位为1,则可能取的值为0,1.所以个位为1的次数为2*1(digit=1)
  • 十位为1,高位没有所以十位出现的次数取决于地位个位。个位为2,可能的取值为0,1,2.所以十位出现的次数为3.
  • 共计为2+3=5

为了总结出结论,选取2304作为例子
十位为当前位。digit=10,23位高位,4为低位。求cur当前位1的个数

  • 当cur=0时:
    在这里插入图片描述
    因为是求当前位为1的个数,所以令cur=1,所以高位就不能取23了,只能取[0,22]。低位为4,但是因为高位降维了,所以可以取[0,9]。此时当前位1的个数取决于高位和低位。
    res=23*10=230,总结成公式就是res=high*digit
  • 当cur=1时:
    在这里插入图片描述
    此时就要分两种情况:
    1.当高位取值为【0,22】时,低位可以取【0,9】
    2.当高位为23时,低位只能取到【0,4】
    所以,需要把这两种情况加在一起。
    res = 23*10+5=235,总结公式就是res=high*digit+low+1
  • 当cur=2,3,4,5,6,7,8,9时:
    在这里插入图片描述
    此时也要零cur=1,则高位可以取【0,23】,低位可以取到【0,9】.
    res = (23+1)*10,总结公式就是(high+1)*digit
    public int countDigitOne(int n) {
        int res = 0;//1的个数
        int digit = 1;//因子
        int high = n / 10;//当前为的高位(初始是个位的高位)
        int low = 0;//低位,个位没有低位
        int cur = n % 10;//当前位
        while (high != 0 || cur != 0) {
            if (cur == 0) res += high * digit;
            else if (cur==1) res += high * digit + low + 1;
            else res += (high + 1) * digit;
            //当前位生位
            low += cur*digit;
            cur = high % 10;
            high = high / 10;
            digit *= 10;
        }
        return res;
    }

18 剑指 Offer 44. 数字序列中某一位的数字

在这里插入图片描述
在这里插入图片描述
因为位数是从0开始数的,所以可以不用管0,直接从1开始计算
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

    public int findNthDigit(int n) {
        if (n < 10) {
            return n;
        }
        //因为start和count可能会超出int范围,所以用long
        long start = 1;//起始值
        int digit = 1;//位数
        long count = 9;//位数
        //计算出digit
        while (n > count) {
            n -= count;
            start *= 10;//1,10,100
            digit += 1;//1,2,3
            count = 9 * start * digit;//9,180...
        }
        //此时的n表示了从start开始的第几位

        //第n位的数字时num
        long num = start + (n - 1) / digit;

        //index表示第n位在所求数的哪一位
        int index = (n - 1) % digit;//0,1;0,1,2;0,1,2,3

        return Long.toString(num).charAt(index)-'0';
    }

19.剑指 Offer 45. 把数组排成最小的数

思想:
1.首先应该把第一位最小的放在最前面
2.当第一位有多个一样的时候,就应该考虑拼接的情况。即拼起来最小的
所以总结下来就是需要将拼接起来最小的情况进行排序
若x+y>y+x,则说明x>y,按照y,x排序,得到的拼接情况最小
因此需要排序,排序判断大小的规则即拼接比较。

具体实现的方式有两种,第一种是使用优先队列,传入比较器。第二中是自己写快排

//优先队列
class Solution {
    public String minNumber(int[] nums) {
        String[] strings = new String[nums.length];
        for (int i = 0; i < nums.length; i++) {
            strings[i] = String.valueOf(nums[i]);
        }

        PriorityQueue<String> queue = new PriorityQueue<>((a, b) -> {
            return (a + b).compareTo(b + a);
        });

        for (String str : strings) {
            queue.offer(str);
        }
        StringBuffer stringBuffer = new StringBuffer();
        while (!queue.isEmpty()) {
            stringBuffer.append(queue.poll());
        }
        return stringBuffer.toString();
    }
}
//手写快排
class Solution {
public String minNumber(int[] nums) {
        String[] strings = new String[nums.length];
        for (int i = 0; i < nums.length; i++) {
            strings[i] = String.valueOf(nums[i]);
        }

        quickSort(strings,0,strings.length-1);
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < strings.length; i++) {
            stringBuilder.append(strings[i]);
        }
        return new String(stringBuilder);
    }

    private void quickSort(String[] strings, int left, int right) {
        if (left >= right) {
            return;
        }
        int p = partition(strings, left, right);
        quickSort(strings, left, p - 1);
        quickSort(strings, p + 1, right);
    }

    private int partition(String[] strings, int left, int right) {
        String privot = strings[left];
        while (left < right) {
            while (left < right && (strings[right]+privot).compareTo(privot+strings[right]) >= 0) {
                right--;
            }
            strings[left] = strings[right];
            while (left < right && (strings[left] + privot).compareTo(privot + strings[left]) <= 0) {
                left++;
            }
            strings[right] = strings[left];
        }
        strings[left] = privot;
        return left;
    }
}

20.剑指 Offer 46. 把数字翻译成字符串

在这里插入图片描述

在这里插入图片描述
设动态规划列表 dp ,dp[i]代表以 Xi 为结尾的数字的翻译方案数量。
所以初始化,dp[0] = dp[1] = 1,即 “无数字” 和 “第 1 位数字” 的翻译方法数量均为 1 ;

class Solution {
    public int translateNum(int num) {
        String s = String.valueOf(num);
        int[] dp = new int[s.length()+1];//+1是因为有dp[0]表示没有数时候的排序情况。
        dp[0] = 1;//没有数的排序有1种
        dp[1] = 1;//第一个数的排序有一种
        for (int i = 2; i <= s.length(); i++) {
            String c = s.substring(i - 2, i);//拿到第Xi-1和第Xi数的组合
            //如果Xi-1和第Xi可以联合翻译
            if (c.compareTo("10") >= 0 && c.compareTo("25") <= 0) {
                dp[i] = dp[i - 1] + dp[i-2];
            } else {
                dp[i] = dp[i - 1];
            }
        }
        return dp[s.length()];
    }
}

21.剑指 Offer 47. 礼物的最大价值

在这里插入图片描述

这种一步一步寻找最优的——动态规划
在这里插入图片描述

这道题的关键是只能向右或者向下,所以比如:在填充dp[0][2]的时候,一定是dp[0][1]+当前值

class Solution {
    public int maxValue(int[][] grid) {
        int[][] dp = new int[grid.length][grid[0].length];

        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[0].length; j++) {
                if(i==0&&j==0){
                    dp[0][0] = grid[0][0];
                }
                else if (i == 0) {
                    dp[i][j] = dp[i][j - 1] + grid[i][j];
                } else if (j == 0) {
                    dp[i][j] = dp[i - 1][j] + grid[i][j];
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
                } 
            }
        }
        return dp[grid.length - 1][grid[0].length - 1];
    }
}

在这里插入图片描述

22.剑指 Offer 48. 最长不含重复字符的子字符串

在这里插入图片描述

方法一:滑动窗口
遍历每一个开头的最长长度
这个方法空间和时间的消耗都是极大的,但是方便想出来

在这里插入图片描述

class Solution {
    public int lengthOfLongestSubstring(String s) {
       int res = 0;
        int r = -1;//右指针控制窗口
        for (int i = 0; i < s.length(); i++) {
            r = i - 1;
            Set<Character> set = new HashSet<>();
            while (r+1 < s.length() && !set.contains(s.charAt(r+1))) {
                set.add(s.charAt(r+1));
                r++;
            }
            res = Math.max(res, r - i + 1);
        }
        return res;
    }
}

方法二:动态规划+Map
在这里插入图片描述
使用Map来拿到i的位置
如果不能使用Map来记录各个位置的话,就可以得到j之后,往前遍历到i,但是这样时间复杂度就是O(n2)

class Solution {
    public int lengthOfLongestSubstring(String s) {
        if(s.length()==0){
            return 0;
        }
        if(s.length()==1){
            return 1;
        }
        //map存放字符的位置,方便找出确定有边界j时,最近的相同字符的位置i
        Map<Character, Integer> map = new HashMap<>();
        int res = 1;
        int[] dp = new int[s.length()];
        dp[0] = 1;
        map.put(s.charAt(0), 0);
        for (int p = 1; p < s.length(); p++) {
            int j = p;
            //得到i
            Integer i = map.getOrDefault(s.charAt(j), -1);
            map.put(s.charAt(p), p);
            //这里要注意,如果i=-1,则j-1一定>dp[p-1],所以就可以在这里一起判断,不用再判断map是否包含了
            dp[p] = dp[p - 1] < j - i ? dp[j - 1] + 1 : j - i;
            res = Math.max(res, dp[p]);
        }
        return res;
    }
}

23.剑指 Offer 49. 丑数

思想:
只包含质因子的才是丑数
所以不能逐个判定,对2,3,5取余数,比如14质因子有7
所以
1.一个丑数,一定是一个较小的丑数质因子产生的
2.同样,一个丑数
质因子一定是一个丑数
所以就可以根据之前的丑数推出新的丑数——动态规划
一个丑数可以*2,*3,*5得到3个丑数,但是最新的丑数是最小的那个

一开始的丑数为1,则dp[0]=1
为了得到三种质因子产生的丑数,就需要为他们创造三个索引a,b,c指向乘以2,3,5能创建的最小丑数的之前丑数的位置。

当第一个丑数*三个质因子,得到三个新的丑数,第二个丑数应当是他们最小的这个,代表这个质因子的索引就应该++(到下一个丑数)

class Solution {
    public int nthUglyNumber(int n) {
        /**
         *  dp[0]表示第一个丑数
         *  dp[n-1]表示第n个丑数
         *  a表示要成质因子2的较小丑数的索引,所以dp[a]*2就是有可能的最新丑数
         *  b,c同理
         *
         */

        int[] dp = new int[n];
        int a = 0, b = 0, c = 0;//初始化索引位置
        dp[0] = 1;
        //遍历,把dp补充起来
        for (int i = 1; i < n; i++) {
            int n1 = dp[a] * 2;//n1,n2,n3表示三个质因子算出来的新的丑数
            int n2 = dp[b] * 3;
            int n3 = dp[c] * 5;
            dp[i] = Math.min(Math.min(n1, n2), n3);//dp[i]为三者的最小
            //判断最新的丑数是哪个质因子的,为其更新最新的位置
            //注意当同时满足的时候,都应该++
            //所以不能用ifelse
            if (dp[i] == n1) {
                a++;
            }
            if (dp[i] == n2) {
                b++;
            }
            if (dp[i] == n3) {
                c++;
            }
        }
        return dp[n - 1];
    }
}

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

在这里插入图片描述

方法一:这道题很简单,就是使用Map遍历两遍
不过要找到第第一个出现一次的,所以第二次遍历还要遍历字符串s

方法二:主要学习使用有序哈希表,第二次遍历就可以遍历Map了
在哈希表的基础上,有序哈希表中的键值对是 按照插入顺序排序 的。基于此,可通过遍历有序哈希表,实现搜索首个 “数量为 1 的字符”。
Java 使用 LinkedHashMap 实现有序哈希表。

剑指 Offer 51. 数组中的逆序对

在这里插入图片描述

暴力求解,超时

归并排序
分成小的分组,然后两个分组合并有序的,得到他们的逆序对
在这里插入图片描述

class Solution {
    public int reversePairs(int[] nums) {
        return mergeSort(nums, 0, nums.length - 1);
    }

    private int mergeSort(int[] nums, int l, int r) {
        if (l < r) {
            int mid = l + ((r - l) >> 1);
            //得到子递归的逆序对
            int res = mergeSort(nums, l, mid) + mergeSort(nums, mid + 1, r);
            //计算两个子递归组合的逆序对,并排列有序
            if (nums[mid] <= nums[mid + 1]) {
                return res;
            }
            //排序
            int[] help = new int[r - l + 1];
            int i = 0;
            int p1 = l;
            int p2 = mid + 1;
            while (p1 <= mid && p2 <= r) {
                if (nums[p1] <= nums[p2]) {
                    help[i++] = nums[p1++];
                } else {
                    //res++;错
                    //res += p1-l+1;错
                    //这里比较两个有序分组的逆序对,比较容易出错
                    res += mid-p1+1;
                    help[i++] = nums[p2++];
                } 
            }
            while (p1 <= mid) {
                help[i++] = nums[p1++];
            }
            while (p2 <= r) {
                help[i++] = nums[p2++];
            }
            //把排序好的help放回nums
            for (int j = 0; j < help.length; j++) {
                nums[l + j] = help[j];
            }
            return res;
        }
        return 0;
    }
}

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

在这里插入图片描述

先到统一起点,再一起走

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        //计算表长,让统一位置开始
        ListNode longHead, shortHead;
        int dist = 0;//长度差
        int len1 = getLongth(headA);
        int len2 = getLongth(headB);
        if (len1 > len2) {
            longHead = headA;
            shortHead = headB;
            dist = len1 - len2;
        }else {
            longHead = headB;
            shortHead = headA;
            dist = len2 - len1;
        }
        //到统一起始点
        while (dist != 0) {
            longHead = longHead.next;
            dist--;
        }
        while (longHead != null) {
            if (longHead == shortHead) {
                return longHead;
            } else {
                longHead = longHead.next;
                shortHead = shortHead.next;
            } 
        }
        return null;
    }

    public int getLongth(ListNode listNode) {
        int longth = 0;
        while (listNode != null) {
            longth++;
            listNode = listNode.next;
        }
        return longth;
    }

}

剑指 Offer 53 - I. 在排序数组中查找数字 I

在这里插入图片描述

先用二分查找,然后找到之后往前后遍历

class Solution {
public int search(int[] nums, int target) {
        int i = EFsearch(nums, 0, nums.length - 1, target);
        if (i == -1) {
            return 0;
        }
        int index = i-1;
        int res = 1;
        //越界判断要放到前面,不然比较时候会越界
        while (index >= 0 && nums[index] == target) {
            res++;
            index--;
        }
        index = i+1;
        while (index < nums.length && nums[index] == target) {
            res++;
            index++;
        }
        return res;
    }

    private int EFsearch(int[] nums, int l, int r, int target) {
        while (l <= r) {
            int mid = l + ((r - l) >> 1);
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        return -1;
    }
}

剑指 Offer 53 - II. 0~n-1中缺失的数字

在这里插入图片描述

二分查找,数字和位置进行比较

class Solution {
    public int missingNumber(int[] nums) {
        int i = ef(nums, 0, nums.length - 1);
        return i;
    }

    private int ef(int[] nums, int left, int right) {
        while (left <= right) {
            int mid = (right + left) / 2;
            if (mid == nums[mid]) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return left;
    }
}

25 剑指 Offer 54. 二叉搜索树的第k大节点

在这里插入图片描述

这个题要注意的是对k不能设置为局部变量,传入进行递归。那样子递归对k-操作返回之后,父程序的k还是操作之前的

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    int k = 0;
    int res = 0;
    public int kthLargest(TreeNode root, int k) {
        this.k = k;
        test(root);
        return res;
    }

    private void test(TreeNode root) {
        if (root == null) {
            return;
        }
        test(root.right);
        k--;
        if (k == 0) {
            res = root.val;
        }
        test(root.left);
    }
}

剑指 Offer 55 - I. 二叉树的深度

class Solution {
    public int maxDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
    }
}

剑指 Offer 55 - II. 平衡二叉树

在这里插入图片描述

    public boolean isBalanced(TreeNode root) {
        if (root == null) {
            return true;
        }
        int leftHigh = getHigh(root.left);
        int rightHigh = getHigh(root.right);
        return (Math.abs(leftHigh - rightHigh) <2)&&isBalanced(root.left)&&isBalanced(root.right);
    }

    //得到二叉树高度
    private int getHigh(TreeNode root) {
        if (root == null) {
            return 0;
        }
        return Math.max(getHigh(root.left), getHigh(root.right) + 1);
    }

优化

上面的方法是把判断子树平衡不平衡(isBalanced)和以当前为节点的树平不平衡(判断子树高度差)分开了
但是其实可以在得到子树长度的时候就判断平衡不平衡
也就是说在得到以当前节点为根的高度的时候,如果它为根的树不是平衡的,就返回-1
这种方法因为少了很多多余的递归,所以时间上有明显提升

class Solution {
    public boolean isBalanced(TreeNode root) {
        return getHigh(root) != -1;
    }

    private int getHigh(TreeNode root) {
        if (root == null) {
            return 0;
        }
        //得到子树的高度
        int leftH = getHigh(root.left);
        //子树不是平衡的
        if (leftH == -1) {
            return -1;
        }
        int rightH = getHigh(root.right);
        //子树不是平衡的
        if (rightH == -1) {
            return -1;
        }
        //加上root是不是平衡的
        if (Math.abs(rightH - leftH) > 1) {
            return -1;
        }
        //都是平衡的,返回真正高度
        return Math.max(getHigh(root.left), getHigh(root.right)) + 1;
    }
}

26.剑指 Offer 56 - I. 数组中数字出现的次数

在这里插入图片描述

思想:
如果是只有一个单独的数,其他都是两个一样的数,那么做异或^操作,得到的就是这个单独的数
在这里插入图片描述
所以:把两个不同的数,拆分成两个子数组,每个数组进行异或,得到的分别就是所求的两个数

怎么分?
把nums异或,可以得到结果z = x^y.因为x,y肯定是不同的,所以,他们俩的二进制位肯定至少有一位是不同的。那么异或操作得到的z肯定至少有一位是1.可以利用m=1。m不断左移和z进行&操作,判断哪一位是1.
在这里插入图片描述

找到之后
遍历整个nums,若和m做&操作,有两个结果:
1.num&m==0——num 的m位为0
2.num&m!=0——num的m位不为0
这就划分成m位不同的两个组,也就把x和y划分开了

class Solution {
    public int[] singleNumbers(int[] nums) {
        //nums全部做异或运算,得到z=x^y
        int z = 0;
        for (int num : nums) {
            z ^= num;
        }
        //得到x和y的第m位是不一样的
        int m = 1;//000001
        while ((z & m) == 0) {
            m <<= 1;//左移1
        }
        //按照m划分两个组(m位相同的组,m位不同的组)
        int x = 0, y = 0;//划分两个组后,每个组异或之后的结果
        for (int num : nums) {
            if ((m & num) == 0) {
                x ^= num;
            } else {
                y ^= num;
            } 
        }
        return new int[]{x, y};
    }
}

27.剑指 Offer 56 - II. 数组中数字出现的次数 II

在这里插入图片描述

思想:
只要时出现m次的数字,他们的二进制位1的个数加起来对m取余数,都是0
所以,此时那一个只出现1次的数,二进制位1也加进去,取余得到的就是那个数
在这里插入图片描述

class Solution {
    public int singleNumber(int[] nums) {
        //把nums所有数的每一位的二进制的1加起来
        int[] count = new int[32];//32位
        for (int i = 0; i < nums.length; i++) {
            for (int j = 0; j < 32; j++) {
                count[j] += nums[i] & 1;//count从0-32保存所有数从低到高位1的个数
                nums[i] >>= 1;
            }
        }
        //对每一位的1得到个数对3取余,得到唯一一个不是三个相同数的数
        int res = 0;
        for (int i = 0; i < 32; i++) {
            res <<= 1;
            res |= count[31 - i] % 3;
        }
        return res;
    }
}

28.剑指 Offer 57. 和为s的两个数字

在这里插入图片描述

双指针

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        while (left < right) {
            int sum = nums[left] + nums[right];
            if (sum == target) {
                return new int[]{nums[left], nums[right]};
            } else if (sum < target) {
                left++;
            } else {
                right--;
            } 
        }
        return new int[0];
    }
}

因为这个题说的是递增的,所以不会出现一样的。但是,当是非递减的时候,就可以优化掉相同的情况

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        while (left < right) {
            int sum = nums[left] + nums[right];
            int leftNum = nums[left];
            int rightNum = nums[right];
            if (sum == target) {
                return new int[]{nums[left], nums[right]};
            } else if (sum < target) {
                while (left < right && leftNum == nums[left]) {
                    left++;
                }
            } else {
                while (left < right && rightNum == nums[right]) {
                    right--;
                }
            }
        }
        return new int[0];
    }
}

29.剑指 Offer 57 - II. 和为s的连续正数序列

在这里插入图片描述

滑动窗口

class Solution {
//滑动窗口
    public int[][] findContinuousSequence(int target) {
        List<int[]> list = new ArrayList<>();
        //滑动窗口,i表示左窗口,j表示右窗口,s表示窗口内和
        //i为开区间,j为闭区间,因为最少为2个数,所以一开始就是1,2
        int i = 1, j = 2, s = 3;
        while (i < j) {
            if (s == target) {
                int[] ans = new int[j - i + 1];
                for (int k = i; k <= j; k++) {
                    ans[k - i] = k;
                }
                list.add(ans);
                s-=i;
                i++;
            } else if (s > target) {
                s -= i;
                i++;
            } else {
                j++;
                s += j;
            } 
        }
        return list.toArray(new int[0][]);
    }
}

剑指 Offer 58 - I. 翻转单词顺序

在这里插入图片描述

1.不用双指针进行交换,直接从后面进行倒序遍历使用StringBuilder进行连接即可
2.使用String[] s1 = s.split(" ");进行划分之后,原来里面的>=2的空格全部被分成""了,所以使用if (s1[i].equals("")进行判断

class Solution {
    public String reverseWords(String s) {
        String[] s1 = s.split(" ");
        StringBuffer stringBuffer = new StringBuffer();
        for (int i = s1.length - 1; i >= 0; i--) {
            if (s1[i].equals("")) {
                continue;
            }
            stringBuffer.append(s1[i]).append(" ");
        }
        return new String(stringBuffer).trim();
    }
}

上面是将字符串s划分之后,使用额外的空间得到一个新的逆转后的字符串。所以空间复杂度是O(n)。那么如何原地进行逆转?
划分之后,先对所有的字符进行逆转,然后再将每一个单词进行逆转就可以实现原地逆转了。但是对于Java来说,划分之后还是需要额外的空间来存储划分之后的字符串,所以空间复杂度还是n。
另外,如何自己实现划分的代码,而不使用API?

class Solution {
public String reverseWords(String s) {
        //去除前后空格和单词之间多余的空格
        StringBuilder sb = myTrim(s);
        //反转整个sb字符
        reverse(sb, 0, sb.length() - 1);
        //反转每个单词
        reverseEveryWord(sb);
        return new String(sb);
    }

    private void reverseEveryWord(StringBuilder sb) {
        int start = 0;
        int end = 1;
        int n = sb.length();
        while (start < n) {
            //遍历得到每个单词的结尾
            while (end < n && sb.charAt(end) != ' ') {
                end++;
            }
            //反转每个单词
            reverse(sb, start, end - 1);
            start = end + 1;
            end = start + 1;
        }
    }

    /**
     * 反转字符串
     * @param sb
     * @param start
     * @param end
     */
    private void reverse(StringBuilder sb, int start, int end) {
        while (start < end) {
            char temp = sb.charAt(start);
            sb.setCharAt(start, sb.charAt(end));
            sb.setCharAt(end, temp);
            start++;
            end--;
        }
    }

    /**
     * 自定义去除空格
     * @param s
     * @return
     */
    private StringBuilder myTrim(String s) {
        //双指针先把前后空格去掉
        int start = 0;
        int end = s.length() - 1;
        while (s.charAt(start) == ' ') start++;
        while (s.charAt(end) == ' ') end--;
        StringBuilder sb = new StringBuilder();
        while (start <= end) {
            char c = s.charAt(start);
            //后面的条件是把两个单词之间多余的空格变成一个
            if (c != ' ' || sb.charAt(sb.length() - 1) != ' ') {
                sb.append(c);
            }
            start++;
        }
        // System.out.println("ReverseWords.removeSpace returned: sb = [" + sb + "]");
        return sb;
    }
}

剑指 Offer 58 - II. 左旋转字符串

在这里插入图片描述

简单做法:
1.切片:return s.substring(n, s.length()) + s.substring(0, n);
2.遍历:先遍历[n,len),用StringBuilder连接,再把前[0,n)遍历,连接

好做法:
先将[0,n)逆转
再将[n,len)逆转
再将[0,len)逆转

class Solution {
    public String reverseLeftWords(String s, int n) {
        char[] arr = s.toCharArray();
        reversed(arr, 0, n - 1);
        reversed(arr, n, s.length() - 1);
        reversed(arr, 0, s.length() - 1);
        return new String(arr);
    }

    public void reversed(char[] arr, int left, int right) {
        while (left < right) {
            char temp = arr[left];
            arr[left] = arr[right];
            arr[right] = temp;
            left++;
            right--;
        }
    }
}

剑指 Offer 59 - I. 滑动窗口的最大值

在这里插入图片描述

单调队列

参考剑指 Offer 30. 包含min函数的栈要求实现一个栈,可以返回栈中元素的最小值,则栈1保存正常的栈数据,栈2保存栈中可能的最小值。即不保存所有的值,只有新加入的值更小的时候才入栈。

所以,单调队列也是一样
1.当对头保存的最大值不再是窗口内的元素时,出队
2.当新加入的元素比对尾元素大的时候,队尾元素出队==(队头永远保存最大值)==

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        //单调队列,保存的是索引
        Deque<Integer> queue = new ArrayDeque<>();
        //结果数组
        int[] res = new int[nums.length - k + 1];
        //结果数组的索引
        int index = 0;

        //开始遍历
        for (int i = 0; i < nums.length; i++) {
            //两种情况需要出队:
            // 1.队头元素不在索引为[i-k+1,i]的范围
            while (!queue.isEmpty() && queue.peek() < i - k + 1) {
                queue.poll();
            }
            // 2.新加入的元素比队尾的元素大,队尾一直出队
            while (!queue.isEmpty() && nums[i] > nums[queue.peekLast()]) {
                queue.removeLast();
            }

            //新元素入队
            queue.offer(i);

            //把结果放入res
            if (i + 1 >= k) {
                res[index++] = nums[queue.peek()];
            }
        }
        return res;
    }
}

面试题59 - II. 队列的最大值

在这里插入图片描述

和59-Ⅰ一样,都是求一个时间序列的最大值(最小值)。所以依旧想到构造一个递减队列

class MaxQueue {

    Deque<Integer> queue;
    Deque<Integer> helpQueue;

    public MaxQueue() {
        queue = new LinkedList<>();
        helpQueue = new LinkedList<>();
    }

    public int max_value() {
        if (helpQueue.size() == 0) {
            return -1;
        }
        return helpQueue.peek();
    }

    public void push_back(int value) {
        queue.addLast(value);
        while (!helpQueue.isEmpty()&&helpQueue.peekLast() < value) {
            helpQueue.removeLast();
        }
        helpQueue.addLast(value);
    }

    public int pop_front() {
        if (queue.size() == 0) {
            return -1;
        }
        if (queue.peek().equals(helpQueue.peek())) {
            helpQueue.poll();
        }
        return queue.poll();
    }
}

/**
 * Your MaxQueue object will be instantiated and called as such:
 * MaxQueue obj = new MaxQueue();
 * int param_1 = obj.max_value();
 * obj.push_back(value);
 * int param_3 = obj.pop_front();
 */

注意:使用链表进行构造比使用数组要省时间的多

复习:这个是维护队列的最大值,那维护栈的最小值那?第30题

剑指 Offer 60. n个骰子的点数

在这里插入图片描述

思想

f(n,x)表示n个骰子,和为x的概率
一个骰子得到的6种情况概率记为f(1,1-6)
两个骰子得到的11种情况f(2,2-12)

如果想计算f(n,x),那么

  • 当第n个骰子为1是,前n-1个骰子和要为x-1,概率为f(n-1,x-1)。此时f(n,x)=f(n-1,x-1)*1/6
  • 当第n个骰子为2是,前n-1个骰子和要为x-2,概率为f(n-1,x-2)。此时f(n,x)=f(n-1,x-2)*1/6
  • 当第n个骰子为3是,前n-1个骰子和要为x-3,概率为f(n-1,x-3)。此时f(n,x)=f(n-1,x-3)*1/6
  • 当第n个骰子为4是,前n-1个骰子和要为x-4,概率为f(n-1,x-4)。此时f(n,x)=f(n-1,x-4)*1/6
  • 当第n个骰子为5是,前n-1个骰子和要为x-5,概率为f(n-1,x-5)。此时f(n,x)=f(n-1,x-5)*1/6
  • 当第n个骰子为6是,前n-1个骰子和要为x-6,概率为f(n-1,x-6)。此时f(n,x)=f(n-1,x-6)*1/6
    所以总的f(n,x)就是把他们加起来。在这里插入图片描述
    观察发现,以上递推公式虽然可行,但 f(n - 1, x - i)f(n−1,x−i) 中的 x - ix−i 会有越界问题。例如,若希望递推计算 f(2, 2)f(2,2) ,由于一个骰子的点数和范围为 [1, 6][1,6] ,因此只应求和 f(1, 1)f(1,1) ,即 f(1, 0)f(1,0) , f(1, -1)f(1,−1) , … , f(1, -4)f(1,−4) 皆无意义。此越界问题导致代码编写的难度提升。

为了让不越界,就需要换一种思维。之前是求一个f(n,x),往前推6个加起来
在这里插入图片描述
现在可以,遍历所有的f(n-1),看他们对谁有贡献,逐个把他们的共享加起来。
在这里插入图片描述
在这里插入图片描述
以此类推,把贡献们都加载一起,就得到了对应的概率。这样也不会越界

class Solution {
    public double[] dicesProbability(int n) {
        //本来是dp[i][j]表示i个骰子和为j的概率。但是只需要最后一组概率,所以用一个数组dp,逐步和temp数组替换就可以了
        // 初始为6,是因为n=1,就是6种情况
        double[] dp = new double[6];
        Arrays.fill(dp, 1.0 / 6.0);
        //此时dp[0]代表1个骰子和为1的概率

        //从2个骰子遍历到n个骰子
        for (int i = 2; i <= n; i++) {
            //temp表示i个骰子的dp。最小和为i,最大为6*i。个数和为(6*i)-i+1=5*i+1
            double[] temp = new double[5 * i + 1];
            //填充temp
            for (int j = 0; j < dp.length; j++) {//遍历dp,填充的每个temp[j]
                for (int k = 0; k < 6; k++) {
                    //dp[j]对temp[j~j+6]有贡献
                    temp[j + k] += dp[j] / 6.0;//还要乘以1/6,因为这是当前第i个骰子的概率
                }
            }
            dp = temp;
        }
        return dp;
    }
}

31.剑指 Offer 61. 扑克牌中的顺子

在这里插入图片描述

分析题目:

说是扑克牌,但是实际上是就是看一个数组,里面5个数字,0可以代表任何数,数字的取值是[0~13]。这里面数字出现的次数都可以是多次的。

自己的写法

思想:用变量wn记录万能牌0的个数,数组arr记录[1-13]出现的次数,这样就可以通过遍历确定导致不连续的牌。因为填充好arr之后,只有最多5个位置有数字,所以缩小到第一个牌和最后一个牌的位置。然后看他们里面有几个位置是0,则有几个不连续的,看万能牌0能否够填充他们。
因为牌可以多次出现,所以万能牌0的个数不止两张。且只要有别的牌出现两次,必然不是连续的,直接false

    public static boolean isStraight(int[] nums) {
        //记录0的个数
        int wn = 0;
        int[] arr = new int[14];
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] == 0) {
                wn++;
                continue;
            }
            arr[nums[i]] += 1;
        }

        //此时得到的arr,可以通过遍历它来看是否有序
        int i = 0, j = arr.length - 1;
        while (i < j && (arr[i] == 0 || arr[j] == 0)) {
            if (arr[i] == 0) {
                i++;
            }
            if (arr[j] == 0) {
                j--;
            }
        }
        while (i <= j) {
            if (arr[i] > 1) {
                return false;
            }
            if (arr[i] == 0) {
                wn--;
            }
            i++;
        }
        if (wn < 0) {
            return false;
        } else {
            return true;
        }
    }

改进方法一
改进思路:
1.上面是通过数组的方式将nums的最大最小值放在数组arr中,然后再遍历缩小arr的范围,得到最小最大值的位置,再遍历得到他们之间不连续的位置数,看万能牌0能否够填充。其实这样太麻烦,可以利用比较的方法得到最大最小值这样就不用再用数组了。
2.上面是还要通过遍历记录0的次数,但是实际上得到最大最小值,max-min<5那么就是可以填充的,因为一个数要么是在最大最小之间,要么就是0.如果有不连续,最大最小确定了是<5的。肯定有对应个数的0来填充。比如0,0,3,5,7.max-min=4<5,必然有对应的0去补它
3.上面使用数组记录的个数来判断重复,因为没有数组了,就要用set

在这里插入图片描述

改进二:
1.上面使用比较来确定最大最小值,也可以使用排序,然后遍历,跳过==0的,就可以得到最大最小值
2.判断num[i]和num[i+1]是否相等就可以判断是否有重复的

在这里插入图片描述
实际上三个方法效率差不多,但是我自己写的方法因为要多次遍历,就感觉很傻

32.剑指 Offer 62. 圆圈中最后剩下的数字

在这里插入图片描述

在这里插入图片描述

class Solution {
    public int lastRemaining(int n, int m) {
        //f(1,m)=0
        int x = 0;
        //从f(2,m)开始循环推导
        for (int i = 2; i <= n; i++) {
            x = (x + m) % i;
        }
        return x;
    }
}

剑指 Offer 63. 股票的最大利润

在这里插入图片描述

只需要记录之前最低价目前最大利润

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

33.剑指 Offer 64. 求1+2+…+n

在这里插入图片描述
考虑递归

        //使用if的递归
        if (n == 1) {
            return 1;
        }
        n += sumNums(n - 1);
        return n;

但是不能用if
考虑短路运算符&&
n > 1 && n += sumNums(n - 1)当n==1的时候,就不会执行后面的递归,直接返回n

    public int sumNums(int n) {

        //前面是boolen,后面也的是,所以加一个">0"的永远成立的条件,防止报错
        // x也是防止报错
        boolean x = (n > 1) && (n += sumNums(n - 1)) > 0;
        return n;
    }

34.剑指 Offer 65. 不用加减乘除做加法

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

class Solution {
    public int add(int a, int b) {
        //当有进位的时候一直进行无进位和的操作,直到得到无进位时候的所以无进位和
        while (b != 0) {
            int c = (a & b) << 1;  //&操作可以得到两个数进位的值,左移1为得到进位之后的进位值
            //a 无进位和 异或操作可以得到两个数的无进位和
            a ^= b;
            //b为最新的进位 
            b = c;
        }
        return a;
    }
}

35.剑指 Offer 66. 构建乘积数组

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

class Solution {
    public int[] constructArr(int[] a) {
        if(a.length==0){
            return new int[0];
        }
        int[] b = new int[a.length];
        int temp = 1;//计算上半部分三角时候的辅助变量
        b[0] = 1;//初始计算下半部分三角时候b[0]=1
        // 计算下三角,从B[1]开始,因为B[0]左边部分第一个就是1
        for (int i = 1; i < a.length; i++) {
            b[i] = b[i - 1] * a[i - 1];
        }
        //计算上三角,从B[len-2]开始
        for (int i = a.length - 2; i >= 0; i--) {
            temp *= a[i + 1];
            b[i] *= temp;
        }
        return b;
    }
}

面试题67. 把字符串转换成整数

在这里插入图片描述

如果知识简单的把字符串变成数字:
1.trim去掉空格
2.判断正负号,使用标记位记录,最后乘上
直接截取字符串进行拼接在转成数字

但是要考虑:
1.越界
怎么判断越界很重要,如果已经得到了结果在和最值比较,则是不对的,因为已经越界了(越界的值时Integer.MAX_VALUE=2147483647)
所以应当把res每次准备和x进行计算时和Integer.MAX_VALUE/10=214748364进行比较
(1)如果大于则,res10至少为2147483650直接越界
(2)如果不大于,但是x>7,则res
10+x得到的值至少为2147483648也越界
2.有可能有"123abc"这种不是数字的存在
这种情况就不能直接字符串截取了,就必须一个一个遍历

class Solution {
    public int strToInt(String str) {
        String s = str.trim();
        if(s.length()==0){
            return 0;
        }
        char[] chars = s.toCharArray();
        //正负数标记
        int sign = 1;
        //结果
        int res = 0;
        //遍历的索引,默认一开始有正负号
        int i = 1;
        if (chars[0] == '-') {
            sign = -1;
        } else if (chars[0] != '+') {
            //第一位不是正负号,所以从0开始遍历
            i = 0;
        }
        //越界判断
        int binary = Integer.MAX_VALUE / 10;
        for (; i < chars.length; i++) {
            //1。判断是否是数字
            if (chars[i] < '0' || chars[i] > '9') {
                break;
            }
            //2.判断是否越界(题目要求越界返回最值)
            if (res > binary || (res == binary && chars[i] > '7')) {
                return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
            }
            //计算
            res = res * 10 + (chars[i] - '0');
        }
        return sign*res;
    }
}

剑指 Offer 68 - I. 二叉搜索树的最近公共祖先

在这里插入图片描述
注意:是找最近的公共父节点

思路:

对于二叉搜索树可以分为三种情况:

  • p,q位于root的两个子树内,则root就是所求节点
  • p,q位于左子树内,向root的左子树寻找,直到找到公共
  • 同上的右子树

所以只要找到这个公共的交叉节点即可

    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 {
                //找到了
                break;
            }
        }
        return root;
    }

剑指 Offer 68 - II. 二叉树的最近公共祖先

在这里插入图片描述
因为这个不是搜索树了,所以无法使用比较值来判断:1.公共节点在哪个子树。2.什么时候找到公共节点
因此就要左右子树都找,直到找到

    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        /**
         * 1.当一开始p/q就是公共节点,直接返回
         * 2.往子树遍历,只要找到p/q,就返回
         */
        if (root == null || root == p || root == q) {
            return root;
        }
        //子树递归,返回找到的p,q
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        //如果找到了,则返回他们的最近公共节点
        if (left != null && right != null) {
            return root;
        }
        //没有全都找到,或者是返回的left/right内全都找到了,即向上一层递归返回找到的最终结果
        return left == null ? right : left;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值