【2023】三月做题总结

2023-3-1

1.【205】同构字符串(easy)

题目链接: 同构字符串

题目描述:
给定两个字符串st,判断它们是否是同构的。

如果s中的字符可以按某种映射关系替换得到t,那么这两个字符串是同构的。

每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。

涉及知识点: 哈希表

思路: 这道题可以使用两个map来保存s[i]t[j]t[j]s[i]的映射关系,在映射的过程中要判断一下能否对应上,如果发现对应不上,立刻返回false。例如:s = foo ,t = bar,在s->t中,f->b,o->a,那么当遍历到s.o,t.r时就会发现和o->a的映射关系对不上,返回false

代码:

class Solution {
    public boolean isIsomorphic(String s, String t) {
        // 使用两个map 保存 s[i] 到 t[j] 和 t[j] 到 s[i] 的映射关系
        // 如果发现对应不上,立刻返回 false
        Map<Character,Character> mapStoT= new HashMap<>();
        Map<Character,Character> mapTtoS= new HashMap<>();
        for(int i = 0,j = 0;i < s.length();i++,j++){
            if(!mapStoT.containsKey(s.charAt(i))){
                // s -> t 映射,s[i]做key,t[j]做value
                mapStoT.put(s.charAt(i),t.charAt(j));
            }
            if(!mapTtoS.containsKey(t.charAt(j))){
                // t -> s 映射,t[j]做key,s[i]做value
                mapTtoS.put(t.charAt(j),s.charAt(i));
            }
            // 如果相互不匹配
            if(mapStoT.get(s.charAt(i))!=t.charAt(j) || mapTtoS.get(t.charAt(j))!=s.charAt(i)){
                return false;
            }
        }
        return true;
    }
}

2.【1002】查找共用字符(easy)

题目链接: 查找共用字符

题目描述:
给你一个字符串数组words,请你找出所有在words的每个字符串中都出现的共用字符(包括重复字符),并以数组形式返回。你可以按任意顺序返回答案。

涉及知识点: 哈希表

思路: 这道题翻译一下就是在26个小写字母中如果有字母在所有字符串中都出现就输出,重复的也要输出,那么重点就是如何判断是否都出现过,以及出现的次数。这道的解法很巧妙地用到两个数组,第一个数组arr1先用第一个字符串初始化,记录下该字符串所有字符出现的次数,然后遍历其他的字符串,同样记下字符的出现次数记录在arr2,在遍历的过程中arr1arr2比较,找到两个数组中各字符对应的最小出现次数更新到arr1中,这样就能找到所有在全部字符串中都出现的字母,它们出现的次数也能知道了。

代码:

class Solution {
    public List<String> commonChars(String[] words) {
        // 小写字母组成,可以用数组
        int[] arrFirst = new int[26]; // 记录第一个字符串中各字母出现的次数
        // 统计第一个字符串字符的出现频率
        for(int i = 0; i < words[0].length(); i++){
            arrFirst[words[0].charAt(i)-'a'] ++;
        }
        // 统计除第一个字符串外字符的出现频率
        for (int i = 1; i < words.length; i++) {
            int[] arrOther = new int[26];
            for (int j = 0; j < words[i].length(); j++) {
                arrOther[words[i].charAt(j)- 'a']++;
            }
            // 更新arrFirst,保证arrFirst里统计26个字符在所有字符串里出现的最小次数
            for (int k = 0; k < 26; k++) {
                arrFirst[k] = Math.min(arrFirst[k], arrOther[k]);
            }
        }
        List<String> result = new ArrayList<>();
        // 将arrFirst统计的字符次数,转成输出形式
        for (int i = 0; i < 26; i++) {
            while (arrFirst[i] != 0) { // 注意这里是while,因为可能有多个重复的字符
                char c= (char) (i +'a');
                result.add(String.valueOf(c));
                arrFirst[i]--;
            }
        }
        return result;
    }
}

2023-3-2

3.【925】长按键入(easy)

题目链接: 长按键入

题目描述:
你的朋友正在使用键盘输入他的名字name。偶尔,在键入字符c时,按键可能会被长按,而字符可能被输入1次或多次。你将会检查键盘输入的字符typed。如果它对应的可能是你的朋友的名字(其中一些字符可能被长按),那么就返回True

涉及知识点: 字符串

思路: 本题其实可以把两个字符串当成数组,模拟数组的遍历中比较是否字符相同,但是要注意两个指针的移动条件,当两个字符串中对应的字符相等时都向后移一位,但如果不相等,则要看当前是不是第一位,如果是,那么直接false,如果不是,则移动type的指针越过重复项,然后再和name当前的字符比较,如果相同都后移一位,如果不同则返回false。这样比较结束后可能会有两种情况:name没比较完或是typed没比较完,前者则直接false,后者则要看看余下的字符是否都和比较的最后一位字符相同,不相同则返回false,这样经过重重关卡后才返回true

代码:

class Solution {
    public boolean isLongPressedName(String name, String typed) {
        // 双指针模拟两个数组遍历
        int i = 0;
        int j = 0;
        int m = name.length();
        int n = typed.length();
        while(i < m && j < n){
            if(name.charAt(i)==typed.charAt(j)){
                // 如果相同就集体后移
                i++;
                j++;
            }else{
                // 不相同的情况下
                if(j == 0){
                    // 如果第一个就不相同,直接返回false
                    return false;
                }else{
                    // 如果不是第一个则要跨越重复项
                    while(j < n-1 && typed.charAt(j) == typed.charAt(j-1)) j++;
                    // 跨越重复项后再比较
                    if(name.charAt(i)==typed.charAt(j)){
                        i++;
                        j++;
                    }else return false;
                }
            }
        }
        // 这样比较后可能是name或typed没比较完
        if(i < m) return false;
        while(j < n){
            if(typed.charAt(j)==typed.charAt(j-1)) j++;
            else return false;
        }
        return true;
    }
}

4.【844】比较含退格的字符串(easy)

题目链接: 比较含退格的字符串

题目描述:

给定st两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回true#代表退格字符。

注意:如果对空文本输入退格字符,文本继续为空。

涉及知识点: 字符串,栈

思路: 栈很适合用来解决这类匹配消除的问题,最直接的思路就是用栈解决,再巧妙一点是使用栈的思路,但不使用栈,
直接使用字符串string来作为栈,末尾添加和弹出,string都有相应的接口,最后比较的时候,只要比较两个字符串就可以了,比栈要方便一点。再优化就是从后到前使用双指针,空间复杂度为O(1) ,记录#的数量,模拟消除的操作,如果#用完了,就开始比较S[i]S[j]

代码:

 // 最基础的栈方法
class Solution {
    public boolean backspaceCompare(String s, String t) {
        Deque<Character> stack1 = new LinkedList<>();
        Deque<Character> stack2 = new LinkedList<>();
        // 分别入栈
        for(int i = 0; i < s.length(); i++){
            if(s.charAt(i) == '#'){
                if(!stack1.isEmpty()){
                    stack1.pop();
                }else{
                    continue;
                }
            }else{
                stack1.push(s.charAt(i));
            }
        }
        for(int i = 0; i < t.length(); i++){
            if(t.charAt(i) == '#'){
                if(!stack2.isEmpty()){
                    stack2.pop();
                }else{
                    continue;
                }
            }else{
                stack2.push(t.charAt(i));
            }
            
        }
        // 出栈比较
        if(stack1.size()!=stack2.size()){
            return false;
        }
        while(!stack1.isEmpty()&&!stack2.isEmpty()){
            if(stack1.peek()!=stack2.peek()){
                return false;
            }
            stack1.pop();
            stack2.pop();
        }
        return true;
    }
}

// 用string代替普通栈,只是用到了栈的思想
class Solution {
    public boolean backspaceCompare(String s, String t) { 
        StringBuilder ssb = new StringBuilder();
        StringBuilder tsb = new StringBuilder();
        for(char c : s.toCharArray()){
            if(c!='#'){
                ssb.append(c);
            }else{
                if(ssb.length()>0){
                    ssb.deleteCharAt(ssb.length()-1);
                }
            }
        }
        for(char c : t.toCharArray()){
            if(c!='#'){
                tsb.append(c);
            }else{
                if(tsb.length()>0){
                    tsb.deleteCharAt(tsb.length()-1);
                }
            }
        }
        // 比较两个新字符串是否相等
        return ssb.toString().equals(tsb.toString());
        
    }
}

2023-3-3

5.【129】求根节点到叶节点数字之和(medium)

题目链接: 求根节点到叶节点数字之和

题目描述:
给你一个二叉树的根节点root,树中每个节点都存放有一个09之间的数字。

每条从根节点到叶节点的路径都代表一个数字:例如,从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。

计算从根节点到叶节点生成的所有数字之和。叶节点是指没有子节点的节点。

涉及知识点: 二叉树、回溯

思路: 本题基本上也是二叉树的常规题,但是因为忘记二叉树的内容了所以不太记得做题步骤了。其实还是要按递归三部曲来做,有点收集路径的感觉在里面,所以这里面还要用到回溯。

代码:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    List<Integer> path = new ArrayList<>();
    int res = 0; //记录最后的结果
    public int sumNumbers(TreeNode root) {
        if(root==null) return 0;
        path.add(root.val);
        traversal(root);
        return res;
    }
    // 递归
    public void traversal(TreeNode node){
        if(node.left==null&&node.right==null){
            // 到达叶子节点时说明一个数收集好了
            res += list2Int(path);
            return;
        }
        if(node.left!=null){
            path.add(node.left.val);
            traversal(node.left);
            path.remove(path.size()-1); //回溯
        }
        if(node.right!=null){
            path.add(node.right.val);
            traversal(node.right);
            path.remove(path.size()-1); //回溯
        }
        return;
    }
    // 将list转为数字
    public int list2Int(List<Integer> path){
        int sum = 0;
        for(Integer num:path){
            sum = sum*10 + num;
        }
        return sum;
    }
}

6.【1382】将二叉搜索树变平衡(medium)

题目链接: 将二叉搜索树变平衡

