文章目录
- 题目解析
- 滑动窗口
- 最长回文字符串(5)
- Z字形变换(6)
- 正则化匹配 (10)
- 盛水最多的容器(11)
- 整数转罗马数字(12)
- 三数之和(15)
- 最接近的三数之和(16)
- 电话号码的字母组合(17)
- 删除链表倒数第N个(19)
- 合并两个有序链表(21)
- 括号生成(22)
- 合并K个升序链表(23)
- 两两交换链表中的点(24)
- k个一组翻转链表(25)
- 删除数组的重复项(26)
- 两数相除(29)
- 串联所有单词的字符串(30)
- 下一个排列(31)
- 最长有效括号(32)
- 搜索旋转排列数组(33)
- 在排序数组中查找元素的第一个和最后一个位置(34)
- 有效的数独(36)
- 解数独(37)
- 组合数组(39)
- 组合数组2(40)
- 缺失的一个正数(41)
- 接雨水(42)
- 字符串相乘(43)
- 通配符匹配(44)
- 跳跃游戏II (45)
- 全排列(46)
- 全排列2
- 旋转图像 (48)
- 字母异位词分组(49)
- Power(x,n) (50)
- N皇后问题 (51、52)
- 最大子序和(53)
- 螺旋矩阵(54)
- 跳跃游戏(55)
- 合并区间 (56)
- 插入区间 (57)
- 螺旋矩阵2(59)
- 排列序列 (60)
- 旋转链表 (61)
- 不同路径(62)
- 不同路径 (63)
- 最小路径和(64)
- 二进制求和 (67)
- 文本左右对齐(68)
- x的平方根(69)
- 爬楼梯(70)
- 简化路径(71)
- 编辑距离 (72)
- 矩阵置零 (73)
- 搜索二维矩阵 (74)
- 颜色分类 (75)
- 自小覆盖子串(76)
- 组合(77)
- 子集 (78)
- 单词搜索 (79)
- 删除有序数组的重复项II (80)
- 搜索旋转排序数组II (81)
- 删除列表中的重复元素(82)
- 删除列表中的重复元素 (83)
- 柱状图中最大的矩形 (84)
- 最大矩形 (85)
- 分割链表(86)
- 扰乱字符串 (87)
- 格雷编码 (89)
- 子集II (90)
- 解码方法 (91)
- 翻转链表II (92)
- 复原IP地址 (93)
- 二叉树的中序遍历 (94)
- 不同的二叉搜索树II(95)
- 不同的二叉树 (96)
- 交错字符串 (97)
- 验证二叉搜索树 (98)
- 恢复二叉搜索树(99)
- 相同的树(100)
- 对称二叉树 (101)
- 二叉树的层次遍历 (102)
- 二叉树的锯齿形遍历 (103)
- 二叉树的最大深度 (104)
- 从前序与中序遍历序列构造二叉树(105)
- 从中序与后续遍历序列构造二叉树 (106)
- 二叉树的层次遍历 (107)
- 将有序数组转换为二叉搜索树 (108)
- 将有序链表转换为二叉搜索树 (109)
- 平衡二叉树 (110)
- 二叉树的最小深度 (111)
- 路径总和(112)
- 115. 不同的子序列
- 116. 填充每个节点的下一个右侧节点指针
- 117、填充每个节点的下一个右侧节点指针II
- 118、杨辉三角
- 119、杨辉三角II
- 120、 三角形最小路径和
- 121、 买卖股票的最佳时机
- 122、买卖股票的最佳时机II
- 123、买卖股票的最佳时机III
- 124、二叉树的最大路径和
- 125、验证回文串
- 126、单词接龙
- 127、单词接龙I
- 128、最长连续序列
- 129、求根节点到叶节点数字之和
- 130、 被围绕的区域
- 131、 分割回文串
- 132、分割回文串II
- 133、克隆图
- 134、加油站
- 135、分发糖果
- 136 、 只出现一次的数字
- 137、只出现一次的数字2
- 138、复制带随机指针的链表
- 139、 单词拆分
- 140 、单词拆分2
- 141、环形链表
- 142、环形链表2
- 143、重排链表
- 144、二叉树的前序遍历
- 145、二叉树的后续遍历
- 146、LRU缓存机制
- 147、对链表进行插入排序
- 148、排序链表
- 150、逆波兰数
- 151、 反转字符串里的单词
- 152、 乘积最大子数组
- 153、寻找旋转排序数组中的最小值
- 154、寻找旋转排序数组中的最小值
- 155、最小栈
- 160、相交链表
- 162、寻找峰值
- 164、最大间距
题目解析
滑动窗口
其实就是一个队列,比如例题中的 abcabcbb,进入这个队列(窗口)为 abc 满足题目要求,当再进入 a,队列变成了 abca,这时候不满足要求。所以,我们要移动这个队列!
如何移动?
我们只要把队列的左边的元素移出就行了,直到满足题目要求!
一直维持这样的队列,找出队列出现最长的长度时候,求出解
涉及题目:3
最长回文字符串(5)
- 蛮力法
首先最简单的做法就是暴力解法,通过二重循环确定子串的范围,然后判断子串是不是回文,最后返回最长的回文子串即可。 - 动态规划
定义函数f(i,j)表示区间在[i,j]内的字符串是不是回文串,其中i和j表示子串在s中的左右位置。
f ( i , j ) = f ( i + 1 , j − 1 ) f(i,j)=f(i+1,j-1) f(i,j)=f(i+1,j−1) 当 s i = s j s_i=s_j si=sj时
思考边界条件,当 s i = s j s_i=s_j si=sj并且j=i+1的时候,此时字符串由两个相同字符构成;当i=j的时候,此时字符串由单个字符构成。 - 中心向两边扩展
将每个字符串的的每个元素当做中心元素,来进行遍历找到最长的
找到回文串的中心,然后向两边扩展即可。这里的中心位置我们要奇偶分开考虑,如果字符串长度是奇数的话,中心就只有一个元素;如果字符串是偶数的话,那么中心是两个元素。
Z字形变换(6)
- 暴力法
构建二维数组,然后进行寻访,在进行遍历 - 按行访问
一维数组,只有在最上边或最下边的时候进行变化,进行标记,其余时间直接在相应行的字符串后边添加字符
正则化匹配 (10)
- 动态规划
- 最优子结构
dp[i][j]表示对长度为i的子串能否用长度为j的串来正则化匹配 - 转移方程
- p [ j ] ! = ∗ p[j]!=* p[j]!=∗时,$dp[i][j] = d[i-1][j-1]&&(s[i]p[j]||p[j]’.’
-
p
[
j
]
=
=
∗
p[j]==*
p[j]==∗时:
- dp[i][j] = dp[i-1][j]||dp[i][j-2] 当 s[i]==p[j-1] 它有重复0次或重复多次的可能,其中重复多次由递归可得:dp[i][j] = dp[i-1][j-2] 不能漏掉重复0次的可能
- dp[i][j] = dp[i][j-2] 当 s[i]!=p[j-1]
- 递归
参考链接:https://www.cnblogs.com/helloworldcode/p/11649837.html
盛水最多的容器(11)
- 双指针法
在初始时,左右指针分别指向数组的左右两端。移动数字较小的那个指针。
双指针代表的是 可以作为容器边界的所有位置的范围。在一开始,双指针指向数组的左右边界,表示 数组中所有的位置都可以作为容器的边界,因为我们还没有进行过任何尝试。在这之后,我们每次将 对应的数字较小的那个指针 往 另一个指针 的方向移动一个位置,就表示我们认为 这个指针不可能再作为容器的边界了。
整数转罗马数字(12)
- 贪心策略
将可以表示的数字的按从大到小的顺序排列,每次选择最大的数字看是否可以进行的匹配。
三数之和(15)
- 双指针解法
首先这个大小相关的数组,首先将列表进行排序list.sort
遍历第一个数,寻找另外两个数之和等于这个数的相反数。
在寻找另外两个数之和等于这个的数的相反数的过程中使用双指针法:- 当两数之和大于tmp时,挪动右边的指针的一位(因为右边的指针指的数较大),看能否相等。
- 当两数之和小于tmp时,挪动左边的指针的一位(因为左边的指针指的数较大),看能否相等。
最接近的三数之和(16)
- 双指针解法
首先这个大小相关的数组,首先将列表进行排序list.sort (遇到可以排序的数组,且原始数组的位置不提供信息,直接进行排序)
遍历第一个数,寻找另外两个数加这个数之和最接近目标数
在寻找另外两个数过程中使三数之和最接近目标的数的过程中使用双指针法:- 当三数之和大于target时,挪动右边的指针的一位(因为右边的指针指的数较大),看能否更接近
- 当三数之和小于target时,挪动左边的指针的一位(因为左边的指针指的数较大),看能否更接近
电话号码的字母组合(17)
首先使用哈希表存储每个数字对应的所有可能的字母,然后进行回溯操作。
回溯过程中维护一个字符串,表示已有的字母排列(如果未遍历完电话号码的所有数字,则已有的字母排列是不完整的)。该字符串初始为空。每次取电话号码的一位数字,从哈希表中获得该数字对应的所有可能的字母,并将其中的一个字母插入到已有的字母排列后面,然后继续处理电话号码的后一位数字,直到处理完电话号码中的所有数字,即得到一个完整的字母排列。然后进行回退操作,遍历其余的字母排列。
删除链表倒数第N个(19)
1.栈
建立一个prev保留为头结点,最后方便返回,将链表进栈,然后依次出栈,出到第n次便找到自己要找的节点了
2.双指针法
使用一前一后两个指针,两个指针相差n个节点,当后指针到末尾时,前指针便是要删除节点的位置
3.统计长度法
先遍历一遍统计长度,得到正向的位置,然后删除即可。
合并两个有序链表(21)
使用递归法,将其划分为比较两个节点的值,比较只有,选择较小作为最新的节点,再调用自身来进行比较,
结束状态为有一个列表为空
括号生成(22)
1.回溯法
将这个问题当做一个向长度为2N的列表中添加字符串,维护一个列表S,当列表S长为2N的时候,将该列表拼接成字符串添加到ans中
往每个位置添加字符串的情况有两种:添加 ‘(’ 或 ‘)’,遍历完一种情况,进行出栈来计算另一种情况。
其中每次都要检查进栈的字符串是否合法,即初始化bal = 0,遍历字符串列表,若出现bal<0的不符合,直接返回。
2.回溯法优化
将这个问题当做一个向长度为2N的列表中添加字符串,维护一个列表S,当列表S长为2N的时候,将该列表拼接成字符串添加到ans中
往每个位置添加字符串的情况有两种:添加 ‘(’ 或 ‘)’。
优化的部分是:left(左括号数)小于n,便可以添加;right(右括号数)小于左括号数才可以添加。
合并K个升序链表(23)
1.递归+分治
首先使用递归法,每次的比较两个节点值,选择值小的节点,该节点的next调用的递归函数,其中一个节点改为的该节点的下一个传入进去。
分治法:每次两两合并链表,存到新的list中,如果为奇数,将最后一个直接放入进去,最终为长度为1表示合并成一个链表了。
两两交换链表中的点(24)
- 迭代法
首先建立头节点,进行遍历,每次都保留的是要比较的两个节点的前一个节点,当只有一个节点或没有节点时结束遍历。
在遍历的过程中,将前一个节点的next指向第二个节点,第一个节点的next指向第二个节点next,第二个节点的next指向第一个节点。
注意:这个问题中的关键的就是每次进行遍历都是要保存要交换的两个节点的前一个节点 - 递归法
递归方程:
- 结束条件:
节点为空或只有一个节点 - 递归情况:
将传入节点的第二节点作为新的头结点,将第一个节点的next指向这两个节点的后面的节点调用递归函数,第二个节点
的next指向第一个节点,返回第一个节点。
k个一组翻转链表(25)
- 递归+栈
使用递归的方法进行返回,以k个为一组,若从第一节点开始不足k个,直接返回第一个节点。若满足k个,将这个k个节点依次入栈,出栈第一个作为新的头结点,保留这个节点的下一个节点作为新的递归的传入的节点。每次出栈一个,将之前的出栈的节点p的next指向刚出栈的节点,将p作为之前出栈的节点,遍历完成。将最后的节点调用递归函数。
删除数组的重复项(26)
- 快慢指针
快的指针指向检查是否重复的项,慢的指针指向要未重复的项添加的位置。 该方法可以实现在原数组的基础上,不创建新数组空间来去除重复项。
两数相除(29)
- 快乘+递归
首先讨论被除数为0,除数为1或为-1对应的特殊情况所对应的结果。检查是否越界
将两数变为正数,使用快乘法:每次的使用的遍历,每次遍历将除数翻倍,count数也翻倍,直到最后结果大于等于,将数减去最大的,再次递归调用,
检查是否越界。
串联所有单词的字符串(30)
- 滑动窗口+ 哈希表
哈希表的应用:主要是使得不用在意字符串的位置,并且可以统计字符串出现的次数
新建一个哈希表,用来存储words,key为字符串的值,value为字符串出现的次数,建立一个空的哈希表cur_hash,每次将里面的字符串的数目加一。
使用滑动窗口:起始位置从one_word长度的进行遍历,因为每个字符串的长度相同,每个位置每次增加的一个word长度,直到遍历完整个字符串的长度,
建立left,right指向窗口的左右,每次右边窗口加一个word 长度,即添加一个word,这个word如果不在哈希表中,直接left和right同时放到遍历的最右侧,
并同时清空当前的哈希表,如果字符串哈希表中,将该字符串的哈希表数目加一,如果count>初始哈希表时,移动left,之至将之前相同的word移出,最终
cnt与words的数目相同时,left为一个所需的结果。
下一个排列(31)
- 两遍扫描+双指针翻转区间
首先使用两遍扫描找到需要交换的两个数:第一层指针i从右边开始扫描,结束为到最左边。该指针指向的是需要交换的第一个位置,第二个指j从最右边开始扫描到i结束,这个指针要找需要交换的第二个位置,要交换的条件是:如果右边指针的数比左边指针的数大,则交换两个位置的数。交换完成之后,此时左边的区间的数为降序区间,使用双指针翻转区间。
对于有序区间使用双指针翻转区间:第一个指针指向列表的开头,第二个指针指向列表的末尾,每次的交换两个指针指向的数,然后两个指针分别往中间位置挪动一位,结束条件是:右边的指针的位置不再大于左边指针的位置。
最长有效括号(32)
-
动态规划
DP转移方程:
(1) 当s[i]’(‘时,一定不是结束的状态的,对最终dp结果无影响,后面的dp不会用到’('的dp的值
(2) 当s[i]’)‘时:
(1) dp[i] = dp[i-2]+2 ---------当s[i-1]’(’
(2) dp[i] = dp[i-1]+dp[i-dp[i-1]-2]+2 ------ 当s[i-1]’)’且s[i-dp[i-1]-1]==’(’
边界情况,每个转移情况都要检查是否越界,如果越界,并对越界的情况进行处理 -
栈
栈中最顶部的位置的存储的为上一未匹配的右括号的位置,因此在栈中首先要加入-1,表示上一个未匹配右括号位置为-1
当s[i] == ‘(’, 将其进栈
当s[i] == ‘)’,进行出栈,如果栈为空:说明出栈的不是’(‘的位置,将该’('的位置进栈,如果不为空,将该右括号的位置减去栈中顶部位置为长度。 -
贪心策略
在此方法中,我们利用两个计数器 left 和right 。首先,我们从左到右遍历字符串,对于遇到的每个‘(’,我们增加left 计数器,对于遇到的每个 ‘)’ ,我们增加right 计数器。每当left 计数器right 计数器相等时,我们计算当前有效字符串的长度,并且记录目前为止找到的最长子字符串。当 right 计数器比eft 计数器大时,我们将 left 和right 计数器同时变回0。这样的做法贪心地考虑了以当前字符下标结尾的有效括号长度,每次当右括号数量多于左括号数量的时候之前的字符我们都扔掉不再考虑,重新从下一个字符开始计算,但这样会漏掉一种情况,就是遍历的时候左括号的数量始终大于右括号的数量,即 (() ,这种时候最长有效括号是求不出来的。
解决的方法也很简单,我们只需要从右往左遍历用类似的方法计算即可,只是这个时候判断条件反了过来:当 left 计数器比right 计数器大时,我们将left 和right 计数器同时变回 0
当left 计数器right 计数器相等时,我们计算当前有效字符串的长度,并且记录目前为止找到的最长子字符串
搜索旋转排列数组(33)
- 暴力法
首先根据旋转排列数组的性质,数组的第一个元素为有序的:
如果查找的目标元素比数组的第一个元素小,那么从最后的元素的开始进行向前进行遍历,结束条件:(1)当前元素如果为目标元素,则结束遍历,返回结果位置。(2)如果当前的元素的值比前一个元素的值小,说明已经到了翻转的位置,则结束遍历,返回没有找到目标元素。
如果查找的目标元素比数组的第一个元素大,那么从第一个元素的开始遍历。结束条件:(1)当前元素如果为目标元素,则结束遍历,返回结果位置。(2)如果当前的元素的值比后一个元素的值小,说明已经到了翻转的位置,则结束遍历,返回没有找到目标元素。 - 二分查找
二分查找是针对的有序数组的进行查找的方法,每次使用的是left和right指针,由这两个指针来求出中间指针的位置。检查位置的元素是否为自己所需要的目标的元素,如果是则返回位置。检查中间位置的两侧,根据题目分析,至少有一侧有序的。从有序的这一侧对数据进行检查, 因为这一侧的是有序的,所以所以只需检查目标元素是否在这两端的元素之间。如果在则取这一端,否则取另一端,即将left或right改为mid+1或mid-1.
注意:(1)循环的条件为left<=right,注意等号,当等号成立的时候,mid就是检查两个指针共同指向的元素。结束的条件为right>left。
(2)每次mid为(left+right+1)//2 或(left+right)//2的效果是一样的不用进行考虑。
在排序数组中查找元素的第一个和最后一个位置(34)
- 二分查找
使用两次二分查找,每次查找一个位置,如果查找第一个出现的位置,每次比较中间的位置,然后根据其来选择区间。
即:查找元素最右侧边界的下一个位置,即每次比较mid和target,如果target 小于mid,说明mid比target大,此时该mid位置符合条件,将right改为mid+1,继续搜寻下一个符合条件的位置,遍历结束后找到最符合的位置,然后减一为右边界位置。
查找的第一个出现的位置,在比较的时候的加上的等号,来进行等于的扩充。
当找到两个位置之后,检查两个位置的是否越界,两个位置的上的值是否为目标值,两个位置值的是否的符合顺序。
有效的数独(36)
- 数组法
建立三个二位数组([[] for _in range(9)],分别用来行、列、3*3box中的值,其中每个一维数组代表一行、一列、一组,其中box的值可以使用((index//3)*3+column//3)来求出。遍历所有元素,如果在所在行的数组、所在列的数组、所在box的数组都不存在,则将该元素加入到的相应数组的位置中,否则返回false
解数独(37)
1.递归+回溯+DFS
首先将缺少数字的位置加入到数组中,将每个数字的行、列、box中对应的值进行设置为False表示该行、列、box没有该值。
设置一个valid值,使用nolocal声明,在函数中共享,当dfs中的值与empty的数组的相等时说明在所有的位置都以填入了合适的数字,设置valid为True是结束状态;针对每个空白位置,进行1-9的数字遍历,即分别填入空白位置中(将行、列、box该位置的值更新为True,更新board),进行dfs下一个位置的递归,下一个位置的不成功,将行、列、box该位置的值更新为False,进行下一个数字的尝试。dfs结束条件:valid为True时,停止遍历直接return,遍历完之后没有成功的话进行return,使上一状态重新选择值。
组合数组(39)
1.递归+回溯+剪枝
使用递归加回溯的方法,首先对数组的进行从大到下的排序,遍历所有的候选数,(1)如果数字比target大结束遍历,因为后面的数也比target大(2)如果数字等于target,将该数加入答案ret,并结束遍历,将ret进行返回。(3)如果数字小于target,进行递归的调用,传入的数组和(target-该数)。剪枝操作:传入的数组从该数的位置往后数组,即不需要在考虑前面的数组的中的数,因为前面的数组中的数一定已经在前面遍历的递归调用时使用过了。,将递归返回的答案进行遍历,里面的每个数组加入该数,组成解加入ret中。遍历结果返回ret,结束递归。
组合数组2(40)
- 递归+回溯+剪枝
使用递归加回溯的方法,首先对数组的进行从大到下的排序,遍历所有的候选数,(1)如果数字比target大结束遍历,因为后面的数也比target大(2)如果数字等于target,将该数加入答案ret,并结束遍历,将ret进行返回。(3)如果数字小于target,进行递归的调用,传入的数组和(target-该数)。剪枝操作:加入判断(if(index>begin and candidates[index]==candidates[index-1]):continue。即在这一层不让重复的元素进行递归,只让第一次出现的元素进行递归。出入的数组改为从目前数的位置的下一个。)。,将递归返回的答案进行遍历,里面的每个数组加入该数,组成解加入ret中。遍历结果返回ret,结束递归。 - 递归+回溯+哈希表+剪枝
使用哈希表计算个元素的出现的次数,并将hash表根据出现次数的大小进行排序,这样就避免重复元素出现的重复递归的问题。
缺失的一个正数(41)
-
哈希表改造法
在这类需要保持在原数组的上的操作,额外的空间需求为O(1),将原数组的改变成的Hash表。
在此题中,首先进行一遍遍历,将数组的中小于等于的数的值改变为的大于这个数组长度的值,即一定不可能在数组的中的有位置的值。此时列表中没有负数了,可以使用负数作为标记位:即遍历数组,如果一个数n = abs(nums[i])在1到nums_len之间,将n所在的位置(nums[n-1])的数变为负数,表示该位置的数在数组中存在,然后遍历数组,第一个不为负数的位置+1,为我们所要的结果,如果都存在,结果为长度下一位数。 -
置换法
遍历数组,每个位置的数使用一个while循环,条件为:1<=nums[i]<=len(nums) and nums[i]!=nums[nums[i-1]],注:将每个数组存在正确位置的数组的数放到其正确的位置上,其循环结束后面关键条件!!:如果已经在nums[i]的数已经在正确位置上了要结束循环,如果这个位置的数和要交换的位置上的数相同,也要结束循环,这样可以避免死循环,然后遍历数组,找到不合适位置的数i+1!=nums[i],如果都存在,结果为长度的下一位。
接雨水(42)
- 暴力法
关键思想:计算每个位置水的高度,对每个位置进行遍历,每个位置的水的的高度为(它左边最大高度和右边最大高度)中较小的值减去它的高度。
关键:是要想到对每个点进行位置的分析。 - 暴力法的动态规划改进
使用动态规划的方法进行改进,使用两个数组每别来记录改为最大的left和right。以left数组为例,当前位置的最大的left高度为(前一个位置的left的最大的高度和此位置的高度中较大的一个),left数组从左往右开始,right数组从右往左开始。 - 使用栈的方法
注:该方法使用方法不是一个一个的点求,求的一个最长格的水的行量,每次遍历,使用while循环检查【当栈不为空的时候,如果当前元素比栈顶的元素的值小(下界由当前元素决定),不进入循环】,循环检查,将栈顶元素弹出(此为水的底部),如果栈空,跳出循环,否则由(由当前元素的高度和栈顶元素的高度)中较小的元素的高度减去出了栈的元素的高度,长度为当前元素的位置减去栈顶元素的位置减1。循环结束后,将当前元素进栈。 - 快慢指针的方法
已知当前位置水的高度为(左边最大高度和有点的最大高度中较小的来减去当前元素的高度决定),而从左和从右使用双指针left和right之后,使用maxleft和maxright表示左边的最好的高度和右边的最大高度,已left指针为例,left的元素的左边的maxleft是确定的,如果maxleft小于右边的maxright,则该位置的水的高度就是确定的了。遍历元素,首先比较maxleft和maxright,如果的maxleft小,对left指针所指的元素和maxleft进行比较,如果该元素小于maxleft,计算水高,否则maxleft的值等于left所指元素的值,移动left指针。
字符串相乘(43)
- 暴力模拟法
将两个乘数按照竖式计算的规则进行模型,如num1,num2,遍历num2,每次使用num2的一位乘以num1的中的每一位,将答案存储为一个str字符串,注意:在此过程中,字符串应该由大到小进行遍历(range(n-1,-1,-1),实现的从位数的低位到高位逐渐相乘,其中每次对10取余得到得到想应位置的数,对结果//10得进位的数,遍历完毕,如果进位的数不为0,则向字符串最前面加上该数,得到的结果字符串后,根据乘数的位置,决定在字符串后面添加0的个数,得到的最终的字符串,然后对所有的字符串俩俩相加,其中的加法的模拟是,从每个数的低位(即数组的高位n,每次n-1),进行计算,循环条件(n>0 or m>0 or add>0),每次两数相加再加上进位上的数,得到的结果取余放到相应位置,//10 得到进位。 - 数组模拟法
m位数乘以n位数的结果的范围应该为m+n-1<=x<=m+n位数之间,具体自己证明(最大最小),设置的m+n位的数组,初始化每位的元素为’0’,将两个乘数模拟相乘,每次得到的结果应该存储到(i+j+1)的位置上。
然后从后往前对数组进行处理,每次为进位的数加上该位置的数,结果对10取余为该位置应该存储的数,进位的数为//10之后所得的结果。
通配符匹配(44)
- 动态规划
- 注:针对两个字符串进行匹配的问题,即相似与正则化的实现的问题蛮实用动态规划。
- 状态转移方程:
- dp[i][j] = dp[i-1][j-1] ----------当s[i]==p[j]||p[j] ==’?’
- dp[i][j] = dp[i-1][j]||dp[i][j-1] --------- 当p[j]==’*'时:*匹配0个字符时的情况:dp[i][j-1],*匹配多字符的情况:dp[i-1][j]。
- 边界条件
- dp[0][0]=True 都是空字符串为0
- dp[i][0]= False s不为空,p为空
- dp[0][j] 此初始情况的赋值需要进行讨论,若在j位置,p从j开始前面字符全部都是*,则赋值为True,否则则为false,针对s=’’,p = '****'的情况。
跳跃游戏II (45)
- 贪心策略
局部最优策略:在一次能走的步子中,从这之中的区间选出能到达下一步最大距离的位置作为下一步应该走的位置。
从位置1到n-1进行遍历(最后一个位置不需要检查),每次位置更新最大的距离,
当遍历的i=上一步能到达的最大距离(end),更新最大end,step步数加一。
全排列(46)
-
贪心策略
局部最优,从数组列表为1个数排列为1,当每次加入一个数,将之前个数的数的排列组合拿出来,然后在每个组合的不同位置插入新的元素,形成新的排列加入列表。 -
回溯算法
相当于一共有n个坑,每个坑要填入不同的值,使用回溯办法进行排列,没次在一个位置填入一个数之后,使用递归的方法,在下一个位置从剩余的数(将填入的数与数组中该位置的数组的交互,那么该位置后面的数全部是未填入的数。)中选择要填入的数,继续下一个位置的递归。结束条件为所有位置的都填满,将该排列加入答案,然后return。结束递归后,进行回溯,将排列中的数进行出栈,数组交互位置的数再交互换来,在下一个数填入该位置,开始新一轮的遍历。
全排列2
-
贪心策略
与上面相同,最后加上去重,或者在加入时就判断是否重复来考虑是否加入。 -
回溯+剪枝
在进行遍历时,判断元素是否在这一层已经加入过,如果加入过,不再调用递归。即重复的元素只能出现在数的不同层,不能在数的同一层出现。
旋转图像 (48)
- 模拟法
首先观察规律,图像得旋转为4个位置的循环,这四个位置循环的规律是:每次要旋转到的行值i为上一个要旋转位置的列值j(i = j),其中列的位置的为上一个位置行值i计算(j = n-i-1)
确定要循环的边界:由外圈逐步往里圈。
字母异位词分组(49)
- 排序法
遍历列表中的字符串,将字符串进行排序,排序后的字符串作为hash表的键,如果存在,再添加到hash表对应键的列表中,如果不存在,则在新的进行添加。 - 计数法
字符串最多用26个字母,使用一个列表来统计字符串中字母出现的次数,将该次数出现组成元组作为hash表的键,判断是否是字母异位词。
Power(x,n) (50)
- 暴力法
考虑n=0,x=1,-1的特殊条件,然后是暴力法一个一个进行相乘进行计算。 - 快乘法
首先考虑特殊条件,可以加快速度。使用快乘法,每次传递进入所需乘数和幂的次数,每次递归都计算能计算的该幂次数下的最大值,然后将剩余的幂次数放入下一次的递归中,返回3. 幂乘法
每次递归计算的是该幂次数//2的结果,幂返回的便是该幂下的一半的结果,然后判断该幂是否是2的整数倍,如果是2的整数倍,返回的是y*y(其中y的该幂一半的结果),如果不是整数倍,返回的是y*y*x。结束条件n =0.的是ret+下一次幂次数的递归结果,递归条件,如果n=0,返回1.0.
N皇后问题 (51、52)
- 深度优先搜索+回溯
采用回溯的方法,维护两个表,ret为已经加入表的格子,rol为已经加入的列(这些列不可再使用),按行进行回溯,这样行便不需再检查,每次函数中,遍历该行中的每一列的元素,检查的是否符合(元素的列不再的col中,元素的左上角对角线(j-1,k-1)不含ret元素,元素的右上角对角线(j-1,k+1)中不包含的ret的元素),将该元素加入ret,所在列加入col,将元素的下一行进行递归。递归结束,将加入的ret和col弹出。结束条件(row==n),行已经遍历完。
最大子序和(53)
- 动态规划
状态转移方程: f ( i ) = m a x ( f ( i − i ) + n u m s [ i ] , n u m s [ i ] ) f(i) = max(f(i-i)+nums[i],nums[i]) f(i)=max(f(i−i)+nums[i],nums[i])
其中 f ( i ) f(i) f(i) 为i为结尾的最大子序和。max_ret 为最终返回结果,每次 m a x r e t = m a x ( m a x r e t , f ( i ) ) max_ret = max(max_ret,f(i)) maxret=max(maxret,f(i))
理解:需要单独维护一个f(i),f(i)为以i为结尾的最大子序和,所以每遍历到i只要考虑状态状态转移方程即可。
螺旋矩阵(54)
-
模拟法+矩阵标记
矩阵标记:每次走过的位置的元素,将该位置的元素的值设置为一个特殊值,表示该元素已经走过,如果下一个位置为走过的位置则改变方向。首先建立移动方向的数组,direct = [[0,1],[1,0],[0,-1],[-1,0]],每次数组的下一个矩阵元素的位置为当前的位置加上的对应的direct的值,如果下一个位置符合条件,将该元素加入答案数组,并修改为特殊值,标志已经走过。否则改变方向(i = (i+1)%4),其中i表示的是direct矩阵中的方向。
跳跃游戏(55)
- 贪心算法
每次选择可以跳跃的最大位置,遍历所有的跳跃位置,每次遍历一个位置更新能到达的最右侧的位置,若当能到达的最右侧的位置大于等于n-1时,则说明能达到最终位置。
合并区间 (56)
- 排序+贪心
将数组按照序列的第一个元素进行从小到大的排序,每次比较区间,若新元素的起始位置小于等于加入元素的结束位置,合并两个元素的位置,否则将加入元素加入到ret中,将新元素作为新的加入元素。
插入区间 (57)
- 模拟法
将前面加入,合并与new交集的区间,跳出循环,将new加入进去,再将后续数组中的元素加入
螺旋矩阵2(59)
- 模拟法
建立方向列表,其中方向列表中的元素为二元元组,构建原始矩阵赋值-1,作为未遍历的标志,注:如果一个位置不对,不用循环,它的正确位置的就是当前位置加上下一个方向。
排列序列 (60)
- 暴力回溯+剪枝
使用回溯的方法,每次使用pop(n)弹出指定位置的元素,回溯后将其加入原来的位置,每次递归维护一个ret的列表。
剪枝操作:当获取到第k个排列,返回1,检查的递归的返回为1,停止迭代,直接返回。否则继续迭代。
旋转链表 (61)
- 循环链表
将指针指向链表的尾部,并尾部节点的next指向头部元素,构建成循环链表。同时统计链表的长度n,要移动的位置的数为n-(k%n) ,从尾部移动相应的个数,将指针的下一个位置节点作为头节点,并断开循环链表,即:使尾部节点指向None
不同路径(62)
- 动态规划
状态转移方程:dp[i][j] = dp[i-1][j] +dp[i][j-1] ,即每一次到达的位置的路径数为上下可到达的路径数之和。注意初始化状态为dp[1][1] = 1,dp[][0] = 0 ,dp[0][] = 0。
不同路径 (63)
- 动态规划
状态转移方程:dp[i][j] = dp[i-1][j] +dp[i][j-1] ,有障碍物时:dp[i][j] = 0 .即每一次到达的位置的路径数为上下可到达的路径数之和。注意初始化状态为dp[1][1] = 1,dp[][0] = 0 ,dp[0][] = 0。
注意:初始位置为1,有障碍物返回0
最小路径和(64)
- 动态规划
此类题只存在向右和向下两种状况,其中向下的结果可以由第一层遍历得到,向右的可以由上一次得到。
状态转移方程:dp[i][j] = min(dp[i-1][j],dp[i][j-1])+grid[i-1][j-1]所得。
边界条件:dp[][0] 和 dp[0][] 为inf ,其中dp[1][1]为初始化值。
二进制求和 (67)
- 模拟法
使用加法进行模拟二进制求和。 - 转换法
先转为十进制 >> 十进制相加 >> 转二进制
使用int(x,b)将字符串转换为整数,然后使用{0:b}进行二进制输出。 - 位运算
首先result存储两个数按位亦或的值(相当于两数不进位的数的和),然后carry存储两数与&的值并左移一位,此时的值相当于两数的进位值。然后继续两数亦或,相当于结果与进位数不进位的和,然后&并且右移一位得到两数的进位值。结束条件carry进位值为0;
文本左右对齐(68)
- 模拟法
分情况,如果加入单词小于目标长度,temp+=单词+’ ';
如果等于时,加入单词,并将字符串加入到ret列表中,并将temp初始化为0;
如果大于时,如果只有一个字符串,如果字符串的长度比maxwidth大时,将字符串阶段,如果小于,补全的空格。否则,根据里面单词的个数,计算字符串间最小的长度。squaze = maxwitdh//count,取余得到前面一个空隙要额外加一个空格,从列表加入单词和响应空格长度(取余的值自减,来判断是否要加额外空格),最后一个单词不用加空隙。
最后单词不足时,判断是否需要补全空格或者截断字符串。
x的平方根(69)
- 二分查找
设置边界条件,首先将边界设置为初始边界,然后每次二分查找更新,当满足平方不大于时,更新ans。继续查找,直到不满足left<=right
爬楼梯(70)
- 动态规划+斐波那契数
在到达每个阶层的楼梯之前,迈步的选择有两种,由一阶或两阶迈步到达。使用f(i)表示到达i层的方法数,初试化:f(1) = 1,f(2)=2。状态转移方程:f(i) = f(i-1)+f(i-2)
简化路径(71)
- 栈的方法
每次以’/‘为结尾来划分字符串,如果字符串为’…‘且栈不为空,进行出栈。如果是’.‘或’’,进行遍历下一个字符串,其他情况将字符串进栈。如果初始的字符串的结尾不是’/’,手动添加一个。
使用string的split来按’/‘进行字符串的划分,如果字符串为’…‘且栈不为空则弹栈,将其他符合字符串进栈,最后使用‘/’.join(list)获取最终答案。
编辑距离 (72)
- 动态规划
最优子结构与转状态转移方程的获取
对于dp[i][j] 的获得,当word1的i删除一个字符串获得:dp[i-1][j]+1,通过word1添加一个字符获得:dp[i][j-1]+1。通过替换两个word的最后一个字符,如果两个相同dp[i-1][j-1],不同为dp[i-1][j-1]+1。
边界条件:dp[i][0] = i ,dp[0][j] = j
矩阵置零 (73)
- 标记矩阵
使用第一列和第一行作为标记位置,如果第一行中的第j列元素为0,表示这一列的元素全部标为0,对于第一行和第一列的元素,使用index和col来标记是否第一行和第一列标记为0。使用遍历第一行和第一列,来将index和col元素做好标记。 然后遍历矩阵,如果元素值为0,将其对应的第一行该列的值设为0,第一列该行的值设为0。然后遍历第一行(注意列数从1开始,不要从0开始,否则列会被重置掉),将值为0对应的列改为0、遍历第一列(行数从1开始),将对应的行改为0,最后根据index,col修改第一行和第一列。
搜索二维矩阵 (74)
- 二分查找
首先针对第一列进行二分查找,找出不大于目标元素的行值,然后针对这一列的元素进行二分查找,找出该元素是否存在。
颜色分类 (75)
- 快慢指针
使用left和right表示最后和最前的0,2的左右位置,遍历i,如果i对应的元素的值为0,与left交换,i+1(因为交换的i值一定是已经比较过的元素了),对应的元素值为2,与right交换,i不变(因为交换的right对应的元素是还没有比较过的元素)。
自小覆盖子串(76)
- 滑动窗口+hash表
- 首先对要覆盖的子串建立hash表,其中hash表的键值为该字符出现的次数。建立滑动窗口:left、right,将right进行遍历,如果是要覆盖的字符串,将该字符串的对应的hash值减一。
- 如果hash值为0,检查hash表中的所有的键值是否全部小于0,如果全部小于0,该子串的为一个覆盖子串,检查长度,如果长度比ret,将ret改为该子串长度。将left指针的hash值加一,左指针加一。不断移动left指针,循环条件(left<right)and(字符串不是子串中的元素或者该位置元素对应的子串的元素的hash值小于0(该情况要在对应的hash值加一))。
- 如果hash值小于0,移动left指针。循环条件(left<right)and(字符串不是子串中的元素或者该位置元素对应的子串的元素的hash值小于0(该情况要在对应的hash值加一))。
组合(77)
- 回溯+深度优先搜索
- 结束条件(startn)元素遍历完毕return,k0,元素添加完成,将该组合加入答案,并return。
- 遍历start位置到结束:每次ret加入一个元素,位置为遍历的元素的位置的下一个位置,k减一。调用递归(start+1),然后元素弹栈,k+1。进行下一次的遍历。
子集 (78)
- 回溯+深度优先搜索
- 结束条件(startn)元素遍历完毕return,k0,元素添加完成,将该组合加入答案,并return。
- 遍历start位置到结束:每次ret加入一个元素,位置为遍历的元素的位置的下一个位置,k减一。调用递归(start+1),然后元素弹栈,k+1。进行下一次的遍历。
- 初始调用条件(元素的个数是从0到n,即range(n+1).
单词搜索 (79)
- 递-归+深度优先搜索
- 结束条件(w==k) 单词已经全部匹配,返回值为True;进行该位置上下左右移动,全部失败返回false或找到单词返回True。
- 递归方程:建立一个标志数组,表示是否走过该位置。对目前的数组的位置进行上、下、左、右的移动,如果目标位置没有的越界and目标位置为目标单词的值、目标位置未被访问过:将目标位置标志设置为走过,对该目标位置递归,递归的返回值为True时,表示已经找到了单词,直接return True。若为false,进行回溯,将改为位置设置为未走过,遍历一下个目标位置。
- 起始:遍历矩阵中的所有位置,如果该位置为时单词的起始字母,进行递归(该位置设置为走过,结束递归时判断是否成功,未成功要将改位置设置为未走过,在进行下一个位置的遍历)。
删除有序数组的重复项II (80)
- 快慢指针
- 使用快慢指针,慢指针left指向有效元素的最后一个位置,快指针right指向要比较的元素。
- left初始化0,right初始化1,对right进行遍历,加入flag标志元素在前面是否出现过,如果left和right指向的元素的值不相同,将left加一并赋值为right的值;如果相同,判断flag:如果为True,是第一次出现,将其flag置为false,并left+1并赋值为right的值。如果flag为false,只移动right。
搜索旋转排序数组II (81)
- 二分查找
- 使用二分查找,left,right指针,循环条件:left<=right,首先判断mid是否等于目标元素,如果为目标元素返回True。
- 判断left<mid:在left-mid区间是有序的,判断left<=target<mid区间,在这个区间中将right改为mid-1,否则left变为mid+1
- 判断right>mid:在mid-right区间有序,判断mid<target<=right区间,在这个区间中left变为mid+1,否则right变为mid-1
- 判断left的值是否为target,如果等于返回True,否则left+=1.
删除列表中的重复元素(82)
- 链表
- 建立prev节点,建立两个指针p(已获取的未重复的元素的最后一个位置),q要检查的元素的位置,循环条件(q!=None)
- 如果q.next!=None and q.val==q.next.val ,使用q所有的重复元素略过,循环条件q!=None and q.val = temp
- 否则,p.next = q ,p = q,q = q.next
- 最后 p.next = q
删除列表中的重复元素 (83)
- 链表
- 建立prev节点,p指针指向head,循环条件:p!=None
- 如果 p.next!=None and p.next.val == p.val , 过滤元素:p.next = p.next.next
- 否则 p = p.next
柱状图中最大的矩形 (84)
- 单调栈
- 解题思路:遍历元素,获取每个高度可以左右扩展的最大长度。 获取最大长度(左边的第一个低于该元素的高度元素位置,右边第一个低于该元素的位置)
- 使用单调栈,每次进入元素,将所有高于该元素位置的元素弹出,栈为空时,左边列表添加-1,否则添加栈中最后一个元素的位置。然后将该元素位置如果。获取左边位置列表,然后反向遍历,获取右边元素位置的列表。
最大矩形 (85)
- 暴力法
- 使用84题相同的思路,首先建立一个二维矩阵,用来保存元素左边的最大长度。然后进行遍历,对于每个元素求其位置及上方的最大矩形的面积。剪枝:当上方的元素的长度为0时,停止向上寻找。
- 单调栈
- 类似于84,对于每行,使用单调栈,求出高度,然后套用84题的方法。
分割链表(86)
- 模拟法
- 使用right指针进行遍历,使用left指针指向插入较小元素的位置。
- 注:BUG:首先要将left指针遍历到大于目标元素的前一个位置,令right指针初始化指向该元素,否则会造成闭环,从而产生BUG
扰乱字符串 (87)
- 递归
- 递归算法:传入参数i,j,l分别s1的起始位置,s2的起始位置,字符串长度,如果s1[i:i+l]== s2[j:j+l]返回True,如果Counter(s1)不等于Counter(s2)返回False。否则遍历从1到l-1,每个作为字符串的划分点,检查不改变顺序分割的字符串是否符合,符合返回True。检验改变顺序的字符串是否符合,注意:两个字符串长度的匹配(dfs(i,j+k,l-k)和dfs(i+l-k,j,k))的情况,如果遍历结束返回False。
格雷编码 (89)
- 递推方法
- 设 nn 阶格雷码集合为 G(n)G(n),则 G(n+1)G(n+1) 阶格雷码为:
- 给 G(n) 阶格雷码每个元素二进制形式前面添加 0,得到 G’(n);
- 设 G(n) 集合倒序(镜像)为 R(n),给 R(n) 每个元素二进制形式前面添加 1,得到 R’(n)
- G(n+1) = G’(n) ∪ R’(n)G(n+1)=G’(n)∪R ′(n) 拼接两个集合即可得到下一阶格雷码。
- 总结思路:就由上一层得到前面加0得到前半部分,拼接前面加1的上一层逆序作为后半部分
- 注:前面加0不需要操作,前面加一相当于上一层的数都加上1<<i位的数
- 设 nn 阶格雷码集合为 G(n)G(n),则 G(n+1)G(n+1) 阶格雷码为:
子集II (90)
- 递归和深度优先搜索
- 方法思路:使用递归,注:首先对数组的进行排序,避免重复,维护一个数组,递归每次传入一个遍历数组的位置start,结束条件:位置为n。对该位置的进行遍历,剪枝:若i为start位置或者nums[i]!=nums[i-1]时,往每次往维护的数组中添加元素,调用递归,传入下一个位置,结束后,进行回溯,弹出元素pop。
- 注:关键要进行数组排序,进行剪枝,使得i==start or nums[i]!=nums[i-1]时,进行添加,这样使得相同元素不会在树的同一层出现,从而实现了剪枝的效果。
解码方法 (91)
- 动态规划
- 解码的种数有两种到达状态,前面的解析当个字符串或前面的解析两个字符串两种状态。
- 状态转移方程:
- dp[i] = dp[i-2] ------------ s[i]==0 and s[i-1:i+1]<=26
- dp[i] = dp[i-1] ------------if(int(s[i-1:i+1]>26 and s[i]!=‘0’)
- dp[i] = dp[i-1]+dp[i-2] -------s[i-1]!=‘0’ and int(s[i-1:i+1])<=26 and s[i]!=‘0’
- return 0 ---------------------- s[i-1:i+1]‘00’ or (s[i]‘0’ and s[i-1:i+1] >‘26’))
- 边界条件 dp[0] =1 dp[1] =0
翻转链表II (92)
- 栈
- 保留翻转部分的前一个指针与结束的后一个指针,将元素依次压入栈,然后将前一个指针依次连接出栈元素,最后连接后一个指针
复原IP地址 (93)
- 回溯
- 传递参数:要考虑的字符串的起始位置start,与迭代的次数iteration
- 结束条件:start>=n时退出
- 当iteration为3的时候,查看剩余的字符的个数大于3,或者值大于255或者存在’0n’的情况退出,否则加入字符串,并存储到结果列表中,回溯。
- iteration不为3,遍历判断三位字符串,是否符合条件,如符合条件,调用递归,然后回溯。
二叉树的中序遍历 (94)
- 递归
- 结束条件:节点为空
- 如果左节点不为空调用dfs,将该节点加入ret,如果右节点不为空,调用dfs。
不同的二叉搜索树II(95)
- 分治+递归
- 结束条件:start>end时,return [None]即当没有元素时,应该返回一个None的指针。
- 递归,遍历start到end 分别作为根节点,左边的元素递归(start,i-1),右边的元素递归(i+1,end),然后分别遍历左子树和右子树的返回的元素的集合来来组合左右子树的所有情况。
不同的二叉树 (96)
- 动态规划
- G(n)表示n个节点不同的二叉树的个数
- 对n个元素进行遍历,每个元素作为根节点,左边的和右边的元素分别就是左子树的种数和右子树的种数,此时可以调用动态。
- 状态转移方程:遍历每个节点 : G(n) += G(i-1)*G(n-i)
交错字符串 (97)
- 动态规划
- 使用 f[i][j] 表示加入s1的第i个字符和s2的第j个字符,是否能组成s3的i+j个字符。
- 状态转移方程
- f[i][j] |= f[i-1][j] -------------s1[i]==s3[i+j]
- f[i][j] |= f[i][j-1] -------------s2[j]==s3[i+j]
- 注:前题条件:如果m+n!=l 直接返回False。
验证二叉搜索树 (98)
- 深度优先搜索
- 使用深度优先搜索的方式验证是否二叉搜索树
- 参数,节点、值下界low、值的上界high,初始化的上界为(float(‘inf’)),下界为(float(’-inf’))
- 如果node为空,返回True,检查的左子树的值是否小于上界与大于下界,如果不满足返回False,否则:调用递归,上界为该节点的值,节点变为左孩子节点,下界为传入的下界值。
- 检查的右子树的值是否小于上界与大于下界,如果不满足返回False,否则:调用递归,下界为该节点的值,节点变为右孩子节点,上界为传入的上界值。
- 该题的关键思想为传入上界与下界,默认为最小与最大的,每次递归的时候都会传入适合的上界与下界,即每次更新为相应的上下界,这点是最重要的,这样相应的界限会划分清楚。
恢复二叉搜索树(99)
- 中序遍历
- 中序遍历保存节点,找到需要交换的节点,然后进行交换。
- 寻找交换节点时注意两个是否是连续的,或者是间隔的
相同的树(100)
- 递归
- 关键点:使用双指针,两个指针分别指向两棵树被检查的节点,每次递归检查两个节点是否完全相同
- 如果两个节点都为None,返回True;如果两个节点存在一个为None,返回false;如果两个节点的值不同返回False;递归调用检查两棵树的左子树节点与右子树节点。
对称二叉树 (101)
- 递归
- 关键点:使用双指针,两个指针分别指向树被检查的镜像节点,每次递归检查两个节点是否完全相同
- 如果两个节点都为None,返回True;如果两个节点存在一个为None,返回false;如果两个节点的值不同返回False;递归调用检查p的左孩子节点与q的右孩子节点和p的右孩子节点与q的左孩子节点。
二叉树的层次遍历 (102)
- 队列
- 可以使用start,end,nextEnd实现在一个队列里遍历完所有的元素
- 或者每次进行出队,检查队列为空的情况
二叉树的锯齿形遍历 (103)
- 双端队列
- 双端队列的方式
- 或者由上一题,设置一个标志位,来决定每次是将答案列表正序插入还是逆序插入。
二叉树的最大深度 (104)
- 深度优先搜索
从前序与中序遍历序列构造二叉树(105)
- 递归+分治
- 使用递归加分治的思想,首先找到根节点root。
- 由前序序列可以找到根节点,前序序列的第一个元素为该树的根节点,根据找到根节点,可以从中序序列的找到给树的左子树与右子树,即中序序列中根节点左边的元素为该树的左子树,右边的元素为该树的右子树。将左右子树分别进行递归迭代。注意:寻找元素的时候,首先根据前序的序列确定根节点,根据根节点在中序序列中的位置确定中序序列的左子树和右子树的区分,统计左子树元素的个数,然后可以去右子树中去找出相应的左子树序列和右子树序列。
- 前序序列为空时表示没有元素了,返回None。
从中序与后续遍历序列构造二叉树 (106)
- 递归+分治
- 解法同上,区别的是后续遍历的最后一个元素为根节点。
二叉树的层次遍历 (107)
- 队列
- 首先根据上面的层次遍历,修改的部分是每次讲结果插入头部
将有序数组转换为二叉搜索树 (108)
- 递归+模拟二分
- 问题的关键: 数之间的高度的差值不能超过1
- 采用递归,类似于二分查找的方式,设置left和right,结束条件left>right,返回None。 每次找到mid中间节点值构造为根节点,将mid左边的调用递归作为左子树,右边的调用递归作为右子树,返回值为构造的根节点。
- 注意:这样每个节点都作为mid只被访问一遍
将有序链表转换为二叉搜索树 (109)
- 递归+模拟二分
- 遍历链表,将值存储到列表,然后调用上一题的方法
- 快慢指针
- 使用快慢指针找出中间节点,方法:快指针每次走两位,慢指针走一位,快指针到达尾部时,慢指针正好指向中间节点。
平衡二叉树 (110)
- 自底向上的递归
- 使用自底向上的递归方式,每次计算获取高度,比较左右子树的高度差,如果高度差大于1,返回-1表示存在不平衡树,如果检查到返回值中存在-1直接继续返回-1,否则返回两者中较大的高度+1(因为根节点的不为空时,要子树的高度加一)。
二叉树的最小深度 (111)
- 自底向上的递归
- 使用自底向上的递归方式,如果左子树或右子树的返回的高度为0,即没有左子树或右子树,此时应选择返回的两个高度中较大的一个,否则(左右子树高度皆不为0),此时选择左右子树高度较小的一个,返回的高度为子树高度加1。
路径总和(112)
- 递归
- 采用递归的方式,如果root为None返回false,如果root.left==None and root.right == None ,说明已经到达了叶子节点,判断该节点的值是否与要判断的相同,如果相同返回True。否则如果左子树的不为空,调用递归,传入参数(root.left,target-root.val),检查返回值,如果返回值为True,直接返回True。同理调用右子树的递归。
115. 不同的子序列
- 动态规划
- 明确问题
两个字符串匹配,求最大的不同的匹配的状态数,使用动态规划。 - dp数组含义确定
d p [ i ] [ j ] dp[i][j] dp[i][j] 代表在s长为i和t的长为j的字符串的最大的匹配的状态数。 - 明确选择
-
s
[
i
]
=
=
t
[
j
]
s[i]==t[j]
s[i]==t[j] 这样存在两种情况,匹配该字符或不匹配该字符。
匹配该字符: d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] dp[i][j] = dp[i-1][j-1] dp[i][j]=dp[i−1][j−1]
不匹配该字符$dp[i][j] = dp[i-1][j] $ - s [ i ] ! = t [ j ] s[i]!=t[j] s[i]!=t[j] 不匹配该字符,转态只有 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j] dp[i][j]=dp[i−1][j]
-
s
[
i
]
=
=
t
[
j
]
s[i]==t[j]
s[i]==t[j] 这样存在两种情况,匹配该字符或不匹配该字符。
- 边界状态
- 两个都是空字符串可以匹配1个,即 d p [ 0 ] [ 0 ] = 1 dp[0][0]=1 dp[0][0]=1
- s为空,t不为空,不能匹配,即 d p [ i ] [ 0 ] = 0 dp[i][0] = 0 dp[i][0]=0
- s不为空,t为空,至少匹配一个。即 d p [ 0 ] [ j ] = 1 dp[0][j] = 1 dp[0][j]=1
- 状态转移方程
- d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + d p [ i ] [ j − 1 ] dp[i][j] = dp[i-1][j-1]+dp[i][j-1] dp[i][j]=dp[i−1][j−1]+dp[i][j−1] ---------- s [ j ] = = t [ i ] s[j] ==t[i] s[j]==t[i]
- d p [ i ] [ j ] = d p [ i ] [ j − 1 ] dp[i][j] = dp[i][j-1] dp[i][j]=dp[i][j−1] ------------------------- s [ j ] ! = t [ i ] s[j] !=t[i] s[j]!=t[i]
- 明确问题
-
代码
public int numDistinct(String s, String t){ int ret; int n = s.length()+1; int m = t.length()+1; int[][] dp = new int[m][n]; dp[0][0] = 1; for(int i=0;i<m;i++){ for(int j=1;j<n;j++){ if(i==0){dp[i][j]=1;} else{ char si = t.charAt(i-1); char tj = s.charAt(j-1); if(tj == si){ dp[i][j] = dp[i][j-1] + dp[i-1][j-1]; } else { dp[i][j] = dp[i][j-1]; } } } } return dp[m-1][n-1]; }
116. 填充每个节点的下一个右侧节点指针
- 层次遍历的方法
- 使用层次遍历,然后根据层次遍历的结果添加右指针
- 代码示例
public Node connect(Node root){ // 层次遍历 ArrayList<Node> a = new ArrayList<>(); a.add(root); int start = 0; int end = 0; int nextEnd = 0; while(start<=end){ Node temp = a.get(start); if(temp.left!=null){ a.add(temp.left); a.add(temp.right); } nextEnd =Math.max(nextEnd,a.size()-1); if(start == end){ end = nextEnd; start++; } else{ Node nextTemp = a.get(start+1); temp.next = nextTemp; start++; } } return root; }
- 使用递归的方法+next指针
- 关键点:如果root的next指针不为空的时候,应将root的right节点的next指针指向root的next指针节点的left左孩子节点
- root的left不为空,root 的left 节点的next指向root 的right节点,right节点的next指向root的next 的left节点。
- 代码示例
// 使用next+递归的方式完成 public Node dfs(Node root){ if(root==null){ return root; } if(root.left!=null) { root.left.next = root.right; if(root.next!=null){ root.right.next = root.next.left; } dfs(root.left); dfs(root.right); } return root ; } public static void main(String[] args){ Solution sl = new Solution(); }
117、填充每个节点的下一个右侧节点指针II
- 层次遍历+优先队列
- 使用层次遍历,在完成层次遍历时,使用queue队列,注:在使用队列的时候,注意每次while循环时,统计一次队列长度,作为一个层次,使用for循环进行遍历(该次循环会将该层的元素全部弹出)。
// 使用层次遍历的方法实现 public Node connect(Node root){ if(root==null) return root; Queue<Node> queue = new LinkedList<>(); queue.offer(root); while(!queue.isEmpty()){ int n = queue.size(); Node last = null; for(int i =0;i<n;i++) { Node temp = queue.poll(); if(temp.left!=null) queue.offer(temp.left); if(temp.right!=null) queue.offer(temp.right); if(i!=0) last.next = temp; last = temp; } } return root; }
- 使用层次遍历,在完成层次遍历时,使用queue队列,注:在使用队列的时候,注意每次while循环时,统计一次队列长度,作为一个层次,使用for循环进行遍历(该次循环会将该层的元素全部弹出)。
118、杨辉三角
- 模拟法
- 使用二维数组模拟,关键点:如果
j
=
=
0
∣
∣
j
=
=
i
j==0 || j==i
j==0∣∣j==i的时候直接添加1,其余情况使用该元素与左边元素相加可得
public List<List<Integer>> generate(int numRows){ List<List<Integer>> ret = new ArrayList<List<Integer>>(); if(numRows==0) return ret; for(int i =0;i<numRows;i++){ List<Integer> temp = new ArrayList<>(); for(int j=0;j<=i;j++) { if(j==0||j==i){ temp.add(1); } else{ temp.add((ret.get(i-1).get(j-1)+ret.get(i-1).get(j))); } } ret.add(temp); } return ret; }
- 使用二维数组模拟,关键点:如果
j
=
=
0
∣
∣
j
=
=
i
j==0 || j==i
j==0∣∣j==i的时候直接添加1,其余情况使用该元素与左边元素相加可得
119、杨辉三角II
- 递推的方式,在原始的数组递推求解新的数组
public List<Integer> getRow(int rowIndex) { List<Integer> ret = new ArrayList<>(); for(int i=0;i<=rowIndex;i++){ int tempNum = 1; for (int j=1;j<i;j++){ int next = tempNum+ret.get(j); tempNum = ret.get(j); ret.set(j,next); } ret.add(1); } return ret; }
120、 三角形最小路径和
- 动态规划
- 状态转移方程
- 代码
public int minimumTotal(List<List<Integer>> triangle) { int n = triangle.size(); for(int i=1;i<n;i++) { List<Integer> temp = triangle.get(i); for(int j=0;j<=i;j++){ if(j==0){ int _temp = triangle.get(i-1).get(j)+temp.get(j); temp.set(j,_temp); } else if(j==i){ int _temp = triangle.get(i-1).get(j-1)+temp.get(j); temp.set(j,_temp); } else{ int _temp = Math.min(triangle.get(i-1).get(j),triangle.get(i-1).get(j-1))+temp.get(j); temp.set(j,_temp); } } triangle.set(i,temp); } int ret = 0x3f3f3f3f; for(int j=0;j<=n-1;j++){ ret = Math.min(triangle.get(n-1).get(j),ret); } return ret; }
- 状态转移方程
121、 买卖股票的最佳时机
- 一次遍历
- 在遍历每个元素的时候,如果该元素的值便买入的价格高,则计算在此时卖出的利润会不会是最大,否则将买入价格更新为此时的价格,(因为此时的更低的价格会遮住左边的价格)
- 代码:
public int maxProfit(int[] prices) { int n = prices.length; int ans = 0; int buyPrice = prices[0]; for(int i=0;i<n;i++){ if(prices[i]>buyPrice ){ ans = Math.max(prices[i] - buyPrice,ans); } else{ buyPrice = prices[i]; } } return ans; }
122、买卖股票的最佳时机II
-
动态规划
- 明确问题,在持有和不持有股票的两种状态下的最大利润,使用动态规划
- dp数组定义,dp[i][0]表示在第i天不持有股票时的最大利润,dp[i][1]表示在第i天时持有股票的最大利润
- 明确选择
- 在第i天持有股票dp[i][1] ,可以由前一天持有股票时的利润所得,或者前一天不持有股票,买入股票的利润所得,两者取最大值。
- 在第i天不持有股票dp[i][0],可以由前一天不持有股票时的利润或前一天持有股票然后卖出所得,两者取最大值
- 状态转移方程
- d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] + p r i c e [ i ] ) dp[i][0] = max(dp[i-1][0],dp[i-1][1]+price[i]) dp[i][0]=max(dp[i−1][0],dp[i−1][1]+price[i])
- d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 0 ] − p r i c e [ i ] ) dp[i][1] = max(dp[i-1][1],dp[i-1][0]-price[i]) dp[i][1]=max(dp[i−1][1],dp[i−1][0]−price[i])
- 边界条件
- d p [ 0 ] [ 0 ] = 0 dp[0][0] = 0 dp[0][0]=0
-
d
p
[
0
]
[
1
]
=
−
p
r
i
c
e
[
i
]
dp[0][1] = - price[i]
dp[0][1]=−price[i]
-分析可得最终一定时未持有股票的利润最大
-
贪心算法
- 每次的计算当天与前一天利润, a n s + = m a x ( 0 , p r i c e [ i ] − p r i c e [ i − 1 ] ans+= max(0,price[i]-price[i-1] ans+=max(0,price[i]−price[i−1]
- 相当于计算多个区间的利润,使用贪心每次只选择区间值大于0的进行相加。
-
一次遍历
- 只有当天的价格比前一天的价格低时,将前一天的股票卖出,买入价格更新为改天的价格。注:遍历完后,要将最后一天的股票卖出计算利润。
- 代码
public int maxProfit(int[] prices) { int n = prices.length; int ans = 0; int buyPrice = prices[0]; for(int i=1;i<n;i++){ if(prices[i]<prices[i-1] ){ ans += prices[i-1]-buyPrice; buyPrice = prices[i]; } } if(prices[n-1]>buyPrice){ ans += prices[n-1]-buyPrice; } return ans; }
123、买卖股票的最佳时机III
- 动态规划
- 明确状态:在最多买卖两次的情况下的最大利润,4种状态
- buy1:第一次买入,手中持有股票
- sell1: 第一次卖出,手中不持有股票
- buy2:第二次买入,手中持有股票
- sell2:第二次卖出,手中不持有股票
- dp数组的定义
- buy1:手中第一次买入的最大利润;同理:sell1、buy2、sell2
- 明确选择
- buy1:第i天的buy1是已经买入股票,手中持有股票,从max(buy1,-price[i])所得,其中-price[i]表示前面没买,今天买入股票。
- sell1:第i天已经卖出股票的且手中不持有股票
- 最终的结果由sell2,表示的利润一定是最大的
- 状态转移方程
- b u y 1 = m a x ( b u y 1 ′ , p r i c e [ i ] ) buy1 = max(buy1',price[i]) buy1=max(buy1′,price[i])
- s e l l 1 = m a x ( b u y 1 + p r i c e [ i ] , s e l l 1 ′ ) sell1 = max(buy1+price[i],sell1') sell1=max(buy1+price[i],sell1′)
- b u y 2 = m a x ( b u y 2 ′ , s e l l 1 − p r i c e [ i ] ) buy2 = max(buy2',sell1-price[i]) buy2=max(buy2′,sell1−price[i])
- s e l l 2 = m a x ( s e l l 2 ′ , b u y 2 + p r c i e [ i ] ) sell2 = max(sell2',buy2+prcie[i]) sell2=max(sell2′,buy2+prcie[i])
- 边界条件
- b u y 1 = − p r i c e [ 0 ] buy1 = -price[0] buy1=−price[0]
- s e l l 1 = 0 sell1 = 0 sell1=0
- b u y 2 = − p r i c e [ 0 ] buy2 = -price[0] buy2=−price[0]
- s e l l [ i ] = 0 sell[i] = 0 sell[i]=0
- 代码
public int maxProfit(int[] prices) { int n = prices.length; int sell1 = 0,buy1 = -prices[0],sell2 = 0,buy2 = -prices[0]; for(int i =1;i<n;i++){ buy1 = Math.max(-prices[i],buy1); sell1 = Math.max(buy1+prices[i],sell1); buy2 = Math.max(buy2,sell1-prices[i]); sell2 = Math.max(sell2,buy2+prices[i]); } return sell2; }
- 明确状态:在最多买卖两次的情况下的最大利润,4种状态
124、二叉树的最大路径和
- 递归
- 首先计算每个节点的左右孩子节点的返回值,采取递归调用,即:因为根节点只有走左孩子或有孩子中的其中一个,不能同时走左右。所以返回值为
r o o t . v a l + m a x ( l e f t M a x V a l , r i g h t M a x V a l ) root.val+max(leftMaxVal,rightMaxVal) root.val+max(leftMaxVal,rightMaxVal) - 针对最好的路径和,维护一个最终答案maxSum,每次遍历到一个节点,将其作为路径的根节点。以该节点为根节点的最大路径和维护maxSum, m a x S u m = m a x ( m a x S u m , r o o t . v a l + l e f t V a l + r i g h t V a l ) maxSum = max(maxSum,root.val+leftVal+rightVal) maxSum=max(maxSum,root.val+leftVal+rightVal),注:leftVal,rightVal如果返回的是负值将其设置为0.
- 代码:
private int maxSum = Integer.MIN_VALUE; public int dfs(TreeNode root){ if(root == null) return 0; int leftVal = dfs(root.left); int rightVal = dfs(root.right); maxSum = Math.max(maxSum,leftVal+rightVal+root.val); return root.val+Math.max(leftVal,rightVal); } public int maxPathSum(TreeNode root) { dfs(root); return maxSum; } }
- 首先计算每个节点的左右孩子节点的返回值,采取递归调用,即:因为根节点只有走左孩子或有孩子中的其中一个,不能同时走左右。所以返回值为
125、验证回文串
- 左右指针
使用左右指针分别进行元素比较,判断相应的元素是否是字符class Solution { public boolean isPalindrome(String s) { int right = s.length()-1; int left = 0; while(left<right){ while(left<right && !Character.isLetterOrDigit(s.charAt(left))) left++; while (left<right && !Character.isLetterOrDigit(s.charAt(right))) right--; if(Character.toLowerCase(s.charAt(left)) != Character.toLowerCase(s.charAt(right))){ return false; } left++; right--; } return true; } }
126、单词接龙
- 构建图+广度优先搜索+深度优先搜索
- 构建包含最短序列的图,首先将所有的单词放入hash集合中,检查集合中是否由endWord,如果没有直接返回空路径;使用层次遍历的方式的构建的图:使用队列,首先将beginWord放入队列中,层次遍历的方式,初始的步数为零,针对每次取出的单词,对其所有的位置进行’a’-'z’的替换,如果替换的结果在from中存在(from为hashMap,key为当前的单词,value为当前单词的前一个位置的单词)即:替换的结果形成的单词的为下一个步数的单词,如果其存在且它的步数等于当前的步数,将此时原始的单词放入value中作为变换单词。检查dict中是否存在该单词,如果不存在直接continue,否则:从dict中移除该单词(该单词一出现,后面出现一定不合要求),将该单词对应的key、value加入from中,将该单词进入队列。设置一个标志位表示是否找到endWord。
- 使用深度优先遍历,从endWord向前寻找,使用深度优先遍历,如果endword==beginword说明找到,将该序列加入结果。注意回溯。
- 代码
public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) { List<List<String>> res = new ArrayList<>(); Set<String> wordDict= new HashSet<>(wordList); // 如果不含结束单词直接退出 if(!wordDict.contains(endWord)){ return res; } wordDict.remove(beginWord); Map<String,Integer> steps = new HashMap<>(); steps.put(beginWord,0); Map<String,List<String>> from = new HashMap<>(); int step = 1; boolean found = false; int wordLen = beginWord.length(); Queue<String> queue = new LinkedList<>(); queue.offer(beginWord); while(!queue.isEmpty()){ int size = queue.size(); for(int i = 0;i<size;i++){ String curWord = queue.poll(); char[] charWord = curWord.toCharArray(); for(int j = 0;j<wordLen;j++){ char origin = charWord[j]; for(char c= 'a';c<='z';c++){ charWord[j] = c; String newWord = String.valueOf(charWord); if(steps.containsKey(newWord) && step== steps.get(newWord)){ from.get(newWord).add(curWord); } if(!wordDict.contains(newWord)){ continue; } wordDict.remove(newWord); queue.offer(newWord); from.putIfAbsent(newWord,new ArrayList<>()); from.get(newWord).add(curWord); steps.put(newWord,step); if(newWord.equals(endWord)){ found = true; } } charWord[j] = origin; } } step++; if(found){ break; } } if(found){ Deque<String> path = new ArrayDeque(); path.add(endWord); dfs(from,path,beginWord,endWord,res); } return res; } public void dfs(Map<String,List<String>> from,Deque<String> path,String beginWord,String endWord, List<List<String>> res){ if(endWord.equals(beginWord)){ res.add(new ArrayList<>(path)); return; } else{ for(String s:from.get(endWord)){ path.addFirst(s); dfs(from,path,beginWord,s,res); path.removeFirst(); } } }
127、单词接龙I
- 广度优先搜索+set集合
- 将List中的数据放入集合,使用HashSet,检查Set中是否包含endword,如果不包含直接返回false;
- 使用广度优先搜索,并记录步数step
- 基于队列queue的方式,每次遍历这一步时所对应的队列中所有单词,针对该单词的所有位置进行’a’-'z’的替换,看产生的新单词是否在结合中,如果在集合中,从集合中移除该单词,并将该单词加入队列。如果该单词为endword返会step。
- 代码示例:
public int ladderLength(String beginWord, String endWord, List<String> wordList) { Set<String> wordDict = new HashSet<>(wordList); if(!wordDict.contains(endWord)){ return 0; } wordDict.remove(beginWord); int currentStep = 0; Queue<String> queue = new LinkedList<>(); wordDict.remove(beginWord); queue.add(beginWord); int wordLen = beginWord.length(); while(!queue.isEmpty()){ currentStep+=1; int n = queue.size(); for(int i =0;i<n;i++){ String currentWord = queue.poll(); char[] charList = currentWord.toCharArray(); for(int j=0;j<wordLen;j++){ char origin = charList[j]; for(char c ='a';c<='z';c++){ charList[j] = c; String newWord = String.valueOf(charList); if(!wordDict.contains(newWord)){ continue; } wordDict.remove(newWord); queue.add(newWord); if(newWord.equals(endWord)){ return currentStep+1; } } charList[j] = origin; } } } return 0; }
128、最长连续序列
- Set集合
- 将列表构建成集合。
- 遍历列表,如果这个数前一个数在集合中(前一个数的最大长度一定比这个数长),跳过。否则,开始往后遍历,检查列表中是否存在下一个。记录长度。
- 代码示例:
public int longestConsecutive(int[] nums) { int res = 0; Set<Integer> intSet = new HashSet<>(); for(int num:nums){ intSet.add(num); } for(int num:nums){ if(!intSet.contains(num-1)){ int count = 1; int temp = num+1; while(intSet.contains(temp)){ temp++; count++; } res = Math.max(res,count); } } return res; }
129、求根节点到叶节点数字之和
- 深度优先搜索+递归
- 深度优先搜索,
- 检查节点是否为null,直接返回0
- 检查是否为叶子节点,返回sum+root.val
- 返回递归的调用左孩子与右孩子值之和。
- 示例:
public int sumNumbers(TreeNode root) { return dfs(root,0); } public int dfs(TreeNode root,int sum){ if(root==null){ return 0; } if(root.left==null && root.right==null){ return sum*10+root.val; } return dfs(root.left,sum*10+root.val)+dfs(root.right,sum*10+root.val); }
- 深度优先搜索,
130、 被围绕的区域
- 深度优先搜索+暴力法
- 针对边缘位置使用深度优先搜索分别进行标记
- 针对所有的边缘位置,如果其为’0‘,使用深度优先搜索的方式进行标记
- 深度优先搜索
- 如果位置越界或者该位置的值为‘X’或者该位置的值为’A’(表示已经遍历过),直接返回。
- 否则将该位置的值设为‘A’,然后针对上下左右四个方向再进行深度优先遍历。
- 将所有标记为’A’改为‘O’,其余的改为’X’.
- 代码示例:
public void solve(char[][] board) { int m = board.length; if(m==0) return ; int n = board[0].length; for(int i=0;i<m;i++) for(int j=0;j<n;j++){ if((i==0||i==m-1||j==0||j==n-1)&&board[i][j]=='O'){ dfs(board,i,j,m,n); } } for(int i=0;i<m;i++) for(int j=0;j<n;j++){ if(board[i][j]=='A'){ board[i][j]='O'; } else { board[i][j] = 'X'; } } } public void dfs(char[][] board,int col ,int row,int m,int n){ if(col<0||col==m||row<0||row==n||board[col][row]=='X'||board[col][row]=='A'){ return; } board[col][row]='A'; dfs(board,col+1,row,m,n); dfs(board,col-1,row,m,n); dfs(board,col,row+1,m,n); dfs(board,col,row-1,m,n); }
131、 分割回文串
- 回溯+动态规划预处理
- 因为在进行回溯来遍历所有可能的分割情况的时候的适用所有指针检查字符串是否符合要求的时候会出现重复的进行的检查的情况的,所以使用动态规划的方式将所有位置的字符串是否能够成回文串的情况进行保存
- 动态规划
- 状态转移方程: d p [ i ] [ i ] = d p [ i + 1 ] [ j − 1 ] dp[i][i] = dp[i+1][j-1] dp[i][i]=dp[i+1][j−1] ----------- s [ i ] = = s [ j ] 时 s[i] == s[j]时 s[i]==s[j]时
- 观察状态转移方程可得:i的遍历方式应为是:从 n − > 0 n->0 n−>0, i中套的循环应该为从 j − > n j->n j−>n,这样循序的情况的会将之前的状态首先处理得到。
- 回溯的方法
- 使用深度优先搜索的方式+回溯遍历所有的情况。
- 深度优先搜索传入的参数i,代表在i位置之前的已经做成的回文串放入了ans中,遍历j为 i − > n i->n i−>n,检查的i->j位置的字符串能否构成回文串,如果构成的回文串,将其加入ans,并从j+1的位置调用递归,递归结束后,从ans中弹出最后一个元素,形成回溯。 结束条件:如果i==n,代表所有的字符已经放入ans,将ans加入res,并return。
- 代码示例:
List<List<String>> res = new ArrayList<>(); List<String> ans = new ArrayList<>(); boolean[][] dp; int m; public void dfs(String s,int i){ if(i==m){ res.add(new ArrayList<>(ans)); return ; } for(int j =i;j<m;j++){ if(dp[i][j]){ ans.add(s.substring(i,j+1)); dfs(s,j+1); ans.remove(ans.size()-1); } } } public List<List<String>> partition(String s) { // 动态规划 m = s.length(); if(m==0){ return new ArrayList<List<String>>(); } dp = new boolean[m][m]; // 动态规划,标记所有的回文字符串的真伪 for(int i =m-1;i>=0;i--) for(int j=i;j<m;j++){ 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]; } } } } // 使用回溯的方法将所有的结果存储下来 dfs(s,0); return res; }
132、分割回文串II
- 动态规划
- 使用两次动态规划来解决该问题。
- 第一次动态规划标记回文串
- 状态转移方程: d p [ i ] [ i ] = d p [ i + 1 ] [ j − 1 ] dp[i][i] = dp[i+1][j-1] dp[i][i]=dp[i+1][j−1] ----------- s [ i ] = = s [ j ] 时 s[i] == s[j]时 s[i]==s[j]时
- 观察状态转移方程可得:i的遍历方式应为是:从 n − > 0 n->0 n−>0, i中套的循环应该为从 j − > n j->n j−>n,这样循序的情况的会将之前的状态首先处理得到。
- 经过该次动态规划,得到该字符串的个位置是否能构成回文串。
- 第二次动态规划找最少的次数
- 如果字符串为回文串,不需要分割,分割次数为0。否则从最后一个开始检查构成回文串的位置,将该位置前的次数加1,遍历所有之前可行的位置保持最小的
- 明确状态:f[i] 代表前i个字符串划分成回文串的最少次数。
- 状态转移方程
- f [ i ] = 0 f[i] = 0 f[i]=0---------- d p [ 0 ] [ i ] = = t r u e dp[0][i]==true dp[0][i]==true
- f [ i ] = m i n f [ j − 1 ] + 1 f[i] = min{f[j-1]}+1 f[i]=minf[j−1]+1------- d p [ j ] [ i ] = = t r u e dp[j][i]==true dp[j][i]==true
- 代码示例:
public int minCut(String s) { int n = s.length(); boolean[][] dp = new boolean[n][n]; // 动态规划验证字符串是否为回文字符串 for(int i=n-1;i>=0;i--) for(int j = i;j<n;j++){ 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]; } } } } int[] f = new int[n]; for(int i=0;i<n;i++){ if(dp[0][i]){ f[i] = 0; } else{ f[i] = Integer.MAX_VALUE; for(int j=i;j>=0;j--){ if(dp[j][i]) f[i] = Math.min(f[i],f[j-1]+1); } } } return f[n-1]; }
133、克隆图
- 哈希表+深度优先搜索
- 问题:在进行图拷贝的时候,可能会出现闭环,导致无限运行,因此:对已经拷贝过的点,将其放入hash表中,使用点的val作为k值。
- 对每个需要拷贝的Node的邻居节点使用深度优先搜索的方式来分别进行拷贝。存在于Hash中的点直接返回,不存在的调用新DFS进行构建。
- 代码示例:
public Node cloneGraph(Node node) { if(node==null) return null; return dfs(node); } public Node dfs(Node node){ if(nodeMap.containsKey(node.val)) return nodeMap.get(node.val); Node newNode = new Node(node.val,new ArrayList<>()); nodeMap.put(newNode.val,newNode); for(Node neighborNode: node.neighbors){ newNode.neighbors.add(dfs(neighborNode)); } return newNode; }
- 哈希表+广度优先搜索
- hash表同上,将深度优先搜索改为广度优先搜索。、
- 代码示例:
Map<Integer,Node> nodeMap = new HashMap<>(); public Node cloneGraph(Node node) { if(node==null) return null; Node newNode = new Node(node.val,new ArrayList<>()); nodeMap.put(newNode.val,newNode); Deque<Node> deque = new LinkedList<>(); deque.offer(node); while(!deque.isEmpty()){ Node tempNode = deque.poll(); for(Node n:tempNode.neighbors){ if(nodeMap.containsKey(n.val)){ nodeMap.get(tempNode.val).neighbors.add(nodeMap.get(n.val)); } else{ Node nextNode = new Node(n.val,new ArrayList<>()); nodeMap.put(nextNode.val,nextNode); nodeMap.get(tempNode.val).neighbors.add(nextNode); deque.offer(n); } } } return newNode; }
134、加油站
-
一次遍历的方法
- 使用一次遍历的方法。针对所有的加油站的个数进行一次遍历,使用cnt记录从某个加油站走过的站数,如果cnt为n,表示已经走过所有的加油站,则从该站能走到所有的站。
- 如果从该站走,退出之后发现cnt不等于n,则表示不能走过所有的站,下一个需要的遍历的站应该为第一个不可达的站。
- 注:如果从i站开始走之后到达不来j站,表示,那么从i-j之间的任何一站开始也是必到达不了的j站的。因为假设从k站开始,其起始油量是0,但是从i站能到达k站,表示在k站时的油量一定是大于等于0的。
- 从到达不了的该站的开始,如果可以到达该站之前的那站,它在前一站剩余的油量一定是最多的,看能否从前一站到达该站。
- 代码示例:
public int canCompleteCircuit2(int[] gas, int[] cost){ int n = gas.length; int i = 0; while(i<n){ int sumGas = 0,sumCost = 0; int cnt = 0; while(cnt<n){ sumGas += gas[cnt+i]; sumCost += cost[cnt+i]; if(sumCost>sumGas) break; } if(cnt==n){ return i; } i = i+cnt; } return -1; }
-
移动最低点的方法
- 如果总油量大于等于总消耗量,一定存在一个站能使其走完一圈。因为一定存在一站开始的剩余能把最大的消耗填补上
- 一次遍历找出最低点,和总的耗油量和加油量。
- 最低点的下一站便是可行的一站,如果总耗油小于总加油时。
- 代码示例:
public int canCompleteCircuit(int[] gas, int[] cost) { int n = gas.length; int minIndex = 0; int minGas = Integer.MAX_VALUE; int currentgas = 0; for(int i=0;i<n;i++){ currentgas = gas[i]-cost[i]; if(currentgas<minGas){ minGas = currentgas; minIndex = i; } } return currentgas>=0?minIndex+1:-1; }
135、分发糖果
- 贪心算法+两次遍历
- 初始化res数组,并将数组中的元素的值全部初始化为1。
- 使用贪心算法,首先从左往右遍历,如果后一个元素的得分比前一个元素的得分高,将后一个元素的值变为前一个元素的值加一。这样会使得满足的正向遍历的情况像所有孩子最少的糖果数。
- 从后向前遍历,如果后一个孩子的得分比前一个孩子的得分高,并且前一个孩子的糖果数不大于后一个孩子的糖果数(如果大于,保持不变,不能减小,否则不满足正向的结果),将前一个孩子的糖果数变为后一个孩子糖果数的值加一。遍历完成的之后就可以满足两个条件。
- 代码示例:
public int candy(int[] ratings) { // 贪心算法 int n = ratings.length; int[] res = new int[n]; for(int i =0;i<n;i++){ res[i] = 1; } for(int i =1;i<n;i++){ if(ratings[i]>ratings[i-1]){ res[i] = res[i-1]+1; } } int sum = res[n-1]; for(int i = n-2;i>=0;i--){ if(ratings[i]>ratings[i+1]&& res[i]<=res[i+1]){ res[i] = res[i+1]+1; } sum+=res[i]; } return sum; }
136 、 只出现一次的数字
- 亦或
- 因为两个相同的数亦或为0,0和任何数亦或都等于数本身。
- 代码示例:
public int singleNumber(int[] nums) { int res = 0 ; int n = nums.length; for(int i = 0;i<n;i++){ res^= nums[i]; } return res; }
137、只出现一次的数字2
- 哈希表
- 通过HashMap哈希表统计每个元素出现的次数,在通过HashMap的entrySet遍历哈希表中的所有元素,将元素出现次数为1的取出来。
- 代码示例:
public int singleNumber(int[] nums) { Map<Integer,Integer> res = new HashMap<>(); int n = nums.length; for(int i = 0;i<n;i++){ res.put(nums[i],res.getOrDefault(nums[i],0)+1); } int ret =0 ; for(Map.Entry<Integer,Integer> entry:res.entrySet()){ if(entry.getValue()==1){ ret = entry.getKey(); } } return ret; }
138、复制带随机指针的链表
- 哈希表+深度优先搜索
- 使用哈希表,key设置为题目中给出的节点,value值对应的是新创建的节点。
- 针对每个出现的节点,创建新的节点,并将原有的节点和新的节点放入到哈希表中。然后检查的原节点的next节点,如果它在哈希表中,直接的取出并将新创建的节点的next执行取出的节点,否则,调用dfs进行该节点的深度优先遍历。再对random节点进行一次相同的操作。
- 代码示例:
HashMap<Node,Node> map = new HashMap<>(); public Node copyRandomList(Node head) { if(head==null){ return head; } return dfs(head); } public Node dfs(Node p){ Node temp = new Node(p.val); map.put(p,temp); if(p.next!=null ){ if(!map.containsKey(p.next)) temp.next = dfs(p.next); else{ temp.next = map.get(p.next); } } if(p.random!=null ){ if(!map.containsKey(p.random)) temp.random = dfs(p.random); else{ temp.random = map.get(p.random); } } return temp; }
139、 单词拆分
- Set集合+动态规划
- 将列表中的单词的放到构建的Set集合中,方便直接使用Set进行检索是否存在。
- 动态规划
- 明确状态
- dp[n] --------代表长度为n的字符串是否可以拆分成列表中的单词,true代表可以,false代表不可以。
- 明确选择
- 针对每个新加入的单词,将其从后向前遍历位置j,如果位置j到新加入字符位置的字符串在列表中且,dp[j]的值为true,代表j位置之前的字符也可以拆分成列表中单词。则将改位置设置为dp[i]设置为true。否则设置为false。
- 初始状态
- dp[0] 代表没有字符,则为true。
- 状态转移方程
- d p [ i ] = d p [ j ] & & s [ j − i ] dp[i] = dp[j] \&\& s[j-i] dp[i]=dp[j]&&s[j−i] 注意:s[j-i]代表其是否在set中,只要存在满足的即可。
- 明确状态
- 代码示例:
public boolean wordBreak(String s, List<String> wordDict) { Set<String> wordSet = new HashSet<>(wordDict); int n = s.length(); boolean[] dp = new boolean[n+1]; dp[0] = true; for(int i = 0;i<n;i++){ for(int j = i;j>=0;j--){ String temp = s.substring(j,i+1); if(wordSet.contains(temp) && dp[j]){ dp[i+1] = true; break; } } } return dp[n]; }
140 、单词拆分2
- 记忆化优先搜索(回溯+HashMap存储)
- 从每个位置开始遍历i,针对0-i的单词,如果其在单词的set集合,将0-i单词加入ret,调用深度优先搜索,从i+1的位置开始。
- 优化:在从某个位置之后的元素,可以建立一个Hash哈希表,key值设置为index,value为这个位置之后的元素可以排列的组合。
- 代码示例:
List<String> ans = new ArrayList<>(); public List<String> wordBreak(String s, List<String> wordDict) { Set<String> wordSet = new HashSet<>(wordDict); List<String> ret = new ArrayList<>(); int n = s.length(); return ans; } public void dfs(int start,int n,String s, List<String> ret, Set<String> wordSet){ if(start==n){ ans.add(String.join(" ",ret)); return; } for(int i = start;i<n;i++){ String temp = s.substring(start,i+1); if(wordSet.contains(temp)){ ret.add(temp); dfs(i+1,n,s,ret,wordSet); } } }
141、环形链表
- 快慢指针
- 使用快慢指针,快指针每次走两步,慢指针每次走一步,如果快指针与慢指针相遇说明存在环;如果快指针指向了null值,说明遍历完了列表,并没有环。
- 代码示例:
public boolean hasCycle(ListNode head) { if(head==null){ return false; } ListNode p = head,q =head; while(p!=null && q!=null){ p = p.next; if(q.next==null) return false; q = q.next.next; if(p==q) return true; } return false; }
142、环形链表2
- 哈希表
- 使用HashSet集合,遍历链表,检查集合中是否有遍历的元素,如果没有元素将其加入Set集合中,如果存在该元素,则该元素为第一个出现的循环链表的元素。如果到null,则没有环。
public ListNode detectCycle(ListNode head) { HashSet<ListNode> set = new HashSet<>(); ListNode p = head; while(p!=null){ if(set.contains(p)){ return p; } set.add(p); p = p.next; } return null; }
- 使用HashSet集合,遍历链表,检查集合中是否有遍历的元素,如果没有元素将其加入Set集合中,如果存在该元素,则该元素为第一个出现的循环链表的元素。如果到null,则没有环。
- 快慢链表
- 快指针每次走两步,慢指针走一步。 a = c + ( n − 1 ) ( b + c ) a = c+(n-1)(b+c) a=c+(n−1)(b+c)
- 在慢指针的相遇点,创建新的指针从head开始与慢指针的一起移动,两者的相遇点就是环的入口点。
143、重排链表
- 队列
- 创建队列,将所有的元素放入到队列中,然后一次从队列头和队列尾取元素进行链接。
- 代码示例:
public ListNode detectCycle(ListNode head) { HashSet<ListNode> set = new HashSet<>(); ListNode p = head; while(p!=null){ if(set.contains(p)){ return p; } set.add(p); p = p.next; } return null; }
144、二叉树的前序遍历
- 递归
- 代码示例:
List<Integer> ret = new ArrayList<>(); public List<Integer> preorderTraversal(TreeNode root) { if(root== null) return ret; ret.add(root.val); preorderTraversal(root.left); preorderTraversal(root.right); return ret; }
- 代码示例:
145、二叉树的后续遍历
- 递归
- 代码示例:
List<Integer> ret = new ArrayList<>(); public List<Integer> postorderTraversal(TreeNode root) { if(root== null) return ret; postorderTraversal(root.left); postorderTraversal(root.right); ret.add(root.val); return ret; }
- 代码示例:
146、LRU缓存机制
- 双端链表队列+哈希表
- 构建HashMap,哈希表,key值的为索引值key,value值的为一个的包含key,value,prev,next的node节点,该节点的value未相应的value值,prev和next可以构建双端链表。
- 哈希表的作用主要可以通过key值,直接找到相应的node节点。双端的队列的作用主要是实现先进先出,将最近访问的节点放入到头节点中,表示是最新访问的,使用伪head和伪tail节点可以直接进行访问,根据tai删除最久为访问的,通过head将最新的节点的放入到队头。
- !!!注:双端队列的head可以快速定位到头部,方便更新;tail可以快速的定位到队列的尾部,方便快速的删除,Map哈希表方便快速根据key值快速找到相应的node节点,size和capacity检查LRU是否已经满了。
- 代码示例
public class LRUCache { class DlinkNode{ int key; int val; DlinkNode prev; DlinkNode next; public DlinkNode(){} public DlinkNode(int key,int val){ this.key = key; this.val = val; } } private int size; private int capacity; private DlinkNode head,tail; private Map<Integer,DlinkNode> cache= new HashMap<>(); public LRUCache(int capacity){ this.size = 0; this.capacity = capacity; head = new DlinkNode(); tail = new DlinkNode(); head.next = tail; tail.prev =head; } public int get(int key){ DlinkNode node = cache.get(key); if(node==null){ return -1; } removeToHead(node); return node.val; } public void removeToHead(DlinkNode node){ node.prev.next = node.next; node.next.prev = node.prev; node.prev = head; node.next =head.next; head.next.prev = node; head.next = node; } public void put(int key,int val){ DlinkNode node = cache.get(key); if(node==null){ node = new DlinkNode(key,val); cache.put(key,node); node.prev = head; node.next = head.next; head.next.prev = node; head.next = node; size++; if(size>capacity){ delTail(); } System.out.println(node.val); } else{ node.val = val; removeToHead(node); } } public void delTail(){ int key = tail.prev.key; tail.prev =tail.prev.prev; tail.prev.next = tail; cache.remove(key); --size; } }
147、对链表进行插入排序
- 针对链表的模拟法
- 模拟插入排序
- 创建一个哑节点(哑节点初始化的next指针指向head节点,哑节点的主要作用是用来指向头节点,因为头节点会随着插入排序的过程而改变但是哑节点的不是改变,在使用链表时哑节点的作用是非常重要的)
- cur指向当前要处理的节点,lastSortNode指向已经有序的链表的最后一个节点。针对每个访问的节点,遍历链表找到其应该插入的位置,使用next的方法,如果找到的为lastSortNode,直接将last Sort Node指向cur,cur指向下一个节点,否则将cur插入到相应位置,cur更新为下一个节点。
- 代码示例:
public ListNode insertionSortList(ListNode head) { ListNode pre = new ListNode(); pre.next = head; if(head==null) return head; ListNode cur = head.next; ListNode lastNode = head; while(cur!=null){ ListNode q = pre; while(q.next.val<cur.val){ q = q.next; } if(q == lastNode){ lastNode = cur; cur = cur.next; } else{ lastNode.next = cur.next; cur.next = q.next; q.next = cur; cur = lastNode.next; } } return pre.next; }
148、排序链表
- 自定向下的归并排序
- 首先自顶向下的划分数组,使用快慢指针找到链表的中点(快指针每次走两步,慢指针每次走一步),当快指针走到链表尾部时,慢指针到达链表中的重点,然后针对左右链表使用递归,left和right分别为递归返回的结果,然后对left和right进行合并。
- 如果传入的节点为null,或者它的next节点为null,直接返回head。
- 代码示例:
public ListNode sortList(ListNode head) { // 自定向下的归并排序 if(head==null || head.next==null) return head; ListNode pre = new ListNode(); pre.next = head; ListNode slow = head,fast = head; while(fast.next!=null && fast.next.next!=null){ fast = fast.next.next; slow = slow.next; } ListNode tmp = slow.next; slow.next = null; ListNode left = sortList(head); ListNode right = sortList(tmp); ListNode p = pre; while(left!=null && right!=null){ if(left.val<right.val){ p.next = left; left = left.next; } else{ p.next = right; right = right.next; } p = p.next; } if(left!=null) p.next = left; else{ p.next = right; } return pre.next; }
150、逆波兰数
- 使用栈的方法:出栈、入栈
- 代码示例:
public int evalRPN(String[] tokens) { Stack<String> stack = new Stack<>(); for(String s:tokens){ if(!s.equals("+") && !s.equals("-") &&!s.equals("*")&& !s.equals("/")) stack.push(s); else{ int num1,num2; switch (s){ case "+": num1 = Integer.parseInt(stack.pop()); num2 = Integer.parseInt(stack.pop()); stack.push(String.valueOf(num1+num2)); break; case "-": num1 = Integer.parseInt(stack.pop()); num2 = Integer.parseInt(stack.pop()); stack.push(String.valueOf(num2-num1)); break; case "*": num1 = Integer.parseInt(stack.pop()); num2 = Integer.parseInt(stack.pop()); stack.push(String.valueOf(num1*num2)); break; case "/": num1 = Integer.parseInt(stack.pop()); num2 = Integer.parseInt(stack.pop()); stack.push(String.valueOf(num2/num1)); break; } System.out.println(s); } } return Integer.parseInt(stack.pop()); }
- 代码示例:
151、 反转字符串里的单词
-
使用语言特性
public String reverseWords(String s) { s.trim(); // 去除两端的空格 List<String> wordList = Arrays.asList(s.split("\\s+")); // 分割字符串构建成的集合 Collections.reverse(wordList); return String.join(" ",wordList); }
-
双端队列+StringBuffer
- 使用left和right指针,首先去除两端多余的空格。
- 构建StringBuffer类word来动态的存储获取到字符串,
- 每次当遇到’ '检查word是否为空,如果不为空将其插入双端队列的前部(实现了逆转)并将word的长度置为0;
- 如果不为’ ’ ,将其添加到word的尾部。
- 每次移动left
- 循环结束,检查word不为空时,将其插入字符串的头部。
- 使用string.join()返回
- 代码示例:
public String reverseWords(String s) { Deque<String> d = new ArrayDeque<>(); int left =0,right = s.length()-1; while (left<=right && s.charAt(left)==' '){ left++; } while (left<=right && s.charAt(right)==' '){ right--; } StringBuffer word = new StringBuffer(); while(left<=right){ char c = s.charAt(left); if((word.length()!=0)&&c==' '){ d.offerFirst(word.toString()); word.setLength(0); } else if(c !=' '){ word.append(c); } left++; } if(word.length()!=0){ d.offerFirst(word.toString()); } return String.join(" ",d); }
152、 乘积最大子数组
- 动态规划
- 如果不考虑数组中元素的正负情况,直接使用动态规划的方式。
- 最优子结构, f [ i ] f[i] f[i] 代表以i为结尾的最大的子序列乘积,
- 状态转移方程: f [ i ] = m a x { f [ i − 1 ] ∗ a [ i ] , a [ i ] } f[i] = max\{f[i-1]*a[i],a[i]\} f[i]=max{f[i−1]∗a[i],a[i]}
- 边界条件:f[0] = 0;
- 最终答案:将序列中的每个f[i] 状态保存最大值。
- 考虑数组的元素的正负情况g
- 最优子结构, f [ i ] f[i] f[i] 代表以i为结尾的最大的子序列乘积, g [ i ] g[i] g[i] 代表以i为结尾的最小的子序列乘积,
- 状态转移方程:
- f [ i ] = m a x { f [ i − 1 ] ∗ a [ i ] , g [ i − 1 ] ∗ a [ i ] , a [ i ] } f[i] = max\{f[i-1]*a[i],g[i-1]*a[i],a[i]\} f[i]=max{f[i−1]∗a[i],g[i−1]∗a[i],a[i]}
- g [ i ] = m i n { f [ i − 1 ] ∗ a [ i ] , g [ i − 1 ] ∗ a [ i ] , a [ i ] } g[i] = min\{f[i-1]*a[i],g[i-1]*a[i],a[i]\} g[i]=min{f[i−1]∗a[i],g[i−1]∗a[i],a[i]}
- 边界条件:f[0] = nums[0],g[0] = nums[0];
- 最终答案:将序列中的每个f[i] 状态进行检查保存最大值。ret初始化nums[0]
- 代码实现:
public int maxProduct(int[] nums) { // 动态规划 int f = nums[0]; int g = nums[0]; int n = nums.length; int ret = nums[0]; for(int i = 1;i<n;i++){ int tmp = f; f = Math.max(f*nums[i],g*nums[i]); f = Math.max(f,nums[i]); g = Math.min(tmp*nums[i],g*nums[i]); g = Math.min(g,nums[i]); ret = Math.max(f,ret); } return ret; }
- 如果不考虑数组中元素的正负情况,直接使用动态规划的方式。
153、寻找旋转排序数组中的最小值
- 二分查找
- 构建left和right指针
- 循环条件:
l
e
f
t
<
r
i
g
h
t
left<right
left<right,最终结束之后left指针指向元素最小的值
- m i d = ( l e f t + r i g h t ) / 2 mid = (left+right)/2 mid=(left+right)/2,指向中间元素位置
- 如果:right指针指向的向元素的大于中间元素: n u m s [ r i g h t ] > n u m s [ m i d ] nums[right]>nums[mid] nums[right]>nums[mid],说明mid到right中间的元素一定是有序的,最小元素的一定不在(mid,right)之间,可以将right设置为mid(因为mid可能为最小元素,所以将right设置为mid);
- 否则:在mid到right一定存在最小元素,且mid一定不是最小元素,将left设置为mid+1
- 代码示例:
public int findMin(int[] nums) { // 二分查找 int n = nums.length; int left = 0,right = n-1; while(left<right){ int mid = (left+right)/2; if(nums[right]>nums[mid]){ right = mid; } else { left = mid+1; } } return nums[left]; }
154、寻找旋转排序数组中的最小值
- 二分查找
- 本题相较于之前一个题的区别是存在重复的元素的,因此需要额外的判断
- 构建left和right指针
- 循环条件:
l
e
f
t
<
r
i
g
h
t
left<right
left<right,最终结束之后left指针指向元素最小的值l
- m i d = ( l e f t + r i g h t ) / 2 mid = (left+right)/2 mid=(left+right)/2,指向中间元素位置
- 如果nums[mid]<nums[right],在mid-right之间的元素是有序的,且不存在最小元素,将right设置为mid
- 否则:如果nums[mid]>nums[right],最小的元素的一定在mid-right的区间之内,所以left = mid+1;
- 否则:如果nums[mid]==nums[right],不能确定l最小元素所在的区间,right所在位置元素存在多个,所以可以right–;
- 代码示例:
public int findMin(int[] nums) { int left = 0,right = nums.length-1; while(left<right){ int mid = (left+right)/2; if(nums[mid]<nums[right]){ right = mid; } else{ if(nums[right]<nums[mid]){ left = mid+1; } else{ right--; } } } return nums[left]; }
155、最小栈
- 模拟实现
- 代码实现
class MinStack { /** initialize your data structure here. */ private Stack<Integer> s= new Stack<>(); public MinStack() { } public void push(int val) { s.add(val); } public void pop() { s.pop(); } public int top() { return s.peek(); } public int getMin() { int ret = Integer.MAX_VALUE; for(int i:s){ if(i<ret) ret = i; } return ret; } }
- 代码实现
160、相交链表
- Set集合方法
- 将两个链表同步移动,将每次遍历的元素加入Set中,如果链表遍历发现里面含有节点则表示为第一个相交的节点。
- 代码示例:
public ListNode getIntersectionNode(ListNode headA, ListNode headB) { HashSet<ListNode> set = new HashSet<>(); ListNode p = headA; ListNode q = headB; while(p!=null || q!=null){ if(p!=null){ if(set.contains(p)){ return p; } set.add(p); p = p.next; } if(q!=null){ if(set.contains(q)){ return q; } set.add(q); q = q.next; } } return q; }
162、寻找峰值
- 暴力法
- 直接遍历数组进行寻找,优化:将只有一个数字和数组头和数组为单独讨论。
- 二分查找
- 题目中给出相邻的两个元素一定不相等,所以通过二分查找,每次比较mid元素与mid+1元素,如果mid的值大于mid+1,峰值元素在mid左边,否则在右边。每次选择较大元素所在的区间,最终剩下的一个元素就是一个峰值元素。(因为每次元素选择之后一定之前一边的元素大,且因为这样选择之后一定比边界元素大。)
- 相当于找到了最优状态,向最终结果靠近,(最终的元素一定比上一次选择的元素大,其一定比另一个边界的元素大)
- 代码示例:
public int findPeakElement(int[] nums) { int right = nums.length-1; int left = 0; while(left<right){ int mid = (left+right)/2; if(nums[mid]>nums[mid+1]){ right = mid; } else { left = mid+1; } } return left; }
164、最大间距
- 基数排序
- 对数组使用基数排序之后然后遍历数组
- 代码示例:
public int maximumGap(int[] nums) { int n = nums.length; int[] cp = new int[n]; int maxValue = Arrays.stream(nums).max().getAsInt(); int e = 1; while(maxValue>=e){ int[] cnt = new int[10]; for(int i =0;i<n;i++){ int digit = (nums[i]/e)%10; cnt[digit]++; } for(int i =1;i<10;i++){ cnt[i] += cnt[i-1]; } for(int i =n-1;i>=0;i--){ int digit = (nums[i]/e)%10; cp[cnt[digit]-1] = nums[i]; cnt[digit]--; } e*= 10; System.arraycopy(cp,0,nums,0,n); } int ret = 0; for(int i =1;i<n;i++){ ret = Math.max(ret,nums[i]-nums[i-1]); } return ret; }