算法题解2

033、LeetCode688-“马”在棋盘上的概率(dfs,dp)

knight

本题目可以很简单的用dfs做出来,但是时间复杂度较高。

思路:

  • 模拟马在棋盘上走,对走到的任意一地方
  • 如果不在就返回0
  • 如果在就继续走,当走的步数等于K,发现还在棋盘中就返回0
  • 对返回的8个方向的数据取平均数
/**
 * 时间复杂度:O(8 ^ k) 每次能往8个方向走,一共能走K次
 * 空间复杂度:O(K) 递归栈
 */
class Solution {
	// 移动的方向
    int[][] move = {
        {1, 2}, {2, 1}, {2, -1}, {1, -2},
        {-1, -2}, {-2, -1}, {-2, 1}, {-1, 2}
    };

    public double knightProbability(int N, int K, int r, int c) {
        return dfs(0, K, N, r, c);
    }

    // level: 当前走了几步
    public double dfs(int level, int K, int N, int x, int y) {
        // 如果走到了棋盘外,就返回0,代表这条路的概率为0
        if(x < 0 || x >= N || y < 0 || y >= N) {
            return 0d;
        }
        if(level == K) return 1d;
        double probabilities = 0d;
        for(int i = 0; i < 8; i++) {
            probabilities += dfs(level + 1, K, N, x + move[i][0], y + move[i][1]);
        }
        return probabilities / 8;
    }
}

显然这样的时间复杂度我们是接受不了的。我们可以换个角度来像这个问题:

保证第k步在棋盘上的概率 = 第k - 1步在棋盘上并且在该不出界点的8个方向任意一个的概率

  • (i, j)走k步不出界的概率 = (i, j)的八个方向走k - 1步不出界的概率之和 / 8
  • 构建三位dp数组,每一层代表K的不同,从0到K
  • 自底向上构建三维数组,可以用两个二维数组代替三位数组,空间复杂度优化为O(N^2)
/**
 * 时间复杂度:O(N^N * K) 构建K次二位dp数组
 * 空间复杂度:O(N^N) dp数组
 */
class Solution {
    public double knightProbability(int N, int K, int r, int c) {
        //dp[row][col]用于存储在row行,col列步不出界的概率
        double[][] dp = new double[N][N];

        int[][] directions = {
            {1, 2}, {-1, 2}, {1, -2}, {-1, -2}, 
            {2, 1}, {-2, 1}, {2, -1}, {-2, -1}
        };//八个方向
        
        //初始化,当k == 0时,此时不能再移动了,将永远停在此地,所以不出界的概率为1.0
        for (int row = 0; row < N; ++row){
            for (int col = 0; col < N; ++col){
                dp[row][col] = 1.0;
            }
        }
        //开始动态规划
        for (int k = 1; k <= K; ++k){//自底向上,逐渐增加步数
            dounle[][] dp2 = new double[N][N];

            //下面的两个for是穷举起始点
            for (int row = 0; row < N; ++row){
                for (int col = 0; col < N; ++col){
                    double tempRes = 0;
                    //统计八个方向走k - 1步不出界的概率之和
                    for (int[] direction : directions){
                        int nextRow = row + direction[0];
                        int nextCol = col + direction[1];
                        if (nextRow < 0 || nextCol < 0 || nextRow >= N || nextCol >= N){ //出界
                            continue; 
                        }
                        else{
                            tempRes += dp[nextRow][nextCol];
                        }
                    }
                    dp2[row][col] += tempRes / 8; //最后除8,因为当前从八步中选择不出界也有概率
                }
            }
            dp = dp2;		// 将新的数据放在dp中,下次循环使用
        }
        
        return dp[r][c];
    }
}

034、LeetCode78-子集(回溯,位运算)

本题目可以用回溯法做出来,即保存所有路径,将过程中每一项都放入结果集中

/**
 * 时间复杂度:O(n * 2^n) 一共有2^n个结果,每个结果都需n的时间来构造
 * 空间复杂度:O(n) 递归站的深度为n, 且list长度最长位n
 */
class Solution {

    List<List<Integer>> ans = new ArrayList<>();

    public List<List<Integer>> subsets(int[] nums) {
        backTracking(new ArrayList<Integer>(), nums, 0);
        return ans;
    }
    
    void backTracking(List<Integer> temp, int[] nums, int start) {
        ans.add(new ArrayList<>(temp));
        for(int i = start; i < nums.length; i++){
            temp.add(nums[i]);
            backTracking(temp, nums, i + 1);
            temp.remove(temp.size() - 1);
        }
    }
}

第二种方法是位运算,即对于这样的一个数组[1,2,3], 三个二进制位就可以表示所有情况:

  • 000 ===> []
  • 001 ===> [3]
  • 111 ===> [1,2,3]

即将数组的长度当作二进制位的长度,从0遍历,分别移动位数即可找出所有情况

class Solution {
    
    public List<List<Integer>> subsets(int[] nums) {
        int size = nums.length;
        int n = 1 << size;
        List<List<Integer>> res = new ArrayList<>();

        for (int i = 0; i < n; i++) {
            List<Integer> cur = new ArrayList<>();
            for (int j = 0; j < size; j++) {
                if (((i >> j) & 1) == 1) {
                    cur.add(nums[j]);
                }
            }
            res.add(cur);
        }
        return res;
    }
}

035、LeetCode260-只出现一次的数字3(位运算)

本题目要求时间复杂度为O(n),空间复杂度为O(1)