题目描述:
给你一棵二叉搜索树,请你返回一棵平衡后的二叉搜索树,新生成的树应该与原来的树有着相同的节点值。如果有多种构造方法,请你返回任意一种。

如果一棵二叉搜索树中,每个节点的两棵子树高度差不超过1,我们就称这棵二叉搜索树是平衡的。

涉及知识点: 二叉树

思路: 本题分为两个步骤,第一步是使用中序遍历来将原本的二叉树转成一个有序数组,第二步是使用这个有序数组来构造新的二叉搜索树。

代码:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    // 先中序遍历把二叉树变为有序数组,然后再构造新二叉树
    ArrayList<Integer> list = new ArrayList<>();
    public TreeNode balanceBST(TreeNode root) {
        // 变有序数组
        travesal(root);
        // 根据数组构造二叉树
        return crateTree(list,0,list.size()-1);
    }
    // 二叉树变数组
    public void travesal(TreeNode node){
        if(node==null){
            return;
        }
        travesal(node.left);
        list.add(node.val);
        travesal(node.right);
        return;
    }
    // 数组构造二叉树
    public TreeNode crateTree(ArrayList<Integer> list,int left, int right){
        if (left > right) return null;
        int mid = left + (right-left)/2;
        TreeNode root = new TreeNode(list.get(mid));
        root.left = crateTree(list,left,mid-1);
        root.right = crateTree(list,mid+1,right);
        return root;
    }
}

7.【100】相同的树(easy)

题目链接: 相同的树

题目描述:
给你两棵二叉树的根节点pq,编写一个函数来检验这两棵树是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

涉及知识点: 二叉树

思路: 这道题的思路不难,只要比较两棵树的子节点是否都相同就可以了,就是终止条件的情况比较多一点。本题和对称二叉树有点异曲同工之意。

代码:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public boolean isSameTree(TreeNode p, TreeNode q) {
        return compare(p,q);
    }
    public boolean compare(TreeNode p,TreeNode q){
        if(p==null&&q==null){
            return true;
        }else if(p==null&&q!=null){
            return false;
        }else if(p!=null&&q==null){
            return false;
        }else if(p.val!=q.val){
            return false;
        }
        boolean left_boo = compare(p.left,q.left);
        boolean right_boo = compare(p.right,q.right);
        return left_boo&&right_boo;
    }
}

8.【116】填充每个节点的下一个右侧节点指针(medium)

题目链接: 填充每个节点的下一个右侧节点指针

题目描述:
给定一个完美二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
完美二叉树
填充它的每个next指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将next指针设置为 NULL。

初始状态下,所有next指针都被设置为NULL

涉及知识点: 二叉树

思路: 这道题用层序遍历的方法很好理解,如果用递归稍微有点绕,每一层的指向操作需要用到上一层的指向结果,如下图所示,看懂这个图就知道每一步的操作应该怎么写了。

操作一:
	if (cur->left) cur->left->next = cur->right;
操作二:
	if (cur->right) {
    	if (cur->next) cur->right->next = cur->next->left;
    	else cur->right->next = NULL;
	}

递归示意图

代码:

/*
// Definition for a Node.
class Node {
    public int val;
    public Node left;
    public Node right;
    public Node next;

    public Node() {}
    
    public Node(int _val) {
        val = _val;
    }

    public Node(int _val, Node _left, Node _right, Node _next) {
        val = _val;
        left = _left;
        right = _right;
        next = _next;
    }
};
*/
1. 层序遍历方法
class Solution {
    public Node connect(Node root) {
        Queue<Node> que = new LinkedList<Node>();
        if(root!=null){
            que.add(root);
        }

        while(!que.isEmpty()){
            int size = que.size();
            Node cur = que.poll();
            if(cur.left!=null){ //记录下每一层的第一个节点
                que.add(cur.left);
            } 
            if(cur.right!=null){
                que.add(cur.right);
            }
            while(size-->1){ // 将要指向的节点
                Node node = que.poll();
                if(node.left!=null){
                    que.add(node.left);
                } 
                if(node.right!=null){
                    que.add(node.right);
                }
                cur.next = node;
                cur = node;
            }
        }
        return root;
    }
}
2. 递归方法
class Solution {
    public Node connect(Node root) {
        travesal(root);
        return root; 
    }
    public void travesal(Node node){
        if(node==null){
            return;
        }
        if(node.left!=null){
            node.left.next = node.right;
        }
        if(node.right!=null){
            if(node.next!=null){
                node.right.next = node.next.left;
            }else{
                node.right.next = null;
            }
        }
        travesal(node.left);
        travesal(node.right);
        return;
    }
}

2023-3-4

9.【52】N 皇后 II(hard)

题目链接: N 皇后 II

题目描述:

N皇后问题研究的是如何将n个皇后放置在n × n的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数n,返回n皇后问题不同的解决方案的数量。

涉及知识点: 回溯

思路: 这道题和N皇后基本上一样的,代码都不用怎么改,区别在于N皇后还要输出具体方案,本题只要统计方案个数就行了,其实更简单一点。

代码:

class Solution {
    int res = 0;
    public int totalNQueens(int n) {
        // 回溯,和N皇后那题基本上一样
        char[][] chessboard = new char[n][n]; // 棋盘
        for (char[] c : chessboard) {
            Arrays.fill(c, '.');
        }
        backTracing(n, 0, chessboard);
        return res;

    }
    public void backTracing(int n, int row,char[][] chessboard){
        if(row==n){
            res++;
            return;
        }
        for(int col = 0;col<n;col++){
            if(isValid(chessboard,row,col,n)){
                chessboard[row][col]='Q';
                backTracing(n,row+1,chessboard);
                chessboard[row][col]='.';
            }
        }
    }
    public boolean isValid(char[][] chessboard,int row,int col,int n){
        for(int i = 0;i<row;i++){
            if(chessboard[i][col]=='Q'){
                return false;
            }
        }
        // 检查45度对角
        for(int i = row-1 ,j = col+1; i>=0&&j<n;i--,j++){
            if(chessboard[i][j]=='Q'){
                return false;
            }
        }
        // 检查135度对角
        for(int i = row-1,j=col-1;i>=0&&j>=0;i--,j--){
            if(chessboard[i][j]=='Q'){
                return false;
            }
        }
        return true;
    }
}

10.【649】Dota2 参议院(medium)

题目链接: Dota2 参议院

题目描述:
题目

涉及知识点: 贪心

思路: 这道题是真的绕,用到贪心的思路的话是要尽量消灭自己后面的对手,前面的对手已经使用过权利了,而后面的对手还能消灭自己的同伴。具体模拟过程见代码吧。

代码:

class Solution {
    public String predictPartyVictory(String senate) {
        // 贪心,消灭的策略是尽量消灭自己后面的对手
        Boolean R = true; // 表示R是否存活
        Boolean D = true; // 表示R是否存活
        int flag = 0; //小于0是D在R前面,反之是R在D前面
        byte[] senates =  senate.getBytes();
        while(R&&D){ //一旦有一个为false说明结果已经出来了
            R = false;
            D = false;
            for(int i = 0; i<senates.length;i++){
                if(senates[i]=='R'){
                    if(flag<0) {
                        senates[i] = 0; //R被消灭
                    } else {
                        R = true;
                    }
                    flag++;
                }
                if(senates[i]=='D'){
                    if(flag>0) {
                        senates[i] = 0; //D被消灭
                    } else{
                        D = true;
                    } 
                    flag--;
                }
            }
            
        }
        return R == true ? "Radiant" : "Dire";
    }
}

11.【1221】分割平衡字符串(easy)

题目链接: 分割平衡字符串

题目描述:

平衡字符串中,'L''R' 字符的数量是相同的。

给你一个平衡字符串s,请你将它分割成尽可能多的子字符串,并满足:每个子字符串都是平衡字符串。

返回可以通过分割得到的平衡字符串的最大数量

涉及知识点: 贪心

思路: 这道题很简单,想让输出子串的数量最大,那么子串要尽可能地短,因此可以比较LR出现的次数,一旦相等就进行分割。

代码:

class Solution {
    public int balancedStringSplit(String s) {
        // 找最大数量,那么子串长度要尽可能短
        int L = 0,R = 0;
        int res = 0;
        for(int i = 0;i<s.length();i++){
            if(s.charAt(i)=='L'){
                L++;
            }else{
                R++;
            }
            if(L==R){
                res++;
                L = 0;
                R = 0;
            }
        }
        return res;
    }
}

2023-3-5

12.【5】最长回文子串(mdium)

题目链接: 最长回文子串

题目描述:

给你一个字符串s,找到s中最长的回文子串。如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

涉及知识点: 动态规划

思路: 这道题和回文子串差不多,不过回文子串是统计数量,本题是要找最长的,所以要记录下最长子串的起始位置和长度,整体的解题方法就按回文子串的来。

代码:

class Solution {
    public String longestPalindrome(String s) {
        // 动态规划
        boolean[][] dp = new boolean[s.length()][s.length()];
        int start = 0;
        int maxLength = 0;
        // 从后向前,从左到右遍历
        for(int i = s.length()-1;i>=0;i--){
            for(int j = i;j<s.length();j++){
                if(s.charAt(i)==s.charAt(j)){
                    if(j-i<=1){
                        dp[i][j] = true;
                    }else{
                        if(dp[i+1][j-1]==true){
                            dp[i][j] = true;
                        }else{
                            dp[i][j] = false;
                        }    
                    }
                }
                if(dp[i][j]==true&&j-i+1>maxLength){
                        start = i;
                        maxLength = j-i+1;
                }
            }
        }
        String res = s.substring(start,start+maxLength);
        return res;
    }
}

13.【132】分割回文串 II(hard)

题目链接: 最长回文子串

题目描述:

给你一个字符串s,请你将s分割成一些子串,使每个子串都是回文。返回符合要求的最少分割次数

涉及知识点: 动态规划

