数据结构与算法学习(十六)
1、最长公共前缀
1.1、题目描述
编写一个函数来查找字符串数组中的最长公共前缀,如果不存在公共前缀,返回空字符串
""
。
1.2、解题思路
- 创建一个变量 result,这个变量存放结果,初始化为第一个字符串
- 用 result 去与剩下的字符串进行比较,截取它们公共的前缀
- 以此类推,在与最后一个字符串进行比较并截取后,result 就是最后的结果,如果在此过程中 result 的长度变为 0 ,那么证明没有最长公共前缀
1.3、解题代码
1 | public String longestCommonPrefix(String[] strs) { |
2、两数之和
2.1、题目描述
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
2.2、解题思路
- 使用哈希表完成
循环遍历数组,得到整数目标值
target
与当前遍历元素current
的差,记为 difference ,然后去哈希表中查找有没有关于 difference 的记录,哈希表中以值为键,以该值在数组中的下标为值
- 如果有,那么此时就找到了一个结果,将结果返回即可
- 如果没有,我们需要将当前值
current
作为键,以current
对应下标作为值,将这条记录放入哈希表中
- 使用双指针求解
对数组进行排序
初始化两个指针,左指针指向数组开头的元素,而右指针指向数组结尾元素,对此时指针指向的两个元素进行相加,查看得到的和 sum
如果 sum < target ,那么让左指针 left 右移
如果 sum > target ,那么让右指针 right 左移
如果 sum == target ,那么返回此时两个数在原来数组的下标即可。
2.3、解题代码
- 使用 哈希表求解
1 | public int[] twoSum(int[] nums, int target) { |
- 使用双指针进行求解
1 | public int[] twoSum(int[] nums, int target) { |
3、三数之和
3.1、题目描述
给你一个包含 n 个整数的数组
nums
,判断nums
中是否存在三个元素 a,b,c ,使得a + b + c = 0
?请你找出所有和为 0 且不重复的三元组。
3.2、解题思路
- 这个问题从两数之和演进而来,我们参考两数之和中双指针的解题思路,先对数组排序,然后让一根指针 P 指向数组最左侧
此时我们将 P 指针指向的元素作为三元组的第一个数,此时题目就可以转换为在剩下的元素中找和为
target - nums[P]
的两个数
在得到以 nums[P] 作为第一个元素的所有三元组后,让 P 后移,此时如果发现 nums[P] 与前一个数相等,那么直接跳过;
重复以上流程,当一次循环结束后,我们就找到了所有符合条件的三元组
在指针移动 P 移动到 i 时,我们只需要从剩下的 [i + 1,nums.length - 1] 中找二元组即可
这是因为当 P 在 [0, i] 时,已经将前面的答案都试过了
- 当指针 P 指向的元素大于 target 时,此时可以终止流程,因为上述流程是在有序数组中进行的,当 nums[P] 大于 target 时,后面的元素一定大于 target ,所以不可能存在和为
target - nums[P]
的二元组
3.3、解题代码
1 | public List<List<Integer>> threeSum(int[] nums) { |
4、电话号码的字母组合
4.1、题目描述
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
- 示例一
1 | 输入:digits = "23" |
- 示例二
1 | 输入:digits = "2" |
4.2、解题思路
- 先列出每个数字对应的字母列表
- 使用回溯算法,找出每个字母下对应的字符串数组,分别递归,查找出所有的可能性
比如说用户输入了 “23” ,那么先深度遍历 2 对应的字符数组,即 [‘a’,’b’,’c’] ,然后遍历 2 对应的字符数组中元素的过程中,又对 3 对应的字符数组进行遍历,组成所有结果并收集
4.3、解题代码
1 | public char[][] phoneCharArray = { |
5、有效的括号
5.1、题目描述
给定一个只包括
'('
,')'
,'{'
,'}'
,'['
,']'
的字符串s
,判断字符串是否有效。有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 示例一
1 | 输入:s = "{[]}" |
- 示例二
1 | 输入:s = "(]" |
- 示例三
1 | 输入:s = "([)]" |
5.2、解题思路
- 使用一个栈来解决这个问题
当遇到左括号类型的符号(包括小括号、中括号、大括号)时,直接压栈,遇到右括号类型的符号,就弹栈,需要此时弹出栈的左括号能与此时遇到的右括号能进行匹配,如果不匹配,那么直接返回 false ,如果要返回 true ,那么必须符合三个条件
- 栈为空
- 期间没有发生不匹配情况
- 遍历到字符串末尾
5.3、解题代码
1 | public boolean isValid(String s) { |
6、合并 K 个有序节点
6.1、题目描述
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
6.2、解题思路
使用小顶堆来解决这个问题,小顶堆的加入和弹出操作的时间复杂度都是 O(logN)
- 将每个链表的头节点放入小顶堆
- 弹出当前小根堆中的元素,将这个节点加入到结果链表中,检查这个元素是从哪个链表中弹出来的,然后将该链表的下一个节点加入到小顶堆中
重复以上操作,直到小根堆中没有元素,结果链表就将所有链表中的元素有序地结合起来了
6.3、解题代码
1 | public ListNode mergeKLists(ListNode[] lists) { |
7、接雨水
7.1、题目描述
给定
n
个非负整数表示每个宽度为1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
- 示例一
1 | 输入:height = [0,1,0,2,1,0,1,3,2,1,2,1] |
- 示例二
1 | 输入:height = [4,2,0,3,2,5] |
7.2、解题思路
第一个柱子和最后一个柱子上一定没有水
i 位置的柱子最多能接多少水?这个值由三个因素决定,我们设这个值为 water [i]
- 高度数组 height 从 [0, i - 1] 的最大值
- 高度数组 height 从 [i + 1, height.length - 1] 的最大值
- i 的最大值
- water[i] = max {0, min{ max{height[0,i - 1]},max{height[i + 1,height.length - 1] }}}
min{ max{height[0,i - 1]},max{height[i + 1,height.length - 1] }}
可以这样理解,如果左边的高度最大值为 10 ,而右边高度最大值为 20 ,那么如果 i 位置有水,则水 + 柱子的高度一定小于等于左边的 10 (超过 10 水会从左边溜出去)max {0, ...}
可以这样理解,如果 i 位置柱子的高度大于左右边界的值,由于水的量是min{ max{height[0,i - 1]},max{height[i + 1,height.length - 1] }}
- height[i] ,此时得到的水会是一个负数,所以当 i 位置的柱子高度大于左右边界值时,直接将当前柱子的水置为 0
从上面的分析中,我们需要知道数组中下标从 [0,i - 1] 的柱子高度最大值,同时知道数组中下标从 [i + 1,height.length - 1] 的柱子高度最大值。
使用双指针,初始化左指针 L 指向下标为 1 的数,初始化右指针 R 指向下标为 height.length - 2 的数(第一个柱子和最后一个柱子没有水)
记 L 左边边界最大值为 leftMax ,记 R 右边界的最大值为 rightMax ,则当
- leftMax < rightMax 时,结算 L 位置的水量
这是因为 L 位置的左边界最大值是确定的,而 L 的右边界的最大值虽然是不确定的,但是 L 右边界的最大值不会小于 rightMax ,此时可以确定 L 位置的水
- leftMax > rightMax 时,结算 R 位置的水量
同理,这是因为 R 位置的右边界的最大值已经确定,而 R 位置的左边界的最大值虽然是不确定的,但是 R 的左边界的最大值不会小于 leftMax ,此时可以确定 R 位置的水
- leftMax == rightMax 时,此时可以结算 L 和 R 两个位置的值
- 左指针右移,右指针左移,当 L == R 时,循环结束,此时得到了所有位置上的水
7.3、解题代码
1 | public int trap(int[] height) { |
8、全排列
8.1、题目描述
给定一个不含重复数字的数组
nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
- 示例 1:
1 | 输入:nums = [1,2,3] |
8.2、解题思路
这道题使用到了回溯算法,假设给定数组长度为 3 ,先确定第一个位置的数,然后在剩下的数中确定第二个位置的数,然后剩下的数就是第三个数,然后回过头来修改第二个位置的数,将第三个数与第二个数进行交换,获取到两个结果,后面再修改第一个数的值…
9、跳跃游戏
9.1、题目描述
给定一个非负整数数组
nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度,判断你是否能够到达最后一个下标。
示例 1:
1
2
3输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。示例 2:
1
2
3输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
9.2、解题思路
- 初始化一个变量 max 为 nums[0] ,这个变量记录了当前下标 i 能能够到达的最大位置
- 循环遍历数组,在进行遍历时需要判断当前能否到达 i 位置,如果 i > max 的值,那么说明此时无法到达 i 位置,直接返回 false 即可
比如说数组中第一个元素的值为 0 ,那么由于它无法到达下标为 1 的位置,所以直接返回 false 即可
- 在遍历时,如果发现在当前位置能到达的位置 > max 的位置,那么更新 max
假设现在有一个数组为 {3,1,1,1,0,0,0,1} ,那么初始化 max 为数组的第一个元素的值,即 3 ,代表在 0 位置最大能跳到下标为 3 的位置,当遍历到 i = 1 的元素时,发现此时在 i = 1 位置能到达的最大下标 positionI 为 2 ,此时由于 positionI <= max ,所以不更新
当 i = 2 时, positionI 为 3 ,此时仍然不更新,当 i = 3 时, positionI 为 4 ,此时更新 max 为 4 ,当 i = 4 时,此时 positionI 为 4 ,不更新 max ,当 i = 5时,由于 i > max ,此时证明无法到达 i == 5 的位置,此时直接返回 false 即可。
9.3、解题代码
1 | public boolean canJump(int[] nums) { |
10、跳跃游戏 II
10.1、题目描述
给你一个非负整数数组
nums
,你最初位于数组的第一个位置,数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置,假设你总是可以到达数组的最后一个位置。
说明:你总是能到达数组的最后一个位置。
- 示例一:
1 | 输入: nums = [2,3,1,1,4] |
- 示例二:
1 | 输入: nums = [2,3,0,1,4] |
10.2、解题思路
与上一题不同,这道题的小人总是能到达终点,但本题要求小人在每次跳跃中做出选择,这个选择能让他到达终点所用的总次数最少,下面以数组 {2,3,1,1,4,3,2,5} 为例
- 小人永远从下标为 0 的位置起跳,此时我们标出在下标为 0 的位置可以到达的下标标记出来
小人在下标为 0 处能到达的下标范围为 [0, nums[0]],然后我们遍历小人能到达的下标范围,更新小人下一步能到达的最大位置,小人下一步能到达的最大位置 newMax 的值为
max(i + nums[i])
,在上面的例子中,第一步小人能到达的范围是 [0,2]
- 在下标为 0 - 2 的范围内,newMax 的值会被更新为 4 (1 + 3),表示小人下一步能远能到达下标为 4 的位置
- 同理,查看在第三步能到达的最远位置,此时我们找到下标为 4 的位置,更新下一步能到达的最远位置为 8 (4 + 4)
由于此时 8 已经超过了数组的最后一个下标 7 ,所以我们直接返回 3
10.3、解题代码
1 | public int jump(int[] nums) { |
11、最长同值路径
11.1、题目描述
给定一个二叉树,找到最长的路径,这个路径中的每个节点具有相同值。
这条路径可以经过也可以不经过根节点,两个节点之间的路径长度由它们之间的边数表示
- 示例一:
输入:
1 | 5 |
输出:2
- 示例二:
输入:
1 | 1 |
输出:2
11.2、解题思路
- 使用二叉树的递归来解决这个问题,
路径长度 = 路径上的节点数 - 1
,所以这个问题可以等价于求路径上的节点数 - 以 root 为根节点的树,它的最长同值路径可能与 root 无关(路径不经过 root 节点,比如示例二)。
此时以 root 为根节点的树,它的最长同值路径要么是它左子树的最长同值路径,要么是它右子树的最长同值路径
- 以 root 为根节点的树,它的最长同值路径可能与 root 有关(路径经过 root)
- 当以 root 为根节点的树,它的最长同值路径长度只包括 root 时,此时结果为 1 ,这种情况下, root 和 root 左右节点的值均不相等。
- 当以 root 为根节点的树,它的最长同值路径只包括 root 和 root 的左/右子树时,此时只需要求出以 root 的左/右子节点的最长通知路径再 + 1 即可
这种情况下,root 的值需要和 root 的左/右子节点的值相等
- 当以 root 为根节点的树,它的最长同值路径既包括 root 的左子树,也包含 root 的右子树时,这个时候的结果为 root 左子树的最长同值路径 + root 右子树的最长同值路径 + 1
这种情况下,root 、root.left 和 root.right 三者的值必须相等。
- 上面六种情况中的最大值,就是我们要求的值
11.3、解题代码
- 包装一个静态内部类 Info ,它封装了左右子树需要向根节点返回的信息
- 路径必须包含 x 时,同值路径的最大距离,为属性 len
在最长同值路径包含 root 节点时,我们需要的属性为 len 属性,因为这个时候要求 root 子节点的值也必须为 root.val
- 路径不要求包含 x 时,同值路径的最大距离,为属性 max
当最长同值路径不包含 root 时,我们需要的属性为 max 属性,因为这个时候不要求 root 子节点的值为 root.val
1 | private static class Info { |
- 完整解题代码如下
1 | public int longestUnivaluePath(TreeNode root) { |
12、字母异位词分组
12.1、题目描述
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母都恰好只用一次。
- 如果两个字符串中的字符一致且字符的出现次数一致,那么我们称这两个字符串为同形词,这道题要求我们将同形词放在一个 List 中
- “abc”,”bca” 两个词互为同形词,而 “aabc” 和 “abc” 则不算。
12.2、解题思路
- 如何判断两个字符串是否为同形词?
将两个字符串排完序后使用 equals 判断即可
- 创建一个哈希表,**其中将排完序后的字符串作为 key **,将存放该种类同形词的 List 作为 value
循环遍历字符数组,对每个字符串的字符数组进行排序,然后将排好序的字符数组构造成字符串,将这个构造出来的字符串作为 key ,到哈希表中判断这个 key 是否存在,如果不存在,那么需要创建一个全新的列表,将当前遍历的字符串加入到列表中,然后将列表放回到哈希表中;如果存在,根据 key 拿出对应的列表,将当前遍历的字符串加入到列表中就可以了。
12.3、解题代码
1 | public List<List<String>> groupAnagrams(String[] strs) { |
13、Pow(x, n)
13.1、题目描述
实现 pow(x, n) ,即计算 x 的 n 次幂函数(即,xn),其中 x 为一个 Double 类型的数,n 是一个整数
13.2、解题思路
- 如何快速计算 xn ?我们将 n 拆为二进制形式
假设我们现在要计算 a75 ,75 = (1001011)2 ,用一个变量来保存 a1 ,记这个数为 t ,让 t 与自己相乘,将得到的数记为 t1 ,t2 = a2 ,同理 t3 = a4、t3 = a8…t6 = a64,然后我们只需要将 1001011 中为 1 的那些数对应的值乘起来即可,即 a75 = a64 * a8 * a2 * a1 = t6 * t3 * t1 * t
- 使用右移和 & 运算来简化运算
- 将次方数绝对值 pow 与 0 进行 & 运算,如果结果为 0 ,证明 pow 二进制形式的最后一位为 0 ,如果结果为 1 ,那么证明 pow 二进制形式的最后一位为 1
- 在每次进行 & 运算后,将 pow 右移一位,这样做既可以来控制循环次数(当 pow 为 0 时,循环结束),又可以遍历到 pow 二进制形式中的任何一个数
13.3、解题代码
1 | public double myPow(double x, int n) { |