可以使用异或操作来求出答案:

  • 首先,数组中的数全部xor操作,得到的就是要返回的两个数的xor结果
  • mask = xor & (-xor) 得到的就是该结果的最右边一个1的数。
  • 根据这一位是1还是0,将数组分成两个部分,要求的两个数分别在两个部分
  • 问题转换成求一个数了,每部分再分别异或,求出结果
/**
 * 时间复杂度:O(n)
 * 空间复杂度:O(1)
 */
class Solution {
    public int[] singleNumber(int[] nums) {
        int xor = 0;
        for(int num : nums) {
            xor ^= num;
        }
        int bitmask = xor & (-xor);
        int[] rets = {0, 0};
        for (int num : nums) {
            //然后再把数组分为两部分,每部分再分别异或
            if ((num & bitmask) == 0) {
                rets[0] ^= num;
            } else {
                rets[1] ^= num;
            }
        }
        return rets;
    }
}

036、LeetCode778-水位上升的泳池中游泳(并查集、二分、bfs、dfs)

本题目的解法综合了很多知识点,是一道比较好的题目。

首先可以使用并查集,和leetcode1631-最小体力消耗路径一样,是一个图论的题目

我们将这 mn 个节点放入并查集中,实时维护它们的连通性。由于我们需要找到从左上角到右下角的最短路径,因此我们可以将图中的所有边按照权值从小到大进行排序(这里边的权值极为两点的最大值),并依次加入并查集中。当我们加入一条权值为 x 的边之后,如果左上角和右下角从非连通状态变为连通状态,那么 x 即为答案。

/**
 * 时间复杂度:O(n^n logn) 
 * 空间复杂度:O(n^n) 数组长度以及并查集长度
 */
class Solution {
    public int swimInWater(int[][] grid) {
        int N = grid.length;
        /**
         * int[3] = {第一个点的序号,第二个点的序号,权值(两点的最大值)}
         */
        List<int[]> list = new ArrayList<>();
        for(int i = 0; i < N; i++) {
            for(int j = 0; j < N; j++) {
                if(j != N - 1) {
                    list.add(new int[] {i * N + j, i * N + j + 1, Math.max(grid[i][j], grid[i][j+1])});
                }
                if(i != N - 1) {
                    list.add(new int[] {i * N + j, i * N + j + N, Math.max(grid[i][j], grid[i+1][j])});
                }
            }
        }
        // 按照权值进行排序
        Collections.sort(list, (a, b) -> (a[2] - b[2]));
        DSU dsu = new DSU(N * N);

        int ans = 0;
        for(int[] arr : list) {
            int f = arr[0], s = arr[1], t = arr[2];            
            dsu.union(f, s);
            // 如果加的边满足了左上角和右下角联通,则返回此时的权值就是最小值
            if(dsu.connected(0, N * N - 1)) {
                ans = t;
                break;
            }
        }
        return ans;
    }
}

除此之外,还可以使用二分+搜索的方法来解答,因为题目条件grid[i][j] 是 [0, N^N-1]的排列,所以t的最大值就是N^N -1

我们用二分思想,一步步逼近t的最小值,搜索的过程可以使用dfs和bfs

// 时间复杂度:O(N^2 logN) 二分时间复杂度为 logN^2, 每次查找都要便利所有的节点(N^N),所以总的时间复杂度为(N^N * logN)
// 空间复杂度:O(N^2)。数组 visited 的大小为 N^2,如果使用深度优先遍历,须要使用的栈的大小最多为 N^2,如果使用广度优先遍历,须要使用的队列的大小最大为N^2。

class Solution {
    // binarysearch
    
    int[][] directions = {
        {1, 0}, {-1, 0}, {0, 1}, {0, -1}
    };
    int N = 0;

    public int swimInWater(int[][] grid) {
        N = grid.length;

        int left = 0, right = N * N - 1;
        while(left < right) {
            int mid = (left + right) / 2;
            // boolean[][] visit = new boolean[N][N];
            if(mid >= grid[0][0] && bfs(mid, grid)) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }

        return left;
    }

    // dfs 在时间为t时看是否能走到右下角
    public boolean dfs(int x, int y, int t, boolean[][] visit, int[][] grid) {
        if(x < 0 || x >= N || y < 0 || y >= N || grid[x][y] > t || visit[x][y]) return false;
        if(x == N - 1 && y == N - 1) return true;
        visit[x][y] = true;
        
        for(int i = 0; i < 4; i++) {
            if(dfs(x + directions[i][0], y + directions[i][1], t, visit, grid)) return true;
        }
        return false;
    }

    // bfs 在时间为t时看是否能走到右下角
    public boolean bfs(int t, int[][] grid) {
        Queue<int[]> queue = new LinkedList<>();
        queue.offer(new int[]{0, 0});
        boolean[][] visited = new boolean[N][N];
        visited[0][0] = true;

        while(!queue.isEmpty()) {
            int[] node = queue.poll();
            for(int i = 0; i < 4; i++) {
                int newX = node[0] + directions[i][0];
                int newY = node[1] + directions[i][1];
                if(newX < 0 || newX >= N || newY < 0 || newY >= N 
                    || visited[newX][newY] || grid[newX][newY] > t) {
                    continue;
                }
                if(newX == N - 1 && newY == N - 1) return true;
                queue.offer(new int[] {newX, newY});
                visited[newX][newY] = true;
            }
        }
        return false;
    }
}

037、LeetCode435-无重叠区间(贪心)