思路: 这道题要分成两个部分,第一部分要判断[0,i]是否为回文子串,写法参考回文子串那道题,然后在知道了是否是回文子串的基础上再进行分割。

  • dp[i] :范围[0, i]的回文子串,最少分割次数是dp[i]
  • 递推公式:如果[0,i]是回文子串,那么不用分割,dp[i]=0;反之则要在[0,i]中找能使得[j+1,i]为回文子串的j,这样dp[i]就要在dp[j][0,j]区间的最少分割次数)基础上加一,因为要找最少次数,则dp[i]=min(dp[i],dp[j]+1)
  • dp数组初始化:dp[i] = i,即假设每个字符都分割。
  • 确定遍历顺序:j是在[0,i]之间,所以遍历i的for循环一定在外层,两层循环都从小到大。

代码:

class Solution {
    public int minCut(String s) {
        // dp[i]:范围是[0, i]的回文子串,最少分割次数是dp[i]
        // 首先先判断是不是回文
        boolean[][] isHuiwen = new boolean[s.length()][s.length()];
        // 从后向前,从左到右遍历
        for(int i = s.length()-1;i>=0;i--){
            for(int j = i;j<s.length();j++){
                if(s.charAt(i)==s.charAt(j)){
                    if(j-i<=1){
                        isHuiwen[i][j] = true;
                    }else{
                        isHuiwen[i][j] = isHuiwen[i+1][j-1];
                    }
                }
            }
        }
        // 然后再考虑分割
        int[] dp = new int[s.length()];
        // 初始化
        for(int i = 0;i<s.length();i++){
            dp[i] = i;
        }
        for(int i = 1;i<s.length();i++){
            if(isHuiwen[0][i]){
                dp[i] = 0; //[0,i]这个区间内是回文就不用分割
            }
            for(int j = 0;j<i;j++){
                if(isHuiwen[j+1][i]){ //[0, j]区间的最小切割数量是dp[j]
                    dp[i] = Math.min(dp[i],dp[j]+1);
                }
            }
        }
        return dp[s.length()-1];
    }
}

14.【673】最长递增子序列的个数(medium)

题目链接: 最长递增子序列的个数

题目描述:

给定一个未排序的整数数组nums,返回最长递增子序列的个数。注意这个数列必须是严格递增的。

涉及知识点: 动态规划

**思路:**这道题有点绕,只维护一个数组不够,这里要维护dpcount两个数组。

  • dp[i]i之前(包括i)最长递增子序列的长度。
  • count[i]:以nums[i]为结尾的字符串,最长递增子序列的个数。
  • 递推公式:
if (nums[i] > nums[j]) {
   if (dp[j] + 1 > dp[i]) {  //在[0, i-1]的范围内,找到了j使得dp[j] + 1 > dp[i],说明找到了一个更长的递增子序列。
       count[i] = count[j];
   } else if (dp[j] + 1 == dp[i]) { //在[0, i-1]的范围内,找到了j使得dp[j] + 1 == dp[i],说明找到了两个相同长度的递增子序列。
       count[i] += count[j];
   }
   dp[i] = max(dp[i], dp[j] + 1);
}
  • dp数组初始化:dp[i] = 1,count[i]=1
  • 确定遍历顺序:从前向后遍历

代码:

class Solution {
    public int findNumberOfLIS(int[] nums) {
        // 动态规划
        // 本题要维护两个数组
        int[] dp = new int[nums.length]; //0~i的最长递增序列的长度。
        int[] count = new int[nums.length]; // 以nums[i]结尾最长递增子序列的个数
        for(int i = 0; i < dp.length; i++) dp[i] = 1;
        for(int i = 0; i < count.length; i++) count[i] = 1;

        int maxLength = 0;
        for(int i = 0;i<nums.length;i++){
            for(int j = 0;j<i;j++){
                if(nums[i]>nums[j]){
                    if(dp[j]+1>dp[i]){
                        dp[i] = dp[j]+1;
                        count[i] = count[j];
                    }else if(dp[j]+1==dp[i]){
                        count[i] += count[j];
                    }    
                }    
            }
            maxLength = Math.max(dp[i],maxLength);            
        }

        int result = 0;
        for (int i = 0; i < nums.length; i++) {
            if (maxLength == dp[i]) result += count[i];
        }
        return result;

    }
}

2023-3-6

15.【841】钥匙和房间(mdium)

题目链接: 钥匙和房间

题目描述:

n个房间,房间按从0n - 1编号。最初,除0号房间外的其余所有房间都被锁住。你的目标是进入所有的房间。然而,你不能在没有获得钥匙的时候进入锁住的房间。

当你进入一个房间,你可能会在里面找到一套不同的钥匙,每把钥匙上都有对应的房间号,即表示钥匙可以打开的房间。你可以拿上所有钥匙去解锁其他房间。

给你一个数组rooms其中rooms[i]是你进入i号房间可以获得的钥匙集合。如果能进入所有房间返回true,否则返回 false

涉及知识点: 图论

