目录
10. 正则表达式匹配
给你一个字符串 s
和一个字符规律 p
,请你来实现一个支持 '.'
和 '*'
的正则表达式匹配。
'.'
匹配任意单个字符'*'
匹配零个或多个前面的那一个元素
解题思路:
- 匹配的意思,随着s长度的增加,p长度的增加,p是否依旧能够和s匹配起来,这就很容易想到动态规划。
- 从双方都为空开始考虑,直到最终全部走完
- 构建一个用于动态规划的二维数组f,分别代表不同长度下是否匹配的结果,i 代表 s 的长度,j 代表 p 的长度,真实的位数得是 i - 1 , j - 1
- 起始 f [0] [0] 为 true,代表两个空字符串能匹配起来
- i 从 0 开始,即当s为空的时候,p能否和空匹配,p如果是普通字符,则和s不匹配,只有遇到
*
的时候才需要考虑特殊情况,例如'
a*'
,可以代表'
*'
把'
a'
带走了,即也为空- j 从 1 开始,因为p为空的时候,s的长度只要是大于0的,就不能匹配起来,这种默认就为 false
- 循环开始,逐步考虑,当 s 的长度只有 i 的时候,随着 j 长度的增加,p 是否依旧能和 s 匹配起来
- 这里有特殊情况就是遇到
'*'
的时候,所以分为两种情况进行讨论:
- 遇到
'*',
看'*'之前跟的字符是什么,是否和 第 i-1 位上的字符匹配(这里当前状态还是 i、j,j 是 '*',所以结果得根据'*'之前的走,看 j - 1)
匹配,则表示'*'使得之前的字符重复零次、一次或者多次
'
a*' = '
',等于 j 去掉两位,i 保持不变,看 j - 2'
a*' = '
a',匹配,看 i - 1 ,j - 2'
a*' = '
aaaaa......',看 i - 1,j 保持不变,因为'*'可以表示多个,例如
aaaaaaaaa和
a*
- 不匹配,则表示
'*' 使得之前的字符重复零次,'
a*' = ''
,即 p 去掉了两位,结果自然看 j - 2 和 当前长度依旧 为 i 的 s 是否匹配- 没有遇到
'*'
,看第i-1位和第j-1位是否匹配
- 这里如果遇到
'.',代表万能符,直接认为是匹配
匹配:表示当前位置是匹配的,最终结果还是要看双方都没到这一步之前的时候。例如 aa 和 ba ,双方都到达了第二个a,当前位置匹配,则结果看第一个位置即 a 和 b,即看 i - 1 和 j - 1
不匹配:直接为 false
class Solution {
public boolean isMatch(String s, String p) {
int m = s.length();
int n = p.length();
boolean[][] f = new boolean[m+1][n+1];
f[0][0] = true;
for (int i=0;i<=m;i++){
for (int j=1;j<=n;j++){
if (p.charAt(j-1) == '*'){
if (match(s,p,i,j-1)){
f[i][j] = f[i][j-2] || f[i-1][j-2] || f[i-1][j] ;
}else{
f[i][j] = f[i][j-2];
}
}else{
if (match(s,p,i,j)){
f[i][j] = f[i-1][j-1];
}
}
}
}
return f[m][n];
}
public boolean match(String s,String p,int i,int j){
if (i == 0){
return false;
}
if (p.charAt(j-1) == '.'){
return true;
}
return s.charAt(i-1) == p.charAt(j-1);
}
}
347. 前 K 个高频元素
难度:中等
给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
解题思路:
- 前k个高的元素,很容易想到优先队列,始终维护长度为k的队列,按照次数进行排序。
- 遍历一遍数组,将对应的值以及出现的次数,用map保存下来。
- 创建优先队列,重写它的比较方法,这里用的是一个数组,数组的第一位是具体值,第二位是出现的次数,比较的话,就是按照次数进行排序,小的始终在顶部。
- 遍历之前存储的map集合,构建数组插入到队列中去,因为始终维护一个长度为k大小的队列,所以要判断当前队列长度是否超过k,如果超过,则和队列顶端最小的比,比较它们出现的次数,如果大于,则进行替换,然后依次进行下去,最终得到一个长度为k的,出现次数最高的这样的一个队列,最终将其转换为数组输出。
class Solution {
// 前k高,即你要维护一个k长度的优先队列
// 用HashMap记录每个元素出现的个数
public int[] topKFrequent(int[] nums, int k) {
HashMap<Integer,Integer> map = new HashMap();
for (int n : nums){
map.put(n,map.getOrDefault(n,0)+1);
}
// int[] [值,次数]
PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>(){
public int compare(int[] n ,int[] m){
return n[1] - m[1];
}
});
// 遍历map,往queue里面插值
for(Map.Entry<Integer, Integer> entry:map.entrySet()){
int[] p = new int[]{entry.getKey(),entry.getValue()};
// 如果不满k个,直接先往里面塞
if (queue.size() < k){
queue.add(p);
}else{
// 塞满了以后,拿出最小的一个判断,如果当前值比最小的要大,则进行替换,否则不动
if (queue.peek()[1] < p[1]){
queue.poll();
queue.add(p);
}
}
}
int[] res = new int[k];
for (int i=0;i<k;i++){
res[i] = queue.poll()[0];
}
return res;
}
}
105. 从前序与中序遍历序列构造二叉树
难度:中等
给定两个整数数组 preorder
和 inorder
,其中 preorder
是二叉树的先序遍历, inorder
是同一棵树的中序遍历,请构造二叉树并返回其根节点。
例如:
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
解题思路:
- 前序遍历,根节点在前。中序遍历,根节点在中间。即从前序遍历中拿到根节点,根据根节点在中序遍历的位置,可以把中序遍历结果分为左右两颗子树,进而知道左子树长度,右子树长度,根据左子树长度,回到前序遍历中,找到左子树,也可以将其分为左右两颗子树,然后递归下去。
- 拿例子举例就是,前序遍历第一个元素肯定为根节点,拿到 根节点 3 去中序遍历中找到 3 的下角标位置为 1,即 从 0 到 1 之前为左子树,1 之后直到最后为右子树,根据计算可以知道左子树大小为 1,拿到大小去前序遍历中,从 3 往后数 1 则为左子树,左子树后面跟的就是右子树。即 9 为 左子树 ,20 15 7 为右子树。这里有指针对应记录树的开始位置和结束位置。而又因为 20 15 7 为前序遍历,所以 20 为根节点,拿到 20 去中序遍历中查找其位置,因为之前找根节点 3 的时候,已经划分最开始的左右子树,这里有指针记录当前右子树的开始位置在15,即15 到 20之前为左子树,20之后直到最后为右子树,由此构建出一个完整的树。
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
return dps(preorder,0,preorder.length,inorder,0,inorder.length);
}
public TreeNode dps(int[] preorder, int p_start,int p_end,int[] inorder,int i_start,int i_end){
if (p_start == p_end){
return null;
}
TreeNode root = new TreeNode(preorder[p_start]);
// 找到根节点,划分左右两棵子树
int root_index = 0;
for (int i=0;i<inorder.length;i++){
if (inorder[i] == preorder[p_start]){
root_index = i;
break;
}
}
// 这里就知道左子树的长度
int leftNum = root_index - i_start;
// 划分左子树,根节点后面跟左子树的长度即为左子树
root.left = dps(preorder,p_start+1,p_start+leftNum+1,inorder,i_start,root_index);
root.right = dps(preorder,p_start+leftNum+1,p_end,inorder,root_index+1,i_end);
return root;
}
}
438. 找到字符串中所有字母异位词
难度:中等
给定两个字符串 s
和 p
,找到 s
中所有 p
的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
解题思路:
- 这道题很容易想到,暴力解决,枚举s中每个和p长度相等的子字符串,判断其是否是异位词。这里判断两个字符串是否为异位词采用的就是先转换为字符数组,进行排序,最终比较是否相同。
- 同样这道题还能用滑动窗口来做,也容易理解。维护一个26长度的数组,位置对应a-z,数值对应数量,这样一个字符数组就代表一个字符串。滑动窗口大小为p的长度,将窗口内的子字符串也转换为数组,比较和p转换的数组是否相同,滑动的话,就将左边的剔除,将右边的加入,对应的就是数组位置上数字的加减
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int s_length = s.length();
int p_length = p.length();
char[] new_p_c = p.toCharArray();
Arrays.sort(new_p_c);
List<Integer> res = new ArrayList();
for (int i=0;i<=s_length-p_length;i++){
char[] new_c = s.substring(i,i+p_length).toCharArray();
Arrays.sort(new_c);
if (Arrays.equals(new_p_c,new_c)){
res.add(i);
}
}
return res;
}
}
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int s_length = s.length();
int p_length = p.length();
List<Integer> res = new ArrayList();
int[] num_1 = new int[26]; // s 子字符串转换的数组
int[] num_2 = new int[26]; // p 字符串转换的数组
if (p_length > s_length){
return res;
}
for (int i=0;i<p_length;i++){
num_1[s.charAt(i) - 'a']++;
num_2[p.charAt(i) - 'a']++;
}
// 如果一开始两个就相等,则加入
if (Arrays.equals(num_1,num_2)){
res.add(0);
}
for (int i=1;i<=s_length-p_length;i++){
// 减去左边
num_1[s.charAt(i-1) - 'a']--;
// 加入右边
num_1[s.charAt(i + p_length - 1) - 'a']++;
if (Arrays.equals(num_1,num_2)){
res.add(i);
}
}
return res;
}
}
437. 路径总和 III
难度:中等
给定一个二叉树的根节点 root
,和一个整数 targetSum
,求该二叉树里节点值之和等于 targetSum
的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
解题思路:
- 采用前缀和的形式,记录经过每个节点的时候的前缀和,key为前缀和,value为出现的次数。如果能在map中找到 (前缀和 - target),就说明存在从该节点到之前的节点,中间正好差了个 target的这种情况。
- 把当前节点加入,计算当前前缀和
- 查看之前是否存在 前缀和 = 当前前缀和 - target ,如果存在,就表示有这样一条,就可以加入进去
- 把当前前缀和也记录一下,如果和之前前缀和重复了,则+1,表示有多条
- 分别递归左子树和右子树
- 为了互相不影响,这条路径的前缀和不会影响到别的路径的前缀和。整体递归结束后,应当还原,将该前缀和删除。因为可能存在多条,所以这里不会删除,只是进行 -1 操作
map.put(0, 1) :因为任何节点本身也可以形成一个路径。如果某个节点的值就为target,那么它本身就是一个解。currSum - target = 0 , 当前节点算一个解,所以一开始就要申明。
class Solution {
// 前缀和的方式
public int pathSum(TreeNode root, int targetSum) {
HashMap<Long,Integer> map = new HashMap();
map.put(0L,1);
return dps(root,map,targetSum,0L);
}
public int dps(TreeNode root, HashMap<Long,Integer> map ,int targetSum,long currSum) {
if (root == null){
return 0;
}
long res = 0;
currSum += root.val;
res += map.getOrDefault(currSum-targetSum,0);
map.put(currSum,map.getOrDefault(currSum,0)+1);
res += dps(root.left,map,targetSum,currSum);
res += dps(root.right,map,targetSum,currSum);
map.put(currSum,map.getOrDefault(currSum,0)-1);
return (int)res;
}
}
64. 最小路径和
难度:中等
给定一个包含非负整数的 m x n
网格 grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
解题思路:
- 动态规划的问题,路径只能往右或者往下,所以说只要一个累加,选择小的那一方就可以。将上边界和左边界先初始化好,中间的值只需逐步选择小的值,每个步径的值都是累加的结果。
class Solution {
public int minPathSum(int[][] grid) {
int n = grid.length;
int m = grid[0].length;
int[][] res = new int[n][m];
res[0][0] = grid[0][0];
for (int i=1;i<n;i++){
res[i][0] = res[i-1][0] + grid[i][0];
}
for (int j=1;j<m;j++){
res[0][j] = res[0][j-1] + grid[0][j];
}
for (int i=1;i<n;i++){
for (int j=1;j<m;j++){
res[i][j] = Math.min(res[i-1][j],res[i][j-1]) + grid[i][j];
}
}
return res[n-1][m-1];
}
}
62. 不同路径
难度:中等
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
解题思路:
- 动态规划的问题,初始化两条边界都为1,因为就一条路。每次都只能向下向右,所以每次走到下一步,都有两种选择,选择数即是上方和左方的累加。
class Solution {
public int uniquePaths(int m, int n) {
int[][] res = new int[m][n];
res[0][0] = 1;
for (int i=1;i<m;i++){
res[i][0] = 1;
}
for (int j=1;j<n;j++){
res[0][j] = 1;
}
for (int i=1;i<m;i++){
for (int j=1;j<n;j++){
res[i][j] = res[i-1][j] + res[i][j-1];
}
}
return res[m-1][n-1];
}
}
56. 合并区间
难度:中等
以数组 intervals
表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
解题思路:
- 首先根据数组的第一位进行排序,保证顺序,将第一个数组作为起始数组,也就是比较组,第二个数组开始和第一个比,如果第二个数组的起始已经比比较组的结束要大,则可以作为新的数组添加进去,相反,则需要进行合并,因为是排过序的,所以起始肯定是比较组,结束则是比较两个组的结束谁更加大,最终结果就选谁。
class Solution {
public int[][] merge(int[][] intervals) {
if (intervals.length == 1){
return intervals;
}
Arrays.sort(intervals, (v1, v2) -> v1[0] - v2[0]);
List<int[]> res = new ArrayList();
res.add(intervals[0]);
for (int i=1;i<intervals.length;i++){
if (res.get(res.size()-1)[1] < intervals[i][0]){
res.add(intervals[i]);
}else{
res.get(res.size()-1)[1] = Math.max(res.get(res.size()-1)[1], intervals[i][1]);
}
}
return res.toArray(new int[res.size()][]);
}
}
55. 跳跃游戏
难度:中等
给定一个非负整数数组 nums
,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
解题思路:
- 看的就是你能去到的最远的地方,同时把最远的地方记录下来。当前会记录一个能到达的最远地方,如果遍历到当前位置超过能到达的最远地方则表示到不了,如果可以到达,则更新从当前位置出发能到达的最远地方,比较更新最大值,一直遍历下去直到倒数第二位,看最终的结果,能够到达的地方是不是远远超过最后一位。
class Solution {
public boolean canJump(int[] nums) {
if (nums[0] == 0 && nums.length > 1) return false;
int res = 0;
for (int i=0;i<nums.length-1;i++){
if (i <= res){
res = Math.max(res,nums[i]+i);
}
}
return res >= nums.length-1;
}
}
75. 颜色分类
难度:中等
给定一个包含红色、白色和蓝色、共 n
个元素的数组 nums
,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0
、 1
和 2
分别表示红色、白色和蓝色。
必须在不使用库的sort函数的情况下解决这个问题。
解题思路:
- 三种颜色分别用0 1 2表示,每个各占一块区域,0占左边,1占中间,2占右边,所以遇见0就往左边塞,遇到2就往右边塞,遇到1可以不动。
- 三个指针,其中一个指针负责遍历。每次遇到0,当前位置和左指针交换,交换完毕后,左指针和遍历指针都要一起移动。如果遇到的是1则,不动,如果遇到的是2,尾部指针向前移动,然后和尾部指针交换,,当前指针不动,因为不知道交换过来的是什么,所以还需要再判断一次当前位置的数值需要往哪里移动。
class Solution {
public void sortColors(int[] nums) {
int left = 0;
int mid = 0;
int right = nums.length;
while (mid < right){
if (nums[mid] == 0){
swap(nums,left,mid);
mid++;
left++;
}else if (nums[mid] == 1){
mid++;
}else if (nums[mid] == 2){
right--;
swap(nums,mid,right);
}
}
}
public void swap(int[] nums,int i,int j){
int x = nums[i];
nums[i] = nums[j];
nums[j] = x;
}
}
78. 子集
难度:中等
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
解题思路:
- 典型的一道回溯算法题,这边每经过一个值,都需要输入到最终的结果集当中。区别在于只往后,之前用过的就不再用了。
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList();
dps(nums,res,new ArrayList(),0);
return res;
}
public void dps(int[] nums,List<List<Integer>> res,List<Integer> list,int depth){
res.add(new ArrayList<>(list));
for (int i=depth;i<nums.length;i++){
list.add(nums[i]);
dps(nums,res,list,i+1);
list.remove(list.size()-1);
}
}
}
79. 单词搜索
难度:中等
给定一个 m x n
二维字符网格 board
和一个字符串单词 word
。如果 word
存在于网格中,返回 true
;否则,返回 false
。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
解题思路:
- 走格子,走过的格子不能再走,找到格子中的单词和 word 匹配,首先在格子中找到 word 头个单词字母,然后从其开始出发往后找寻剩下的。
- 这里注意几个条件,首先遇到超过格子边界的就不用再找下去了,其次因为不能重复使用,所以需要有个数组记录使用情况,遇到用过的也直接返回,说明这条路不通。其次就是都没用过,就得比较是不是接下去得字母,例如 ABC,从A开始,接下来就找B,找到B接下来找C。最终返回条件就是如果能成功找到最后一位则返回 true。因为是格子,所以可以从格子的上下左右走,只要一条路符合条件,就可以接着往下走,这里如果每条路都走不通,说明这个格子错误,所以要暂时回退,回退的时候得把这个格子使用记录消除。
class Solution {
public boolean exist(char[][] board, String word) {
int len = word.length();
int m = board.length;
int n = board[0].length;
boolean[][] used = new boolean[m][n];
for (int i=0;i<m;i++){
for (int j=0;j<n;j++){
if (board[i][j] == word.charAt(0)){
if (dfs(len,m,n,i,j,0,used,board,word)){
return true;
}
}
}
}
return false;
}
public boolean dfs(int len,int m,int n,int i,int j,int index,boolean[][] used,char[][] board,String word){
if (i < 0 || j < 0 || i > m-1 || j > n-1 || used[i][j] || board[i][j] != word.charAt(index) ){
return false;
}
if (index == len-1){
return word.charAt(index) == board[i][j];
}
used[i][j] = true;
if (dfs(len,m,n,i+1,j,index+1,used,board,word) || dfs(len,m,n,i-1,j,index+1,used,board,word) || dfs(len,m,n,i,j+1,index+1,used,board,word) || dfs(len,m,n,i,j-1,index+1,used,board,word)){
return true;
}
used[i][j] = false;
return false;
}
}
406. 根据身高重建队列
难度:中等
假设有打乱顺序的一群人站成一个队列,数组 people
表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki]
表示第 i
个人的身高为 hi
,前面 正好 有 ki
个身高大于或等于 hi
的人。
请你重新构造并返回输入数组 people
所表示的队列。返回的队列应该格式化为数组 queue
,其中 queue[j] = [hj, kj]
是队列中第 j
个人的属性(queue[0]
是排在队列前面的人)。
解题思路:
- 这道题有个技巧。先对原数组进行排序,因为本身就是看身高排序,所以对身高进行排序,如果遇到相同得,第二位 0 肯定在 1 得前面,所以最终,第一位是身高降序排列,排序后再对第二位进行升序排列。原数组就变成了 [[7,0],[7,1],[6,1],[5,0],[5,2],[4,4]]。这时候开始插入,因为本身就是按照身高排序得,高的先进去排序,后面矮的无论怎么移动,都不会影响到高的,所以就只需要经过细微调整,而调整得方式就看第二位,排在它前面得有几个,每次插入当前得时候会进行判断。
例如:
输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
- 插入 [7,0] 变成 [7,0]
- 插入 [7,1] 变成 [7,0],[7,1]
- 插入 [6,1] 变成 [7,0],[6,1],[7,1],因为6得前面只有一位比它高,所以理所当然第二位
- 插入 [5,0] 变成 [5,0],[7,0],[6,1],[7,1],因为5得前面没有比它高,所以理所当然第一位
- 插入 [5,2] 变成 [5,0],[7,0],[5,2],[6,1],[7,1],因为5得前面有两个比它高的,所以理所当然第三位
- 插入 [4,4] 变成 [5,0],[7,0],[5,2],[6,1],[4,4],[7,1],因为4得前面有四个比它高的,所以理所当然第五位
class Solution {
public int[][] reconstructQueue(int[][] people) {
Arrays.sort(people,new Comparator<int[]>(){
@Override
public int compare(int[] person1, int[] person2){
if (person1[0] != person2[0]){
// 如果两个数不相等,第一个数则从高到低排序
return person2[0] - person1[0];
}else{
// 如果两个数相等,第二个数则从低到高排序
return person1[1] - person2[1];
}
}
});
List<int[]> res = new ArrayList();
for (int i=0;i<people.length;i++){
// 如果队列中得数量超过了,应该排在第几位,则插入到对应得第几位中去
if (res.size() > people[i][1]){
res.add(people[i][1],people[i]);
}else{
// 暂且前面得人如果没超过,则直接插入到末尾
res.add(res.size(),people[i]);
}
}
return res.toArray(new int[res.size()][]);
}
}
96. 不同的二叉搜索树
难度:中等
给你一个整数 n
,求恰由 n
个节点组成且节点值从 1
到 n
互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
解题思路:
满足以下性质:
令h(0)=1,h(1)=1,满足递推式 h(n)= h(0)*h(n-1)+h(1)*h(n-2) + … + h(n-1)h(0) (n>=2)。以题目来说,假设总长度为n,当前访问到第 j 个根节点,那么它的左子树可以是 j - 1 个,右子树是 n - j 个。这里 j 的取值不同,可能的答案就不同,所以可以进行累加。
class Solution {
public int numTrees(int n) {
int[] dp = new int[n+1];
dp[0] = 1;
dp[1] = 1;
for (int i=2;i<=n;i++){
for (int j=1;j<=i;j++){
dp[i] += dp[j-1]*dp[i-j];
}
}
return dp[n];
}
}
300. 最长递增子序列
难度:中等
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
解题思路:
- 维护了dp数组用来动态规划:
- 初始状态,单个数字本身就可以作为一个子序列,所以长度都可以先设置为1
- 依次遍历一遍整个数组,当遍历到该位置时,这之前的数字都要和其作比较,这样才能直到是否是递增序列,同时记录下到当前位置时,最大的子序列数量
例如:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
初始值:dp:[1,1,1,1,1,1,1,1]
从 10 开始:因为是第一位,所以 dp 保持不变 ,dp:[1,1,1,1,1,1,1,1]
从 9 开始:9 和它之前的相比,就一个10,但因为是降序,所以本身不构成递增序列,dp 依旧保持不变,dp:[1,1,1,1,1,1,1,1]
从 2 开始:2 和它之前的相比,一个 10 ,一个 9,依旧降序,dp 保持不变,dp:[1,1,1,1,1,1,1,1]
从 5 开始:10 ,9 都是降序,2 是 升序,则 2,5 构成 递增序列,原本 2 的位置是 1,所以 5 的位置在 2 位置的基础上 + 1,dp:[1,1,1,2,1,1,1,1]
从 3 开始:10 ,9 ,5 都是降序,2 是 升序,则 2 ,3 构成递增序列,原本 2 的位置是1,所以 3 的位置在 2 位置基础上 + 1,dp:[1,1,1,2,2,1,1,1]
从 7 开始:10 , 9 都是降序,2,5,3 都是升序,所以要有比较,取最大者。2,7 构成递增,7 就是 在 2的位置上 + 1。5,7 构成递增,7 在 5 的位置上 + 1。3,7构成递增,7 在 3 的基础上 + 1,取最大值则为 3 ,dp:[1,1,1,2,2,3,1,1]
从 101 开始 :之前的都可以构成递增数列,所以可以是10,101,这里是 10 的基础上 + 1,后面依旧,取最大,结果 dp:[1,1,1,2,2,3,4,1]
从 18 开始:10,9,2,5,3,7 都可以构成,在此基础上 + 1,最终结果 dp:[1,1,1,2,2,3,4,4]
最终只要从该数组中取最大值,则是能构成的最大递增子序列
class Solution {
public int lengthOfLIS(int[] nums) {
int res = 0;
int[] dp = new int[nums.length];
Arrays.fill(dp,1);
// 每一位遍历一遍
for (int i=0;i<nums.length;i++){
// 遍历每一位的时候,之前的数都要和其比较
for (int j=0;j<i;j++){
if (nums[j] < nums[i]){
dp[i] = Math.max(dp[i],dp[j]+1);
}
}
res = Math.max(res,dp[i]);
}
return res;
}
}
152. 乘积最大子数组
难度:中等
给你一个整数数组 nums
,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
子数组 是数组的连续子序列。
解题思路:
- 暴力破解:例举出所有的连续子序列,计算出它们值,维护一个最大值
- 动态规划:依次乘起来,把之前的乘起来的数和当前数作比较,如果大于则表示还是乘起来有效,如果反而小了,说明之前的无效,可以从当前重新算起。因为会存在负数,所以要同时维护最小值,再遇到负数的时候就乘以最小值
class Solution {
public int maxProduct(int[] nums) {
if (nums.length == 1){
return nums[0];
}
int max = Integer.MIN_VALUE;
for (int i=0;i<nums.length;i++){
int curr = nums[i];
if (curr >= 0){
max = Math.max(max,curr);
}
for (int j=i+1;j<nums.length;j++){
curr *= nums[j];
if (curr >= 0){
max = Math.max(max,curr);
}
}
}
return max;
}
}
class Solution {
public int maxProduct(int[] nums) {
int res = Integer.MIN_VALUE;
int max = 1;
int min = 1;
for (int i=0;i<nums.length;i++){
if (nums[i] < 0){
int temp = max;
max = min;
min = temp;
}
max = Math.max(nums[i],nums[i]*max);
min = Math.min(nums[i],nums[i]*min);
res = Math.max(res,max);
}
return res;
}
}
148. 排序链表
难度:中等
给你链表的头结点 head
,请将其按 升序 排列并返回 排序后的链表 。
解题思路:
- 用优先队列,遍历一遍节点,加入到队列中,排序过后,再依个取出,重新组成链表(不符合题意)
- 用归并排序(递归),快慢指针定位中间节点,拆分出左右两部分,然后递归拆分,直到拆到最小单位,然后对应的左右开始合并,小的在前,大的在后。等于是合并两个有序链表。
class Solution {
public ListNode sortList(ListNode head) {
PriorityQueue<ListNode> queue = new PriorityQueue(new Comparator<ListNode>(){
public int compare(ListNode head1,ListNode head2){
return head1.val - head2.val;
}
});
while (head != null){
ListNode next = head.next;
head.next = null;
queue.add(head);
head = next;
}
int size = queue.size();
ListNode res = new ListNode(0);
ListNode h = res;
for (int i=0;i<size;i++){
h.next = queue.poll();
h = h.next;
}
return res.next;
}
}
class Solution {
// 归并排序
public ListNode sortList(ListNode head) {
if (head == null || head.next == null){
return head;
}
// 快慢指针找到中间节点
ListNode fast = head.next;
ListNode slow = head;
while (fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
ListNode temp = slow.next;
slow.next = null;
// head是左边,slow是右边
// 递归着分解
ListNode left = sortList(head);
ListNode right = sortList(temp);
// 定义一个新的,用于合并
ListNode res = new ListNode(0);
ListNode h = res;
while (left != null && right != null){
if (left.val < right.val){
h.next = left;
left = left.next;
}else{
h.next = right;
right = right.next;
}
h = h.next;
}
h.next = left != null ? left : right;
return res.next;
}
}
279. 完全平方数
难度:中等
给你一个整数 n
,返回 和为 n
的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1
、4
、9
和 16
都是完全平方数,而 3
和 11
不是。
解题思路:
- 动态规划
dp[ i ] = i :比如当前是3,可能的组成就是 + 1 + 1 + 1,这样结果就是 3,即都是由1组成的。
总体思想就是,a^2 + b^2 = n,遍历 j 的同时,通过 n - j * j 找到 b^2,看 b^2 最小是由多少个数量构成的,最终就是 b^2 的方案 + 当前的 j^2,即 dp [ i - j * j ] + 1。
举例: 5
i = 1,dp [1] = 1; 当 j = 1,dp[1-1*1] = 0,最终为1;
i = 2,dp [2] = 2; 当 j = 1,dp[2-1*1] = 1,最终为2;
i = 3,dp [3] = 3; 当 j = 1,dp[3-1*1] = 2,最终为3;
i = 4,dp [4] = 1; 当 j = 1,dp[4-1*1] = 3,最终为4; 当 j =2,dp[4-2*2] = 0,最终为1,取最小为1
i = 5,dp [5] = 2; 当 j = 1,dp[5-1*1] = 1,最终为2; 当 j =2,dp[5-2*2] = 1,最终为2,取最小为2
class Solution {
public int numSquares(int n) {
int[] dp = new int[n+1];
for (int i=1;i<=n;i++){
dp[i] = i;
for (int j=1;i-j*j>=0;j++){
dp[i] = Math.min(dp[i],dp[i-j*j]+1);
}
}
return dp[n];
}
}
208. 实现 Trie (前缀树)
难度:中等
Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie()
初始化前缀树对象。void insert(String word)
向前缀树中插入字符串word
。boolean search(String word)
如果字符串word
在前缀树中,返回true
(即,在检索之前已经插入);否则,返回false
。boolean startsWith(String prefix)
如果之前已经插入的字符串word
的前缀之一为prefix
,返回true
;否则,返回false
。
解题思路:
- 前缀树,顾名思义把一个个字符弄乘树的形式,当查找的时候可以沿着树遍历查找,插入方法这里用一个长度26的数组代替,在对应的位置再次挂载一个长度26的数组,这样挂载下去,就可以表示一个完整的字符串。查找方法这里是遍历字符串中字符算出字符应该所在位置,该位置如果没有挂载任何东西,则表示断路,如果字符串还有后续则不匹配,只有字符串都匹配到了才行。
- 这里会出现一个问题,就算匹配到相应结果,但是不确定是不是就是想要的字符串,可能该位置还是会挂载其他,举个例子说明:app、apple,先有了app,后续的le继续挂载在p后面。这时候当查找ap的时候,显然ap的p后面还挂载了东西,结果就是理应搜不到的,但是查找app的时候,虽然p后面还挂载le,但是app确实之前完整的字符串输入,为了解决这个问题,需要额外的一个标志位判断,当输入一个字符串例如app的时候,在末尾p处打上记号,表示这是一个字符串的结尾,当查看app的时候,遍历到p处停止,且p是带有标志位的,则表示一个完整的字符串,返回查找到。如果查前缀的话,只要查到就满足,无需管是不是完整。
class Trie {
class TrieNode {
boolean val;
TrieNode[] children = new TrieNode[26];
}
private TrieNode root;
public Trie() {
root = new TrieNode();
}
public void insert(String word) {
TrieNode p = root;
for (char c : word.toCharArray()){
int i = c - 'a';
if (p.children[i] == null){
p.children[i] = new TrieNode();
}
p = p.children[i];
}
p.val = true;
}
public boolean search(String word) {
TrieNode p = root;
for (char c : word.toCharArray()){
int i = c - 'a';
if (p.children[i] == null)return false;
p = p.children[i];
}
return p.val;
}
public boolean startsWith(String prefix) {
TrieNode p = root;
for (char c : prefix.toCharArray()){
int i = c - 'a';
if (p.children[i] == null)return false;
p = p.children[i];
}
return true;
}
}
/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* boolean param_2 = obj.search(word);
* boolean param_3 = obj.startsWith(prefix);
*/
53. 最大子数组和
难度:简单
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
解题思路:
- 动态规划:平移的过程中,不断更新最大值。不断更新最大子序列的左端,如果累加的结果和当前的数比起来,当前的数更大,说明之前的数起了反效果,所以要不断更新左端。其次每次都得更新最大值,来保证最后能获取到最大值。
- 分治:将数组一分为二,一直分下去,直到最小单位再合并,合并的时候要弄清除概念。即最终所求是区间的最大值,这个最大值可能出现在左边,也可能出现在右边,也有可能出现在中间,所以每个都得维护这几个变量。
lSum:以左侧为开端,最大子数组和
rSum:以右侧为开端,最大子数组和
mSum:整个区间的最大值
iSum:整个区间的和
合并后的lSum。有2中可能:左序列+右序列的左侧 ,左序列的左侧
合并后的rSum。有2中可能:右序列+左序列的右侧 ,右序列的右侧
合并后的mSum。有3种可能:
合并后的最大连续子列和序列完全在L中,即 L.mSum
合并后的最大连续子列和序列完全在R中,即 R.mSum
合并后的最大连续子列和序列横跨L和R,则该序列一定是从L中的某一位置开始延续到mid,然后从mid+1(R的左边界)开始延续到R中的某一位置。即L.rSum + R.lSum
class Solution {
public int maxSubArray(int[] nums) {
int pre = 0;
int max = nums[0];
for (int x:nums){
pre = Math.max(pre+x,x);
max = Math.max(max,pre);
}
return max;
}
}
class Solution {
public class Status{
int lSum; // 以左侧为开端,最大和
int rSum; // 以右侧为开端,最大和
int mSum; // 区间最大和
int iSum; // 区间整体和
public Status(int lSum,int rSum,int mSum,int iSum){
this.lSum = lSum;
this.rSum = rSum;
this.mSum = mSum;
this.iSum = iSum;
}
}
public int maxSubArray(int[] nums) {
return getInfo(nums,0,nums.length-1).mSum;
}
public Status getInfo(int[] a,int left,int right){
if (left == right){
return new Status(a[left],a[left],a[left],a[left]);
}
int m = (left + right) >> 1; // 获取中间值,右移等于除二
Status l = getInfo(a,left,m);
Status r = getInfo(a,m+1,right);
return com(l,r);
}
public Status com(Status left,Status right){
int iSum = left.iSum + right.iSum;
int lSum = Math.max(left.lSum,left.iSum + right.lSum);
int rSum = Math.max(right.rSum,right.iSum + left.rSum);
//可能是左区间的最大值,也可能是右区间的最大值,也有可能是中间合并即左边的右端点开始的最大值加右边以左端开始的最大值
int mSum = Math.max(left.mSum,Math.max(right.mSum,left.rSum+right.lSum));
return new Status(lSum,rSum,mSum,iSum);
}
}
1. 两数之和
难度:简单
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
解题思路:
- 需要用到额外的空间,因为是查找两个数和为target,所以当你拿到一个数,你就知道另外一个对应的数是多少,利用这个思想,遍历的同时去查找看是否存在target-curr,如果存在则表示找到这一对值,如果不存在则表示还没有遇到,则可以插入进去。key作为另一半,value则是当前位置用于最终返回。
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] res = new int[2];
HashMap<Integer,Integer> map = new HashMap();
for (int i=0;i<nums.length;i++){
if (map.containsKey(nums[i])){
return new int[]{map.get(nums[i]),i};
}else{
map.put(target-nums[i],i);
}
}
return res;
}
}
20. 有效的括号
难度:简单
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
解题思路:
- 用一个栈来接受左括号,当遇到右括号的时候,需要判断栈顶元素是不是对应的括号,如果不是则不是一个有效的括号,如果对应,则将栈顶元素弹出,表示匹配。这里注意,当栈为空的时候,直接放入右括号则是错误情况,例如:‘)’,无论后续如何,都是对应不上的。
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack();
char[] c = s.toCharArray();
for (char i : c){
if (i == '(' || i == '{' || i == '['){
stack.push(i);
}else{
if (!stack.isEmpty()){
if (i == ')' && stack.peek() != '('){
return false;
}else if (i == '}' && stack.peek() != '{'){
return false;
}else if (i == ']' && stack.peek() != '['){
return false;
}
stack.pop();
}else{
return false;
}
}
}
return stack.isEmpty();
}
}
70. 爬楼梯
难度:简单
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
解题思路:
- 经典爬楼,每次可以一步或者两步,所以用动态规划求解。每增加一层,都可以是前一层跳过来,或者是前两层调过来。很容易得到 dp[i] = dp [i-1] + dp[i-2]
class Solution {
public int climbStairs(int n) {
if (n <= 1){
return n;
}
int[] dp = new int[n+1];
dp[0] = 0;
dp[1] = 1;
dp[2] = 2;
for (int i=3;i<=n;i++){
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
}
338. 比特位计数
难度:简单
给你一个整数 n
,对于 0 <= i <= n
中的每个 i
,计算其二进制表示中 1
的个数 ,返回一个长度为 n + 1
的数组 ans
作为答案。
解题思路:
- 这里分为两种情况偶数和奇数,偶数的1的个数总和它的倍数的1的个数相同,例如:2 4 8 16 它们的1的个数都为1,所以只要知道2的1的个数,就能知道后面2 4 8 等的1的个数。奇数的1的个数总是比它小1的偶数的1的个数再多1,例如:1 就比0的1个个数多1,3就比2的1的个数多1,5就比4的1的个数多1。
class Solution {
public int[] countBits(int n) {
int[] res = new int[n+1];
res[0] = 0;
for (int i=1;i<=n;i++){
// 奇数,比偶数+1
if (i%2==1){
res[i] = res[i-1] + 1;
}else{
// 偶数,和 /2 结果相同 2 4 8 16 32 64 1的个数都相同
res[i] = res[i/2];
}
}
return res;
}
}
21. 合并两个有序链表
难度:简单
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
解题思路:
- 合并两个有序链表和合并两个有序数组一样,只不过换成了链表,所以自己得构建一个链表,然后依次去比较两个链表的头节点,谁比较小就取谁,最终一方将被取完,这时候循环比较停止,将剩下的一方直接连接到构建的链表末尾,最终返回结果。
/**
* 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 mergeTwoLists(ListNode list1, ListNode list2) {
ListNode head = new ListNode(0);
ListNode res = head;
while (list1 != null && list2 != null){
if (list1.val < list2.val){
res.next = list1;
res = res.next;
list1 = list1.next;
}else{
res.next = list2;
res = res.next;
list2 = list2.next;
}
}
if (list1 != null) res.next = list1;
if (list2 != null) res.next = list2;
return head.next;
}
}
94. 二叉树的中序遍历
难度:简单
给定一个二叉树的根节点 root
,返回 它的 中序 遍历 。
解题思路:
- 递归
- 迭代
中序遍历就是(左 根 右),所以先向左遍历,然后依次放入左节点,拿出的同时查看右节点,有右节点的放入右节点。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList();
dfs(root,res);
return res;
}
public void dfs(TreeNode root,List<Integer> res){
if (root == null)return;
dfs(root.left,res);
res.add(root.val);
dfs(root.right,res);
}
}
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList();
if (root == null)return res;
Deque<TreeNode> queue = new LinkedList();
while (root != null || !queue.isEmpty()){
while (root != null){
queue.push(root);
root = root.left;
}
root = queue.pop();
res.add(root.val);
root = root.right;
}
return res;
}
}
283. 移动零
难度:简单
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
解题思路:
- 总体就是遇到非零,就从头开始填入,填入后,原本的位置就可以置为0,这里注意一点,如果本身填入的位置就是自己,则表示不动,无需置为0。
class Solution {
public void moveZeroes(int[] nums) {
int index = 0;
for (int i=0;i<nums.length;i++){
if (nums[i] != 0){
nums[index] = nums[i];
if (index != i){
nums[i] = 0;
}
index++;
}
}
}
}
101. 对称二叉树
难度:简单
给你一个二叉树的根节点 root
, 检查它是否轴对称。
解题思路:
- 左右节点分别开始迭代,对称:当有值时,左边的左边等于右边的右边,左边的右边等于右边的左边,没有值时,两者得都没有值。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public boolean isSymmetric(TreeNode root) {
return isSym(root.left,root.right);
}
public boolean isSym (TreeNode left,TreeNode right){
if (left == null && right == null){
return true;
}
if (left == null && right != null){
return false;
}
if (left != null && right == null){
return false;
}
if (left.val == right.val){
return isSym(left.left,right.right) && isSym(left.right,right.left);
}
return false;
}
}
104. 二叉树的最大深度
难度:简单
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
解题思路:
- 这里得写法和层序遍历一样。按层放入队列,再取出,再放入,每次取都算是一次记录,进行累加,最终得到答案。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
Deque<TreeNode> queue = new LinkedList();
if (root == null){
return 0;
}
queue.add(root);
int max = 0;
while (!queue.isEmpty()){
int size = queue.size();
max++;
for (int i=0;i<size;i++){
TreeNode node = queue.poll();
if (node.left != null){
queue.add(node.left);
}
if (node.right != null){
queue.add(node.right);
}
}
}
return max;
}
}
136. 只出现一次的数字
难度:简单
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
解题思路:
- 都是成对,只有一个落单。先从小到大排序,将相同的挨在一起,两两判断,不同的话就是前面那个为答案,如果最终都没有找到,说明最后一位就是答案。
- 该题,还可以用异或来解决,只要是相同的异或就变成0了,0和任何数异或还是原来的数,所以最后留下的则是答案。
class Solution {
public int singleNumber(int[] nums) {
Arrays.sort(nums);
for (int i=1;i<nums.length;i++){
if (nums[i] != nums[i-1]){
return nums[i-1];
}
i++;
}
return nums[nums.length-1];
}
}
class Solution {
public int singleNumber(int[] nums) {
int res = nums[0];
for (int i=1;i<nums.length;i++){
res ^= nums[i];
}
return res;
}
}
141. 环形链表
难度:简单
给你一个链表的头节点 head
,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos
不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true
。 否则,返回 false
。
解题思路:
- 快慢指针,交错,一个移动一步,一个移动两步,如果有环总会相遇,要判断的很简单,相遇的时候停下来,同时要保证快指针不为null,不然next方法会报错,最后停止的时候进行判断,究竟是相遇停止还是没有环导致fast为空才停止。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null) return false;
ListNode fast = head.next;
ListNode slow = head;
while (fast != slow && fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
return fast == slow;
}
}
169. 多数元素
难度:简单
给定一个大小为 n
的数组 nums
,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
解题思路:
- 最多的元素占了超过1/2,所以排序后,无论怎么排,中间的位置肯定就是最多的元素。
例如:1 1 2 2 3 3 3 3 3
例如:1 1 1 1 2 2 2
例如:1 2 2 2 2 2 3 3 3
class Solution {
public int majorityElement(int[] nums) {
Arrays.sort(nums);
return nums[nums.length/2];
}
}
121. 买卖股票的最佳时机
难度:简单
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
。
解题思路:
- 动态规划问题:每天都可以分为持有和不持有两个状态。因为只允许买卖一次,所以当前想要持有,就表明昨天是持有的今天保持,或者说之前都没有持有,今天开始买进。而不持有就表示之前一直不持有,或者之前一直是持有的,今天刚刚卖出。
- 暴力破解方法:遍历的同时记录最小的数,要获利最大,肯定是后面减之前出现最小数的时候,所以要更新最小数值,同时更新下最大值。
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
int[][] dp = new int[len][2];
dp[0][0] = 0; // 第一天不持有
dp[0][1] = -prices[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],-prices[i]); // 现在持有,可能是一直持有,可能是上一阶段不持有,当前刚刚持有(所以是之前一直是0,现在持有就要扣掉现在的钱)
}
return dp[len-1][0];
}
}
class Solution {
public int maxProfit(int[] prices) {
int max = Integer.MIN_VALUE;
int p = Integer.MAX_VALUE;
for (int n : prices){
p = Math.min(n,p); // 维护一个最小的值,后面的数就可以减最小的数得到一个最大值
max = Math.max(max,n-p);
}
return max;
}
}
448. 找到所有数组中消失的数字
难度:简单
给你一个含 n
个整数的数组 nums
,其中 nums[i]
在区间 [1, n]
内。请你找出所有在 [1, n]
范围内但没有出现在 nums
中的数字,并以数组的形式返回结果。
解题思路:
- 比较直白的一种方式,先排序,后遍历,然后x从1开始记录,如果遇到当前的数,则把后续相同的数都跳过,同时x+1,查看2是否出现,没有出现则加入到列表中,同时x+1,当前index不变,因为当前位置还没匹配上。最终遍历结束记录下x目前是多少,理应x达到n,如果没有达到,就说明还缺了后面这几位,则需要补全。
- 根据题目所知,在1-n,所以干脆构建一个数组,每个下角标对应的就是1-n,遍历原数组,找到对应下角标的位置,在该位置上+1,证明有过值,最终遍历下数组,只要还是0的,则表示该下角标的数没有出现过。
class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
List<Integer> res = new ArrayList();
Arrays.sort(nums);
int x = 1;
int index = 0;
while (index < nums.length){
if (x != nums[index]){
res.add(x);
x++;
}else{
while (index < nums.length && x == nums[index]){
index++;
}
x++;
}
}
for (int i=x;i<=nums.length;i++){
res.add(i);
}
return res;
}
}
class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
List<Integer> res = new ArrayList();
int[] p = new int[nums.length+1];
for (int n:nums){
p[n]++;
}
for (int i=1;i<p.length;i++){
if (p[i] == 0){
res.add(i);
}
}
return res;
}
}
160. 相交链表
难度:简单
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
图示两个链表在节点 c1
开始相交:
题目数据 保证 整个链式结构中不存在环。
解题思路:
- 如果有相交,则表示后半部分相同。
- A = a + c
- B = b + c
- 唯一就在于a,b不同,所以要想相同,a + b = b + a
- 扩展一下,a + c + b = b + c + a,即 A + b = B + a,相遇的点就是相交的点
- 也就是说走完A再走B,走完B再走A
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode l1 = headA;
ListNode l2 = headB;
boolean isFirstA = true;
boolean isFirstB = true;
while (l1 != l2){
if (l1 == null){
l1 = headB;
}else{
l1 = l1.next;
}
if (l2 == null){
l2 = headA;
}else{
l2 = l2.next;
}
}
return l1;
}
}
206. 反转链表
难度:简单
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
解题思路:
- 经典题,反转的意思就是头变成尾,尾变成头
- 所以在这里得自己先整一个虚拟尾出来,第一个节点的next指向它,注意在指向前,应该把原本链表后面的元素都先保存起来,然后进行迭代,现在的尾部就是第一个元素了,下一个要接上去的链表就是之前事先保存起来的。
/**
* 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 reverseList(ListNode head) {
ListNode pre = null;
while (head != null){
ListNode next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
}
226. 翻转二叉树
难度:简单
给你一棵二叉树的根节点 root
,翻转这棵二叉树,并返回其根节点。
解题思路:
- 首先分为左右,然后每个子树都要进行反转,左节点和右节点对调。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null){
return null;
}
TreeNode node = root.left;
root.left = root.right;
root.right = node;
invertTree(root.left);
invertTree(root.right);
return root;
}
}
543. 二叉树的直径
难度:简单
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
1 / \ 2 3 / \ 4 5
解题思路:
- 迭代计算左边和右边,遇到null返回,回退后比较左右,取最大值+1,同时更新下最大路径,因为最大值可能不穿过根节点,再一侧就可能出现,所以随时更新下最大值。
- 举例:最左侧最后一个节点4,由于左右节点皆为null,所以left和right都为0,所以该节点最后的值为1,他的父节点2,看它的右节点5,同理5也是1,这时候对于2这个节点来说,左右都是1,加上本身就是2,在往上1节点,先看右侧,右侧就一个3,3的节点深度为1,所有1的右侧深度为1,1的左侧深度为2,1节点最终为3。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
private int res = 0;
public int diameterOfBinaryTree(TreeNode root) {
com(root);
return res;
}
public int com(TreeNode root){
if (root == null){
return 0;
}
int left = com(root.left);
int right = com(root.right);
res = Math.max(res,left+right);
return Math.max(left,right) + 1;
}
}
617. 合并二叉树
难度:简单
给你两棵二叉树: root1
和 root2
。
想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。
返回合并后的二叉树。
注意: 合并过程必须从两个树的根节点开始。
解题思路:
- 当前节点则是两个节点val的和,左节点和左节点,右节点和右节点,如果一方为空,则直接以另外一方为准。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if (root1 == null) return root2;
if (root2 == null) return root1;
TreeNode root = new TreeNode(root1.val + root2.val);
root.left = mergeTrees(root1.left,root2.left);
root.right = mergeTrees(root1.right,root2.right);
return root;
}
}
234. 回文链表
难度:简单
给你一个单链表的头节点 head
,请你判断该链表是否为回文链表。如果是,返回 true
;否则,返回 false
。
解题思路:
- 快慢指针,当快指针走到末尾的时候,慢指针正好走到中间,这里得区分奇偶,当奇数的时候,慢指针就会停留在正中间,所以需要往后移动一位,这样以head为头部,slow为尾部,这两节是头尾相反链表,所以只要将slow进行翻转,然后挨个和head匹配是不是相等就可以了。
/**
* 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 boolean isPalindrome(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
/* 奇数 */
if (fast != null){
slow = slow.next;
}
// 反转链表
slow = revs(slow);
fast = head;
while (slow != null){
if (slow.val != fast.val){
return false;
}
slow = slow.next;
fast = fast.next;
}
return true;
}
public ListNode revs(ListNode head){
ListNode pre = null;
while (head != null){
ListNode next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
}
461. 汉明距离
难度:简单
两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。
给你两个整数 x
和 y
,计算并返回它们之间的汉明距离。
解题思路:
- 当两个数异或在一起的时候,只有不同的时候才能保留下来,也就是题目所说的汉明距离,要想知道这其中包含多少个1,就需要逐步从末尾算出有多少个1, 这时候可以利用平移,将1慢慢的都平移到末尾,然后只需要判断末尾是否是1。
class Solution {
public int hammingDistance(int x, int y) {
int res = 0;
int s = x^y;
while (s != 0){
res += s&1;
s = s>>1;
}
return res;
}
}
139. 单词拆分
难度:中等
给你一个字符串 s
和一个字符串列表 wordDict
作为字典。请你判断是否可以利用字典中出现的单词拼接出 s
。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
解题思路:
- DFS求解,从字符串第一位开始截取,查看是否存在在wordDict中,如果有就可以这样迭代下去,剩余的字符串逐个开始匹配,最终如果全部匹配完则表示完全匹配。这里要注意一个点,要记录访问过的节点,如后续再遇到这个位置,可以直接返回,因为之前也从该位置走过一遍,无需重复。
- 动态规划,即如果0-j 的部分已经匹配上了,就看 j-i的位置能不能匹配上。所以dp[j] 代表的就是之前匹配上了的状态,如果之前都没匹配上,后续根本不用看。
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
return dfs(s,wordDict,0,new HashSet<Integer>());
}
public boolean dfs(String s,List<String> wordDict,int begin,HashSet<Integer> set){
if (begin == s.length()){
return true;
}
// 这是依次截取字符串判断,list中是否包含字符串
for (int i=begin+1;i<=s.length();i++){
// 遇到过经历过的开始,直接返回
if (set.contains(i))continue;
// 新截取的字符串
if (wordDict.contains(s.substring(begin,i))){
if(dfs(s,wordDict,i,set)){
return true;
}
// 记录下i的位置,表示这之后都已经 经历过一遍了,不用重复经历
set.add(i);
}
}
return false;
}
}
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int len = s.length();
Set<String> wordDictSet = new HashSet(wordDict);
boolean[] dp = new boolean[len+1];
dp[0] = true;
for (int i=1;i<=len;i++){
for (int j=0;j<i;j++){
if (dp[j] && wordDict.contains(s.substring(j,i))){
dp[i] = true;
}
}
}
return dp[len];
}
}
142. 环形链表 II
难度:中等
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
解题思路:
- 假设有环,环节点之前的路程称为 a ,环的路径为 b
- 快慢指针,
- f = 2 * s (快指针是慢指针的两倍)
- f = s + n * b (当两个指针相遇的时候,快指针肯定比慢指针多饶了 n 圈)
- s = n * b (联立上述关系式)
- f = 2 * n * b (联立上述关系式)
- 假设 k = a + n * b 也就是说,要停留在环的入口需要满足这个关系
- 而慢指针 s 正好满足 n * b,也就是说慢指针只需要再走个 a ,就可以到达环的入口,a是不确定的,所以如何去寻找这个a呢?通过上述定义可以发现 a 就是环节点之前的路程,所以我们可以起一个指针,它的位置在链表的头部,让它和慢指针同时走 a 步,这时候两指针就会再环入口相遇。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (true){
if (fast == null || fast.next == null || slow == null) return null;
fast = fast.next.next;
slow = slow.next;
if (fast == slow){
break;
}
}
fast = head;
while (fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
}
287. 寻找重复数
难度:中等
给定一个包含 n + 1
个整数的数组 nums
,其数字都在 [1, n]
范围内(包括 1
和 n
),可知至少存在一个重复的整数。
假设 nums
只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums
且只用常量级 O(1)
的额外空间。
解题思路:
- 按照题目说范围在1-n中,所以可以构建一个n+1的数组,将出现的数都在数组中打上标记,因为重复的数只有一个,所以每次打标记之前可以查询,如果在这之前有过标记,则说明该数重复。
- 二分法,为了满足空间O(1)。在一个1-n的区间中,任意一个数i,小于等于它的数的纵隔count应该就是i。例如:3,小于等于它的数就是1、2、3,利用整个想法,count如果大于了 i ,就说明前面的数有了重复,肯定在mid前面的区间里面,当前可能当前 i 是重复的,所以要把当前值作为一个备选答案。至于小于等于则是正常情况,说明比 i 大的值有缺失,或者正正好好,说明并没有重复数
- 数组中有环,判断出环的入口节点,快慢指针的做法
二进制,
nums中二进制的每一位相加结果x
[1,n]中二进制的每一位相加结果y
比较二进制每一位的x和y的值
x>y,就加入
举例:[1 2 3 3 4]
1:0001
2:0010
3:0011
3:0011
4:0100
x:0133 (每一位相加结果)
原本的 [1 2 3 4]
1:0001
2:0010
3:0011
4:0100
y:0122 (每一位相加结果)
对比发现,最后两位x大于y,所以最终结果0011 = 3
class Solution {
public int findDuplicate(int[] nums) {
int[] p = new int[nums.length+1];
for (int n:nums){
if (p[n] != 0){
return n;
}else{
p[n]++;
}
}
return nums[0];
}
}
class Solution {
public int findDuplicate(int[] nums) {
int left = 0;
int right = nums.length;
int res = -1;
while (left<=right){
int mid = (left+right)>>1;
int count = 0;
for (int i=0;i<nums.length;i++){
if (mid >= nums[i]){
count++;
}
}
if (count>mid){
right = mid - 1;
res = mid;
}else{
left = mid + 1;
}
}
return res;
}
}
class Solution {
public int findDuplicate(int[] nums) {
int slow = nums[0];
int fast = nums[nums[0]];
while (fast != slow){
slow = nums[slow];
fast = nums[nums[fast]];
}
slow = 0;
while (fast != slow){
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
}
class Solution {
public int findDuplicate(int[] nums) {
int n = nums.length, ans = 0;
int bit_max = 31;
while (((n - 1) >> bit_max) == 0) {
bit_max -= 1;
}
for (int bit = 0; bit <= bit_max; ++bit) {
int x = 0, y = 0;
for (int i = 0; i < n; ++i) {
if ((nums[i] & (1 << bit)) != 0) {
x += 1;
}
if (i >= 1 && ((i & (1 << bit)) != 0)) {
y += 1;
}
}
if (x > y) {
ans |= 1 << bit;
}
}
return ans;
}
}
198. 打家劫舍
难度:中等
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
解题思路:
- 动态规划:因为不能偷相邻的,所以每天都有两种选择,偷还是不偷
- 如果选择今天偷,说明昨天就不能偷,也就是前天的金额加今天的金额
- 如果选择今天不偷,则和昨天的金额一样
class Solution {
public int rob(int[] nums) {
int[] dp = new int[nums.length+1];
dp[0] = 0;
dp[1] = nums[0];
for (int i=2;i<=nums.length;i++){
dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i-1]);
}
return dp[nums.length];
}
}
221. 最大正方形
难度:中等
在一个由 '0'
和 '1'
组成的二维矩阵内,找到只包含 '1'
的最大正方形,并返回其面积。
解题思路:
- 动态规划:正方形,至少四个方块都得有值,所以就以右下角得方块为准,它得上方,左方,左上方都得有值,而每个含有1得方块都可以自己作为一个正方形,所以本身就有值为1,所以每个值得确定都得看另外三个值,取最小得那一方,因为只要有一方是0的话,就无法组成大三角.
- 以四个方块都为1为例
- 1 1
- 1 x
- 这里的x首先自身是1,其次构成了一个大的正方形所以1+1
- 以四个方块有0有1为例
- 1 0
- 1 x
- 这里的x,只能自身构成正方形,所以只能0+1
class Solution {
public int maximalSquare(char[][] matrix) {
int max = 0;
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return max;
}
int n = matrix.length;
int m = matrix[0].length;
int[][] dp = new int[n][m];
for (int i=0;i<n;i++){
for (int j=0;j<m;j++){
if (matrix[i][j] == '1'){
if (i == 0 || j == 0){
dp[i][j] = 1;
}else{
dp[i][j] = Math.min(Math.min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1])+1;
}
max = Math.max(max, dp[i][j]);
}
}
}
return max*max;
}
}
494. 目标和
难度:中等
给你一个整数数组 nums
和一个整数 target
。
向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1]
,可以在2
之前添加'+'
,在1
之前添加'-'
,然后串联起来得到表达式"+2-1"
。
返回可以通过上述方法构造的、运算结果等于 target
的不同 表达式 的数目。
解题思路:
- 回溯:每种都有两个选择,所以可以直接暴力向下搜索
class Solution {
int res = 0;
public int findTargetSumWays(int[] nums, int target) {
int[] p = new int[]{+1,-1};
dfs(nums,target,p,0);
return res;
}
public void dfs(int[] nums,int target,int[] p,int depth){
if (depth == nums.length){
if (target == 0){
res++;
}
return;
}
for (int i=0;i<2;i++){
dfs(nums,target-nums[depth]*p[i],p,depth+1);
}
}
}
416. 分割等和子集
难度:中等
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
解题思路:
- 背包问题:找到几个目标值,装入背包,正好能把背包装完,可以用动态规划来解决
- 两个参量,第一个为目标值,第二个为背包容量
- 随着目标值得不同,目标容量得不同,对应得结果也不同
- 当前值的状态有这几种情况:
- 不选当前值,所以和上一时刻的状态保持不变,当然容量也是一样
- 如果选择当前值,则要看直接有没有选择过另外一半,也就是说如果当前值是4,目标值是7,我要看之前的值能不能填满3,所以要看上一个状态下的另一部分。
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for (int n:nums){
sum += n;
}
// 要想一分为2,首先和必须是偶数
if (sum % 2 == 1){
return false;
}
// 找到目标值target
int target = sum/2;
// 第一列是每个数,第二个数容量
boolean[][] dp = new boolean[nums.length][target+1];
// 定义下初始值
if (nums[0] <= target){
dp[0][nums[0]] = true;
}
for (int i=1;i<nums.length;i++){
for (int j=0;j<=target;j++){
// 可以是不装当前值,所以和上一个得状态一样,容量没有变化
dp[i][j] = dp[i-1][j];
// 如果当前值就是我要找得值,就说明可以直接放入,那直接为true
if (nums[i] == j) {
dp[i][j] = true;
continue;
}
// 如果比当前值要小,即如果选当前值,那要找到余下得另外一半,另一半的状态肯定是上一时刻的
if (nums[i] < j) {
dp[i][j] = dp[i][j] || dp[i-1][j-nums[i]];
}
// 如果比目标值要大,那就可以直接跳过不考虑
}
}
return dp[nums.length-1][target];
}
}
337. 打家劫舍 III
难度:中等
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root
。
除了 root
之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root
。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
解题思路:
- 每个节点都有两种状态选和不选,因为是树节点,所以需要两个map分别存取,每个节点选和不选的状态
- 所以递归左右子树的时候,每个节点的状态都要写入map中
- 如果选择该节点,则下面的左右子树不能选,所以从不选取当前节点的左右子树中获取之前打家劫舍过的值。
- 如果不选择该节点,则下面的左右子树就可选可不选,在可选可不选中选择更大的
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
HashMap<TreeNode,Integer> l = new HashMap(); // 选取当前节点
HashMap<TreeNode,Integer> r = new HashMap(); // 不选取当前节点
public int rob(TreeNode root) {
dfs(root);
return Math.max(l.getOrDefault(root,0),r.getOrDefault(root,0));
}
public void dfs(TreeNode node){
if (node == null)return;
dfs(node.left);
dfs(node.right);
l.put(node,node.val + r.getOrDefault(node.left,0) + r.getOrDefault(node.right,0)); // 如果选择当前节点,就不能选取下面的左右
r.put(node,Math.max(l.getOrDefault(node.left,0),r.getOrDefault(node.left,0)) + Math.max(l.getOrDefault(node.right,0),r.getOrDefault(node.right,0))); // 如果不选当前节点,它的左右节点可以选可以不选,所以可以选更大的那一个
}
}
22. 括号生成
难度:中等
数字 n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
解题思路:
- 括号对,即左括号有n个,右括号有n个
- 随机放左括号,放右括号,当都放完了,则是一种结果
- 这里注意,左括号的数理应是比右括号数要少,先有左才有右,如果右多,则表示右多余的右括号,这种情况就是错误的。
- 剩下的就是递归左括号和右括号,注意剩下的括号数量
class Solution {
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList();
dfs(res,"",n,n);
return res;
}
public void dfs(List<String> res,String s,int left,int right){
if (left == 0 && right == 0){
res.add(s);
return;
}
if (left > right)return; // 必须要左括号小于右括号,如果右括号少,说明之前有多个右括号,则无法闭合
if (left > 0){
dfs(res,s+"(",left-1,right);
}
if (right > 0){
dfs(res,s+")",left,right-1);
}
}
}
31. 下一个排列
难度:中等
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
- 例如,
arr = [1,2,3]
,以下这些都可以视作arr
的排列:[1,2,3]
、[1,3,2]
、[3,1,2]
、[2,3,1]
。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
- 例如,
arr = [1,2,3]
的下一个排列是[1,3,2]
。 - 类似地,
arr = [2,3,1]
的下一个排列是[3,1,2]
。 - 而
arr = [3,2,1]
的下一个排列是[1,2,3]
,因为[3,2,1]
不存在一个字典序更大的排列。
给你一个整数数组 nums
,找出 nums
的下一个排列。
必须 原地 修改,只允许使用额外常数空间。
解题思路:
- 举例:12385764
- 下一个排列: 12386457
- 查找规律,从最后第一位开始找,第一个升序的位置 5 7,即5开始往后可以作调整
- 从下个排列中其实可以看出,6变成了第一位,也就是从后往前找的第一个比5大的,然后交换位置
- 交换后变成 6754
- 这时候离最终的答案,差一个倒序754变成457
- 这里还得分情况讨论:
- 如果说是升序的情况,但升序的时候已经是最后两个数了 ,比如 4 5,下一位就是 5 4,所以只需要交换这个两个数,然后直接返回就行
- 如果说是升序的情况,但后面的数没有一个比开头的数大的,比如 5 7 4 3,下一位是 7 3 4 5,因为后续没有大的数了,所以也是升序这两个数直接进行交换,交换过后变成 7 5 4 3,只需要将后面的降序排列,就是最终答案
- 如果说是升序的情况,且能找到后面比开头大的数,则是正常情况
- 如果说以上情况都没有发生,则表明当前数已经是最后一个排列了,它的下一位就是最开始即从小到大,而最后一位就是从大到小,只需倒叙即可
class Solution {
// 找到下一个排列
// 举例
// 12385764
// 下一个排列 12386457
// 查找规律,从最后第一位开始找,第一个升序的位置 5 7,即5开始往后可以作调整
// 从下个排列中其实可以看出,6变成了第一位,也就是从后往前找的第一个比5大的,然后交换位置
// 交换后变成6754
// 这时候离最终的答案,差一个倒序754变成457
public void nextPermutation(int[] nums) {
for (int i=nums.length-1;i>0;i--){
// 从后往前找到第一个升序的位置
if (nums[i-1] < nums[i]){
// 如果已经是组后一位了,就直接交换,然后返回
if (i == nums.length -1){
swap(nums,i-1,i);
return;
}
// 从后往前找到第一个大于开端的数
for (int j=nums.length-1;j>i;j--){
if (nums[i-1] < nums[j]){
// 交换两个值
swap(nums,i-1,j);
// 从降序变成升序
reverse(nums,i,nums.length-1);
return;
}
}
// 如果从后往前并没有找到比它还大的数,则直接交换升序的两个值,然后把后面的变成升序
swap(nums,i-1,i);
reverse(nums,i,nums.length-1);
return;
}
}
// 如果什么都没有找到,说明已经在最后一位,只需要倒转就行
reverse(nums,0,nums.length-1);
}
public void swap(int[] nums,int i,int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
public void reverse(int[] nums,int i,int j){
while (i<j){
swap(nums,i,j);
i++;
j--;
}
}
}
560. 和为 K 的子数组
难度:中等
给你一个整数数组 nums
和一个整数 k
,请你统计并返回 该数组中和为 k
的连续子数组的个数 。
解题思路:
- 看见连续子数组,就想到前缀和
- 遍历整个数组,当前缀和sum等于目标k的时候,算一个。同时去查看之前的前缀和,有没有差值是sum-k的,如果有就说明之前已经存在,则当前的数值得叠加上之前的,map中保留该次前缀和,次数+1,如果有重复就叠加上去。
- 举个例子:
- -1,1,0
- k = 0
- 遍历到当前 -1,sum = -1,-1不等于k,同时查看之前有无前缀和为-1得节点,发现没有,则将当前 -1 ,存入map,value 为 1.
- 遍历到当前 1,sum = 0, 0等于k,本次就为1个,res++,同时去判断之前有无前缀和 0 的节点,发现没有,则将当前 0 ,存入map,value 为 0.
- 遍历到当前 0,sum = 0,0等于k,本次就为1个,res++,同时去判断之前有无前缀和 0的节点,发现有,则res 的加上之前的结果,然后存入map,value + 1。这里作个说明,本身sum如果为k,肯定算一个,其次查找两个前缀和之差能否等于k,也就是说例子中,-1到1的前缀和为0,-1到0的前缀和也为0,所以说1往后到0之间的前缀和也为0,所以最终答案为3,即-1到1,-1到0,0本身
class Solution {
public int subarraySum(int[] nums, int k) {
// 前缀和
HashMap<Integer,Integer> map = new HashMap();
int sum = 0;
int res = 0;
for (int n:nums){
sum += n;
// 前缀和加起来,如果
if (sum == k){
res++;
}
if (map.containsKey(sum-k)){
res += map.get(sum-k);
}
map.put(sum,map.getOrDefault(sum,0)+1);
}
return res;
}
}
739. 每日温度
难度:中等
给定一个整数数组 temperatures
,表示每天的温度,返回一个数组 answer
,其中 answer[i]
是指对于第 i
天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0
来代替。
解题思路:
- 暴力解法:从每个温度后开始遍历,第一个大于该温度的位置,和当前位置的差值就是需要的天数。
- 栈:遍历数组,每当一个数组即将放入栈时,先和栈顶元素比较,如果比栈顶元素要小。直接如栈。如果比栈顶元素要大,则说明最近的一个温度要高的天来了,所以取出栈顶元素,它们下角标的差值就是需要等待的天数。比较的时候,只要是比栈顶元素大的,就挨个弹出。直到遇到比自己要大的为止,或者说是栈为空的时候。总结:当前的元素肯定是要入栈的,因为要得到之后最近一天比它高的温度的天数,就一定要和后面作比较,每次操作区别就在于之前的元素要不要出栈,只要是小的就出栈,值就是下角标的差值。
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int len = temperatures.length;
int[] res = new int[len];
for (int i=0;i<len;i++){
for (int j=i+1;j<len;j++){
if (temperatures[i] < temperatures[j]){
res[i] = j-i;
break;
}
}
}
return res;
}
}
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int len = temperatures.length;
int[] res = new int[len];
Stack<int[]> stack = new Stack();
for (int i=0;i<len;i++){
while (!stack.isEmpty() && temperatures[i] > stack.peek()[0]){
int[] r = stack.pop();
res[r[1]] = i - r[1];
}
stack.push(new int[]{temperatures[i],i});
}
return res;
}
}
39. 组合总和
难度:中等
给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150
个。
解题思路:
- 回溯:每个元素都可以用多次。但要避免重复,得加一个起始条件,只能往前不能往后
- 用一个Deque 存储当前元素,把当前元素加入,回退的时候得删除
- 举例: [2 ,3,7],target = 7
- 如果不加起始条件,可以是[2 2 3]、[2 3 2]、[3 2 2],要至少一个元素不同才能视为一个不同的组合,所以第一个 2 2 3,只往后可以。2 3 2 和 3 2 2 不行。当回退到元素3时,后面跟的元素只能从 3 开始往后。
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList();
Deque<Integer> queue = new LinkedList();
dfs(candidates,target,res,queue,0);
return res;
}
public void dfs(int[] candidates, int target,List<List<Integer>> res,Deque<Integer> queue,int begin){
if (target < 0){
return;
}
if (target == 0){
res.add(new ArrayList(queue));
return;
}
for (int i=begin;i<candidates.length;i++){
queue.addLast(candidates[i]);
dfs(candidates,target-candidates[i],res,queue,i);
queue.removeLast();
}
}
}
5. 最长回文子串
难度:中等
给你一个字符串 s
,找到 s
中最长的回文子串。
解题思路:
- 动态规划:dp[i][j]表示 [ i , j ] 这个区间是否能构成回文子串
- 找到最长的回文子串,只需要找到开端和长度,就能得到最终答案
- 首先得知道,每个单个字符都是回文,所以 i = j 时,dp[i][j] = true
- 其他情况:
- 回文,检查两端,即 i 和 j ,如果 i ,j 不相等,直接可以判断不是
- 如果相等,则看它们缩小范围的时候是不是,这里注意一点,有特殊情况,举例说明 abb,当 i = 1,j = 2,即子字符串 bb,理应是回文,但是当我们判断两端i和j相等,所以要看缩小范围的时候,i = 2 ,j =1,因为遍历的时候 i 是小于等于 j 的,所以当 j = 1的时候,i 顶多到 1 ,所以 i = 2,j = 1,默认为 false,这就不符合实际了,所以要加一个判断,当两个差值小于3的时候,直接判断 true。
class Solution {
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2){
return s;
}
int maxLen = 1;
int begin = 0;
// [i,j] 这个区间的字符串是否是回文
boolean[][] dp = new boolean[len][len];
dp[0][0] = true;
for (int j=1;j<len;j++){
for (int i=0;i<=j;i++){
if (i == j){
dp[i][j] = true;
}else{
if (s.charAt(i) == s.charAt(j)){
if (j - i == 1){
dp[i][j] = true;
}else{
dp[i][j] = dp[i+1][j-1];
}
}else{
dp[i][j] = false; // 两边都不相等,肯定不是
}
}
if (dp[i][j]){
if (j - i + 1 > maxLen){
begin = i;
maxLen = j - i + 1;
}
}
}
}
return s.substring(begin, begin + maxLen);
}
}
3. 无重复字符的最长子串
难度:中等
给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串 的长度。
解题思路:
- 滑动窗口,始终保持无重复子串,即如果遇到重复的,则对应着移动
- 举个例子:abca
- 当移动到第二个a的时候,为了保证窗口内没有重复,所以窗口左端得移动到该重复字符得后一位,即bca,这样就能无重复,如何判断之前有没有遇到过重复,就要用一个map保留信息,当发现直线遇到过,立马移动左端点
- 这里有特殊情况
- 举个例子:abba
- 当先移动到第二个b的时候,左端点自动移动到第一个b的后面,但是当后续又遇到a的时候,犹豫a也是重复的,这时候理应移动到第一个a的后面,但是滑动窗口是单向的,不能向后滑动,所以得和目前的左端点进行比较,只能往后不能往前。每次移动过后都要进行最大值的判断,同时保存下当前值的信息
class Solution {
public int lengthOfLongestSubstring(String s) {
if (s.length() == 0){
return 0;
}
HashMap<Character,Integer> map = new HashMap();
int left = 0;
int maxLen = 1;
for (int i=0;i<s.length();i++){
// 如果有key存在,则把左指针移动到该key
if (map.containsKey(s.charAt(i))){
left = Math.max(map.get(s.charAt(i))+1,left);
}
maxLen = Math.max(maxLen,i-left+1);
map.put(s.charAt(i),i);
}
return maxLen;
}
}
11. 盛最多水的容器
难度:中等
给定一个长度为 n
的整数数组 height
。有 n
条垂线,第 i
条线的两个端点是 (i, 0)
和 (i, height[i])
。
找出其中的两条线,使得它们与 x
轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。
解题思路:
- 测试最大容器,也就是要知道容器的两边应该固定在什么位置
- 两端肯定需要向里收缩,关键是一起收缩还是收缩哪一边
- 如果收缩长的那边,因为短的那一边没动,容器大小以短的那边为准,再加上它们直线距离缩短了,所以容量肯定是缩小了。如果收缩短的那边,因为长的那边没动,虽然它们直线距离缩短了,但是短的那边的板子是有可能变长的,这样最终的容器大小就有可能变大。综上理应每次都移动短的那一边。
- 所以选用两个指针分别在一左一右,长的那边固定不动,短的那边收紧一格,每次都判断当前容器大小,选出最大的,指针相遇即为结束
class Solution {
public int maxArea(int[] height) {
int left = 0;
int right = height.length-1;
int max = 0;
while (left < right){
if (height[left] < height[right]){
max = Math.max(max,(right-left)*height[left]);
left++;
}else{
max = Math.max(max,(right-left)*height[right]);
right--;
}
}
return max;
}
}
200. 岛屿数量
难度:中等
给你一个由 '1'
(陆地)和 '0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
解题思路:
- 岛屿问题:
- 只要是一块岛屿,都是‘1’相连,也就是说判断岛屿就靠‘1’判断,那如何判断有几块呢?只需要该块走过的岛屿全部变成‘0’,这时候就不会影响到别的岛屿的判断
- 从岛屿的任何一点出发,只要是岛屿,总有‘1’相连,只需要把相连的‘1’都置为‘0’
- 举例:
- ["1","1","0","0","0"]
- ["1","1","0","0","0"]
- ["0","0","1","0","0"]
- ["0","0","0","1","1"]
- 遍历整个二维数组,从头部出发,每个格子都可以上下左右再出发,为了走过的不在继续走,把走过的‘1’,置为‘0’,同时如果遇到‘0’或者超出边界直接返回
- ["0","0","0","0","0"]
- ["0","0","0","0","0"]
- ["0","0","1","0","0"]
- ["0","0","0","1","1"]
- 犹豫第一个岛屿的‘1’,都置为了‘0’,所以第二次遍历到第[2,2]这个点的'1'
- ["0","0","0","0","0"]
- ["0","0","0","0","0"]
- ["0","0","0","0","0"]
- ["0","0","0","1","1"]
- 第三次遍历到[3,3]这个点
- ["0","0","0","0","0"]
- ["0","0","0","0","0"]
- ["0","0","0","0","0"]
- ["0","0","0","0","0"]
- 遍历结束,计算每次遍历到‘1’的次数就行
class Solution {
public int numIslands(char[][] grid) {
int n = grid.length;
int m = grid[0].length;
int res = 0;
for (int i=0;i<n;i++){
for (int j=0;j<m;j++){
if (grid[i][j] == '1'){
dfs(grid,n,m,i,j);
res++;
}
}
}
return res;
}
public void dfs(char[][] grid,int n,int m,int i,int j){
if (i < 0 || j < 0 || i >= n || j >= m || grid[i][j] == '0'){
return;
}
if (grid[i][j] == '1'){
grid[i][j] = '0';
}
dfs(grid,n,m,i+1,j);
dfs(grid,n,m,i-1,j);
dfs(grid,n,m,i,j+1);
dfs(grid,n,m,i,j-1);
}
}
207. 课程表
难度:中等
你这个学期必须选修 numCourses
门课程,记为 0
到 numCourses - 1
。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites
给出,其中 prerequisites[i] = [ai, bi]
,表示如果要学习课程 ai
则 必须 先学习课程 bi
。
- 例如,先修课程对
[0, 1]
表示:想要学习课程0
,你需要先完成课程1
。
请你判断是否可能完成所有课程的学习?如果可以,返回 true
;否则,返回 false
。
解题思路:
- 完成一个课程之前需要完成前置课程,所以得找到最开始得课程,即不用完成别的课程的课程,可能会有好几个这样的课程。
- 遍历课程数组,用一个数组去接受,每个有前置条件的该课程数量+1,代表着想要完成该课程需要先上完多少个前置课程,对应的你也得知道前置课程是什么,所以还要用个map来接受。
- 当数组生成之后,你再遍历一遍就知道哪些课程直接就可以完成,即那些值为0的课程,即不需要完成前置课程的课程,将它们都放入到队列中去
- 从队列中获取这些课程,获取代表着完成,当完成该课程时,去查看哪些课程依赖着该课程,所有依赖的课程,它们的数组中前置课程数量可以-1,如果减完后为0,表示该课程就可以完成了,将课程加入队列中,队列持续运转直到队列为空且没有新的课程加入
- 最终遍历一遍原来的数组,如果还有不是0的课程,说明存在互相依赖的关系,始终无法抵消,结果就为false。如果最终结果都为0,则没有环,可以全部完成为true
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
int[] p = new int[numCourses];
HashMap<Integer,List<Integer>> map = new HashMap();
// 每个课程要完成需要别的课程的次数
for (int[] n : prerequisites){
p[n[0]]++;
List<Integer> list = map.getOrDefault(n[1],new ArrayList<Integer>());
list.add(n[0]);
map.put(n[1],list);
}
Deque<Integer> queue = new LinkedList();
for (int i=0;i<numCourses;i++){
// 如果不被需要,说明可以先一步完成,完成后可删除对应需要它作为先置条件的课程
if (p[i] == 0){
queue.add(i);
}
}
while (!queue.isEmpty()){
int i = queue.poll();
List<Integer> list = map.get(i);
if (list != null){
for (int l : list){
p[l]--;
if (p[l] == 0){
queue.add(l);
}
}
}
}
for (int i=0;i<numCourses;i++){
if (p[i] != 0){
return false;
}
}
return true;
}
}
17. 电话号码的字母组合
难度:中等
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
解题思路:
- 将整个数字键盘列举出来,如果是23,就是去char二维数组把对应的字符数组取到
- 像是全排列一样,只不过每次排列的选择都是新的一位,可以用StringBuffer拼接,当拼接到最后一位的时候可以把当前结果加入到结果集中,然后回溯。
- 举例:23
- 先获取2,找到char二维数组中对应{'a','b','c'}
- 从头开始遍历 {'a','b','c'},获取 'a',StringBuffer添加'a',继续下一位是3,找到对应的{'d','e','f'},从头遍历,获取'd',加入StringBuffer,继续,发现已经到末尾了,则把‘ad’放入结果集中,回溯,之前StringBuffer中添加了'd',现在删除,并遍历到第二位'e',加入StringBuffer中,继续发现又到了末尾,‘ae’加入,然后如此回溯 'af' 加入,最后一位遍历结束则往前回溯,之前遍历到'a',现在是'b',然后往下继续是'd',然后如此继续
class Solution {
private char[][] c = {{' '},{' '},{'a','b','c'},{'d','e','f'},{'g','h','i'},{'j','k','l'},{'m','n','o'},{'p','q','r','s'},{'t','u','v'},{'w','x','y','z'}};
public List<String> letterCombinations(String digits) {
List<String> res = new ArrayList();
if (digits.length() == 0){
return res;
}
dfs(digits,0,res,new StringBuffer());
return res;
}
public void dfs(String digits,int index,List<String> res,StringBuffer s){
if (index == digits.length()){
res.add(s.toString());
return;
}
char[] x = c[digits.charAt(index) - '0'];
for (int i=0;i<x.length;i++){
s.append(x[i]+"");
dfs(digits,index+1,res,s);
s.deleteCharAt(s.length()-1);
}
}
}
15. 三数之和
难度:中等
给你一个包含 n
个整数的数组 nums
,判断 nums
中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0
且不重复的三元组。
注意:答案中不可以包含重复的三元组。
解题思路:
- 三数之和可以说是二数之和的加强版,因为最终的结果要是不能重复的,所以说先要排个序,有重复的方便跳过,两数之和就是找到两个和为目标值的数,三数就是另一个数的相反数充当目标值,充当目标数的数可以是任意一个,所以可以遍历一遍原数组,每个数都可以充当一遍,犹豫是不能重复,所以当轮到后面的数的时候就不能往前去查找
- 查找两个数的时候,因为是排过序的,所以可以用双指针,左右两边往里面收缩,如果左右两边相加sum等于目标值,则可以作为一种结果,同时左右边界都可以往里面收缩。如果sum比目标值要大,则表示过大,得缩小右边界,因为可能存在重复,所以要用个while把所有重复得都跳过。如果当前过小,则左边界要右移,加大sum值
- twoSum的结果集中只有两个,所以每个结果集要把当前数也加入进去,最后加入到三数的结果集中,同样的,之后遍历的时候和本次一样的数
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
// 排序避免重复数
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList();
for (int i=0;i<nums.length;i++){
List<List<Integer>> list = twoSum(nums,i+1,-nums[i]);
if (list.size() > 0){
// 每个list都补上当前数,然后再加入到大的结果集中
for (List<Integer> list1 : list){
list1.add(nums[i]);
res.add(list1);
}
}
// 跳过重复的数
while (i < nums.length-1 && nums[i+1] == nums[i]){
i++;
}
}
return res;
}
public List<List<Integer>> twoSum(int[] nums,int left,int target){
int right = nums.length-1;
List<List<Integer>> res = new ArrayList();
while (left < right){
int leftNum = nums[left];
int rightNum = nums[right];
int sum = leftNum + rightNum;
if (sum == target){
// 找到了两个值
List<Integer> list = new ArrayList();
list.add(leftNum);
list.add(rightNum);
res.add(list);
// 左边如果有重复的则跳过
while (left < right && nums[left] == leftNum){
left++;
}
// 右边如果有重复的则跳过
while (left < right && nums[right] == rightNum){
right--;
}
}else if (sum > target){
// 表示大了,右边得缩小范围
while (left < right && nums[right] == rightNum){
right--;
}
}else if (sum < target){
// 表示小了,左边得扩大范围
while (left < right && nums[left] == leftNum){
left++;
}
}
}
return res;
}
}
19. 删除链表的倒数第 N 个结点
难度:中等
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
解题思路:
- 这里有个技巧,想要删除倒数第N个节点,就可以用双指针,让双指针间隔N个节点,这样当右指针到末尾的时候,左指针正好在删除节点的左侧
- 这里有个问题,如果我要删除的倒数第N个正好是第一个,这样我的左指针一直指向的都是第一个节点。而通常解法是说同时移动,左指针的下一位才是要删除的元素,这样就删除不了了。所以解决办法就是新定义一个头部,从头部开始往后,这样左指针一开始的位置就是在定义的头部,而如果要删除第一个元素,即左指针的后一位就可以删除了,最终结果返回头部后面的链表
- 举例:[1],删除1
- 定义了一个头部 0, 0 -> 1
- 左指针在0,右指针移动到1
- 由于右指针已经在末尾,所以并不存在同时移动
- 因为始终认为左指针的下一位是要删除的节点,所以自然而然删除了第一位
/**
* 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 removeNthFromEnd(ListNode head, int n) {
// 删除倒数第N个节点,就可以利用两指针
// 让右指针先动倒数N个节点
// 然后左指针再和右指针同时动
// 当右指针移动到最后的时候,左指针正好移动到要删除节点的前端
ListNode pre = new ListNode(0);
pre.next = head;
ListNode left = pre;
ListNode right = pre;
while (n-- > 0){
right = right.next;
}
while (right.next != null){
left = left.next;
right = right.next;
}
left.next = left.next.next;
return pre.next;
}
}
34. 在排序数组中查找元素的第一个和最后一个位置
难度:中等
给你一个按照非递减顺序排列的整数数组 nums
,和一个目标值 target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target
,返回 [-1, -1]
。
你必须设计并实现时间复杂度为 O(log n)
的算法解决此问题。
解题思路:
- 既然是在有序数组里面找到目标值的左右两个端点
- 可以用两次二分查找,分别找到左端点和右端点
- 第一次查找,找到第一个>=target值的,也就是左边界。所以当二分查找的时候,所有比target要小的数肯定不是,那left 每次更新就从mid+1 开始,而右边界始终保持大于等于的状态,最终当左右相遇的时候,right也就是左边界
- 第二次查找,找到最后一个<=target值的,也就是右边界。所以当二分查找的时候判断当前num[mid]的值是否比target要大,如果要大,那就肯定不是右边界,就收缩右边界 right = mid - 1,相反 left = mid,保持左边界始终是收紧状态,这样当left 和 right 相遇的时候,right可以保证是右边界
- 二分while的结束条件是left > right,所以left可能会大于right,有可能会产生越界,因此取right肯定没错
class Solution {
public int[] searchRange(int[] nums, int target) {
if (nums.length == 0){
return new int[]{-1,-1};
}
if (nums.length == 1){
if (nums[0] == target){
return new int[]{0,0};
}
}
// 两次循环,一次找到左端点,一次找到右端点
int left = 0;
int right = nums.length-1;
while (left < right){
int mid = left + (right-left)/2;
if (nums[mid] < target){
left = mid + 1;
}else{
right = mid;
}
}
if (nums[right] != target)return new int[]{-1,-1};
int L = right;
left = 0;
right = nums.length-1;
while (left < right){
int mid = left + (right-left+1)/2;
if (nums[mid] <= target){
left = mid;
}else{
right = mid - 1;
}
}
return new int[]{L,right};
}
}
647. 回文子串
难度:中等
给你一个字符串 s
,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
解题思路:
- 两层遍历,探索不同位置时,从0到该位置不同情况字符串能否构成回文
- 回文,首先每个字符都是一个回文,所以当 i == j 的时候算一次
- 其次得两端 i,j 位置的字符相等,再看里面的,而里面的情况取决于去掉这两端后子字符串是否能构成回文,这个在之前遍历的时候已经得知了。这里注意一个点,当 i 和 j靠的很近的时候,也就是 baa ,这种情况当 i 在 2,j 在 1的时候,dp[i-1][j+1]=dp[1][2],j是不能超过 i 的,所以默认为 false,但是这种情况 aa 是回文,所以要单独拿出来判定
class Solution {
public int countSubstrings(String s) {
// 回文子串的数目
int s_len = s.length();
int res = 0;
boolean[][] dp = new boolean[s_len][s_len];
dp[0][0] = true;
for (int i=0;i<s_len;i++){
for (int j=0;j<=i;j++){
if (i == j){
dp[i][j] = true;
}else{
if (s.charAt(i) == s.charAt(j)){
if (i - j == 1){
dp[i][j] = true;
}else {
dp[i][j] = dp[i-1][j+1];
}
}
}
if (dp[i][j])res++;
}
}
return res;
}
}
48. 旋转图像
难度:中等
给定一个 n × n 的二维矩阵 matrix
表示一个图像。请你将图像顺时针旋转 90 度。
你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
解题思路:
上下对称:matrix[i][j] -> matrix[n-i-1][j],(列不变)
左右对称:matrix[i][j] -> matrix[i][n-j-1],(行不变)
主对角线对称:matrix[i][j] -> matrix[j][i],(行列互换)
副对角线对称:matrix[i][j] -> matrix[n-j-1][n-i-1] (行列均变,且互换)顺时针旋转 90 度:上下对称 + 主对角线对称 或者 主对角线对称 + 左右对称
顺时针旋转 180 度:上下对称 + 左右对称 (两次 90 度)
顺时针旋转 270 度:左右对称 + 主对角线对称 (180 度 + 90 度)
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
// 水平翻转
// matrix[i][j] = matrix[n-i-1][j]
for (int i=0;i<n/2;i++){
for (int j=0;j<n;j++){
int temp = matrix[n-i-1][j];
matrix[n-i-1][j] = matrix[i][j];
matrix[i][j] = temp;
}
}
// 主对角线翻转
// matrix[i][j] = matrix[j][i]
for (int i=0;i<n;i++){
for (int j=0;j<i;j++){
int temp = matrix[j][i];
matrix[j][i] = matrix[i][j];
matrix[i][j] = temp;
}
}
}
}
322. 零钱兑换
难度:中等
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1
。
你可以认为每种硬币的数量是无限的。
解题思路:
- 动态规划,随着金额的增加,每次都可以从硬币组里面选硬币来填充它,但前提是当前钱币的金额得小于总金额,这样才有意义。如果当前选择了该硬币,就可以查看总金额减去该硬币,剩余金额组成需要最少多少个硬币,而这个金额在之前就已经得出,每次计算当前金额所需最少硬币,肯定是从硬币组里面选择
- 举个例子:1 2 5 硬币组 金额 6
- 当金额为1的时候,只有硬币组中1满足,所以dp[1] = 1
- 当金额为2的时候,硬币组里选择硬币1的时候,剩余金额为1,查看金额1的时候需要最少几个硬币,发现是1,所以总共加自己为2。当选择硬币2的时候,剩余金额为0,加上自己为1,两者取小的就是1,dp[2] = 1
- 当金额为3的时候,选择硬币1,剩余金额为2,能组成金额为2的最少硬币个数dp[2] = 1,所以加上自己为2。选择硬币2,剩余金额为1,组成金额为1的最少硬币个数dp[1] =1,所以加上自己也为2,最终dp[3] = 2
- 当金额为4的时候,选择硬币1 + dp[3] = 3,选择硬币 2 + dp[2] = 2,最终dp[4] = 2
- 当金额为5的时候,选择硬币1 + dp[4] = 3, 选择硬币2 + dp[3] = 3,选择硬币5 + dp[0] = 1,最终dp[5] = 1
- 当金额为6的时候,选择硬币1 + dp[5] = 2,选择硬币2 + dp[4] = 4,选择硬币5 + dp[1] = 2,所以最终dp[6] = 2
- 这里对于没有硬币的情况判定,假设硬币组中存在1,要组成amount,需要amount个硬币,所以这里设置最大值amount+1,如果最后还是这个值,表示没有硬币能够组成
class Solution {
public int coinChange(int[] coins, int amount) {
// 背包问题,每个都可以重复装直到装满
int[] dp = new int[amount+1];
Arrays.fill(dp,amount+1);
dp[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(dp[i], dp[i-coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
236. 二叉树的最近公共祖先
难度:中等
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
解题思路:
- 找到公共父节点,分两种情况
- 如果正好在左边找到一个,在右边找到一个,那它们的父节点就是上一级
- 如果只在一边找到一个,另一边没找到,说明只存在一边,那就返回有值得一边
- 如果都没找到,就直接返回null就可以
- 没找到得那一侧会向上递交null,找到的也就是有值得那一侧会像上递交值。没值和没值返回没值,有值和没值返回有值,当两个有值得分支相遇,说明它们到达了共同的父节点,也就是我们要找的
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q){
return root;
}
TreeNode left = lowestCommonAncestor(root.left,p,q);
TreeNode right = lowestCommonAncestor(root.right,p,q);
// 都没找到
if (left == null && right == null){
return null;
}
// 其余情况一个为null,一个不为null,说明找到一个
if (left == null){
return right;
}
if (right == null){
return left;
}
// 还有情况就是,在左右各找到一个,说明它们的父节点就是root
return root;
}
}
155. 最小栈
难度:中等
设计一个支持 push
,pop
,top
操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack
类:
MinStack()
初始化堆栈对象。void push(int val)
将元素val推入堆栈。void pop()
删除堆栈顶部的元素。int top()
获取堆栈顶部的元素。int getMin()
获取堆栈中的最小元素。
解题思路:
- 设计两个栈
- 一个用来普通放入、删除、获取顶部元素
- 一个用来记录当前最小元素的栈
- 最小元素栈每次放入的时候和栈的顶部元素比较大小,哪个小就放入哪个
- 弹出的时候,两个栈同时弹出
- 举例:依次放入 -1 ,0 ,1
- 最小元素栈应该为 -1,-1,-1
- 举例:依次放入 -1,-2,1,-3
- 最小元素栈应该为 -1,-2,-2,-3
class MinStack {
private Stack<Integer> stack;
private Stack<Integer> stack1;
public MinStack() {
stack = new Stack();
stack1 = new Stack();
}
public void push(int val) {
if (stack.isEmpty()){
stack.push(val);
stack1.push(val);
}else{
stack.push(val);
if (stack1.peek() > val){
stack1.push(val);
}else{
stack1.push(stack1.peek());
}
}
}
public void pop() {
if (!stack.isEmpty()){
stack.pop();
stack1.pop();
}
}
public int top() {
return stack.peek();
}
public int getMin() {
return stack1.peek();
}
}
/**
* Your MinStack object will be instantiated and called as such:
* MinStack obj = new MinStack();
* obj.push(val);
* obj.pop();
* int param_3 = obj.top();
* int param_4 = obj.getMin();
*/
238. 除自身以外数组的乘积
难度:中等
给你一个整数数组 nums
,返回 数组 answer
,其中 answer[i]
等于 nums
中除 nums[i]
之外其余各元素的乘积 。
题目数据 保证 数组 nums
之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请不要使用除法,且在 O(n)
时间复杂度内完成此题。
解题思路:
- 除了当前以外的元素乘起来,即元素的左边*元素的右边
- 可以用两次遍历
- 第一次遍历,计算从左边挨个乘起来的值,每个元素的值是该元素左边的累乘
- 第二遍遍历,计算从右边挨个乘起来的值,每个元素的值是该元素右边的累乘
- 第一遍是赋值,第二遍乘积,第二遍是两者结合(右边+左边)
- 举个例子:1 2 3 4
- 第一遍遍历,正向遍历,当前是之前元素累乘,res 为 1,1,1*2,1*2*3
- 第二遍遍历,反向遍历,当前是之后元素累乘,res 为 1*4*3*2,1*4*3,1*2*4,1*2*3
class Solution {
public int[] productExceptSelf(int[] nums) {
// 先把该元素左边乘起来,再把右边乘起来
int[] res = new int[nums.length];
res[0] = 1;
int p = 1;
for (int i=1;i<nums.length;i++){
p *= nums[i-1];
res[i] = p;
}
p = 1;
for (int j=nums.length-1;j>0;j--){
p *= nums[j];
res[j-1] *= p;
}
return res;
}
}
309. 最佳买卖股票时机含冷冻期
难度:中等
给定一个整数数组prices
,其中第 prices[i]
表示第 i
天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
- 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
解题思路:
- 这是买卖股票的变形,多个一个冷冻期且可以多次买卖
- 每天都有两个选择,持有不持有
- 不持有:可以是保持昨天的不持有状态;也可以是昨天持有状态下,今天卖掉了
- 持有:可以是保持昨天的持有状态;因为有冷东期,所以是保持前天的不持有状态,然后今天持有了
- 都是取最大收益
class Solution {
public int maxProfit(int[] prices) {
if (prices.length == 1)return 0;
int[][] dp = new int[prices.length][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[1][0] = Math.max(dp[0][0],dp[0][1] + prices[1]);
dp[1][1] = Math.max(dp[0][1],-prices[1]);
for(int i=2;i<prices.length;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-2][0] - prices[i]);
}
return dp[prices.length-1][0];
}
}
621. 任务调度器
难度:中等
给你一个用字符数组 tasks
表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间,CPU 可以完成一个任务,或者处于待命状态。
然而,两个 相同种类 的任务之间必须有长度为整数 n
的冷却时间,因此至少有连续 n
个单位时间内 CPU 在执行不同的任务,或者在待命状态。
你需要计算完成所有任务所需要的 最短时间 。
解题思路:
- 每个相同任务之间,需要间隔n个时间休息
- 所以出现次数最多的任务,肯定需要(max-1)*(n+1)+1个时间
- 举个例子:A出现次数最多,出现了3次,n为2,所以两个A直接隔了2
- A x x A x x A ,其中x代表其他,也就是不看其他任务,单完成A需要, (3-1)*(2+1)+1,最后一个任务可直接完成
- 其他任务出现次数小于A,所以其他任务可以随意的插入到两个A的间隔中去,比如A B C A B x A,对总的等待时间没有影响
- 如果说出现次数等于A,比如 A B C A B x A B,最终 (3-1)*(2-1)+1+1,所以还要计算等于出现最大次数的种类的个数
- 如果说种类很多超过了间隔,A B C D E A B C D E,那本身就不需要额外的等待时间,挨个执行任务就行,数组大小就是需要用的时间
- 所以想要完成这些任务,至少需要大于等于数组大小,如果发现次数比数组大小要小,就说明种类多的情况没考虑进去,最终结果就应该是数组大小,反之如果大于,就表明是正常情况,取计算结果就行
class Solution {
public int leastInterval(char[] tasks, int n) {
int[] dp = new int[26];
for (int i=0;i<tasks.length;i++){
dp[tasks[i]-'A']++;
}
// 获取出现次数最多的任务
int max = 0;
for (int num : dp){
max = Math.max(max,num);
}
// 前max-1个任务,每个任务都需要n+1的时间,最后一个任务无需等待直接完成
int time = (max-1)*(n+1);
// 计算有多少个和最大值并列的任务,最后一个任务需要多久能完成
for (int num : dp){
if (num == max){
time++;
}
}
return Math.max(tasks.length,time);
}
}
102. 二叉树的层序遍历
难度:中等
给你二叉树的根节点 root
,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
解题思路:
- 常规遍历二叉树的题目,按层输出,所以每次把同一层的节点放入队列中,取得时候也把同一层得全部取出来,因为是从左到右,所以放下一层节点得时候也是先看有没有左节点,再看有没有右节点
- 放一层,取一层,放下一层,再取下一层
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList();
if (root == null)return res;
Queue<TreeNode> queue = new LinkedList();
queue.add(root);
while (!queue.isEmpty()){
int size = queue.size();
List<Integer> list = new ArrayList();
for (int i=0;i<size;i++){
TreeNode node = queue.poll();
list.add(node.val);
if (node.left != null){
queue.add(node.left);
}
if (node.right != null){
queue.add(node.right);
}
}
res.add(list);
}
return res;
}
}
114. 二叉树展开为链表
难度:中等
给你二叉树的根结点 root
,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用
TreeNode
,其中right
子指针指向链表中下一个结点,而左子指针始终为null
。 - 展开后的单链表应该与二叉树 先序遍历 顺序相同。
解题思路:
- 按照前序遍历,把每个节点放入list中去
- 遍历,相邻两个节点互为父子节点,前一个节点左边为空,右边则是下一个节点
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public void flatten(TreeNode root) {
List<TreeNode> list = new ArrayList();
dfs(root,list);
for (int i=1;i<list.size();i++){
TreeNode pre = list.get(i-1);
TreeNode next = list.get(i);
pre.right = next;
pre.left = null;
}
}
public void dfs(TreeNode root,List<TreeNode> list){
if (root == null){
return;
}
list.add(root);
dfs(root.left,list);
dfs(root.right,list);
}
}
394. 字符串解码
难度:中等
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string]
,表示其中方括号内部的 encoded_string
正好重复 k
次。注意 k
保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k
,例如不会出现像 3a
或 2[4]
的输入。
解题思路:
- 这个和判断有效括号一样,遇到闭合 ' ] ',就去栈中找另外一半 ' [ ',它们中间的字符串就是需要重复的,' [ ' 开括号之前的就是需要重复的数字,由于数字大小不一定,所以要循环判断是不是数字,将数字都弹出,然后合并成最终循环的次数,再根据次数循环着把字符串挨个推入栈中,最后当遍历完整个字符串后,栈中就是我们最后的结果,只不过顺序是反的
class Solution {
public String decodeString(String s) {
Stack<Character> stack = new Stack();
char[] c = s.toCharArray();
for (int i=0;i<c.length;i++){
// 判断是不是']'闭合符号
if (c[i] == ']'){
// 如果是,则先取出'['开符号之间的字母组合
String new_s = "";
while (stack.peek() != '['){
new_s = stack.pop().toString() + new_s;
}
stack.pop(); // 把'['取出
// 取出数字
String nums = "";
while (true){
if (!stack.isEmpty() && stack.peek()-'0'>=0 && stack.peek()-'0' <=9){
nums = stack.pop() + nums;
}else{
break;
}
}
// 重复字母nums次,也是一个字符一个字符往栈里面加
char[] x = new_s.toCharArray();
for (int j=0;j<Integer.parseInt(nums);j++){
for (int k=0;k<x.length;k++){
stack.push(x[k]);
}
}
}else{
// 不是闭合都可以往栈里面加
stack.push(c[i]);
}
}
String res = "";
// 合并栈中元素
while (!stack.isEmpty()){
res = stack.pop().toString() + res;
}
return res;
}
}
538. 把二叉搜索树转换为累加树
难度:中等
给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node
的新值等于原树中大于或等于 node.val
的值之和。
提醒一下,二叉搜索树满足下列约束条件:
- 节点的左子树仅包含键 小于 节点键的节点。
- 节点的右子树仅包含键 大于 节点键的节点。
- 左右子树也必须是二叉搜索树。
注意:本题和 1038: 力扣 相同
解题思路:
- 把中序遍历反过来遍历,先遍历右边再遍历左边,就满足了累加的顺序
- 只需要再维护一个sum,把遍历过的值都累加起来,每次赋给遍历到的节点即可
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int sum;
public TreeNode convertBST(TreeNode root) {
dfs(root);
return root;
}
public void dfs(TreeNode root){
if (root == null){
return;
}
dfs(root.right);
sum += root.val;
root.val = sum;
dfs(root.left);
}
}
581. 最短无序连续子数组
难度:中等
给你一个整数数组 nums
,你需要找出一个 连续子数组 ,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。
请你找出符合题意的 最短 子数组,并输出它的长度。
解题思路:
- 改变一个连续子数组,使其升序,就可以把整个区间变成一个递增区间。抛开中间的连续子数组不说,该区间的左边原本一定是递增的且比后面的值都要来的小,该区间的右边一定是递增的,且比之前的都要大
- 不妨就找到两个分界点,这样就能把这个区间给确定下来
- 从左往右扫,找到最后一个比最大值小的点,做法就是维护一个最大值,比最大值要大就更新最大值,比它要小就更新右区间端点。因为如果后续都在更新最大值,则表示后续是一个递增的且比所有数都大的区间,满足右边的要求,而最后一次不满足该要求的点就是要找的分界点
- 从又往左扫,找到最后一个比最小值大的点,做法就是维护一个最小值,比最小值要小就更新最小值,比它要大就更新左区间端点。因为如果后续都在更新最小值,则便是后续是一个递减的且比所有数都小的区间,满足左边的要求,而最后一次不满足该要求的点就是要找的分界点
- 举例:1 2 3 5 4 6 9 8 10 11 12
- 从左往右扫,维护最大值,注意分界点,当到8的时候是降序,不满足更新最大值,所以在8的地方留个记号,后续10 11 12都在更新最大值,满足递增,最后一个留存点8就是区间的右端点
- 从右往左扫,维护最小值,注意分界点,当到5的时候是升序,不满足更新最小值,所以在5的地方留个记号,后续 3 2 1 都在更新最小值,满足递减,最后一个留存点5就是区间的左端点
class Solution {
public int findUnsortedSubarray(int[] nums) {
// 一个连续子数组,要确定区间,就要确定两端
// 这个两端,保证该区间左侧都是单调减的且比后面都要来的小
// 保证该区间右端都是单调增的且比之前的都要大
// 所以要确定个最大值,最小值,以及确定最后一个比最大值小的地方在哪里,以及最后一个比最小值大的地方在哪里
int len = nums.length;
int begin = 0;
int end = -1;
int max = nums[0];
int min = nums[len-1];
for (int i=0;i<len;i++){
// 从左往右 确定最大值
if (nums[i] >= max){
max = nums[i];
}else{
end = i;
}
// 从右往左 确定最小值
if (nums[len-i-1] <= min){
min = nums[len-i-1];
}else{
begin = len-i-1;
}
}
return end-begin+1;
}
}
46. 全排列
难度:中等
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
解题思路:
- 回溯:每次都可以从nums数组随便选值,但是选过的不能再选,所以每次都会从剩下的里面挑值,每次回退的时候要把对应用过的状态改成没用,这样后续才能继续添加使用
- 举例:[1 ,2 ] , [false false]
- 每次从0开始遍历,取到第一个数1,放入list中,当前 [true,false]。递归取第二个数也是从0开始,取 1,只不过它用过了,所以跳过,取 2, [true true]。最终当list.size()和数组大小相同的时候表示全部都用到了,加入到结果集中
- 开始回退,[true,false] ,2从list中删除,循环到了末尾遍历结束
- 继续回退,[false,false],1也从list中删除
- 这时候来到了最外层的遍历中,开始遍历到 2,[false,true],2加入list中,递归遍历,还从0开始遍历,发现是false,可以取,1放入list中,[true,true],该结果也将加入到结果集中去,循环到了末尾遍历结束
- 最终返回结果
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList();
boolean[] used = new boolean[nums.length];
dfs(nums,res,new ArrayList(),used);
return res;
}
public void dfs(int[] nums,List<List<Integer>> res,List<Integer> list,boolean[] used){
if (list.size() == nums.length){
res.add(new ArrayList(list));
return;
}
for (int i=0;i<nums.length;i++){
if (!used[i]){
used[i] = true;
list.add(nums[i]);
dfs(nums,res,list,used);
list.remove(list.size()-1);
used[i] = false;
}
}
}
}
49. 字母异位词分组
难度:中等
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母通常恰好只用一次。
解题思路:
- 字母异味词,只需要把它们按照指定顺序排列后,这些字符串就都相等了。所以只需要维护一个map,将顺序排列后的字符串作为key,value是一个list,存放排序后有相同结果的字符串,最终map中的value就是想要的答案
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
List<List<String>> res = new ArrayList();
Map<String,List<String>> map = new HashMap();
for (int i=0;i<strs.length;i++){
char[] c = strs[i].toCharArray();
Arrays.sort(c);
String s = String.valueOf(c);
List<String> list;
if (map.containsKey(s)){
list = map.get(s);
}else{
list = new ArrayList();
}
list.add(strs[i]);
map.put(s,list);
}
for (Map.Entry<String,List<String>> entry : map.entrySet()){
res.add(entry.getValue());
}
return res;
}
}
98. 验证二叉搜索树
难度:中等
给你一个二叉树的根节点 root
,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
- 节点的左子树只包含 小于 当前节点的数。
- 节点的右子树只包含 大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
解题思路:
- 中序遍历正好是先取左节点,再取中间,再取右节点
- 而二叉搜索树,正好是左边小于中间小于右边
- 按照这个顺序来说,正好是中序遍历,只要遍历的同时检查当前是不是比前面一个要大,如果不满足直接返回false,如果满足就不停的更新上一个值,直到结束
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
long pre = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
// 中序遍历,左节点,右节点
if (root == null) {
return true;
}
if (!isValidBST(root.left)){
return false;
}
if (root.val > pre){
pre = root.val;
}else{
return false;
}
if (!isValidBST(root.right)){
return false;
}
return true;
}
}
33. 搜索旋转排序数组
难度:中等
整数数组 nums
按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums
在预先未知的某个下标 k
(0 <= k < nums.length
)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]
(下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7]
在下标 3
处经旋转后可能变为 [4,5,6,7,0,1,2]
。
给你 旋转后 的数组 nums
和一个整数 target
,如果 nums
中存在这个目标值 target
,则返回它的下标,否则返回 -1
。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
解题思路:
- 用二分法去查找指定值
- 由于它有旋转点,就只能按照中间值和一侧大小判断旋转点在何处,因为本身这个数组就是升序数组,旋转后右侧的值都比左侧来的小,所以中间值如果比左侧要大,那说明旋转点在右侧,左侧为升序序列。接下来就根据target判断,数在mid左还是右,如果在左边,且比最左侧大,说明它夹在left和mid中间,那就缩小右边界。如果知道左侧是升序,而目标值又比中间值大,那就直接缩小左侧区间。这道题就是通过知道绝对确定的区间去缩小左右界限,其他情况就缩小相反的就行
- 如果当前值比左侧要小,旋转点肯定在左侧,右侧为升序。这时候target又大于了mid,小于了right,说明target在右侧升序数组中,那就缩小左侧区间。相反,缩小右侧区间
class Solution {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length-1;
int mid = 0;
while (left <= right){
mid = left + (right-left)/2;
if (nums[mid] == target){
return mid;
}
// 如果中间值比左侧值要大,从左至右升序
if (nums[mid] >= nums[left]){
// mid大于target,左侧又是升序,且target也是大于left的,说明target在mid和left中间
if (nums[mid] > target && nums[left] <=target){
right = mid - 1;
}else{
left = mid + 1;
}
}else{
// 反之左边有旋转点,那么右边就是升序
// mid小于target,且target比最右端小,说明target在mid和right中间
if (nums[mid] < target && nums[right] >= target){
left = mid + 1;
}else{
right = mid - 1;
}
}
}
return -1;
}
}
240. 搜索二维矩阵 II
难度:中等
编写一个高效的算法来搜索 m x n
矩阵 matrix
中的一个目标值 target
。该矩阵具有以下特性:
- 每行的元素从左到右升序排列。
- 每列的元素从上到下升序排列。
解题思路:
- 通俗理解,利用题目信息,去除无用的行和列,挨个访问
- 这里根据题目,可以从右上角开始,逐步的去除行和列。因为从上到下是升序,只要当前值大于目标值,那么向下的都会大于目标值,整个一列可以去除。因为从左至右是升序,只要当前值比目标值要小,那么左侧的都比它小,整个一行可以去除。这两个操作不能同时进行,会出现越界问题,所以每次操作完用while条件判断一下
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int n = matrix.length;
int m = matrix[0].length;
for (int i=0;i<n;i++){
if (matrix[i][0] > target)break; // 如果每行第一个没找到,那下面就不用找了,因为从上到下是升序
if (matrix[i][m-1] < target)continue; // 如果每行最后一个比它小,这一行也不用找了
for (int j=0;j<m;j++){
if (matrix[i][j] == target)return true; // 找到直接返回true
if (matrix[i][j] > target){
m = j;
break;
}
}
}
return false;
}
}
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int row = 0;
int col = matrix[0].length - 1;
while (row < matrix.length && col >= 0){
if (matrix[row][col] == target)return true;
// 从右上角开始
// 从上到下是升序,所以当前值如果大于target,那么向下的都大于target,这一列就不用看了
if (matrix[row][col] > target){
col--;
}
// 从左至右是升序,如果大于当前值,那么肯定都大于左边的值,这一行就不用看了
else if (matrix[row][col] < target){
row++;
}
}
return false;
}
}
215. 数组中的第K个最大元素
难度:中等
给定整数数组 nums
和整数 k
,请返回数组中第 k
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
解题思路:
- 维护一个大小为K的优先队列,默认为小根堆
- 从头开始先插入K个元素,插入后优先队列自动进行排序,这时候把剩下的元素挨个插入,每次和堆顶的元素比较,谁大谁保留,插入后会继续进行排序,所以优先队列始终维护K个最大的元素,且堆顶就是第K个最大的元素
class Solution {
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> queue = new PriorityQueue<>();//系统默认即为小根堆
int i=0;
for (;i<k;i++){
queue.offer(nums[i]);
}
for (;i<nums.length;i++){
if (queue.peek() < nums[i]){
queue.poll();
queue.offer(nums[i]);
}
}
return queue.peek();
}
}
128. 最长连续序列
难度:中等
给定一个未排序的整数数组 nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n)
的算法解决此问题。
解题思路:
- 找出最长序列
- 假设找到了这样得序列,该序列得特征是连续,而且有个头部
- 根据这个就可以先去寻找头部 x ,只要不存在 x - 1,也就是它得上一位,就能表示是个头部。为了快速判断数组中有没有含有这个数,就需要用个map存起来。其次当你找到一个头部后,你就可以顺着往下寻找 x + 1,如此往复,观察什么时候停止,停止前的序列就是连续序列。最后只需要从一堆连续序列中找到最大的那个
class Solution {
public int longestConsecutive(int[] nums) {
// 要找出连续的最长序列,首先得找到头部,如何知道,哪个是头部,即能否找到它得上一位
// 而获取数据,得先存起来,才能方便获取
Set<Integer> set = new HashSet();
for (int num:nums){
set.add(num);
}
int res = 0;
for (int n:set){
// 看能不能找到上一位,找不到即可作为头部
if (!set.contains(n-1)){
int currentNum = n;
int count = 1;
// 作为头部去不停查找下一位
while(set.contains(currentNum+1)){
currentNum++;
count++;
}
res = Math.max(res,count);
}
}
return res;
}
}
253. 会议室 II
给定一个会议时间安排的数组,每个会议时间都会包括开始和结束的时间 [[s1,e1],[s2,e2],…] (si < ei),为避免会议冲突,同时要考虑充分利用会议室资源,请你计算至少需要多少间会议室,才能满足这些会议安排。
解题思路:
- 这道题没有解锁,就光写了下过程,没有实际去运行过
- 会议室安排,首先能想到的就是按照开始时间先排个计划表出来,至于每个会议使用多长时间再看
- 会议室使用,需不需要再开一间,取决于其他房间有没有用完
- 假设前面有个会议在开,新的会议来了,可以看新的会议开始时间是否在前面会议结束时间之后,如果时间完全不冲突,则可以直接占用前面的会议室。如果开始时间夹在了中间,则表示得另外开一间。后面陆续添加的会议室也是同种情况,可以和之前的会议结束时间做比较,大于则直接占用,顶掉了原来的会议室,所以原来的会议室结束时间也得变化
- 每次去做比较,肯定是结束时间最早的做比较。因为能进行比较的都是已经在开的会议,只能看哪个最先结束,然后看可不可以直接占用,如果连最早结束的都不行,只能另开一个会议室
class Solution {
public int minMeetingRooms(int[][] intervals) {
// 将会议时间,按照开始时间从小到大排序
Arrays.sort(intervals,new Comparator<int[]>(){
@Override
public int compare(int[] i1 , int[] i2) {
return i1[0] - i2[0];
}
});
// 构建小顶堆,把结束时间插入
PriorityQueue<Integer> queue = new PriorityQueue();
for (int i=0;i<intervals.length;i++){
if (queue.isEmpty()){
queue.offer(intervals[i][1]);
}else{
// 判断后一个会议的开始时间之前,上一个会议已经结束了,如果上一个结束了,这个就可以直接使用上一个的会议室
// 如果开始时间夹在第一个的中间就说明需要另开一个
if (intervals[i][0] >= queue.peek()) {
queue.poll();
}
// 无论是哪种情况,肯定是要把当前会议结束时间塞入
queue.offer(intervals[i][1]);
}
}
return queue.size();
}
}
2. 两数相加
难度:中等
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
解题思路:
- 新起一个节点,头节点位一开始 l1 和 l2 的头节点的和,sum = l1.val + l2.val
- 维护sum,因为包含进位的情况,可能会有+1,所以每次计算sum需要带着进位一起
- 两个链表同时向后移动,计算它们val相加得结果,sum = l1.val + l2.val + jinwei,再创建个以sum为val的新节点拼接在新链表的后面
- 这里有多种情况:
- 两方都不为null,正常计算value,注意进位,同时再向后移动一格
- 两方都为null,观察最后有无进位,有进位得新增个val=1得节点在最后
- 其中一方为null,另外一方不为null,为null得一方默认val=0,另一方还得计算,因为会存在进位的情况,例如:9999,jinwei = 1, 最后是00001
- 综上所述,循环的条件根据双方长度还有进位情况决定。每次计算都得带着进位,为null的一方参与计算val默认为0
/**
* 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) {
int sum = l1.val + l2.val;
int jinwei = sum/10;
ListNode node = new ListNode(sum%10);
ListNode head = node;
l1 = l1.next;
l2 = l2.next;
while (l1 != null || l2 != null || jinwei == 1){
sum = (l1 == null ? 0 : l1.val) + (l2 == null ? 0 : l2.val) + jinwei;
jinwei = sum/10;
head.next = new ListNode(sum%10);
head = head.next;
if (l1 != null){
l1 = l1.next;
}
if (l2 != null){
l2 = l2.next;
}
}
return node;
}
}
146. LRU 缓存
难度:中等
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache
类:
LRUCache(int capacity)
以 正整数 作为容量capacity
初始化 LRU 缓存int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回-1
。void put(int key, int value)
如果关键字key
已经存在,则变更其数据值value
;如果不存在,则向缓存中插入该组key-value
。如果插入操作导致关键字数量超过capacity
,则应该 逐出 最久未使用的关键字。
函数 get
和 put
必须以 O(1)
的平均时间复杂度运行。
解题思路:
- 设计一个双向链表,维护热点数据,定义虚拟两端点,pre,tail
- 涉及这几个方法:
- 当有新的方法加入,而容量又满了,则需要剔除最近最少使用的数据 removeTail
- 当有新的方法加入,这就是最新的热点数据,需要放在头部 addToHead
- 当查询的时候,默认当前值为热点数据,需要移动到最前面 moveToHead,那原来的就需要从链表中删除 removeNode
- 设计的双向链表也有key,value值,key值得作用在于当你插入一个新节点,要把末尾节点给剔除,剔除节点得同时,还需要把map中得信息也删除,所以需要知道key,在节点中保存一份,这样就能方便获取了。value值的原因是map本身保留的是节点,并不保存value,当查询的时候,根据map拿到的是节点,所以为了返回value值,需要在节点中保存一份
- 插入一个新值,先去判断存不存在,去链表遍历查看太麻烦,所以用map保存
- 如果能查询的到,说明已经存在,只需要更新value值,并把当前值作为热点数据,在链表中找到并删除该值,然后把该值移动到最前面。为了方便操作链表,所以map根据key查找的value为链表节点,存的时候就是存链表节点
- 如果不能查询到,则新建一个节点,放入到头节点的后面 pre.next,同时map中也存一份。这里还要考虑到容量问题,如果超出容量,得删除一个最近最少使用得数据,这最少使用得数据也要从map中剔除
- 查找一个值的时候
- 如果不能找到,直接返回 -1
- 如果可以找到,value为链表节点,返回链表节点的value值,同时当前数据作为热点数据,进行删除并移动到链表开头的操作
class LRUCache {
// 定义一个双向链表
class DLinkedNode {
private int key;
private int value;
DLinkedNode pre;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int key,int value){
this.key = key;
this.value = value;
}
}
int capacity; // 容量
int size; // 现有大小
private HashMap<Integer,DLinkedNode> map; // 存值
DLinkedNode head; // 头部
DLinkedNode tail; // 尾部
public LRUCache(int capacity) {
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.pre = head;
map = new HashMap();
size = 0;
this.capacity = capacity;
}
public int get(int key) {
DLinkedNode node = map.get(key);
if (node != null){
moveToHead(node);
return node.value;
}
return -1;
}
public void put(int key, int value) {
DLinkedNode node = map.get(key);
if (node == null){
// 新建一个
DLinkedNode new_node = new DLinkedNode(key,value);
map.put(key,new_node);
addToHead(new_node);
++size;
if(size > capacity){
// 满了,需要删除尾部节点
DLinkedNode tail_node = removeTail();
map.remove(tail_node.key);
--size;
}
}else{
node.value = value;
moveToHead(node);
}
}
public void moveToHead(DLinkedNode node){
removeNode(node);
addToHead(node);
}
public void addToHead(DLinkedNode node){
node.pre = head;
node.next = head.next;
head.next.pre = node;
head.next = node;
}
public void removeNode(DLinkedNode node){
node.pre.next = node.next;
node.next.pre = node.pre;
}
public DLinkedNode removeTail(){
DLinkedNode res = tail.pre;
removeNode(res);
return res;
}
}
399. 除法求值
难度:中等
给你一个变量对数组 equations
和一个实数值数组 values
作为已知条件,其中 equations[i] = [Ai, Bi]
和 values[i]
共同表示等式 Ai / Bi = values[i]
。每个 Ai
或 Bi
是一个表示单个变量的字符串。
另有一些以数组 queries
表示的问题,其中 queries[j] = [Cj, Dj]
表示第 j
个问题,请你根据已知条件找出 Cj / Dj = ?
的结果作为答案。
返回 所有问题的答案 。如果存在某个无法确定的答案,则用 -1.0
替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用 -1.0
替代这个答案。
注意:输入总是有效的。你可以假设除法运算中不会出现除数为 0 的情况,且不存在任何矛盾的结果。
解题思路:
- 把所有除的可能都存到map中去,例如:a/c = 2,分别以a为key,把c和2作为value存入;以c为key,把a和1/2作为value
- 求得时候,递归得求解,注意不要重复求解,用set查重
class Solution {
class Pair{
String couple;
double value;
public Pair(String couple,double value){
this.couple = couple;
this.value = value;
}
}
HashSet<String> set = new HashSet();
Map<String,List<Pair>> map = new HashMap();
double[] res;
public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
for (int i=0;i<equations.size();i++){
String a = equations.get(i).get(0);
String b = equations.get(i).get(1);
List<Pair> list;
if (map.containsKey(a)){
list = map.get(a);
}else{
list = new ArrayList<Pair>();
}
list.add(new Pair(b,values[i]));
map.put(a,list);
List<Pair> list1;
if (map.containsKey(b)){
list = map.get(b);
}else{
list = new ArrayList<Pair>();
}
list.add(new Pair(a,1/values[i]));
map.put(b,list);
}
res = new double[queries.size()];
Arrays.fill(res,-1);
for (int i=0;i<queries.size();i++){
String a = queries.get(i).get(0);
String b = queries.get(i).get(1);
// 如果有一个不在集合里面,就表示不属于
if (!map.containsKey(a) || !map.containsKey(b)){
continue;
}
// 如果两个一样
if (a == b){
res[i] = 1;
continue;
}
dfs(a,b,1,i);
}
return res;
}
public void dfs(String start,String end,double mul,int i){
if (start.equals(end)){
res[i] = mul;
return;
}
List<Pair> list = map.get(start);
for (Pair p : list){
if (!set.contains(start)){
set.add(start);
dfs(p.couple,end,mul*p.value,i);
set.remove(start);
}
}
}
}
312. 戳气球
难度:困难
有 n
个气球,编号为0
到 n - 1
,每个气球上都标有一个数字,这些数字存在数组 nums
中。
现在要求你戳破所有的气球。戳破第 i
个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1]
枚硬币。 这里的 i - 1
和 i + 1
代表和 i
相邻的两个气球的序号。如果 i - 1
或 i + 1
超出了数组的边界,那么就当它是一个数字为 1
的气球。
求所能获得硬币的最大数量。
解题思路:
- 动态规划
- 规划的是一个区间最多能拿多少钱
- 动态规划就是如何找到个体,然后由个体膨胀到整体
- 假设整个区间只剩下气球K,左边右边都没有气球
- 那如果戳破气球K,最后能拿到的金钱就是气球左边之前戳破的最大金钱+气球右边之前戳破的最大金钱+戳破当前气球K得到得金额。K如果被戳破得到1*nums[k]*1,用动态规划来说,如果气球旁边有别的气球就得用个数表示,这里我们假设左边界 i ,右边界 j,nums[i] * nums[k] * nums[j],向第一个气球和最后一个气球就无法这么表示,所以我们把 i 和 j 往外拓展作为额外的头部和尾部
- 总的来说 dp[ i,k ] + nums[k] + dp[ k , j ]
- 重新定义一个包含额外头和尾的数组temp,也就是在原来数据向前向后个扩展1,temp[0] = 1,temp[n+1]=1
- 动态规划,从数组长度为3开始(因为至少包含一个气球,头和尾是虚拟的,所以整体长度是从3开始的)。既然是区间最大,就得定义个左端点,从左端点到最大长度为一个区间,动态计算赋值。
- 区间左端点从0出发,最大到n+2-len (n为原数组长度),这个得举例说明
- 总长度3,数组 [ i , k , j ],i 只能取 0,因为头尾是虚拟的,实际中间只有一个气球
- 总长度4,数组 [ i , k , k , j ], i 可以取 0 ,1
- 总长度5,数组 [ i , k , k , k,j ],i可以取0,1,2
- 例如总长度为5的时候,len长度为3,左端点就可以在0,1,2移动
- 总长度为5的数组[1,2,3.4,5],取len为3的时候,可以是[1,2,3],[2,3,4],[3,4,5]
- 由此左端点,在总长度固定的情况下,根据 len 值不同,取值不同 (n+2)-len
- k 即是区间中间的值,取值范围可以是左端点 i 的后一位到最后一位的前一位,就是下载i 和 j 中间,这里的 j 根据长度调整。
- 整个动态规划就是左半部分的钱+当前+右半部分的钱,由于左端点在移动,算出不同情况下当前区间的最大值
- 最终返回的就是整个区间范围的最大值
class Solution {
public int maxCoins(int[] nums) {
// 动态规划
// 规划的是单独看一个气球
// 假设剩下最后一个气球k,那么戳破最后一个球k,获得的金钱是 1*nums[k]*1,两边都没球了,只剩下边界所以就是1,为了方便,给两个1一个身份,左i,右j
// 那么它左边的已经早就戳破了,获得的金钱 dp[i,k]
// 那么它右边得已经早就戳破了,获得得金钱 dp[k,j]
// 此时的最大金钱就是dp[i,k] + nums[i]*nums[k]*nums[j] + dp[k,j]
int n = nums.length;
int[] temp = new int[n+2]; // 多了两个边界 i,j
temp[0] = 1; // 左端点i=1
temp[n+1] = 1; // 右端点j=1
// 转移到新的数组
for (int i=0;i<n;i++){
temp[i+1] = nums[i];
}
int[][] dp = new int[n+2][n+2];
// 长度从1开始,因为加了左右边界,所以如今从3开始
for (int len=3;len<=n+2;len++){
// 左端点可以从0开始,但是得在最后空出来k和j的位置,最小也是这个[i,k,j],随着长度增加i可以多向后移动
for (int i=0;i<=n+2-len;i++){
int max = 0;
// 定义一个索引K,k的取值夹在i,j中间,所以k可以从i+1取到i+len-1
for (int k=i+1;k<i+len-1;k++){
int left = dp[i][k];
int right = dp[k][i+len-1];
max = Math.max(max,left + temp[i]*temp[k]*temp[i+len-1] + right);
}
// 每次dp保留的就是区间的最大值,因为每次算k,都是左边+k+右边
dp[i][i+len-1] = max;
}
}
// 返回区间最大值
return dp[0][n+1];
}
}
297. 二叉树的序列化与反序列化
难度:困难
序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
提示: 输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。
解题思路:
- 采用DFS
- TreeNode 前序遍历,将结果拼接到字符串中,以逗号区分,遇到Null,赋值None
- String,字符串以逗号切割,按照前序遍历构造TreeNode
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
return serializeToString(root,"");
}
public String serializeToString(TreeNode root,String s){
if (root == null){
s += "None,";
}else{
s += "" + root.val + ",";
s = serializeToString(root.left,s);
s = serializeToString(root.right,s);
}
return s;
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
String[] data_c = data.split(",");
List<String> list = new ArrayList(Arrays.asList(data_c));
return deserializeToTreeNode(list);
}
public TreeNode deserializeToTreeNode(List<String> list){
if (list.get(0).equals("None")){
list.remove(0);
return null;
}
TreeNode node = new TreeNode(Integer.parseInt(list.get(0)));
list.remove(0);
node.left = deserializeToTreeNode(list);
node.right = deserializeToTreeNode(list);
return node;
}
}
4. 寻找两个正序数组的中位数
难度:困难
给定两个大小分别为 m
和 n
的正序(从小到大)数组 nums1
和 nums2
。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n))
。
解题思路:
- 该做法忽略了题目给的有序数组
- 直接把所有值分成两部分A、B;A保留一半的较大的值,B保留一半的较小的值
- A:从小到大排列的优先队列
- B:从大到小排列的优先队列
- 当A、B 最终大小不一样的时候,默认 A > B,中位数就是A的队列顶端的数
- 当大小一样的时候,就是A顶端和B顶端值的一半
- 插入的时候由于不知道新值大小,去向何处
- 当 A.size() = B.siz() 理应插入A,但应该先去B过滤一下,选出最大的进入A
- 当 A.size() ≠ B.siz() 理应插入B,但应该先去A过滤一下,选出最小的进入B
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
PriorityQueue<Integer> A = new PriorityQueue<>((o1,o2)->(o2-o1)); // 放入较大的那部分
PriorityQueue<Integer> B = new PriorityQueue<>(); // 放入较小的那部分
for (int n : nums1){
if (A.size() == B.size()){
B.offer(n);
A.offer(B.poll());
}else{
A.offer(n);
B.offer(A.poll());
}
}
for (int n : nums2){
if (A.size() == B.size()){
B.offer(n);
A.offer(B.poll());
}else{
A.offer(n);
B.offer(A.poll());
}
}
if (A.size() == B.size()){
return (A.peek()+B.peek())/2.0;
}else{
return A.peek()/1.0;
}
}
}
23. 合并K个升序链表
难度:困难
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
解题思路:
- 升序链表的加强版
- 用归并排序,分割成最小单元,然后两两链表合并,向上合并
/**
* 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 mergeKLists(ListNode[] lists) {
if (lists.length == 0)return null;
return divide(lists,0,lists.length-1);
}
public ListNode divide(ListNode[] lists,int left,int right){
if (left != right){
int mid = left + (right - left)/2;
ListNode leftListNode = divide(lists,left,mid);
ListNode rightListNode = divide(lists,mid+1,right);
return merge(leftListNode,rightListNode);
}
return lists[left];
}
public ListNode merge(ListNode node1,ListNode node2){
ListNode node = new ListNode(0);
ListNode head = node;
ListNode l1 = node1;
ListNode l2 = node2;
while (l1 != null && l2 != null){
if (l1.val < l2.val){
head.next = l1;
l1 = l1.next;
}else{
head.next = l2;
l2 = l2.next;
}
head = head.next;
}
if (l1 != null){
head.next = l1;
}
if (l2 != null){
head.next = l2;
}
return node.next;
}
}
76. 最小覆盖子串
难度:困难
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
解题思路:
- 滑动窗口
- 记录 t 中字母,以及出现次数,只有当窗口内 包含所有 t 字母,且字母出现次数对的上才算是一个覆盖子串。
- 双指针,右指针往右移动,直到符合上述条件。这时缩小左指针,因为左侧的字母可能是多余的,每移动一个字母,它的出现次数减1,直到减到小于 t 中字母应该出现的次数,这是才算是一个可能的最小覆盖子串,记录下当前长度,比较长度,更新最小
class Solution {
public String minWindow(String s, String t) {
int n = s.length();
int m = t.length();
if (s.equals(t))return t;
if (m > n)return "";
int[] t_temp = new int[58]; // 存储出现字母的数量
Set<Integer> set = new HashSet(); // 存储出现的字母
for (int i=0;i<m;i++){
set.add(t.charAt(i)-'A');
t_temp[t.charAt(i)-'A']++;
}
int[] s_temp = new int[58];
int left = 0;
int right = 0;
int min_len = Integer.MAX_VALUE;
String res = "";
while (right < n){
int currNum = s.charAt(right++)-'A';
// 如果相加过后的字母数量和t相同了,就可以把该字母从set中移除
if (++s_temp[currNum] == t_temp[currNum]){
set.remove(currNum);
}
// 当发现set中所有字母都有了,且数量也对了,说明当前字符串满足要求,可以缩减左边
while (set.size() == 0 && left < right){
int leftNum = s.charAt(left++)-'A';
// 表示当前字母不属于t,移除无关紧要
if (t_temp[leftNum] == 0)continue;
// 表示当前数字属于t,但是不是多余的不好说,可以移除试试
// 如果说减了过后,不符合了,说明缺失了一个,需要计算当前最小的字符串,同时set要把当前字母的加入
if (--s_temp[leftNum] < t_temp[leftNum]){
set.add(leftNum);
if (right-left+1 < min_len){
res = s.substring(left-1,right);
min_len = right - left + 1;
}
}
}
}
return res;
}
}
84. 柱状图中最大的矩形
难度:困难
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
解题思路:
- 维护一个栈,用来保存升序序列
- 最原始的做法,就是遍历每一根柱子,以当前柱子为高,左右拓展,拓展的旁边的柱子的高度只能大于等于当前高度
- 所以现在的问题就是如何确定左右边界
- 假设维护一个升序序列的栈,当遇到第一个柱子高度小于栈顶元素,也就是如果插入当前柱子,就构不成升序序列了,那就可以对之前的柱子做一个结算,因为右边界已经出现就是当前柱子,因为是升序,左边界就是栈顶之后的元素,所以最终可对栈中元素进行计算,把大于右边界的都弹出计算,直到栈中元素和当前柱子构成了升序序列
- 由于可能一直到最后都保持了升序,栈中的元素还没有结算,所以干脆在结尾处加个0,主动构成降序,这样就可以主动进行结算。在栈底也加入个0,作为左边界,因为如果中间存在一个很小的数,两边都计算完了,栈中就还剩这个元素,为了能继续计算,所以0这个左边界是必不可少的
class Solution {
public int largestRectangleArea(int[] heights) {
// 最大矩形根据短的一边决定的
int n = heights.length;
int[] f = new int[n+2];
f[0] = 0;
f[n+1] = 0;
for (int i=1;i<=n;i++){
f[i] = heights[i-1];
}
int res = 0;
Stack<Integer> stack = new Stack();
for (int i = 0; i < f.length; i++) {
// 每当降序的时候,进行一波结算
// 对于一个柱子来说,只要知道左边和右边,即宽度就能决定面积
// 高度以每个柱子的高为准,右边界就是第一个比当前值小的,左边界即左边第一个比当前值小的
// 也就是说,如果维护一个升序序列,当遇到第一个小于顶端的元素,则出现了右边界
// 因为是升序,所以顶部元素已经最大,以它为高的面积,就是它自己
// 直到和新的值重新构成一个升序序列
while (!stack.isEmpty() && f[stack.peek()] > f[i]) {
int cur = stack.pop();
res = Math.max(res, (i - stack.peek() - 1) * f[cur]);
}
// 小的都入栈,升序排列
stack.push(i);
}
return res;
}
}
85. 最大矩形
难度:困难
给定一个仅包含 0
和 1
、大小为 rows x cols
的二维二进制矩阵,找出只包含 1
的最大矩形,并返回其面积。
解题思路:
- 竖条来看,可以把原矩阵中连1进行一个累加,表示当前方块的高
- 例如:
1101
1101
1111
转换成:
1101
2202
3313
每一层从右往左遍历,当前的矩形的高就是当前数值,让后向左延申,直到遇到0为止或者遍历结束。这里注意:每往左拓展一下,高度应该维护一个最小高度,同时每次得计算面积,例如最后一层3313,从右往左,最左是3,然后是1,这时候高度就要变成1
class Solution {
public int maximalRectangle(char[][] matrix) {
int n = matrix.length;
int m = matrix[0].length;
int[][] dp = new int[n][m];
for (int j=0;j<m;j++){
dp[0][j] = Integer.parseInt(matrix[0][j]+"");
}
for (int i=1;i<n;i++){
for (int j=0;j<m;j++){
if (matrix[i][j] != '0'){
dp[i][j] = dp[i-1][j] + Integer.parseInt(matrix[i][j]+"");
}
}
}
int res = 0;
for (int i=0;i<n;i++){
for (int j=m-1;j>=0;j--){
int width = 0;
int height = n+1;
int x = j;
while (x >= 0 && dp[i][x] != 0){
width++;
height = Math.min(height,dp[i][x]);
res = Math.max(res,height*width);
x--;
}
}
}
return res;
}
}
124. 二叉树中的最大路径和
难度:困难
路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root
,返回其 最大路径和 。
解题思路:
- 对于一个节点来说,因为要往上合并,所以只能选一条路,对当前做贡献得路径要么从左子树要么从右子树,就看两边谁得大
- 对于一条路径来说,可以从左子树经过根节点穿到右子树,每经过一个节点就要计算一下这种可能性,求最大值
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int res = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
// 路径,要么选左,要么选右,要么穿过根节点
// 如果左右路径为负值,那就是0,如果为正值那就选取
dfs(root);
return res;
}
public int dfs(TreeNode root){
if (root == null){
return 0;
}
int left = Math.max(0,dfs(root.left));
int right = Math.max(0,dfs(root.right));
int len = left + right + root.val;
res = Math.max(res,len);
return root.val + Math.max(left,right);
}
}
239. 滑动窗口最大值
难度:困难
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
解题思路:
- 用一个队列维护一个降序序列,队列头部的是滑动窗口中最大值
- 例如:
[1,3,-1,-3,5,3,6,7],k=3- 先挨个插入,直到满足一个窗口,一开始的窗口 [1,3,-1]。队列先插入1,然后插入3,按照原则队列必须是降序的,队首肯定是最大值,所以把1弹出,3放入,然后插入-1,跟在3后面,目前最大值就是队首的3
- 窗口移动,插入-3,跟在-1后面,移除1,因为1不在队首,不是最大值所以无需操作
- 窗口移动,3被移除,3正好是队首,3从队列中移除,目前最大了就是-1,加进来一个5,把前面的-3,-1都顶了出去,这时候队列就剩下一个5,窗口中最大的也就是5
- 接着循环到末尾,队列头部始终是最大值,只有当队首的元素滑出后,才会从队首移除
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
Deque<Integer> deque = new LinkedList();
int[] res = new int[nums.length - k + 1];
int left = 0;
int i = 0;
for (;i<k;i++){
while (!deque.isEmpty() && deque.peekLast() < nums[i]){
deque.pollLast();
}
deque.addLast(nums[i]);
}
res[left++] = deque.peekFirst();
for (;i<nums.length;i++){
if (nums[i-k] == deque.peekFirst()){
deque.pollFirst();
}
while (!deque.isEmpty() && deque.peekLast() < nums[i]){
deque.pollLast();
}
deque.addLast(nums[i]);
res[left++] = deque.peekFirst();
}
return res;
}
}
301. 删除无效的括号
难度:困难
给你一个由若干括号和字母组成的字符串 s
,删除最小数量的无效括号,使得输入的字符串有效。
返回所有可能的结果。答案可以按 任意顺序 返回。
解题思路;
- ()是一对,出现( 就左括号数+1,出现 )就左括号数-1,如果在( 没出现前就出现了),那就右括号数+1,这样就得到了应该删除的括号数量
- DFS
- 判断左括号和右括号要删除的数量,挨个删除,对应数量-1,最终删完得到的字符串判别下是否是正确有效的就行
class Solution {
public List<String> removeInvalidParentheses(String s) {
List<String> res = new ArrayList();
// 记录不合理的左右括号
int lNum = 0;
int rNum = 0;
// 每个)理应出现在(后面
for (int i=0;i<s.length();i++){
if (s.charAt(i) == '('){
lNum++;
}else if (s.charAt(i) == ')'){
if (lNum == 0){
rNum++; // 如果lNum=0,表示)出现在了开头,记录次数
}else{
lNum--; // 可以和前面的(抵消
}
}
}
dfs(s,0,res,lNum,rNum);
return res;
}
public void dfs(String s,int begin,List<String> res,int lNum,int rNum){
// 表示已经被修正了
if (lNum == 0 && rNum == 0){
if (Valid(s)){
res.add(s);
}
return;
}
for (int i=begin;i<s.length();i++){
if (i > 0 && s.charAt(i) == s.charAt(i-1)){
continue;
}
// 剩余的
if (lNum + rNum > s.length()-i)return;
if (lNum > 0 && s.charAt(i) == '('){
dfs(s.substring(0,i)+s.substring(i+1),i,res,lNum-1,rNum);
}
if (rNum > 0 && s.charAt(i) == ')'){
dfs(s.substring(0,i)+s.substring(i+1),i,res,lNum,rNum-1);
}
}
}
public boolean Valid(String s){
int count = 0;
for (int i=0;i<s.length();i++){
if (s.charAt(i) == '('){
count++;
}else if (s.charAt(i) == ')'){
if (count == 0)return false;
count--;
}
}
return count == 0;
}
}
42. 接雨水
难度:困难
给定 n
个非负整数表示每个宽度为 1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
解题思路:
- 很好理解,蓄水池,蓄水量根据左右两边决定,找到左边的最高边界,找到右边的最高边界,蓄水量就是左右两边最高取矮的那个 减去 当前的高度
- 如果两边有一边小于等于当前高度则不能蓄水
class Solution {
public int trap(int[] height) {
int n = height.length;
int sum = 0;
for (int i=0;i<n;i++){
// 找到左侧高点
int leftMax = height[i];
for (int j=i-1;j>=0;j--){
leftMax = Math.max(leftMax,height[j]);
}
// 找到右侧高点
int rightMax = height[i];
for (int j=i+1;j<n;j++){
rightMax = Math.max(rightMax,height[j]);
}
// 两个当中只能选矮的
int h = Math.min(leftMax,rightMax);
// 只有两边高于目前的柱子,当前柱子才能储水
if (h > height[i]){
sum += h - height[i];
}
}
return sum;
}
}
72. 编辑距离
难度:困难
给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
解题思路:
- 动态规划
如果其中一个字符串是空串,那么编辑距离是另一个字符串的长度。比如空串“”和“ro”的编辑距离是2(做两次“插入”操作)。再比如"hor"和空串“”的编辑距离是3(做三次“删除”操作)。
知道"abcd"变成"fgh"多少步(假设X步),那么从"abcde"到"fgh"就是"abcde"->"abcd"->"fgh"。(一次删除,加X步,总共X+1步)
知道"abcde"变成“fg”多少步(假设Y步),那么从"abcde"到"fgh"就是"abcde"->"fg"->"fgh"。(先Y步,再一次添加,加X步,总共Y+1步)
知道"abcd"变成“fg”多少步(假设Z步),那么从"abcde"到"fgh"就是"abcde"->"fge"->"fgh"。(先不管最后一个字符,把前面的先变好,用了Z步,然后把最后一个字符给替换了。这里如果最后一个字符碰巧就一样,那就不用替换,省了一步)
class Solution {
public int minDistance(String word1, String word2) {
int len1 = word1.length();
int len2 = word2.length();
int[][] dp = new int[len1+1][len2+1];
for (int i=0;i<=len1;i++){
dp[i][0] = i;
}
for (int j=0;j<=len2;j++){
dp[0][j] = j;
}
for (int i=1;i<=len1;i++){
for (int j=1;j<=len2;j++){
if (word1.charAt(i-1) == word2.charAt(j-1)){
dp[i][j] = Math.min(dp[i-1][j]+1,Math.min(dp[i][j-1]+1,dp[i-1][j-1]));
}else{
dp[i][j] = Math.min(dp[i-1][j]+1,Math.min(dp[i][j-1]+1,dp[i-1][j-1]+1));
}
}
}
return dp[len1][len2];
}
}
32. 最长有效括号
难度:困难
给你一个只包含 '('
和 ')'
的字符串,找出最长有效(格式正确且连续)括号子串的长度。
解题思路:
- 用一个栈来维护
- 左括号直接加入,遇到右括号就弹出,这样左括号和右括号就可以成对的匹配
- 为了防止一开始就遇到一个右括号,那就在栈中存一个-1,弹出-1后栈为空,之后如果又遇到右括号就又不行了,所以将当前坐标也放入栈中,充当一个左边界
- 正确的情况下,例如一开始栈中有-1,()成对的匹配后,栈中始终会有元素留存,这样一来表示从当前位置到留存的元素位置,这之间是一个有效的括号子串
class Solution {
public int longestValidParentheses(String s) {
Stack<Integer> stack = new Stack();
stack.push(-1);
char[] c = s.toCharArray();
int max = 0;
for (int i=0;i<c.length;i++){
if (c[i] == '('){
stack.push(i);
}else {
// 防止第一位就是)的话,也可以弹出,-1
stack.pop();
if (stack.isEmpty()){
// ()配对的话正好没有,又因为一开始有个-1的缘故,如果配对弹出后,栈中剩余一个-1,而如今不剩余,说明把-1弹出了
// 当前位置就代表是一个左边界
stack.push(i);
}else{
// 说明栈中留了一个底,从底开始到现在的位置就是一个正确的字符串
max = Math.max(max,i-stack.peek());
}
}
}
return max;
}
}