- 一.解法
- 二.代码
- 2.两数相加">2.两数相加
- 5.最长回文子串">5.最长回文子串
- 15.三数之和">15.三数之和
- 17. 电话号码的字母组合">17. 电话号码的字母组合
- 19.删除链表的倒数第 N 个结点">19.删除链表的倒数第 N 个结点
- 39.组合总和">39.组合总和
- 42.接雨水">42.接雨水
- 46.全排列">46.全排列
- 75.颜色分类">75.颜色分类
- 98.验证二叉搜索树">98.验证二叉搜索树
- 169.多数元素">169.多数元素
- 206.反转链表">206.反转链表
- 128.最长连续序列">128.最长连续序列
- 139.单词拆分">139.单词拆分
- 141.环形链表">141.环形链表
- 207.课程表">207.课程表
- 208.实现 Trie (前缀树)">208.实现 Trie (前缀树)
- 234. 回文链表">234. 回文链表
- 236.二叉树的最近公共祖先">236.二叉树的最近公共祖先
- 238. 除自身以外数组的乘积">238. 除自身以外数组的乘积
- 461.汉明距离">461.汉明距离
一.解法
1.1-10
| 序号 | 题目 | 描述 | 解法 | 难度 |
|---|---|---|---|---|
| 1 | 1.两数之和 | 一个数组和一个目标值target,找到数组中和为目标数的两个数,并返回数组下标,若有多对和为target,找出所有的组合并返回其下标,结果中不能有重复的组合, | 注意:数组不是有序 (1)哈希表{num:index}+set去重:元素值作为key,下标作为value存入map中,扫描数组,当前元素val,下标pos,查看map中是否存在target-x的key且set中是否存在target-x, 若map中存在target-x,且set中不存在target-x则找到一对(val, target-val)且set.add(target-x),若map中不存在则map.put(val,pos),继续处理数组下一个元素直到找到所有对,时间O(N) 使用set是为了去重,当找到一对(x,y)后,就要将x放入set中,当下次再次遇到y时,x已经存在于set中就知道(x,y)已经加入结果集了。 (2)枚举:枚举所有两个元素组合并判断是否符合条件,时间O(N*N) |
简单 |
| 2 | 2.两数相加 | 给两个链表,分别表示两个非负数,每个节点存储一位,逆序表示一个数(链头是个位,链尾是最高位),以一个逆序链表的形式返回两数相加的和 | (1)双指针: 从两个链表的链头开始模拟相加,使用进位carry,sum = p1.val+p2.val+carry,表示和的链表的当前位置的值currentVal = sum%10,进位carry=sum/10 注意链表x和y可能不一样长, 如果循环是必须两个链表同步一起走的话,需要对x、y补偿扫描一遍,一样计算carry、sum、和链表当前位置 注意扫描x、y完毕后若carry为·1,则和链表还要加上一个值为1的节点 注意使用dummy节点方便处理头节点的尾插入 |
中等 |
|---|---|---|---|---|
| 3 | 3.无重复字符的最长子串 | 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。 | (1)滑动窗口,使用滑动窗口模板,map保存窗口中每个字符及其下标,{char:index},每次移动右指针right都要更新left, left = max(map.get(left)+1, left) 然后更新最长子串长度。 maxLen = max(maxLen, right-left+1) |
中等 |
|---|---|---|---|---|
| 4 | 4.寻找两个正序数组的中位数 | 困难 | ||
|---|---|---|---|---|
| 5 | 5.最长回文子串 | 给你一个字符串 s,找到 s 中最长的回文子串 | (1)扩展:枚举所有的「回文中心」并尝试「扩展」,直到无法扩展为止,此时的回文串长度即为此「回文中心」下的最长回文串长度,对位置i可以尝试以i同时向左右扩展或从(i,i+1)向左右扩展,当扩展的左右两边字符不同时停止扩展。 (2)动态规划:设f(i,j)表示下标i到j既[i,j]的子串是否为回文子串 f(i,j) = f(i+1, j-1) && s[i] == s[j] 基准情况:f(i,i)=true; f(i,i+1) = s[i]==s[i+1] 代码:根据转移方程可以看出长子串是由短子串转移得到,所以外层循环枚举start的增量Increment,内层循环枚举start值,根据Increment=0、Increment=1、Increment>1来分情况讨论,时间O(N*N) |
中等 |
|---|---|---|---|---|
| 6 | 10.正则表达式匹配 | 困难 | ||
|---|---|---|---|---|
| 7 | 11.盛最多水的容器 | ![]() |
(1)双指针+贪心:每次移动较低的指针,因为移动较高的指针,容积只能变小。 移动过程中统计全局最大容积即可,时间O(N) |
中等 |
|---|---|---|---|---|
| 8 | 15.三数之和 | 给一个数组,判断是否其中存放三个元素使得和为0,找到所有三元组 | 注意:数组中可能有重复元素 (1)排序+双指针:外层枚举第一个元素a,去重(重复元素跳过),内层循环使用双指针left、right枚举第二、第三个元素b、c,当找到一组符合条件的b、c时要让left向移动跳过所的相同的b,让right向左移动跳过所有的相同的c。 |
中等 |
|---|---|---|---|---|
| 9 | 17.电话号码的字母组合 | 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。![]() |
(1)递归+回溯:在叶子节点时将当前组合加入到结果集中,对每一个数字(也是一个节点)遍历其映射的所有可能字符。 | 中等 |
|---|---|---|---|---|
| 10 | 19.删除链表的倒数第 N 个结点 | 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。 | (1)双指针+help节点:指针pre、指针p1、p2,pre为p1前驱,先让p2跑n-1步,然后p1、p2一起跑,当p2为尾节点时(不为null),p1就是倒数第n个节点。 注意,因为要删除倒数第n个节点,所以要得到p1的前驱p0,而被删除的节点p1可能是头节点,所以必须用辅助节点helpNode作为头节点head的前驱,并在p1、p2移动时同步移动pre。 |
中等 |
|---|---|---|---|---|
2.11-20
| 11 | 20.有效的括号 | 给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串 s ,判断字符串是否有效。 有效字符串需满足: 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合 |
(1)栈:扫描字符串,对左括号入栈,遇到右括号时,若栈顶为对应的左括号,则栈顶出栈,否则该右括号入栈,遍历完字符串后,若栈为空则有效 | 简单 |
|---|---|---|---|---|
| 12 | 21.合并两个有序链表 | 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 | (1)迭代:merge+help节点尾插法 https://leetcode-cn.com/submissions/detail/20600755/ (2)递归: https://leetcode-cn.com/submissions/detail/47705058/ 注意代码和leetcode-2两数相加的区别。 |
简单 |
|---|---|---|---|---|
| 13 | 22.括号生成 | 数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。 | 递归+回溯:使用leftCount和rightCount统计从左到右扫描到的左括号和右括号的数量,当leftCount<max时可以附加一个左括号,当rightCount<leftCount时可以附加一个右括号 | 中等 |
|---|---|---|---|---|
| 14 | 23.合并K个升序链表 | (1)堆:用堆以O(k)时间取得k个链表的当前最小值,外排序思路。 | 困难 | |
|---|---|---|---|---|
| 15 | 31.下一个排列 | 将给定数字序列重新排列成字典序中下一个更大的排列,若不存在则将数字重新排列成最小的排列(即升序排列) | (1)要得到更大的排列,所以要将右边较大的数和左边较小的数交换,从右到左扫描,扫描到第一个顺序对(i, i+1),i是下标,使得nums[i] |
中等 |
|---|---|---|---|---|
| 16 | 32.最长有效括号 | 困难 | ||
|---|---|---|---|---|
| 17 | 33.搜索旋转排序数组 | 数组nums,值互不相同,在某个下标进行了旋转,将右边部分放到了左边,查看旋转后的数组中是否存在目标值target | (1)二分:旋转后的数组,使用二分的mid将其分成两部分,找到有序的那部分,判断这部分是否包含target即可每次搜索都排除掉一半元素,left=0,right=len-1,若nums[left]<=nums[mid]则[left, mid]是有序部分,否则[mid, right]是有序部分 | 中等 |
|---|---|---|---|---|
| 18 | 34.在排序数组中查找元素的第一个和最后一个位置 | 给一个升序排列的整数数组 nums,一个目标值 target。找出给定目标值在数组中的开始位置和结束位置 | (1)二分:找开始位置,当nums[mid]==target时继续向左找,则right=mid+1,当left>right时,left就是第一个位置;找结束位置,当nums[mide]==target时继续向右找,则left=mid+1,当left>right时,right就是结束位置 | 中等 |
|---|---|---|---|---|
| 19 | 39.组合总和 | 无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合,数字可以无限制重复被选取 | (1)递归+回溯:看代码部分 (2)递归+回溯+循环:看代码部分 |
中等 |
|---|---|---|---|---|
| 20 | 42.接雨水 | 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。![]() |
思路:得到每个柱子左边和右边的最大高度就可以得到该柱子能盛的水量了。 (1)动态规划:使用两个数组,leftMax[i]表示位置i及其左边位置的最大高度,rightMax[i]表示位置i及其右边位置的最大高度 leftMax[i]=max(leftMax[i-1], height[i]); rightMax[i]=max(rightMax[i+1], height[i]); 位置i能盛的水量为min(leftMax[i],rightMax[i]) - height[i],所以遍历height数组得到所有位置的盛水量即可 (2)单调栈:难理解 (3)双指针:难理解 (4)最大值+分割为2部分:扫描一趟,得到最大的height[x],位置x就是最高点,然后将其分割为两部分, 左部分从左向右遍历,计算当前位置i及其左边的最大值leftMax,能盛容量leftMax-height[i]; 右部分从右向左遍历,计算当前j及其右边的最大值rightMax,能盛容量rightMax-height[j] |
困难 |
|---|---|---|---|---|
3.21-30
| 21 | 46.全排列 | 给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 | (1)递归+回溯+循环:使用boolean[] visited数组visited[i]为true表示i位置元素已经被使用,不能再次使用,当前排列current.size()=n时加入结果集。 | 中等 |
|---|---|---|---|---|
| 22 | 48.旋转图像 | 将N*N矩阵顺时间旋转90度,要空间复杂度O(1) | (2)水平翻转+主对角线翻转: 顺时针旋转后(x,y)->(y, n-x),水平翻转(x,y)->(n-x,y),主对角线翻转(x,y)->(y,x);所以水平+主对角线翻转(x,y)->(y,n-x) |
中等 |
|---|---|---|---|---|
| 23 | 49.字母异位词分组 | 给定一个字符串数组,将字母异位词组合在一起。可以按任意顺序返回结果列表。 “字母异位词”指字母相同,但排列不同的字符串。 |
思路:用一种同一的方式表示同种所有字母异位词,比如用一种方式表达abc、bac、cab、… (1)排序+hash表{key:[]}:将一个字符串s按照字符的字典顺序排序后得到的字符串s0作为key,然后将s作为一个元素加入到值list中,时间复杂度O(klogkN),k是字符串的平均长度,N是字符数量,空间O(Nk) (2)计数+hash表{key:[]}:统计将一个字符串s的所有字符种类个数,然后构造一个标识每种字符个数的标识字符s0,比如babt就标识为1a2b1t,然后将s0作为key存入hash表,将s作为该key的一个元素存入其值的list中 |
中等 |
|---|---|---|---|---|
| 24 | 53.最大子序和 | 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和 | (1)动态规划:设dp[i]为以位置i结尾的最大和连续子数组,则dp[i] = dp[i-1]>0?(dp[i-1]+d[i]):dp[i],统计所有dp[i]最大值就是整个数组的最大连续子数组和 | 简单 |
|---|---|---|---|---|
| 25 | 55.跳跃游戏 | 给定一个非负整数数组 nums ,最初位于数组0位置 ,数组中的每个元素代表在该位置可以跳跃的最大长度,判断你是否能够到达最后一个下标 | (1)贪心:维护一个当前能达到的最远位置maxFar,从0开始遍历,遍历到位置i时判断maxFar=len-1,则能达到,然后更新maxFar=max(maxFar,nums[i]) | 中等 |
|---|---|---|---|---|
| 26 | 56.合并区间 | 数组 intervals 表示若干个区间的集合,合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区 | (1)排序+重建:对数组intervals按照每个区间的starti升序排列,相同的starti按照endi升序排列,然后重建,扫描intervals的每一个区间[starti, endi],若[starti,endi]和结果集(尾插法)的尾区间[startTail, endTail]重叠,既starti<=endTail时。就将[starti,endi]和[startTail, endTail]融合再放入结果集,然后继续扫描intervals,融合的新区间为[startTail, max(endTail,endi) | 中等 |
|---|---|---|---|---|
| 27 | 62.不同路径 | 给一个m x n矩阵,从位置(0,0)走到(m,n)有多少种走法,每次只能向下或向右走一步 | (1)动态规划:设dp[i][j]为从(0,)走到(i,j)的所有走法,则 dp[i][j] = dp[i-1][j] + dp[i][j-1] 当j==0时,d[i][j] = dp[i-1][j] 当i==0时,dp[i][j] = d[i][j-1] |
中等 |
|---|---|---|---|---|
| 28 | 64.最小路径和 | 非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。 | (1)动态规划:设dp[i][j]为从(0,0)走到(i,j)的最小数字和路径,则 dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j] 当j==0时,d[i][j] = dp[i-1][j] + grid[i][j] 当i==0时,dp[i][j] = d[i][j-1]+ grid[i][j] |
中等 |
|---|---|---|---|---|
| 29 | 70.爬楼梯 | 楼梯有 n 阶,每次可爬 1 或 2 个台阶,有多少种不同的方法可以爬到楼顶 | (1)动态规划(斐波那契数列):dp[n]为爬到第n阶的方法,则 dp[n] = dp[n-1]+dp[n-2] n==1时,dp[1] = 1 n==2时,dp[2] = 2 |
简单 |
|---|---|---|---|---|
| 30 | 72.编辑距离 | 给两个单词 word1 和 word2,计算出将 word1 转成 word2 所使用的最少操作数 。 可对一个单词进行如下三种操作: 插入一个字符 删除一个字符 替换一个字符 |
(1)动态规划: | 困难 |
|---|---|---|---|---|
4.31-40
| 31 | 75.颜色分类 | 只包含数字0、1、2的数字,原地对其排序 | (1)单指针:第一次遍历中将数组中所有的 0 交换到数组的头部;第二次遍历中,我们将数组中所有的 1交换到头部的 0 之后 (2)双指针(三向切分快排)-1: (2)双指针(三向切分快排)-2: |
中等 |
|---|---|---|---|---|
| 32 | 76.最小覆盖子串 | 给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” | (1)滑动窗口:模板,sMap,tMap,valid变量表示s的窗口中的字符种类满足要求的数量,valid的增减规则,向右移动right扩大窗口寻找可能解,向右移动left缩小窗口寻找最优解 | 困难 |
|---|---|---|---|---|
| 33 | 78.子集 | 给一个整数数组 nums ,其元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集 | (1)递归+回溯: 在叶子节点时才将临时组合加入结果集 (2)递归+回溯+循环: 每个临时组合都加入结果集 |
中等 |
|---|---|---|---|---|
| 34 | 79.单词搜索 | 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false | (1)回溯:状态数组status[m][n]标识每个位置是否已经被使用 | 中等 |
|---|---|---|---|---|
| 35 | 84.柱状图中最大的矩形 | 困难 | ||
|---|---|---|---|---|
| 36 | 85.最大矩形 | 困难 | ||
|---|---|---|---|---|
| 37 | 94.二叉树的中序遍历 | 中序遍历的迭代算法 | https://www.yuque.com/yingge-azgaz/ustyhx/wcsolx | 简单 |
|---|---|---|---|---|
| 38 | 96.不同的二叉搜索树 | 给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种? | (1)动态规划: G(n): 长度为 n的序列能构成的不同二叉搜索树的个数;G(n)=i从1取到n的F(i,n)的累积和 F(i, n): 以 i为根、序列长度为 n 的不同二叉搜索树个数 ,给定序列 1⋯n,我们选择数字 i作为根,则根为 i的所有二叉搜索树的集合是左子树集合和右子树集合的笛卡尔积 F(i,n) = G(i-1)G(n-i); 将G(n)和F(i,n)综合得到G(n) = n从1取到n的 G(i-1)G(n-i)的累积和 初始化: G(1)=1; G(1)=G(1-1)*G(1-1) = G(0) = 1; 所以G(0) = 1; |
中等 |
|---|---|---|---|---|
| 39 | 98.验证二叉搜索树 | 给定一个二叉树,判断其是否是一个有效的二叉搜索树。 | (1)递归:设计一个helper函数,help(root, lowBorder, highBorder);每个节点值都要满足lowBorder 遍历左子树 help(root.left,-Inf, root.val), 遍历右子树 help(root.right, root.val, +Inf) 输入是int时,用Long.MIN_VALUE、Long.MAX_VALUE作为-Inf、+Inf (2)中序遍历:二叉排序树的中序遍历是一个升序排列,所以对BST进行中序遍历,只要遍历到的每个节点都大于上一个节点值即可 |
中等 |
|---|---|---|---|---|
| 40 | 101.对称二叉树 | 给定一个二叉树,检查它是否是镜像对称的。 | (1)递归: 一棵树要是镜像,则其右子树和左子树要互为镜像 两棵树要互为镜像,则根节点值相等且每棵树的左子树都要和另一颗树的右子树互为镜像。 |
简单 |
|---|---|---|---|---|
5.41-50
| 41 | 102.二叉树的层序遍历 | (1)队列+迭代:注意二叉树层序遍历的三个题,正常型、锯齿形、自底向上型 | 中等 | |
|---|---|---|---|---|
| 42 | 104.二叉树的最大深度 | 给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数 | (1)递归: 设F(node)为node节点的最大深度 则F(node) = max(F(node.left), F(node.right) + 1 |
简单 |
|---|---|---|---|---|
| 43 | 105.从前序与中序遍历序列构造二叉树 | 给定一棵树的前序遍历 preorder 与中序遍历 inorder。请构造二叉树并返回其根节点。 | (1)递归: 前序遍历的第一个节点是根节点root 使用root可以将中序序列分成左部分inLeftSequence既左子树中序序列 右部分inRightSequence既右子树中序序列 根据 inLeftSequence的长度可以从前序序列preSequence中得到root的前序序列左部分preLeftSequence和右部分preRightSequence,然后递归构建左子树和右子树。 |
中等 |
|---|---|---|---|---|
| 44 | 114.二叉树展开为链表 | 将二叉树以先序遍历的顺序展开为一个链表, 要求空间O(1),right 子指针指向链表中下一个结点,而左子指针始终为 null,展开后的链表是先序遍历顺序 |
(1)递归:递归先序遍历,维护一个全局的preNode,但是空间是O(N) (2)寻找前驱节点: 对于节点node,若node左子节点不为空,则在左子树中找到最右边的节点nodeMaxRight,作为右子树的前驱节点,既nodeMaxRight.right = node.right 然后将node的左子树作为右子树,并将左子树置空,既node.right = node.left; node.left = null; 对当前节点处理结束后,继续处理链表中的下一个节点,直到所有节点都处理结束。 |
中等 |
|---|---|---|---|---|
| 45 | 121.买卖股票的最佳时机 | 数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格,你可以在x天买入,在第y天卖出,x<y,求能得到的最大收益 | (1)动态规划-股票通解:T[i][k][0],T[i][k][1],第i天,最多交易k手,交易完成后第i天还剩0个或1个股票 (2)贪心:因为只能交易一手,所以可以在最天之前的最低时买入,并在当前卖出,统计全局最大收益。 |
简单 |
|---|---|---|---|---|
| 46 | 124.二叉树中的最大路径和 | 路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列,路径 至少包含一个 节点,且不一定经过根节点,路径和 是路径中各节点值的总和 | (1)递归:定义函数pathSum(node)为从节点node出发一直向下的最大路径和,througthPathSum(node)为穿过node的最大路径和 则througthPathSum(node) = pathSum(node.left ) + pathSum(node.right) + node.val,统计每个节点node的througthPathSum,统计全局最大值即可 |
困难 |
|---|---|---|---|---|
| 47 | 128.最长连续序列 | 给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。既从输入中重新组合为最长连续升序序列的长度 输入:nums = [0,3,7,2,5,8,4,6,0,1] 输出:9 |
(1)哈希表:用set保存数组nums的所有元素,遍历数组nums,对当前元素x尝试在set中寻找x、x+1、x+2、…是否存在,然后遍历nums下一个元素 优化:如果前面已经找到了x、x+1、x+2、…,那么后面从x+1开始寻找开始肯定不能找到比前面更长的结果了,所以遍历到y时判断set中是否存在y-1,若存在则跳过。 时间O(N) |
中等 |
|---|---|---|---|---|
| 48 | 136.只出现一次的数字 | 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。 | (1)set: 扫描数组,当前扫描元素x,若set中不存在x就set.add(x);若存在就从set中删除x,最后剩下一个元素就是答案 (2)map:存放{num:count},记录每个数出现的次数,最后出现一次的key就是答案 (3)异或:相同元素异或为0,与0异或得本身,异或满足交换律和结合律 将所有元素异或,结果就是出现一次的数 |
简单 |
|---|---|---|---|---|
| 49 | 139.单词拆分 | 给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词 | (1)动态规划+hash表:设dp[i]表示s的下标区间[0,i-1]子串是否可以被空格拆分后都出现在字典中, 则dp[i] = dp[j] && check(j+1, i),jcheck(j+1, i)表示检查s的子串下标区间(j,i-1)是否出现在字典中(使用hash表) dp[0] = true,表示空字串存在于字典中,主要是为了表示字符串s本身是否在字典中,因为dp[len] = dp[0] && check(1,len),若字符串本身在字典中,则dp[len]=true,则dp[0]也为true |
中等 |
|---|---|---|---|---|
| 50 | 141.环形链表 | 给定一个链表,判断链表中是否有环。 | (1)快慢指针: 慢指针slow,快指针fast,慢指针slow步长为1,快指针fast步长为2,slow、fast都从head出发各走一步,然后继续走,若有环则fast、slow会相遇,若无环则不会相遇 |
简单 |
|---|---|---|---|---|
6.51-60
| 51 | 142.环形链表 II | 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。 | (1)快慢指针:设慢指针slow,快指针fast都从O出发,slow以步长1出发,fast以步长2出发。 起点O,环入口A,快慢指针相遇的第一个点B,指针在环内顺时针运行,O->A长a,A->B长b,B->A长c,则2(a+b) = a+b+n(b+c)=> a+b = n(b+c)=> a = (n-1)b+nc,所以让快慢指针slow从O,快指针fast从B一起出发,都以步长1跑,则相遇于A。 |
中等 |
|---|---|---|---|---|
| 52 | 146.LRU 缓存机制 | 实现 LRUCache 类 LRUCache(int capacity) 以capacity作为容量初始化 LRU 缓存 int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」,当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值 |
(1)双向链表+hash表 get方法的O(1)使用hash表来提供,put方法的O(1)使用双向链表来提供 put方法: 检测key是否存在,若key存在,更新map中key的value,并把链表中的key移动到队尾;若key不存在,判断map的大小是否超过容量,若超过容量则删除队尾的keyTail,并在map中删除keyTail,若没超过容量,在map中插入key-value,并在队尾插入key。 get方法: 若key不存在,返回-1 若key存在,将链表中的key移动到队尾,并返回map.get(key) |
中等 |
|---|---|---|---|---|
| 53 | 148.排序链表 | 要求在O(nlogn)时间内队链表进行升序排序 | (1)递归+快慢指针+归并merge: 使用快慢指针的方法找到链表的中点,然后将链表分成两部分,分别对其排序后进行merge。 merge就是将两个升序链表归并为升序链表的模板 注意:递归出口应该是node为null或node.next为null;要将链表分为两部分,就得找到中点的前驱pre,以为要置pre.next = null才能将链表切断 |
中等 |
|---|---|---|---|---|
| 54 | 152.乘积最大子数组 | 给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组,返回其乘积 | (1)动态规划:因为输入数组nums中可能有负数,设两个数组maxArr[n],minArr[n],maxArr[i]表示以i位置结尾的最大连续子数组的乘积,minArr[i]表示以i位置结尾的最小连续子数组的乘积,则 maxArr[i] = max(nums[i], maxArr[i-1]nums[i], minArr[i-1]nums[i]) minArr[i] = min(nums[i], maxArr[i-1]nums[i], minArr[i-1]nums[i]) 最后统计全局maxArr[i]最大值即可 |
中等 |
|---|---|---|---|---|
| 55 | 155.最小栈 | 设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。 | (1)辅助栈minStack:使用一个辅助栈minStack,当push一个元素进入stack时,就把当前最小值min也push进minStack,而从stack pop一个元素时也执行minStack.pop() 当前最小元素的判定: 当minStack.size()==0时,当前元素ele就是minEle 当minStack.size()>0时,minEle= min(minStack.peek(), ele) |
简单 |
|---|---|---|---|---|
| 56 | 160.相交链表 | 给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null | (1)双指针+交替遍历:指针p1指向headA,p2指向headB,一起遍历,循环条件是p1!=p2, 当p1不为空时,p1 = p1.next,p1为空时p1 = headB,当p2不为空时,p2=p2.next,否则p2 = headA。 退出循环后,p1和p2要嘛相交于两个链表的交点,要嘛都是null,返回p1或p2 |
简单 |
|---|---|---|---|---|
| 57 | 169.多数元素 | 给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。假设数组是非空的,并且给定的数组总是存在多数元素 | (1)map: 使用一个map保存每个数字出现次数{num:count},返回出现最多次数的那个数,时间O(N),空间O(N) (2)摩尔投票法:用一个变量num记录当前遍历过的数字,count记录该数字的次数,初始化count=0, 当遍历一个数字x时,若count==0,则置num=x; count++;否则:若x!=num, count—;若x==num则count++; 最后返回num |
简单 |
|---|---|---|---|---|
| 58 | 198.打家劫舍 | 给一个非负整数数组nums,数组的每一个元素都表示一个金额,不能选取相邻的两个元素,求能选取的最大金额总量是多少 | (1)动态规划:设dp[i]表示区间[0, i]的最大选择金额数,则 dp[i] = max(nums[i] + dp[i-2], dp[i-1]) dp[0] = nums[0]; dp[1] =max(nusm[0], nums[1]) 遍历数组可以依次得到dp[i],最后得到dp[n]。 |
中等 |
|---|---|---|---|---|
| 59 | 200.岛屿数量 | 给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 | (1)深度优先搜索DFS:遍历矩阵,从矩阵的每个位置(i,j)出发进行深度优先搜索,若grid[i][j]==1就进行DFS,DFS过程中统计1的个数,搜索后就将1置为0,(i,j)搜索后就向四周发散搜索,若出界或遇到0则搜索停止。 (2)广度优先搜索BFS:同DFS |
中等 |
|---|---|---|---|---|
| 60 | 206.反转链表 | 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 | (1)递归:设reverse(node)返回逆向后的链表头节点 执行reverse(root.next);后,root.next就是root的前驱 注意 (2)迭代:将链表的所有指针逆向即可 |
简单 |
|---|---|---|---|---|
7.61-70
| 61 | 207.课程表 | 要修numCourses 门课程,记 0 到 numCourses - 1 ,修某些课程前需要修先修课程。 先修课程按数组 prerequisites 给出,prerequisites[i] = [ai, bi] ,表示学习课程 ai 前得先学习课程 bi 。 例如,先修课程对 [0, 1] 则学习课程 0前要先修课程 1 。判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。 |
(1)拓扑排序+邻接表构建图+BFS: List int[] indgree;indgree[i]表示节点i的所有入度 Deque |
中等 |
|---|---|---|---|---|
| 62 | 208.实现 Trie (前缀树) | (1)每个节点Trie都引用一个长为26的Trie数组,和一个布尔flag表示本节点是否为字 insert: 字符c - ‘a’找到对应的下标 i,若children[i]==null, search: 字符c - ‘a’找到对应的下标 i,并看children[]中是否有对应的trie,然后沿着链条一直向下找,注意看字符串末尾字符对应的trie是否isEnd为true |
中等 | |
|---|---|---|---|---|
| 63 | 215.数组中的第K个最大元素 | 给定整数数组 nums 和整数 k,请返回数组中第 k 大的元素。 | (1)排序:升序排序后第k个元素,时间O(NlogN) (2)top-k堆:维护一个大小为k的小根堆heap,用[0, k-1]的元素初始化heap,然后扫描数组剩余元素,当元素x小于堆顶元素时抛弃,大于堆顶时,heap.poll(),heap.push(x);时间O(NlogK) (3)快速选择*:使用快排的partition,每次选择都能找到一个元素y将数组分成两部分,下次就可以排除一半元素,只需要log(N)次就能找到第k大的元素,时间O(N) |
中等 |
|---|---|---|---|---|
| 64 | 221.最大正方形 | 中等 | ||
|---|---|---|---|---|
| 65 | 226.翻转二叉树 | 翻转一棵二叉树。 | (1)递归:将root的左右子树root.left, root.right分别翻转,然后交换root.left, root.right. | 简单 |
|---|---|---|---|---|
| 66 | 234.回文链表 | 请判断一个链表是否为回文链表。 | (1)栈:遍历链表list,将链表每个节点压入栈stack,然后再次遍历链表,并同时将栈中元素出栈,判断是否相同。空间O(N) (2)快慢指针找中点+翻转:使用快慢指针找到中点,然后翻转后半部分,再同时遍历前半部分和翻转后的后半部分看是否每个元素相同。 |
简单 |
|---|---|---|---|---|
| 67 | 236.二叉树的最近公共祖先 | 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 | (1)递归: 设boolean nodeContains(node, p, q)返回是否node包含p、q,node自己也可以是p、q。 设leftFlag = nodeContains(node.left , p, q) , rightFlag = nodeContains(node.right, p ,q), 当((node==p || node==q) && (leftFlag || rightFlag ) ) || (leftFlag &&rightFlag )时,node就是最近公共祖先,返回root==p||root==q||leftContain|| rightContain (2)存储节点的父节点:用hash表存储所有节点的父节点,利用节点的父节点信息从 p 结点开始不断往上跳,并记录已经访问过的节点,再从 q 节点开始不断往上跳,如果碰到已经访问过的节点,那么这个节点就是我们要找的最近公共祖先。 |
中等 |
|---|---|---|---|---|
| 68 | 238.除自身以外数组的乘积 | (1)左右乘积列表:设L[i]表示位置i左边所有元素乘积,R[i]表示位置i右边所有元素乘积,则L[i]*R[i]就是nums[i]除自身外的乘积 初始化L[0] = 1; R[len-1]=1;因为第1个元素左边和最后一个元素右边没有元素 从左向右遍历初始化L,从右向左遍历初始化R 最后计算结果集res,空间O(n) (2)使用res作左乘积列表,动态构造右乘积: 将输出结果res作为左乘积列表L,然后用一个变量动态构造右乘积列表,并乘到res对应位置上即可,空间O(1) |
中等 | |
|---|---|---|---|---|
| 69 | 239.滑动窗口最大值 | (1)优先队列(heap): 用一个heap来保存窗口中所有元素,和窗口前面的元素,当窗口滑动时,若前面有比窗口内更大的元素时,则前面的元素需要从heap中弹出,所以需要记录每个元素的位置,int[] element, element[0]为 元素值,element[1]为位置,滑动窗口时,将新加入元素值val,位置i,[val,i] push进heap,当heap.peek()[1]不在窗口内时,heap.poll(),设窗口右端为right,则左端为right-k+1. (2)单调队列: |
困难 | |
|---|---|---|---|---|
| 70 | 240.搜索二维矩阵 II | 搜索 m x n 矩阵 matrix 中是否存在target 。矩阵满足: 每行的元素从左到右升序排列。 每列的元素从上到下升序排列。 |
(1)从左上角搜索:从(0, n-1)位置开始搜索,若当前扫描元素x |
中等 |
|---|---|---|---|---|
8.71-80
| 71 | 279.完全平方数 | 给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。 | (1)动态规划: 设dp[i]为组成i的完全平方数的最小数量, 则dp[i] = 1 + min(dp[i-jj],j>=1且jj<=i。 dp[0]=0表示当j*j为i时,i恰好可以由一个j组成。 |
中等 |
|---|---|---|---|---|
| 72 | 283.移动零 | 定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。 | (1)双指针交换: 指针left在非0序列尾部,right碰到非0数时,将left、right位置数交换,则left左边和right右边之间全是0,交换的是right指向的非0和left指向的0,所以能保证非0数的顺序,特殊情况下right和left指向同一个位置,left、right初始化为0 (2)单指针重建+末尾赋值: index=0,扫描数组,当前元素num,若num!=0则nums[index++]=num,扫描完毕将[index,len-1]区间全部置0 |
简单 |
|---|---|---|---|---|
| 73 | 287.寻找重复数 | 给定一个包含 n + 1 个整数的数组 nums ,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设 nums 只有 一个重复的整数 ,找出 这个重复的数 不修改数组 nums 且只用常量级 O(1) 的额外空间 |
(1)对值的二分搜索: 定义cnt[i] 表示nums 数组中小于等于 i的数有多少个,假设我们重复的数是target,那么 [1,target−1]里的所有数满足 icnt[i]≤i,[target,n] 里的所有数满足cnt[i]>i,具有单调性。 所以二分搜索所有的可能重复的值i,看数组中小于等于 i的数有多少个 |
中等 |
|---|---|---|---|---|
| 74 | 297.二叉树的序列化与反序列化 | (1)递归: serialize:将null序列化为’none’,节点序列化为对应值val,用逗号隔开。 deSerialize:先把字符串按逗号,拆分转化为list,然后按照根、左、由顺序处理,比如’none’就是null,处理完一个就从序列化的list头部删除一个元素,list.remove(0). |
困难 | |
|---|---|---|---|---|
| 75 | 300.最长递增子序列 | 给你一个整数数组 nums ,找到其中最长严格递增子序列的长度 子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。 |
(1)动态规划: 设dp[i]为以i位置结尾的最长递增子序列, 则dp[i] = 1 + max(dp[j]),j必须满足j<i且nums[j]<nums[i] |
中等 |
|---|---|---|---|---|
| 76 | 301.删除无效的括号 | 困难 | ||
|---|---|---|---|---|
| 77 | 309.最佳买卖股票时机含冷冻期 | (1)动态规划:冷冻期表示前一天卖了,要等一天才能买,k为无限大 T[i][k][0] = max(T[i-1][k][0], T[i-1][k][1] + prices[i]) T[i][k][1] = max(T[i-1][k][1], T[i-2][k-1][0] - prices[i]) |
中等 | |
|---|---|---|---|---|
| 78 | 312.戳气球 | 困难 | ||
|---|---|---|---|---|
| 79 | 322.零钱兑换 | 中等 | ||
|---|---|---|---|---|
| 80 | 337.打家劫舍 III | 偷窃一颗二叉树,父子相连的两个节点不能同时一起偷,求能偷窃的最大金额 | (1)树型-动态规划: f(node)表示要偷node的最大金额 f(node) = node.val + g(node.left) + g(node.right) g(node)表示不偷node的最大金额 g(node) = max(f(node.left), g(node.left) + max(f(node.right), g(node.right) 递归遍历树,所以用map保存状态 fMap保存{node:money},要偷node时的最大金额 gMap保存{node:money},不偷nod时的最大金额 |
中等 |
|---|---|---|---|---|
9.81-90
| 81 | 338.比特位计数 | 简单 | ||
|---|---|---|---|---|
| 82 | 347.前 K 个高频元素 | 给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。 | 思路:首先使用map,得到所有元素及其出现频率,然后构建一个int[][] frequency数组frequency[i]表示一个元素值及其出现频率 frequency[i][0]为元素值,frequency[i][1]为出现频率 (1)排序 若元素值有n个,则时间O(NlogN) (2)堆 构建一个k个大小的小根堆,时间O(Nlogk) (3)快速选择 每次partition都能选出一个数,使得左边都小于x,右边都大于x,log(n)次后就能得到第k高的频率元素,时间O(N) |
中等 |
|---|---|---|---|---|
| 83 | 394.字符串解码 | 给定一个经过编码的字符串,返回它解码后的字符串。 编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数 |
(1)栈 扫描字符串,非右括号入栈,扫描到右括号时,出栈并头插法到字符序列中,直到遇到左括号[为止,seq就是字符串,然后stack.pop()弹出左括号,然后从栈中取数字,栈不为空且栈顶为0到9之间时一直出栈头插法构建数字num,将seq重复num次,然后重新入栈。 扫描完毕后,栈中就是解码后的字符串。 |
中等 |
|---|---|---|---|---|
| 84 | 399.除法求值 | 中等 | ||
|---|---|---|---|---|
| 85 | 406.根据身高重建队列 | 有打乱顺序的一群人站成一个队列,用数组 people表示 ,people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。 请你重新构造并返回输入数组 people ,使得people [j] = [hj, kj] 前面恰好有kj个人身高小于等于people[j][0](people [0] 是排在队列0位置的人) |
(1)贪心: 按照hi升序排列,当hi一样时,按照ki降序排列,然后从左往右扫描peeple,每个元素people[i]的插入为止就是第ki个空位置 注意排序后已经插入的元素的身高都小于people[i][0],所以不会影响people[i][1]的值。 为什么hi一样时,按照ki降序呢? 因为如果按照ki升序,那么[5,0],[5,2]同时存在时,[5,0]会先插入,当[5,2]插入时会插入到第3个空闲位置,此时前面有3个人身高大于等于[5,2] |
中等 |
|---|---|---|---|---|
| 86 | 416.分割等和子集 | 给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。 |
(1)动态规划: 设数组nums累积和为sum,则题目转化为选出一组数使得和为sum/2,这样的方案总数就是答案 设布尔数组dp[i][j]表示从nums下标区间[0, i-1]是否可以选出一组数使得和为sum/2 dp[0][j]表示不选择任何一个数,使得其和为j, dp[0][0] = true, dp[0][j] = false, j>0时 当sum为奇数时,选不出来一组数使得和为sum/2。 根据i-1位置元素是否被选: nums[i-1]>j时,dp[i][j] = dp[i-1][j] nums[i-1]<=j时,dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]] |
中等 |
|---|---|---|---|---|
| 87 | 437.路径总和 III |
给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。 路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的 |
(1)双递归: 可以以DFS(前、中、后)遍历每个节点node,然后从node开始向下尝试得到所有路径和为targetSum的路径的数量x,所有node的x的累积和就是答案 时间复杂度指数。 (2)前缀和+hash表: map.put(0,1)用来表示从根节点到node节点路径和恰好为targetSum的情况。 DFS遍历每个节点node,遍历到节点node时,map里保存的是从根节点到该节点node上的路径上的每个节点x的前缀和(node的前缀和就是从根节点一直累加到node的和)xSum,及xSum出现的次数xCount,所以若node的前缀和为sum,那么就有map.get(sum-targetSum)个节点,从这些节点到node的路径和为targetSum;所以对每个节点都累加map.get(sum-targetSum)并将当前前缀和加入map。 注意回溯时要map.put(sum, map.get(sum)-1) 类似:560.和为K的子数组 |
中等 |
|---|---|---|---|---|
| 88 | 438.找到字符串中所有字母异位词 | 给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。 异位词 指字母相同,但排列不同的字符串。 |
(1)滑动窗口 窗口window,sMap,pMap,valid window==p.length() && valid==pMap.size()时,是一个解 |
中等 |
|---|---|---|---|---|
| 89 | 448.找到所有数组中消失的数字 | 给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。 | (1)set: 将数组nums所有元素放入set,遍历区间[1,n],查看哪些元素没有出现在set中,空间O(1) (2)抽屉原理+原地标记: 范围[1,n]的数出现在长为n的数组里,如果没有重复值,那么将值为i的数放在i-1位置上就能实现每个位置上一个不同的数。 所以,遍历数组nums,对遍历到的每个值x,将其位置x-1标记,nums[x-1] += n+1,所以遍历到一个数x时,要先还原,x = x%(n+1),遍历并标记完后再次遍历,当位置i满足nums[i]<n+1时,说明该位置上应该出现的数i+1没有出现,则将其加入结果集。 |
简单 |
|---|---|---|---|---|
| 90 | 461.汉明距离 | 两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。 给你两个整数 x 和 y,计算并返回它们之间的汉明距离。 |
(1)异或,判断每个bit位是否为1 (2)优化:Brian W. Kernighan算法 x & (x-1)可以得到x跳过最右边的一个1的二进制的结果 |
简单 |
|---|---|---|---|---|
10.91-100
| 91 | 494.目标和 | 给你一个整数数组 nums 和一个整数 target 。 向数组中的每个整数前添加 ‘+’ 或 ‘-‘ ,然后串联起所有整数,可以构造一个 表达式,返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目 |
(1)动态规划: 可以转化为0-1背包问题,设nums的累积和为sum,选为负数的那部分和为neg,则(sum-neg)-neg = target.则neg = (sum-target)/2,所以若sum-target是奇数,答案为0。 问题转化为了从nums中挑选i个数,使得这i个数的和为(sum-target)/2的方案数。 设dp[i][j]为从nums的[0,i-1]区间挑选的数和为j的方案数,判断i-1位置元素nums[i]是否可以被选, nums[i-1]>j,则i-1位置不可选,则dp[i][j]=dp[i-1][j] nums[i-1]<=j,则i可选,则dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]] 初始dp[0][0]=1,表示没有任何元素可选的累积和为0的方案为1,这从数组nums只有一个元素为1,dp[1][1] = dp[0][1] + dp[0][0]可以转化来 |
中等 |
|---|---|---|---|---|
| 92 | 538.把二叉搜索树转换为累加树 | 给一个二叉搜索树的根节点,使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和,左子树小于root.val,root.val小于右子树 | (1)递归:右、根、左的遍历顺序能得到一个降序排列,累计遍历到的每一个节点的值,将当前累积和赋值给当前节点即可 | 中等 |
|---|---|---|---|---|
| 93 | 543.二叉树的直径 | 一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点,计算给定二叉树的直径 | (1)设f(node)表示穿过node的最大路径长度,一棵树所有node的f(node)最大值就是树的直径。 g(node)表示从node一直向下的最大路径,则 g(node) = max(g(node.left), g(node.right) + 1; f(node) =g(node.left) + g(node.right)) +1 所以可以用一个递归函数表示g(node),在计算g(node)时顺带计算f(node),并统计所有node的f(node)最大值 |
简单 |
|---|---|---|---|---|
| 94 | 560.和为K的子数组 | 给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。 | (1)暴力枚举: 枚举所有区间(i,j),查看其和是否为k,计算区间(i,j)的和只需要O(1)时间,因为可以采用累加和,两层循环,外层枚举start,内层枚举end,end>=start (2)前缀和+hash表: 方法1枚举了所有以i结尾的区间中,要计算有多少个j,j<=i使得区间[j,i]的和为k需要O(N)时间,如何将其变为O(1)呢?,若我们知道从[0,i]的累积和为sum,如果对于j,0<=j<=i,所有的[0,j]的累积和(前缀和)都保存到了hash表map中,map.get(sum-k)的值就是有多少个j满足[j,i]累积和为k,对每个i都计算这个值,总的数量就是答案 算法: 遍历数组nums,当前下标i,初始map.put(0,1),表示当[0,i]的累积恰好为k的情况 当前累积和sum,map.put(sum, map.getOrDefault(sum,0)+1),计算map.getOrDefault(sum-k,0),累加到全局数量count中 |
中等 |
|---|---|---|---|---|
| 95 | 581.最短无序连续子数组 | 给你一个整数数组 nums ,你需要找出一个 连续子数组 ,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。 请你找出符合题意的 最短 子数组,并输出它的长度。 |
(1)暴力枚举: 枚举所有区间(i,j),若(0,i-1)、(j+1,len-1)都是升序,而且(0,i-1)的所有元素都小于(i,j)的任一元素,(j+1,len-1)的所有元素都大于(i,j)的任一元素,则(i,j)是一个可能的解,从所有可能的解中找到最短的即可。 判断(0,i-1)、(j+1,len-1)是否为升序,一次遍历即可 遍历(0,i-1)得到最大值max,(j+1,len-1)得到最小值min,若max小于(i,j)任一元素, min大于(i,j)任一元素,就可以得到是否 “(0,i-1)的所有元素都小于(i,j)的任一元素,(j+1,len-1)的所有元素都大于(i,j)的任一元素” 时间O(N*N) (2)排序 数组nums由三块构成,numsA、numsB、numsC,因为将numsB排序后整个nums就是升序了,所以排序前numsA和numsB就是升序,且和排序后不变。 复制nums得copyNums,对copyNums排序,将排序后的copyNums和nums对比,不相同部分就是最短数组 时间O(NlogN) (3)单调栈 寻找无序部分的左端left = Integer.MAX_VALUE: 从左到右遍历数组,当前扫描元素为x,下标为i,当栈为空或栈顶小于x时就将i入栈,否则出栈,出栈的元素属入无序部分,所以left = min(left, 出栈元素下标),一直到栈顶小于当前扫描元素就将i入栈 寻找无序部分右段right = Integer.MIN_VALUE: 从右向左遍历数组,当前扫描元素为x,下标为i,当栈为空或栈顶大于x时就将i入栈,否则出栈,出栈的元素属入无序部分,所以left = max(left, 出栈元素下标),一直到栈顶大于当前扫描元素就将i入栈 (4)一次遍历 |
中等 |
|---|---|---|---|---|
| 96 | 617.合并二叉树 | 给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠 注意:平面上的覆盖 |
(1)递归:对树root1、root2,相同位置上都有,则值相加,若root1为null则返回root2,若root2为null返回root1 | 简单 |
|---|---|---|---|---|
| 97 | 621.任务调度器 | 字符数组 tasks 表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务,任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间,CPU 可以完成一个任务,或者处于待命状态。 然而,两个 相同种类 的任务之间必须有长度为整数 n 的冷却时间,因此至少有连续 n 个单位时间内 CPU 在执行不同的任务,或者在待命状态。 计算完成所有任务所需要的 最短时间 |
(1)贪心: 因为连续执行两个相同任务之间必须休息n,所以倾向于连续执行不同的任务,所以剩下的任务倾向于执行同一种类下更多数量那种,来保持不同种类任务数量均衡,使得休息时间最少 每种任务记录一个任务数量count,任务下次可执行的时间nextRunTime。 维护一个系统系统systemTime,初始化systemTime=1,每个任务的nextRunTime初始化为1. 从所有任务种类种选择一个执行,该种任务必须nextRunTime<=systemTime,而且count最大。 每次执行完一个任务后将该任务的nextRunTime= systemTime+1+n,systemTime++; 注意:当选择任务时,若所有任务的nextRunTime都大于systemTime时,就都不能选,则产生了空循环,优化就是将systemTime置为所有任务的nextRunTime的最小值 执行完所有任务后的systemTime-1的就是需要的最短时间 |
中等 |
|---|---|---|---|---|
| 98 | 647.回文子串 | 给定一个字符串,计算这个字符串中有多少个回文子串。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。 | (1)动态规划: dp[i][j] = dp[i+1][j-1] && str[i] == str[j] (2)中心扩展: 参考:最长回文字符串 |
中等 |
|---|---|---|---|---|
| 99 | 739.每日温度 | 温度列表temperatures ,计算在每一天需要等几天才会有更高的温度。如果气温之后都不会升高,在该位置用 0 来代替。 30<=temperatures[i]<= 100 |
(1)暴力枚举: 对每个位置i都遍历[i+1, len-1]查看更高温度的位置,时间O(N*N) (2)第一次出现温度的数组: 因为温度范围在[30, 100],所以可以设置一个firstAppear数组,firstAppear[i]表示温度i第一次出现的在temperatures数组的下标firstAppear[i],初始化firstAppear所有元素为-1 从右向左遍历temperatures数组,对每个位置j都做以下操作:若firstAppear[temperatures[j]]为-1, 则firstAppear[temperatures[j]] = j;从temperatures[j]+1到100寻找大于temperatures[j]的最近出现的温度位置, 既x>=temperatures[j]+1且x<=100,若firstAppear[x]不为-1,统计所有x的firstAppear[x]最小值 (3)单调栈 设置一个stack,扫描temperatures ,当前扫描元素值val,位置pos,栈为空或栈顶元素大于当前扫描元素val时,将pos入栈;若栈顶元素小于val,则一直出栈,并把出栈的位置i的下一个更大温度位置设置为pos,直到栈为空或栈顶元素大于val。依次处理temperatures 的下一个元素 |
中等 |
|---|---|---|---|---|
二.代码
2.两数相加
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {//尾插法ListNode helpNode = new ListNode(0);ListNode p=helpNode;int carry=0;int sum = 0;while(l1!=null || l2!=null){sum = carry;carry=0;if(l1!=null){sum += l1.val;l1 = l1.next;}if(l2!=null){sum += l2.val;l2 = l2.next;}sum += carry;if(sum>9){carry=1;sum = sum%10;}ListNode one = new ListNode(sum);p.next = one;p = one;}if(carry==1){p.next = new ListNode(1);}return helpNode.next;}
5.最长回文子串
动态规划:
枚举中心,扩展法:
String answer = "";int answerLen = 0;// 扩展法public String longestPalindrome(String s) {int len = s.length();for(int i=0;i<len;i++){expand(s, i, i+1);expand(s, i, i);}return answer;}public void expand(String s, int start, int end){if(end>=s.length()){return;}// 扩展过程中有一对(i,j)不是回文就停止扩展while(start>=0 && end<s.length() && s.charAt(start) == s.charAt(end)){if(answerLen<end-start+1){answerLen = end-start+1;answer = s.substring(start,end+1);}start--;end++;}}
15.三数之和
// 排序+双指针+去重(外循环去重+内循环去重), 时间O(NlogN)public List<List<Integer>> threeSum(int[] nums) {if(nums.length<3){return new ArrayList<>();}// 排序Arrays.sort(nums);List<List<Integer>> res = new ArrayList<>();for(int i=0;i<nums.length-2;i++){// 外循环去重if(i>0 && nums[i]==nums[i-1]){continue;}int low = i + 1, high = nums.length - 1;while(low<high){int sum = nums[low]+nums[high];if(sum == -nums[i]){res.add(Arrays.asList(nums[i], nums[low], nums[high]));// 内循环low去重while(low<high && nums[low]==nums[low+1]){low++;}// 内循环high去重while(low<high && nums[high]==nums[high-1]){high--;}// 不能有重复则自增low++;high--;}else if(sum < -nums[i]){low++;}else{high--;}}}return res;}
17. 电话号码的字母组合
// 设digits长度为n,就是有n个槽位,每个槽位都有对应的可能的几种字符,把所有可能的字符组合输出即可// 在叶子节点将选择的组合加入结果集:必须每个槽位都放置了字符时才可以输出public List<String> letterCombinations(String digits) {if(digits.length()==0){return new ArrayList<>();}Map<Character, String> map = new HashMap<>();map.put('2', "abc");map.put('3', "def");map.put('4', "ghi");map.put('5', "jkl");map.put('6', "mno");map.put('7', "pqrs");map.put('8', "tuv");map.put('9', "wxyz");List<String> res = new ArrayList<>();search(digits, res, new StringBuffer(), 0, map);return res;}// 递归// res结果集, current是当前放置了字符的所有槽位的字符组合public void search(String digits, List<String> res, StringBuffer current, int idx, Map<Character, String> map){// 每个槽位都放置了字符时才可以输出if(current.length() == digits.length()){res.add(current.toString());return;}char digit = digits.charAt(idx);String chars = map.get(digit);// 对每一个数字遍历其映射的所有字符for(int i=0;i<chars.length();i++){current.append(chars.charAt(i));search(digits, res, current, idx+1, map);current.deleteCharAt(current.length()-1);}}
19.删除链表的倒数第 N 个结点
public ListNode removeNthFromEnd(ListNode head, int n) {//加上一个辅助头结点来避免讨论两种情况ListNode helpNode = new ListNode(0);helpNode.next = head;ListNode pre = helpNode;int move = n-1;ListNode p1=head,p2=head;// 让p2先走n-1步while(move>0){p2 = p2.next;move--;}while(p2.next!=null){p1 = p1.next;p2 = p2.next;pre = pre.next;}pre.next = p1.next;return helpNode.next;}
39.组合总和
递归+回溯:
// 每个元素都大于0, 所以累积和达到target就可以停止向下搜索// (1)递归+回溯, 排序:当target小于0时就停止向下搜索public List<List<Integer>> combinationSum(int[] candidates, int target) {// Arrays.sort(candidates);List<List<Integer>> res = new ArrayList<>();combinationSum(0, candidates, target, res, new ArrayList<>());return res;}/*** target: 当前目标值, 会一直减小* res: 全局结果集* current: 当前组合* idx:考虑输入数组的idx位置元素是否被选择*/public void combinationSum(int idx, int[] candidates, int target, List<List<Integer>> res, List<Integer> current){if(target==0){res.add(new ArrayList<>(current));return;}// 考虑了所有位置元素是否选择后结束搜索// 放在当前组合加入全局结果集代码后面的原因:// 因为当前位置被选择后, 当前组合是否被加入全局结果集要在下次搜索时判断if(target<0 || idx>=candidates.length){return;}for(int i=idx;i<candidates.length;i++){current.add(candidates[i]);combinationSum(i, candidates, target-candidates[i], res, current);current.remove(current.size()-1);}
递归+回溯+循环:
// 每个元素都大于0, 所以累积和达到target就可以停止向下搜索// (1)递归+回溯, 排序:当target小于0时就停止向下搜索public List<List<Integer>> combinationSum(int[] candidates, int target) {Arrays.sort(candidates);List<List<Integer>> res = new ArrayList<>();combinationSum(0, candidates, target, res, new ArrayList<>());return res;}/*** target: 当前目标值, 会一直减小* res: 全局结果集* current: 当前组合* idx:考虑输入数组的idx位置元素是否被选择*/public void combinationSum(int idx, int[] candidates, int target, List<List<Integer>> res, List<Integer> current){if(target==0){res.add(new ArrayList<>(current));return;}// 考虑了所有位置元素后结束搜索// 放在当前组合加入全局结果集代码后面的原因:// 因为当前位置被选择后, 当前组合是否被加入全局结果集要在下次搜索时判断if(target<0 || idx>=candidates.length){return;}// 当前组合选择 idx 位置元素current.add(candidates[idx]);// 因为可以重复选则, 所以下次选择还是从idx位置开始combinationSum(idx, candidates, target-candidates[idx], res, current);// 当前组合不选择 idx 位置元素, 不选择了就从 idx+1 开始current.remove(current.size()-1);combinationSum(idx+1, candidates, target, res, current);}
42.接雨水
最大值+划分两部分:
public int trap(int[] height) {
int len = height.length;
// 要能盛水至少要三根柱子
if(len<3){
return 0;
}
int volume = 0, maxHeightIndex = 0, maxHeight = Integer.MIN_VALUE;
for(int i=0;i<len;i++){
if(height[i]>maxHeight){
maxHeight = height[i];
maxHeightIndex = i;
}
}
int leftMax=height[0], rightMax=height[len-1];
// 遍历左部分
for(int i=0;i<=maxHeightIndex-1;i++){
leftMax = Math.max(leftMax, height[i]);
volume += leftMax - height[i];
}
// 遍历右部分
for(int j=len-1;j>=maxHeightIndex+1;j--){
rightMax = Math.max(rightMax, height[j]);
volume += rightMax - height[j];
}
return volume;
}
46.全排列
// 递归 + 循环 + 回溯 + 状态
public List<List<Integer>> permute(int[] nums) {
if(nums==null || nums.length==0){
return new ArrayList<>();
}
List<List<Integer>> res = new ArrayList<>();
permute(nums, res, new ArrayList<>(), new boolean[nums.length]);
return res;
}
// 第一调用该函数时,for循环会把所有元素都尝试放在第一个位置,然后继续调用函数
public void permute(int[] nums, List<List<Integer>> res, List<Integer> current, boolean[] status){
if(current.size()==nums.length){
res.add(new ArrayList<>(current));
return;
}
for(int i=0;i<nums.length;i++){
if(status[i]){
continue;
}
current.add(nums[i]);
status[i] = true;
permute(nums, res, current, status);
current.remove(current.size()-1);
status[i] = false;
}
}
75.颜色分类
// 两趟遍历,O(1)空间
public void sortColors(int[] nums) {
if(nums==null || nums.length==0){
return;
}
int index=0;
// 将所有的0放到头部
// 有没有可能的元素和要待交换的位置index的元素都是0既nums[index]、nums[i]都是0
// 只可能出现在最开始位置,比如对于序列0、0、1、0,前两个0对应的i、index都是0,但从1
// 之后就不是了
for(int i=0;i<nums.length;i++){
if(nums[i]==0){
swap(nums, index, i);
index++;
}
}
// 将所有的1放到头部的0后面
for(int i=0;i<nums.length;i++){
if(nums[i]==1){
swap(nums, index, i);
index++;
}
}
}
public void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
98.验证二叉搜索树
递归:hep函数,节点值应该在一(lowBorder, highBorder)内
// help(root, lowBorder, highBorder)
// 输入可能是Integer.MIN_VALUE或Integer.MAX_VALUE这种极端情况,所以使用long的最大值、最小值来规避
public boolean isValidBST(TreeNode root) {
return help(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
public boolean help(TreeNode root, long lowBorder, long highBorder){
if(root == null){
return true;
}
if(root.val<=lowBorder || root.val >=highBorder){
return false;
}
return help(root.left, lowBorder, root.val) && help(root.right, root.val ,highBorder);
}
中序遍历:
// 输入可能是Integer.MIN_VALUE或Integer.MAX_VALUE这种极端情况,所以使用long的最小值来规避
private long pre = Long.MIN_VALUE;
private boolean flag = true;
public boolean isValidBST(TreeNode root) {
inOrder(root);
return flag;
}
public void inOrder(TreeNode root){
if(root==null){
return;
}
inOrder(root.left);
if(root.val>pre){
pre = root.val;
}else{
flag = false;
return;
}
inOrder(root.right);
}
169.多数元素
摩尔投票法:
public int majorityElement(int[] nums) {
int target = -1;
int count = 0;
for(int num:nums){
if(count==0){
target = num;
}else{
if(num!=target){
count--;
}else{
count++;
}
}
}
return target;
}
206.反转链表
递归:
public ListNode reverseList(ListNode head) {
if(head==null || head.next==null){
return head;
}
ListNode nextNode = head.next;
ListNode root = reverseList(nextNode);
head.next = null;
nextNode.next = head;
return root;
}
迭代:
public ListNode reverseList(ListNode head) {
if(head==null || head.next==null){
return head;
}
ListNode p1 = head;
ListNode p2 = head.next;
// 防止循环
head.next = null;
while(p2!=null){
// 保存p2的next
ListNode nextNode = p2.next;
// 反转指针
p2.next = p1;
// 移动p1, p2
p1 = p2;
p2 = nextNode;
}
return p1;
}
128.最长连续序列
哈希表,时间O(N)
public int longestConsecutive(int[] nums) {
if(nums==null || nums.length==0){
return 0;
}
Set<Integer> set = new HashSet<>();
for(int num:nums){
set.add(num);
}
int maxLen = 1;
for(int num:nums){
if(set.contains(num-1)){
continue;
}
int count = 0;
while(set.contains(num)){
count++;
num++;
}
maxLen = Math.max(maxLen, count);
}
return maxLen;
}
139.单词拆分
动态规划
public boolean wordBreak(String s, List<String> wordDict) {
if(s==null || s.length()==0){
return true;
}
int len = s.length();
Set<String> set = new HashSet<>();
for(String str: wordDict){
set.add(str);
}
// dp[0]表示空串是否在wordDict中存在, dp[i]表示s的[0,i-1]区间是否可以拆分后存在于wordDict
boolean[] dp = new boolean[len+1];
dp[0] = true;
// 枚举所有长度, 小于等于len
for(int i=1;i<=len;i++){
// 枚举i的所有子区间
for(int j=0;j<i;j++){
if(dp[j] && set.contains(s.substring(j,i))){
dp[i] = true;
break;
}
}
}
return dp[len];
}
141.环形链表
public boolean hasCycle(ListNode head) {
if(head==null || head.next==null){
return false;
}
ListNode slow = head.next;
ListNode fast = head.next.next;
while(fast!=null && fast.next!=null){
if(fast==slow){
return true;
}
fast = fast.next.next;
slow = slow.next;
}
return false;
}
207.课程表
拓扑排序+DFS
// 拓扑排序+DFS
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<Integer>[] edges = new ArrayList[numCourses];
// 入度数组, inDegree[i]表示课程i的前置课程数量
int[] inDegree = new int[numCourses];
// 构建临接表
for(int[] pre: prerequisites){
int inNumber = pre[0];
int outNumber = pre[1];
inDegree[inNumber]++;
if(edges[outNumber]==null){
edges[outNumber] = new ArrayList<>();
}
edges[outNumber].add(inNumber);
}
Deque<Integer> queue = new LinkedList<>();
for(int i=0;i<inDegree.length;i++){
if(inDegree[i]==0){
queue.add(i);
}
}
int count = 0;
while(queue.size()>0){
int courseNumber = queue.poll();
count++;
if(edges[courseNumber]!=null){
for(int num: edges[courseNumber]){
inDegree[num]--;
if(inDegree[num]==0){
queue.add(num);
}
}
}
}
return count==numCourses;
}
208.实现 Trie (前缀树)
class Trie {
Trie[] children;
boolean isEnd;
/** Initialize your data structure here. */
public Trie() {
children = new Trie[26];
isEnd = false;
}
/** Inserts a word into the trie. */
public void insert(String word) {
Trie node = this;
for(int i=0;i<word.length();i++){
int index = word.charAt(i) - 'a';
if(node.children[index]==null){
node.children[index] = new Trie();
}
node = node.children[index];
}
node.isEnd = true;
}
/** Returns if the word is in the trie. */
public boolean search(String word) {
Trie node = searchPrefix(word);
return node!=null && node.isEnd;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
public boolean startsWith(String prefix) {
return searchPrefix(prefix)!=null;
}
public Trie searchPrefix(String word){
Trie node = this;
for(int i=0;i<word.length();i++){
int index = word.charAt(i) - 'a';
if(node.children[index]==null){
return null;
}
node = node.children[index];
}
return node;
}
}
234. 回文链表
快慢指针找中点+翻转后半段+遍历前半段和翻转后的后半段,空间O(N),如果是用迭代翻转就是O(1)
public boolean isPalindrome(ListNode head) {
if(head==null || head.next==null){
return true;
}
ListNode midPre = getMid(head);
ListNode nextHalfHead = midPre.next;
midPre.next = null;
nextHalfHead = reverse(nextHalfHead);
while(head!=null){
if(head.val!=nextHalfHead.val){
return false;
}
head = head.next;
nextHalfHead = nextHalfHead.next;
}
return true;
}
// 返回中点的pre节点,当节点数为奇数时,后半段会比前半段多一个,但是不影响判断
// 因为反转后多的这一个处于后半段的最后一个位置
public ListNode getMid(ListNode head){
// 只有两个节点时,将head返回
if(head.next.next==null){
return head;
}
ListNode slow = head.next;
ListNode fast = head.next.next;
// slow的前驱,既中点的前驱节点
ListNode pre = head;
while(fast!=null && fast.next!=null){
slow = slow.next;
fast = fast.next.next;
pre = pre.next;
}
return pre;
}
// 返回翻转后的链表
public ListNode reverse(ListNode head){
if(head==null || head.next==null){
return head;
}
ListNode nextTemp = head.next;
ListNode root = reverse(nextTemp);
head.next = null;
nextTemp.next = head;
return root;
}
236.二叉树的最近公共祖先
递归
TreeNode answer = null;
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
nodeContains(root, p, q);
return answer;
}
public boolean nodeContains(TreeNode root, TreeNode p, TreeNode q){
if(root==null){
return false;
}
boolean leftContain = nodeContains(root.left, p, q);
boolean rightContain = nodeContains(root.right, p, q);
// root是p或q, 同时左右子树中有一颗树包含p或q
if((root==p || root==q) && (leftContain || rightContain)){
answer = root;
return true;
}
// 左右子树都包含p或q
if(leftContain && rightContain){
answer = root;
return true;
}
return root==p || root==q || leftContain || rightContain;
}
存储父节点
TreeNode answer = null;
// 记录父节点信息
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
Map<TreeNode, TreeNode> map = new HashMap<>();
recordParent(root, map);
Set<TreeNode> set = new HashSet<>();
visitUp(p, set, map);
visitUp(q, set, map);
return answer;
}
public void recordParent(TreeNode root, Map<TreeNode,TreeNode> map){
if(root==null){
return;
}
if(root.left!=null){
map.put(root.left, root);
recordParent(root.left, map);
}
if(root.right!=null){
map.put(root.right, root);
recordParent(root.right, map);
}
}
// 从node节点一直向上遍历
public void visitUp(TreeNode node, Set<TreeNode> set, Map<TreeNode, TreeNode> map){
if(node==null){
return;
}
if(set.contains(node)){
answer = node;
return;
}
set.add(node);
visitUp(map.get(node), set, map);
}
找到从root到p、q的路径,分叉点就是LCA
238. 除自身以外数组的乘积
左右乘积列表,使用结果集res作为左乘积列表,动态构建右乘积列表,并将其乘到res对应位置上
// 左右乘积数组
public int[] productExceptSelf(int[] nums) {
int len = nums.length;
int[] res = new int[len];
res[0] = 1;
// 构造左乘积数组
for(int i=1;i<len;i++){
res[i] = nums[i-1]*res[i-1];
}
//
int rightProduct = 1;
for(int i=len-2;i>=0;i--){
// 动态构建右乘积
rightProduct *= nums[i+1];
// 将右乘积乘到结果集对应位置上
res[i] *= rightProduct;
}
return res;
}
461.汉明距离
Brian W. Kernighan算法
时间复杂度O(logn)
public int hammingDistance(int x, int y) {
int s = x ^ y, ret = 0;
while (s != 0) {
s &= s - 1;
ret++;
}
return ret;
}