思路: 这道题其实是一个有向图的问题,可以使用深度优先搜索或广度优先搜索,这里用的是深度优先搜索。深度优先搜索其实就是一个方向找到底,是要用到递归的,递归中的处理逻辑其实就是记录下遍历过的方向,这里可以用一个visited来记录,true表示到过,false表示没有到过,到达每个房间后先判断一下是否已经来过了,是则这一层递归直接return,不是则将这一间房的visitd状态置true,然后看当前房间的有哪些钥匙,带着钥匙继续前往下一个房间(即进行递归)。

代码:

class Solution {
    public boolean canVisitAllRooms(List<List<Integer>> rooms) {
        boolean[] visited = new boolean[rooms.size()]; //记录房间是否到过
        // 有向图,可用深度优先搜索
        dfs(0,rooms,visited);
        for(boolean flag:visited){
            if(flag!=true){
                return false;
            }
        }
        return true;
    }
    public void dfs(int key,List<List<Integer>> rooms,boolean[] visited){
        if(visited[key]){
            return;
        }
        visited[key] = true;
        for(int current_keys:rooms.get(key)){ //从当前房间得到接下来可以进的房间号
            dfs(current_keys,rooms,visited);
        }
    }
}

16.【127】单词接龙(hard)

题目链接: 单词接龙

题目描述:

字典wordList中从单词beginWordendWord转换序列是一个按下述规格形成的序列beginWord -> s1 -> s2 -> ... -> sk

  • 每一对相邻的单词只差一个字母。
  • 对于1 <= i <= k 时,每个 si 都在 wordList中。注意,beginWord不需要在wordList中。
  • sk == endWord

给你两个单词beginWordendWord和一个字典wordList,返回从beginWordendWord最短转换序列中的单词数目。如果不存在这样的转换序列,返回 0

涉及知识点: 图论

思路: 这道题是无向图,节点之间的是否连接还需要自己判断,因为是找无向图的最短路径,所以用广度优先搜索最为合适,广搜只要搜到了终点,那么一定是最短的路径。(因为广搜就是以起点中心向四周扩散的搜索。)广搜是用队列,每出一个队头元素,就要把和他相连的节点入队(没有遍历过的节点),这样来实现遍历,所以看起来像是四周扩散的感觉。本题中判断节点是否相连靠的是看单词是否只相差一位,因此会涉及到重新构造字符串的操作。

代码:

class Solution {
    // 广度优先搜索,向四周扩散
    public int ladderLength(String beginWord, String endWord, List<String> wordList) { 
        HashSet<String> wordSet = new HashSet<>(wordList);
        // 先判断endword是否在字典中
        if(wordSet.size()==0||!wordSet.contains(endWord)) return 0;
        Deque<String> deque = new LinkedList<>(); //bfs的队列
        Map<String,Integer> map = new HashMap<>(); // 用来存单词对应路径长度
        deque.offer(beginWord);
        map.put(beginWord,1);

        while(!deque.isEmpty()){
            String word = deque.poll(); // 取队列头元素
            int pathLength = map.get(word);
            for(int i = 0; i<word.length();i++){
                char[] chars = word.toCharArray();
                for (char k = 'a'; k <= 'z'; k++) { //从'a' 到 'z' 遍历替换来造新词
                    chars[i] = k; //替换第i个字符
                    String newWord = String.valueOf(chars); //得到新的字符串
                    // 看新词是否出现在wordSet中
                    if(newWord.equals(endWord)){
                        return pathLength + 1; //找到了结尾词,直接返回
                    }
                    if(wordSet.contains(newWord)&&!map.containsKey(newWord)){
                        // 如果字典中含新单词且之前没有遍历过
                        map.put(newWord,pathLength+1);
                        deque.offer(newWord);
                    }
                }
            }
        }
        return 0;
    } 
}

2023-3-7

【知识点】并查集

(1)可解决的问题

并查集主要就是解决一些集合类问题,比如看两个节点在不在一个集合,也可以将两个节点添加到一个集合中。

(2)并查集模板

int n; // 节点数量3 到 1000
int father[];

// 并查集初始化
void init() {
    for (int i = 0; i < n; ++i) {
        father[i] = i;
    }
}
// 并查集里寻根的过程,判断这个节点的祖先节点是哪个
int find(int u) {
    return u == father[u] ? u : father[u] = find(father[u]);
}
// 将v->u 这条边加入并查集,将两个节点连在同一个根节点上
void join(int u, int v) {
    u = find(u);
    v = find(v);
    if (u == v) return ;
    father[v] = u; //v的父亲的根指向u的根
}
// 判断 u 和 v是否找到同一个根
boolean same(int u, int v) {
    u = find(u);
    v = find(v);
    return u == v;
}

17.【684】冗余连接(medium)

题目链接: 冗余连接

题目描述:

树可以看成是一个连通且无环无向图。

给定往一棵n个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在1n中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为n的二维数组edgesedges[i] = [ai, bi]表示图中在aibi之间存在一条边。

请找出一条可以删去的边,删除后可使得剩余部分是一个有着n个节点的树。如果有多个答案,则返回数组edges中最后出现的边。

涉及知识点: 图论之并查集

