系列汇总:《刷题系列汇总》
文章目录
——————《剑指offeer》———————
1. 把二叉树打印成多行
- 题目描述:从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。
- 优秀思路:利用队列“先进先出”的特点来保存每层节点,并动态的把本层出队,下层入队,
import java.util.*; public class Solution { ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) { ArrayList<ArrayList<Integer>> res = new ArrayList<>(); //注意这种情况后面不能加泛型 if(pRoot == null) return res; ArrayList<Integer> tempRes; Queue<TreeNode> q = new LinkedList<TreeNode>(); q.add(pRoot); // 核心部分 while(!q.isEmpty()){ tempRes = new ArrayList<Integer>(); int size = q.size(); // size即为本层节点的数量 for(int i = 0;i<size;i++){ // 注意不能写成 q.size(),否则是动态值,则不能代表每一层的数量了 TreeNode cur = q.poll(); //将上一层的节点逐个出队 tempRes.add(cur.val); if(cur.left != null) q.add(cur.left); if(cur.right != null) q.add(cur.right); } res.add(tempRes); } return res; } }
——————《LeectCode》———————
1. 岛屿数量
- 题目描述:给你一个由
'1'
(陆地)和'0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。 - 优秀思路:为了求出岛屿的数量,我们可以扫描整个二维网格。如果一个位置为 1,则将其加入队列,开始进行广度优先搜索。在广度优先搜索的过程中,每个搜索到的 1 都会被重新标记为 0。直到队列为空,搜索结束。
class Solution {
public int numIslands(char[][] grid) {
if(grid==null || grid.length == 0 || grid[0].length==0) return 0;
int nIsland = 0;
int row = grid.length, col = grid[0].length;
int cur = 0, curRow = 0, curCol = 0; // 当前位置
for(int i = 0;i<row;i++){
for(int j = 0;j<col;j++){
if(grid[i][j]=='1'){
nIsland++;
grid[i][j] ='0';
Queue<Integer> q = new LinkedList<Integer>();
q.add(i*col+j); // i*col+j 表示元素的序号
while(!q.isEmpty()){
cur = q.poll(); // 将搜索起点出队
curRow = cur / col;
curCol = cur % col;
// 上
if(curRow - 1 >= 0 && grid[curRow-1][curCol] == '1'){
grid[curRow-1][curCol] = '0';
q.add((curRow-1)*col + curCol);
}
// 下
if(curRow + 1<= row - 1 && grid[curRow+1][curCol] == '1'){
grid[curRow+1][curCol] = '0';
q.add((curRow+1)*col + curCol);
}
// 左
if(curCol - 1 >= 0 && grid[curRow][curCol-1] == '1'){
grid[curRow][curCol-1] = '0';
q.add(curRow*col + curCol-1);
}
// 右
if(curCol + 1 <= col - 1 && grid[curRow][curCol+1] == '1'){
grid[curRow][curCol+1] = '0';
q.add(curRow*col + curCol+1);
}
}
}
}
}
return nIsland;
}
}
2. 完全平方数
- 题目描述:给定正整数
n
,找到若干个完全平方数(比如1, 4, 9, 16, ...
)使得它们的和等于n
。你需要让组成和的完全平方数的个数最少。给你一个整数n
,返回和为n
的完全平方数的 最少数量 。完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9
和16
都是完全平方数,而3
和11
不是。 - 优秀思路(我编写的):贪心BFS
- 建立一个队列q存储每一层的所有余数
- 下一层逐个抛出上一层的余数remain,并计算其与所有小于它的完全平方数之差,并将其存入下一层。【注意:从最接近当前余数的最小平方数从大到小搜索,速度更快】
- 直至新的余数 = 0,则停止搜索。
class Solution {
public int numSquares(int n) {
if(n<=0) return 0;
Queue<Integer> q = new LinkedList<>(); // 存储每一层余数
q.add(n);
int numPS = 0; // 最小完全平方数数量
// 贪心bfs
while(!q.isEmpty()){
int size = q.size(); // 本层余数的个数
numPS++; // 每层 numPS 加 1
for(int i = 0;i < size;i++){ // 遍历本层所有余数
int remain = q.poll();
for(int j = (int)Math.sqrt(remain);j >= 1;j--){ // 从最接近当前余数的最小平方数开始搜索
if(remain - j*j == 0) return numPS; // 若余数减小为0,则停止搜索
else q.add(remain - j*j);
}
}
}
return numPS;
}
}
3. 删除无效的括号
- 题目描述:给你一个由若干括号和字母组成的字符串
s
,删除最小数量的无效括号,使得输入的字符串有效。返回所有可能的结果。答案可以按 任意顺序 返回。 - 优秀思路:逐个删除括号,判断删除后是否合法即可
class Solution {
public List<String> removeInvalidParentheses(String s) {
// 当前层的
Set<String> level = new HashSet<>();
level.add(s);
while (true) {
// 过滤不合法的:写法1——速度更快
List<String> valid = new ArrayList<String>();
for(String tempS:level) if(isValid(tempS)) valid.add(tempS);
// 过滤不合法的:写法2
// List<String> valid = level.stream().filter(this::isValid).collect(Collectors.toList());
if (valid.size() > 0) return valid;
// 下一层
Set<String> nextLevel = new HashSet<>(); // 可防止重复
for (String item : level) {
// 每次移除一个括号
for (int i = 0; i < item.length(); i++) {
if (item.charAt(i) == '(' || item.charAt(i) == ')') {
nextLevel.add(item.substring(0, i) + item.substring(i + 1));
}
}
}
level = nextLevel;
// 全部括号都被移除依然不符合,跳出循环
if (level.size() == 0) return new ArrayList<>();
}
}
// 计数法判断括号是否合法
public boolean isValid(String s) {
int cnt = 0;
for (char c : s.toCharArray()) {
if (c == '(') cnt++;
else if (c == ')') cnt--;
if (cnt < 0) return false;
}
// 最后左右括号不相等,也不合法
return cnt == 0;
}
}
4.二叉树的右视图
-
题目描述:给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
-
优秀思路(我的):建立队列存储每一层的节点,一直更新最右边的点
class Solution { public List<Integer> rightSideView(TreeNode root) { List<Integer> rightView = new ArrayList<>(); if(root == null) return rightView; // 输出每层最右边的节点 Queue<TreeNode> q = new LinkedList<>(); q.add(root); rightView.add(root.val); while(!q.isEmpty()){ int size = q.size(); int rightValue = -1; for(int i = 0;i < size;i++){ TreeNode temp = q.poll(); if(temp.left != null){ q.add(temp.left); rightValue = temp.left.val; // 更新最右边的点 } if(temp.right != null){ q.add(temp.right); rightValue = temp.right.val; // 更新最右边的点 } } if(rightValue != -1) rightView.add(rightValue); } return rightView; } }
-
可改进的点:不用一直更新右边的值,当遍历到本层最右边的值时,加入其值即可
class Solution { public List<Integer> rightSideView(TreeNode root) { List<Integer> res = new ArrayList<>(); if(root == null) return res; // 输出每层最右边的节点 Queue<TreeNode> q = new LinkedList<>(); q.add(root); while(!q.isEmpty()){ int size = q.size(); for(int i = 0;i < size;i++){ TreeNode temp = q.poll(); if(temp.left != null) q.add(temp.left); if(temp.right != null) q.add(temp.right); if(i == size - 1) res.add(temp.val); // 遍历到本层最右边的值时,加入其值即可 } } return res; } }
5. 对称二叉树
- 题目描述:给定一个二叉树,检查它是否是镜像对称的。
-
- 我的思路(6% - 8%):list保存每层的节点值,判断当前list是否对称
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
Queue<TreeNode> q = new LinkedList<>();
List<Integer> list;
// Stack<Integer> Stack = new Stack<Integer>(); // 用栈实现
q.add(root);
while(!q.isEmpty()){
int size = q.size();
list = new ArrayList<>();
for(int i = 0;i<size;i++){
TreeNode temp = q.poll();
if(temp.left != null){
q.add(temp.left);
list.add(temp.left.val);
}else{
list.add(-100); // null用-100代替
}
if(temp.right != null){
q.add(temp.right);
list.add(temp.right.val);
}else{
list.add(-100);
}
}
if(!isSymmetricHelper(list)) return false;
}
return true;
}
// 判断序列是否对称
private boolean isSymmetricHelper(List<Integer> list){
int i = 0;
int j = list.size() - 1;
while(i <= j){
if(list.get(i) != list.get(j)) return false;
else{
i++;
j--;
}
}
return true;
}
}
- 优秀思路1:【递归】因镜像对称,定义双指针p、q从根节点开始跑,p向左时q向右,p向右时q向左,两指针对应节点值必然相等,若不等则说明不对称
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
return check(root, root);
}
private boolean check(TreeNode p, TreeNode q){
if(p == null && q == null) return true;
else if((p == null || q == null) || p.val != q.val) return false;
else return check(p.left,q.right) && check(p.right,q.left);
}
}
- 优秀思路2:【迭代】因镜像对称,定义双指针p、q从根节点开始跑,p向左时q向右,p向右时q向左,两指针对应节点值必然相等,若不等则说明不对称
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
return check(root, root);
}
private boolean check(TreeNode p, TreeNode q){
Queue<TreeNode> queue = new LinkedList<>();
queue.add(p);
queue.add(q);
while(!queue.isEmpty()){
p = queue.poll();
q = queue.poll();
if(p == null && q == null) continue; // 注意:局部相同不能说明相同
else if((p == null || q == null) || p.val != q.val) return false;
else{
queue.add(p.left);
queue.add(q.right);
queue.add(p.right);
queue.add(q.left);
}
}
return true;
}
}
6. 单词接龙(困难,需重写)
- 题目描述:字典
wordList
中从单词beginWord
和endWord
的 转换序列 是一个按下述规格形成的序列,给你两个单词beginWord
和endWord
和一个字典wordList
,找到从beginWord
到endWord
的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回0
。- 序列中第一个单词是
beginWord
。 - 序列中最后一个单词是
endWord
。 - 每次转换只能改变一个字母。
- 转换过程中的中间单词必须是字典
wordList
中的单词。
- 序列中第一个单词是
- 一般思路:传统BFS
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
Queue<String> queue = new LinkedList<String>();
queue.add(beginWord);
boolean[] vis = new boolean[wordList.size()+1];
int layer = 1;
while(!queue.isEmpty()) {
layer++;
int size = queue.size();
while(size-->0) { // 遍历所有层
String cur = queue.poll(); // 取出当前遍历的单词
for (int i = 0; i < wordList.size(); i++) {
if(!vis[i]){
String next = wordList.get(i);
if(canChange(next,cur)) {
if(next.equals(endWord)) return layer;
queue.add(next);
vis[i] = true;
}
}
}
}
}
return 0;
}
//判断两单词是否可以相互转换
public boolean canChange(String s,String t) {
int unSame = 0;
for (int i = 0; i < s.length(); i++) {
if(s.charAt(i) != t.charAt(i)) unSame++;
if(unSame > 1) return false;
}
return true;
}
}
- 优秀思路:双向BFS,定义双队列分别从
beginWord
和endWord
开始搜索,遍历每一层时总是从层单词数较少的那段开始搜索。(小技巧就是:每次比较两队列的size,总是让queue1存储size较少的队列,就可以实现总是对queue1进行操作)
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
if(!wordList.contains(endWord)) return 0;
int end = wordList.indexOf(endWord);
wordList.add(beginWord);
int start = wordList.size() - 1;
Queue<Integer> queue1 = new LinkedList<>();
Queue<Integer> queue2 = new LinkedList<>();
Set<Integer> visited1 = new HashSet<>();
Set<Integer> visited2 = new HashSet<>();
queue1.offer(start);
queue2.offer(end);
visited1.add(start);
visited2.add(end);
int count = 1;
while (!queue1.isEmpty() && !queue2.isEmpty()) {
count++;
// 若queue1的当前层size比queue2大,交换的时候queue2也同时保存了queue1上次遍历的结果
if (queue1.size() > queue2.size()) {
Queue<Integer> tmp = queue1;
queue1 = queue2;
queue2 = tmp;
Set<Integer> t = visited1;
visited1 = visited2;
visited2 = t;
}
// 从size较小的一头开始搜索
int size1 = queue1.size();
while (size1-- > 0) {
String s = wordList.get(queue1.poll());
for (int i = 0; i < wordList.size(); ++i) {
if (visited1.contains(i) || !canConvert(s, wordList.get(i))) continue; // 遍历过了 或 不能相互转换
// 核心
if (visited2.contains(i)) return count;// 另一端也遍历到了这里
visited1.add(i);
queue1.offer(i);
}
}
}
return 0;
}
// 判断两单词是否可以相互转换
public boolean canConvert(String a, String b) {
int count = 0;
for (int i = 0; i < a.length(); ++i) {
if (a.charAt(i) != b.charAt(i)) {
if (++count > 1) return false;
}
}
return count == 1;
}
}
- 优秀思路改进:优化判断单词是否可以转换的过程,因为单词是由 a~z 这有限数量的字符组成的,可以遍历当前单词能转换成的所有单词,判断其是否包含在候选单词中。候选单词用
HashSet
保存,可以大大提高判断包含关系的性能。
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
if(!wordList.contains(endWord)) return 0;
wordList.add(beginWord);
// 从两端 BFS 遍历要用的队列
Queue<String> queue1 = new LinkedList<>();
Queue<String> queue2 = new LinkedList<>();
// 两端已经遍历过的节点
Set<String> visited1 = new HashSet<>();
Set<String> visited2 = new HashSet<>();
queue1.offer(beginWord);
queue2.offer(endWord);
visited1.add(beginWord);
visited2.add(endWord);
int count = 0;
// 将所有候选单词存在HashSet里,方便查找
Set<String> allWordSet = new HashSet<>(wordList);
while (!queue1.isEmpty() && !queue2.isEmpty()) {
count++;
// 从较小一端开始搜索
if (queue1.size() > queue2.size()) {
Queue<String> tmp = queue1;
queue1 = queue2;
queue2 = tmp;
Set<String> t = visited1;
visited1 = visited2;
visited2 = t;
}
// 遍历该层
int size1 = queue1.size();
while (size1-- > 0) {
String s = queue1.poll();
char[] chars = s.toCharArray();
for (int j = 0; j < s.length(); ++j) {
// 保存第j位的原始字符
char c0 = chars[j];
for (char c = 'a'; c <= 'z'; ++c) {
chars[j] = c;
// 修改后的单词,判断其是否包含于候选单词set
String newString = new String(chars);
// 已经访问过了,跳过
if (visited1.contains(newString)) {
continue;
}
// 两端遍历相遇,结束遍历,返回 count
if (visited2.contains(newString)) {
return count + 1;
}
// 如果单词在列表中存在,将其添加到队列,并标记为已访问
if (allWordSet.contains(newString)) {
queue1.offer(newString);
visited1.add(newString);
}
}
// 恢复第j位的原始字符
chars[j] = c0;
}
}
}
return 0;
}
}
7. 二叉树的层序遍历
-
题目描述:给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
-
优秀思路(我的):BFS
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if(root == null) return res;
List<Integer> tempList;
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while(!q.isEmpty()){
int size = q.size();
tempList = new ArrayList<Integer>();
while(size-- > 0){
TreeNode cur = q.poll();
tempList.add(cur.val);
if(cur.left != null) q.offer(cur.left);
if(cur.right != null) q.offer(cur.right);
}
res.add(tempList);
}
return res;
}
}
8. 接雨水 II(平面接雨水的升级版,需重写)
- 题目描述:给你一个
m x n
的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。 - 优秀思路:
class Solution {
public int trapRainWater(int[][] heights) {
if (heights == null || heights.length < 3 || heights[0].length < 3) return 0;
int n = heights.length;
int m = heights[0].length;
// 用一个vis数组来标记这个位置有没有被访问过
boolean[][] vis = new boolean[n][m];
// 优先队列中存放三元组 [x,y,h] 坐标和高度
PriorityQueue<int[]> pq = new PriorityQueue<>((o1, o2) -> o1[2] - o2[2]);// 意思是比较两元素的索引2处的值,0 1 2分别存储的 i j height
// 先把最外一圈放进去
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (i == 0 || i == n - 1 || j == 0 || j == m - 1) {
pq.offer(new int[]{i, j, heights[i][j]});
vis[i][j] = true;
}
}
}
int res = 0;
// 方向数组,把dx和dy压缩成一维来做
int[] dirs = {-1, 0, 1, 0, -1};
while (!pq.isEmpty()) {
int[] poll = pq.poll();
// 看一下周围四个方向,没访问过的话能不能往里灌水
for (int k = 0; k < 4; k++) {
int nx = poll[0] + dirs[k];
int ny = poll[1] + dirs[k + 1];
// 如果位置合法且没访问过
if (nx >= 0 && nx < n && ny >= 0 && ny < m && !vis[nx][ny]) {
// 如果外围这一圈中最小的比当前这个还高,那就说明能往里面灌水啊
if (poll[2] > heights[nx][ny]) {
res += poll[2] - heights[nx][ny];
}
// 如果灌水,高度得是你灌水后的高度了,如果没灌水也要取高的
pq.offer(new int[]{nx, ny, Math.max(heights[nx][ny], poll[2])});
vis[nx][ny] = true;
}
}
}
return res;
}
}
9.克隆图
- 题目描述:给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。图中的每个节点都包含它的值
val(int)
和其邻居的列表(list[Node]
)。 - 优秀思路1:【递归】深度优先搜索
class Solution {
private HashMap<Node,Node> mapClone = new HashMap<>();
public Node cloneGraph(Node node) {
if(node == null) return node;
if(mapClone.containsKey(node)) return mapClone.get(node); // 已拷贝
else{
Node nodeClone = new Node(node.val,new ArrayList<Node>());// 拷贝节点的val
mapClone.put(node,nodeClone);// 更新已拷贝map
for(Node nei:node.neighbors){ // 拷贝节点的neighbors
nodeClone.neighbors.add(cloneGraph(nei)); //核心!!!拷贝节点的nei = 拷贝的node的nei
}
return nodeClone;
}
}
}
- 优秀思路2:广度优先搜索
class Solution {
public Node cloneGraph(Node node) {
if (node == null) return node;
HashMap<Node,Node> mapVis = new HashMap<>();
Node newNode = new Node(node.val,new ArrayList<Node>());
mapVis.put(node,newNode);
Queue<Node> q = new LinkedList<>();
q.add(node);
while(!q.isEmpty()){
Node cur = q.poll();
for(Node nei : cur.neighbors){
if(!mapVis.containsKey(nei)){
mapVis.put(nei,new Node(nei.val,new ArrayList<Node>()));
q.add(nei);
}
mapVis.get(cur).neighbors.add(mapVis.get(nei));
}
}
return mapVis.get(node);
}
}
10. 、二叉树的层序遍历 II(同7)
-
题目描述:给定一个二叉树,返回其节点值自底向上的层序遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
-
优秀思路:
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
List<List<Integer>> res2 = new ArrayList<>();
if(root == null) return res;
List<Integer> tempList;
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while(!q.isEmpty()){
int size = q.size();
tempList = new ArrayList<Integer>();
while(size-- > 0){
TreeNode cur = q.poll();
tempList.add(cur.val);
if(cur.left != null) q.offer(cur.left);
if(cur.right != null) q.offer(cur.right);
}
res.add(0,tempList); // 0表示插入的索引,保证总是将后面的插到前面
}
return res;
}
}