首先要对区间进行排序,这里先以区间的头来排序,然后在遍历区间。

  1. 如果后面区间的头小于当前区间的尾,比如当前区间是[3,6],后面区间是[4,5]或者是[5,9]
    说明这两个区间有重复,必须要移除一个,那么要移除哪个呢,为了防止在下一个区间和现有区间有重叠,我们应该让现有区间越短越好,所以应该移除尾部比较大的,保留尾部比较小的。
  2. 如果后面区间的头不小于当前区间的尾,说明他们没有重合,不需要移除
/**
 * 时间复杂度:O(nlogn) 排序
 * 空间复杂度:O(1)
 */
class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        if (intervals.length == 0)    return 0;
        //先排序
        Arrays.sort(intervals, (a, b) -> a[1] - b[1]);
        //记录区间尾部的位置
        int end = intervals[0][1];
        //需要移除的数量
        int count = 0;
        for (int i = 1; i < intervals.length; i++) {
            if (intervals[i][0] < end) {
                //如果重叠了,必须要移除一个,所以count要加1,
                //然后更新尾部的位置,我们取尾部比较小的, 也就是上一个
                count++;
            } else {
                //如果没有重叠,就不需要移除,只需要更新尾部的位置即可
                end = intervals[i][1];
            }
        }
        return count;
    }
}

038、AcWing 1101-献给阿尔吉侬的花束(BFS)

本题目很明显是用搜索算法写出来的,需要注意的是:一旦涉及到图和最短路径、最短距离、最小长度首先应该想到的是BFS,通过BFS一层一层遍历可以找到最短路径

  • 如果图中可以从上下左右四个方向行进,那么我们一般使用一个长度为4的二维数组来模拟
  • 因为BFS一层一层向四个方向遍历,需要用相同的大小记录一下哪些遍历哪些没有,同时这个空间还用来判断走了几步
  • 如果原空间可以更改的话,可以直接在原来的空间上修改
import java.util.*;
/**
 * 时间复杂度(针对一个用例):O(mn) m是该用例的长度,n是宽度
 * 空间复杂度(针对一个用例):O(mn)
 */
public class Main{
    
    static int[][] directions = {
        {0, 1}, {0, -1}, {1, 0}, {-1, 0}
    };
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int num = sc.nextInt();
        for(int i = 0; i < num; i++) {
            int rows = sc.nextInt(), cols = sc.nextInt();
            char[][] chars = new char[rows][cols];
            int startX = 0, startY = 0;
            for(int  j = 0; j < rows; j++) {
                chars[j] = sc.next().toCharArray();      
                for(int k = 0; k < cols; k++) {
                    if(chars[j][k] == 'S') {
                        startX = j;
                        startY = k;
                    }
                }
                
            } 
            int ans = bfs(startX, startY, chars, new int[rows][cols]);
            if(ans != -1) {
                System.out.println(ans);
            } else {
                System.out.println("oop!");
            }
            
        }
        
    }
    
    // 队列实现BFS
    public static int bfs(int x, int y, char[][] chars, int[][] visited) {
        Queue<int[]> queue = new LinkedList<>();
        queue.add(new int[]{x, y});
        visited[x][y] = 1;
        while(!queue.isEmpty()) {
            int[] p = queue.poll();
            for(int i = 0; i < 4; i++) {
                int newX = p[0] + directions[i][0];
                int newY = p[1] + directions[i][1];
                if(newX < 0 || newX >= chars.length || 
                    newY < 0 || newY >= chars[0].length 
                    || chars[newX][newY] == '#' || visited[newX][newY] != 0) continue;
                visited[newX][newY] = visited[p[0]][p[1]] + 1;
                if(chars[newX][newY] == 'E') return visited[newX][newY] - 1;
                queue.add(new int[] {newX, newY});
            }
            
        }
        return -1;   
    }   
}

039、Acwing 89-a^b(位运算)

040、LeetCode1423-可获得的最大点数(SW)

一句话,通过求出剩余连续卡牌点数之和的最小值,来求出拿走卡牌点数之和的最大值

/**
 * 时间复杂度:O(n)
 * 空间复杂度:O(1)
 */
class Solution {
    // 通过求出剩余连续卡牌点数之和的最小值,来求出拿走卡牌点数之和的最大值
    public int maxScore(int[] cardPoints, int k) {
        int sum = 0;
        for(int card:cardPoints) {
            sum += card;
        }
        int left = 0, min = Integer.MAX_VALUE, curSum = 0;
        for(int i = 0; i < cardPoints.length; i++) {
            if(i >= cardPoints.length - k) {
                curSum -= cardPoints[i - (cardPoints.length - k)];
            }
            curSum += cardPoints[i];
            if(i >= cardPoints.length - k - 1) min = Math.min(min, curSum);
        }
        return sum - min;
    }
}

041、LeetCode1208-尽可能使字符串相等(SW)

本题目的思考过程:

首先,可以构建一个长度为字符串长度的数组costcost[i]表示第i个字符转换的代价,如cost = [0,1,2,0,8,7,6]

给定一个最大值maxCost,题目就变成了找出满足和小于等于maxCost的子数组的最大长度。当然这样只是方便分析问题,实际上没有必要建立一个数组。

自然就可以使用滑动窗口

  • 如果maxCost可以被减去,就伸长窗口
  • 如果maxCost不可以被减去(剪完小于0),就将窗口最左边补到maxCost,在这个过程中动态维护最大值
/**
 * 时间复杂度:O(n) n为两个字符串的长度
 * 空间复杂度:O(1) 
 */
class Solution {