思路: 这是一道并查集的基础问题,题目翻译一下想让我们去把一些符合条件的边加入到一个集合中(符合条件指的是加入集合后不会形成环),所以从前向后遍历每一条边,边的两个节点如果不在同一个集合,就加入集合(即:同一个根节点)。如果边的两个节点已经出现在同一个集合里,说明这条边的两个节点已经连在一起了,如果再加入这条边一定就出现环,所以不能加入。

代码:

class Solution {
    private int n;  // 节点数
    private int[] father;
    public int[] findRedundantConnection(int[][] edges) {
        // 并查集
        init();
        for(int i = 0;i<edges.length;i++){
            if(same(edges[i][0],edges[i][1])){
                // 说明边的两个节点已经在集合中了,就不再加入
                return edges[i];
            }else{
                join(edges[i][0],edges[i][1]);
            }
        }
        return null;
    }
    public void init(){
        n = 1005;
        father = new int[n];
        // 初始化,父亲全设为自己
         for (int i = 0; i < n; ++i) {
            father[i] = i;
        }
    }
    public int find(int u){
        if(father[u]==u){
            return u;
        }else{
            father[u] = find(father[u]);
            return father[u];
        }
    }
    public void join(int u ,int v){ //让u的父亲的根指向v的父亲
        u = find(u);
        v = find(v);
        if(u==v){
            return;
        }
        father[u] = v; 
    }
    public Boolean same(int u, int v) {
        u = find(u);
        v = find(v);
        return u == v;
    }
}

18.【685】冗余连接 II(hard)

题目链接: 冗余连接 II

题目描述:

在本问题中,有根树指满足以下条件的有向图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。

输入一个有向图,该图由一个有着n个节点(节点值不重复,从1n)的树及一条附加的有向边构成。附加的边包含在1n中的两个不同顶点间,这条附加的边不属于树中已存在的边。

结果图是一个以边组成的二维数组edges。 每个元素是一对[ui, vi],用以表示有向图中连接顶点ui和顶点vi的边,其中uivi的一个父节点。

返回一条能删除的边,使得剩下的图是有n个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。

涉及知识点: 图论之并查集

思路: 这道题还挺复杂,要根据节点的入度值来分情况讨论。

  • 入度为2时,可能有以下两种情况,因为该树除了根节点之外的每一个节点都有且只有一个父节点,所以一定是删除指向入度为2的节点的两条边其中的一条才可能构成树,于是就要判断删除了一条边后这个图是不是一个树,如果是那么这条边就是答案,同时根据题意要从后向前遍历。
    入度为2
  • 入度为1时是下面这种情况,这种情况下一定有环,那么删除成环的那条边就可以了,写法类似冗余连接。
    入度为1

代码:

 class Solution {
    private  int n = 1005;  // 如题:二维数组大小的在3到1000范围内
    private int[] father = new int[n];

    public int[] findRedundantDirectedConnection(int[][] edges) {
        int[] inDegree = new int[n];
        for(int i = 0; i < edges.length; i++)
        {
            // 入度
            inDegree[ edges[i][1] ] += 1;
        }

        // 倒序找入度为2的节点所对应的边
        ArrayList<Integer> twoDegree = new ArrayList<Integer>();
        for(int i = edges.length - 1; i >= 0; i--)
        {
            if(inDegree[edges[i][1]] == 2) {
                twoDegree.add(i);
            }
        }

        // 如果有入度为2的节点,那么一定是两条边里删一个,看删哪个可以构成树
        if(!twoDegree.isEmpty())
        {
            if(isTreeAfterRemoveEdge(edges, twoDegree.get(0))) {
                return edges[ twoDegree.get(0)];
            }
            return edges[ twoDegree.get(1)];
        }else{
            // 明确没有入度为2的情况,那么一定有有向环,找到构成环的边返回就可以了
        return getRemoveEdge(edges);
        }
        
    }

    //初始化
    private void initFather() {
        for (int i = 0; i < n; ++i) {
            father[i] = i;
        }
    }

    // 寻根
    private int find(int u) {
        if(u == father[u]) {
            return u;
        }
        father[u] = find(father[u]);
        return father[u];
    }

    // 将v->u 这条边加入并查集
    private void join(int u, int v) {
        u = find(u);
        v = find(v);
        if (u == v) return ;
        father[v] = u;
    }

    // 判断 u 和 v是否找到同一个根
    private Boolean same(int u, int v) {
        u = find(u);
        v = find(v);
        return u == v;
    }

    /**
     * 在有向图里找到删除的那条边,使其变成树
     * @param edges
     * @return 要删除的边
     */
    private int[] getRemoveEdge(int[][] edges) {
        initFather();
        for(int i = 0; i < edges.length; i++) {
            if(same(edges[i][0], edges[i][1])) { // 构成有向环了,就是要删除的边
                return edges[i];
            }
            join(edges[i][0], edges[i][1]);
        }
        return null;
    }

    /**
     * 删一条边之后判断是不是树
     * @param edges
     * @param deleteEdge 要删除的边
     * @return  true: 是树, false: 不是树
     */
    private Boolean isTreeAfterRemoveEdge(int[][] edges, int deleteEdge)
    {
        initFather();
        for(int i = 0; i < edges.length; i++)
        {
            if(i == deleteEdge) continue;
            if(same(edges[i][0], edges[i][1])) { // 构成有向环了,一定不是树
                return false;
            }
            join(edges[i][0], edges[i][1]);
        }
        return true;
    }
}

