目录(序号为leetcode题号)
124. 二叉树中的最大路径和
路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中至多出现一次 。该路径至少包含一个节点,且不一定经过根节点。路径和是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其最大路径和 。
示例:
输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42
解题思路:递归,在树中的一条路径就是从叶子节点往上走到根节点,然后再往下。从叶子节点向上出发,叶子节点告诉父节点自己的值,父节点根据左右两个子节点返回的值,选择一个最大值的叶子节点(叶子节点都不为负,负则不选择叶子节点作为路径中的节点),父节点将选择的叶子节点的值与自身的值相加,值的和向上返回,在这个过程中使用 一个全局变量保存最大的路径值。
class Solution {
// 路径的最大值初始为最小值。
Integer max = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
// 求解节点的最大值
maxSum(root);
// 返回路径的最大值
return max;
}
public int maxSum(TreeNode root){
if(root == null){
return 0;
}
// 求解左右节点能返回的最大值,小于0,则舍弃
int maxLeft = Math.max(maxSum(root.left),0);
int maxRight = Math.max(maxSum(root.right),0);
// 当前节点所能返回的最大路径值
int maxRoot = Math.max(maxLeft,maxRight)+root.val;
// 以当前节点为树根的路径最大值与历史最大值,选择最大的作为当前最大值。
max = Math.max(maxLeft+maxRight+root.val,max);
// 返回给父节点,以该子节点的最大路径值
return maxRoot;
}
}
125. 验证回文串
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
说明:本题中,我们将空字符串定义为有效的回文串。
示例 :
输入: "A man, a plan, a canal: Panama"
输出: true
解释:"amanaplanacanalpanama" 是回文串
解题思路:直接写就行,注意忽略非字母和非数字的其它字符
public boolean isPalindrome(String s) {
// 忽略大小写,只考虑字母和数字
// 大写字母 65-90 小写字母 97 - 122 数字 48-57
int left = 0;
int right = s.length() - 1;
char[] chars = s.toCharArray();
while(left < right){
// 非字母,非数字,left++,如果left不小于right,说明字符串判断完毕,返回true
while(!(65<=chars[left] && chars[left]<=90) &&
!(97<=chars[left]&&chars[left]<=122)&&
!(48<=chars[left] && chars[left] <= 57)){
if(left < right){
left++;
}else {
return true;
}
}
// 非字母,非数字,right++,如果right不大于left,说明字符串判断完毕,返回true
while(!(65<=chars[right] && chars[right]<=90) &&
!(97<=chars[right]&&chars[right]<=122)&&
!(48<=chars[right] && chars[right] <= 57)){
if(left < right) {
right--;
}else{
return true;
}
}
if(48<=chars[left] && chars[left] <= 57){
// 数字比较
if(chars[left] != chars[right]){
return false;
}
}else{
// 字母比较,先都转换为小写
chars[left] = (char) ((int)(chars[left])|32);
chars[right] = (char) ((int)(chars[right])|32);
if(chars[left] != chars[right]){
return false;
}
}
left++;
right--;
}
return true;
}
127. 单词接龙
字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列:
1.序列中第一个单词是 beginWord 。
2.序列中最后一个单词是 endWord 。
3.每次转换只能改变一个字母。
4.转换过程中的中间单词必须是字典 wordList 中的单词。
给你两个单词 beginWord 和 endWord 和一个字典 wordList,找到从 beginWord 到 endWord 的 最短转换序列中的单词数目 。如果不存在这样的转换序列,返回 0。
输入:beginWord = "hit", endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]
输出:5
解释:一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。
解题思路:求最短的转换序列,所以使用BFS广度优先。从开始单词判断,改变了单词中的某一位,查看字典中是否有这个单词,如果有则加入下一层,等到本层判断完成后,进入下一层的判断,直到遇到结束单词或者没有下层结束。
将字典中的单词加入哈希表,方便查找字典。使用队列,存放下一层的单词。使用一个哈希表,记录已经访问过的单词。
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
// 哈希容器 set 将字典中所有的单词放入set集合
HashSet<String> set = new HashSet<>(wordList);
if(!set.contains(endWord)){
return 0;
}
// 哈希容器 visited,将已经访问的单词加入容器,表示已经访问
HashSet<String> visited = new HashSet<>();
visited.add(beginWord);
// 用于存放要遍历的单词
Deque<String> deque = new LinkedList<>();
deque.add(beginWord);
int step = 1;
while(!deque.isEmpty()){
// 记录本层的单词个数。
int size = deque.size();
for(int i = 0; i < size; i++){
String s = deque.poll();
// 将本层每个单词的每个字母进行修改为 a-z,然后判断新单词是否在字典中存在。
for(int j = 0;j<s.length();j++){
char[] chars = s.toCharArray();
for(char k = 'a';k <= 'z';k++){
chars[j] = k;
String c = String.valueOf(chars);
// 新单词在字典中存在且未被访问过,访问并加入下一层
if(set.contains(c) && !visited.contains(c)){
// 如果这个单词是结束单词,返回层数,即为转换的长度
if(c.equals(endWord)){
return step + 1;
}
deque.add(c);
visited.add(c);
}
}
}
}
step++;
}
return 0;
}
以上是单向BFS的写法,对于本题,已知起点和终点,则可以使用双向BFS,从两端同时搜索。
如图所示
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
Set<String> set = new HashSet<>(wordList);
if (!set.contains(endWord)) {
return 0;
}
Set<String> visited = new HashSet<>();
// 分别用左边和右边扩散的哈希表代替单向 BFS 里的队列,它们在双向 BFS 的过程中交替使用
Set<String> beginVisited = new HashSet<>();
beginVisited.add(beginWord);
Set<String> endVisited = new HashSet<>();
endVisited.add(endWord);
int step = 1;
// beginVisited和endVisited都不为空,因为只要为空,就不能存在beginWord到endWord的转换
while (!beginVisited.isEmpty() && !endVisited.isEmpty()) {
// 优先选择小的哈希表进行扩散
if (beginVisited.size() > endVisited.size()) {
Set<String> temp = beginVisited;
beginVisited = endVisited;
endVisited = temp;
}
// 保证 beginVisited 是相对较小的集合
Set<String> nextLevel = new HashSet<>();
// beginVisited存放的是当前层的所有单词。
for (String word : beginVisited) {
// 对于当前层每一个单词的每一个位置都改变为 a-z,再判断有没有词典中有没有这个单词
for (int i = 0; i < word.length(); i++) {
char[] charArray = word.toCharArray();
for (char c = 'a'; c <= 'z'; c++) {
charArray[i] = c;
String nextWord = String.valueOf(charArray);
if (set.contains(nextWord)) {
// 如果这个新单词,已经在endVisited中说明,可以完成转换,返回层数
if (endVisited.contains(nextWord)) {
return step + 1;
}
// 进行访问,并加入下一层。
if (!visited.contains(nextWord)) {
nextLevel.add(nextWord);
visited.add(nextWord);
}
}
}
}
}
beginVisited = nextLevel;
step++;
}
return 0;
}
128. 最长连续序列
给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
解题思路:使用哈希表。将数组存放到hashset,进行去重,然后在数组中只从最开始的值开始技术寻找最长连续序列。如果一个值为 x,如果x-1存在于数组中,那么以x开头的序列一定不是最长连续序列。
public int longestConsecutive(int[] nums) {
int maxnum = 0;
int num;
HashSet<Integer> hashSet = new HashSet<>();
// 数组存放入hash表
for(int i = 0;i < nums.length; i++){
hashSet.add(nums[i]);
}
Iterator<Integer> iterator = hashSet.iterator();
// 遍历hash表
while(iterator.hasNext()){
Integer next = iterator.next();
// 只有在值为开头的序列才进行寻找最长子序列
if(!hashSet.contains(next-1)) {
next++;
num = 1;
while (hashSet.contains(next)) {
next++;
num++;
}
if (num > maxnum)
maxnum = num;
}
}
return maxnum;
}
130. 被围绕的区域
给你一个 m x n 的矩阵 board ,由若干字符 ‘X’ 和 ‘O’ ,找到所有被 ‘X’ 围绕的区域,并将这些区域里所有的 ‘O’ 用 ‘X’ 填充。
示例:
输入:board = [["X","X","X","X"],
["X","O","O","X"],
["X","X","O","X"],
["X","O","X","X"]]
输出: [["X","X","X","X"],
["X","X","X","X"],
["X","X","X","X"],
["X","O","X","X"]]
解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。
任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。
如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
解题思路:遍历边界上的 字母 O,并进行递归将所有与边界相连的 字母 O改变为 字母 A,然后再将数组中的字母 O变成 字母X,最后将字母A变为字母O
public void solve(char[][] board) {
int m,n;
m = board.length;
n = board[0].length;
//遍历四个边,将与边上相连的字母O变为字母A
for(int i = 0;i<n; i++){
if(board[0][i] == 'O'){
dfs(board,0,i,m,n);
}
}
for(int i = 0;i<n; i++){
if(board[m-1][i] == 'O'){
dfs(board,m-1,i,m,n);
}
}
for(int i = 0;i<m; i++){
if(board[i][0] == 'O'){
dfs(board,i,0,m,n);
}
}
for(int i = 0;i<m; i++){
if(board[i][n-1] == 'O'){
dfs(board,i,n-1,m,n);
}
}
// 将字母O变为 X
for(int i=1;i<m-1;i++){
for(int j=1;j<n-1;j++){
if(board[i][j] == 'O'){
board[i][j] = 'X';
}
}
}
//将所有的字母A再变回字母O
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(board[i][j] == 'A'){
board[i][j] = 'O';
}
}
}
}
// dfs,将所以边界相邻的 O 替换为 A
public void dfs(char[][] board,int i,int j,int m,int n){
board[i][j] = 'A';
// 上
if(0 <= i-1 && board[i-1][j] == 'O'){
dfs(board,i-1,j,m,n);
}
// 下
if(m > i + 1 && board[i+1][j] == 'O'){
dfs(board,i+1,j,m,n);
}
// 左
if(0 <= j-1 && board[i][j-1] == 'O'){
dfs(board,i,j-1,m,n);
}
// 右
if(n > j+1 && board[i][j+1] == 'O'){
dfs(board,i,j+1,m,n);
}
}
131. 分割回文串
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。回文串 是正着读和反着读都一样的字符串。
示例:
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
解题思路:动态规划对字符串预处理+回溯
动态规划:dp[i][j]表示 字符 i 到字符 j 是否为回文串。
状态转移方程为
true i >= j
/
dp[i][j] =
\
s[i]==s[j] && dp[i+1][i-1]
在回溯时,直接使用dp[i][j]进行判断。
public List<List<String>> partition(String s) {
List<List<String>> result = new ArrayList<List<String>>();
List<String> list = new ArrayList<String>();
int n = s.length();
// dp数组记录是否为回文串
boolean[][] dp = new boolean[n][n];
for (int i = 0; i < s.length(); ++i) {
Arrays.fill(dp[i], true);
}
// 初始化dp数组。
for (int i = s.length() - 1; i >= 0; --i) {
for (int j = i + 1; j < s.length(); ++j) {
dp[i][j] = (s.charAt(i) == s.charAt(j)) && dp[i + 1][j - 1];
}
}
// 回溯
dfs(s,result,new ArrayList<String>(),dp,0);
return result;
}
// 参数说明,n为当前要分割的元素下标
public void dfs(String s,List<List<String>> result,List<String> list,boolean[][] dp,int n) {
// 等于 s长度,将当前结果加入最终结果集
if (s.length() == n) {
result.add(new ArrayList<String>(list));
return;
}
for (int i = n;i< s.length(); ++i) {
// 判断是否为回文串,是就继续分割。
if (dp[n][i]) {
list.add(s.substring(n, i + 1));
dfs(s,result,list,dp,i + 1);
list.remove(list.size() - 1);
}
}
}
无动态规划代码
public List<List<String>> partition(String s) {
List<List<String>> result = new ArrayList<>();
partition(result,new ArrayList<String>(),s,0);
return result;
}
public void partition(List<List<String>> result,List<String> list,String s,int n){
if(n == s.length()){
result.add(new ArrayList<>(list));
return;
}
for(int j = 1;j + n - 1 < s.length();j++){
// 从 n开始,j个字符是不是回文串
int l = n;
int r = n + j - 1;
while(l <= r && s.charAt(l) == s.charAt(r)){
l++;r--;
}
if(l > r){
// 是回文串
list.add(s.substring(n,n + j));
partition(result,list,s,n + j);
list.remove(list.size()-1);
}
}
}
134. 加油站
在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i + 1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。
说明:
如果题目有解,该答案即为唯一答案。
输入数组均为非空数组,且长度相同。
输入数组中的元素均为非负数。
示例:
输入:
gas = [1,2,3,4,5]
cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。
解题思路:如果全部加油站的油小于全部的花费,则必无解,否则有解。如果从第 i 点出发,到第 j点无解,则从第 i 点到 第 j 点的任意点都不存在解。下次直接从j+1点开始求解即可。
public int canCompleteCircuit(int[] gas, int[] cost) {
int gass = 0,costs = 0;
int num = 0,start = 0;
// 判断是否有解
for(int i = 0;i<gas.length;i++){
gass += gas[i];
costs += cost[i];
}
if(costs > gass){
return -1;
}
// 若有解,从第0点开始求解
for(int i = 0;i < gas.length;i++){
num += gas[i]-cost[i];
// 若无解,从第i+1开始求解
if(num < 0){
num = 0;
start = i + 1;
}
}
return start;
}
136. 只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
示例:
输入: [4,1,2,1,2]
输出: 4
解题思路:使用异或运算,因为a^a = 0,所以最后得到的结果一定是只出现一次的元素。
public int singleNumber(int[] nums) {
int num = 0;
for(int i = 0;i < nums.length;i++){
num = num^nums[i];
}
return num;
}
138. 复制带随机指针的链表
给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
构造这个链表深拷贝。 深拷贝应该正好由 n 个 全新节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。
返回复制链表的头节点。
用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
你的代码 只 接受原链表的头节点 head 作为传入参数。
示例 :
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
方法一
解题思路:使用集合将旧的节点和新创建的节点进行映射,然后在根据旧节点的值对新节点进行赋值。
public Node copyRandomList(Node head) {
if(head == null){
return null;
}
HashMap<Node,Node> map = new HashMap<>();
Node current = head;
// 创建新节点,新节点与旧节点进行映射
while(current != null){
map.put(current,new Node(current.val));
current = current.next;
}
current = head;
while(current != null){
// 根据旧节点的值,为新节点赋值
map.get(current).next = map.get(current.next);
map.get(current).random = map.get(current.random);
current = current.next;
}
return map.get(head);
}
递归写法
HashMap<Node,Node> map = new HashMap<>();
public Node copyRandomList(Node head) {
if(head == null){
return null;
}
if(!map.containsKey(head)){
// 创建新的节点,旧的节点与新的节点进行映射
Node key = new Node(head.val);
map.put(head,key);
// 根据旧的节点为新的节点赋值
key.next = copyRandomList(head.next);
key.random = copyRandomList(head.random);
}
return map.get(head);
}
方法二
解题思路:迭代 + 节点拆分,在原始链表中创建新的节点,并插入到原始节点的后面。如有链表 A->B->C,复制原始节点并插入到原始节点的后面。链表为 A->A’->B->B’->C->C’。然后对新节点的 random赋值,然后将链表拆分为 A->B->C与A’->B’->C’两个链表。返回新链表的头。
public Node copyRandomList(Node head) {
if(head == null){
return null;
}
Node newNode;
// 赋值旧节点,插入原始链表
for(Node node = head;node!=null;node = node.next.next){
newNode = new Node(node.val);
newNode.next = node.next;
node.next = newNode;
}
// 为新节点的random赋值
for(Node node = head;node!=null;node = node.next.next){
newNode = node.next;
if(node.random != null){
newNode.random = node.random.next;
}
}
// 链表分离
Node newHead = head.next;
for(Node node = head;node != null; node = node.next) {
newNode = node.next;
node.next = node.next.next;
if(newNode.next != null){
newNode.next = newNode.next.next;
}
}
return newHead;
}
139. 单词拆分
给你一个字符串 s 和一个字符串列表 wordDict 作为字典,判定 s 是否可以由空格拆分为一个或多个在字典中出现的单词。
说明:拆分时可以重复使用字典中的单词。
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
方法一:回溯
超时了
public boolean wordBreak(String s, List<String> wordDict) {
// 回溯法
HashSet<String> hashSet = new HashSet<>(wordDict);
return wordBreak(s,hashSet,0,0);
}
public boolean wordBreak(String s,HashSet<String> wordDict,int left,int right){
if(right == s.length()){
if(left == right){
return true;
}
return left != 0 && wordDict.contains(s.substring(left,right));
}
if(wordDict.contains(s.substring(left,right + 1))){
// 拆分
if(wordBreak(s,wordDict,right+1,right+1)){
return true;
}
// 不拆分
if(wordBreak(s,wordDict,left,right + 1)){
return true;
}
}else{
// 不可以拆分
if(wordBreak(s,wordDict,left,right + 1)){
return true;
}
}
return false;
}
方法二:动态规划
定义dp[i] 表示字符串 s 前 i 个字符组成的字符串 s[0…i-1] 是否能被空格拆分成若干个字典中出现的单词。
考虑状态转移方程,dp[i] 的取值,看s[0…i-1]中的分割点 j,s[0…j−1] 组成的字符串 s1 和 s[j…i-1] 组成的字符串 s2是否都合法。s1 是否合法可以直接由 dp[j]直接得知,剩下的只需要看s2是否合法。
状态转移方程为:
dp[i]=dp[j] && check(s[j…i−1])
其中 check(s[j…i-1]) 表示子串 s[j…i-1] 是否出现在字典中。
public boolean wordBreak(String s, List<String> wordDict) {
HashSet<String> hashSet = new HashSet<>(wordDict);
boolean[] dp = new boolean[s.length()+1];
// 0表示空串,空串为true
dp[0] = true;
for(int i = 1;i <= s.length();i++){
// 对于前 i 个字符,将 0 - i-1中的每一个中间字符尝试分割。
for(int j = 0;j < i;j++){
if (dp[j] && hashSet.contains(s.substring(j, i))) {
// dp[j] 为true,且j - i-1在字典中,可以分割。
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}