数据结构与算法学习(十八)
1、判断字符是否唯一
1.1、题目描述
实现一个算法,确定一个字符串
s
的所有字符是否全都不同。
1.2、解题思路
- 使用一个数组来解决这个问题,创建一个长度为 256 的数组(ascii 码数目)
- 循环遍历字符串转换而成的字符数组,当遍历到一个字符时,判断这个字符对应的 ascii 码对应的值是否为 1 ,如果是,直接返回 false ,否则将字符 ascii 码对应的那个元素置为 1
这道题也可以用集合来做,但没必要
1.3、解题代码
1 | public boolean isUnique(String astr) { |
2、字符串压缩
2.1、题目描述
字符串压缩。利用字符重复出现的次数,编写一种方法,实现基本的字符串压缩功能。比如,字符串
aabcccccaaa
会变为a2b1c5a3。若“压缩”后的字符串没有变短,则返回原先的字符串。你可以假设字符串中只包含大小写英文字母(a至z)。
2.2、解题思路
使用快慢指针完成这道题
初始化两个指针,慢指针 slow 一开始指向下标为 0 的元素,快指针 fast 一开始指向下标为 1 的元素
当 fast 指向的元素等于 slow 指向的元素时,此时让 right 往后移
当 fast 指向的元素不等于 slow 指向的元素时,此时进行结果字符串拼接,将 left 指向的字符拼接到结果字符串中,然后再将 right - slow 的值拼接到结果字符串中,然后让 left 移动到 right 的位置,同时 right 后移
2.3、解题代码
1 | public String compressString(String S) { |
3、字符串轮转
3.1、题目描述
字符串轮转。给定两个字符串
s1
和s2
,请编写代码检查s2
是否为s1
旋转而成(比如,waterbottle
是erbottlewat
旋转后的字符串)。
示例1:
1
2输入:s1 = "waterbottle", s2 = "erbottlewat"
输出:True示例2:
1
2输入:s1 = "aa", s2 = "aba"
输出:False
3.2、解题思路
- 两个字符串排序后判断是否相等
- 将两个 s2 进行拼接,得到结果串 Str ,然后判断 Str 是否含有
s1
,如果有,返回 true ,否则返回 false
3.3、解题代码
- 排序后判断是否相等
1 | public boolean isFlipedString(String s1, String s2) { |
- 拼接后判断
1 | public boolean isFlipedString(String s1, String s2) { |
4、字符串回文排列
4.1、题目描述
给定一个字符串,编写一个函数判定其是否为某个回文串的排列之一。
回文串是指正反两个方向都一样的单词或短语。排列是指字母的重新排列。
回文串不一定是字典当中的单词。
4.2、解题思路
- 要想一个字符串可以回文排列,那么必须满足以下条件之一
- 字符串有奇数个字符,同时只能有一个字符与其他字符不同,剩下的字符必须两两成对
- 字符串有偶数个字符,此时字符串中的字符必须两两成对。
使用一个 HashSet 来解决这个问题,遍历字符串中的每一个字符,然后尝试将其加入到 set 中,如果添加成功,那么什么都不做,如果添加失败,证明 set 中已经存在过当前遍历的字符,我们需要将其从 set 中剔除。
遍历完成后,判断 set 中的元素个数,如果小于等于 1 ,那么返回 true ,否则返回 false
4.3、解题代码
1 | public boolean canPermutePalindrome(String s) { |
5、一次编辑
5.1、题目描述
字符串有三种编辑操作:插入一个字符、删除一个字符或者替换一个字符。 给定两个字符串,编写一个函数判定它们是否只需要一次(或者零次)编辑。
5.2、解题思路
- 如果两个字符串长度相差一,那么直接返回 false
- 如果两个字符串长度相等,那么只需要逐个比较字符串中的字符即可,使用一个标识符来标识是否编辑过,当遇到第一个不等字符时,将这个标识符从 false 置为 true ,表示已经编辑过,如果在之后的遍历中遇到另外的不等字符,那么直接返回 false 即可。
- 如果两个字符串长度不相等,那么此时我们记长的字符串为 long ,短的字符串为 small ,仍然使用一个标识符来表示是否编辑过,这里使用双指针遍历两个字符串,如果两个指针指向的字符不等,那么将标识符从 false 置为 true ,表示已经编辑过,然后将用于遍历长字符串的指针后移
因为对于这次编辑来说,如果是添加,那么这个多出来的元素可能是短字符串中缺少的元素,此时我们需要让长字符串的下一个字符与当前短字符串的字符相比。
删除同理,将不等的那个字符删掉后,比较长字符串下一个字符与短字符串当前字符是否相等。
5.3、解题代码
1 | public boolean oneEditAway(String first, String second) { |
6、移除重复节点
6.1、题目描述
编写代码,移除未排序链表中的重复节点。保留最开始出现的节点。
6.2、解题思路
- 使用一个 Set 来解决这道题,初始化两个指针 pre 和 cur ,一个指向头节点,用于结果返回,第二个用于遍历链表
- 每遍历到一个节点,就尝试着将其加入到 set 中,如果加入成功,那么继续遍历,如果添加不成功,则删除那个无法加入到 set 之中的节点
- 最后返回头节点即可
6.3、解题代码
1 | public ListNode removeDuplicateNodes(ListNode head) { |
7、旋转矩阵
7.1、题目描述
给你一幅由
N × N
矩阵表示的图像,其中每个像素的大小为 4 字节。请你设计一种算法,将图像顺时针旋转 90 度。
- 示例 1:
1 | 给定 matrix = |
- 示例 2:
1 | 给定 matrix = |
7.2、解题思路
- 从外到内进行处理,在进行顺时针九十度旋转时,外圈和内圈的元素不会进行交换,也就是说,外圈元素只是和外圈元素进行交换,而内圈元素也仅和内圈元素进行交换
- 我们先处理最外圈的元素,以示例二的矩阵为例
1 | [ 5, 1, 9,11], |
在我们处理完最外圈元素后,我们再处理第二层的元素,此时只需要将左上角点向右下角移动,右下角点向左上角点移动即可处理第二圈…
在一个圈内,我们可以将这个 n * n 的圈的元素分为 n - 1 组,然后进行交换,下面以 4 * 4 的圈为例
我们将同组元素用相同的形状划分,在进行顺时针九十度旋转时,每个元素都根据箭头的指向进行元素覆盖
- 我们记这个圈的左上角点的行号为 sR ,右下角点的列号为 eC ,那么
- 第 i 组的第一个元素就可以表示为
martix[sR] [sR + i]
- 第 i 组的第二个元素就可以表示为
martix[sR + i] [eC]
(第 i 组的第二个元素全部都在最后一列) - 第 i 组的第三个元素就可以表示为
martix[eC] [eC - i]
- 第 i 组的第四个元素就可以表示为
martix[eC - i] [sR]
(第 i 组的第四个元素一定在第一列)
- 如何交换元素,以上面的外圈矩阵为例
- 先使用一个变量保存第 i 组第一个元素的值
- 然后用第四个元素的值覆盖第一个元素的位置
- 用第三个元素的值覆盖第四个元素的位置
- 用第二个元素的值覆盖第三个元素的位置
- 最后用临时变量中第一个元素的值覆盖第二个元素的位置
7.3、解题代码
1 | public void rotate(int[][] matrix) { |
8、分割链表
8.1、题目描述
给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。
你不需要 保留 每个分区中各节点的初始相对位置。
8.2、解题思路
- 使用快慢指针遍历链表,当快指针指向的节点的 val 大于等于 x 时,此时慢指针不动,而快指针右移
- 当快指针指向的节点的 val 小于 x 时,此时交换慢指针指向节点的值和快指针指向的节点的值,然后快慢指针一起后移
8.3、解题代码
1 | public ListNode partition(ListNode head, int x) { |
9、回文链表
9.1、题目描述
编写一个函数,检查输入的链表是否是回文的。
9.2、解题思路
- 使用快慢指针找到链表的中间结点,然后将中间结点之后的部分反转
- 比较链表前半部分和后半部分是否相同,如果相同,则证明是回文链表
9.3、解题代码
1 | public boolean isPalindrome(ListNode head) { |
10、最小栈
10.1、题目描述
请设计一个栈,除了常规栈支持的
pop
与push
函数以外,还支持min
函数,该函数返回栈元素中的最小值。**执行push、pop和min操作的时间复杂度必须为O(1)**。
10.2、解题思路
- 定义一个 Node 类,这个类包含两个属性
- value:结点的值
- min:这个 Node 对象为栈顶元素时,当前栈的最小值
- push 方法
当我们将一个值 x 压入栈中时,判断这个 x 与栈顶元素的关系
- 如果 x 小于当前栈顶元素的值,那么构造出一个 Node 对象压入栈中,这个 Node 对象的 value 为 x ,min 为 x
- 如果 x 大于等于当前栈顶元素的值,那么构造一个 Node 对象压入栈中,这个 Node 对象的 value 为 x ,min 为
stack.peek().min
(栈顶元素的 min 值)
- pop、peek 方法
返回栈顶 Node 对象的 value 值即可
- getMin() 方法
返回栈顶 Node 对象 的 min 值即可
10.3、解题代码
- Node 类
1 | class Node { |
- MinStack 实现
1 | public class MinStack { |
11、Sqrt(x)
11.1、题目描述
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5)
11.2、解题思路
- 使用二分法来解决这个问题,假设我们要求 104 的算术平方根
- 104 的算术平方根一定处于 [1,104] 之间,先求出 (1 + 104) / 2 的值,向下取整后得到 52
- 计算 52 * 52 的值,由于 52 * 52 的值远远大于 104 ,故 104 的算术平方根一定不会落在 [52, 104] 之间
- 所以我们从 [1,51] 中继续二分查找 104 的算术平方根
- 按照上面的步骤,我们可以一步步地缩减查找的范围
查找 1 - 51 之间的中位数,发现 26 * 26 的值大于 104 ,故我们缩减范围,在 [1,25] 之间查找
查找 1 - 25 之间的中位数,由于 13 * 13 的值大于 104 ,所以我们在 [1,12] 之间查找
在 1 - 12 之间查找,由于 6 * 6 的值小于 104 ,所以我们在 7 - 12 之间查找
在 7 - 12 之间查找,由于 9 * 9 的值小于 104 ,所以我们在 10 - 12 之间查找
在 10 - 12 之间查找,由于 11 * 11 的值大于 104 ,所以我们得到结果为 10
11.3、解题代码
1 | public int mySqrt(int x) { |
12、最小覆盖子串
12.1、题目描述
给你一个字符串
s
、一个字符串t
。返回s
中涵盖t
所有字符的最小子串。如果
s
中不存在涵盖t
所有字符的子串,则返回空字符串""
。
示例 1:
1
输入:s = "sabsbbcca", t = "aabbc"
12.2、解题思路
只有当 s 长度大于等于 t 长度时,才需要进行讨论
创建一张“欠债表”,这张表中初始化为 t 字符串中出现的字符及其出现次数,并使用一个变量
all
记录欠债表中所有字符的个数,初始化为 t 字符串的长度遍历 s 字符串,根据遍历到的内容对欠债表中内容和
all
变量中的值进行修改以示例一为例
- 初始化一张欠债表,欠债表中的内容为字符串 t 中出现的字符及其出现次数,即 {“a”:2,”b”:2 ,”c”:1},并初始化
all
为 t 字符串的长度 5 - 初始化两个指针,一开始均指向下标为 0 的元素,一开始,保持 left 不动,而 right 向右移动。根据遍历到的字符串的字符来修改欠债表和 all 的值
当遍历到 ‘s’ 字符时,由于 t 字符串中没有对应的字符,所以在欠债表中添加一条记录 s:-1 ,不修改 all ;
遍历到 ‘a’ 字符时,由于 t 字符串中含有 a ,所以修改欠债表和 all 的值,将欠债表中 a 记录对应的值 - 1,然后如果此时 a 记录对应的值不小于 0 ,那么我们称这次修改为 有效修改,所以我们将 all 的值 -1
遍历到 ‘b’ 时,此时修改欠债表和 all 的值;
遍历到 ‘s’ 时,修改欠债表中 s 对应的值为 -2 ,不修改 all
遍历到第二个 ‘b’ 时,修改欠债表和 all 的值,在遍历到第三个 ‘b’ 时,由于修改欠债表后,b 对应的值变为 -1 ,所以此时不是有效修改,不修改 all 的值
遍历到第二个 a 后,此时 all 的值已经变为 0 ,同时 right 来到第二个 ‘a’ 下。
- 当
all
变为 0 时,我们就找到了题目的一个解 [left ,right ],此时我们保持 right 不动,让 left 后移,如果 left 后移后不会使 s 字符串变为“欠款”状态(欠债表中有记录值大于 0)
如果不会,那么让 left 继续后移
如果会,那么让 right 继续往后移,直到重新恢复未欠款状态
- 这样我们就可以得到所有的答案,之后在所有的答案中选出一个最短的即可。
12.3、解题代码
1 | public String minWindow(String s, String t) { |
13、汉诺塔问题
13.1、题目描述
在经典汉诺塔问题中,有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时受到以下限制:
- 每次只能移动一个盘子;
- 盘子只能从柱子顶端滑出移到下一根柱子;
- 盘子只能叠在比它大的盘子上。
请编写程序,将所有盘子从第一根柱子移到最后一根柱子。
13.2、解题思路
- 使用递归来解决这个问题,我们需要将 A 中的 A.size() 个盘子从 source 柱(A 集合所表示的柱子)移动到 target 柱(C 集合所表示的柱子)
- 先将
A.size() - 1
个从 source 柱借助 target 柱移动到 help 柱,然后此时 source 柱上只剩下一个柱子,此时直接将 source 柱子上的最后一个盘子直接从 source 柱放到 target 柱上即可。 - 将存放了
A.size() - 1
个柱子的 help 柱看为新的 source 柱,重复以上步骤,借助 target 柱将A.size() - 2
个盘子放到原来的 source 柱,然后将第A.size() - 1
个盘子放到 target 柱… - 重复以上步骤,问题就得到了解决。
13.3、解题代码
1 | public void hanota(List<Integer> A, List<Integer> B, List<Integer> C) { |
14、二叉树的锯齿形层序遍历
14.1、题目描述
给定一个二叉树,返回其节点值的锯齿形层序遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
- 示例
返回:
[
[a],
[c,b],
[d,e,f],
[j,i,h,g]
]
14.2、解题思路
- 使用一个双端队列来解决这道题
在将一层节点全部加入队列之后,我们需要记录一个 size ,来让我们知道什么时候该更改遍历的顺序
- 当节点从左往右遍历时,我们将节点从队列尾部放入,从队列头部弹出
当一个节点从队列头部弹出时,我们先将该节点的左子树加入队列,然后将它的右孩子加入队列,注意,此时需要将子节点从尾部放入
- 节点从右往左遍历时,我们将节点从队列头部放入,从队列尾部弹出
当一个节点从队列尾部弹出时,我们先将该节点的右子树加入队列,然后将其左子树加入队列,注意,此时需要将子节点从头部放入
- 假设在一个双端队列中存放着 4 个元素,它们是二叉树中某一层的所有元素,此时的方向是从左往右遍历,同时它们也存在子节点,情况如下
- 确定 size 为 4
- 由于是从左往右遍历,所以需要将节点从 头 弹出,同时先加左节点再加右节点,需要从尾部加入
将 a 弹出,同时将 L 节点从尾巴加入到队列中,size – ,此时队列中元素从左到右依次为:
b c d L
将 b 弹出,同时将 M N 依次从尾部加入到队列中,size – ,此时队列元素从左到右依次为:
c d L M N
当 c、d 依次弹出后,队列中的元素变为
L M N O P Y Z
- 当 c、d 弹出后,由于 size == 0 ,所以表示当前层已经遍历完毕,我们需要调换遍历顺序,此时元素应该从尾部弹出,同时添加子节点时先添加右节点,后加左节点,子节点从头部加入。
此时弹出的顺序为
Z Y P O N M L
14.3、解题代码
1 | public List<List<Integer>> zigzagLevelOrder(TreeNode root) { |
15、填充每个节点的下一个右侧节点指针
15.1、题目描述
给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
1 | struct Node { |
- 填充它的每个 next 指针,让这个指针指向其下一个右侧节点。
- 如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
- 初始状态下,所有 next 指针都被设置为 NULL。
15.2、解题思路
- 可以利用一个队列,按照层序遍历的原理将遍历到的每一个节点都连起来
- 创建一个单向的队列,这个单向队列中的元素为 Node 对象
Node 类定义如下
1 | class Node { |
- MyQueue 实现如下
1 | class MyQueue { |
- 以下面的树为例,先将根加入到队列中,此时队列中只有
a
此时队列不为空,将 a 节点弹出,然后如果它的左节点不为空,那么先将它的左节点加入到队列中;
如果右节点不为空,那么将它的右节点加入到队列中
弹出 a 后,因为它的左右节点均不为空,所以 b 和 c 会被加入到队列中,所以 b 的next 域会置为 c ,也就是说 b 的 next 已经填充为了 c ,此时队列中的值为
b c
- 此时队列不为空,那么会先弹出 b ,然后将 d、e 加入到队列中,此时 d 的 next 域被置为了 e ,当两个节点都被弹出后,队列中的值为
d -> e -> f -> g
15.3、解题代码
1 | public Node connect(Node root) { |
16、买卖股票的最佳时机 I
16.1、题目描述
给定一个数组
prices
,它的第i
个元素prices[i]
表示一支给定股票第 i 天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
- 示例一
1 | 输入:[7,1,5,3,6,4] |
- 示例二
1 | 输入:prices = [7,6,4,3,1] |
16.2、解题思路
- 如果拥有的股票必须在 i 时间段卖出,那么在
min{nums[0:i]}
时段买入利润最大 - 遍历数组,在每一个位置根据上面的规则求出在 i 位置能获取利润的最大值,然后在这些最大值中再求一个最大值,就得到了我们想要的答案
16.3、解题代码
1 | public int maxProfit(int[] prices) { |
17、买卖股票的最佳时机 II
17.1、题目描述
给定一个数组
prices
,其中prices[i]
是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
- 示例 1:
1 | 输入: prices = [7,1,5,3,6,4] |
- 示例 2:
1 | 输入: prices = [1,2,3,4,5] |
17.2、解题思路
- 对于所有 i ,如果
prices[i] > prices[i - 1]
,那么将prices[i] - prices[i - 1]
的值累加到 result 中
因为此时可以进行无数次交易,因为如果满足
prices[i] > prices[i - 1]
,那么证明在 i - 1 位置买入,在 i 位置卖出有利可图,所以我们只需要将每一个位置的利润都累加起来,就可以得到最终利润。
17.3、解题代码
1 | public int maxProfit(int[] prices) { |
18、买卖股票的最佳时机 III
18.1、题目描述
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
18.2、解题思路
- 在 i 位置卖出第二支股票,如何在 i 位置卖出第二支股票时,获得最大收益?
我们需要考虑两个条件,即在 [0,i - 1] 位置中卖出第一支股票时,获得最大利润,同时在卖出后以一个最有利的低价买进第二支股票。
- 在 [0, i - 1] 选择一个合适的卖出时机 x ,在 x 卖出第一支股票时,获得利润
a
- 在 [x + 1,i - 1] 中选择一个合适的买入时机以价格
b
买入第二支股票
我们要求
a
-b
最大
- 此时在 i 位置卖出第二支股票,能得到的全局收益就为上一步中获取的 Max(
a
-b
) 与当前位置股票价格的和
18.3、解题代码
1 | public int maxProfit(int[] prices) { |
19、二叉树的最大路径和
19.1、题目描述
路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其 最大路径和 。
- 示例一
1 | 输入:root = [1,2,3] |
- 示例二
1 | 输入:root = [-10,9,20,null,null,15,7] |
19.2、解题思路
对于一棵以 x 节点为头节点的树(x 不一定为整棵树的根节点,可以是某棵子树的根节点),它的最大路径和可能存在以下几种可能性
它的最大路径和与 x 节点无关,此时它的最大路径和是 x 节点内某一棵更小的子树的最大路径和
比如,在示例二中的树中,以
-10
为根节点的树它的最大路径和与-10
无关,而是它的一棵子树(以20
为根节点的子树)的最大路径和此时结果为该节点左子树最大路径和与右子树最大路径和中的最大值,即
maxPath(root) = Math.max {maxPath(root.left), maxPath(root.right)}
- 它的最大路径与 x 节点有关,此时yi x 为根节点的树的最大路径和有以下几种可能性
- x 节点的最大路径和只包括 x 节点的值
- x 节点的最大路径和包括 x 节点,我们记 x 节点的值为
a
,同时在以x.left
为出发点的前提下,设 x.left 的最大路径和b
,这个可能性下的值为a
+b
- x 节点的最大路径和包括 x 节点,我们记 x 节点的值为
c
,同时在以x.right
为出发点的前提下,设 x.right 的最大路径和d
,这个可能性下的值为c
+d
- 最后一种可能性,即上面两种可能的性的总和,此时最大路径和经过 x 节点同时与 x 的左右节点均关联。
- 根据递归套路,我们需要向子树要什么信息?
- 不要求从 x 出发时,子树 x 的最大路径和,在这个条件下,最大路径不必从 x 节点出发
- 要求从 x 出发时,子树 x 的最大路径和, 这个条件下,最大路径和中必须从 x 出发
比如在示例二的右子树中,如果要求从 20 出发,那么最大路径和为 20 + 15 ;如果不要求从 20 出发,那么最大路径和为 15 + 20 + 7
19.3、解题代码
- 创建一个类,用于封装左右子树返回的数据
1 | public static class Info { |
- 递归过程
1 | public int maxPathSum(TreeNode root) { |
20、验证回文串
20.1、题目描述
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
说明:本题中,我们将空字符串定义为有效的回文串。
- 示例一
1 | 输入: "A man, a plan, a canal: Panama" |
- 示例二
1 | 输入: "race a car" |
20.2、解题思路
- 使用双指针求解,左指针从下标
0
位置开始,右指针从下标s.length() - 1
位置开始 - 只有当左右指针指向的值为有效字符时,才进行比较,循环时,左指针向右移,右指针向左移
20.3、解题代码
- 方法一,判断字符为有效字母或者有效数字的方法
1 | public boolean isValidChar(char ch) { |
- 方法二,判断两个字符是否相等的方法
1 | public boolean equals(char ch1, char ch2) { |
- 判断字符串是否为回文串的方法
1 | public boolean isPalindrome(String s) { |
21、单词接龙
21.1、题目描述
字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列:
- 序列中第一个单词是 beginWord 。
- 序列中最后一个单词是 endWord 。
- 每次转换只能改变一个字母。
- 转换过程中的中间单词必须是字典 wordList 中的单词。
给你两个单词 beginWord 和 endWord 和一个字典 wordList ,找到从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0。
- 示例一
1 | 输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"] |
- 示例二
1 | 输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"] |
21.2、解题思路
- 我们以 startWord 为基准,构造一个字典,字典中存放与 startWord 只有一字之差的字符串,我们将这些字符串称为 startWord 的下一层
- 以 startWord 下一层的元素为基准,分别构造它们各自的下一层元素,然后逐层构建下去
- 当 endWord 在字典中首次出现时,我们就找到了一条最短路径
如何创建这个字典?我们假设所有的词都只有小写字母
- 先将所有字符串都放在一个 HashSet 对象中
- 遍历基准词中的每一个字符,对它的每一次字符都进行一次变化,然后判断这个变化后的词在不在 HashSet 对象中
假设当前基准词为
abc
,那么我们先对 a 进行变化,变为bbc
,cbc
…zbc
,然后每变化一次,判断变化后的词是否在 HashSet 中同理,将
abc
中的第二个字符进行变化,判断aac
…azc
是否在 HashSet 中,最后变化 c ,比较判断
- 在上一步变化判断的过程中,如果发现有变化后的字符串在 set 中(要求不和原始字符串相同),那么就将当前变化后的字符串加入到该基准词的下一层元素中。
- 同理,可以从 endWord 逐步向上推,在字典第一次出现 startWord 时,也找到了最短路径,所以我们可以这样进行优化
假设从 startWord 向下推时,发现它的字典中含有 n 个字符串,而从 endWord 向上推时,发现它的字典中含有 m 个字符串,而 n > m ,那么我们此时从 endWord 向上推,如果 n < m ,那么从 startWord 向下推。
每次都选择发散小的路走,可以少走一点弯路。
22、最长连续序列
22.1、题目描述
给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
- 示例 1:
1 | 输入:nums = [100,4,200,1,3,2] |
- 示例 2:
1 | 输入:nums = [0,3,7,2,5,8,4,6,0,1] |
22.2、解题思路
- 使用两张哈希表完成这道题,其中一张为头表(存放连续区间的头),一张为尾表(存放连续区间的尾)
以数组 {100,1,200,3,4,0,2} 为例
遍历到 100 时,由于 100 可以单独作为一个区间,所以在头表中插入键值对 100 - 1 ,表示以 100 作为连续区间头的区间当前只有一个数,同时在未表中插入键值对 100 - 1 ,表示以 100 作为连续区间尾的区间当前只有一个数
在尾表中查询有没有以 99 作为结尾的区间,发现没有,所以什么都不做;
在头表中查询有没有以 101 作为头部的区间,发现没有,所以什么都不做;
遍历到 1 ,同理在头尾表中插入 {1 - 1} , 此时发现尾表中没有以 0 结尾的区间、在头表中没有以 2 为开头的区间,所以什么都不做
同理,200、3也是这样,加入头尾表后不做修改
遍历到 4 ,此时将 4 - 1 作为键值对插入头尾表中,然后遍历头表,发现此时没有以 5 作为开头的数据,所以什么都不做,遍历尾表,发现确实有以 3 作为结尾的序列,且序列长度为 1 ,现在我们需要将 3 -> 3 序列变为 3 -> 4 序列
将以 3 作为结尾的记录从尾表中删除,将以 4 作为开头的记录从头表中删除,同时修改头表中的记录,将 3 开头的序列长度从 1 改为 2 ,即头表中元素变为 { 3- 2},同理尾表中记录也变为 {4 - 2}
- …
- 使用 HashSet 解决这个问题,先将给的数组所有元素加入到 HashSet 中
对于一个连续序列,它一定存在一个左边界,如果一个序列以 num 作为左边界,那么
num - 1
就不应该存在与 HashSet 中;因此,如果对于一个 num ,满足
num - 1
不在 HashSet 中,那么这个 num 就可以作为连续序列的左边界。
- 将数组中的所有元素加入到一个 HashSet 中
- 遍历数组,对于每一个元素 num ,进行判断,如果
num - 1
存在于 HashSet 中,那么 num 不可能作为连续序列的左边界,直接跳过遍历下一个即可;
num - 1
不存在于 HashSet 中,那么 num 会是一个左边界,我们再不断查找num + 1
、num + 2
… 是否存在于 HashSet 中,来看以num
作为左边界的连续序列有多长
- 遍历后,我们就知道了对于每一个可能的左边界,所括出的最长连续序列的长度,再在这些长度中取最大值即可
22.3、解题代码
1 | public int longestConsecutive(int[] nums) { |
23、单词拆分
23.1、题目描述
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
- 说明
- 拆分时可以重复使用字典中的单词。
- 你可以假设字典中没有重复的单词。
- 示例 1:
1 | 输入: s = "leetcode", wordDict = ["leet", "code"] |
- 示例 2:
1 | 输入: s = "applepenapple", wordDict = ["apple", "pen"] |
23.2、解题思路
- 查询要分解的 s 在字典表中能否找到一个前缀,以示例二为例,查询以
a
字符串作为前缀,发现字典表中没有a
这个字符串,所以不可以以a
作为前缀,同理ap
…appl
也不都可以作为前缀 - 查询 s 字符串是否能以
apple
作为前缀,此时发现apple
存在于字典中,所以可以以apple
作为前缀,此时问题转换为以求 s 中除去前缀之外的部分penapple
能否被拆分的问题 - 将字符串所有的前缀枚举出来,然后在 s 中一次剔除这些前缀,问题就转换为了剩下字符串的拆分问题,最后将这些前缀的可能性都加起来,就得到了 s 字符串的所有拆分情况,问题就得到了解决。
23.3、解题代码
- 优化前的代码
1 | public boolean wordBreak(String s, List<String> wordDict) { |
- 优化后的代码
- 将 List 换为 Set ,加快查重速度
- 将递归改为动态规划
在原来的递归参数中,只有一个参数是变化的,我们构造一个一维数组
dp[i]
,dp[i]
表示从下标 i 开始,字符串可以被wordDict
中的字符串分解的次数
1 | public boolean wordBreak(String s, List<String> wordDict) { |
24、寻找峰值
24.1、题目描述
峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞ 。
你必须实现时间复杂度为 O(log n) 的算法来解决此问题。
示例 1:
1
2
3输入:nums = [1,2,3,1]
输出:2
解释:3 是峰值元素,你的函数应该返回其索引 2。示例 2:
1
2
3输入:nums = [1,2,1,3,5,6,4]
输出:1 或 5
解释:你的函数可以返回索引 1,其峰值元素为 2;或者返回索引 5, 其峰值元素为 6。
24.2、解题思路
- 如果
nums[0] > nums[1]
,那么返回 0 ,因为nums[-1]
为负无穷 - 如果
nums[nums.length - 1] > nums[nums.length - 2]
,那么返回 0 ,因为nums[nums.length]
为负无穷 - 如果上面的条件均不满足,那么证明此时
nums[0] < nums[1]
且nums[nums.length - 2] > nums[nums.length - 1]
从 0 –> 1 ,呈现向上的趋势,而从倒数第二个到最后一个元素,呈下降的趋势,那么证明在数组中间必定存在一个拐点 M ,使得在 M 左边的元素小于 M 元素,而 M 右边的元素也小于 M 元素,这个 M 元素所在下标就是我们要找的值
- 使用二分查找,找到中间下标 mid 时,判断是否满足以下条件,即
nums[mid] > nums[mid - 1] && nums[mid] < nums[mid + 1]
,如果满足,直接返回 mid 即可 - 如果不满足以上条件,此时
nums[mid - 1] > nums[mid]
,那么由于在mid - 1
–> mid 之间呈现向下趋势,而 0 -> 1 呈现向上趋势,那么 1 –>mid - 1
之间一定存在一个拐点 - 如果
nums[mid] < nums[mid + 1]
,那么可以往右侧二分; - 如果两个条件均满足,那么往左右二分都可以
24.3、解题代码
1 | public int findPeakElement(int[] nums) { |
25、多数元素
25.1、题目描述
给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
25.2、解题思路
- 在这道题中,数组一定有一个数出现的次数大于 N / 2;
- 一次在数组中删掉两个不同的数,当数组删无可删时,剩下的数就是要求的数
- 使用一个 target 变量来表示靶子,用一个 hp 变量来表示靶子的血量,遍历数组
- 遍历到一个元素时,查看当前是否存在靶子,如果不存在,那么将当前的元素立为靶子,然后为这个靶子添加一点 hp
- 继续遍历,判断当前的元素是否和靶子相同,如果相同,那么让靶子血量 + 1
- 继续遍历,如果当前元素与靶子不同,那么将当前元素删除,同时将当前靶子的 hp - 1
- 最后剩下的数就是那个多数元素
25.3、解题代码
1 | public int majorityElement(int[] nums) { |
26、逆波兰表达式求值
26.1、题目描述
根据 逆波兰表示法,求表达式的值。
有效的算符包括
+
、-
、*
、/
。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
- 整数除法只保留整数部分。
- 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
- 示例一
1 | 输入:tokens = ["2","1","+","3","*"] |
26.2、解题思路
- 创建一个栈,然后遍历字符数组,如果遇到的是一个数字,那么直接压入栈中,如果是符号,那么从栈中弹出两个数,进行运算,然后将结果压入栈中,遍历完成后,最后还留在栈中的元素就是结果
对于减法和除法,需要用后弹出的数
减去/除以
先弹出的数
26.3、解题代码
1 | public int evalRPN(String[] tokens) { |