2023-3-1
1.【205】同构字符串(easy)
题目链接: 同构字符串
题目描述:
给定两个字符串s
和t
,判断它们是否是同构的。
如果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
,在遍历的过程中arr1
与arr2
比较,找到两个数组中各字符对应的最小出现次数更新到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)
题目链接: 比较含退格的字符串
题目描述:
给定s
和t
两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回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
,树中每个节点都存放有一个0
到9
之间的数字。
每条从根节点到叶节点的路径都代表一个数字:例如,从根节点到叶节点的路径 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)
题目链接: 相同的树
题目描述:
给你两棵二叉树的根节点p
和q
,编写一个函数来检验这两棵树是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
涉及知识点: 二叉树
思路: 这道题的思路不难,只要比较两棵树的子节点是否都相同就可以了,就是终止条件的情况比较多一点。本题和对称二叉树有点异曲同工之意。
代码:
/**
* 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
,请你将它分割成尽可能多的子字符串,并满足:每个子字符串都是平衡字符串。
返回可以通过分割得到的平衡字符串的最大数量。
涉及知识点: 贪心
思路: 这道题很简单,想让输出子串的数量最大,那么子串要尽可能地短,因此可以比较L
和R
出现的次数,一旦相等就进行分割。
代码:
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
,返回最长递增子序列的个数。注意这个数列必须是严格递增的。
涉及知识点: 动态规划
**思路:**这道题有点绕,只维护一个数组不够,这里要维护dp
和count
两个数组。
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
个房间,房间按从0
到n - 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
中从单词beginWord
和endWord
的转换序列是一个按下述规格形成的序列beginWord -> s1 -> s2 -> ... -> sk
:
- 每一对相邻的单词只差一个字母。
- 对于
1 <= i <= k
时,每个si
都在wordList
中。注意,beginWord
不需要在wordList
中。 sk == endWord
给你两个单词beginWord
和endWord
和一个字典wordList
,返回从beginWord
到endWord
的最短转换序列中的单词数目。如果不存在这样的转换序列,返回 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
) 的树中添加一条边后的图。添加的边的两个顶点包含在1
到n
中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为n
的二维数组edges
,edges[i] = [ai, bi]
表示图中在ai
和bi
之间存在一条边。
请找出一条可以删去的边,删除后可使得剩余部分是一个有着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
个节点(节点值不重复,从1
到n
)的树及一条附加的有向边构成。附加的边包含在1
到n
中的两个不同顶点间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组edges
。 每个元素是一对[ui, vi]
,用以表示有向图中连接顶点ui
和顶点vi
的边,其中ui
是vi
的一个父节点。
返回一条能删除的边,使得剩下的图是有n
个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。
涉及知识点: 图论之并查集
思路: 这道题还挺复杂,要根据节点的入度值来分情况讨论。
- 入度为2时,可能有以下两种情况,因为该树除了根节点之外的每一个节点都有且只有一个父节点,所以一定是删除指向入度为2的节点的两条边其中的一条才可能构成树,于是就要判断删除了一条边后这个图是不是一个树,如果是那么这条边就是答案,同时根据题意要从后向前遍历。
- 入度为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和D
及L和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;
}
}