    public int equalSubstring(String s, String t, int maxCost) {
        int N = s.length();
        int ans = 0, left = 0;
        for(int i = 0; i < N; i++) {
            while(cost(s, t, i) > maxCost && left < i) {
                maxCost += cost(s, t, left);
                left++;
            }
            // 如果是因为“当前cost太大,从而使得left移动到和i相等”的情况,就让left和i从下一个开始
            if(maxCost < cost(s, t, i)) {
                left = i+1;
                continue;
            }
            maxCost -= cost(s, t, i);
            ans = Math.max(ans, i - left + 1);
        }
        return ans;
    }
	// 计算两个字符串索引为i的两个字符的 “代价”
    public int cost(String s, String t, int i) {
        return Math.abs(s.charAt(i) - t.charAt(i));
    }
}

042、LeetCode665-非递减数列(贪心)

本题目是一道没有做出来的简单题目

"在最多改变一个元素的情况下,判断该数组是否能变成单调递增的数组(可以相等)"

在遍历元素的时候,如果我们碰到一个元素下一个元素比当前元素小,此时有两种处理方法:

  1. 将本元素减小,并且本元素不能小于本元素的上一个元素
  2. 将下一个元素增大,但是不能保证下一个元素增大有没有超过后面元素

所以,我们应该做的是尽可能不放大下一个元素(贪心),这样会让后续非递减更困难,但是如果下一个元素小到比上一个元素还小,这样我们没有办法让本元素减小来维持整个数组的递增,只能将下一个元素增大

/**
 * 时间复杂度:O(n) n为数组长度
 * 空间复杂度:O(1)
 */
class Solution {
    public boolean checkPossibility(int[] nums) {
        if(nums.length == 1) return true;
        // 初始化修改机会
        boolean flag = nums[1] >= nums[0] ? true : false;
        for(int i = 1; i < nums.length - 1; i++) {
            // 出现递减
            if(nums[i] > nums[i+1]) {
                if(flag) {	// 如果还有更改机会
                    if(nums[i+1] >= nums[i-1]) {	// 将本元素减小
                        nums[i] = nums[i+1];
                    } else {						// 将下一个元素增大
                        nums[i+1] = nums[i];
                    }
                    flag = false;
                } else {
                    return false;
                }
            }
        }
        return true;
    }
}

043、LeetCode567-字符串的排列(SW)

由题目分析,s1的排列即可以认为组成s1的所有元素及其数量,又因为s1s2都为小写字母,故可以用int[26]来记录s1中所有的字母及其数量情况。

题目即变为:求s2中有无一个子串,其字符长度和数量和从s1构建的int[26]相同,用滑动窗口解决:

  • 遍历s2,如果碰到的字母在int[26]不存在,则缩小窗口(left右移)
  • 如果存在,则扩大窗口(right右移)
  • 如果当中某个时候滑动窗口长度正好等于s1,则代表有答案
/**
 * 时间复杂度:O(n) n为s2的长度
 * 空间复杂度:O(1) 
 */
class Solution {
    public boolean checkInclusion(String s1, String s2) {
        int[] arr = new int[26];
        for(int i = 0; i < s1.length(); i++) {
            arr[s1.charAt(i) - 'a']++;
        }

        int left = 0, ans = 0;
        for(int i = 0; i < s2.length(); i++) {
            // 如果没有本字符,就缩小滑动窗口
            while (arr[s2.charAt(i) - 'a'] <= 0 && left < i) {
                arr[s2.charAt(left) - 'a']++;
                left++;
            }
            // 如果是因为缩小窗口导致left和i相等,就直接跳到下一个
            if(arr[s2.charAt(i) - 'a'] <= 0) { 
                left = i+1;
                continue;
            }
            arr[s2.charAt(i) - 'a']--;
            // 如果滑动窗口的大小和s1的大小一样,就代表条件成立
            if(i - left == s1.length() - 1) return true;
        }

        return false;
    }
}

本题目可以和LeetCode30-串联所单词的子串配合使用:

和本题目不同的是

  • 该题将字符换成了单词,所以不能用int[26]换成了Map,整体解决思路一样。
  • 因为单词起始点不同导致遍历结果不同,所以需要加一个外层循环长度为单词的长度
  • 外层循环导致map在外层一次完成后没有恢复到原始数据,所以将left完全右移恢复map

044、LeetCode227-基本计算器2(模拟、Stack)

可以使用两个stack,一个作为符号栈、另一个为数字栈。

也可以用一个栈来表示,关键是将四则运算转化为最后相加。碰到不同情况不同讨论:

  • 加法直接入栈即可
  • 减法转化为加上相反数
  • 乘除法直接拿栈顶元素计算再入栈
class Solution {
    public int calculate(String s) {
        // 保存上一个符号,初始为 +
        char sign = '+';
        Stack<Integer> numStack = new Stack<>();
        // 保存当前数字,如:12是两个字符,需要进位累加
        int num = 0;
        for(int i = 0; i < s.length(); i++){
            char cur = s.charAt(i);
            if(cur >= '0'){
                // 记录当前数字。先减,防溢出
                num = num*10 - '0' + cur;
            }
            if((cur < '0' && cur !=' ' ) || i == s.length() - 1){
                // 判断上一个符号是什么
                switch(sign) {
                    // 当前符号前的数字直接压栈
                    case '+': numStack.push(num);break;
                    // 当前符号前的数字取反压栈
                    case '-': numStack.push(-num);break;
                    // 数字栈栈顶数字出栈,与当前符号前的数字相乘,结果值压栈
                    case '*': numStack.push(numStack.pop() * num);break;
                    // 数字栈栈顶数字出栈,除于当前符号前的数字,结果值压栈
                    case '/': numStack.push(numStack.pop() / num);break;
                }
                // 记录当前符号
                sign = cur;
                // 数字清零
                num = 0;
            }
        }

        int result = 0;
        // 将栈内剩余数字累加,即为结果
        while(!numStack.isEmpty()){
            result += numStack.pop();
        }
        return result;
    }
}

