LeetCode刷题总结 - 面试经典 150 题 - 持续更新
- 其他系列
- 数组 / 字符串
- 88. 合并两个有序数组
- 27. 移除元素
- 26. 删除有序数组中的重复项
- 80. 删除有序数组中的重复项 II
- 169. 多数元素
- 189. 轮转数组
- 121. 买卖股票的最佳时机
- 122. 买卖股票的最佳时机 II
- 55. 跳跃游戏
- 274. H 指数
- 380. O(1) 时间插入、删除和获取随机元素
- 238. 除自身以外数组的乘积
- 739. 每日温度
- 42. 接雨水
- 双指针
- 125. 验证回文串
- 392. 判断子序列
- 167. 两数之和 II - 输入有序数组
- 11. 盛最多水的容器
- 15. 三数之和(已总结)
- 区间
- 228. 汇总区间
- 252. 会议室
- 56. 合并区间
- 57. 插入区间
- 二叉树
- 104. 二叉树的最大深度
- 100. 相同的树
- 101. 对称二叉树
- 根相同的两棵树root1,root2,判断root2是否是root1的子树?
- 剑指 Offer 26. 树的子结构
- 226. 翻转二叉树
- 105. 从前序与中序遍历序列构造二叉树
- 106. 从中序与后序遍历序列构造二叉树
- 117. 填充每个节点的下一个右侧节点指针 II
- 114. 二叉树展开为链表
- 112. 路径总和
- 129. 求根节点到叶节点数字之和
- 222. 完全二叉树的节点个数
- 236. 二叉树的最近公共祖先
- 二叉树层次遍历
- 199. 二叉树的右视图
- 637. 二叉树的层平均值
- 102. 二叉树的层序遍历
- 103. 二叉树的锯齿形层序遍历
- 数学
- 9. 回文数
- 66. 加一
- 172. 阶乘后的零
- 69. x 的平方根
- 50. Pow(x, n)
- 二分查找
- 35. 搜索插入位置
- 69. x 的平方根
- 74. 搜索二维矩阵
- 162. 寻找峰值
- JZ11 旋转数组的最小数字(存在重复值)
- 153. 寻找旋转排序数组中的最小值(不包含重复值)
- 33. 搜索旋转排序数组
- 一维动态规划
- 70. 爬楼梯
- 198. 打家劫舍
- 139. 单词拆分
- 322. 零钱兑换
- 300. 最长递增子序列
- 多维动态规划
- JZ47 礼物的最大价值
- 120. 三角形最小路径和
- 64. 最小路径和
- 63. 不同路径 II
- 5. 最长回文子串
- 解法一:暴力 - 遍历所有字串
- 解法二:中心扩展法
- 买卖股票系列
- 121. 买卖股票的最佳时机
- 122. 买卖股票的最佳时机 II
- 123. 买卖股票的最佳时机 III
- 188. 买卖股票的最佳时机 IV
- 回溯
- 17. 电话号码的字母组合
- 77. 组合
- 39. 组合总和
- 剑指 Offer 34. 二叉树中和为某一值的路径
- 46. 全排列
- 51. N 皇后
- 22. 括号生成
- 正序
- 1. 两数之和(难度:简单)
- 2. 两数相加(难度:中等)
- 3. 无重复字符的最长子串(难度:中等)
- 4. 寻找两个正序数组的中位数(难度:困难)- 先不做
- 5. 最长回文子串(难度:中等)
- 解法一:暴力 - 遍历所有字串
- 解法二:中心扩展法
- 7. 整数反转(难度:中等-简单)
- 9. 回文数(难度:简单)
其他系列
这篇文章原本和【LeetCode刷题总结 - 剑指offer系列 - 持续更新】是一篇文章,但由于篇幅过大没法更新,所以就拆成了两篇。
若题中分析出现“上面有总结过…”等字样,且发现本篇文章没有总结,则可以到【LeetCode刷题总结 - 剑指offer系列 - 持续更新】中去寻找
【LeetCode刷题总结 - 剑指offer系列 - 持续更新】
数组 / 字符串
88. 合并两个有序数组
【88. 合并两个有序数组】
分析:
类似于归并排序的merge部分,可以看一下这篇文章【归并排序】
代码:
class Solution {public void merge(int[] nums1, int m, int[] nums2, int n) {// 辅助数组,复制nums1的前m个数据int[] copyFromNums1 = new int[m];for(int i=0; i<m; i++) {copyFromNums1[i] = nums1[i];}// 指针指向 copyFromNums1int i=0;// 指针指向 num2int j=0;// 指针指向 nums1int index = 0;// 比较 copyFromNums1 和 nums2,更小的放到nums1中while(i < m && j < n) {if(copyFromNums1[i] <= nums2[j]) {nums1[index++] = copyFromNums1[i++];} else {nums1[index++] = nums2[j++];}}// 执行到这里,要么 i==m 要么 j==n// 将剩余的放进nums1(可能已经空了)while(i < m) {nums1[index++] = copyFromNums1[i++];}// 将剩余的放进nums1(可能已经空了)while(j < n) {nums1[index++] = nums2[j++];}}
}
27. 移除元素
【27. 移除元素】
分析:
- 新增一个指针
left
,[0,left)
之间的元素就是!=val
的 - 遍历数组,判断每个元素是否等于
val
,!= val
则添加到nums[left]
,left++
参考动画:https://leetcode.cn/problems/remove-element/solution/xue-sheng-wu-de-nu-peng-you-du-neng-kan-nk7yy/
代码:
class Solution {public int removeElement(int[] nums, int val) {int left = 0;// 遍历数组,判断每个元素for(int right=0; right<nums.length; right++) {if(nums[right] != val) {nums[left] = nums[right];left++;}}return left;}
}
26. 删除有序数组中的重复项
【26. 删除有序数组中的重复项】
分析:
跟上一题思路一样,不同点在于比较的对象变成了“最后不重复的元素”
代码:
class Solution {public int removeDuplicates(int[] nums) {if(nums.length == 1) {return 1;}/*** left、right* left:指向最后不重复的元素* right:指向当前遍历的元素*/int left = 0;for(int right=1; right<nums.length; right++) {if(nums[right] != nums[left]) {nums[left+1] = nums[right];left++;} }return left+1;}
}
80. 删除有序数组中的重复项 II
【80. 删除有序数组中的重复项 II】
分析:
跟上一题思路一样,不同点在于比较的对象变成了“新数组中的倒数第k个元素”(此题中 k=2
)
其实该题就是上一题的升华版,将上一题总结的更通用,上一题可理解为 k=1
代码:
class Solution {public int removeDuplicates(int[] nums) {if(nums.length == 1 || nums.length == 2) {return nums.length;}return removeK(nums, 2);}public int removeK(int[] nums, int k) {int left = k - 1;for(int right=k; right<nums.length; right++) {if(nums[right] != nums[left-k+1]) {nums[left+1] = nums[right];left++;}}return left+1;}
}
169. 多数元素
【169. 多数元素】
分析:
某个值在该数组中的个数超过一半,该值即叫做多数元素,这样的元素在这个数组中肯定只有一个
摩尔投票算法
可以使空间复杂度O(1)
,时间复杂度为O(n)
.
B站上上视频讲解:https://www.bilibili.com/video/BV1Ey4y1n7hb
代码:
public class Solution {/* 摩尔投票 */public int MoreThanHalfNum_Solution(int [] arr) {// 票数int rating = 0;// m假设是个数超过一半的那个数int m = arr[0];for(int i=0; i<arr.length; i++) {// 当票数为0时, 将当前数当做m,并且票数设为1if(rating == 0){m = arr[i];rating = 1;} else {if(arr[i] == m) // 若与m相同 票数就++rating++;else // 不同则--rating--;}}return m;}
}
189. 轮转数组
【189. 轮转数组】
分析:
- 步骤一:整体翻转,
[1,2,3,4,5,6,7]
->[7,6,5,4,3,2,1]
- 步骤二:数组截断,分成两段,若k=3,则
[7,6,5,4,3,2,1]
->[7,6,5]
、[4,3,2,1]
- 步骤三:分别对两段数组进行翻转,
[7,6,5]
、[4,3,2,1]
->[5,6,7]
、[1,2,3,4]
- 步骤四:拼接两段,
[5,6,7]
、[1,2,3,4]
->[5,6,7,1,2,3,4]
代码:
class Solution {/*** 因为我们实际上并没有,分割数组(所有操作都是在原数组上),因此【步骤二】 和 【步骤四】是可以省略的*/public void rotate(int[] nums, int k) {// 注意,该题的k有可能大于数组的长度,因此我们要提前取余k = k % nums.length;// 步骤一:整体翻转,[1,2,3,4,5,6,7] -> [7,6,5,4,3,2,1]reverse(nums, 0, nums.length-1);// 步骤二:数组截断,分成两段,若k=3,则[7,6,5,4,3,2,1] -> [7,6,5]、[4,3,2,1]// 步骤三:分别对两段数组进行翻转,[7,6,5]、[4,3,2,1] -> [5,6,7]、[1,2,3,4]reverse(nums, 0, k-1);reverse(nums, k, nums.length-1);// 步骤四:拼接两段,[5,6,7]、[1,2,3,4] -> [5,6,7,1,2,3,4]}/*** 翻转数组中元素 [a,b,c,d] -> [d,c,b,a]*/public void reverse(int[] nums, int left, int right) {while(left < right) {swap(nums, left, right);left++;right--;}}/*** 交换数组中 index1 和index2 的位置*/public void swap(int[] nums, int index1, int index2) {int temp = nums[index1];nums[index1] = nums[index2];nums[index2] = temp;}
}
121. 买卖股票的最佳时机
【121. 买卖股票的最佳时机】
分析:
- 需要两个全局变量,
minLowVal
:表示历史最低点;maxProfit
:历史最大的模拟收益 - 遍历每天的股票,更新
minLowVal
,模拟 若当前价格大于历史最低点
则卖掉股票
,同时更新maxProfit
代码:
class Solution {public int maxProfit(int[] prices) {// 历史最低点int minLowVal = Integer.MAX_VALUE;// 历史最大的模拟收益int maxProfit = 0;for(int i=0; i<prices.length; i++) {int curPrice = prices[i];// 更新minLowValminLowVal = Math.min(minLowVal, curPrice);// 若当天价格 高于 前面最低的价格,则模拟卖出股票if(curPrice > minLowVal) {int profit = curPrice - minLowVal;// 更新maxProfitmaxProfit = Math.max(maxProfit, profit);}}return maxProfit;}
}
122. 买卖股票的最佳时机 II
【122. 买卖股票的最佳时机 II】
分析:
这题比上题还简单,(#^.^#)
不同点:
- 上题只能买卖一次,因此需要找到
前面的最低点
和后面的最高点
- 这题可以无限买卖,只要
当天价格
>昨天价格
,我们就可以卖股票(就有收益),然后收益累加
代码:
class Solution {public int maxProfit(int[] prices) {// 若只有一天的,则收益为0if(prices.length == 1) {return 0;}// 收益是累加的,初始值为0int maxProfit = 0;// 从第2天开始遍历for(int i=1; i<prices.length; i++) {// 若 当天价格 > 前一天价格,则模拟卖出股票,并且收益累加if(prices[i] > prices[i-1]) {int newProfit = prices[i] - prices[i-1];maxProfit += newProfit;}}return maxProfit;}
}
55. 跳跃游戏
【55. 跳跃游戏】
分析:
参考视频:【LeetCode_55_跳跃游戏】
代码:
class Solution {public boolean canJump(int[] nums) {/*** maxLen:表示历史情况下能够达到的最远下标的位置* 当到达0时,maxLen指 【0所能到达最远位置】* 当到达1时,maxLen指 【0所能到达最远位置】 和 【1所能到达最远位置】 的最大值* 当到达i时,maxLen指 【0所能到达最远位置】 和 【1所能到达最远位置】 ... 【i所能到达最远位置】 的最大值*/int maxLen = 0;for(int i=0; i<nums.length; i++) {// 如果 maxLen 小于 i,则说明无论怎么跳都不能到达i的,直接返回falseif(i > maxLen) {return false;}// 更新maxLenmaxLen = Math.max(maxLen, i + nums[i]);}return true;}
}
274. H 指数
【274. H 指数】
分析:
- 先排序
- 引用值从大到小进行遍历
- 满足条件就
count++
,直到不满足为止
代码:
class Solution {public int hIndex(int[] citations) {// 先对数组进行排序,默认是递增Arrays.sort(citations);// 记录满足条件的文章数,初始值为0int count = 0;// 以 引用值从高到低 进行遍历for(int i = citations.length-1; i>=0; i--) {// 在比较的时候,要把当前文章也加上,因此这里是count+1if(citations[i] >= count+1) {count++;} else {break;}}return count;}
}
380. O(1) 时间插入、删除和获取随机元素
【380. O(1) 时间插入、删除和获取随机元素】
分析:
- 获取随机值,并且时间复杂度为
O(1)
,我们很容易想到使用数组来存储数据 - 而
insert
和remove
的时间复杂度也为O(1)
,我们又可以使用map
,key
存储对应的值,value
存储该元素在数组中的下标
复杂的是remove函数,细细咀嚼吧
代码:
class RandomizedSet {// 用于存储添加的valList<Integer> list = new ArrayList();// key:具体插入的值 value:表示该值在list中的下标Map<Integer, Integer> map = new HashMap();// 随机数生成器对象Random random = new Random();public RandomizedSet() { }public boolean insert(int val) {if(map.containsKey(val)) {return false;}list.add(val);map.put(val, list.size()-1);return true;}public boolean remove(int val) {if(!map.containsKey(val)) {return false;}// 1、获取待删除值的下标int idx = map.get(val);// 2、取list中最后一个值:lastint last = list.get(list.size()-1);// 3、用last覆盖,要删除的值的位置list.set(idx, last);map.put(last, idx);// 4、删除最后一个位置,以及对应值map.remove(val);list.remove(list.size()-1);return true;}public int getRandom() {return list.get(random.nextInt(list.size()));}
}
238. 除自身以外数组的乘积
【238. 除自身以外数组的乘积】
分析:
解题思路:
B站上视频连接:https://www.bilibili.com/video/BV1xV411f773
- 利用类似于动态规划的思想,构建
[0,i]
的乘积数组,即i及i之前的所有数的乘积 - 利用类似于动态规划的思想,构建
[i,n-1]
的乘积数组,即i及i之后的所有数的乘积 - 最终根据题意
res[i] = cj1[i-1] * cj2[i+1]
,求最终结果集
代码:
public class Solution {public int[] multiply(int[] A) {int n = A.length;// 用于存放 i及i之前的所有乘积(包含i:[0,i])int[] cj1 = new int[n];// 用于存放 i及i之后的所有乘积(包含i:[i,n-1])int[] cj2 = new int[n];// 用于存放那结果集int[] res = new int[n];// 类似于动态规划的求法,求cj1。[0,i]for(int i=0; i<n; i++) {// 若i为0,则区A[0] 边界条件if(i == 0)cj1[i] = A[0];else // 动态规划cj1[i] = cj1[i-1] * A[i];}// 类似于动态规划的求法,求cj2。[i,n-1]。 同上for(int i=n-1; i>=0; i--) {if(i == n-1)cj2[i] = A[n-1];elsecj2[i] = cj2[i+1] * A[i];}// 最后根据题意,求结果集for(int i=0; i<n; i++) {if(i == 0)res[i] = cj2[i+1];else if(i == n-1)res[i] = cj1[i-1];elseres[i] = cj1[i-1] * cj2[i+1];}return res;}
}
739. 每日温度
【739. 每日温度】
分析:
单调栈
维护 还未找到后面更高温度的日期
参考视频:【单调栈,你该了解的,这里都讲了!LeetCode:739.每日温度】
代码:
class Solution {public int[] dailyTemperatures(int[] temperatures) {// 单调递增栈,存放还未找到最近最高温度的日期,存放的是下标Stack<Integer> stack = new Stack();// 存放待返回的结果int[] ret = new int[temperatures.length]; stack.push(0);// 遍历温度表for(int i=1; i<temperatures.length; i++) {int cur = temperatures[i];// 若栈顶温度 小于 当天温度,则说明当天温度 相对于 栈顶元素来说,就是最近的更高温度while(!stack.isEmpty() && temperatures[stack.peek()] < cur) {// 已经为栈顶元素找到了 最近的最高温度,则没必要放到栈中了int popedIdx = stack.pop();// 记录天数差ret[popedIdx] = i - popedIdx;}stack.push(i);}// 最后stack不为空,则说明stack中的元素 后面找不到更高温度的日期while(!stack.isEmpty()) {int popedIdx = stack.pop();ret[popedIdx] = 0;}return ret;}
}
42. 接雨水
【42. 接雨水】
分析:
参考视频:【单调栈,经典来袭!LeetCode:42.接雨水】
建议先做上一题
- 找到
较低点
的前一个较大值
和后一个较大值
,则可以算较低点
相对于前、后两个较大点
所接的雨水(横向求解,水平求解) - 维护一个
单调栈
(存储下标),若当前值
大于栈顶元素
,则栈顶元素
就是较低点
,栈顶第二个元素
就是较低点
的前一个较大点
,当前值
就是较低点
的下一个较大点
代码:
class Solution {public int trap(int[] height) {// stack维护一个单调栈(非递减),存储下标Stack<Integer> stack = new Stack();// 记录接雨水的总数int sum = 0;for(int i=0; i<height.length; i++) {int cur = height[i];// 若栈顶元素 小于 当前值则弹出(表示当前值是栈顶元素的后续第一个较大者)while(!stack.isEmpty() && height[stack.peek()] < cur) {// nextHeight:下个较大值(下标)int nextHeight = i;// mid:就是中间点(下标)int mid = stack.pop();if(stack.isEmpty()) { // 若为空 则直接跳过,不处理break;}// preHeight:前一个较大值(下标)int preHeight = stack.peek();// 高度差int diffY = Math.min(height[preHeight], height[nextHeight]) - height[mid];// 宽度差int diffX = nextHeight - preHeight - 1;// 面积累计sum += diffX * diffY;}stack.push(i);}return sum;}
}
双指针
125. 验证回文串
【125. 验证回文串】
分析:
- 双指针:左右指针同步往内移动,若字符(排除掉特殊字符)不同则为
false
Character.isLetterOrDigit()
:判断是否是数字或者字母字符Character.toLowerCase()
:字母转成小写
代码:
class Solution {public boolean isPalindrome(String s) {s = s.trim();if(s.length() == 0) {return true;}int left = 0;int right = s.length() - 1;while(left < right) {// 左侧:left指针,跳过非数字或者字母的字符while(left < right && !Character.isLetterOrDigit(s.charAt(left))) {left++;}// 右侧:right指针,跳过非数字或者字母的字符while(left < right && !Character.isLetterOrDigit(s.charAt(right))) {right--;}// 字符都归一化成小写char leftChar = Character.toLowerCase(s.charAt(left));char rightChar = Character.toLowerCase(s.charAt(right));// 若不同 返回falseif(leftChar != rightChar) {return false;}// 同步向内移动left++;right--;}return true;}
}
392. 判断子序列
【392. 判断子序列】
分析:
- 双指针,
p1
指向字符串s
,p2
指向字符串t
- 字符遍历,一直遍历,直到
s
结束 或者t
结束 p2
指针每次后后移一步,p1
指针只有 两个字符相同时 才会后移
代码:
class Solution {public boolean isSubsequence(String s, String t) {if(s.length() == 0) {return true;}if(t.length() == 0) {return false;}int p1 = 0;int p2 = 0;// 一直遍历,直到s结束 或者 t结束while(p1 < s.length() && p2 < t.length()) {if(s.charAt(p1) == t.charAt(p2)) {// 若相同,则s的指针-p1也会后移p1++;}// t的指针-p2总会后移一步p2++;}// p1 == s.length() 时,表明s已经遍历完了return p1 == s.length();}
}
167. 两数之和 II - 输入有序数组
【167. 两数之和 II - 输入有序数组】
分析:
B站上视频讲解:https://www.bilibili.com/video/BV1J741157eS
- 左右双指针。左指针-
l
初始为0位置,右指针-r
初始为length-1
。 while(l<r)
循环。 若和大于sum
则右指针左移;若和小于sum
则左指针右移。
代码:
class Solution {public int[] twoSum(int[] numbers, int target) {int left = 0;int right = numbers.length - 1;while(left < right) {int sum = numbers[left] + numbers[right];if(sum == target) {int[] res = new int[2];res[0] = left+1;res[1] = right+1;return res;}if(sum < target) {left++;} else {right--;}}return new int[]{-1, -1};}
}
11. 盛最多水的容器
【11. 盛最多水的容器】
分析:
借鉴题解:https://leetcode.cn/problems/container-with-most-water/solution/container-with-most-water-shuang-zhi-zhen-fa-yi-do/
结论:左右指针,谁的高度越低,谁就往中间移动
- 若向内移动短板,水槽的短板
min(h[i], h[j])
可能变大,因此下个水槽面积可能增大 - 若向内移动长板,水槽的短板
min(h[i], h[j])
不变或变小,因此下个水槽面积一定变小
流程:
- 初始化:双指针
left
,right
分列水槽的两端 - 循环收窄:直到双指针相遇则跳出
- 更新面积最大值
max
- 选定两板高度中的短板,向中间收窄一格
- 更新面积最大值
- 返回值:返回面积最大值即可
代码:
class Solution {public int maxArea(int[] height) {int max = Integer.MIN_VALUE;int left = 0;int right = height.length-1;while(left < right) {int area = (right - left) * Math.min(height[left], height[right]);max = Math.max(max, area);// 谁低谁往中间移动if(height[left] < height[right]) {left++;} else {right--;}}return max;}
}
15. 三数之和(已总结)
【15. 三数之和】
分析:
- 先排序
- 通过枚举
i
确定第一个数,另外两个指针left
,right
分别从左边i + 1
和右边length - 1
往中间移动,找到满足nums[left] + nums[right] == -nums[i]
的所有组合 - 去重:因为是有序序列,因此我们可以用如下方式去重(重点,看代码细细品味)
代码:
class Solution {public List<List<Integer>> threeSum(int[] nums) {List<List<Integer>> res = new ArrayList();// 先排序Arrays.sort(nums);int n = nums.length;// 遍历每个元素,当做三元组的首个元素for(int i=0; i<n; i++) {// 对第一个元素去重if(i != 0 && nums[i] == nums[i-1]) {continue;}int target = -nums[i];int left = i+1;int right = n-1;while(left < right) {int sum = nums[left] + nums[right];if(sum < target) {left++;} else if (sum > target) {right--;} else {if(sum == target) {res.add(Arrays.asList(nums[i], nums[left], nums[right]));// 因为我们要找到所有 三元组,因此需要继续找// 找下一个跟 当前tempL不同的值作为新的left(因为数组是有序的,因此这样可以保证去重)int tempL = nums[left++];while(left < right && nums[left] == tempL) {left++;}// 找下一个跟 当前tempR不同的值作为新的right(同理)int tempR = nums[right--];while(left < right && nums[right] == tempR) {right--;}}}}}return res;}
}
区间
228. 汇总区间
【228. 汇总区间】
分析:
- 双指针
low
、high
,[low, high]
用于维护递增1的有序序列 high
每次累加1
low
每次更新到high
代码:
class Solution {public List<String> summaryRanges(int[] nums) {List<String> res = new ArrayList();if(nums.length == 0) {return new ArrayList();}int len = nums.length;int low = 0;// [low, high] 用于维护递增1的有序序列while(low < len) {int high = low + 1;// 若 nums[high-1] + 1 == nums[high] 则high后移,直到不满足条件为止while(high < len && nums[high-1] + 1 == nums[high]) {high++;}// 此时 nums[high-1] + 1 != nums[high], 则[low, high-1] 就是递增1的有序序列String s = "";if(nums[low] == nums[high-1]) {s = String.valueOf(nums[low]);} else {s = nums[low] + "->" + nums[high-1];}res.add(s);// low重置,指向high位置low = high;}return res;}
}
252. 会议室
分析:
参考文章:【秒懂力扣区间题目:重叠区间、合并区间、插入区间】
因为一个人在同一时刻只能参加一个会议,因此题目实质是判断是否存在重叠区间?,这个简单,将区间按照会议开始时间进行排序,然后遍历一遍判断即可。
代码:
class Solution {public boolean canAttendMeetings(int[][] intervals) {// 将区间按照会议开始实现升序排序Arrays.sort(intervals, (v1, v2) -> v1[0] - v2[0]);// 遍历会议,如果下一个会议在前一个会议结束之前就开始了,返回 false。for (int i = 1; i < intervals.length; i++) {if (intervals[i][0] < intervals[i - 1][1]) {return false;}}return true;}
}
56. 合并区间
【56. 合并区间】
分析:
参考文章:【秒懂力扣区间题目:重叠区间、合并区间、插入区间】
- 遍历
intervals
- 若
interval
与res
的最后一个元素区间重合,则合并最后一个区间 - 否则 则直接加入到
res
中
代码:
class Solution {public int[][] merge(int[][] intervals) {List<int[]> res = new ArrayList();// 升序排列Arrays.sort(intervals, (v1,v2) -> v1[0] - v2[0]);// 遍历intervals,更新resfor(int[] interval: intervals) {if(!res.isEmpty() && interval[0] <= res.get(res.size()-1)[1]) { // 重叠了,则更新res的最后一个元素// 拿到 res的最后一个元素int[] last = res.get(res.size()-1);// 更新last[1]last[1] = Math.max(last[1], interval[1]);} else { // 不重叠则直接更新res.add(interval);}}int[][] res1 = new int[res.size()][2];for(int i=0; i<res.size(); i++) {for(int j=0; j<2; j++) {res1[i][j] = res.get(i)[j];}}return res1;}
}
57. 插入区间
【57. 插入区间】
分析:
用指针去扫 intervals
,最多可能有三个阶段:
- 不重叠的前半部分
- 重叠的部分
- 不重叠的后半部分
参考解题思路:【「手画图解」57. 插入区间 | 分成 3 个阶段考察】
代码:
class Solution {public int[][] insert(int[][] intervals, int[] newInterval) {List<int[]> res = new ArrayList();int i=0;int len = intervals.length;// 处理第一部分:不重叠的前半部区间while(i < len && intervals[i][1] < newInterval[0]) {res.add(intervals[i]);i++;}// 处理第二部分:重叠的区间while(i < len && intervals[i][0] <= newInterval[1]) {// 左边界,则取最小值newInterval[0] = Math.min(intervals[i][0], newInterval[0]);// 右边界,则取最大值newInterval[1] = Math.max(intervals[i][1], newInterval[1]);i++;}res.add(newInterval);// 处理第三部分:不重叠的后半部区间while(i < len) {res.add(intervals[i]);i++;}int[][] res1 = new int[res.size()][2];for(i=0; i<res.size(); i++) {for(int j=0; j<2; j++) {res1[i][j] = res.get(i)[j];}}return res1;}
}
二叉树
104. 二叉树的最大深度
【104. 二叉树的最大深度】
分析:
计算公式:以root为根树的高度 = max(root左子树的高度, root右子树的高度) + 1
很明显想算出以root为根树的高度,就要先算出子树的高度,因此我们很容想到的就是后序遍历
后序遍历特点:先算出左、右子节点结果,再通过回溯的特点往上推,实际上就是自底向上的计算方式
代码:
class Solution {public int maxDepth(TreeNode root) {return depth(root);}public int depth(TreeNode root) {if(root == null) {return 0;}// 左子树高度int leftDepth = depth(root.left);// 右子树高度int rightDepth = depth(root.right);// 代入公式算出 以root为根的树的高度int depth = Math.max(leftDepth, rightDepth) + 1;return depth;}
}
100. 相同的树
【100. 相同的树】
分析:
- 深度优先,后序遍历
- 先判断边界条件
p == null && q == null
:都为null
,则说明遍历到叶子节点了p == null || q == null
:到这里,其实p
、q
一个为null
、另一个不为null
,因此一定不相同p.val != q.val
:值不相同,一定不相同
- 判断左、右子树结果
- 汇总
代码:
class Solution {public boolean isSameTree(TreeNode p, TreeNode q) {return isSame(p, q);}public boolean isSame(TreeNode p, TreeNode q) {// 都为null,则说明遍历到叶子节点了if(p == null && q == null) {return true;}// 到这里,其实 p、q一个为null、另一个不为null,因此一定不相同if(p == null || q == null) {return false;}// 值不相同,一定不相同if(p.val != q.val) {return false;}boolean leftIsSame = isSame(p.left, q.left);boolean rightIsSame = isSame(p.right, q.right);// 只有左右子树都为相同,才相同return leftIsSame && rightIsSame;}
}
101. 对称二叉树
【101. 对称二叉树】
分析:
大体框架与上题一样:
- 这里是把根节点的左右子树当做两棵树
- 然后比较这两棵树是否对称
比较两棵树是否相同的主要代码:
boolean leftIsSame = isSame(root1.left, root2.left);
boolean rightIsSame = isSame(root1.right, root2.right);
比较两棵树是否对称的主要代码:
// 判断对称的外侧,即 左树的左孩子 与 右树的右孩子
boolean outFlag = symmetric(root1.left, root2.right);
// 判断对称的内侧,即 左树的右孩子 与 右树的左孩子
boolean inFlag = symmetric(root1.right, root2.left);
代码:
class Solution {public boolean isSymmetric(TreeNode root) {return symmetric(root.left, root.right);}public boolean symmetric(TreeNode root1, TreeNode root2) {if(root1 == null && root2 == null) {return true;}if(root1 == null || root2 == null) {return false;}if(root1.val != root2.val) {return false;}// 判断对称的外侧,即 左树的左孩子 与 右树的右孩子boolean outFlag = symmetric(root1.left, root2.right);// 判断对称的内侧,即 左树的右孩子 与 右树的左孩子boolean inFlag = symmetric(root1.right, root2.left);return outFlag && inFlag;}
}
根相同的两棵树root1,root2,判断root2是否是root1的子树?
该题前提:
- root1.val == root2.val
- 是否为子树?我们可理解为 “root1(大树)是否包含root2(小树)?”
分析:
大体框架和【100. 相同的树】类似,只是处理边界逻辑发生改变
// 如果匹配的树为null 则说明匹配到叶子节点了 已经匹配完了
if(subtree == null) {return true;
}
// 如果root为null 则说明大树匹配到叶子节点了 还没匹配完子树
if(root == null) {return false;
}
代码:
/*** 判断subTree是否为root的子树?*/
public boolean judge(TreeNode root, TreeNode subtree ) {// 如果匹配的树为null 则说明匹配到叶子节点了 已经匹配完了if(subtree == null) {return true;}// 如果root为null 则说明大树匹配到叶子节点了 还没匹配完子树if(root == null) {return false;}// 节点值不同,则直接返回falseif(root.val != subtree.val) {return false;}boolean leftJudge = judge(root.left, subtree.left);boolean rightJudge = judge(root.right, subtree.right);return leftJudge && rightJudge;}
剑指 Offer 26. 树的子结构
【剑指 Offer 26. 树的子结构】
分析:
解题步骤:
- 遍历大树
- 若以任意节点为根的树 包含 目标子树,则返回true
注意点:在遍历大树时,别调错递归方法了,调的是isSubStructure,而不是judge
代码:
class Solution {public boolean isSubStructure(TreeNode root1,TreeNode root2) {if(root1==null || root2==null) return false;// 若以当前节点为根的树 包含目标子树,则直接返回trueif(judge(root1, root2)) {return true;}// 下面调isSubStructure是起遍历的作用,别调错成judge了boolean leftFlag = isSubStructure(root1.left, root2);boolean rightFlag = isSubStructure(root1.right, root2);// 存在左右子树任意匹配即可return leftFlag || rightFlag;}/*** 判断subTree是否为root的子树?*/public boolean judge(TreeNode root, TreeNode subtree ) {// 如果匹配的树为null 则说明匹配到叶子节点了 已经匹配完了if(subtree == null) {return true;}// 如果root为null 则说明大树匹配到叶子节点了 还没匹配完子树if(root == null) {return false;}// 节点值不同,则直接返回falseif(root.val != subtree.val) {return false;}boolean leftJudge = judge(root.left, subtree.left);boolean rightJudge = judge(root.right, subtree.right);return leftJudge && rightJudge; }
}
226. 翻转二叉树
【226. 翻转二叉树】
分析:
- 遍历大树,并且交换左、右孩子
代码:
class Solution {public TreeNode invertTree(TreeNode root) {dfs(root);return root;}/*** 遍历大树,且交换左、右孩子*/public void dfs(TreeNode root) {if(root == null) {return;}swap(root);dfs(root.left);dfs(root.right);}/*** 交换root 的 左子树 和 右子树*/public void swap(TreeNode root) {if(root == null) {return;}TreeNode temp = root.left;root.left = root.right;root.right = temp;}
}
105. 从前序与中序遍历序列构造二叉树
【105. 从前序与中序遍历序列构造二叉树】
分析:
上面【分治算法-系列】的【剑指 Offer 07. 重建二叉树】
代码:
class Solution {int[] preorder;int[] inorder;// 存储中序遍历 对应值的下标private HashMap<Integer, Integer> inorderMap = new HashMap();public TreeNode buildTree(int[] preorder, int[] inorder) {this.preorder = preorder;this.inorder = inorder;for(int i=0; i<inorder.length; i++) {inorderMap.put(inorder[i], i);}return build(0, preorder.length-1, 0, inorder.length-1);}public TreeNode build(int left1, int right1, int left2, int right2) {if(left1>right1 || left2>right2) {return null;}int rootVal = preorder[left1];TreeNode root = new TreeNode(rootVal);int idx = inorderMap.get(rootVal);int leftTreeSize = idx - left2; // 左子树大小int rightTreeSize = right2 - idx; // 右子树大小// 构建左孩子root.left = build(left1+1, left1+leftTreeSize, left2, left2+leftTreeSize-1);// 构建右孩子root.right = build(left1+leftTreeSize+1, right1, idx+1, right2);return root;}
}
106. 从中序与后序遍历序列构造二叉树
【106. 从中序与后序遍历序列构造二叉树】
分析:
和上题类思路一致
代码:
class Solution {int[] postorder;int[] inorder;// 存储中序遍历 对应值的下标private HashMap<Integer, Integer> inorderMap = new HashMap();public TreeNode buildTree(int[] inorder, int[] postorder) {this.postorder = postorder;this.inorder = inorder;for(int i=0; i<inorder.length; i++) {inorderMap.put(inorder[i], i);}return build(0, postorder.length-1, 0, inorder.length-1);}public TreeNode build(int left1, int right1, int left2, int right2) {if(left1>right1 || left2>right2) {return null;}int rootVal = postorder[right1];TreeNode root = new TreeNode(rootVal);int idx = inorderMap.get(rootVal);int leftTreeSize = idx - left2; // 左子树大小int rightTreeSize = right2 - idx; // 右子树大小// 构建左孩子root.left = build(left1, left1+leftTreeSize-1, left2, left2+leftTreeSize-1);// 构建右孩子root.right = build(left1+leftTreeSize, right1-1, idx+1, right2);return root;}
}
117. 填充每个节点的下一个右侧节点指针 II
【117. 填充每个节点的下一个右侧节点指针 II】
分析:
使用宽度优先遍历
代码:
class Solution {public Node connect(Node root) {if(root == null) {return null;}Queue<Node> queue = new LinkedList();queue.offer(root);while(!queue.isEmpty()) {int len = queue.size();for(int i=0; i<len; i++) {Node node = queue.poll();if(node.left != null) {queue.offer(node.left);}if(node.right != null) {queue.offer(node.right);}// next指向下一个节点,但是要判断queue不为空if(!queue.isEmpty()) {node.next = queue.peek();}// 最后一个要指向nullif(i == len-1) {node.next = null;}}}return root;}
}
114. 二叉树展开为链表
【114. 二叉树展开为链表】
分析:
该方法不是时间复杂度最低的
- 前序遍历将节点放入到
list
- 遍历
list
,更新左、右子节点的指向关系
代码:
class Solution {List<TreeNode> list = new ArrayList();public void flatten(TreeNode root) {preorderDfs(root);for(int i=0; i<list.size(); i++) {TreeNode cur = list.get(i);cur.left = null;if(i == list.size()-1) {cur.right = null;} else {cur.right = list.get(i+1);}}}// 前序遍历public void preorderDfs(TreeNode root) {if(root == null) {return;}list.add(root);preorderDfs(root.left);preorderDfs(root.right);}
}
112. 路径总和
【112. 路径总和】
分析:
前面已经总结过了【JZ82 二叉树中和为某一值的路径(一)】
代码:
class Solution {public boolean hasPathSum(TreeNode root, int targetSum) {if(root == null) {return false;}return dfs(root, targetSum);}public boolean dfs(TreeNode root, int target) {if(root == null) {return false;}target -= root.val;// 叶子节点:root.left == null && root.right == nullif(root.left == null && root.right == null && target == 0) {return true;}return dfs(root.left, target) || dfs(root.right, target);}
}
129. 求根节点到叶节点数字之和
【129. 求根节点到叶节点数字之和】
分析:
该题上面已总结过:【剑指 Offer II 049. 从根节点到叶节点的路径数字之和】
代码:
class Solution {int sum = 0;int pro = 0;public int sumNumbers(TreeNode root) {dfs(root);return sum;}public void dfs(TreeNode root) {if(root == null) {return;}int curVal = root.val;pro = pro * 10 + curVal;if(root.left == null && root.right == null) {sum += pro;}dfs(root.left);dfs(root.right);pro /= 10;}
}
222. 完全二叉树的节点个数
【222. 完全二叉树的节点个数】
分析:
参考视频:【leetcode-树篇 222题 完全二叉树的节点个数】
求完全二叉树深度:
/*** 完全二叉树深度*/
public int getDepth(TreeNode root) {int level = 0;while(root != null) {level++;root = root.left;}return level;
}
代码:
其中 1 << leftDepth
: 【当前节点数:1】
+ 【左子树节点个数】
= 1
+ 2^n -1
= 2^n
class Solution {public int countNodes(TreeNode root) {if(root == null) {return 0;}// 求左子树的高度int leftDepth = getDepth(root.left);// 求右子树的高度int rightDepth = getDepth(root.right);if(leftDepth == rightDepth) { // 此时左子树肯定是完全二叉树// 1(根节点) + n^2-1(左完全二叉树节点数)+ 右子树节点数量return (1 << leftDepth) + countNodes(root.right);} else { // 此时右子树肯定是完全二叉树// 1(根节点) + n^2-1(右完全二叉树节点数)+ 右子树节点数量return (1 << rightDepth) + countNodes(root.left);}}/*** 完全二叉树深度*/public int getDepth(TreeNode root) {int level = 0;while(root != null) {level++;root = root.left;}return level;}
}
236. 二叉树的最近公共祖先
【236. 二叉树的最近公共祖先】
分析:
上面已经总结过该题:【剑指 Offer 68 - II. 二叉树的最近公共祖先】
代码:
class Solution {public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {if(root == null) {return null;}return dfs(root, p, q);}// 【后续遍历】的特点:可以把最终返回的节点 通过递归一直往上推public TreeNode dfs(TreeNode root, TreeNode p, TreeNode q) {// 当前节点为null,则肯定找不到,直接返回nullif(root == null) {return null;}// 找到了p 或者 q,则向上返回if(root == p || root == q) {return root;}// 【左】TreeNode left = dfs(root.left, p, q);// 【右】TreeNode right = dfs(root.right, p, q);// 【中】if(left != null && right != null) {// 若左、右子树都找到了,则当前节点就是最近公共节点return root;} else if (left != null && right == null) {// 左树找到了,但右树没找到,则返回left结果return left;} else if(right != null && left == null) {// 右树找到了,但左树没找到,则返回right结果return right;} else {// 都没找到,则向上返回nullreturn null;}}
}
二叉树层次遍历
199. 二叉树的右视图
【199. 二叉树的右视图】
分析:
上面已经总结过了
代码:
class Solution {public List<Integer> rightSideView(TreeNode root) {if(root == null) {return new ArrayList();}Queue<TreeNode> queue = new LinkedList();queue.offer(root);List<Integer> res = new ArrayList();while(!queue.isEmpty()) {int length = queue.size();for(int i=0; i<length; i++) {TreeNode node = queue.poll();if(node.left != null) {queue.offer(node.left);}if(node.right != null) {queue.offer(node.right);}if(i == length-1) {res.add(node.val);}}}return res;}
}
637. 二叉树的层平均值
【637. 二叉树的层平均值】
分析:
- 分层遍历(广度优先)
- 每层计算平均值
代码:
class Solution {public List<Double> averageOfLevels(TreeNode root) {List<Double> res = new ArrayList();if(root == null) {return new ArrayList();} Queue<TreeNode> queue = new LinkedList();queue.offer(root);while(!queue.isEmpty()) {int len = queue.size();double sum = 0;for(int i=0; i<len; i++) {TreeNode node = queue.poll();if(node.left != null) {queue.offer(node.left);}if(node.right != null) {queue.offer(node.right); }sum += node.val;}res.add(sum/len);}return res;}
}
102. 二叉树的层序遍历
【102. 二叉树的层序遍历】
分析:
一样的模板套路
代码:
class Solution {public List<List<Integer>> levelOrder(TreeNode root) {if(root == null) {return new ArrayList();}List<List<Integer>> res = new ArrayList();Queue<TreeNode> queue = new LinkedList();queue.offer(root);while(!queue.isEmpty()) {int len = queue.size();List<Integer> list = new ArrayList();for(int i=0; i<len; i++) {TreeNode node = queue.poll();list.add(node.val);if(node.left != null) {queue.offer(node.left);}if(node.right != null) {queue.offer(node.right);}}res.add(list);}return res;}
}
103. 二叉树的锯齿形层序遍历
【103. 二叉树的锯齿形层序遍历】
分析:
- 和上面一样的套路,不同点是插入list的方式。该题 尾插 和 头插 每一行交替操作
list
的头插法:list.add(0, node.val)
代码:
class Solution {public List<List<Integer>> zigzagLevelOrder(TreeNode root) {if(root == null) {return new ArrayList();}List<List<Integer>> res = new ArrayList();Queue<TreeNode> queue = new LinkedList();queue.offer(root);boolean flag = true;while(!queue.isEmpty()) {int len = queue.size();List<Integer> list = new ArrayList();for(int i=0; i<len; i++) {TreeNode node = queue.poll();if(flag) {list.add(node.val);} else {list.add(0, node.val);}if(node.left != null) {queue.offer(node.left);}if(node.right != null) {queue.offer(node.right);}}res.add(list);flag = !flag;}return res;}
}
数学
9. 回文数
分析:
- 数字转成字符串
- 判断字符串是否是回文串
- 双指针法
代码:
class Solution {public boolean isPalindrome(int x) {String s = String.valueOf(x);if(s.length() == 0 || s.length() == 1) {return true;}char[] chars = s.toCharArray();int left = 0;int right = chars.length - 1;while(left <= right) {if(chars[left] != chars[right]) {return false;}left++;right--;}return true;}
}
66. 加一
【66. 加一】
分析:
- 从尾到头遍历,模拟
+1
- 遍历
carry
来记录上一轮是否进位,因为该题需要+1
,因此carry
默认值为true
- 直到
+1
后当前元素 < 10
,这可直接返回当前数组 - 若远数组为
[9,9,9,9,9]
这种情况,则最终会是[0,0,0,0,0]
,需要新创建一个数组,新增一位,第一位设置1即可,最终是[1,0,0,0,0,0]
代码:
class Solution {public int[] plusOne(int[] digits) {// carry标记上一轮计算是否进位?因为该题需要我们+1,所以这里默认值我们设为trueboolean carry = true;for(int i=digits.length-1; i>=0; i--) {int curVal = digits[i];if(carry) {curVal++;}if(curVal < 10) { // 不存在进位,则直接返回结果digits[i] = curVal;return digits;} else { // 因为是加一,所有进位之后肯定是10,因此当前位为0carry = true;digits[i] = 0;}}if(digits[0] == 0) { // 这种情况肯定是 原数组都是9,例如:99999,加一后100000,但数组中只存储00000int[] newDigits = new int[digits.length+1];newDigits[0] = 1; // 此时newDigits -> 100000return newDigits;}return digits;}
}
172. 阶乘后的零
【172. 阶乘后的零】
分析:
参考视频:【【LeetCode 每日一题】172. 阶乘后的零 | 手写图解版思路 + 代码讲解】
该题其实就是求 [1-n]个数,一共存在多少“因子5”?
- 每
5^1
出现1
个5
,每5^2
出现2
个5
,每5^3
出现3
个5
,以此类推 - 则
count = n/5 + n/5^2 + n/5^3 + ....
代码:
class Solution {public int trailingZeroes(int n) {int count = 0;while(n > 0) {count += n/5;n /= 5;}return count;}
}
其中n /= 5
:分子除以5,等价于分母乘以5
69. x 的平方根
【69. x 的平方根 】
分析:
首先 x^0.5 <= x
(肯定的)
所以该题可以看做 从[0,1,2,3...x]
中,找到 n*n <= x
的最大值
[0,1,2,3...x]
是个有序序列,因此我们可以使用二分法求解
在判断mid*mid
是否是 小于 x
的最大值时,我们可以这样写:
if(mid < x/mid) { // 等价于 mid*mid < x// 【关键判断】:若 mid+1 > x/(mid+1),则mid*mid 肯定是小于 x 的最大值if(mid+1 > x/(mid+1)) { // 等价于 (mid+1)*(mid+1) > x return mid;}left = mid + 1;
}
注意点:
x的范围 0 <= x <= 2^31 - 1
错误写法:n*n <= x
(肯定会超出int
或者long
的范围)
正确写法:n <= x/n
(分子、分母同时除以n,n*n <= x
等价于 n <= x/n
)
代码:
class Solution {public int mySqrt(int x) {if(x == 0) {return 0;}if(x == 1) {return 1;}return binarySearch(x);}/*** 目标: n*n <= x 的 最大值*/public int binarySearch(int x) {// 可以看做从[0,1,2,3...x] 这些元素中查找目标元素int left = 0;int right = x;while(left <= right) {int mid = (left + right) / 2;if(mid == x/mid) { // 等价于 mid*mid == xreturn mid;}if(mid < x/mid) { // 等价于 mid*mid < x// 【关键判断】:若 mid+1 > x/(mid+1),则mid*mid 肯定是小于 x 的最大值if(mid+1 > x/(mid+1)) { // 等价于 (mid+1)*(mid+1) > x return mid;}left = mid + 1;} else {right = mid - 1;}}return -1;}
}
50. Pow(x, n)
【50. Pow(x, n)】
分析:
上面已经总结过了 【剑指 Offer 16. 数值的整数次方】
错误写法:(这种会超时)
if(n % 2 == 0) {return myPow(x, n/2) * myPow(x, n/2);
} else {return myPow(x, n-1) * x;
}
正确写法:(不会超时,但和上面写法是一个思路)
if(n % 2 == 0) {return myPow(x * x, n/2);
} else {return myPow(x, n-1) * x;
}
代码:
class Solution {public double myPow(double x, int n) {if(n == 0) {return 1;}if(n == 1) {return x;}if(n == -1) {return 1 / x;}if(n % 2 == 0) {return myPow(x * x, n/2);} else {return myPow(x, n-1) * x;}}
}
二分查找
总结注意点
【注意点一】:若[left,right]
对应 [0,length-1]
,当代码中需要mid与相邻元素比较时,要和nums[mid+1]
比较,如果和num[mid-1]
比较,则nums[mid-1]
可能会下标越界
- 若数组只有一个元素,其实
nums[mid+1]
也会越界。因此只有一个元素时要单独当做边界条件处理 - 若数组只有两个元素,则
mid = (0+1)/2 = 0
,那么nums[mid-1]
就会越界,nums[mid+1]
则不会越界
35. 搜索插入位置
【35. 搜索插入位置】
分析:
该题是个模板题
/*** 二分查找*/
public int binarySearch(int[] nums, int target) {int left = 0;int right = nums.length - 1;while(left <= right) {int mid = (left + right) >> 1; // 等价于 (left + right) / 2if(nums[mid] == target) {return mid;}if(nums[mid] < target) {left = mid + 1;} else {right = mid - 1;}}return left;
}
代码:
class Solution {public int searchInsert(int[] nums, int target) {return binarySearch(nums, target);}/*** 二分查找*/public int binarySearch(int[] nums, int target) {int left = 0;int right = nums.length - 1;while(left <= right) {int mid = (left + right) >> 1; // 等价于 (left + right) / 2if(nums[mid] == target) {return mid;}if(nums[mid] < target) {left = mid + 1;} else {right = mid - 1;}}return left;}
}
69. x 的平方根
【69. x 的平方根 】
分析:
首先 x^0.5 <= x
(肯定的)
所以该题可以看做 从[0,1,2,3...x]
中,找到 n*n <= x
的最大值
[0,1,2,3...x]
是个有序序列,因此我们可以使用二分法求解
在判断mid*mid
是否是 小于 x
的最大值时,我们可以这样写:
if(mid < x/mid) { // 等价于 mid*mid < x// 【关键判断】:若 mid+1 > x/(mid+1),则mid*mid 肯定是小于 x 的最大值if(mid+1 > x/(mid+1)) { // 等价于 (mid+1)*(mid+1) > x return mid;}left = mid + 1;
}
注意点:
x的范围 0 <= x <= 2^31 - 1
错误写法:n*n <= x
(肯定会超出int
或者long
的范围)
正确写法:n <= x/n
(分子、分母同时除以n,n*n <= x
等价于 n <= x/n
)
代码:
class Solution {public int mySqrt(int x) {if(x == 0) {return 0;}if(x == 1) {return 1;}return binarySearch(x);}/*** 目标: n*n <= x 的 最大值*/public int binarySearch(int x) {// 可以看做从[0,1,2,3...x] 这些元素中查找目标元素int left = 0;int right = x;while(left <= right) {int mid = (left + right) / 2;if(mid == x/mid) { // 等价于 mid*mid == xreturn mid;}if(mid < x/mid) { // 等价于 mid*mid < x// 【关键判断】:若 mid+1 > x/(mid+1),则mid*mid 肯定是小于 x 的最大值if(mid+1 > x/(mid+1)) { // 等价于 (mid+1)*(mid+1) > x return mid;}left = mid + 1;} else {right = mid - 1;}}return -1;}
}
74. 搜索二维矩阵
【74. 搜索二维矩阵】
分析:
首先题中说明:
每一行都按照从左到右递增的顺序排序
每一列都按照从上到下递增的顺序排序。
那么一看数据是有序的, 那么我们肯定第一时间想到二分查找法。但在着整个二维数组中好像没法直接使用二分查找,但是我们可以使用二分查找的思想。
二分查找思想: 每一轮比较首先获取一个特殊值
,然后让目标值与该值进行比较,每次比较都能排除一些数据进而缩小搜索的范围。
解决该题我们用的方法和二叉查找法类似,也是每次都取一个特殊值与目标值比较,每轮都排除一部分数据进而缩小数据的查找范围
B站上讲解视频:
https://www.bilibili.com/video/BV12J411i7A6
https://www.bilibili.com/video/BV1Tt411F7YD?spm_id_from=333.999.0.0
代码:
class Solution {public boolean searchMatrix(int[][] array, int target) {// 套路模板 先判空if(array.length == 0) return false;// row表示有几行,col表示有几列。int row = array.length;int col = array[0].length;// 我们取右上角的值int x = 0; int y = col - 1;// 不断排除一列或者一行,不断缩小范围while(x <= row-1 && y >= 0) {if(target > array[x][y]) {// 排除头一行的数据x++;}else if (target < array[x][y]) {// 排除后一列的数据y--;}else {return true;}}// 若判处完所有数据仍没有找目标值 该值不存在return false;}
}
162. 寻找峰值
【162. 寻找峰值】
分析:
参考视频:【【LeetCode 每日一题】162. 寻找峰值 | 手写图解版思路 + 代码讲解】
- 较大的值则有可能是峰值,因此保留,并缩小区间
- 较小的值则不可能为峰值,因此舍弃,并缩小区间
代码:
class Solution {public int findPeakElement(int[] nums) {if(nums.length == 1) {return 0;}int left = 0;int right = nums.length - 1;while(left < right) {int mid = (left + right) >> 1;if(nums[mid] > nums[mid+1]) {right = mid;} else {left = mid + 1;}}return left;}
}
JZ11 旋转数组的最小数字(存在重复值)
【JZ11 旋转数组的最小数字】
分析:
参考视频:【剑指Offer.6.旋转数组的最小数字】
序列可被分为两部分递增序列, 第一部分的最小值
>= 第二部分的最最大值
我们每次都取中间下标的值与左右侧的值进行比较
若 min下标的值
> 最右侧的值
,则说明 min下标处于第一部分,而最小值在第二部分的范围内,因此我们就可以排除 min及左侧的所有数据了,最小值在 [min+1,right]
的范围内
若 min下标的值
< 最右侧的值
, 则说明min下标所处第二部分,那么最小值在 [left,min]
的范围内
若min下标的值
== 最右侧的值
,而对于这种情况,又一种特殊情况,如下图:
直接说结论,若 min下标的值
== 最右侧的值
, 则直接 right--
即可,右指针后移
while循环的条件是 left < right,当left == right时,其实就只有一个数据了,该数据就是最小,最终直接返回array[left] 或者 array[right]
代码:
import java.util.ArrayList;
public class Solution {public int minNumberInRotateArray(int [] array) {// 套路模板,先判空if(array.length == 0) return -1;return erfen(array, 0, array.length-1);}public int erfen(int[] array, int left, int right) {int min = 0;while(left < right) { // 因为当left==right时,其实就是我们要找的目标值,因此这里条件是left<rightmin = (left + right) >> 1; // 等同于(left+right)/2// 因为没有目标值,因此每次都是 中间下标值 与 最右侧的值进行比较if(array[min] > array[right]) { // 此情况说明min处于 第一部分的范围left = min + 1;}else if(array[min] < array[right]) { // 此情况说明min处于 第二部分范围 因为可能是最小值 因此right不再是min+1right = min;}else { // 若min 等于 最右侧的值, 直接right往左移一步即可,right--right--;}}// 最后 left=right 就是我们要找的最小值return array[left];}
}
153. 寻找旋转排序数组中的最小值(不包含重复值)
【153. 寻找旋转排序数组中的最小值】
分析:
与上题不同,该题数组元素是不重复的,因此判断逻辑中不需要判断 nums[mid] == nums[right]
这种情况
代码:
class Solution {public int findMin(int[] nums) {int left = 0;int right = nums.length - 1;while(left < right) {int mid = (left + right) >> 1;if(nums[mid] > nums[right]) {left = mid + 1;} else {right = mid;}}return nums[left];}
}
33. 搜索旋转排序数组
【33. 搜索旋转排序数组】
分析:
* 假设turningPointIdx为转折点下标:此时我们知道这两段递增序列范围(若数组本身就没有翻转,则还是一个段[0, nums.length-1]),分别是:
* 第一段:[0,turningPointIdx-1]
* 第二段:[turningPointIdx, nums.length-1]
* "第一段元素" > "第二段元素"
一共分三步:
- 找到转折点下标
- 判断
target
所在的标有序区间turningPointIdx == 0
,说明nums
本身就是有序的,并没有翻转。则target
还在[0, nums.length-1]
内target >= nums[0]
,说明target
在[0,turningPointIdx-1]
target < nums[0]
,说明target
在[turningPointIdx, nums.length-1]
- 在有序区间内进行二分查找
代码:
class Solution {/*** 假设turningPointIdx为转折点下标:此时我们知道这两段递增序列范围(若数组本身就没有翻转,则还是一个段[0, nums.length-1]),分别是:* 第一段:[0,turningPointIdx-1]* 第二段:[turningPointIdx, nums.length-1]* "第一段元素" > "第二段元素"*/public int search(int[] nums, int target) {// 【一、找到转折点下标】int turningPointIdx = getTurningPoint(nums);// 【二、target与nums[0]比较,判断target在 第一段 还是 第二段?】// turningPointIdx == 0,说明nums本身就是有序的,并没有翻转int left, right;if(turningPointIdx == 0) { left = 0;right = nums.length - 1;} else if (target >= nums[0]) {// target在第一段left = 0;right = turningPointIdx - 1;} else {// target在第二段left = turningPointIdx;right = nums.length - 1;}// 【三、在有序范围内进行二分查找】return binarySearch(nums, left, right, target);}/*** 寻找旋转点的下标(就是找到转折数组的最小值)* 若没有旋转(本身是有序的),则最终返回的下标肯定是0*/public int getTurningPoint(int[] nums) {int left = 0;int right = nums.length - 1;while(left < right) {int mid = (left + right) >> 1;if(nums[mid] > nums[right]) {left = mid + 1;} else {right = mid;}}return left;}/*** 在有序数组中进行二分查找*/public int binarySearch(int[] nums, int left, int right, int target) {while(left <= right) {int mid = (left + right) >> 1;if(nums[mid] == target) {return mid;}if(nums[mid] > target) {right = mid - 1;} else {left = mid + 1;}}return -1;}
}
一维动态规划
70. 爬楼梯
【70. 爬楼梯】
分析:
想要跳到第n
阶台阶,只有两种情况:(因为每次你只能爬 1 或 2 个台阶)
- 从第
n-1
阶跳 - 从第
n-2
阶跳
因此 跳到第n阶方法 = 跳到n-1阶方法 + 跳到n-2阶方法
代码:
class Solution {public int climbStairs(int n) {if(n == 1) {return 1;}if(n == 2) {return 2;}int[] dp = new int[n];dp[0] = 1;dp[1] = 2;for(int i=2; i<n; i++) {dp[i] = dp[i-1] + dp[i-2];}return dp[n-1];}
}
198. 打家劫舍
【198. 打家劫舍】
分析:
参考视频:【动态规划,偷不偷这个房间呢?| LeetCode:198.打家劫舍】
dp[n]定义为:考虑第n
间房,所能偷的最大值 【但不一定要偷第n
间,只是考虑到第n
间】(n从0开始)
- 对于第i间来说,有两种选择:1.偷第i间、2.不偷第i间
- 1、偷第
i
间房:则不能偷第i-1
间房屋,也就不能考虑第n-1
间房。则此时value = nums[i] + dp[i-2]
- 2、不偷第
i
间房:则可以考虑偷第i-1
间。则此时value = dp[i-1]
- 1、偷第
- 最终取两种情况的最大值,因此
dp[i] = Math.max(nums[i] + dp[i-2], dp[i-1])
初始化dp:
若只有一间房子肯定要偷第一间,因此dp[0] = nums[0]
有两个房间,不能两个都偷,因此偷最大值,因此 dp[1] = Math.max(nums[0], nums[1])
代码:
class Solution {public int rob(int[] nums) {int len = nums.length;// 只有一个,则肯定偷这一个if(len == 1) {return nums[0];}// 有两个,则偷最大值if(len == 2) {return Math.max(nums[0], nums[1]);}// dp[n]定义为:考虑第n间房,所能偷的最大值 【但不一定要偷第n间,只是考虑到第n间】(n从0开始)int[] dp = new int[len];dp[0] = nums[0];dp[1] = Math.max(nums[0], nums[1]);for(int i=2; i<len; i++) {/*** 有两种选择:1.偷第i间、2.不偷第i间* 1、偷第i间房:则不能偷第i-1间房屋,也就不能考虑第n-1间房。则此时 value = nums[i] + dp[i-2]* 2、不偷第i间房:则可以考虑偷第i-1间。则此时 value = dp[i-1]* 最终取两种情况的最大值,因此 dp[i] = Math.max(nums[i] + dp[i-2], dp[i-1])*/dp[i] = Math.max(nums[i]+dp[i-2], dp[i-1]);}return dp[len-1];}
}
139. 单词拆分
【139. 单词拆分】
分析:
参考视频:【动态规划之完全背包,你的背包如何装满?| LeetCode:139.单词拆分】
代码:
class Solution {public boolean wordBreak(String s, List<String> wordDict) {int len = s.length();// dp[n]定义为:前n个字符组成的字符串是否能被wordDict拼接成功(从1开始)boolean[] dp = new boolean[len + 1];// dp[0]初始为true(没什么特殊含义)dp[0] = true;for(int i=1; i<=len; i++) {for(int j=0; j<i; j++) {// 这个wordDict.contains(s.substring(j, i)) 恰好是第j个字符到i组成的字符串if(dp[j] && wordDict.contains(s.substring(j, i))) {dp[i] = true;}}}return dp[len];}
}
322. 零钱兑换
【322. 零钱兑换】
分析:
参考视频:【腾讯二面笔试题~零钱兑换】
若【爬楼梯】的题改成:
每次可爬coins[0]、coins[1] … coins[length-1]个台阶,问最少跳多少次能跳到amount?
其实就和这题一样了
若这题改成:
求兑换零钱的方法总数
其实就变成了【爬楼梯】的进阶版了
定义dp[n]
为:兑换n
元钱,最少需要的硬币数
dp[n]
的状态 来源于 dp[n-coins[i]]
,因此
dp[n] = min {dp[n] = dp[n - coins[0]] + 1dp[n] = dp[n - coins[1]] + 1dp[n] = dp[n - coins[2]] + 1...dp[n] = dp[n - coins[length-1]] + 1
}
有点类似于【剑指 Offer 60. n个骰子的点数】这题
代码:
class Solution {public int coinChange(int[] coins, int amount) {if(amount == 0) {return 0;}/*** 零钱最小值是1,因此amount最多换amount个硬币。* 为了考虑换不到零钱的情况,我们这里初始值设置为amount+1。最后dp[n] 和 amount+1比较,若仍等于amount+1,则说明n换不到零钱,最终返回-1*/ // dp[n]定义:兑换amount最少的兑换次数int[] dp = new int[amount + 1];Arrays.fill(dp, amount + 1);// 当amount为0时,则兑换方法为0dp[0] = 0;for(int i=1; i<=amount; i++) {for(int j=0; j<coins.length; j++) {if(coins[j] == i) {dp[i] = Math.min(1, dp[i]);}// 若零钱 大于 总钱数, 否则不能兑换。例如总钱数是1,而只有2元、5元的零钱,那么肯定没法兑换if(coins[j] > i) {continue;}int remainingVal = i - coins[j];dp[i] = Math.min(dp[remainingVal] + 1, dp[i]);}}return dp[amount] == amount + 1 ? -1 : dp[amount];}
}
300. 最长递增子序列
【300. 最长递增子序列】
分析:
参考视频:https://www.bilibili.com/video/BV19b4y1R7K3?spm_id_from=333.1007.top_right_bar_window_custom_collection.content.click
代码:
class Solution {public int lengthOfLIS(int[] nums) {if(nums == null || nums.length == 0) return 0;// 用于记录返回值int res = 1;// dp数组的含义: 以nums[i]结尾的最长递增子序列的长度(必须包含nums[i])int[] dp = new int[nums.length];// 为dp数组填充1Arrays.fill(dp, 1);for(int i=0; i<nums.length; i++) { // 外层循环填dp[i]的值for(int j=0; j<i; j++) { // 内层循环 遍历nums[i]前面的元素// 如果前面的值小于当前值 才能满足升序if(nums[j] < nums[i]) {dp[i] = Math.max(dp[i], dp[j]+1);}}// 去dp中的最大值最为res结果res = Math.max(dp[i], res);}return res;}
}
多维动态规划
JZ47 礼物的最大价值
【JZ47 礼物的最大价值】
代码:
import java.util.*;public class Solution {/*** 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可*** @param grid int整型二维数组* @return int整型*/public int maxValue (int[][] grid) {int m = grid.length;int n = grid[0].length;int[][] dp = new int[m][n];// 初始化dp[0][0] 为 grid[0][0]dp[0][0] = grid[0][0];// 初始化第一行for (int i = 1; i < n; i++) {dp[0][i] = dp[0][i-1] + grid[0][i];}// 初始化第一列for (int i = 1; i < m; i++) {dp[i][0] = dp[i-1][0] + grid[i][0];}for(int i=1; i<m; i++) {for(int j=1; j<n; j++) {dp[i][j] = Math.max(dp[i][j-1], dp[i-1][j]) + grid[i][j];}}return dp[m-1][n-1];}
}
120. 三角形最小路径和
【120. 三角形最小路径和】
分析:
这种题需要找一个终点
,一般都是将终点当最终的结果。因为是三角形状,我们最好想的就是,自底向上
把顶点
当做终点
与 【剑指 Offer 47. 礼物的最大价值】非常类似,只不过这里求得是最小值,而且走的方式不太一样
代码:
class Solution {public int minimumTotal(List<List<Integer>> triangle) {int row = triangle.size();// 该图形是三角形,不是方形,因此这里高度别初始化错了,错误写法:col = triangle.get(0).size() 正确写法:col = rowint col = row;int[][] dp = new int[row][col];// 初始化最底部for(int i=0; i<col; i++) {dp[row-1][i] = triangle.get(row-1).get(i);}// 自顶向上,终点是triangle[0][0]for(int i=row-2; i>=0; i--) {for(int j=0; j<=i; j++) {dp[i][j] = Math.min(dp[i+1][j], dp[i+1][j+1]) + triangle.get(i).get(j);}}return dp[0][0];}
}
64. 最小路径和
【64. 最小路径和】
分析:
和上一题一模一样,只不过这题要求的是最小值
代码:
class Solution {public int minPathSum(int[][] grid) {int m = grid.length;int n = grid[0].length;int[][] dp = new int[m][n];// 起点初始化为 grid[0][0]dp[0][0] = grid[0][0];// 初始化第一行for(int i=1; i<n; i++) {dp[0][i] = dp[0][i-1] + grid[0][i];}// 初始化第一列for(int i=1; i<m; i++) {dp[i][0] = dp[i-1][0] + grid[i][0];}for(int i=1; i<m; i++) {for(int j=1; j<n; j++) {dp[i][j] = Math.min(dp[i][j-1], dp[i-1][j]) + grid[i][j];}}return dp[m-1][n-1];}
}
63. 不同路径 II
【63. 不同路径 II】
分析:
有点像【爬台阶】,到达某点的路径
= 到达上面的路径
+ 到达左边的路径
不同点:
- 该题是二维的
- 存在障碍物,若某点是障碍物则到达该点的路径为0
- 初始化边界时要注意,若中间遇到障碍物,则后序的点不可能再被到达,因此dp为0
代码:
class Solution {public int uniquePathsWithObstacles(int[][] obstacleGrid) {int m = obstacleGrid.length;int n = obstacleGrid[0].length;int[][] dp = new int[m][n];/*** 初始化第一行。* 只能 → 从左到右,因此如果中遇见障碍物,后面就走不到了,因此obstacleGrid[0][i]==0写在for循环的条件中,若不满足则终止for循环* 那么后面的则不能被设置为1,最终就是0,表示到达该点的路径为0*/for(int i=0; i<n && obstacleGrid[0][i]==0; i++) {// 等于0,则说明不是障碍物,可以走dp[0][i] = 1;}// 初始化第一列。只能↓ 从上到下,同上for(int i=0; i<m && obstacleGrid[i][0]==0; i++) {// 等于0,则说明不是障碍物,可以走dp[i][0] = 1;}for(int i=1; i<m; i++) {for(int j=1; j<n; j++) {// 只有不是障碍物才能到达该点if(obstacleGrid[i][j] == 0) {dp[i][j] = dp[i][j-1] + dp[i-1][j];}}}return dp[m-1][n-1];}
}
初始化边界时的错误写法
:
// 初始化第一行
for(int i=0; i<n; i++) {// 等于0,则说明不是障碍物,可以走if(obstacleGrid[0][i] == 0) {dp[0][i] = 1;}
}
// 初始化第一列
for(int i=0; i<n; i++) {// 等于0,则说明不是障碍物,可以走if(obstacleGrid[i][0] == 0) {dp[i][0] = 1;}
}
5. 最长回文子串
【最长回文子串】
解法一:暴力 - 遍历所有字串
建议先做第9题【9. 回文数】
代码:
class Solution {public String longestPalindrome(String s) {String longestPalindrome = "";// 遍历所有的子串,若长度比longestPalindrome长 且 是回文串,则更新longestPalindromefor(int i=0; i<s.length(); i++) {for(int j=i; j<s.length(); j++) {if(j - i + 1 > longestPalindrome.length()) {if(isPalindrome(s.substring(i, j+1))) {longestPalindrome = s.substring(i, j+1);}}}}return longestPalindrome;}// 判断是否是回文字符串public Boolean isPalindrome(String s) {// 双指针int left = 0;int right = s.length() - 1;while (left <= right) {if(s.charAt(left) != s.charAt(right)) {return false;}left ++;right --;}return true;}
}
总结注意点:
- 第一步:先写出 判断是否回文 的方法
isPalindrome
。 (利用双指针的思想) - 第二步:暴力遍历每个子字符串,判断是否回文,更长的则更新
时间复杂度: n^3
解法二:中心扩展法
参考讲解视频:https://www.bilibili.com/video/BV1dN4y1g7p9/?spm_id_from=333.337.search-card.all.click&vd_source=bf2066b8675548fac384ffe3bc83793e
代码:
class Solution {// 维护最长回文字符串public String res;public String longestPalindrome(String s) {// 判空处理if(null == s && s.length() == 0) {return "";}// 初始化res res = s.substring(0,1);// 遍历s字符串 for(int i=0; i<s.length(); i++) {// 考虑 bab 的格式extendFromCenter(s, i, i);// 考虑 baab 的格式extendFromCenter(s, i, i+1);}return res;}// 中心扩散public void extendFromCenter(String s, int left, int right) {// 当 left、right 在区间返回内,且 s[left] == s[right] 才会向外扩散while(left>=0 && right<s.length() && s.charAt(left)==s.charAt(right)) {// 向外扩散left--;right++;}// 若当前回文串 比 res长 则更新resif(right - left - 1 > res.length()) {// 因为最后一次循环 left-- right++了,因此实际上回文字符串是[left+1, right-1],由于substring是左闭右开[),因此 res = s.substring(left+1, right)res = s.substring(left+1, right);}}
}
总结注意点:
- 解法一:判断字串是否是回文串,思想是
自外向内
的,left ++; right --;
- 解法二:寻找属于回文串的字串,且思想是
自内向外扩散
的,left--; right++;
- 要考虑
bab
、baab
两种回文串的格式, 因此在遍历到某个字符时,extendFromCenter(s, i, i);extendFromCenter(s, i, i+1);
要调两次extendFromCenter
时间复杂度:n^2
买卖股票系列
参考【代码随想录】系列课程:
【动态规划之 LeetCode:121.买卖股票的最佳时机1】
【动态规划,股票问题第二弹 | LeetCode:122.买卖股票的最佳时机II】
【动态规划,股票至多买卖两次,怎么求? | LeetCode:123.买卖股票最佳时机III】
【动态规划来决定最佳时机,至多可以买卖K次!| LeetCode:188.买卖股票最佳时】
参考题解:【一套模板,几行代码,闭着眼睛轻松默写所有彩票题】
121. 买卖股票的最佳时机
【121. 买卖股票的最佳时机】
分析:
前面总结过这题,之前的做法是用两个变量,分别维护 股票最低点 以及 最大价值
今天我们用动态规划的方法来解这道题:
定义dp方程:
dp
的值表示,当前口袋里有多少钱,默认是没钱的,为0
dp[i][0]
:第i
天,持有一个股票
dp[i][1]
= 第i
天,不持有股票
dp[i][0]
可以从下面两个状态转移过来:
- 可能是前一天就持有,因此是
dp[i-1][0]
- 前天不持有,然后今天才买的,因此是
0 - prices[i]
- 二者求最大值,因此
dp[i][0] = Math.max(dp[i-1][0], 0 - prices[i])
dp[i][1]
可以从下面两个状态转移过来:
- 可能是前一天就不持有,因此是
dp[i-1][1]
- 也有可能前天持有,然后今天才太卖了,因此是
dp[i][0] + prices[i]
- 二者求最大值,因此
dp[i][1] = Math.max(dp[i-1][1], dp[i][0] + prices[i])
初始化:
dp[0][0] = -prices[0]
:(第一天就持有,说明第一天就买了,因此是 0 -prices[0] = -prices[0])dp[0][1] = 0
:(第一天不持有,说明第一天没买,因此兜里钱还是0)
代码:
class Solution {public int maxProfit(int[] prices) {/*** dp 的值表示,收益多少钱?(原始是0)* dp[i][0] = 持有一个股票* 1.1、 可能是前一天就持有* 1.2、 前天不持有,然后今天才买的* dp[i][1] = 不持有股票* 2.2、可能是前一天就不持有* 2.2、也有可能前天持有,然后今天才太卖了)*/int len = prices.length;int[][] dp = new int[len][2];// 初始化dpdp[0][0] = -prices[0]; // 因为要买股票,所以要花钱,0 - prices[0] = -prices[0]dp[0][1] = 0; // 不持有说明今天没买,因此没花钱for(int i=1; i<prices.length; i++) {// 1.1. 从 “前一天就持有” 的状态推过来的// 1.2. 从当天才买股票推出来的,因为只能买一次,这次买口袋肯定是0元,因此是0-prices[i](因为是花钱所以是 -prices[i])dp[i][0] = Math.max(dp[i-1][0], 0 - prices[i]); // 2.1. 从 “前一天就不持有” 的状态推出来的// 2.2. 从 “前天持有,今天才卖了” 的状态推出来的dp[i][1] = Math.max(dp[i-1][1], dp[i][0] + prices[i]);}// 最后一天股票肯定是不持有状态(已经被卖过了),这样才会有收益,否则就亏钱了return dp[len-1][1];}
}
122. 买卖股票的最佳时机 II
【122. 买卖股票的最佳时机 II】
分析:
与上一题唯一的不同在于 dp[i][0]
的状态转移:
上一题:dp[i][0] = Math.max(dp[i-1][0], 0 - prices[i])
这一题:dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] - prices[i])
上一题:因为只能买一次,所以dp[i-1][1] - prices[i]
-> 其中的dp[i-1][1]
(不持有状态)肯定为0,因此就是0 - prices[i]
这一题:因为可以买卖多次,所以dp[i-1][1] - prices[i]
-> 其中的dp[i-1][1]
可能之前就已经有收益了,因此就是dp[i-1][1] - prices[i]
代码:
class Solution {public int maxProfit(int[] prices) {int len = prices.length;int[][] dp = new int[len][2];dp[0][0] = -prices[0];dp[0][1] = 0;for(int i=1; i<len; i++) {dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] - prices[i]);dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i]);}return dp[len-1][1];}
}
123. 买卖股票的最佳时机 III
【123. 买卖股票的最佳时机 III】
分析:
这次定义四个状态:
- dp[i][0]:第i天,持有第1只股票
- dp[i][1]:第i天,不持有第1只股票
- dp[i][2]: 第i天,持有第2只股票
- dp[i][3]: 第i天,不持有第2只股票
dp[i][0]
可有dp[i-1][0]
和0 - prices[i]
推出来,最后求最大值Math.max(dp[i-1][0], 0 - prices[i])
dp[i][1]
可有dp[i-1][1]
和dp[i-1][0] + prices[i]
推出来,最后求最大值Math.max(dp[i-1][1], dp[i-1][0] + prices[i])
dp[i][2]
可有dp[i-1][2]
和dp[i-1][1] - prices[i]
推出来,最后求最大值Math.max(dp[i-1][2], dp[i-1][1] - prices[i])
dp[i][3]
可有dp[i-1][3]
和dp[i-1][2] + prices[i]
推出来,最后求最大值Math.max(dp[i-1][3], dp[i-1][2] + prices[i])
状态转移方程的推导其实和前两题的推导都是一个方法,细细品味吧(#^.^#)
代码:
class Solution {public int maxProfit(int[] prices) {int len = prices.length;int[][] dp = new int[len][4];/*** dp[i][0]:第i天,持有第1只股票* dp[i][1]:第i天,不持有第1只股票* dp[i][2]: 第i天,持有第2只股票* dp[i][3]: 第i天,不持有第2只股票*/dp[0][0] = -prices[0];dp[0][1] = 0;dp[0][2] = -prices[0];dp[0][3] = 0;for(int i=1; i<len; i++) {dp[i][0] = Math.max(dp[i-1][0], 0 - prices[i]);dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i]);dp[i][2] = Math.max(dp[i-1][2], dp[i-1][1] - prices[i]);dp[i][3] = Math.max(dp[i-1][3], dp[i-1][2] + prices[i]);}return dp[len-1][3];}
}
188. 买卖股票的最佳时机 IV
【188. 买卖股票的最佳时机 IV】
分析:
其实这题是对上一题的一个通用性的总结(升华版)
看代码你就就知道了
代码:
class Solution {public int maxProfit(int k, int[] prices) {int len = prices.length;int[][] dp = new int[len][2*k];// 初始化dpfor(int i=0; i<2*k; i++) {dp[0][i] = (i % 2 == 0 ? -prices[0] : 0);}for(int i=1; i<len; i++) {for(int j=0; j<k; j++) {dp[i][j*2] = Math.max(dp[i-1][j*2], (j == 0 ? 0 : dp[i-1][j*2-1]) - prices[i]);dp[i][j*2+1] = Math.max(dp[i-1][j*2+1], dp[i-1][j*2] + prices[i]);}}return dp[len-1][2*k-1];}
}
回溯
回溯模板
void backtracking(参数) {if (终止条件) {存放结果;return;}for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {处理节点;backtracking(路径,选择列表); // 递归回溯,撤销处理结果}
}
常规回溯题 与 二叉树回溯题的差别
二叉树的回溯整体上确实是回溯的思想,大体思路也差不多。但在处理细节上还是有些不同的,例如【剑指 Offer 34. 二叉树中和为某一值的路径】与【39. 组合总和】相比:
- 二叉树要从根节点处理(每次递归中处理的是当前节点);而常规回溯题一般不处理根节点(每次递归中处理的都是孩子节点,而且是多孩子);因此在处理细节上是有差异的
- 二叉树递归代码:
public void backtracking(TreeNode root, int target, List<Integer> path) {if(root == null) {return;}// 处理当前节点target -= root.val;path.add(root.val);if(target == 0 && root.left == null && root.right == null) {res.add(new ArrayList(path));}// 处理左右孩子节点backtracking(root.left, target, path);backtracking(root.right, target, path);// 状态重置target += root.val;path.remove(path.size() - 1);
}
- 常规回溯递归代码:
public void backtracking(List<Integer> path, int startIndex, int[] candidates, int target) {// 终止条件if(target == 0) {res.add(new ArrayList(path));return;}// for循环处理孩子节点for(int i=startIndex; i<candidates.length; i++) {// 剪枝:因为我们事先已经为数组排好序了,越往后数字越大,如果当前数字都已经减到负数了,那后面的就没必要在判断了if(target - candidates[i] < 0) {break;}target -= candidates[i];path.add(candidates[i]);backtracking(path, i, candidates, target);// 状态重置target += candidates[i];path.remove(path.size() - 1); }
}
17. 电话号码的字母组合
【17. 电话号码的字母组合】
分析:
套用上面的模板即可,不细讲了
// 删除最后一个字符
sb.deleteCharAt(sb.length() - 1)
代码:
class Solution {private List<String> res = new ArrayList();private String[] letters = {" ","*","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};public List<String> letterCombinations(String digits) {if (digits.length() == 0) {return new ArrayList();}StringBuilder path = new StringBuilder();backtracking(path, 0, digits);return res;}public void backtracking(StringBuilder path, int level, String digits) {// 不合法(终止条件)if(path.length() == digits.length()) { // 此处也可以写成 level == digits.length() 来做为终止条件res.add(path.toString()); // 添加到结果集return;}int index = digits.charAt(level) - '0';char[] chars = letters[index].toCharArray();// 因为每层的可选集都是独立的,因此for循环都可以从0开始(不会重复)for(int i=0; i<chars.length; i++) {path.append(chars[i]);backtracking(path, level + 1, digits);path.deleteCharAt(path.length() - 1); // 状态重置}}
}
77. 组合
【77. 组合】
分析:
参考视频:【带你学透回溯算法-组合问题(对应力扣题目:77.组合)| 回溯法精讲!】
可以将这题与上题对比来看
相同点:
- 都是组合问题,路径组合不能重复(例如:234 和 324就是重复的)
不同点:
- 上一题,每层的可选集都不一样。例如,输入“23”,第一层的可选集是“a、b、c”,第二层的可选集是“d、e、f”,因此我们在遍历每层的可选集时,都可以从下标0开始遍历(因为是独立的数据集,所以不会重复)
- 而该题,每层的可选集都是一样(都是[1,n]),但是我们要保证路径元素不能重复,所以在处理for循环时就要注意。下面是模拟回溯的多叉树状态,从左往右、从上到下 可选集的返回都会缩小1。这时我们使用
startIndex
来记录下一层搜索的起始位置(for循环的初始下标)
核心代码如下:
for(int i=startIndex; i<=n; i++)
:从startIndex
开始遍历,保证路径组合不会重复
backtracking(temp, i + 1, n, k)
:使用i+1
,保证组合中的元素不会重复
// 从左往右看 因为i本身会自增,且递归函数为backtracking(temp, i + 1, n, k),参数2就是startIndex,因此越往右可选范围越小
// 越往下也是一样,调用backtracking(temp, i + 1, n, k),越往下可选集范围越小
for(int i=startIndex; i<=n; i++) {temp.add(i);backtracking(temp, i + 1, n, k); // 核心temp.remove(temp.size() - 1);
}
回溯的过程的多叉树:
代码:
class Solution {// 结果集private List<List<Integer>> res = new ArrayList();public List<List<Integer>> combine(int n, int k) {List<Integer> path = new ArrayList();backtracking(path, 1, n, k);return res;}public void backtracking(List<Integer> temp, int startIndex, int n, int k) {// 终止条件if(temp.size() == k) {res.add(new ArrayList(temp));return;}// 遍历 [startIndex, n]for(int i=startIndex; i<=n; i++) {temp.add(i); // 操作当前节点backtracking(temp, i + 1, n, k); // 这里是i+1,不要错写成startIndex+1了temp.remove(temp.size() - 1); // 状态重置}}
}
39. 组合总和
【39. 组合总和】
分析:
组合问题,因此我们也需要一个 startIndex
变量作为遍历的起始位置。这样做是为了避免出现重复的组合(例如:234 和 324就是重复的)
但与上题不同在于,该题可以允许组合中的元素重复,因此在调用递归方法时,startIndex
就没必要+1了
剪枝:为了提高效率,我们事先对数组进行排序,越往后数字越大,如果加到某个节点已经超过了目标值,那后面的就没必要在判断了,因为加起来和会更大
代码:
class Solution {private List<List<Integer>> res = new ArrayList();public List<List<Integer>> combinationSum(int[] candidates, int target) {// 排序 后后序剪枝做准备Arrays.sort(candidates);List<Integer> path = new ArrayList();backtracking(path, 0, candidates, target);return res;}public void backtracking(List<Integer> path, int startIndex, int[] candidates, int target) {// 终止条件if(target == 0) {res.add(new ArrayList(path));return;}for(int i=startIndex; i<candidates.length; i++) {// 剪枝:因为我们事先已经为数组排好序了,越往后数字越大,如果当前数字都已经减到负数了,那后面的就没必要在判断了if(target - candidates[i] < 0) {break;}target -= candidates[i];path.add(candidates[i]);backtracking(path, i, candidates, target);// 状态重置target += candidates[i];path.remove(path.size() - 1); }}
}
剑指 Offer 34. 二叉树中和为某一值的路径
【剑指 Offer 34. 二叉树中和为某一值的路径】
分析:
二叉树的回溯整体上确实是回溯的思想,大体思路也差不多。但在处理细节上还是有些不同的,例如和前面组合问题相比:
- 二叉树要从根节点处理(每次递归中处理的是当前节点);而常规回溯题一般不处理根节点(每次递归中处理的都是孩子节点,而且是多孩子);因此在处理细节上是有差异的
- 二叉树代码 :
public void backtracking(TreeNode root, int target, List<Integer> path) {if(root == null) {return;}// 处理当前节点target -= root.val;path.add(root.val);if(target == 0 && root.left == null && root.right == null) {res.add(new ArrayList(path));}// 处理左右孩子节点backtracking(root.left, target, path);backtracking(root.right, target, path);// 状态重置target += root.val;path.remove(path.size() - 1);
}
- 常规回溯代码:
public void backtracking(List<Integer> path, int startIndex, int[] candidates, int target) {// 终止条件if(target == 0) {res.add(new ArrayList(path));return;}// for循环处理孩子节点for(int i=startIndex; i<candidates.length; i++) {// 剪枝:因为我们事先已经为数组排好序了,越往后数字越大,如果当前数字都已经减到负数了,那后面的就没必要在判断了if(target - candidates[i] < 0) {break;}target -= candidates[i];path.add(candidates[i]);backtracking(path, i, candidates, target);// 状态重置target += candidates[i];path.remove(path.size() - 1); }
}
代码:
class Solution {private List<List<Integer>> res = new ArrayList();public List<List<Integer>> pathSum(TreeNode root, int target) {List<Integer> path = new ArrayList();backtracking(root, target, path);return res;}public void backtracking(TreeNode root, int target, List<Integer> path) {if(root == null) {return;}// 处理当前节点target -= root.val;path.add(root.val);if(target == 0 && root.left == null && root.right == null) {res.add(new ArrayList(path));}// 递归调用左右子节点backtracking(root.left, target, path);backtracking(root.right, target, path);// 状态重置target += root.val;path.remove(path.size() - 1);}
}
46. 全排列
【46. 全排列】
分析:
因为不是组合问题,所以我们每层遍历时都不用缩小可选的范围,只需要判断当前路径中是否存在该元素即可
代码:
class Solution {private List<List<Integer>> res = new ArrayList();public List<List<Integer>> permute(int[] nums) {List<Integer> path = new ArrayList();backtracking(path, nums);return res;}public void backtracking(List<Integer> path, int[] nums) {if(path.size() == nums.length) {res.add(new ArrayList(path));return;}for(int i=0; i<nums.length; i++) {// 若存在重复元素则不添加if(path.contains(nums[i])) {continue;}path.add(nums[i]);backtracking(path, nums);path.remove(path.size() -1);}}
}
51. N 皇后
【51. N 皇后】
分析:
参考视频:【这就是传说中的N皇后? 回溯算法安排!| LeetCode:51.N皇后】
参考题解:51. N-Queens:【回溯法经典问题】详解
- 用一个二维数组
chessBoard
来记录棋盘的状态 - 每往下深入一行,然后遍历各个列,判断 当前点
chessBoard[row][col]
是否有效? - 是否有效判断逻辑:判断当前点
左上方
、正上方
、右上方
是否有皇后,若存在则说明不符合规则 是无效的,就返回false
代码:
class Solution {List<List<String>> res = new ArrayList();public List<List<String>> solveNQueens(int n) {char[][] chessBoard = new char[n][n];// 初始化棋盘,默认都是'.'for(char[] chars: chessBoard) {Arrays.fill(chars, '.');}backtracking(chessBoard, n, 0);return res;}public void backtracking(char[][] chessBoard, int n, int row) {if(row == n) {res.add(arrayToList(chessBoard));return;}for(int col=0; col<n; col++) {if(isValid(chessBoard, n, row, col)) {chessBoard[row][col] = 'Q';backtracking(chessBoard, n, row + 1);chessBoard[row][col] = '.';}}}/*** 检查 chessBoard[row][col] 的 “正上方”、“左上方”、“右上方” 是否有皇后?* 存在皇后,说明chessBoard[row][col]是无效的返回false,不存在说明chessBoard[row][col]是有效的,返回true*/public boolean isValid(char[][] chessBoard, int n, int row, int col) {// 检查上方 是否有皇后for(int i=0; i<row; i++) {if(chessBoard[i][col] == 'Q') {return false;}}// 检查左上方 是否有皇后for(int i=row-1, j=col-1; i>=0 && j>=0; i--, j--) { // 往左上角移动if(chessBoard[i][j] == 'Q') {return false;}}// 检查右上方 是否有皇后for(int i=row-1, j=col+1; i>=0 && j<n; i--, j++) { // 往右上角移动if(chessBoard[i][j] == 'Q') {return false;}}return true;}/*** 二维字符数组 -> 字符串集合*/public List arrayToList(char[][] chessBoard) {List<String> list = new ArrayList();for(char[] chars: chessBoard) {list.add(new String(chars));}return list;}
}
22. 括号生成
【22. 括号生成】
分析:
参考题解:【虽然不是最秀的,但至少能让你看得懂!】
还是常规的回溯法,一样的配方
- 该题 可以看做 从 数组
[ '(' , ')' ]
中进行2*n
次排列(该数组仅有左括号
和有括号
2个元素)
在此树的基础上我们要删除掉不符合的路径,进行剪枝
- 需要满足一定条件才能拼接到
path
路径中(剪枝
)- 充要条件:
左括号数量
<=n
并且右括号数量
<=左括号数量
- 代码:
left <= n && right <= left
- 充要条件:
模拟回溯状态的多叉树:
代码:
class Solution {List<String> res = new ArrayList();public List<String> generateParenthesis(int n) {StringBuilder sb = new StringBuilder();backtracking(sb, 0, 0, n);return res;}/*** left、right 分别指左右括号的数量*/public void backtracking(StringBuilder path, int left, int right, int n) {// 终止条件if(path.length() == n * 2) {res.add(path.toString());return;}// 递归处理子孩子(因为只有左、右两种括号,因此就只有两个孩子)if(isValid(left+1, right, n)) { // 追加一个“左括号”path.append('(');backtracking(path, left + 1, right, n);path.deleteCharAt(path.length() - 1);}if(isValid(left, right+1, n)) { // 追加一个“右括号”path.append(')');backtracking(path, left, right + 1, n);path.deleteCharAt(path.length() - 1);}}/*** 判断括号有效的条件* 若left > n 或者 right > left 那就是无效的*/public boolean isValid(int left, int right, int n) {if(left <= n && right <= left) {return true;}return false;}
}
正序
1. 两数之和(难度:简单)
【两数之和】
代码:
class Solution {public int[] twoSum(int[] nums, int target) {// 初始化map: 【值:下标】HashMap<Integer, Integer> tempMap = new HashMap();int[] res = new int[2];for(int i=0; i<nums.length; i++) {Integer another = target - nums[i];// 如果目标值 存在tempMap中,则说明找到了结果if(tempMap.containsKey(another)) {res[0] = i;res[1] = tempMap.get(another);return res;}// 将【值:下标】 存储在map中 (常规套路,在写代码时进入for后 就可以先写这一步)tempMap.put(nums[i], i);}return res;}
}
总结注意点:
- 空间换时间的思想,将当前值 以及对应下标信息 存储在map中
核心代码:
// 初始化:存储 某个值 以及 在nums中的位置信息
HashMap<Integer, Integer> tempMap = new HashMap();
// 如果目标值 存在tempMap中,则说明找到了结果
if(tempMap.containsKey(another)) {res[0] = i;res[1] = tempMap.get(another);return res;
}
// 主流程:把当前值 及 对应位置信息存入到map中
tempMap.put(nums[i], i);
2. 两数相加(难度:中等)
【两数相加】
代码:
/*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val = val; }* ListNode(int val, ListNode next) { this.val = val; this.next = next; }* }*/
class Solution {public ListNode addTwoNumbers(ListNode l1, ListNode l2) {ListNode head1 = l1; // 移动指针1,指向链表1ListNode head2 = l2; // 移动指针2,指向链表2// 构建结果链表ListNode resHead = new ListNode();ListNode temp = resHead;// 移动指针3,指向链表3// 进位值, 默认是0 (作用域一定要是外面)int carry = 0;while(null != head1 || null != head2) {// 为空取0, 否则取val(核心思想)int num1 = null==head1 ? 0 : head1.val;int num2 = null==head2 ? 0 : head2.val;int sum = num1 + num2 + carry;// 求模取余 获取当前节点值int curVal = sum % 10;// 整除获取进位值carry = sum / 10;// 构建当前节点ListNode curNode = new ListNode(curVal);// 尾插法temp.next = curNode; temp = curNode;// 节点后移,只需要考虑不为null的链表即可,因为为null的话我们默认取0, 不会有影响if(null != head1) head1 = head1.next;if(null != head2) head2 = head2.next;}// 最后节点遍历完后,判断最后一步运算是否进位了,进位则补1,否则不处理if(carry == 1) {ListNode lastNode = new ListNode(1);temp.next = lastNode;}return resHead.next;}
}
总结注意点:
- 常规链表的移动指针,三个移动指针对应三个链表
while(null != head1 || null != head2)
循环条件是 || 不是&&(任意一个链表没走完就执行的逻辑)int num1 = null==head1 ? 0 : head1.val
当前节点 为null取0,否则取val- 构建进位变量
carry
,作用域一定要放到外面;放里面就没法用了 - 考虑最后一步:最后节点遍历完后,判断最后一步运算是否进位了,进位则补1,否则不处理
3. 无重复字符的最长子串(难度:中等)
【无重复字符的最长子串】
代码:
class Solution {public int lengthOfLongestSubstring(String s) {// 判空处理if(null == s || s.length() == 0) {return 0;}// 初始化map: 【值:下标】HashMap<Character, Integer> map = new HashMap();// 定义滑动窗口最左侧指针int l = 0;// 最大长度int maxLength = 0;// r可以理解为 滑动窗口最右侧指针for(int r = 0; r < s.length(); r++) {char curChar = s.charAt(r);// 若map中之前存过这个值的下标,则让left指针右移 if(map.containsKey(curChar)) {// 目的:更新l 为 l、map(curChar)最右侧的下标// Math.max的好处:我们不需要考虑这个值是否在滑动窗口内l = Math.max(l, map.get(curChar) + 1);}// 更新最大长度maxLength = Math.max(maxLength, r - l + 1);// 将【值:下标】 存储在map中 (常规套路,在写代码时进入for后 就可以先写这一步)map.put(curChar, r);}return maxLength;}
}
解题思路:
该题是一道滑动窗口的问题。
滑动窗口的一般套路:左区间手动改变,有区间for循环累加
B站上视频链接:
https://www.bilibili.com/video/BV1BV411i77g
https://www.bilibili.com/video/BV1w5411E7EP
总结注意点:
- 和第一道题【1. 两数之和】类似点在于,都可以使用 一个map 来记录【值:下标】
- 使用
双指针
构建滑动窗口
的 经典问题。(套路:左边界手动改变 右边界自动累加) l
和r
滑动窗口的区间[l,r]
表示的就是当前所维护的不重复的字串l = Math.max(l, map.get(curChar) + 1)
,这里取得是map.get(curChar) + 1
,别忘了+1了。如果不+1的话,那么字串可能是abca这种,即第一个字符与后面那个字符重复
4. 寻找两个正序数组的中位数(难度:困难)- 先不做
5. 最长回文子串(难度:中等)
【最长回文子串】
解法一:暴力 - 遍历所有字串
建议先做第9题【9. 回文数】
代码:
class Solution {public String longestPalindrome(String s) {String longestPalindrome = "";// 遍历所有的子串,若长度比longestPalindrome长 且 是回文串,则更新longestPalindromefor(int i=0; i<s.length(); i++) {for(int j=i; j<s.length(); j++) {if(j - i + 1 > longestPalindrome.length()) {if(isPalindrome(s.substring(i, j+1))) {longestPalindrome = s.substring(i, j+1);}}}}return longestPalindrome;}// 判断是否是回文字符串public Boolean isPalindrome(String s) {// 双指针int left = 0;int right = s.length() - 1;while (left <= right) {if(s.charAt(left) != s.charAt(right)) {return false;}left ++;right --;}return true;}
}
总结注意点:
- 第一步:先写出 判断是否回文 的方法
isPalindrome
。 (利用双指针的思想) - 第二步:暴力遍历每个子字符串,判断是否回文,更长的则更新
时间复杂度: n^3
解法二:中心扩展法
参考讲解视频:https://www.bilibili.com/video/BV1dN4y1g7p9/?spm_id_from=333.337.search-card.all.click&vd_source=bf2066b8675548fac384ffe3bc83793e
代码:
class Solution {// 维护最长回文字符串public String res;public String longestPalindrome(String s) {// 判空处理if(null == s && s.length() == 0) {return "";}// 初始化res res = s.substring(0,1);// 遍历s字符串 for(int i=0; i<s.length(); i++) {// 考虑 bab 的格式extendFromCenter(s, i, i);// 考虑 baab 的格式extendFromCenter(s, i, i+1);}return res;}// 中心扩散public void extendFromCenter(String s, int left, int right) {// 当 left、right 在区间返回内,且 s[left] == s[right] 才会向外扩散while(left>=0 && right<s.length() && s.charAt(left)==s.charAt(right)) {// 向外扩散left--;right++;}// 若当前回文串 比 res长 则更新resif(right - left - 1 > res.length()) {// 因为最后一次循环 left-- right++了,因此实际上回文字符串是[left+1, right-1],由于substring是左闭右开[),因此 res = s.substring(left+1, right)res = s.substring(left+1, right);}}
}
总结注意点:
- 解法一:判断字串是否是回文串,思想是
自外向内
的,left ++; right --;
- 解法二:寻找属于回文串的字串,且思想是
自内向外扩散
的,left--; right++;
- 要考虑
bab
、baab
两种回文串的格式, 因此在遍历到某个字符时,extendFromCenter(s, i, i);extendFromCenter(s, i, i+1);
要调两次extendFromCenter
时间复杂度:n^2
7. 整数反转(难度:中等-简单)
【整数反转】
代码:
class Solution {public int reverse(int x) {// 注意: Math.abs(-2147483648) -> -2147483648 Math.abs(-2147483648L) -> 2147483648// 因此这里我先把x 转出一个Long 再调用Math.abslong longNum = Long.parseLong(String.valueOf(x));Long abs = Math.abs(longNum);// 取巧调用StringBuilder的reverse方法StringBuilder reverse = new StringBuilder(Long.toString(abs)).reverse();Long num = Long.parseLong(reverse.toString());// 负数 最小是 -2147483648if (x < 0 && num > 2147483648L) {return 0;}// 正数 最大是 2147483647if (x > 0 && num > 2147483647L) {return 0;}String strNum = x < 0 ? "-"+num : ""+num;return Integer.parseInt(strNum);}
}
总结注意点:
- int类型的范围
-2147483648 <= n <= 2147483647
Math.abs(-2147483648)
的结果为: -2147483648Math.abs(-2147483648L)
的结果为: 2147483648
9. 回文数(难度:简单)
【回文数】
判断字符串是否为回文字符串?
代码:
class Solution {public boolean isPalindrome(int x) {String s = x + "";int left = 0;int right = s.length() - 1;while(left < right) {if(s.charAt(left) != s.charAt(right)) {return false;}left++;right--;}return true;}
}
- 先转成字符串,再判断是否为回文字符串