算法题解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-可获得的最大点数(滑动窗口)

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

/**
 * 时间复杂度: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-尽可能使字符串相等(滑动窗口)

本题目的思考过程:

首先,可以构建一个长度为字符串长度的数组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-字符串的排列(滑动窗口)

由题目分析,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();
    }
}

50、LeetCode115-不同的子序列

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

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);
    }
}

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

猜你喜欢

转载自blog.csdn.net/weixin_42999705/article/details/114969539