045、剑指offer-04-二维数组中的查找(二分,模拟)

​ 实际上一看到题目会觉得应该使用两次二分就可以找到,一次查找所在的行,另一次查找所在的列

​ 但是实际上因为不可以保证本列的第一个元素大于上一列的最后一个元素,所以是不能够用的,随机我就想到了循环二分,可以对每一列都来一次二分,都找不到就返回false,这是可以做到的,并且时间复杂度不算太高,O(nlogm),但是这样做就放弃了每一列从上到下是递增的这一个条件,显然不是最佳答案

​ 我们发现,这种二维数组的特性在右上角,即对于右上角的元素来说,左边的元素比较小,下面的元素比较大。我们可以利用这个性质,想象成一颗二叉搜索树,通过模拟在树上不断选择左右分支即可:

  • 如果找到了返回true
  • 如果没有找到(出界了)返回false
class Solution {
    public boolean findNumberIn2DArray(int[][] matrix, int target) {
        if(matrix.length == 0) return false;

        // 选取右上的元素
        int row = 0, col = matrix[0].length - 1;
        while(row >= 0 && row < matrix.length 
              && col >=0 && col <= matrix[0].length) {
            if(matrix[row][col] == target) {
                return true;
            } else if(matrix[row][col] > target) {
                col--;
            } else {
                row++;
            }
        }
        return false;
    }
}

046、LeetCode331-验证二叉树的前序序列化(栈,模拟)

首先能想到的是用stack模拟:每碰到两个’#’,就pop一个,并再入一个’#’,代表作为了一个新的叶子结点

/**
 * 时间复杂度:O(n)
 * 空间复杂度: O(n) 栈的空间,以及 string[] 的消耗
 */
class Solution {
    public boolean isValidSerialization(String preorder) {
        String[] preOrderArray = preorder.split(",");
        Deque<String> stack = new LinkedList<>();
        for(int i = 0; i < preOrderArray.length; i++) {
            stack.push(preOrderArray[i]);
            if(preOrderArray[i].equals("#")) {
                if(!check(stack)) return false;
            }
        }
        return stack.size() == 1 && stack.peek().equals("#");
    }
    // 循环检查pop,防止出现连锁'##’
    public boolean check(Deque<String> stack) {
        boolean flag = true;
        while (stack.size() >= 2 && flag) {
            String top1 = stack.pop();
            String top2 = stack.pop();
            if(top1.equals("#") && top2.equals("#")) {
                // 如果不够了,就代表‘#'多了,直接返回false
                if(stack.isEmpty()) return false;
                stack.pop();
                stack.push("#");
            } else {
                stack.push(top2);
                stack.push(top1);
                flag = false;
            }
        }
        return true;
    }

但是本题目实际上不需要用stack就可以模拟,时间复杂度没有变但是消耗变少了:

  • string从后遍历,用num记录#的个数
  • 当遇到正常节点时,#的个数-2,并将该节点转化成#,最后就是num+1
  • 当出现num的个数不足2时,即false,最终也须保证num为1(代表只剩一个根节点)
/**
 * 时间复杂度:O(n)
 * 空间复杂度: O(1)
 */
class Solution {
    public boolean isValidSerialization(String preorder) {
        int n = preorder.length();
        int num = 0;        //记录#的个数
        for(int i = n-1; i>=0; i--){
            if(preorder.charAt(i) == ',')
                continue;
            if(preorder.charAt(i) == '#')
                num++;
            else{
                while(i >= 0 && preorder.charAt(i) != ',') //节点数字可能有多位
                    i--;
                if(num < 2) return false; // 如果不够减了就返回false
                num--;  // # 消除2个#,消除一个节点数字并转换成#,即num-1
            }
        }
        return num == 1;
    }
}

047、剑指offer-12-矩阵中的路径(dfs,回溯)

比较基础的一道dfs题目,我们可以

  • 遍历二维矩阵中的每个字符,看看从这个字符开始能否找到一条这样的路线
  • 如果找到了就直接返回true即可
  • 如果遍历完都还没找到就返回false

对于从一个坐标开始查找相应的字符串,我们可以用dfs的方式

class Solution {

    public boolean exist(char[][] board, String word) {
        for(int i = 0; i < board.length; i++) {
            for(int j = 0; j < board[0].length; j++) {
                if(dfs(i, j, word, 0, board)) return true;
            }
        }
        return false;
    }

    private boolean dfs(int x, int y, String word, int n, char[][] board) {
        // 如果出界了,或者已经访问过了,或者该值不符合字符串中的当前值,就返回false
        if(!check(x, y, board) || board[x][y] == '*' 
           || word.charAt(n) != board[x][y]) return false;
        // 如果最后一个字符也被找到了,直接返回true即可
        if(n == word.length() - 1) return true;
        // 对于走过的路,用*标识
        char temp = board[x][y];
        board[x][y] = '*';
        // 短路方法,一个为true就直接返回即可
        boolean res = dfs(x - 1, y, word, n + 1, board) 
            		|| dfs(x + 1, y, word, n + 1, board)
                    || dfs(x, y - 1, word, n + 1, board) 
            		|| dfs(x, y + 1, word, n + 1, board);
        // 标识取消
        board[x][y] = temp;
        return res;
    }

