动态规划
最优子结构:子问题的最优结果推导出更大规模问题的结果。并且子问题之间必须相互独立。
重叠的子问题:通过子问题的重叠性,实现从base case的状态转移。
遇到求最值的题目,往动态规划方向靠。
明确状态=>dp数组的物理意义=>明确状态转移=>明确base case。动态规划就是从最简单的base case,通过状态的链式反应不断地向后推导。
编辑距离
给你两个单词 word1和word2,请返回将 word1转换成word2所使用的最少操作数。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
递归自顶向下的解法
1 | // 递归自顶向下的解法 |
BM78 打家劫舍(单路打)
你是一个经验丰富的小偷,准备偷沿街的一排房间,每个房间都存有一定的现金,为了防止被发现,你不能偷相邻的两家,即,如果偷了第一家,就不能再偷第二家;如果偷了第二家,那么就不能偷第一家和第三家。
给定一个整数数组nums,数组中的元素表示每个房间存有的现金数额,请你计算在不被发现的前提下最多的偷窃金额。
数据范围:数组长度满足$1≤n≤2×10^5$,数组中每个值满足$1≤num[i]≤5000$
输入:[1,2,3,4]
返回值:6
说明:最优方案是偷第 2,4 个房间
1 | int n; |
BM79 打家劫舍(循环路偷)
你是一个经验丰富的小偷,准备偷沿湖的一排房间,每个房间都存有一定的现金,为了防止被发现,你不能偷相邻的两家,即,如果偷了第一家,就不能再偷第二家,如果偷了第二家,那么就不能偷第一家和第三家。沿湖的房间组成一个闭合的圆形,即第一个房间和最后一个房间视为相邻。
给定一个长度为n的整数数组nums,数组中的元素表示每个房间存有的现金数额,请你计算在不被发现的前提下最多的偷窃金额。
数据范围:数组长度满足$1≤n≤2×10^5$,数组中每个值满足$1≤nums[i]≤5000$
输入:[1,3,6]
返回值:6
说明:由于 1 和 3 是相邻的,因此最优方案是偷第 3 个房间
1 | int n; |
地下城游戏
一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。
骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。
有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快到达公主,骑士决定每次只向右或向下移动一步。
编写一个函数来计算确保骑士能够拯救到公主所需的最低初始健康点数。
例如,考虑到如下布局的地下城,如果骑士遵循最佳路径 右 -> 右 -> 下 -> 下,则骑士的初始健康点数至少为 7。
| -2 (K) |-3 | 3 |
| -5 | -10 | 1 |
| 10 | 30 | -5 (P) |
1 | // 从结果作为base case出发的dp |
JZ14 剪绳子
给你一根长度为 n 的绳子,请把绳子剪成整数长的 m 段( m 、 n 都是整数, n>1 并且 m>1 , m <= n ),每段绳子的长度记为 k[1],…,k[m]。请问 k[1]×k[2]×…×k[m] 可能的最大乘积是多少?例如,当绳子的长度是 8 时,我们把它剪成长度分别为 2、3、3 的三段,此时得到的最大乘积是 18。
数据范围:$2≤n≤60$
进阶:空间复杂度 $O(1)$,时间复杂度 $O(n)$
输入:8
返回值:18
说明:
8 = 2+3+3, 2×3×3=18
dp[i]:长度为i的绳子可以被剪出的最大乘积是dp[i]。 dp[i] = max(dp[i], j*dp[i-j]);
1 | int cutRope(int n) { |
乘积最大子数组
给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
子数组 是数组的连续子序列。
输入: nums = [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
1 | // 由于负数的存在,当出现负数时,需要交替计算最大值和最小值 |
JZ42 连续子数组的最大和
输入一个长度为n的整型数组array,数组中的一个或连续多个整数组成一个子数组,子数组最小长度为1。求所有子数组的和的最大值。
数据范围:$1 <= n <= 2×10^5, -100 <= a[i] <= 100$
要求:时间复杂度为 $O(n)$,空间复杂度为 $O(n)$
输入:[1,-2,3,10,-4,7,2,-5]
返回值:18
说明:经分析可知,输入数组的子数组[3,10,-4,7,2]可以求得最大和为18。
1 | int FindGreatestSumOfSubArray(vector<int> array) { |
JZ85 连续子数组的最大和(二)
输入一个长度为n的整型数组array,数组中的一个或连续多个整数组成一个子数组,找到一个具有最大和的连续子数组。
1.子数组是连续的,比如[1,3,5,7,9]的子数组有[1,3],[3,5,7]等等,但是[1,3,7]不是子数组
2.如果存在多个最大和的连续子数组,那么返回其中长度最长的,该题数据保证这个最长的只存在一个
3.该题定义的子数组的最小长度为1,不存在为空的子数组,即不存在[]是某个数组的子数组
4.返回的数组不计入空间复杂度计算
数据范围:
$1<=n<=10^5$
$−100<=a[i]<=100$
要求:时间复杂度$O(n)$,空间复杂度$O(n)$
进阶:时间复杂度$O(n)$,空间复杂度$O(1)$
思路:
- 定义dp[i]:以第i个下标结尾时,前面的i个子数组和的最大值;
- 定义左右指针从0出发找到当前下标最大值的区间子数组;
- 定义res的左右指针维护最大的左右指针区间;
- 最后遍历res的左右指针得到结果。
1 | vector<int> res; |
BM66 最长公共子串
给定两个字符串str1和str2,输出两个字符串的最长公共子串
题目保证str1和str2的最长公共子串存在且唯一。
数据范围:1≤∣str1∣,∣str2∣≤5000
要求:空间复杂度$O(n^2)$,时间复杂度$O(n^2)$
思路:
- dp[i][j]表示str2的第j个字符结尾的时候,str1的第i个字符结尾的最长子串的长度;
- 如果两个字符相等,则最长长度+1,否则置为0;
- 统计最大长度以及最大长度的结束位置。
1 | string LCS(string str1, string str2) { |
BM65 最长公共子序列(二)
给定两个字符串str1和str2,输出两个字符串的最长公共子序列。如果最长公共子序列为空,则返回”-1”。目前给出的数据,仅仅会存在一个最长的公共子序列
数据范围:$0≤∣str1∣,∣str2∣≤2000$
要求:空间复杂度$O(n^2)$,时间复杂度$O(n^2)$
输入:”1A2C3D4B56”,”B1D23A456A”
返回值:”123456”
1 | // 思路: |
贪心算法
BM95 分糖果问题
一群孩子做游戏,现在请你根据游戏得分来发糖果,要求如下:
1.每个孩子不管得分多少,起码分到一个糖果。
2.任意两个相邻的孩子之间,得分较多的孩子必须拿多一些糖果。(若相同则无此限制)
给定一个数组 arr 代表得分数组,请返回最少需要多少糖果。
要求: 时间复杂度为$O(n)$ 空间复杂度为$O(n)$
数据范围:$1≤n≤100000, 1≤a_i≤1000$
输入:[1,1,2]
返回值:4
说明:最优分配方案为1,1,2
1 |
|
股票问题
BM80 买卖股票(买卖一次)
假设你有一个数组prices,长度为n,其中prices[i]是股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益
1.你可以买入一次股票和卖出一次股票,并非每天都可以买入或卖出一次,总共只能买入和卖出一次,且买入必须在卖出的前面的某一天
2.如果不能获取到任何利润,请返回0
3.假设买入卖出均无手续费
数据范围:$0≤val≤10^5, 0≤val≤10^4$
要求:空间复杂度$O(1)$,时间复杂度$O(n)$
输入:[8,9,2,5,4,7,1]
返回值:5
说明:在第3天(股票价格 = 2)的时候买入,在第6天(股票价格 = 7)的时候卖出,最大利润 = 7-2 = 5 ,不能选择在第2天买入,第3天卖出,这样就亏损7了;同时,你也不能在买入前卖出股票。
思路:
- 维护一个购买的变量使得它的值最小;
- 维护一个总收益最大。
1 | int res=0; |
BM81 买卖股票(买卖无限次)
假设你有一个数组prices,长度为n,其中prices[i]是某只股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益
- 你可以多次买卖该只股票,但是再次购买前必须卖出之前的股票
- 如果不能获取收益,请返回0
- 假设买入卖出均无手续费
数据范围:$1≤n≤1×10^5, 1≤prices[i]≤10^4$
要求:空间复杂度$O(n)$,时间复杂度$O(n)$
进阶:空间复杂度$O(1)$,时间复杂度$O(n)$
输入:[8,9,2,5,4,7,1]
返回值:7
说明:
在第1天(股票价格=8)买入,第2天(股票价格=9)卖出,获利9-8=1
在第3天(股票价格=2)买入,第4天(股票价格=5)卖出,获利5-2=3
在第5天(股票价格=4)买入,第6天(股票价格=7)卖出,获利7-4=3
总获利1+3+3=7,返回7
1 | int n; |
BM82 买卖股票(最多买卖两次)
设你有一个数组prices,长度为n,其中prices[i]是某只股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益
- 你最多可以对该股票有两笔交易操作,一笔交易代表着一次买入与一次卖出,但是再次购买前必须卖出之前的股票
- 如果不能获取收益,请返回0
- 假设买入卖出均无手续费
数据范围:$1≤n≤10^5$,股票的价格满足$1≤val≤10^4$
要求: 空间复杂度$O(n)$,时间复杂度$O(n)$
进阶:空间复杂度$O(1)$,时间复杂度$O(n)$
输入:[8,9,3,5,1,3]
返回值:4
说明:第三天(股票价格=3)买进,第四天(股票价格=5)卖出,收益为2
第五天(股票价格=1)买进,第六天(股票价格=3)卖出,收益为2
总收益为4。
1 | int n; |
区间调度问题
贪心算法可以认为是动态规划算法的一个特例,相比动态规划,使用贪心算法需要满足更多的条件(贪心选择性质)。
贪心选择性质:每一步都做出一个局部最优的选择,最终的结果就是全局最优。只有一部分问题拥有这个性质。
思路:给你输入若干时间区间,让你计算同一时刻「最多」有几个区间重叠。
- x_end或者x_start按升序排序;
- 统计后面区间个数和关系,更新x_end或者x_start。
无重叠区间(右端点升序)
给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
思路:
- 按右端点进行升序排列;
- 统计有几个相互不交叉的区间;
- 总区间长度减去相互不交叉的区间个数。
1 | static bool cmp(const vector<int>& a, const vector<int>& b) { |
用最少数量的箭引爆气球(右端点升序)
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。
输入:points = [[1,2],[2,3],[3,4],[4,5]]
输出:2
解释:气球可以用2支箭来爆破:
- 在x = 2处发射箭,击破气球[1,2]和[2,3]。
- 在x = 4处射出箭,击破气球[3,4]和[4,5]。
思路:
- 该问题可转化为至多有几个不相交的子区间;
- 按照右端点升序排列好;
- 统计相互不相交的区间的个数。
1 | static bool cmp(const vector<int>& a, const vector<int> &b) { |
视频拼接(左端点升序+左端点相等右端点降序)
你将会获得一系列视频片段,这些片段来自于一项持续时长为 time 秒的体育赛事。这些片段可能有所重叠,也可能长度不一。使用数组 clips 描述所有的视频片段,其中 clips[i] = [starti, endi] 表示:某个视频片段开始于 starti 并于 endi 结束。
甚至可以对这些片段自由地再剪辑:
例如,片段 [0, 7] 可以剪切成 [0, 1] + [1, 3] + [3, 7] 三部分。
我们需要将这些片段进行再剪辑,并将剪辑后的内容拼接成覆盖整个运动过程的片段([0, time])。返回所需片段的最小数目,如果无法完成该任务,则返回 -1 。
输入:clips = [[0,2],[4,6],[8,10],[1,9],[1,5],[5,9]], time = 10
输出:3
解释:
选中 [0,2], [8,10], [1,9] 这三个片段。
然后,按下面的方案重制比赛片段:
将 [1,9] 再剪辑为 [1,2] + [2,8] + [8,9] 。
现在手上的片段为 [0,2] + [2,8] + [8,10],而这些覆盖了整场比赛 [0, 10]。
思路:
- 先对序列进行左端点升序排序,如果左端点相等则右端点降序排序;
- 将当前的右端点curEnd和区间的右端点比较,不断更新记录最大的nextEnd右端点;
- 更新当前的端点curEnd以及统计最长序列的出现次数;
- 判断当前的端点curEnd的值是否大于等于总时长。
1 | static bool cmp(const vector<int>& a, vector<int>& b) { |
区间问题
区间问题的思路:
- 对区间的每个起点进行升序排序(起点相同时,终点进行降序排序);
- 根据排序后的区间进行画图,找出相邻区间被覆盖,相交,不相交的端点进行解题。
删掉被覆盖的区间
给你一个区间列表,请你删除列表中被其他区间所覆盖的区间。
只有当 c <= a 且 b <= d 时,我们才认为区间 [a,b) 被区间 [c,d) 覆盖。
在完成所有删除操作后,请你返回列表中剩余区间的数目。
你一个区间列表,请你删除列表中被其他区间所覆盖的区间。
只有当 c <= a 且 b <= d 时,我们才认为区间 [a,b) 被区间 [c,d) 覆盖。
在完成所有删除操作后,请你返回列表中剩余区间的数目。
1 <= intervals.length <= 1000
0 <= intervals[i][0] < intervals[i][1] <= 10^5
对于所有的 i != j:intervals[i] != intervals[j]
1 | private: |
合并区间
对区间的起点进行升序排序后,找出相交区间的终点的最大端点进行更新即可。
以数组 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].
1 | static bool cmp(const vector<int>& A, const vector<int>& B) { |
区间的交集
给定两个由一些闭区间组成的列表,firstList 和 secondList ,其中 firstList[i] = [starti, endi] 而 secondList[j] = [startj, endj] 。每个区间列表都是成对不相交的,并且已经排序。
返回这两个区间列表的交集。
形式上,闭区间 [a, b](其中 a <= b)表示实数 x 的集合,而 a <= x <= b 。
两个闭区间的 交集 是一组实数,要么为空集,要么为闭区间。例如,[1, 3] 和 [2, 4] 的交集为 [2, 3] 。
1 | vector<vector<int>> res; |
会议室I(左端点升序)
给定一系列的会议时间间隔,包括起始和结束时间[[s1,e1],[s2,e2],…(si < ei),确定一个人是否可以参加所有会议。
(0,8),(8,10)在8这一时刻不冲突
输入: intervals = [(5,8),(9,15)]
输出: true
解释:这两个时间段不会冲突
思路:
- 题目本质是看任意相邻的两个区间是否有重叠;
- 将序列的左端点进行升序排列;
- 判断相邻的两个区间是否有重叠即可。
1 | static bool cmp(const Interval & a, const Interval & b) { |
会议室II(扫描线法)
给定一系列的会议时间间隔intervals,包括起始和结束时间[[s1,e1],[s2,e2],…] (si < ei),找到所需的最小的会议室数量。
(0,8),(8,10)在8这一时刻不冲突
输入: intervals = [(0,30),(5,10),(15,20)]
输出: 2
解释:
需要两个会议室
会议室1:(0,30)
会议室2:(5,10),(15,20)
思路:
- 题目的本质是求解同一时刻,最多有几个区间相交;
- 单独取出各个会议的左右端点进行升序排序;
- 利用双指针的扫描线法分别扫描左右端点,如果比右端点大,则计数加1,否则计数减一;
- 维护一个最大值的变量。
1 | int minMeetingRooms(vector<Interval> &intervals) { |
跳跃游戏I
给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
思路:
- 题目的本质是最大化每一次跳跃的最大长度;
- 维护每次跳跃最远的距离;
- 如果当前位置大于跳跃最远的位置,则表示无法到达最后一个下标。
1 | bool canJump(vector<int>& nums) { |
跳跃游戏II
给你一个非负整数数组 nums ,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
假设你总是可以到达数组的最后一个位置。
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
dp数组解法
1 | int jump(vector<int>& nums) { |
dp函数解法
1 | vector<int> memo; |
贪心解法
1 | int jump(vector<int>& nums) { |
JZ71 跳台阶扩展问题
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶(n为正整数)总共有多少种跳法。
数据范围:$1≤n≤20$
进阶:空间复杂度 $O(1)$, 时间复杂度$O(1)$
解题关键:每个台阶的方案数是前一个台阶的2倍数。
1 | int jumpFloorII(int number) { |
JZ19 正则表达式匹配
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.'
和 '*'
的正则表达式匹配。
'.'
:匹配任意单个字符'*'
:匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
输入:s = “aa”, p = “a*”
输出:true
解释:因为 '*'
代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'
。因此,字符串 "aa"
可被视为 'a'
重复了一次。
dp数组解法
1 | bool match(string str, string pattern) { |
dp函数解法
1 | int m, n; |
最长递增子序列
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
$1 <= nums.length <= 2500$
$-104 <= nums[i] <= 104$
1 | int res; |
最小路径和
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
说明:每次只能向下或者向右移动一步。
$m == grid.length$
$n == grid[i].length$
$1 <= m, n <= 200$
$0 <= grid[i][j] <= 100$
此题直接用DFS会超时,采用二维DP比较简单。
dp数组解法
明确状态=>DP数组的物理意义=>base case=>状态转移
- 初始化第0行和第0列的某个位置的最短路径;
- 状态dp[i][j]:走到第i行和第j列的最短路径;
- 选择:下一步时选择dp[i-1][j]还是dp[i][j-1]来加上grid[i][j]。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// dp[][]数组的物理意义:从(0,0)走到位置(i,j)的最短路径
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
vector<vector<int>> dp(m, vector<int>(n));
dp[0][0] = grid[0][0];
// base case
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int j = 1; j < n; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
// 状态转移
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = min(dp[i - 1][j],
dp[i][j - 1]) + grid[i][j];
}
}
return dp[m - 1][n - 1];
}
自由之路
电子游戏“辐射4”中,任务 “通向自由” 要求玩家到达名为 “Freedom Trail Ring” 的金属表盘,并使用表盘拼写特定关键词才能开门。
给定一个字符串 ring ,表示刻在外环上的编码;给定另一个字符串 key ,表示需要拼写的关键词。您需要算出能够拼写关键词中所有字符的最少步数。
最初,ring 的第一个字符与 12:00 方向对齐。您需要顺时针或逆时针旋转 ring 以使 key 的一个字符在 12:00 方向对齐,然后按下中心按钮,以此逐个拼写完 key 中的所有字符。
旋转 ring 拼出 key 字符 key[i] 的阶段中:
您可以将 ring 顺时针或逆时针旋转 一个位置 ,计为1步。旋转的最终目的是将字符串 ring 的一个字符与 12:00 方向对齐,并且这个字符必须等于字符 key[i] 。
如果字符 key[i] 已经对齐到12:00方向,您需要按下中心按钮进行拼写,这也将算作 1 步。按完之后,您可以开始拼写 key 的下一个字符(下一阶段), 直至完成所有拼写。
输入: ring = “godding”, key = “gd”
输出: 4
解释:
对于 key 的第一个字符 ‘g’,已经在正确的位置, 我们只需要1步来拼写这个字符。
对于 key 的第二个字符 ‘d’,我们需要逆时针旋转 ring “godding” 2步使它变成 “ddinggo”。
当然, 我们还需要1步进行拼写。
因此最终的输出是 4。
1 | vector<vector<int>> memo; |
BM64 最小花费爬楼梯
给定一个整数数组cost
,其中cost[i]
是从楼梯第i个台阶向上爬需要支付的费用,下标从0开始。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。
数据范围:数组长度满足$1≤n≤10^5$,数组中的值满足$1≤cost≤10^4$
输入:[2,5,20]
返回值:5
说明:你将从下标为1的台阶开始,支付5 ,向上爬两个台阶,到达楼梯顶部。总花费为5。
1 | int n; |
数组
BM22 比较版本号
比较规则:
一. 比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较忽略任何前导零后的整数值。比如”0.1”和”0.01”的版本号是相等的
二. 如果版本号没有指定某个下标处的修订号,则该修订号视为0。例如,”1.1”的版本号小于”1.1.1”。因为”1.1”的版本号相当于”1.1.0”,第3位修订号的下标为0,小于1
三. version1
>version2
返回1,如果version1
<version2
返回-1,不然返回0.
数据范围:
1 <= version1.length, version2.length <= 1000version1
和version2
的修订号不会超过int的表达范围,即不超过32位整数的范围
1 | int res=0; |
滑动窗口问题
最长无重复子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
1 | // 解法:滑动窗口+统计字符个数 |
合并两个有序的数组
给出一个有序的整数数组 A 和有序的整数数组 B ,请将数组 B 合并到数组 A 中,变成一个有序的升序数组
数据范围:$0≤n,m≤100$,$|A_i| <=100,|B_i| <= 100$
注意:
1.保证 A 数组有足够的空间存放 B 数组的元素, A 和 B 中初始的元素数目分别为 m 和 n,A的数组空间大小为 m+n
2.不要返回合并的数组,将数组 B 的数据合并到 A 里面就好了,且后台会自动将合并后的数组 A 的内容打印出来,所以也不需要自己打印
3.A 数组在[0,m-1]的范围也是有序的
输入:[4,5,6],[1,2,3]
返回值:[1,2,3,4,5,6]
1 | // 思路:使用3个指针,一个指向A的最大值,一个指向B的最大值, |
两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
哈希表解法
时间O(n),空间O(n)
1 | vector<int> res; |
BM53 缺失的第一个正整数
给定一个未排序的整数数组nums,请你找出其中没有出现的最小的正整数
进阶: 空间复杂度$O(1)$,时间复杂度$O(n)$
数据范围:
$-2^31<=nums[i]<=2^31-1$
$0<=len(nums)<=5*10^5$
输入:[-2,3,4,1,5]
返回值:2
1 | int n; |
三数之和
排序+双指针法
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
1 | // 解题思路: |
JZ51 数组中的逆序对
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P mod 1000000007
数据范围:对于50%的数据, size≤10^4
对于100%的数据, size≤10^5
数组中所有数字的值满足0≤val≤1000000
要求:空间复杂度O(n),时间复杂度O(nlogn)
输入描述:
题目保证输入的数组中没有的相同的数字
输入:[1,2,3,4,5,6,7,0]
返回值:7
1 | int mod = 1000000007; |
BM97 旋转数组
一个数组A中存有 n 个整数,在不允许使用另外数组的前提下,将每个整数循环向右移$M( M >=0)$个位置,即将A中的数据由$(A_0 A_1 ……A_{N-1} )$变换为$(A_{N-M} …… A_{N-1} A_0 A_1 ……A_{N-M-1} )$(最后 M 个数循环移至最前面的 M 个位置)。如果需要考虑程序移动数据的次数尽量少,要如何设计移动的方法?
数据范围:$0<n≤100,0≤m≤1000$
进阶:空间复杂度$O(1)$,时间复杂度$O(n)$
输入:6,2,[1,2,3,4,5,6]
返回值:[5,6,1,2,3,4]
1 | // 思路:3次翻转数组,第一次整体翻转,第二次m部分翻转,第三次n-m部分翻转 |
字符串
JZ58 左旋字符串
汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列 S ,请你把其循环左移 K 位后的序列输出。例如,字符序列 S = ”abcXYZdef” , 要求输出循环左移 3 位后的结果,即 “XYZdefabc”
数据范围:输入的字符串长度满足 0≤len≤100, 0≤n≤100
进阶:空间复杂度O(n) ,时间复杂度O(n)
“abcXYZdef”,3
“XYZdefabc”
1 | string LeftRotateString(string str, int n) { |
最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
输入:s = “babad”
输出:”bab”
解释:”aba” 同样是符合题意的答案。
思路:
- 枚举每个字符,以每个字符为中心,或者以该字符和下一个字符为中心,向左右拓展进行长度统计;
- 记录下长的回文子串的长度。
1 | int l, r; |
链表
JZ6 从尾到头打印链表
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
输入:head = [1,3,2]
输出:[2,3,1]
1 | vector<int> res; |
JZ24 反转链表
迭代法
1 | ListNode* reverseList(ListNode* head) { |
递归法
1 | ListNode* reverseList(ListNode* head) { |
BM2 链表内指定区间反转
将一个节点数为size
链表m
位置到n
位置之间的区间反转,要求时间复杂度$O(n)$,空间复杂度$O(1)$。
例如:
给出的链表为1→2→3→4→5→NULL, m=2,n=4
返回1→4→3→2→5→NULL
数据范围: 链表长度0<size≤1000,0<m≤n≤size,链表中每个节点的值满足 |val|≤1000
要求:时间复杂度$O(n)$,空间复杂度$O(n)$
进阶:时间复杂度$O(n)$,空间复杂度$O(1)$
递归法
1 | ListNode* reverseBetween(ListNode* head, int m, int n) { |
迭代法
1 |
链表排序
升序 排列并返回 排序后的链表。(归并排序的思想)
4->2->1->3 => 1->2->3->4
1 | /** |
BM3 链表中的节点每k个一组翻转
将给出的链表中的节点每 k 个一组翻转,返回翻转后的链表
如果链表中的节点数不是 k 的倍数,将最后剩下的节点保持原样
你不能更改节点中的值,只能更改节点本身。
数据范围:0≤n≤2000,1≤k≤2000 ,链表中每个元素都满足0≤val≤1000
要求空间复杂度$O(1)$,时间复杂度$O(n)$
例如:
给定的链表是1→2→3→4→5
对于k=2
, 你应该返回2→1→4→3→5
对于k=3
, 你应该返回3→2→1→4→5
思路:
- 按照k进行分组,k个为一组的组内元素反转链表,否则返回表头;
- 将头接到递归的下一组的头上。
1 | ListNode* reverseKGroup(ListNode* head, int k) { |
快慢指针
JZ52 两个链表的第一个公共结点
输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
数据范围:n≤1000
要求:空间复杂度 O(1),时间复杂度 O(n)
输入:{1},{2,3},{}
返回值:{}
说明:2个链表没有公共节点 ,返回null,后台打印{}
1 | ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) { |
环形链表I
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
1 | bool hasCycle(ListNode *head) { |
JZ23 链表中环的入口结点
给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。
数据范围:n≤10000,1<=结点值<=10000
要求:空间复杂度 O(1),时间复杂度 O(n)
输入描述:输入分为2段,第一段是入环前的链表部分,第二段是链表环的部分,后台会根据第二段是否为空将这两段组装成一个无环或者有环单链表
返回值描述:返回链表的环的入口结点即可,我们后台程序会打印这个结点对应的结点值;若没有,则返回对应编程语言的空结点即可。
输入:{1,2},{3,4,5}
返回值:3
说明:返回环形链表入口结点,我们后台程序会打印该环形链表入口结点对应的结点值,即3
1 | ListNode* EntryNodeOfLoop(ListNode* pHead) { |
JZ22 链表中倒数最后k个结点
输入一个长度为 n 的链表,设链表中的元素的值为 ai,返回该链表中倒数第k个节点。
如果该链表长度小于k,请返回一个长度为 0 的链表。
数据范围:0≤n≤10^5, 0≤ai≤10^9, 0≤k≤10
进阶:空间复杂度 O(1),时间复杂度 O(n)
输入:{1,2,3,4,5},2
返回值:{4,5}
说明:返回倒数第2个节点4,系统会打印后面所有的节点来比较。
1 | // 快慢指针:快指针先走k步,慢指针才出发,两者同步,当快指针到终点的位置,慢指针的位置就是待求位置 |
删除链表的倒数第 N 个结点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
1 | ListNode* removeNthFromEnd(ListNode* head, int n) { |
BM14 链表的奇偶重排
给定一个单链表,请设定一个函数,将链表的奇数位节点和偶数位节点分别放在一起,重排后输出。
注意是节点的编号而非节点的数值。
数据范围:节点数量满足0≤n≤10^5,节点中的值都满足0≤val≤1000
要求:空间复杂度$O(n)$,时间复杂度$O(n)$
思路:
- 利用odd指针指向奇数位,even指针指向偶数位;
- 两个指针依次走动即可。
1 | ListNode* oddEvenList(ListNode* head) { |
JZ35 复杂链表的复制
请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
1 | // 使用哈希表建立原链表和新链表的关系 |
JZ25 合并两个排序的链表
输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。
数据范围:10000≤n≤1000,−1000≤节点值≤1000
要求:空间复杂度 O(1),时间复杂度 O(n)
如输入{1,3,5},{2,4,6}时,合并后的链表为{1,2,3,4,5,6}
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
思路:
- 先定义表头;
- 比较两个链表的值,值小的接到新链表上。
1 | ListNode* Merge(ListNode* pHead1, ListNode* pHead2) { |
合并K个升序链表
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
利用最小堆解决不同数组中链表的排序关系。
1 | struct cmp { |
JZ76 删除链表中重复的结点
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表 1->2->3->3->4->4->5 处理后为 1->2->5
数据范围:链表长度满足0≤n≤1000 ,链表中的值满足1≤val≤1000
进阶:空间复杂度 O(n),时间复杂度 O(n)
输入:{1,2,3,3,4,4,5}
返回值:{1,2,5}
哈希表解法
ListNode* deleteDuplication(ListNode* pHead) {
// 哈希表法:不管有序无序都可以做,记录结点出现的次数,删去次数小于1的结点
ListNode *dummy = new ListNode(-1), *p = pHead;
dummy->next = pHead;
unordered_map<int, int> memo;
while (p != nullptr) {
memo[p->val]++;
p = p->next;
}
p = dummy;
while (p->next != nullptr) {
if (memo[p->next->val] > 1) {
p->next = p->next->next;
}
else {
p = p->next;
}
}
return dummy->next;
}
直接法
// 直接比较相邻的元素是否相等
ListNode* deleteDuplication(ListNode* pHead) {
ListNode *dummy = new ListNode(-1), *p = dummy;
dummy->next = pHead;
while (p->next != nullptr && p->next->next != nullptr) {
if (p->next->val == p->next->next->val) {
int temp = p->next->val;
while (p->next != nullptr && temp == p->next->val) {
p->next = p->next->next;
}
}
else {
p = p->next;
}
}
return dummy->next;
}
JZ18 删除链表的节点
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。返回删除后的链表的头节点。
1.此题对比原题有改动
2.题目保证链表中节点的值互不相同
3.该题只会输出返回的链表和结果做对比,所以若使用 C 或 C++ 语言,你不需要 free 或 delete 被删除的节点
数据范围:
0<=链表节点值<=10000
0<=链表长度<=10000
输入:{2,5,1,9},5
返回值:{2,1,9}
说明:
给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 2 -> 1 -> 9
1 | ListNode* deleteNode(ListNode* head, int val) { |
BM11 链表相加(二)
假设链表中每一个节点的值都在0-9
之间,那么链表整体就可以代表一个整数。
给定两个这种链表,请生成代表两个整数相加值的结果链表。
数据范围:0≤n,m≤1000000,链表任意值0≤val≤9
要求:空间复杂度$O(n)$,时间复杂度$O(n)$
例如:链表1为 9->3->7,链表2为 6->3,最后生成新的结果链表为1->0->0->0。
思路:
- 先反转链表;
- 同时遍历两个链表进行求值和进位数;
- 把值添加入链表。
1 | ListNode* addInList(ListNode* head1, ListNode* head2) { |
BM15 删除有序链表中重复的元素-I
删除给出链表中的重复元素(链表中元素从小到大有序),使链表中的所有元素都只出现一次
例如:
给出的链表为1→1→2,返回1→2.
给出的链表为1→1→2→3→3,返回1→2→3.
数据范围:链表长度满足0≤n≤100,链表中任意节点的值满足∣val∣≤100
进阶:空间复杂度$O(1)$,时间复杂度$O(n)$
思路:
- 定义两个指针,一个停留在当前位置,直到下一个值不同的元素才指过去;
- 遇到不一样的值时,更新两个指针的位置;
1 | ListNode* deleteDuplicates(ListNode* head) { |
BM16 删除有序链表中重复的元素-II
给出一个升序排序的链表,删除链表中的所有重复出现的元素,只保留原链表中只出现一次的元素。
例如:
给出的链表为1→2→3→3→4→4→5, 返回1→2→5.
给出的链表为1→1→1→2→3, 返回2→3.
数据范围:链表长度0≤n≤10000,链表中的值满足 |val|≤1000
要求:空间复杂度$O(n)$,时间复杂度$O(n)$
进阶:空间复杂度$O(1)$,时间复杂度$O(n)$
1 | ListNode* deleteDuplicates(ListNode* head) { |
JZII26 重排链表
给定一个单链表 L 的头节点 head ,单链表 L 表示为:
L0 → L1 → … → Ln-1 → Ln
请将其重新排列后变为:
L0 → Ln → L1 → Ln-1 → L2 → Ln-2 → …
不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
输入: head = [1,2,3,4,5]
输出: [1,5,2,4,3]
链表的长度范围为 [1, 5 * 10^4]
1 <= node.val <= 1000
该题由寻找链表的中点,反转链表,合并两个链表三道简单题目组成。
1 | void reorderList(ListNode* head) { |
树
二叉树建树
1 | struct TreeNode { |
二叉树的前序遍历
遍历过程:根->左->右。
递归法
1 | vector<int> res; |
非递归法
1 | stack<TreeNode *> s; |
二叉树的中序遍历
遍历过程:左->根->右。
递归法
1 | vector<int> res; |
非递归法
1 | vector<int> res; |
二叉树的后序遍历
遍历过程:左->右->根。
递归法
1 | vector<int> res; |
非递归法
1 | vector<int> res; |
JZ55 二叉树的最大深度
输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度,根节点的深度视为 1 。
数据范围:节点的数量满足0≤n≤100 ,节点上的值满足0≤val≤100
进阶:空间复杂度 O(1) ,时间复杂度 O(n)
输入:{1,2,3,4,5,#,6,#,#,7}
返回值:4
递归法
1 | int TreeDepth(TreeNode* pRoot) { |
层次遍历法
1 | // 层次遍历写法 |
前缀树
相同前缀的字符串集中在 Trie 树中的一个子树。前缀树一般用来高效操作字符串。Tire树的本质是二叉树衍生出来的多叉树。
Trie() 初始化前缀树对象。
void insert(String word) 向前缀树中插入字符串word。
boolean search(String word) 如果字符串word在前缀树中,返回true(即,在检索之前已经插入);否则,返回false。
boolean startsWith(String prefix) 如果之前已经插入的字符串word的前缀之一为prefix,返回true;否则,返回false。
输入
[“Trie”, “insert”, “search”, “search”, “startsWith”, “insert”, “search”]
[[], [“apple”], [“apple”], [“app”], [“app”], [“app”], [“app”]]
输出
[null, null, true, false, true, null, true]
解释
Trie trie = new Trie();
trie.insert(“apple”);
trie.search(“apple”); // 返回 True
trie.search(“app”); // 返回 False
trie.startsWith(“app”); // 返回 True
trie.insert(“app”);
trie.search(“app”); // 返回 True
1 | class Trie { |
JZ37 序列化二叉树
请实现两个函数,分别用来序列化和反序列化二叉树,不对序列化之后的字符串进行约束,但要求能够根据序列化之后的字符串重新构造出一棵与原二叉树相同的树。
二叉树的序列化(Serialize)是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、层序的二叉树等遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过 某种符号表示空节点(#)
二叉树的反序列化(Deserialize)是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。
输入:{1,2,3,#,#,6,7}
返回值:{1,2,3,#,#,6,7}
1 | char* Serialize(TreeNode *root) { |
ZJ82 二叉树中和为某一值的路径(一)
给定一个二叉树root和一个值 sum ,判断是否有从根节点到叶子节点的节点值之和等于 sum 的路径。
1.该题路径定义为从树的根结点开始往下一直到叶子结点所经过的结点
2.叶子节点是指没有子节点的节点
3.路径只能从父节点到子节点,不能从子节点到父节点
4.总节点数目为n
给出如下的二叉树,sum=22,
返回true,因为存在一条路径25→4→11→2的节点值之和为 22
数据范围:
1.树上的节点数满足0≤n≤10000
2.每 个节点的值都满足 ∣val∣≤1000
要求:空间复杂度 O(n),时间复杂度 O(n)
进阶:空间复杂度 O(树的高度),时间复杂度O(n)
递归法
1 | // 递归法 |
JZ34 二叉树中和为某一值的路径(二)
输入一颗二叉树的根节点root和一个整数expectNumber,找出二叉树中结点值的和为expectNumber的所有路径。
1.该题路径定义为从树的根结点开始往下一直到叶子结点所经过的结点
2.叶子节点是指没有子节点的节点
3.路径只能从父节点到子节点,不能从子节点到父节点
4.总节点数目为n
如二叉树root为{10,5,12,4,7},expectNumber为22则合法路径有[[10,5,7],[10,12]]
数据范围:
树中节点总数在范围 [0, 5000] 内
-1000 <= 节点值 <= 1000
-1000 <= expectNumber <= 1000
回溯解法
1 | vector<vector<int>> res; |
JZ77 按之字形顺序打印二叉树
给定一个二叉树,返回该二叉树的之字形层序遍历,(第一层从左向右,下一层从右向左,一直这样交替)
数据范围:0≤n≤1500,树上每个节点的val满足 ∣val∣<=1500
要求:空间复杂度:O(n),时间复杂度:O(n)
输入:{1,2,3,#,#,4,5}
返回值:[[1],[3,2],[4,5]]
层次遍历
1 | vector<vector<int>> res; |
JZ54 二叉搜索树的第k个节点
给定一棵结点数为n 二叉搜索树,请找出其中的第 k 小的TreeNode结点值。
1.返回第k小的节点值即可
2.不能查找的情况,如二叉树为空,则返回-1,或者k大于n等等,也返回-1
3.保证n个节点的值不一样
数据范围:0≤n≤1000, 0≤k≤1000,树上每个结点的值满足0≤val≤1000
进阶:空间复杂度 O(n),时间复杂度 O(n)
输入:{5,3,7,2,4,6,8},3
返回值:4
递归做法
1 | vector<int> res; |
非递归用栈
1 | int KthNode(TreeNode* proot, int k) { |
BM32 合并二叉树
已知两颗二叉树,将它们合并成一颗二叉树。合并规则是:都存在的结点,就将结点值加起来,否则空的位置就由另一个树的结点来代替。
数据范围:树上节点数量满足$0≤n≤500$,树上节点的值一定在32位整型范围内。
进阶:空间复杂度$O(1)$,时间复杂度$O(n)$
输入:{1,3,2,5},{2,1,3,#,4,#,7}
返回值:{3,4,5,5,4,#,7}
1 | TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { |
JZ7 重建二叉树
给定节点数为 n 的二叉树的前序遍历和中序遍历结果,请重建出该二叉树并返回它的头结点。
数据范围:n≤2000,节点的值−10000≤val≤10000
要求:空间复杂度O(n),时间复杂度O(n)
输入:[1,2,4,7,3,5,6,8],[4,7,2,1,5,3,8,6]
返回值:[1,2,4,7,3,5,6,8],[4,7,2,1,5,3,8,6]
递归做法
1 | // 根据前序和中序重建二叉树 |
非递归做法
1 | // 栈的做法 |
JZ26 树的子结构
输入两棵二叉树A,B,判断B是不是A的子结构。(我们约定空树不是任意一个树的子结构)
假如给定A为{8,8,7,9,2,#,#,#,#,4,7},B为{8,9,2},2个树的结构如下,可以看出B是A的子结构
数据范围:
0 <= A的节点个数 <= 10000
0 <= B的节点个数 <= 10000
输入:{8,8,7,9,2,#,#,#,#,4,7},{8,9,2}
返回值:true
前序遍历法
1 | bool recur(TreeNode *root1, TreeNode *root2) { |
JZ27 二叉树的镜像
操作给定的二叉树,将其变换为源二叉树的镜像。
数据范围:二叉树的节点数0≤n≤1000 , 二叉树每个节点的值0≤val≤1000
要求: 空间复杂度 O(n)。
后序递归法
1 | TreeNode* Mirror(TreeNode* pRoot) { |
非递归用栈
1 | TreeNode* Mirror(TreeNode* pRoot) { |
BM31 对称的二叉树
给定一棵二叉树,判断其是否是自身的镜像(即:是否对称)
数据范围:节点数满足0≤n≤1000,节点上的值满足∣val∣≤1000
要求:空间复杂度 O(n),时间复杂度 O(n)
备注:你可以用递归和迭代两种方法解决这个问题
输入:{1,2,2,3,4,4,3}
返回值:true
如果二叉树是对称的,那么按照”根-左-右”的顺序和按照”根-右-左”的顺序返回的值是一样的。
1 | bool isSymmetrical(TreeNode* pRoot) { |
JZ32 从上往下打印二叉树
不分行从上往下打印出二叉树的每个节点,同层节点从左至右打印。例如输入{8,6,10,#,#,2,1},如以下图中的示例二叉树,则依次打印8,6,10,2,1(空节点不打印,跳过),请你将打印的结果存放到一个数组里面,返回。
数据范围:
0<=节点总数<=1000
-1000<=节点值<=1000
输入:{8,6,10,#,#,2,1}
返回值:[8,6,10,2,1]
1 | vector<int> res; |
BM34 判断是不是二叉搜索树
给定一个二叉树根节点,请你判断这棵树是不是二叉搜索树。
二叉搜索树满足每个节点的左子树上的所有节点均严格小于当前节点且右子树上的所有节点均严格大于当前节点。
数据范围:节点数量满足$1≤n≤10^4$,节点上的值满足$-2^{31}≤val≤2^31-1$
输入:{2,1,3}
返回值:true
1 | // 中序遍历一遍即可 |
JZ79 判断是不是平衡二叉树
输入一棵节点数为 n 二叉树,判断该二叉树是否是平衡二叉树。
在这里,我们只需要考虑其平衡性,不需要考虑其是不是排序二叉树
平衡二叉树(Balanced Binary Tree),具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
注:我们约定空树是平衡二叉树。
数据范围:n≤100,树上节点的val值满足0≤n≤1000
要求:空间复杂度O(1),时间复杂度 O(n)
自顶向下
1 | bool IsBalanced_Solution(TreeNode* pRoot) { |
自底向上
1 | // 从最底的叶子结点开始,计算该结点的高度。 |
BM35 判断是不是完全二叉树
给定一个二叉树,确定他是否是一个完全二叉树。
完全二叉树的定义:若二叉树的深度为 h,除第 h 层外,其它各层的结点数都达到最大个数,第 h 层所有的叶子结点都连续集中在最左边,这就是完全二叉树。(第 h 层可能包含 [1~2h] 个节点)
数据范围:节点数满足$1≤n≤100$
输入:{1,2,3,4,5,#,6}
返回值:false
1 | // 思路:层次遍历,标记出现空结点的位置即可 |
二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
思路:
- 递归左右子树;
- 两种情况:
- 1、要么左右子树都包含p和q结点;
- 2、要么p和q结点只在左子树或只在右子树。
1 | TreeNode *res; |
JZ68 二叉搜索树的最近公共祖先
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
1.对于该题的最近的公共祖先定义:对于有根树T的两个节点p、q,最近公共祖先LCA(T,p,q)表示一个节点x,满足x是p和q的祖先且x的深度尽可能大。在这里,一个节点也可以是它自己的祖先.
2.二叉搜索树是若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值; 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值
3.所有节点的值都是唯一的。
4.p、q 为不同节点且均存在于给定的二叉搜索树中。
数据范围:
3<=节点总数<=10000
0<=节点值<=10000
输入:{7,1,12,0,4,11,14,#,#,3,5},1,12
返回值:7
说明:节点1 和 节点12的最近公共祖先是7
1 | // 1、从根节点出发,找到p和q两个结点的路径 |
BM41 输出二叉树的右视图
请根据二叉树的前序遍历,中序遍历恢复二叉树,并打印出二叉树的右视图
数据范围:$0≤n≤10000$
要求: 空间复杂度$O(n)$,时间复杂度$O(n)$
输入:[1,2,4,5,3],[4,2,5,1,3]
返回值:[1,3,5]
1 | // 利用哈希表来记录中序的位置关系,统计长度来建树 |
二分搜索
JZ4 二维数组中的查找
在一个二维数组array中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
[
[1,2,8,9],
[2,4,9,12],
[4,7,10,13],
[6,8,11,15]
]
给定 target = 7,返回 true。
给定 target = 3,返回 false。
数据范围:矩阵的长宽满足0≤n,m≤500,矩阵中的值满足0≤val≤10^9
进阶:空间复杂度O(1),时间复杂度O(n+m)
1 | // 对于矩阵,按副对角线进行二分,从元素左下遍历到右上的位置; |
BM19 寻找峰值
给定一个长度为n的数组nums,请你找到峰值并返回其索引。数组可能包含多个峰值,在这种情况下,返回任何一个所在位置即可。
1.峰值元素是指其值严格大于左右相邻值的元素。严格大于即不能有等于
2.假设 nums[-1] = nums[n] = −∞
3.对于所有有效的 i 都有 nums[i] != nums[i + 1]
4.你可以使用O(logN)的时间复杂度实现此问题吗?
数据范围:1≤nums.length≤2×10^5
-2^{31}<= nums[i] <= 2^{31}-1
如输入[2,4,1,2,7,8,4]时,会形成两个山峰,一个是索引为1,峰值为4的山峰,另一个是索引为5,峰值为8的山峰。
1 | int findPeakElement(vector<int>& nums) { |
JZ11 旋转数组的最小数字
有一个长度为 n 的非降序数组,比如[1,2,3,4,5],将它进行旋转,即把一个数组最开始的若干个元素搬到数组的末尾,变成一个旋转数组,比如变成了[3,4,5,1,2],或者[4,5,1,2,3]这样的。请问,给定这样一个旋转数组,求数组中的最小值。
数据范围:1≤n≤10000,数组中任意元素的值:0≤val≤10000
要求:空间复杂度:O(1),时间复杂度:O(logn)
1 | int minNumberInRotateArray(vector<int> rotateArray) { |
数学技巧
厄拉多塞筛法
- 如果一个数
n
是质数,它只需要满足在[2,sqrt(n))
的范围内是质数即可。 - 如果一个数
x
是质数,则对应的2x
,3x
…必定不是质数。
计数质数
给定整数 n ,返回 所有小于非负整数 n 的质数的数量 。
输入:n = 10
输出:4
解释:小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。
1 | int countPrimes(int n) { |
位操作
1 | // 利用或操作 `|` 和空格将英文字符转换为小写 |
n & (n-1)
作用是消除数字 n
的二进制表示中的位置最后的 1。
1 | cout << (4&3); // 0 |
位1的个数
输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 ‘1’。
1 | int hammingWeight(uint32_t n) { |
判断一个数是不是 2 的指数
输入:n = 16
输出:true
解释:24 = 16
你能够不使用循环/递归解决此问题吗?
1 | // 如果一个数是2的指数,则它的二进制表示中只有一个1 |
a ^ a = 0
一个数和它本身做异或运算结果为 0,即 a ^ a = 0
;一个数和 0 做异或运算的结果为它本身,即 a ^ 0 = a
。
查找只出现一次的元素
输入: [4,1,2,1,2]
输出: 4
1 | int singleNumber(vector<int>& nums) { |
寻找缺失的元素
只要把所有的元素和索引做异或运算,成对儿的数字都会消为 0,只有这个落单的元素会剩下。
输入:nums = [3,0,1]
输出:2
解释:n = 3,因为有 3 个数字,所以所有的数字都在范围 [0,3] 内。2 是丢失的数字,因为它没有出现在 nums 中。
1 | int missingNumber(vector<int>& nums) { |
快速乘+快速幂
JZ83 剪绳子II
给你一根长度为 n 的绳子,请把绳子剪成整数长的 m 段( m 、 n 都是整数, n > 1 并且 m > 1 , m <= n ),每段绳子的长度记为 k[1],…,k[m] 。请问 k[1]×k[2]×…×k[m] 可能的最大乘积是多少?例如,当绳子的长度是 8 时,我们把它剪成长度分别为 2、3、3 的三段,此时得到的最大乘积是 18 。
由于答案过大,请对 998244353 取模。
数据范围:2≤n≤10^14
进阶:空间复杂度 O(1), 时间复杂度 O(logn)
输入:4
返回值:4
说明:拆分成 2 个长度为 2 的绳子,2×2=4
1 | long long mod = 998244353; |
阶乘操作
阶乘后的零
给定一个整数 n ,返回 n! 结果中尾随零的数量。
输入:n = 5
输出:1
解释:5! = 120 ,有一个尾随 0
该题目可以转化为n!
中含有多少个5的倍数。
5!<=1, 25!<=5×1+1, 125<=5×(5+1)+1=5×5+5+1
1 | // n!中含有多少个5的倍数 |
阶乘后的K个零
二分法+阶乘后的零统计。
输入:k = 0
输出:5
解释:0!, 1!, 2!, 3!, 和 4! 均符合 k = 0 的条件。
0 <= k <= 10^9
1 | int preimageSizeFZF(int k) { |
水塘抽样
题目特点:给一个未知长度n
的序列,如何在其中随机地选择 k
个元素?其中每个样本被选中的概率时一样的。
只选一个元素:当你遇到第 i
个元素时,以 1/i
的概率更新结果就可以保证结果是平均随机。
即第i
个元素被选中的概率为1/n
。
选k
个元素:当你遇到第 i
个元素时,以 k/i
的概率更新结果就可以保证结果是平均随机。
即第i
个元素被选中的概率为k/n
。
做法:随机选择区间 [0,i)
内的一个整数,如果其等于 0,则将返回值置为该元素。
链表随机节点
给你一个单链表,随机选择链表的一个节点,并返回相应的节点值。每个节点 被选中的概率一样。
输入
[“Solution”, “getRandom”, “getRandom”, “getRandom”, “getRandom”, “getRandom”]
[[[1, 2, 3]], [], [], [], [], []]
输出
[null, 1, 3, 2, 2, 3]
解释
Solution solution = new Solution([1, 2, 3]);
solution.getRandom(); // 返回 1
solution.getRandom(); // 返回 3
solution.getRandom(); // 返回 2
solution.getRandom(); // 返回 2
solution.getRandom(); // 返回 3
// getRandom() 方法应随机返回 1、2、3中的一个,每个元素被返回的概率相等。
1 | Solution(ListNode* head) { |
随机数索引
给你一个可能含有 重复元素 的整数数组 nums ,请你随机输出给定的目标数字 target 的索引。你可以假设给定的数字一定存在于数组中。
输入
[“Solution”, “pick”, “pick”, “pick”]
[[[1, 2, 3, 3, 3]], [3], [1], [3]]
输出
[null, 4, 0, 2]
1 | private: |
博弈论
巴什博奕:n个物品,两个人轮流从这堆物品中取物,规定每次至少取一个,最多取m个。最后取光者得胜。
结论:如果 n 是(m+1)的倍数,那么 先手者就必输。证明如下。
令$n=k(m+1)+x$,只要先手者每次保持物品数量是(m+1)的整数倍,最后造成的局面一定是对于后手者,只剩下m+1个物品,此时无论后手者怎么选,先手者都能赢。
Nim游戏
你和你的朋友,两个人一起玩 Nim 游戏:
桌子上有一堆石头。
你们轮流进行自己的回合, 你作为先手 。
每一回合,轮到的人拿掉 1 - 3 块石头。
拿掉最后一块石头的人就是获胜者。
假设你们每一步都是最优解。请编写一个函数,来判断你是否可以在给定石头数量为 n 的情况下赢得游戏。如果可以赢,返回 true;否则,返回 false 。
你和你的朋友,两个人一起玩 Nim 游戏:
输入:n = 4
输出:false
1 | bool canWinNim(int n) { |
脑筋急转弯
灯泡开关
初始时有 n 个灯泡处于关闭状态。第一轮,你将会打开所有灯泡。接下来的第二轮,你将会每两个灯泡关闭第二个。
第三轮,你每三个灯泡就切换第三个灯泡的开关(即,打开变关闭,关闭变打开)。第 i 轮,你每 i 个灯泡就切换第 i 个灯泡的开关。直到第 n 轮,你只需要切换最后一个灯泡的开关。
找出并返回 n 轮后有多少个亮着的灯泡
输入:n = 3
输出:1
解释:
初始时, 灯泡状态 [关闭, 关闭, 关闭].
第一轮后, 灯泡状态 [开启, 开启, 开启].
第二轮后, 灯泡状态 [开启, 关闭, 开启].
第三轮后, 灯泡状态 [开启, 关闭, 关闭].
1 | int bulbSwitch(int n) { |
约瑟夫环问题
JZ62 孩子们的游戏(圆圈中最后剩下的数)
有个游戏是这样的:首先,让 n 个小朋友们围成一个大圈,小朋友们的编号是0~n-1。然后,随机指定一个数 m ,让编号为0的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0… m-1报数….这样下去….直到剩下最后一个小朋友,可以不用表演,并且拿到牛客礼品,请你试着想下,哪个小朋友会得到这份礼品呢?
数据范围:1≤n≤5000,1≤m≤10000
要求:空间复杂度 O(1),时间复杂度 O(n)
输入:5,3
返回值:3
递归方法
1 | int LastRemaining_Solution(int n, int m) { |
迭代方法
1 | int LastRemaining_Solution(int n, int m) { |
特殊数据结构
最大栈
1 | // 对于pair, tuple这样的数据类型。 |
最大频率栈
设计一个类似堆栈的数据结构,将元素推入堆栈,并从堆栈中弹出出现频率最高的元素。
实现 FreqStack 类:
FreqStack() 构造一个空的堆栈。
void push(int val) 将一个整数 val 压入栈顶。
int pop() 删除并返回堆栈中出现频率最高的元素。
如果出现频率最高的元素不只一个,则移除并返回最接近栈顶的元素。
输入:
[“FreqStack”,”push”,”push”,”push”,”push”,”push”,”push”,”pop”,”pop”,”pop”,”pop”],
[[],[5],[7],[5],[7],[4],[5],[],[],[],[]]
输出:[null,null,null,null,null,null,null,5,7,5,4]
解释:
FreqStack = new FreqStack();
freqStack.push (5);//堆栈为 [5]
freqStack.push (7);//堆栈是 [5,7]
freqStack.push (5);//堆栈是 [5,7,5]
freqStack.push (7);//堆栈是 [5,7,5,7]
freqStack.push (4);//堆栈是 [5,7,5,7,4]
freqStack.push (5);//堆栈是 [5,7,5,7,4,5]
freqStack.pop ();//返回 5 ,因为 5 出现频率最高。堆栈变成 [5,7,5,7,4]。
freqStack.pop ();//返回 7 ,因为 5 和 7 出现频率最高,但7最接近顶部。堆栈变成 [5,7,5,4]。
freqStack.pop ();//返回 5 ,因为 5 出现频率最高。堆栈变成 [5,7,4]。
freqStack.pop ();//返回 4 ,因为 4, 5 和 7 出现频率最高,但 4 是最接近顶部的。堆栈变成 [5,7]。
0 <= val <= 109
push 和 pop 的操作数不大于 2 * 104。
输入保证在调用 pop 之前堆栈中至少有一个元素。
优先队列+哈希表
优先队列C++默认是大顶堆。
1 | // 方法一:优先队列+哈希表 |
两个哈希表
1 | // 第二种解法:两个哈希表 |
JZ49 丑数
把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第 n个丑数。
数据范围:0≤n≤2000
要求:空间复杂度 O(n), 时间复杂度 O(n)
输入:7
返回值:8
利用最小堆记录每个丑数,利用哈希表来防止有重复的丑数入最小堆。
1 | int GetUglyNumber_Solution(int index) { |
BM48 数据流中的中位数
输入:[5,2,3,4,1,6,7,0,8]
返回值:”5.00 3.50 3.00 3.50 3.00 3.50 4.00 3.50 4.00 “
说明:
数据流里面不断吐出的是5,2,3…,则得到的平均数分别为5,(5+2)/2,3…
1 | // 默认是大顶堆,存着值较小的值 |
单调栈
每次新元素入栈后,栈内的元素都保持有序(单调递增或单调递减)。
1 | vector<int> nextGreaterElement(vector<int>& nums) { |
步骤:
- 从后往前处理;
- 什么时候出栈;
- 什么时候入栈。
下一个更大的元素I
nums1 中数字 x 的 下一个更大元素 是指 x 在 nums2 中对应位置 右侧 的 第一个 比 x 大的元素。
给你两个 没有重复元素 的数组 nums1 和 nums2 ,下标从 0 开始计数,其中nums1 是 nums2 的子集。
对于每个 0 <= i < nums1.length ,找出满足 nums1[i] == nums2[j] 的下标 j ,并且在 nums2 确定 nums2[j] 的 下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1 。
返回一个长度为 nums1.length 的数组 ans 作为答案,满足 ans[i] 是如上所述的 下一个更大元素 。
输入:nums1 = [4,1,2], nums2 = [1,3,4,2].
输出:[-1,3,-1]
解释:nums1 中每个值的下一个更大元素如下所述:
- 4 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。
- 1 ,用加粗斜体标识,nums2 = [1,3,4,2]。下一个更大元素是 3 。
- 2 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。
解法:单调栈+哈希表 => 时间:O(n+m)
1 | vector<int> res; |
下一个更大的元素II
输入: nums = [1,2,1]
输出: [2,-1,2]
解释: 第一个 1 的下一个更大的数是 2;
数字 2 找不到下一个更大的数;
第二个 1 的下一个最大的数需要循环搜索,结果也是 2。
1 | vector<int> res; |
每日温度
给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指在第 i 天之后,才会有更高的温度。如果气温在这之后都不会升高,请在该位置用 0 来代替。
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]
1 | vector<int> res; |
移掉 K 位数字
给你一个以字符串表示的非负整数 num 和一个整数 k ,移除这个数中的 k 位数字,使得剩下的数字最小。请你以字符串形式返回这个最小的数字。
输入:num = “10200”, k = 1
输出:”200”
解释:移掉首位的 1 剩下的数字为 200. 注意输出不能有任何前导零。
1 | // 单调栈 |
去除重复字母(不同字符的最小子序列)
给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。
输入:s = “cbacdcbc”
输出:”acdb”
1 <= s.length <= 104
s 由小写英文字母组成
1 | // 哈希表+单调栈 |
JZ33 二叉搜索树的后序遍历序列
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则返回 true ,否则返回 false 。假设输入的数组的任意两个数字都互不相同。
数据范围:节点数量0≤n≤1000 ,节点上的值满足 1≤val≤10^5,保证节点上的值各不相同
要求:空间复杂度 O(n),时间时间复杂度 O(n^2)
输入:[5,7,6,9,11,10,8]
返回值:true
判断数组中部分元素是否恒大于或者恒小于某一部分的值,考虑单调栈。
1 | // 单调栈的解法 |
单调队列
队列中的元素会保持单调递增或单调递减的顺序。C++一般使用双向队列deque来实现。deque的API如下。
1 | class deque { |
滑动窗口的最大值
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
1 | class MonotonicQueue { |
队列和栈互转
用栈实现队列
[JZ9 用两个栈实现队列]
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
输入:
[“MyQueue”, “push”, “push”, “peek”, “pop”, “empty”]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]
解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false
1 | class MyQueue { |
用队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
输入:
[“MyStack”, “push”, “push”, “top”, “pop”, “empty”]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]
解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False
思想:把值往后接上,保持一个队列为空,交换两个队列。
1 | class MyStack { |
最小栈
JZ30 包含min函数的栈
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
输入:
[“MinStack”,”push”,”push”,”push”,”getMin”,”pop”,”top”,”getMin”]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); –> 返回 -3.
minStack.pop();
minStack.top(); –> 返回 0.
minStack.getMin(); –> 返回 -2.
思路:用一个新的栈存放小于第一个元素的最小值即可。
1 | class MinStack { |
JZ 31栈的压入、弹出序列
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。
说明:
1、0<=pushV.length == popV.length <=1000
2、 -1000<=pushV[i]<=1000
3、pushV 的所有数字均不相同
输入:[1,2,3,4,5],[4,3,5,1,2]
返回值:false
1 | // 辅助栈 |
回溯法(DFS)
岛屿问题
飞地的数量
给你一个大小为 m x n 的二进制矩阵 grid ,其中 0 表示一个海洋单元格、1 表示一个陆地单元格。
一次 移动 是指从一个陆地单元格走到另一个相邻(上、下、左、右)的陆地单元格或跨过 grid 的边界。
返回网格中 无法 在任意次数的移动中离开网格边界的陆地单元格的数量。
输入:grid = [[0,0,0,0],[1,0,1,0],[0,1,1,0],[0,0,0,0]]
输出:3
解释:有三个 1 被 0 包围。一个 1 没有被包围,因为它在边界上。
思路:
- 题目的本质是回溯边界的岛屿;
- 淹没岛屿;
- 统计剩余的岛屿个数。
1 | int dirs[4][2] = {{0,1},{0,-1},{1,0},{-1,0}}; |
统计封闭岛屿的数目
二维矩阵 grid 由 0 (土地)和 1 (水)组成。岛是由最大的4个方向连通的 0 组成的群,封闭岛是一个 完全 由1包围(左、上、右、下)的岛。
请返回 封闭岛屿 的数目。
输入:grid = [[1,1,1,1,1,1,1,0],[1,0,0,0,0,1,1,0],[1,0,1,0,1,1,1,0],[1,0,0,0,0,1,0,1],[1,1,1,1,1,1,1,0]]
输出:2
解释:灰色区域的岛屿是封闭岛屿,因为这座岛屿完全被水域包围(即被 1 区域包围)。
1 | int dirs[4][2] = {{0,-1},{0,1},{-1,0},{1,0}}; |
JZII105 岛屿的最大面积
给定一个由 0 和 1 组成的非空二维数组 grid ,用来表示海洋岛屿地图。
一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
找到给定的二维数组中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。
输入:[[1,1,0,0,0],[1,1,0,0,0],[0,0,0,1,1],[0,0,0,1,1]]
输出:4
1 | int dirs[4][2] = {{-1,0},{1,0},{0,1},{0,-1}}; |
统计子岛屿
给你两个m x n
的二进制矩阵grid1
和grid2
,它们只包含0
(表示水域)和1
(表示陆地)。一个岛屿是由四个方向(水平或者竖直)上相邻的1
组成的区域。任何矩阵以外的区域都视为水域。
如果grid2
的一个岛屿,被grid1
的一个岛屿完全包含,也就是说grid2
中该岛屿的每一个格子都被grid1
中同一个岛屿完全包含,那么我们称grid2
中的这个岛屿为子岛屿。
请你返回grid2
中子岛屿的数目。
输入:grid1 = [[1,1,1,0,0],[0,1,1,1,1],[0,0,0,0,0],[1,0,0,0,0],[1,1,0,1,1]], grid2 = [[1,1,1,0,0],[0,0,1,1,1],[0,1,0,0,0],[1,0,1,1,0],[0,1,0,1,0]]
输出:3
解释:如上图所示,左边为 grid1 ,右边为 grid2 。
grid2 中标红的 1 区域是子岛屿,总共有 3 个子岛屿。
1 | int dirs[4][2] = {{0,1},{0,-1},{-1,0},{1,0}}; |
岛屿数量
给你一个由'1'
(陆地)和'0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
输入:grid = [
[“1”,”1”,”1”,”1”,”0”],
[“1”,”1”,”0”,”1”,”0”],
[“1”,”1”,”0”,”0”,”0”],
[“0”,”0”,”0”,”0”,”0”]
]
输出:1
1 | int dirs[4][2]={{-1,0},{1,0},{0,1},{0,-1}}; |
JZ12 矩阵中的路径
请设计一个函数,用来判断在一个n乘m的矩阵中是否存在一条包含某长度为len的字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。
数据范围:0≤n,m≤20,1≤len≤25
输入:[[a,b,c,e],[s,f,c,s],[a,d,e,e]],”abcced”
返回值:true
1 | vector<vector<int>> visit; |
括号生成
思路:
- 该问题是利用两次回溯进行括号匹配;
- 剪枝方法是右括号的个数要不大于左括号个数;
- 终止条件是
path
里左右括号的数量相同。
输入:n = 3
输出:[“((()))”,”(()())”,”(())()”,”()(())”,”()()()”]
1 | vector<string> res; |
1 | vector<string> res; |
子集/排列/组合问题
1、穷举元素时,元素不能回头访问,即num[i]之后的元素不出现num[i]左边的元素,用depth深度进行递归;
2、穷举元素时,元素可以回头访问,num[i]之后的元素出现在num[i]左边的元素,用访问数组进行递归。
元素不可复选
子集(数组中无重复)
无重复元素,且元素不能回头访问。
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
1 | vector<vector<int>> res; |
子集(数组中有重复)
有重复元素,且元素不能回头访问。
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
1 | // 基于子集I进行修改 |
全排列(数组中无重复)
思路:
- 该问题是无重复元素,且元素可以回头访问;
- 需要访问数组防止当前元素重复访问;
- 终止条件是
path
里面的个数和数组长度相等。
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
1 <= nums.length <= 6
-10 <= nums[i] <= 10
nums 中的所有整数 互不相同
1 | // 元素无重复,不可多次选择 |
全排列(数组中有重复)
思路:
- 该问题是有重复元素,且元素可以回头访问;
- 需要访问数组防止当前元素重复访问,此外当前元素等于前一个元素并且前一个元素没有被访问时也不能访问该元素;
- 终止条件是
path
里面的个数和数组长度相等。
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
1 <= nums.length <= 8
-10 <= nums[i] <= 10
1 | int visit[9]; |
组合
无重复元素,且元素不能回头访问。
输入:n = 4, k = 2
输出:
[[2,4], [3,4], [2,3], [1,2], [1,3], [1,4],]
1 <= n <= 20
1 <= k <= n
1 | vector<vector<int>> res; |
元素可重复选
组合的和(数组中无重复)
无重复元素,且元素不可回头访问。
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
1 <= candidates.length <= 30
1 <= candidates[i] <= 200
candidate 中的每个元素都 互不相同
1 <= target <= 500
1 | // 元素无重复但是可以重复选择 |
组合的和(数组中有重复)
有重复元素,且元素不可回头访问。
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[[1,1,6], [1,2,5], [1,7], [2,6]]
1 <= candidates.length <= 100
1 <= candidates[i] <= 50
1 <= target <= 30
1 | // 元素有重复且不能重选 |
组合的和III
无重复元素,且元素不可回头访问。
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。
2 <= k <= 9
1 <= n <= 60
1 | vector<vector<int>> res; |
BFS
问题抽象成图,从一个点开始,向四周开始扩散。一般来说,写 BFS 算法都是用「队列」这种数据结构,每次将一个节点周围的所有节点加入队列。BFS解决问题的本质就是让你在一幅「图」中找到从起点 start
到终点 target
的最近距离。
二叉树的最小深度
给定一个二叉树,找出其最小深度。最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
1 | int minDepth(TreeNode* root) { |
depth
每增加一次,队列中的所有节点都向前迈一步,这保证了第一次到达终点的时候,走的步数是最少的。
如何把数字变成字符串?
char(i + ‘0’)即可。
课程表(拓扑排序)
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
1 | private: |
字典序
字典序排数
输入:n = 13
输出:[1,10,11,12,13,2,3,4,5,6,7,8,9]
dfs遍历
1 | // 多叉树的遍历——使用dfs |
时间复杂度:$O(n)$;
空间复杂度:$O(n)$。
迭代法
1 | vector<int> res; |
字典序的第K小数字
输入: n = 13, k = 2
输出: 10
解释: 字典序的排列是 [1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9],所以第二小的数字是 10。
$1 <= k <= n <= 10^9$
1 | int findKthNumber(int n, int k) { |
时间复杂度:$O(log^2(n))$;
空间复杂度:$O(1)$。
高频系列
分治算法
本质就是二叉树的后序遍历。思想是把复杂的问题变成若干个小的子问题,递归求解子问题,再通过子问题的结果合并成原问题。
为运算符表达式设置优先级
给你一个由数字和运算符组成的字符串 expression ,按不同优先级组合数字和运算符,计算并返回所有可能组合的结果。你可以 按任意顺序 返回答案。
输入:expression = “2-1-1”
输出:[0,2]
解释:
((2-1)-1) = 0
(2-(1-1)) = 2
1 | // 记录重复子问题 |
斗地主凑顺子
分割数组为连续的子序列
给你一个按升序排序的整数数组 num(可能包含重复数字),请你将它们分割成一个或多个长度至少为 3 的子序列,其中每个子序列都由连续整数组成。
如果可以完成上述分割,则返回 true ;否则,返回 false 。
输入: [1,2,3,3,4,5]
输出: True
解释:
你可以分割出这样两个连续子序列 :
1, 2, 3
3, 4, 5
1 | class Solution { |
网易吃葡萄
有三种葡萄,每种分别有 a, b, c 颗,现在有三个人,第一个人只吃第一种和第二种葡萄,第二个人只吃第二种和第三种葡萄,第三个人只吃第一种和第三种葡萄。
现在给你输入 a, b, c 三个值,请你适当安排,让三个人吃完所有的葡萄,算法返回吃的最多的人最少要吃多少颗葡萄。
第一行数字T,表示数据组数。
接下来T行,每行三个数a,b,c
1≤a,b,c≤10^18 1≤T≤10
输入
2
1 2 3
1 2 6
输出
2
3
吃的最多的人是指当然可以把所吃的两种葡萄全部吃完,比如说第一个人全部吃完a+b,第二个人吃完c,最后一个人不吃,假设a+b>c,此时第一个人吃的最多,但是还有个条件这个人最少要吃多少颗,就是说这个人吃的还是比别人多,但是可能就比别人多一颗或者几颗或者0颗,上述安排方式显然不能满足,此时,如果安排三个人尽量平均地吃,再把葡萄数向上取整(m/n
是向下取整,改为(m+n-1)/n
则是向上取整),那么就能满足上述条件。
1 |
|
汉诺塔问题
煎饼排序
给你一个整数数组 arr ,请使用 煎饼翻转 完成对数组的排序。
一次煎饼翻转的执行过程如下:
选择一个整数 k ,1 <= k <= arr.length
反转子数组 arr[0…k-1](下标从 0 开始)
例如,arr = [3,2,1,4] ,选择 k = 3 进行一次煎饼翻转,反转子数组 [3,2,1] ,得到 arr = [1,2,3,4] 。
以数组形式返回能使 arr 有序的煎饼翻转操作所对应的 k 值序列。任何将数组排序且翻转次数在 10 * arr.length 范围内的有效答案都将被判断为正确。
输入:[3,2,4,1]
输出:[4,2,4,3]
解释:
我们执行 4 次煎饼翻转,k 值分别为 4,2,4,和 3。
初始状态 arr = [3, 2, 4, 1]
第一次翻转后(k = 4):arr = [1, 4, 2, 3]
第二次翻转后(k = 2):arr = [4, 1, 2, 3]
第三次翻转后(k = 4):arr = [3, 2, 1, 4]
第四次翻转后(k = 3):arr = [1, 2, 3, 4],此时已完成排序。
1 | // 煎饼排序——递归思路 |
字符串相乘
给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。
注意:不能使用任何内置的 BigInteger 库或直接将输入转换为整数。
输入: num1 = “123”, num2 = “456”
输出: “56088”
1 | string multiply(string num1, string num2) { |
接雨水
给定一个条形图,问该条形图能接多少水?
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
备忘录法
记录每个位置i
的能接的雨水=之前的最高柱子高度-当前柱子高度
1 | int ans = 0; |
双指针法
l_max
代表height[0...left]
的最高柱子,r_max
代表height[right...end]
的最高柱子,只要l_max < r_max
,就能以l_max
为主接雨水了,否则以r_max
为主接雨水。
1 |
|
交叉应用
BFS+DP
最小跳跃次数
为了给刷题的同学一些奖励,力扣团队引入了一个弹簧游戏机。游戏机由 N 个特殊弹簧排成一排,编号为 0 到 N-1。初始有一个小球在编号 0 的弹簧处。若小球在编号为 i 的弹簧处,通过按动弹簧,可以选择把小球向右弹射 jump[i] 的距离,或者向左弹射到任意左侧弹簧的位置。也就是说,在编号为 i 弹簧处按动弹簧,小球可以弹向 0 到 i-1 中任意弹簧或者 i+jump[i] 的弹簧(若 i+jump[i]>=N ,则表示小球弹出了机器)。小球位于编号 0 处的弹簧时不能再向左弹。
为了获得奖励,你需要将小球弹出机器。请求出最少需要按动多少次弹簧,可以将小球从编号 0 弹簧弹出整个机器,即向右越过编号 N-1 的弹簧。
示例 1:
输入:jump = [2, 5, 1, 1, 1, 1]
输出:3
解释:小 Z 最少需要按动 3 次弹簧,小球依次到达的顺序为 0 -> 2 -> 1 -> 6,最终小球弹出了机器。
限制:
1 <= jump.length <= 10^6
1 <= jump[i] <= 10000
1 | int minJump(vector<int>& nums) { |
排序算法
冒泡排序
1 |
|
快速排序
1 |
|
堆排序
1 |
|
简单选择排序
1 |
|
直接插入排序
1 |
|
希尔排序
1 |
|
归并排序
1 |
|
BM47 寻找第K大
有一个整数数组,请你根据快速排序的思路,找出数组中第 k 大的数。
给定一个整数数组 a ,同时给定它的大小n和要找的 k ,请返回第 k 大的数(包括重复的元素,不用去重),保证答案存在。
数据范围:$0≤n≤10^5, 1≤K≤n$,数组中每个元素满足$0≤val≤10^9$
输入:[1,3,5,2,2],5,3
返回值:2
1 | int partition(vector<int>& a, int low, int high, int K) { |
C++把输入转为字符串
1 |
|
C++把字符串处理成数组
1 |
|