leetCode进阶算法题+解析(七十三)

自定义字符串排序

题目:字符串S和 T 只包含小写字符。在S中,所有字符只会出现一次。S 已经根据某种规则进行了排序。我们要根据S中的字符顺序对T进行排序。更具体地说,如果S中x在y之前出现,那么返回的字符串中x也应出现在y之前返回任意一种符合条件的字符串T。

示例:
输入:
S = "cba"
T = "abcd"
输出: "cbad"
解释:
S中出现了字符 "a", "b", "c", 所以 "a", "b", "c" 的顺序应该是 "c", "b", "a".
由于 "d" 没有在S中出现, 它可以放在T的任意位置. "dcba", "cdba", "cbda" 都是合法的输出。
注意:
S的最大长度为26,其中没有重复的字符。
T的最大长度为200。
S和T只包含小写字符。

思路:我觉得这个题目还是很简单的,目前的想法是s中每一个元素作为key存map,然後值是t中这个元素出现的次数。等重组t的时候可以先把t中s没出现的写前面。然後继续按照s的顺序重组字符串。反正思路很清晰,就是不知道会不会出现性能问题。我去试试了。
第一版代码:

class Solution {
    public String customSortString(String S, String T) {
        Map map = new HashMap<>();
        char[] arr = S.toCharArray();
        for(char c : arr) map.put(c,0);
        StringBuffer sb = new StringBuffer();
        for(char c : T.toCharArray()){
           if(map.get(c) != null){
               map.put(c,map.get(c)+1);
           }else {
               sb.append(c);
           }
        }
        for(char c : arr){
            int n = map.get(c);
            while (n>0){
                sb.append(c);
                n--;
            }
        }
        return sb.toString();
    }
}

思路是对的,就是性能不太好,我这里再看看怎么优化。
第二版本代码,我是觉得这里其实不用map也可以,毕竟最多就26个数组。然後上面的 代码2ms,这个代码1ms,虽然进步了但是还不是特别好。

class Solution {
    public String customSortString(String S, String T) {
        int[] count = new int[26];
        Arrays.fill(count,-1);
        char[] arr = S.toCharArray();
        for(char c : arr) count[c-'a']++;//这里没出现的是-1.出现了变0
        StringBuffer sb = new StringBuffer();
        for(char c : T.toCharArray()){
           if(count[c-'a'] > -1){
               count[c-'a']++;
           }else {
               sb.append(c);
           }
        }
        for(char c : arr){
            int n = count[c-'a'];
            while (n>0){
                sb.append(c);
                n--;
            }
        }
        return sb.toString();
    }
}

我去看看性能第一的代码:

class Solution {
    public String customSortString(String S, String T) {
        int[] count = new int[26];
        for(char c:T.toCharArray()){
            count[c-'a']++;
        }

        StringBuilder ans = new StringBuilder();
        for(char c :S.toCharArray()){
            for(int i=0;i

不明白为什么这个性能更好,for循环就比while强?反正这个题比较简单,就这样了,下一题。

匹配子序列的单词数

题目:给定字符串 S 和单词字典 words, 求 words[i] 中是 S 的子序列的单词个数。

示例:
输入:
S = "abcde"
words = ["a", "bb", "acd", "ace"]
输出: 3
解释: 有三个是 S 的子序列的单词: "a", "acd", "ace"。
注意:
所有在words和 S 里的单词都只由小写字母组成。
S 的长度在 [1, 50000]。
words 的长度在 [1, 5000]。
words[i]的长度在[1, 50]。

思路:这个题怎么说呢,感觉可实现的方式还是不少的,不管是每个words去对比,还是直接S的所有子串找出来。。但是问题是性能绝对是个大问题。。初步估计应该都会超时吧。总而言之我现在的想法还是用map。S中每一个字母对应的下标都记录。然後words直接去从字符开始判断,每次取尽量小的。当遇到无法取值的时候就false了。(这里的尽量小是满足上一个的后续的最小值。比如说上一个字母的下表取了11,那么当前字母可选下标6,12,16,19,因为6小于上一个所以直接pass。能取的最合适的值就是12.)我感觉思路挺清晰的,我去实现试试。
第一版本代码:

class Solution {
    public int numMatchingSubseq(String s, String[] words) {
        Map> map = new HashMap<>();
        for(int i = 0;i list = map.get(c);
            if(list == null) list =  new ArrayList<>();
            list.add(i);
            map.put(c,list);
        }
        int ans = 0;
        for(String temp : words){
            if(isOk(map,temp)) ans++;
        }
        return ans;
    }
    public boolean isOk(Map> map,String temp){
        int start = -1;
        for(char c:temp.toCharArray()){
            List list = map.get(c);
            if(list == null) return false;
            boolean flag = true;
            for(int i : list){
                if(i>start){
                    flag = false;
                    start = i;
                    break;
                }
            }
            //如果flag为true说明根本没有合适条件的值,所以直接false
            if(flag) return false;
        }
        return true;
    }
}

勉强过了没超时,我居然还挺庆幸。。。感觉可优化的点就是这个从头开始遍历map的list。这块肯定是有问题的,极端点的情况 aaaaaaaaaaa...aa.这样到后面的判断都快n方了。但是也最多就是从遍历变成二分。。觉得还是这个方式有点问题,我去看看题解吧。
题解跟我做的都好像不是一个题目,大概思路是分桶:
因为 S 很长,所以寻找一种只需遍历一次 S 的方法,避免暴力解法的多次遍历。将所有单词根据首字母不同放入不同的桶中。例如当 words = ['dog', 'cat', 'cop'],根据首字母不同可以分为 'c' : ('cat', 'cop'), 'd' : ('dog',)。换句话说,每个桶中的单词就是该单词正在等待匹配的下一个字母。在遍历 S 的同时,将匹配到单词根据下一个需要匹配的字母移动到不同的桶中。
例如,有字符串 S = 'dcaog':

  • 初始化 heads = 'c' : ('cat', 'cop'), 'd' : ('dog',);
  • 遍历 S[0] = 'd' 后,heads = 'c' : ('cat', 'cop'), 'o' : ('og',);
  • 遍历 S[1] = 'c' 后,heads = 'a' : ('at',), 'o' : ('og', 'op');
  • 遍历 S[2] = 'a' 后,heads = 'o' : ('og', 'op'), 't': ('t',) ;
  • 遍历 S[3] = 'o' 后,heads = 'g' : ('g',), 'p': ('p',), 't': ('t',);
  • 遍历 S[0] = 'g' 后,heads = 'p': ('p',), 't': ('t',)。

使用长度为 26 的数组 heads 做桶,每个字母对应一个桶。访问 S 中的每个字母时,将该字母对应桶中的所有单词,根据下一个等待匹配字母放入到不同的桶中。如果已经匹配到单词的最后一个字母,那么子序列单词数加 1。完整代码如下:

class Solution {
    public int numMatchingSubseq(String S, String[] words) {
        int ans = 0;
        ArrayList[] heads = new ArrayList[26];
        for (int i = 0; i < 26; ++i)
            heads[i] = new ArrayList();

        for (String word: words)
            heads[word.charAt(0) - 'a'].add(new Node(word, 0));

        for (char c: S.toCharArray()) {
            ArrayList old_bucket = heads[c - 'a'];
            heads[c - 'a'] = new ArrayList();

            for (Node node: old_bucket) {
                node.index++;
                if (node.index == node.word.length()) {
                    ans++;
                } else {
                    heads[node.word.charAt(node.index) - 'a'].add(node);
                }
            }
            old_bucket.clear();
        }
        return ans;
    }

}

class Node {
    String word;
    int index;
    public Node(String w, int i) {
        word = w;
        index = i;
    }
}

我反正是debug走了两遍才看懂这个写法。。只能说确实挺巧妙的。。而且我还觉得这种做法似曾相识,好像之前做过类似的题目。而且之前也是看题解才想到的。。算了,下一题了。

有效的“井”字游戏

题目:用字符串数组作为井字游戏的游戏板 board。当且仅当在井字游戏过程中,玩家有可能将字符放置成游戏板所显示的状态时,才返回 true。该游戏板是一个 3 x 3 数组,由字符 " ","X" 和 "O" 组成。字符 " " 代表一个空位。以下是井字游戏的规则:
玩家轮流将字符放入空位(" ")中。
第一个玩家总是放字符 “X”,且第二个玩家总是放字符 “O”。
“X” 和 “O” 只允许放置在空位中,不允许对已放有字符的位置进行填充。
当有 3 个相同(且非空)的字符填充任何行、列或对角线时,游戏结束。
当所有位置非空时,也算为游戏结束。
如果游戏结束,玩家不允许再放置字符。

示例 1:
输入: board = ["O ", " ", " "]
输出: false
解释: 第一个玩家总是放置“X”。
示例 2:
输入: board = ["XOX", " X ", " "]
输出: false
解释: 玩家应该是轮流放置的。
示例 3:
输入: board = ["XXX", " ", "OOO"]
输出: false
示例 4:
输入: board = ["XOX", "O O", "XOX"]
输出: true
说明:
游戏板 board 是长度为 3 的字符串数组,其中每个字符串 board[i] 的长度为 3。
board[i][j] 是集合 {" ", "X", "O"} 中的一个字符。

思路:这个题怎么说呢,我觉得有两点:1.场上的符号一定是XO一样多或者X比O多一个。2.当X仅有的出现了三个连一起(如示例3),那么一定O少一个。剩下没别的了吧,我去代码试试。
第一版本代码:

class Solution {
    public boolean validTicTacToe(String[] board) {
        char[][] d = new char[3][3];
        int x = 0;
        int o = 0;
        for(int i = 0;i<3;i++) {
            for(int j = 0;j<3;j++) {
                d[i][j] = board[i].charAt(j);
                if(d[i][j] == 'X') {
                    x++;
                }else if(d[i][j] == 'O'){
                    o++;
                }
            }
        }
        //一对一个的下。要么x多一个,要么一样多,不会出现第三种结果
        if(o>x || x>o+1) return false;
        //不足三个根本不会成型,所以一定可以。
        if(x<=3 && o<3) return true;
        //现在看来数目是对的,继续判断有没有赢了还继续下的
        boolean x1 = isOk(d, 'X');
        boolean o1 = isOk(d, 'O');
        if(x1 && o1) return false;//不可能两个人都赢
        if(x1) return x == o+1;//如果x要赢了,那么最后一步下的,所以x多一步
        if(o1) return x == o;//o要赢最后一步是o下的,所以数目要一样
        return true;
        
    }
    public boolean isOk(char[][] d,char c) {
        if(d[0][0] == c && d[0][1] ==c && d[0][2] == c) return true;
        if(d[1][0] == c && d[1][1] ==c && d[1][2] == c) return true;
        if(d[2][0] == c && d[2][1] ==c && d[2][2] == c) return true;
        if(d[0][0] == c && d[1][0] ==c && d[2][0] == c) return true;
        if(d[0][1] == c && d[1][1] ==c && d[2][1] == c) return true;
        if(d[0][2] == c && d[1][2] ==c && d[2][2] == c) return true;
        if(d[0][0] == c && d[1][1] ==c && d[2][2] == c) return true;
        if(d[0][2] == c && d[1][1] ==c && d[2][0] == c) return true;
        return false;
    }
}

还好只是3 * 3.这要是30 * 30还得累死我。。。这个isok我觉得直接写比for循环判断更直接。所以这个代码的性能超过百分百了,撒花~~
这个题目没啥难度,就是逻辑判断。把所有会报错的挑出来,剩下的就是正确的了。也没啥好说的,我去瞅一眼性能第一的代码:

class Solution {
    public boolean validTicTacToe(String[] board) {
        int xCount = 0, oCount = 0;
        for (String row: board)
            for (char c: row.toCharArray()) {
                if (c == 'X') xCount++;
                if (c == 'O') oCount++;
            }

        if (oCount != xCount && oCount != xCount - 1) return false;
        if (win(board, 'X') && oCount != xCount - 1) return false;
        if (win(board, 'O') && oCount != xCount) return false;
        return true;
    }

    public boolean win(String[] B, char P) {
        // B: board, P: player
        for (int i = 0; i < 3; ++i) {
            if (P == B[0].charAt(i) && P == B[1].charAt(i) && P == B[2].charAt(i))
                return true;
            if (P == B[i].charAt(0) && P == B[i].charAt(1) && P == B[i].charAt(2))
                return true;
        }
        if (P == B[0].charAt(0) && P == B[1].charAt(1) && P == B[2].charAt(2))
            return true;
        if (P == B[0].charAt(2) && P == B[1].charAt(1) && P == B[2].charAt(0))
            return true;
        return false;
    }
}

思路大同小异,就是写法上不太一样。这个题过了。

所有可能的路径

题目:给一个有 n 个结点的有向无环图,找到所有从 0 到 n-1 的路径并输出(不要求按顺序)二维数组的第 i 个数组中的单元都表示有向图中 i 号结点所能到达的下一些结点(译者注:有向图是有方向的,即规定了 a→b 你就不能从 b→a )空就是没有下一个结点了。

示例 1:
输入:graph = [[1,2],[3],[3],[]]
输出:[[0,1,3],[0,2,3]]
解释:有两条路径 0 -> 1 -> 3 和 0 -> 2 -> 3
示例 2:
输入:graph = [[4,3,1],[3,2,4],[3],[4],[]]
输出:[[0,4],[0,3,4],[0,1,3,4],[0,1,2,3,4],[0,1,4]]
示例 3:
输入:graph = [[1],[]]
输出:[[0,1]]
示例 4:
输入:graph = [[1,2,3],[2],[3],[]]
输出:[[0,1,2,3],[0,2,3],[0,3]]
示例 5:
输入:graph = [[1,3],[2],[3],[]]
输出:[[0,1,2,3],[0,3]]
提示:
结点的数量会在范围 [2, 15] 内。
你可以把路径以任意顺序输出,但在路径内的结点的顺序必须保证。


题目截图

思路:这个题我觉得思路还好吧,应该就是广搜或者深搜。而且注意这个题目上说到了是有向无环图。既然无环的话也不怕死循环了,直接就顺序往下遍历?大概思路有的,我去实现下试试。
第一版本代码:

class Solution {
    int n;
    List> ans;
    public List> allPathsSourceTarget(int[][] graph) {
        ans = new ArrayList>();
        n = graph.length-1;
        dfs(graph, 0,new ArrayList());
        return ans;
    }
    public void dfs(int[][] graph,int temp,List list){
        list.add(temp);
        if(temp == n) {
            ans.add(list);
            return;
        }
        for(int i : graph[temp]) {
            dfs(graph, i, new ArrayList(list));
        }
    }
}

果然这个题目比我想的还要简单。一开始看了题目我就想着要不要记忆化啥的。。但是后来仔细审了题目发现无环,所以就直接暴力遍历。代码性能还行,至于优化点不太清楚,我现在能想到的就是dfs变成显示栈调用?但是感觉不会性能差别很大啊,直接去看看性能第一的代码:

class Solution {
  public List> allPathsSourceTarget(int[][] graph) {
        List tem = new ArrayList<>();
        tem.add(0);
        dfs(graph,0,graph.length,tem);
        return ans;
    }

    public List> ans = new ArrayList<>();
    public void dfs(int [][] graph,int next ,int end ,List tem){

        //封装结果
        if(next == end-1){
            ans.add(new ArrayList<>(tem));
            return;
        }

        //next 时候可以选择得路线
        int graph_next[] = graph[next];

        for(int i=0;i

性能第一的代码用的是回溯,虽然我觉得本质上应该差不多,但是因为就只差了1ms的时间,所以可能这么写就是性能更好吧,反正这个题比较简单,直接过了,下一题。

不同的子序列

题目:给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)题目数据保证答案符合 32 位带符号整数范围。

示例 1:
输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下图所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
(上箭头符号 ^ 表示选取的字母)
rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^
示例 2:
输入:s = "babgbag", t = "bag"
输出:5
解释:
如下图所示, 有 5 种可以从 s 中得到 "bag" 的方案。
(上箭头符号 ^ 表示选取的字母)
babgbag
^^ ^
babgbag
^^ ^
babgbag
^ ^^
babgbag
^ ^^
babgbag
^^^
提示:
0 <= s.length, t.length <= 1000
s 和 t 由英文字母组成

思路:这个是21/3/17的每日一题,困难难度的,其实我现在对于困难难度的题目都抱着敬而远之的态度,毕竟自认为太菜。不过既然是现在每日一题做到了也要尽量啃啃。从头说这个题目,这个题目的标签有个动态规划,只要是dp肯定是有个dp公式的。暂时我的想法肯定是从左往右一次遍历,至于递推公式,我的想法是二维数组,长宽是s和t的长度。所以盲猜动态方程肯定是从角角逼近的方式填充的。
上面的思路连蒙带猜的对了,但是动态方程迟迟没有写出来,所以我决定还是直接看题解了。当然只看了下思路,然后自己去写的代码。
附上代码:

class Solution {
    public int numDistinct(String s, String t) {
        int lens = s.length();
        int lent = t.length();
        if(lens=0;i--) {
            char cs = s.charAt(i);
            for(int j = lent-1;j>=0;j--) {
                char ct = t.charAt(j);
                //不管当前元素能不能用上,之前已经能组成的可能都不变,所以都加下面那个数字
                if(cs == ct) {
                    //右下是当前元素的下一个。都可以被当前元素续上,所以加上右下
                    dp[i][j] = dp[i+1][j+1]+dp[i+1][j];
                }else {
                    dp[i][j] = dp[i+1][j];
                }
            }
        }
        return dp[0][0];
    }
}

其实上面的猜测很大一部分都对了的,比如说二维dp数组角落逼近。我差的就是这么个思路清晰的递推公式。。
附上当时我看的递推公式推到的表述:

  • 假设字符串 s和 t的长度分别为 m 和 n。如果 t 是 s 的子序列,则 s 的长度一定大于或等于 t 的长度,即只有当 m≥n 时,t 才可能是 s 的子序列。如果 m
  • 当 m≥n 时,可以通过动态规划的方法计算在 s 的子序列中 t 出现的个数。
  • 创建二维数组dp,其中 dp[i][j] 表示在 s[i:]的子序列中 t[j:] 出现的个数。
  • 上述表示中,s[i:] 表示 s从下标 i 到末尾的子字符串,t[j:] 表示 t从下标 j 到末尾的子字符串。
  • 考虑动态规划的边界情况:
    • 当 j=n时,t[j:] 为空字符串,由于空字符串是任何字符串的子序列,因此对任意dp[i][n]=1;
    • 当 i=m 且 j
    • 当 s[i]=t[j]时,dp[i][j] 由两部分组成:
      如果 s[i] 和 t[j] 匹配,则考虑 t[j+1:]作为 s[i+1:] 的子序列,子序列数为dp[i+1][j+1];
      如果 s[i] 不和 t[j] 匹配,则考虑 t[j:]作为 s[i+1:] 的子序列,子序列数为 dp[i+1][j]。
      因此当 s[i] = t[j]时,有dp[i][j] = dp[i+1][j+1] + dp[i+1][j]。

以上就是全部dp 的思路。也是上面代码的语言表述。由此得出这个题的答案。

本篇笔记就记到这里,如果稍微帮到你了记得点个喜欢点个关注,也祝大家工作顺顺利利吧~

你可能感兴趣的:(leetCode进阶算法题+解析(七十三))