    // 判断坐标是否出界
    private boolean check(int x, int y, char[][] board) {
        return x >= 0 && x < board.length && y >= 0 && y < board[0].length;
    }
}

比较剑指offer-13-机器人的运动范围,本题目需要遍历从哪个地方开始,所以外边套了循环,但是在13中,我们知道一定是从[0,0]开始的,所以就不需要外边套循环,并且因为求的是能够到达多少个格子?,所以我们没有必要四个方向都走,而是只走右方向和下方向即可:

class Solution {
    
    private int ans = 0;
    private int[][] move = {
        {1, 0}, {-1, 0}, {0, 1}, {0, -1}
    };
    
    public int movingCount(int m, int n, int k) {
        dfs(0, 0, m, n, k, new boolean[m][n]);
        return ans;
    }

    private void dfs(int x, int y, int m, int n, int k, boolean[][] visited) {
        if(x < 0 || x >= m || y < 0 || y >= n 
           || visited[x][y] || !check(x, y, k)) return;
        visited[x][y] = true;
        ans++;
        dfs(x+1, y, m, n, k, visited);
        dfs(x, y+1, m, n, k, visited);
    }

    // 能否到达该坐标
    private boolean check(int x, int y, int k) {
        int cnt = 0;
        while(x > 0 || y > 0) {
            if(x > 0) {
                cnt += x % 10;
                x /= 10;
            }
            if(y > 0) {
                cnt += y % 10;
                y /= 10;
            }
        }
        return cnt <= k;
    }
}

048、剑指offer-17-打印从1到最大的n位数(dfs)

本题目要考虑的最核心问题就是大数问题,当n足够大,其值超过了整数(int、long)的最大值所代表的的位数,该用什么来承载值呢?显然应该用字符串,用字符串是否要考虑进位问题?我们可以用全排列来解决

public class Test2 {
    StringBuilder sb;
    int n;
    char[] num, loop = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
    public void printNumbers(int n) {
        sb = new StringBuilder();
        this.n = n;
        num = new char[n];
        dfs(0);
        System.out.println(sb.toString());
    }

    private void dfs(int x) {
        if(x == this.n) {
            sb.append(String.valueOf(num)).append(",");
            return;
        }
        for (char ch : loop) {
            num[x] = ch;
            dfs(x + 1);
        }
    }
}

但是这会有问题:

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

输入:n = 2
输出:"00,01,02,...,10,11,12,...,97,98,99"

输入:n = 3
输出:"000,001,002,...,100,101,102,...,997,998,999"
  • 不是从1开始的
  • 前面的不需要的0应该省略

占坑解决

049、LeetCode402-移掉k位数字(单调栈)

细节很多的一道题目,解法不难想

本题目经过分析可得:

  • 想让留下来的数字最小,则高位数字尽可能小才行
  • 单调栈的作用就是让在栈底的数字一定要尽可能小,于是可以用单调递增的栈
  • 碰到递增就入栈,否则就更新栈顶(需考虑剩下的位数够不够了,够的话才pop),目的就是让前面位尽可能小
  • 如果字符串是1234567,可能不需要更新栈顶即可,我们根据要剩下多少个就从栈底部拿多少个即可(实际上是单调队列)
/*
	- 时间复杂度: O(n) n为num的字符串长度
	- 空间复杂度: O(n) 栈的最大值 
 */

class Solution {
    public String removeKdigits(String num, int k) {
        if (num == null || num.length() == 0 || num.length() == k) return "0";

        Deque<Character> stack = new LinkedList<>();
        int index = 0, len = num.length();
        int popNum = 0;

        while (index < len) {
            // 如果栈为空,或者一个更大的数字过来了,就入栈
            while(index < len && (stack.isEmpty() 
                                  || stack.peekLast() <= num.charAt(index))) {
                stack.addLast(num.charAt(index++));
            }
            // 如果index遍历完了,就直接退出即可
            if(index == len) break;
            // 如果栈不为空,并且符合更新栈顶的条件,就更新
            // popNum为pop掉的元素,不能超过k
            while(!stack.isEmpty() && popNum < k 
                  && stack.peekLast() > num.charAt(index)) {
                stack.removeLast();
                popNum++;
            }
            // 如果栈为空,或者剩下的元素不够了,或者已经将该pop的pop掉了,直接入栈
            stack.addLast(num.charAt(index++));
        }
        index = 0;
        StringBuilder sb = new StringBuilder();
        // 前导0
        while(!stack.isEmpty() && stack.peekFirst() == '0')
            stack.removeFirst();
        if(stack.isEmpty()) return "0";
        // 更新结果集
        while(!stack.isEmpty() && index < len - k) {
            sb.append(stack.removeFirst());
            index++;
        }
        return sb.toString();
    }
}

050、LeetCode115-不同的子序列(回溯,dp)

本题目首先可以看出来,通过回溯是可以做出来的,遍历所有情况即可

class Solution {
    int count = 0;
    public int numDistinct(String s, String t) {
        numDistinctHelper(s, 0, t, 0);
        return count;
    }

    private void numDistinctHelper(String s, int s_start, String t, int t_start) {
        if (t_start == t.length()) {
            count++; 
            return;
        }
        if (s_start == s.length()) {
            return;
        }
        //当前字母相等,s_start 后移一个,t_start 后移一个
        if (s.charAt(s_start) == t.charAt(t_start)) {
            numDistinctHelper(s, s_start + 1, t, t_start + 1);
        }
        //出来以后,继续尝试不选择当前字母,s_start 后移一个,t_start 不后移
        numDistinctHelper(s, s_start + 1, t, t_start);
    }
}

但是本题目显然有更简单的解法,那就是动态规划,占坑

051、NC88-寻找第K大(快排)

本题目可以在原来的非递归快排上进行改造,使得选出pivot,指针遍历完时:

  • 如果发现pivot索引就是第K个,则直接输出
  • 如果pivot索引小于第K个,在右边找(右半边入栈)
  • 如果pivot索引大于第K个,在右边找(左半边入栈)
import java.util.*;

public class Solution {
    
    public int findKth(int[] a, int n, int K) {
        if(a.length == 0 || K == 0) return 0;
        return quickSort(a,n - K + 1);
    }
    // 快排
    private int quickSort(int[] arr, int K) {
        Deque<int[]> stack = new LinkedList<>();
        int[] arrNode = {0, arr.length - 1};
        stack.push(arrNode);

        while (!stack.isEmpty()) {
            int[] node = stack.pop();
            // if(node[0] >= node[1]) continue;
            int left = node[0], right = node[1], pivot = arr[left];
            while(left < right) {
                // 右
                while (left < right) {
                    if (arr[right] > pivot) {
                        right--;
                    } else {
                        arr[left] = arr[right];
                        left++;
                        break;
                    }
                }
                // 左
                while (left < right) {
                    if (arr[left] < pivot) {
                        left++;
                    } else {
                        arr[right] = arr[left];
                        right--;
                        break;
                    }
                }
            }
            arr[left] = pivot;
            if(left == K - 1) {
                return arr[left];
            } else if(left < K - 1) {
                stack.push(new int[]{left + 1, node[1]});
            } else {
                stack.push(new int[]{node[0], left - 1});
            }
        }
        return 0;
    }

}

052、LeetCode56-合并区间(模拟)

本题目最重要的就是分情况讨论,然后模拟即可:

  • 首先对区间按照前面节点的位置进行排序

  • 然后后者相对于前者的情况有三种:

    IMG_20210321_210703
  • 针对三钟情况讨论即可:

    1. 合并到前一个(continue)
    2. 改变前一个的后沿(先pop,再add)
    3. 新增一个(add)
class Solution {
    public int[][] merge(int[][] intervals) {
        if(intervals.length == 0) return null;
        
        List<int[]> ans = new ArrayList<>();
        Arrays.sort(intervals, (a, b) -> (a[0] - b[0]));
        ans.add(intervals[0]);
        for(int i = 1; i < intervals.length; i++) {
            int[] last = ans.get(ans.size() - 1);
            if(intervals[i][0] <= last[1]) {
                if(intervals[i][1] <= last[1]) {
                    // case 1
                    continue;
                } else {
                    // case 2
                    last[1] = intervals[i][1];
                }
            } else {
                // case 3
                ans.add(intervals[i]);
            }
        }
        int[][] ansArr = new int[ans.size()][2];
        for(int i = 0; i < ansArr.length; i++)
            ansArr[i] = ans.get(i);
        return ansArr;
    }
}

053、LeetCode76-最小覆盖子串(SW)

一道SW hard题,2h+ 没有做出来,关键是没有理清楚题意。

  • 对于t串,究竟用什么去存储,很明显对于这种字符串的匹配来说应该用map,但是用map显然没有用数组更加方便,小技巧是对于大小写都有的字符串,我们可以用int[128]表示(ASCII)
  • 对于两个串进行匹配,如何判断子串满足条件,这里比较精巧的地方在于滑动窗口用了一个值count,代表有效的和t串已匹配的个数,当count == t.length() 代表匹配成功。如何维护这个count呢?我们还是用数组实现,设char为SW右移新加入的字符,即t串中char的数量 != 0 && 新加入的char的数量 < t串char的数量才会对count加1
  • 如果一个SW串匹配成功了(count = t.length()),那么SW的后沿后移就一定能匹配成功,因为我们要求最小的SW,则碰见SW匹配成功我们要做的是将左沿右移,看能否逼近最小的SW,当然这个过程要减小count,并且更新ans
/*
 * 时间复杂度:O(n) n为s串的长度
 * 空间复杂度:O(1) 用到的空间是常数级别
 */
class Solution {
    public String minWindow(String s, String t) {
        if (s == null || s == "" || t == null || t == "" || s.length() < t.length()) {
            return "";
        }
        //维护两个数组,记录已有字符串指定字符的出现次数,和目标字符串指定字符的出现次数
        //ASCII表总长128
        int[] need = new int[128];
        int[] have = new int[128];

        //将目标字符串指定字符的出现次数记录
        for (int i = 0; i < t.length(); i++) {
            need[t.charAt(i)]++;
        }

        //分别为左指针,右指针,最小长度(初始值为一定不可达到的长度)
        //已有字符串中目标字符串指定字符的出现总频次以及最小覆盖子串在原字符串中的起始位置
        int left = 0, right = 0, min = s.length() + 1, count = 0, start = 0;
        while (right < s.length()) {
            char r = s.charAt(right);
            //说明该字符不被目标字符串需要,此时有两种情况
            // 1.循环刚开始,那么直接移动右指针即可,不需要做多余判断
            // 2.循环已经开始一段时间,此处又有两种情况
            //  2.1 上一次条件不满足,已有字符串指定字符出现次数不满足目标字符串指定字符出现次数,那么此时
            //      如果该字符还不被目标字符串需要,就不需要进行多余判断,右指针移动即可
            //  2.2 左指针已经移动完毕,那么此时就相当于循环刚开始,同理直接移动右指针
            if (need[r] == 0) {
                right++;
                continue;
            }
            //当且仅当已有字符串目标字符出现的次数小于目标字符串字符的出现次数时,count才会+1
            //是为了后续能直接判断已有字符串是否已经包含了目标字符串的所有字符,不需要挨个比对字符出现的次数
            if (have[r] < need[r]) {
                count++;
            }
            //已有字符串中目标字符出现的次数+1
            have[r]++;
            //移动右指针
            right++;
            //当且仅当已有字符串已经包含了所有目标字符串的字符,且出现频次一定大于或等于指定频次
            while (count == t.length()) {
                //挡窗口的长度比已有的最短值小时,更改最小值,并记录起始位置
                if (right - left < min) {
                    min = right - left;
                    start = left;
                }
                char l = s.charAt(left);
                //如果左边即将要去掉的字符不被目标字符串需要,那么不需要多余判断,直接可以移动左指针
                if (need[l] == 0) {
                    left++;
                    continue;
                }
                //如果左边即将要去掉的字符被目标字符串需要,且出现的频次正好等于指定频次,那么如果去掉了这个字符,
                //就不满足覆盖子串的条件,此时要破坏循环条件跳出循环,即控制目标字符串指定字符的出现总频次(count)-1
                if (have[l] == need[l]) {
                    count--;
                }
                //已有字符串中目标字符出现的次数-1
                have[l]--;
                //移动左指针
                left++;
            }
        }
        //如果最小长度还为初始值,说明没有符合条件的子串
        if (min == s.length() + 1) {
            return "";
        }
        //返回的为以记录的起始位置为起点,记录的最短长度为距离的指定字符串中截取的子串
        return s.substring(start, start + min);
    }
}

054、LeetCode456-132模式(单调栈)

对于三个索引为i,j,k(i < j < k)的整数来说,如果出现了nums[i] < nums[k] < nums[j]的情况时,我们就认为该数组中存在的132模式,返回true,否则返回false

本题目直接暴力是可以做出来的,时间复杂度为O(n^2),思路为