2023-3-8

19.【657】 机器人能否返回原点(easy)

题目链接: 机器人能否返回原点

题目描述:

在二维平面上,有一个机器人从原点(0, 0)开始。给出它的移动顺序,判断这个机器人在完成移动后是否在(0, 0)处结束。

移动顺序由字符串moves表示。字符move[i]表示其第i次移动。机器人的有效动作有R(右),L(左),U(上)和 D(下)。

如果机器人在完成所有动作后返回原点,则返回true。否则,返回false

注意:机器人“面朝”的方向无关紧要。 “R”将始终使机器人向右移动一次,“L”将始终向左移动等。此外,假设每次移动机器人的移动幅度相同。

涉及知识点: 模拟

思路: 这道题还是很简单的,只要分别看U和DL和R出现的次数是否相等就可以了。

代码:

class Solution {
    public boolean judgeCircle(String moves) {
        int[] count = new int[4]; //用数组存还是有点繁琐了,可以直接用两个数表示x和y方向上的移动
        for(int i = 0;i<moves.length();i++){
            if(moves.charAt(i)=='R'){
                count[0]++;
            }else if(moves.charAt(i)=='L'){
                count[1]++;
            }else if(moves.charAt(i)=='U'){
                count[2]++;
            }else{
                count[3]++;
            }
        }
        return count[0]==count[1]&&count[2]==count[3];
    }
}

20.【31】 下一个排列(medium)

题目链接: 下一个排列

题目描述:
下一个排列

涉及知识点: 模拟

**思路:**这道题最大的难点就是理解这个字典序排列然后找规律,流程图如下:
流程图

代码:

class Solution {
    public void nextPermutation(int[] nums) {
        for(int i = nums.length-1; i >= 0; i--){
            for(int j = nums.length-1; j > i; j--){
                if(nums[j]>nums[i]){
                    // 先交换
                    int tmp = nums[i];
                    nums[i] = nums[j];
                    nums[j] = tmp;
                    // 然后将i+1至末尾的数字从小到大排序
                    Arrays.sort(nums, i + 1, nums.length);
                    return;
                }
            }
        }
        // 如果没有更大排列,则找字典序最小的排列
        Arrays.sort(nums,0,nums.length);
        return;
    }
}

21.【463】岛屿的周长(easy)

题目链接: 岛屿的周长

题目描述:
岛屿
岛屿示例

涉及知识点: 模拟

思路: 这道题还是挺简单的,也是要找下规律,模拟一下可以发现总周长 = 岛屿数*4 - 相交的岛屿数*2,这样就很好解了

代码:

class Solution {
    public int islandPerimeter(int[][] grid) {
        // 遇到相连接的边减一下就行了
        int len = 0;
        for(int i = 0; i < grid.length; i++){
            for(int j = 0; j < grid[i].length; j++){
                if(grid[i][j]==1){
                    len += 4;
                    if(i-1>=0&&grid[i-1][j]==1){
                        len -=2;
                    }
                    if(j-1>=0&&grid[i][j-1]==1){
                        len -=2;
                    }
                }
            }
        }
        return len;
    }
}

22.【1356】根据数字二进制下1的数目排序(easy)

题目链接: 根据数字二进制下1的数目排序

题目描述:

给你一个整数数组arr。请你将数组中的元素按照其二进制表示中数字1的数目升序排序。如果存在多个数字二进制中1的数目相同,则必须将它们按照数值大小升序排列。请你返回排序后的数组。

涉及知识点: 模拟

思路: 这道题有两个重点,第一个是如何统计二进制表示中1的数量,最容易想到的就是用位运算遍历二进制的每一位来统计,如方法一所示,更高效一点的方式是只循环n的二进制中1的个数次,如方法二所示。第二个重点是要自己定义一个比较器来根据题中逻辑比较,这里有点忘记了,回头要再看看。

// 方法一
int bitCount(int n) {
    int count = 0; // 计数器
    while (n > 0) {
        if((n & 1) == 1)  count++;  // 当前位是1,count++
        n >>= 1 ; // n向右移位
    }
    return count;
}

// 方法二
int bitCount(int n) {
    int count = 0;
    while (n) {
        n &= (n - 1); // 清除最低位的1
        count++;
    }
    return count;
}

代码:

class Solution {
    public int[] sortByBits(int[] arr) {
        return Arrays.stream(arr).boxed()
            .sorted(new Comparator<Integer>(){
                @Override
                public int compare(Integer o1, Integer o2) {
                    int cnt1 = countOne(o1);
                    int cnt2 = countOne(o2);
                    return (cnt1 == cnt2) ? Integer.compare(o1, o2) : Integer.compare(cnt1, cnt2);
                }
            })
            .mapToInt(Integer::intValue)
            .toArray();
    }
    public int countOne(int num){
        int count = 0;
        while(num>0){
            num &= (num-1);
            count++;
        }
        return count;     
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值