  • 遍历j,即每遍历到一个j,在左边找到一个小于j的数nums[i],在右边找到一个大于j的数\nums[k],并且nums[i] < nums[k]
  • 如果找不到则返回false
  • 对于在左边的数,我们可以维护一个最小值变量,右边则不断循环
/**
 * 时间复杂度:O(n^n) n为数组长度
 * 空间复杂度:O(1) 需要常数级的内存空间 
 */
 class Solution {
    public boolean find132pattern(int[] nums) {
        if(nums.length < 3) return false;
        int N = nums.length, min = nums[0];
        
        for(int i = 1; i < N - 1; i++) {
            for(int j = i+1; j < N; j++) {
                if(min < nums[j] && nums[i] > nums[j]) return true;
            }
            min = Math.min(min, nums[i]);
        }
        return false;
    }
}

本题目显然是可以优化的,优化的点就在于右半部分找到一个小于nums[j]的最大数,我们可以利用单调栈来维护关系

  • 解法一:对 j 进行从右到左遍历,维护一个min数组,min[n] 代表索引为n时,nums[0] ~ nums[n-1]的最小值,右半部分维护一个单调递增栈,但是栈顶必须小于当前元素,比较大小即可

  • 解法二:对 i 进行从右到左遍历,维护一个单调递减栈,如果从右到左一直是递减的,则不断入栈,否则的话元素pop,维护一个最值代表pop的最大值,即为k元素,当前栈顶则为j元素,如果碰到当前元素小于k元素,代表存在这样的模式

    /**
     * 时间复杂度:O(n) n为数组长度
     * 空间复杂度:O(n) stack空间最大值 
     */
    class Solution {
        public boolean find132pattern(int[] nums) {
            int n = nums.length;
            Deque<Integer> d = new ArrayDeque<>();
            int k = Integer.MIN_VALUE;
            
            for (int i = n - 1; i >= 0; i--) {
                if (nums[i] < k) return true;
                while (!d.isEmpty() && d.peekLast() < nums[i]) {
                    k = d.pollLast(); 
                }
                d.addLast(nums[i]);
            }
            return false;
        }
    }
    

055、LeetCode2-两数相加(模拟)

题目意思比较明确,也比较简单,但是怎么样想出一种简单的思路并show code,实际上有点难度。实际上在做这种题目的时候不要一下子就想要原地算法,O(n),应该是一步一步去优化的,所以这里用新的节点来存储ans

  • 设形参的两个头结点为两个指针,从链表头部往尾部遍历,不断加和并且加上进位。
  • 如果其中一个节点到头了,就一直设置为0即可
  • 注意最后的进位,如果进位最后还有,那么还需要再向前面链接一个node
/**
 * 时间复杂度:O(n) 较长链表的长度
 * 空间复杂度:O(n) 如果是原地算法可以优化为 O(1)
 */
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode ans = new ListNode(-1), cur = ans;
        int carry = 0;
        
        while(l1 != null || l2 != null) {
            int x = l1 == null ? 0 : l1.val;
            int y = l2 == null ? 0 : l2.val;
            int sum = x + y + carry;
            
            carry = sum / 10;
            sum %= 10;
            cur.next = new ListNode(sum);
            cur = cur.next;
            if(l1 != null) l1 = l1.next;
            if(l2 != null) l2 = l2.next;            
        }
        
        if(carry == 1) {
            cur.next = new ListNode(1);
        }
        return ans.next;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值