目录
剑指 Offer II 003. 前 n 个数字二进制中 1 的个数
剑指 Offer II 008. 和大于等于 target 的最短子数组
剑指 Offer II 011. 0 和 1 个数相同的子数组
剑指 Offer II 016. 不含重复字符的最长子字符串
剑指 Offer II 021. 删除链表的倒数第 n 个结点
剑指 Offer II 030. 插入、删除和随机访问都是 O(1) 的容器
剑指 Offer II 049. 从根节点到叶节点的路径数字之和
剑指 Offer II 057. 值和下标之差都在给定的范围内
剑指 Offer II 060. 出现频率最高的 k 个数字
剑指 Offer II 070. 排序数组中只出现一次的数字
剑指 Offer II 001. 整数除法
难度:简单
给定两个整数 a
和 b
,求它们的除法的商 a/b
,要求不得使用乘号 '*'
、除号 '/'
以及求余符号 '%'
。
注意:
- 整数除法的结果应当截去(
truncate
)其小数部分,例如:truncate(8.345) = 8
以及truncate(-2.7335) = -2
- 假设我们的环境只能存储 32 位有符号整数,其数值范围是
[−231, 231−1]
。本题中,如果除法结果溢出,则返回231 − 1
解题思路:
- 假设 a = 64,b = 3
- 不能用除,那就只能用减法,看减去了几个3,但是常规减起来太慢,a可能很大
- 这里引入快速减法:通过给3左移,来逐步增大b
- 64 - 3 >= 3
- 64 - 6 (3 << 1) >= 3
- 64 - 12 (3 << 2) >= 3
- 64 - 24 (3 << 3) >= 3
- 64 - 48 (3 << 4) >= 3
- 64 - 96 (3 << 5) < 3 (跳出)
- 需要左移四次,能达到最接近64,1左移的次数就是除的次数, 1<<4 = 16
- 64 - 48 剩余16
- 16 - 3 >= 3
- 16 - 6 (3 << 1) >= 3
- 16 - 12 (3 << 2) >= 3
- 16 - 24 (3 << 3) < 3 (跳出)
- 需要左移两次,能达到最接近16,1左移的次数就是除的次数,1<<2 = 4
- 16 - 12 剩余 4
- 4 - 3 < 3(跳出)
- 需要左移零次,能达到最接近4,1左移的次数就是除的次数,1<<0 = 1
- 最终 16 + 4 + 1 = 21
class Solution {
public int divide(int a, int b) {
if (a == Integer.MIN_VALUE && b == -1)return Integer.MAX_VALUE;
if (a == 0)return 0;
if (b == 1)return a;
boolean sign = (a^b) >= 0;
if (a > 0)a = -a;
if (b > 0)b = -b;
int res = 0;
// 快速相减
while (a <= b){
int sum = 1;
int divisor = b;
// a和b都为负数,所以这里是小于等于
while (a - divisor <= divisor){
divisor <<= 1;
sum <<= 1;
}
res += sum;
a -= divisor;
}
return sign ? res : -res;
}
}
剑指 Offer II 002. 二进制加法
难度:简单
给定两个 01 字符串 a
和 b
,请计算它们的和,并以二进制字符串的形式输出。
输入为 非空 字符串且只包含数字 1
和 0
。
解题思路:
- 从后往前计算,每次计算都要带着三个参数,进位、当前a、当前b
- 循环结束的标志就是没有进位,a和b也遍历完了
- 每次都是a、b当前位置,还有进位 & 的结果,下个进位就看当前数相加有没有超过1
class Solution {
public String addBinary(String a, String b) {
int a_index = a.length()-1;
int b_index = b.length()-1;
int jinwei = 0;
StringBuffer str = new StringBuffer();
while (a_index >= 0 || b_index >= 0 || jinwei == 1){
int a_curr = 0;
int b_curr = 0;
if (a_index >= 0){
a_curr = Integer.parseInt(a.charAt(a_index)+"");
a_index--;
}
if(b_index >= 0){
b_curr = Integer.parseInt(b.charAt(b_index)+"");
b_index--;
}
str.insert(0,a_curr^b_curr^jinwei);
jinwei = a_curr+b_curr+jinwei > 1 ? 1 : 0;
}
return str.toString();
}
}
剑指 Offer II 003. 前 n 个数字二进制中 1 的个数
难度:简单
给定一个非负整数 n
,请计算 0
到 n
之间的每个数字的二进制表示中 1 的个数,并输出一个数组。
解题思路:
- 直接算:每个数,都右移计算下个数
- 动态规划:当遇到一个数,原来是逐步右移计算个数。但是当右移一次发现,这个数肯定比当前数来的小,且之前肯定计算过,唯一不知道的就是右移一次移掉的最右边的数是0是1,所以只要计算下最右 (1 & i),再加上之前计算过的值的结果,就是最终结果
class Solution {
public int[] countBits(int n) {
int[] res = new int[n+1];
for (int i=1;i<=n;i++){
int j = i;
while (j != 0){
res[i] += j&1;
j>>=1;
}
}
return res;
}
}
class Solution {
public int[] countBits(int n) {
int[] dp = new int[n+1];
for (int i=1;i<=n;i++){
dp[i] = dp[i>>1] + (i&1);
}
return dp;
}
}
剑指 Offer II 004. 只出现一次的数字
难度:中等
给你一个整数数组 nums
,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。
解题思路:
- 排序过后,只出现一次的数字就只有三种情况,一头一尾一中间
- 所以每次判断头和尾相同不相同,不相同的话前面那个就是答案,相同就+3后继续判断,如果都没有出现,说明答案就在最后一位
class Solution {
public int singleNumber(int[] nums) {
// 1 2 2 2
// 2 2 2 3
// 2 2 2 3 4 4 4
// 三种情况
Arrays.sort(nums);
if (nums.length == 1)return nums[0];
for (int i=2;i<nums.length;i++){
if (nums[i] != nums[i-2])return nums[i-2];
else i+=2;
}
return nums[nums.length-1];
}
}
剑指 Offer II 005. 单词长度的最大乘积
难度:中等
给定一个字符串数组 words
,请计算当两个字符串 words[i]
和 words[j]
不包含相同字符时,它们长度的乘积的最大值。假设字符串中只包含英语的小写字母。如果没有不包含相同字符的一对字符串,返回 0。
解题思路:
- 循环比较,每个字符串都要和其他字符串比较一遍
- 比较两个字符串是否包含相同的字母:将一个字符串先转换成数组,对应字母对应位置就+1,遍历第二个字符串看该位置上值是否为0,为0则表示没有出现过
class Solution {
public int maxProduct(String[] words) {
int res = 0;
for (int i=0;i<words.length;i++){
for (int j=i+1;j<words.length;j++){
if (compare(words[i],words[j]))res = Math.max(res,(words[i].length())*(words[j].length()));
}
}
return res;
}
public boolean compare(String a,String b){
int[] n = new int[26];
for (int i=0;i<a.length();i++){
n[a.charAt(i)-'a']++;
}
for (int i=0;i<b.length();i++){
if (n[b.charAt(i)-'a'] != 0)return false;
}
return true;
}
}
剑指 Offer II 006. 排序数组中两个数字之和
难度:简单
给定一个已按照 升序排列 的整数数组 numbers
,请你从数组中找出两个数满足相加之和等于目标数 target
。
函数应该以长度为 2
的整数数组的形式返回这两个数的下标值。numbers
的下标 从 0 开始计数 ,所以答案数组应当满足 0 <= answer[0] < answer[1] < numbers.length
。
假设数组中存在且只存在一对符合条件的数字,同时一个数字不能使用两次。
解题思路:
- 题目已经告诉你是排序数组,所以利用这一点,双指针,从两端往里靠
- 如果当前两指针和大于target,右指针得左移。因为左指针右移,值只会更大
- 如果当前两指针和小于target,左指针得右移。因为右指针左移,值只会更小
class Solution {
public int[] twoSum(int[] numbers, int target) {
int left = 0;
int right = numbers.length-1;
while (left < right){
int sum = numbers[left] + numbers[right];
if (sum == target)return new int[]{left,right};
if (sum > target)right--;
if (sum < target)left++;
}
return new int[0];
}
}
剑指 Offer II 007. 数组中和为 0 的三个数
难度:中等
给定一个包含 n
个整数的数组 nums
,判断 nums
中是否存在三个元素 a
,b
,c
,使得 a + b + c = 0
?请找出所有和为 0
且 不重复 的三元组。
解题思路:
- 两数之和的变种
- 依旧是先排序,所谓三数之和可以当成两数之和做,target就是第三个数的相反数
- 这里因为不允许重复值,所以最外层选第三个数的时候需要去重复。同时在里面选取两个值的时候,利用双指针向里收缩查找,也需要去重
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList();
Arrays.sort(nums);
for (int i=0;i<nums.length;i++){
List<List<Integer>> resList = new ArrayList(find(nums,-nums[i],i+1));
if (resList.size() > 0){
for (List<Integer> list : resList){
list.add(nums[i]);
res.add(list);
}
}
while (i < nums.length-1 && nums[i] == nums[i+1]){
i++;
}
}
return res;
}
public List<List<Integer>> find(int[] nums,int target,int start){
List<List<Integer>> list = new ArrayList();
int left = start;
int right = nums.length-1;
while (left < right){
int leftNum = nums[left];
int rightNum = nums[right];
int sum = leftNum + rightNum;
if (sum == target){
List<Integer> l = new ArrayList();
l.add(leftNum);
l.add(rightNum);
list.add(l);
while (left < right && nums[left] == leftNum){
left++;
}
while (left < right && nums[right] == rightNum){
right--;
}
}else if (sum > target){
while (left < right && nums[right] == rightNum){
right--;
}
}else if (sum < target){
while (left < right && nums[left] == leftNum){
left++;
}
}
}
return list;
}
}
剑指 Offer II 008. 和大于等于 target 的最短子数组
难度:中等
给定一个含有 n
个正整数的数组和一个正整数 target
。
找出该数组中满足其和 ≥ target
的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr]
,并返回其长度。如果不存在符合条件的子数组,返回 0
。
解题思路:
- 滑动窗口:先右边界右移动,直到遇到大于target,开始左边界持续右移
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int left = 0;
int right = 0;
int sum = 0;
int res = Integer.MAX_VALUE;
while (right < nums.length){
sum += nums[right];
// 当超出target的时候,需要不断缩小左边界,直到小于
while (sum >= target){
res = Math.min(res,right-left+1);
sum -= nums[left];
left++;
}
right++;
}
return res == Integer.MAX_VALUE ? 0 : res;
}
}
剑指 Offer II 009. 乘积小于 K 的子数组
难度:中等
给定一个正整数数组 nums
和整数 k
,请找出该数组内乘积小于 k
的连续的子数组的个数。
解题思路:
- 滑动窗口:右边界不断右移动,同时调整左边界
- 当发现大于等于k的时候,移动左边界,直到重合或者小于k,对应的乘积也得移除
- 最后统计下窗口内子数组数量
- 至于 res += right - left + 1
- 举例:[ 2、3、4]
- left = 0, right = 0,[ 2 ],res = 1
- left = 0, right = 1,[ 2、3 ],增加一个数就多两个,res = 1 + 2 = 3
- left = 0, right = 2,[ 2、3、4 ],再加一个数,包含前面 res = 1 + 2 + 3 = 6
- 即以新加的数来看,增加了 [ 2、3、4 ]、[ 3、4 ]、[ 4 ],即长度最大为多少,从最大开始算一个,每减少一个长度算一个,所以就是长度是多少算多少个,就是 right - left + 1
class Solution {
public int numSubarrayProductLessThanK(int[] nums, int k) {
if (k == 0)return 0;
int res = 0;
int left = 0;
int mul = 1;
for (int right=0;right<nums.length;right++){
mul *= nums[right];
while (left <= right && mul >= k){
mul/=nums[left];
left++;
}
res += right-left+1;
}
return res;
}
}
剑指 Offer II 010. 和为 k 的子数组
难度:中等
给定一个整数数组和一个整数 k
,请找到该数组中和为 k
的连续子数组的个数。
解题思路:
- 暴力:右边界扩大的同时,看左边界的情况
- 前缀和:前缀和的结果用 map 保存。找到之前是否出现过当前sum和之前sum差值为 k ,即 sum - k。这里注意 map 保存 sum 的时候,要加原先基础上+1
class Solution {
public int subarraySum(int[] nums, int k) {
int res = 0;
for (int right=0;right<nums.length;right++){
int sum = 0;
for (int left=right;left>=0;left--){
sum += nums[left];
if (sum == k){
res++;
}
}
}
return res;
}
}
class Solution {
public int subarraySum(int[] nums, int k) {
HashMap<Integer,Integer> map = new HashMap();
map.put(0,1);
int res = 0;
int sum = 0;
for (int i=0;i<nums.length;i++){
sum += nums[i];
if (map.containsKey(sum-k)){
res += map.get(sum-k);
}
map.put(sum,map.getOrDefault(sum,0)+1);
}
return res;
}
}
剑指 Offer II 011. 0 和 1 个数相同的子数组
难度:中等
给定一个二进制数组 nums
, 找到含有相同数量的 0
和 1
的最长连续子数组,并返回该子数组的长度。
解题思路:
- 将0转换成-1,这样当求0和1数量相同,就变相的求-1和1数量相同,也就是相加为0
- 前缀和的思想,每个不同的sum都只记录第一次出现的位置,往后如果出现相同的sum,即说明这之间的值累加为0,也就是所求的连续子数组
- 如果当前前缀和为0,则表示从开头到目前,是其中一个连续子数组
class Solution {
public int findMaxLength(int[] nums) {
HashMap<Integer,Integer> map = new HashMap();
int sum = 0;
int res = 0;
// 用前缀和来计算,将0替换成-1
// 0和1的数量相同,即-1和1的数量相同,累加为0
for (int i=0;i<nums.length;i++){
if (nums[i] == 0)nums[i]=-1;
sum += nums[i];
if (sum == 0)res = Math.max(i+1,res);
else if (map.containsKey(sum)){
res = Math.max(i-map.get(sum),res);
}else{
map.put(sum,i);
}
}
return res;
}
}
剑指 Offer II 012. 左右两边子数组的和相等
难度:简单
给你一个整数数组 nums
,请计算数组的 中心下标 。
数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。
如果中心下标位于数组最左端,那么左侧数之和视为 0
,因为在下标的左侧不存在元素。这一点对于中心下标位于数组最右端同样适用。
如果数组有多个中心下标,应该返回 最靠近左边 的那一个。如果数组不存在中心下标,返回 -1
。
解题思路:
- 遍历一遍求出总和 sum
- 再遍历一遍,遍历的同时计算前缀和,最终要计算的就是该值的左边和右边相等,也就是 左边前缀和的2倍 + 当前值 = sum,所以遍历的同时就计算,如果发现等式成立就说明找到了最后的值,如果没有发现则返回 -1
class Solution {
public int pivotIndex(int[] nums) {
int sum = 0;
for (int n : nums){
sum += n;
}
int pre_sum = 0;
for (int i=0;i<nums.length;i++){
// 如果左等于右,即2*pre_sum,再加上当前值应该是总和
if (pre_sum*2 + nums[i] == sum){
return i;
}
pre_sum += nums[i];
}
return -1;
}
}
剑指 Offer II 013. 二维子矩阵的和
难度:中等
给定一个二维矩阵 matrix
,以下类型的多个请求:
- 计算其子矩形范围内元素的总和,该子矩阵的左上角为
(row1, col1)
,右下角为(row2, col2)
。
实现 NumMatrix
类:
NumMatrix(int[][] matrix)
给定整数矩阵matrix
进行初始化int sumRegion(int row1, int col1, int row2, int col2)
返回左上角(row1, col1)
、右下角(row2, col2)
的子矩阵的元素总和。
解题思路:
- 每块坐标 [ row , col ],每块的值为从 [ 0 , 0 ] 到 [ row , col ] 矩形内的所有数值相加
- 最终计算
sumRegion
分为几种情况:
- row1 = 0 && col1 = 0,即这个矩形是从头开始的,直接返回 [ row2 , col2 ]
- row1 = 0 && col1 ≠ 0,即贴顶部,大的矩形减去左侧矩形即可
- row1 ≠ 0 && col1 = 0,即贴左边,大的矩形减去上部矩形即可
- row1 ≠ 0 && col1 ≠ 0,哪都不贴,大的矩形减去上部矩形减去左边矩形,因为左边和上部都重叠的部分,减去了两次,所以得补回来,再加上左上矩形
class NumMatrix {
int[][] dp;
public NumMatrix(int[][] matrix) {
int n = matrix.length;
int m = matrix[0].length;
dp = new int[n][m];
dp[0][0] = matrix[0][0];
for (int i=1;i<n;i++){
dp[i][0] = dp[i-1][0] + matrix[i][0];
}
for (int j=1;j<m;j++){
dp[0][j] = dp[0][j-1] + matrix[0][j];
}
for (int i=1;i<n;i++){
for (int j=1;j<m;j++){
dp[i][j] = dp[i-1][j] + dp[i][j-1] + matrix[i][j] - dp[i-1][j-1];
}
}
}
public int sumRegion(int row1, int col1, int row2, int col2) {
if (row1 == 0 && col1 == 0){
return dp[row2][col2];
}
if (row1 == 0){
return dp[row2][col2] - dp[row2][col1-1];
}
if (col1 == 0){
return dp[row2][col2] - dp[row1-1][col2];
}
return dp[row2][col2] - dp[row2][col1-1] - dp[row1-1][col2] + dp[row1-1][col1-1];
}
}
/**
* Your NumMatrix object will be instantiated and called as such:
* NumMatrix obj = new NumMatrix(matrix);
* int param_1 = obj.sumRegion(row1,col1,row2,col2);
*/
剑指 Offer II 014. 字符串中的变位词
难度:中等
给定两个字符串 s1
和 s2
,写一个函数来判断 s2
是否包含 s1
的某个变位词。
换句话说,第一个字符串的排列之一是第二个字符串的 子串 。
解题思路:
- 暴力求解
- 滑动窗口:将字符串转换为数组,由于s2比s1长,所以每次滑动,得删除头部,加入尾部,调整数组,进行比较
class Solution {
public boolean checkInclusion(String s1, String s2) {
if (s1.length() > s2.length())return false;
int[] c1 = new int[26];
int[] c2 = new int[26];
// 滑动窗口
for (int i=0;i<s1.length();i++){
c1[s1.charAt(i)-'a']++;
c2[s2.charAt(i)-'a']++;
}
// 如果第一个就满足可以直接返回
if (Arrays.equals(c1,c2))return true;
// 滑动
for (int i=1;i<s2.length()-s1.length()+1;i++){
// s2拿出左边,加入右边
c2[s2.charAt(i-1)-'a']--;
c2[s2.charAt(i+s1.length()-1)-'a']++;
if (Arrays.equals(c1,c2))return true;
}
return false;
}
}
剑指 Offer II 015. 字符串中的所有变位词
难度:中等
给定两个字符串 s
和 p
,找到 s
中所有 p
的 变位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
变位词 指字母相同,但排列不同的字符串。
解题思路:
- 上一题的变种,把出现相同地方的位置记录下来
class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> res = new ArrayList();
if (p.length() > s.length())return res;
int[] c1 = new int[26];
int[] c2 = new int[26];
// 滑动窗口
for (int i=0;i<p.length();i++){
c1[p.charAt(i)-'a']++;
c2[s.charAt(i)-'a']++;
}
// 如果第一个就满足可以直接返回
if (Arrays.equals(c1,c2))res.add(0);
// 滑动
for (int i=1;i<s.length()-p.length()+1;i++){
// s2拿出左边,加入右边
c2[s.charAt(i-1)-'a']--;
c2[s.charAt(i+p.length()-1)-'a']++;
if (Arrays.equals(c1,c2))res.add(i);
}
return res;
}
}
剑指 Offer II 016. 不含重复字符的最长子字符串
难度:中等
给定一个字符串 s
,请你找出其中不含有重复字符的 最长连续子字符串 的长度。
解题思路:
- 定义一个区间,右区间持续右移动,同时记录每个字符出现的位置,如果移动到当前的字符之前出现过,则要缩小范围,将左边界调整到第一次出现该字符的后方,这样目前的区间里面才不会包含重复的字符,每次都得计算当前最大长度
- 这里注意缩小左边界的时候,只能往右边,不能往左移动
- 例如:abcba,先遇到b,将left指向了b的后面,又出现了a,这时候不可能将左边界移动到第一个a的后面,所以每次得比较left得大小,取最右
class Solution {
public int lengthOfLongestSubstring(String s) {
int left = 0;
int len = s.length();
if (len == 0)return 0;
HashMap<Character,Integer> map = new HashMap();
int max = 1;
for (int right=0;right<len;right++){
// 如果之前存在
if (map.containsKey(s.charAt(right))){
left = Math.max(left,map.get(s.charAt(right))+1);
}
map.put(s.charAt(right),right);
max = Math.max(max,right-left+1);
}
return max;
}
}
剑指 Offer II 017. 含有所有字符的最短字符串
难度:困难
给定两个字符串 s
和 t
。返回 s
中包含 t
的所有字符的最短子字符串。如果 s
中不存在符合条件的子字符串,则返回空字符串 ""
。
如果 s
中存在多个符合条件的子字符串,返回任意一个。
解题思路:
- 用一个长度为58的数组,表示大小写字母,每个字母对应一个位置
- t 中的所有字符对应数组中对应位置上的值 +1
- s 先开一个和 t 长度大小一样的窗口,但是对应的字符所在位置的值 -1。这样如果 s 目前窗口中已经包含 t 中的字符,那一正一负就抵消了,最后记录下数组中还有没有大于0的字符,记录下存在数量 diff,表示有这么多个不包含在当前窗口中的字符
- 滑动窗口,开始先向右扩张,逐步把字符都包含进来,每包含进来一个字符,对应的位置就是-1,如果此时等于0,则表示该位置的字符刚被包含进来且是属于 t 的,对应的diff-1,如果减完之后 diff 正好为0,表示当前窗口包含 t 中所有字符
- 但目前这个窗口并不是最短的,左边界还可以收缩,不断的收缩左边界,左边界的值出去了,对应数组中的位置需要+1,还记得原先添加 s 中字符的时候,是在位置上-1,如今再+1,理应来说会等于0。如果最终结果正好等于1,则表示原先是相互抵消的状态,如今刚把一个 t 中的字符给划出去了,也正好说明找到了左边界,此时计算窗口大小
class Solution {
public String minWindow(String s, String t) {
int s_len = s.length();
int t_len = t.length();
if (t_len > s_len)return "";
int[] hash = new int[58];
// t中所有字符在位置上+1,s中一开始开一个窗口大小和t一样的,把包含的字符-1
for (int i=0;i<t_len;i++){
hash[t.charAt(i)-'A']++;
hash[s.charAt(i)-'A']--;
}
// 检查有多少个已经被抵消了
int diff = 0;
for (int n : hash){
if (n > 0)diff++;
}
if (diff == 0)return s.substring(0,t_len);
// diff代表着当前窗口没有被抵消的数值的数量
int l = 0;
int r = t_len;
int minL = 0;
int minR = s_len;
// 滑动窗口从t_len开始往右边扩
for (;r<s_len;r++){
// 加进来一个字符
int in = s.charAt(r)-'A';
// 对应hash-1,表示包含
hash[in]--;
// 没有被抵消的字符,是+1状态,如今如果-1后等于0,表示抵消了,总数量-1
if (hash[in] == 0)diff--;
// 如果当前diff不为0,表示还有 没有抵消的 字符,则继续往右边扩
if (diff != 0)continue;
// diff等于0,表示当前已经全部包括进去,得计算下窗口得大小
// 计算大小之前,得尽可能得缩小左边界
while (diff == 0){
// 准备移除左边
int out = s.charAt(l)-'A';
// 移除得对应加上去
hash[out]++;
// 如果当前为1,表示t包含字符本来在里面,如今被划出去了
if (hash[out] == 1){
// 多了一个未被抵消得字符
diff++;
}
l++;
}
// 循环跳出,则表示左边边界收缩到位了,
if (r - l + 1 < minR - minL){
minL = l - 1;
minR = r;
}
}
// 最后看下minR,如果右侧都没有动过,说明就没有更新过,则返回""
return minR == s_len ? "" : s.substring(minL, minR + 1);
}
}
注意: 对于 t
中重复字符,我们寻找的子字符串中该字符数量必须不少于 t
中该字符数量。
剑指 Offer II 018. 有效的回文
难度:简单
给定一个字符串 s
,验证 s
是否是 回文串 ,只考虑字母和数字字符,可以忽略字母的大小写。
本题中,将空字符串定义为有效的 回文串 。
解题思路:
- 回文:两端开始往里面缩,每次判断两端相等不相等
- 注意:跳过不是字母和数字的,如果两个字符不相等也有可能是大小写,统一变成小写以后再判断
class Solution {
public boolean isPalindrome(String s) {
int left = 0;
int right = s.length()-1;
while (left < right){
while (left < right && !Character.isLetterOrDigit(s.charAt(left))){
left++;
}
while (left < right && !Character.isLetterOrDigit(s.charAt(right))){
right--;
}
char l = s.charAt(left);
char r = s.charAt(right);
// 如果两个不相等,但有可能是大小写
if (l != r){
if (Character.isLetter(l) && Character.isLetter(r)){
if (Character.toLowerCase(l) != Character.toLowerCase(r)){
return false;
}
}else{
return false;
}
}
left++;
right--;
}
return true;
}
}
剑指 Offer II 019. 最多删除一个字符得到回文
难度:简单
给定一个非空字符串 s
,请判断如果 最多 从字符串中删除一个字符能否得到一个回文字符串。
解题思路:
- 从两端开始比较,如果出现不一样的情况,就有两种选择,删除左边或删除右边,删除后的剩下的字符串就必须是回文,只要任何一种情况满足就是回文
class Solution {
public boolean validPalindrome(String s) {
int left = 0;
int right = s.length()-1;
boolean delete = true;
while (left < right){
if (s.charAt(left) != s.charAt(right)){
return isValidReverse(s,left+1,right) || isValidReverse(s,left,right-1);
}
left++;
right--;
}
return true;
}
public boolean isValidReverse(String s,int left,int right){
if (left < right){
while (left < right){
if (s.charAt(left) != s.charAt(right)){
return false;
}
left++;
right--;
}
}
return true;
}
}
剑指 Offer II 020. 回文子字符串的个数
难度:中等
给定一个字符串 s
,请计算这个字符串中有多少个回文子字符串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
解题思路:
- 找到中心的位置:一共有 2*n - 1 个子区间
- 奇数,left和right在同一个位置
- 偶数,left和right分别占一个位置
- left、right分别往两边扩散
class Solution {
public int countSubstrings(String s) {
int res = 0;
// 中心扩展
for (int i=0;i<(2*s.length()-1);i++){
int left = i/2;
int right = left + i%2;
while (left >= 0 && right <= s.length()-1 && s.charAt(left) == s.charAt(right)){
left--;
right++;
res++;
}
}
return res;
}
}
剑指 Offer II 021. 删除链表的倒数第 n 个结点
难度:中等
给定一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
解题思路:
- 快慢指针,快指针指向了要删除结点的后方,慢指针停留在前方,中间夹的就是要删除的结点。快慢指针的构成,从后往前推,正好就间隔n个,所以快指针先走n,然后快慢再一起走,直到快指针的下一个结点为空,即满指针的下一位就是需要删除的结点
- 删除倒数第n个结点,可能删除的就是第一个,所以快慢指针不能从第一个结点出发,因为最终是删除慢指针后面一个结点,这样并不能删除本身,所以这边额外起一个头部,从头部出发,开始往下进行
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode pre = new ListNode(0);
pre.next = head;
ListNode fast = pre;
ListNode slow = pre;
while (n-- > 0){
fast = fast.next;
}
while (fast.next != null){
slow = slow.next;
fast = fast.next;
}
slow.next = slow.next.next;
return pre.next;
}
}
剑指 Offer II 022. 链表中环的入口节点
难度:中等
给定一个链表,返回链表开始入环的第一个节点。 从链表的头节点开始沿着 next
指针进入环的第一个节点为环的入口节点。如果链表无环,则返回 null
。
为了表示给定链表中的环,我们使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos
是 -1
,则在该链表中没有环。注意,pos
仅仅是用于标识环的情况,并不会作为参数传递到函数中。
说明:不允许修改给定的链表。
解题思路:
- 链表中有环,定义快慢指针
- 假设从头到环入口的路径为 a ,环一圈的路劲为 b
- fast = 2*slow (快指针速度是慢指针的两倍)
- fast = slow + n*b (快指针和慢指针相遇了,是慢指针走过的路程+多饶了n圈环)
- 得到 slow = n*b
- 每次到环的入口,就需要走 a + n*b
- 而如今slow已经是n*b了,只需要再走个a
- a正好是从头到环入口的路径,所以说这时候有个指针移动到开头,然后两边同时走,当他们相遇的时候就是走了a路径的时候,相遇的这个点也正是环的入口
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (true){
if (fast == null || fast.next == null)return null;
fast = fast.next.next;
slow = slow.next;
if (fast == slow)break;
}
fast = head;
while (slow != fast){
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
剑指 Offer II 023. 两个链表的第一个重合节点
难度:简单
给定两个单链表的头节点 headA
和 headB
,请找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null
。
图示两个链表在节点 c1
开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
解题思路:
- A = a + c
- B = b + c
- A 要想和 B 相等,A + b = B + a
- 所以一个指针走完A再走B,一个指针走完B再走A,当它们相遇的时候就是重合节点
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode A = headA;
ListNode B = headB;
while (A != B){
if (A == null){
A = headB;
}else{
A = A.next;
}
if (B == null){
B = headA;
}else{
B = B.next;
}
}
return A;
}
}
剑指 Offer II 024. 反转链表
难度:简单
给定单链表的头节点 head
,请反转链表,并返回反转后的链表的头节点。
解题思路:
- 老生常谈,定义一个头节点,当前节点与后面的断开联系,指向头节点,后面的重复
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null;
while (head != null){
ListNode next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
}
剑指 Offer II 025. 链表中的两数相加
难度:中等
给定两个 非空链表 l1
和 l2
来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。
可以假设除了数字 0 之外,这两个数字都不会以零开头。
解题思路:
- 用栈来进行反转
- 取两个栈的头部值外加上进位,构成了这一次的总和 sum
- 更新当前值 val = sum%10,jinwei = sum/10
- 整个链表的也是反着输出的,所以用反转链表的思路做
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// 利用栈反转链表
Stack<Integer> stack1 = new Stack();
Stack<Integer> stack2 = new Stack();
while (l1 != null){
stack1.push(l1.val);
l1 = l1.next;
}
while (l2 != null){
stack2.push(l2.val);
l2 = l2.next;
}
ListNode pre = null;
int jinwei = 0;
while (!stack1.isEmpty() || !stack2.isEmpty() || jinwei != 0){
int s1 = stack1.isEmpty() ? 0 : stack1.pop();
int s2 = stack2.isEmpty() ? 0 : stack2.pop();
int sum = s1 + s2 + jinwei;
jinwei = sum/10;
ListNode new_node = new ListNode(sum%10);
new_node.next = pre;
pre = new_node;
}
return pre;
}
}
剑指 Offer II 026. 重排链表
难度:中等
给定一个单链表 L
的头节点 head
,单链表 L
表示为:
L0 → L1 → … → Ln-1 → Ln
请将其重新排列后变为:
L0 → Ln → L1 → Ln-1 → L2 → Ln-2 → …
不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
解题思路:
- 快慢指针同时移动,可把链表分成两半,以slow为中心,后半部是需要反转的部分
- 将后半部分进行反转,反转之后就是拼接,两个链表的合并
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public void reorderList(ListNode head) {
// 快慢指针
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
// slow 后面的链表需要进行反转
ListNode node = slow.next;
ListNode pre = null;
while (node != null){
ListNode next = node.next;
node.next = pre;
pre = node;
node = next;
}
// 前面就只有一半
slow.next = null;
ListNode head1 = head;
// 合并两个链表
while (head1 != null && pre != null){
ListNode head1_next = head1.next;
ListNode pre_next = pre.next;
head1.next = pre;
pre.next = head1_next;
head1 = head1_next;
pre = pre_next;
}
}
}
剑指 Offer II 027. 回文链表
难度:简单
给定一个链表的 头节点 head
,请判断其是否为回文链表。
如果一个链表是回文,那么链表节点序列从前往后看和从后往前看是相同的。
解题思路:
- 和上题一样,注意slow和fast起始值不一样,如果只有两个节点的话,slow还是会走动,导致最后分割失败
- 将链表分割成两半,后面的进行反转
- 反转后依次遍历两个链表,进行值的比较
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public boolean isPalindrome(ListNode head) {
// 快慢指针分两半
ListNode fast = head.next;
ListNode slow = head;
while (fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
// 反转后面的链表
ListNode node = slow.next;
ListNode pre = null;
while (node != null){
ListNode next = node.next;
node.next = pre;
pre = node;
node = next;
}
slow.next = null;
while (pre != null){
if (pre.val != head.val){
return false;
}
pre = pre.next;
head = head.next;
}
return true;
}
}
剑指 Offer II 028. 展平多级双向链表
难度:中等
多级双向链表中,除了指向下一个节点和前一个节点指针之外,它还有一个子链表指针,可能指向单独的双向链表。这些子列表也可能会有一个或多个自己的子项,依此类推,生成多级数据结构,如下面的示例所示。
给定位于列表第一级的头节点,请扁平化列表,即将这样的多级双向链表展平成普通的双向链表,使所有结点出现在单级双链表中。
解题思路:
- 递归的去查找,每次递归的都是下一个节点
- 把当前节点的所有情况都考虑好了以后,再进行下一个节点
- 考虑到最后递归返回的情况
- 当前节点没有下一个节点,没有子节点,就是最后一个节点,那就直接返回
- 当前节点没有下一个节点,但是有子节点,那就把子节点作为当前节点的下一个节点,也就是拉平,然后递归的去看子节点的情况
- 当前节点有下一个节点,没有子节点,那就可以直接查看下一个节点,这个节点不操作
- 当前节点有下一个节点,但是存在子节点,就需要先把子节点都连接上,然后下一个节点再拼接上去,所以也是先把子节点当作下一个节点,递归去看子节点的情况,最后返回一个Node节点,已经是排好序的节点,只要把下一个节点连接在后面就行
/*
// Definition for a Node.
class Node {
public int val;
public Node prev;
public Node next;
public Node child;
};
*/
class Solution {
public Node flatten(Node head) {
if (head == null)return null;
dfs(head);
return head;
}
public Node dfs(Node node){
// 如果它的下一位没有,
if (node.next == null){
// 就看它有没有子节点,有子节点,把它拉平,变成下一个节点
if (node.child != null){
node.next = node.child;
node.child.prev = node;
node.child = null;
return dfs(node.next);
}else{
// 也没有子节点,直接返回
return node;
}
}
// next节点存在的情况就要看有没有子节点
// 先展开子节点,后拼接下一个节点
Node next = node.next;
if (node.child != null){
node.next = node.child;
node.child.prev = node;
node.child = null;
// 子节点展开
Node child = dfs(node.next);
// 展开完毕后下个节点拼接上
child.next = next;
next.prev = child;
}
// 返回下一个节点
return dfs(next);
}
}
剑指 Offer II 029. 排序的循环链表
难度:中等
给定循环单调非递减列表中的一个点,写一个函数向这个列表中插入一个新元素 insertVal
,使这个列表仍然是循环升序的。
给定的可以是这个列表中任意一个顶点的指针,并不一定是这个列表中最小元素的指针。
如果有多个满足条件的插入位置,可以选择任意一个位置插入新的值,插入后整个列表仍然保持有序。
如果列表为空(给定的节点是 null
),需要创建一个循环有序列表并返回这个节点。否则。请返回原先给定的节点。
解题思路:
- 插入其中分这几种情况:
- 如果本身head为空,那么就自己和自己形成一个环
- 插入的值正好在中间,当前值比插入值小,且当前值比下一个值小,且当前值小于下一个值。例如:5 < insertVal = 6 < 7
- 插入的值在循环点
- 5 < insertVal = 6 > 1
- 5 < insertVal = 0 < 1
- 插入一个元素全部相等的链表,那就会循环一圈回到头部,然后跳出
- 最终跳出时,pre.next 就是要插入的位置,这个位置.next 就是原先 pre.next
/*
// Definition for a Node.
class Node {
public int val;
public Node next;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, Node _next) {
val = _val;
next = _next;
}
};
*/
class Solution {
public Node insert(Node head, int insertVal) {
// 如果本身head就是null,那当前节点自己形成环
if (head == null){
Node node = new Node(insertVal);
node.next = node;
return node;
}
Node pre = head;
// 如果有节点,循环一圈就得结束
while (pre.next != head){
// 如果正好在一前一后, 2 insertVal 3 ,因为是递增
if (pre.val <= insertVal && pre.next.val >= insertVal)break;
// 如果发现大于当前,大于下一个值,且左边大于右边 ,例如 7 < insertVal=9 > 1,说明处于循环点
if (pre.val <= insertVal && pre.next.val <= insertVal && pre.val > pre.next.val)break;
// 或者就是小于当前,也小于这之后,且左边大于右边,例如 7 > insertVal = 0 < 1,也是处于循环点
if (pre.val > insertVal && pre.next.val > insertVal && pre.val > pre.next.val)break;
// 剩下得就是发现当前点大于当前,也大于下一个节点,说明还要再往后
pre = pre.next;
}
// 如果只有一个节点,那就和这个节点形成循环
// 例如 2 2 2 2 2,插入1
// pre.next 指向了头节点,代表已经到达了尾部
// 所以将新值插入,让尾部得下一个节点是新节点,新节点得下一个节点是头部
pre.next = new Node(insertVal,pre.next);
return head;
}
}
剑指 Offer II 030. 插入、删除和随机访问都是 O(1) 的容器
难度:中等
设计一个支持在平均 时间复杂度 O(1) 下,执行以下操作的数据结构:
insert(val)
:当元素val
不存在时返回true
,并向集合中插入该项,否则返回false
。remove(val)
:当元素val
存在时返回true
,并从集合中移除该项,否则返回false
。getRandom
:随机返回现有集合中的一项。每个元素应该有 相同的概率 被返回。
解题思路:
- 用map,来方便 插入、删除元素
- 用数组,来方便随机访问,根据随机获取下角标来随机获取值
- 添加:先去map中查找,无重复值就插入,map中一份,数组中一份
- 删除:map中删除,对应数组得位置也得清空,清空的位置不能就放着不管,需要拿最末尾的元素填补进去,末尾元素就移动到了删除元素处,移动元素对应的map中的信息也需要更新一下
- 这里注意一点:如果本身删除的数就是数组中最后一个数,则无需移值填充,直接删除
class RandomizedSet {
Random random = new Random();
HashMap<Integer,Integer> map;
int[] nums;
int index = -1;
/** Initialize your data structure here. */
public RandomizedSet() {
map = new HashMap();
nums = new int[200000];
}
/** Inserts a value to the set. Returns true if the set did not already contain the specified element. */
public boolean insert(int val) {
// 如果存在,插入失败
if (map.containsKey(val))return false;
// 数组元素一个个往后存放,所以index++
index++;
// map中存一份便于查找,删除
map.put(val,index);
// 数组中存一份用于随机访问方便
nums[index] = val;
return true;
}
/** Removes a value from the set. Returns true if the set contained the specified element. */
public boolean remove(int val) {
// 如果不存在返回false
if (!map.containsKey(val))return false;
// 获取要删除元素,所在数组的下角标
int i = map.get(val);
// 删除map元素
map.remove(val);
// 删除数组元素
// 删除数组中某个元素,该位置就空缺了,没有值了,所以得从数组末尾拿个数填充进去
// 但是前提是删除数组中间得某个数,不包含最后一个数,如果删除得就是最后得数,本来就无需替换
if (i != index){
nums[i] = nums[index];
// 末尾元素得map里面得信息也得更新
map.put(nums[index],i);
}
index--;
return true;
}
/** Get a random element from the set. */
public int getRandom() {
return nums[random.nextInt(index+1)];
}
}
/**
* Your RandomizedSet object will be instantiated and called as such:
* RandomizedSet obj = new RandomizedSet();
* boolean param_1 = obj.insert(val);
* boolean param_2 = obj.remove(val);
* int param_3 = obj.getRandom();
*/
剑指 Offer II 031. 最近最少使用缓存
难度:中等
运用所掌握的数据结构,设计和实现一个 LRU (Least Recently Used,最近最少使用) 缓存机制
实现 LRUCache
类:
LRUCache(int capacity)
以正整数作为容量capacity
初始化 LRU 缓存int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回-1
。void put(int key, int value)
如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
解题思路:
- 最少使用得缓存
- 自定义一个双向链表来表示,定义一个头部head,定义一个尾部tail,首位相连
- 越接近head表示越经常使用,越靠近tail表示不经常使用
- 为了快速查询需要用个map来保存key,因为需要频繁调动链表节点,所以为了方便获取是哪个节点,map中value保存的就是节点,而节点中储存了对应的key,value。方便移除最近最少使用节点的时候,可以通过节点的key反推map,将map中的信息也移除
- 无外乎几种情况
- 插入的时候,首先看之前有没有存在过。
- 存在:更换最新的value值,其次把当前节点放到链表的开头,也就是head后面
- 不存在:看容量满了没有,如果满了,就要删除一个最少使用的,也就是删除尾部节点,顺便把map中的信息也删除了。删除完毕后,新添加一个新的,map里面保留一份,链表中新增节点放在head后面
- 获取的时候,去map中查找有无key,如果存在,则需要返回value。又因为查询操作,使得该节点成为最近使用的节点,所以要把该节点从原来的位置删除,把它移向头部
class LRUCache {
class DLink{
DLink prev;
DLink next;
int key;
int value;
public DLink(){}
public DLink(int key,int value){
this.key = key;
this.value = value;
}
}
HashMap<Integer,DLink> map;
int capacity;
DLink head,tail; // 自定义头部和尾部
public LRUCache(int capacity) {
this.capacity = capacity;
map = new HashMap();
// 收尾相连
head = new DLink();
tail = new DLink();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
if (!map.containsKey(key))return -1;
DLink node = map.get(key);
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
// 看之前是否存在过
if (map.containsKey(key)){
DLink old_node = map.get(key);
old_node.value = value;
moveToHead(old_node);
map.put(key,old_node);
}else{
// 看容量满了没有
if (capacity == 0){
// 删除最后一位
DLink node = removeTail();
// 删除map
map.remove(node.key);
}else{
capacity--;
}
// 没有满直接加进去
DLink node = new DLink(key,value);
map.put(key,node);
addToHead(node);
}
}
public DLink removeTail(){
DLink node = tail.prev;
removeNode(node);
return node;
}
public void removeNode(DLink node){
node.next.prev = node.prev;
node.prev.next = node.next;
}
public void moveToHead(DLink node){
removeNode(node);
addToHead(node);
}
public void addToHead(DLink node){
head.next.prev = node;
node.next = head.next;
head.next = node;
node.prev = head;
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
剑指 Offer II 032. 有效的变位词
难度:简单
给定两个字符串 s
和 t
,编写一个函数来判断它们是不是一组变位词(字母异位词)。
注意:若 s
和 t
中每个字符出现的次数都相同且字符顺序不完全相同,则称 s
和 t
互为变位词(字母异位词)。
解题思路:
- 用一个数组表示字母,如果s和t是变为词,则它们的字母相互抵消,最终数组都是0
class Solution {
public boolean isAnagram(String s, String t) {
if (s.equals(t))return false;
if (s.length() != t.length())return false;
int[] res = new int[26];
for (int i=0;i<s.length();i++){
res[s.charAt(i) - 'a']++;
res[t.charAt(i) - 'a']--;
}
for (int n : res){
if (n != 0)return false;
}
return true;
}
}
剑指 Offer II 033. 变位词组
难度:中等
给定一个字符串数组 strs
,将 变位词 组合在一起。 可以按任意顺序返回结果列表。
注意:若两个字符串中每个字符出现的次数都相同,则称它们互为变位词。
解题思路:
- 给每个字符串都排序,相同的排序结果相同就可以归为一类
- 用map来存储,key是排序结果,value就是一个list存放同种变为词
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
HashMap<String,List<String>> map = new HashMap();
for (String s : strs){
char[] c = s.toCharArray();
Arrays.sort(c);
String new_s = String.valueOf(c);
if (map.containsKey(new_s)){
List<String> list = map.get(new_s);
list.add(s);
}else{
List<String> list = new ArrayList();
list.add(s);
map.put(new_s,list);
}
}
List<List<String>> res = new ArrayList();
for (Map.Entry<String,List<String>> entry : map.entrySet()){
res.add(entry.getValue());
}
return res;
}
}
剑指 Offer II 034. 外星语言是否排序
难度:简单
某种外星语也使用英文小写字母,但可能顺序 order
不同。字母表的顺序(order
)是一些小写字母的排列。
给定一组用外星语书写的单词 words
,以及其字母表的顺序 order
,只有当给定的单词在这种外星语中按字典序排列时,返回 true
;否则,返回 false
。
解题思路:
- 看懂题目的意思很重要
- 给你一个字符串order
- 有一组字符串组words,两两比较,从第一位开始比较,例如:hello,leetcode,从第一位 h 和 l 开始,然后理应来说h在order所在位置 比 l 在order所在位置靠前,然后比较下一组。如果遇到相同的字母则向后继续比较,如果说最终word[i-1]没遍历完,word[i]遍历完了,也算是false
class Solution {
public boolean isAlienSorted(String[] words, String order) {
int[] index = new int[26];
for (int i=0;i<order.length();i++){
index[order.charAt(i)-'a'] = i;
}
for (int i=1;i<words.length;i++){
boolean valid = false;
for (int j = 0; j < words[i - 1].length() && j < words[i].length(); j++) {
int prev = index[words[i - 1].charAt(j) - 'a'];
int curr = index[words[i].charAt(j) - 'a'];
if (prev < curr) {
valid = true;
break;
} else if (prev > curr) {
return false;
}
}
if (!valid){
if (words[i-1].length() > words[i].length())return false;
}
}
return true;
}
}
剑指 Offer II 035. 最小时间差
难度:中等
给定一个 24 小时制(小时:分钟 "HH:MM")的时间列表,找出列表中任意两个时间的最小时间差并以分钟数表示。
解题思路:
- 本身最终得结果就是以分钟数表示,所以可以将时间转换成分钟,然后排序,两两进行比较,求出最小时间差
- 注意一点,由于00:00,这种也可以看作是24:00,所以为了得到23:59这种和00:00得比较,需要将最小得时间加上一个24:00,补充在末尾和最后一个大数进行比较
class Solution {
public int findMinDifference(List<String> timePoints) {
// 转换成分钟,进行比较,最开始的数,可以是00:00,也可以是24:00,所以为了能和23:59这种比较,需要取最小值加上24:00
if (timePoints.size() > 24*60)return 0;
List<Integer> res = new ArrayList();
for (String s:timePoints){
String[] c = s.split(":");
res.add(Integer.parseInt(c[0])*60 + Integer.parseInt(c[1]));
}
Collections.sort(res);
res.add(res.get(0) + 24*60);
int max = 24*60;
for (int i=1;i<res.size();i++){
max = Math.min(max,res.get(i)-res.get(i-1));
}
return max;
}
}
剑指 Offer II 036. 后缀表达式
难度:中等
根据 逆波兰表示法,求该后缀表达式的计算结果。
有效的算符包括 +
、-
、*
、/
。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:
- 整数除法只保留整数部分。
- 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
解题思路:
- 用一个栈来实现
- 是数字就压栈,是字符就把前面两个数字拿出来进行计算,计算完毕后把值再压入栈
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack();
for (String s : tokens){
// 如果长度为1,就肯定要么字符要么数字,如果不是数字,那就肯定是字符
if (s.length() == 1 && !Character.isDigit(s.charAt(0))){
int a = stack.pop();
int c = stack.pop();
if (s.equals("+"))stack.push(c+a);
else if (s.equals("-"))stack.push(c-a);
else if (s.equals("*"))stack.push(c*a);
else if (s.equals("/"))stack.push(c/a);
}else{
stack.push(Integer.parseInt(s));
}
}
return stack.pop();
}
}
剑指 Offer II 037. 小行星碰撞
难度:中等
给定一个整数数组 asteroids
,表示在同一行的小行星。
对于数组中的每一个元素,其绝对值表示小行星的大小,正负表示小行星的移动方向(正表示向右移动,负表示向左移动)。每一颗小行星以相同的速度移动。
找出碰撞后剩下的所有小行星。碰撞规则:两个行星相互碰撞,较小的行星会爆炸。如果两颗行星大小相同,则两颗行星都会爆炸。两颗移动方向相同的行星,永远不会发生碰撞。
解题思路:
- 用一个栈来模拟
- 当栈为空,可以直接加进去
- 当栈不为空
- 如果当前值大于0,表示向右,无论之前向左向右都不会碰撞
- 如果当前值小于0,表示向左,之前的值如果是向右的则会产生碰撞,碰撞就有三种结果:
- 等于,两者抵消,把之前的值也弹出
- 小于,当前值爆炸,不做后续操作
- 大于,把之前值移除,然后循环之前的判断
- 如果前面的值和当前值同向、或者之前值向左,当前值向右都需要插入当前值
- 为了兼容,加了个flag判断,不需要加入的情况就是等于和小于
class Solution {
public int[] asteroidCollision(int[] asteroids) {
Stack<Integer> stack = new Stack();
for (int n : asteroids){
boolean in = true;
if (stack.isEmpty()){
stack.push(n);
}else{
while (n < 0 && !stack.isEmpty() && stack.peek() > 0){
if (-n == stack.peek()){
stack.pop();
in = false;
break;
}else if (-n > stack.peek()){
stack.pop();
}else if (-n < stack.peek()){
in = false;
break;
}
}
if (in){
stack.push(n);
}
}
}
int[] res = new int[stack.size()];
for (int i=res.length-1;i>=0;i--){
res[i] = stack.pop();
}
return res;
}
}
剑指 Offer II 038. 每日温度
难度:中等
请根据每日 气温
列表 temperatures
,重新生成一个列表,要求其对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0
来代替。
解题思路:
- 辅助栈,栈中存数组,第一位是当前值,第二位是位置
- 当天 气温 加入的时候,判断之前的气温有无小于的,有则利用位置计算间隔天数
- 例如:75 73 76
- [75,0] 插入
- 73 < 75,[73,1] 插入
- 76 > 73,则出现第一个比73高的天,根据当前76下角标2和73的下角标1,算出天数1
- 76 > 75,则出现第一个比75高的天,算出天数2
- 大于的情况会循环判断,直至小于就加入进去
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int[] res = new int[temperatures.length];
Stack<int[]> stack = new Stack();
for (int i=0;i<temperatures.length;i++){
// 如果之前的值比当前值小,就说明出现了最近的一个气温高的
while (!stack.isEmpty() && stack.peek()[0] < temperatures[i]){
res[stack.peek()[1]] = i - stack.peek()[1];
stack.pop();
}
stack.push(new int[]{temperatures[i],i});
}
return res;
}
}
剑指 Offer II 039. 直方图最大矩形面积
难度:困难
给定非负整数数组 heights
,数组中的数字用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1
。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
解题思路:
- 单调栈,单调递增数
- 每个元素都会入栈,每次入栈之前会与栈顶元素比较,如果是大于栈顶元素就直接入栈,因为是递增栈。如果是小于则需要把比当前大的元素都顶出去,同时计算之前存在的最大值
- 顶出的栈顶元素需要计算以它为最高的点的大小,以当前的小值作为right边界,以栈顶的后一个元素作为left,因为后一个元素是第一个比栈顶元素小的元素,所以要以当前值作为高的面积,高肯定不能比当前更矮,所以left就是这么来的,计算对应的长度即可。
- 例如:2 3 1,前面2、3构成递增序列直接加入,当加入1的时候一下子就把高度拉低了,所以要计算下之前都是大值时的最大面积,先和3比,需要把3弹出,同时计算以3为高的时候的最大面积。以1所在位置作为right边界也就是2,找到第一个比3小的元素2作为left也就是0,长度就是2-0-1。3弹出后和2比较,right还是元素1所在位置2,由于元素2已经是唯一的值了,所以左边界在2的左边,这里为了能放便计算长度 2-x-1 = 2,x 只能等于 -1,所以可以事先插入一个最小值 -1,作为边界
- 这里注意特殊情况,如果遍历结束了,栈中还剩下 5、6、7,则需要将其计算一下,还是按照之前的方法,因为已经遍历结束,所以右边界来到了数组的最右边,也就是当前数组长度
class Solution {
public int largestRectangleArea(int[] heights) {
// 单调栈
// 有值比它小就计算之前的值的大小就行
Stack<Integer> stack = new Stack();
// 方便计算第一位
stack.push(-1);
int max = 0;
for (int i=0;i<heights.length;i++){
while (stack.peek() != -1 && heights[i] <= heights[stack.peek()]){
max = Math.max(max,heights[stack.pop()]*(i-stack.peek()-1));
}
stack.push(i);
}
while (stack.peek() != -1){
max = Math.max(max,heights[stack.pop()]*(heights.length-stack.peek()-1));
}
return max;
}
}
剑指 Offer II 040. 矩阵中最大的矩形
难度:困难
给定一个由 0
和 1
组成的矩阵 matrix
,找出只包含 1
的最大矩形,并返回其面积。
注意:此题 matrix
输入格式为一维 01
字符串数组。
解题思路:
- 动态规划
- 以右下角出发,如果直到矩形的高和宽就能算出面积了
- 矩形的高用累加的方式,以行为基准,一行一行往下添加,进行一个累加
- 1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0转换成
1 0 1 0 0
2 0 2 1 1
3 1 3 2 2
4 0 0 3 0所以当从右下角开始的时候,就知道当前高度是多少,往左延申的时候,高度以最低的为准,长度就一直计算直到遇到0为止
class Solution {
public int maximalRectangle(String[] matrix) {
int n = matrix.length;
if (n == 0)return 0;
int m= matrix[0].length();
int[][] dp = new int[n+1][m+1];
int res = 0;
for (int i=1;i<=n;i++){
for (int j=1;j<=m;j++){
if (matrix[i-1].charAt(j-1) == '1'){
dp[i][j] = dp[i-1][j] + 1;
}
int height = dp[i][j];
for (int k=j;k>=0 && dp[i][k] != 0;k--){
height = Math.min(height,dp[i][k]);
res = Math.max(res,height*(j-k+1));
}
}
}
return res;
}
}
剑指 Offer II 041. 滑动窗口的平均值
难度:简单
给定一个整数数据流和一个窗口大小,根据该滑动窗口的大小,计算滑动窗口里所有数字的平均值。
实现 MovingAverage
类:
MovingAverage(int size)
用窗口大小size
初始化对象。double next(int val)
成员函数next
每次调用的时候都会往滑动窗口增加一个整数,请计算并返回数据流中最后size
个值的移动平均值,即滑动窗口里所有数字的平均值。
解题思路:
- 用一个队列来维护滑动窗口
- sum就是直接求和,如果超过size大小就要移除一个
class MovingAverage {
double sum = 0;
int size = 0;
Queue<Double> queue;
/** Initialize your data structure here. */
public MovingAverage(int size) {
this.size = size;
queue = new LinkedList();
}
public double next(int val) {
queue.offer((double)val);
sum += (double)val;
if (size == 0){
sum -= queue.poll();
}else{
size--;
}
return sum/queue.size();
}
}
/**
* Your MovingAverage object will be instantiated and called as such:
* MovingAverage obj = new MovingAverage(size);
* double param_1 = obj.next(val);
*/
剑指 Offer II 042. 最近请求次数
难度:简单
写一个 RecentCounter
类来计算特定时间范围内最近的请求。
请实现 RecentCounter
类:
RecentCounter()
初始化计数器,请求数为 0 。int ping(int t)
在时间t
添加一个新请求,其中t
表示以毫秒为单位的某个时间,并返回过去3000
毫秒内发生的所有请求数(包括新请求)。确切地说,返回在[t-3000, t]
内发生的请求数。
保证 每次对 ping
的调用都使用比之前更大的 t
值。
解题思路:
- 用一个队列来接收时间
- 新进来一个时间,和队列顶部时间比较,也就是最早的时候比较,如果相差大于3000,则需要剔除顶部元素,循环剔除
class RecentCounter {
Queue<Integer> queue;
public RecentCounter() {
queue = new LinkedList();
}
public int ping(int t) {
while (!queue.isEmpty() && queue.peek()+3000<t){
queue.poll();
}
queue.offer(t);
return queue.size();
}
}
/**
* Your RecentCounter object will be instantiated and called as such:
* RecentCounter obj = new RecentCounter();
* int param_1 = obj.ping(t);
*/
剑指 Offer II 043. 往完全二叉树添加节点
难度:中等
完全二叉树是每一层(除最后一层外)都是完全填充(即,节点数达到最大,第 n
层有 2n-1
个节点)的,并且所有的节点都尽可能地集中在左侧。
设计一个用完全二叉树初始化的数据结构 CBTInserter
,它支持以下几种操作:
CBTInserter(TreeNode root)
使用根节点为root
的给定树初始化该数据结构;CBTInserter.insert(int v)
向树中插入一个新节点,节点类型为TreeNode
,值为v
。使树保持完全二叉树的状态,并返回插入的新节点的父节点的值;CBTInserter.get_root()
将返回树的根节点。
解题思路:
- 完全二叉树,新加入的节点从没有满子节点的父节点开始,所以按照从左到右,按照一层层的填满的原则,这就是中序遍历
- 中序遍历,按照顺序把所有缺失子节点的节点都扔到队列中去
- 插入的时候从队列头部取,可能存在这样几种情况
- 因为是从左至右添加,所以先判断有没有左节点,如果不存在左节点,则将新增的挂载到左节点,同时没有左节点说明肯定没有右节点,所以把该节点还有没有完成的子节点,而下一个新增的节点,就应该挂载到该节点的右节点上,所以把当前节点再次插入到队列头部去。而因为左节点已经被填满,它本身又可以成为一个父节点,所以按照顺序插入到队尾
- 在队列中的节点都是缺失子节点的,所以当存在左节点的时候,肯定缺失右节点,只需要将新增挂载到右节点上就可以,右节点就可以作为一个新的父节点加入到队列的尾部。当前节点左右节点就都被填满了
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class CBTInserter {
TreeNode root;
Queue<TreeNode> queue;
Deque<TreeNode> deque;
public CBTInserter(TreeNode root) {
this.root = root;
queue = new LinkedList();
deque = new LinkedList();
queue.add(root);
while (!queue.isEmpty()){
int size = queue.size();
for (int i=0;i<size;i++){
root = queue.poll();
if (root.left != null){
queue.add(root.left);
}
if (root.right != null){
queue.add(root.right);
}
if (root.left == null || root.right == null){
deque.add(root);
}
}
}
}
public int insert(int v) {
// 只有缺左右子树的才会插入到deque中
TreeNode node = deque.poll();
// 如果缺左子树
if (node.left == null){
node.left = new TreeNode(v);
deque.addFirst(node);
// 左子树也作为一个备选的父节点加入到队列中去
deque.addLast(node.left);
}else{
// 那就是缺右子树
node.right = new TreeNode(v);
// 右子树也作为一个备选的父节点加入到队列中去
deque.addLast(node.right);
}
return node.val;
}
public TreeNode get_root() {
return root;
}
}
/**
* Your CBTInserter object will be instantiated and called as such:
* CBTInserter obj = new CBTInserter(root);
* int param_1 = obj.insert(v);
* TreeNode param_2 = obj.get_root();
*/
剑指 Offer II 044. 二叉树每层的最大值
难度:中等
给定一棵二叉树的根节点 root
,请找出该二叉树中每一层的最大值。
解题思路:
- 层序遍历的同时,计算每层的最大值,每层遍历结束将结果输入到list中去
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> largestValues(TreeNode root) {
List<Integer> res = new ArrayList();
if (root == null)return res;
Queue<TreeNode> queue = new LinkedList();
queue.add(root);
while (!queue.isEmpty()){
int size = queue.size();
int max = Integer.MIN_VALUE;
for (int i=0;i<size;i++){
root = queue.poll();
if (root.left != null){
queue.add(root.left);
}
if (root.right != null){
queue.add(root.right);
}
max = Math.max(max,root.val);
}
res.add(max);
}
return res;
}
}
剑指 Offer II 045. 二叉树最底层最左边的值
难度:中等
给定一个二叉树的 根节点 root
,请找出该二叉树的 最底层 最左边 节点的值。
假设二叉树中至少有一个节点。
解题思路:
- 层序遍历,题目所求是最左边节点
- 和往常的从左至右遍历不同,从左至右层序遍历,最终值会不断的被右边的值顶掉,所以利用这一点,可以把顺序反过来,从右至左遍历,最终留下的就是最左边的值,按层序遍历的结果,最终最后一个值就是最底层最左边的节点的值
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int findBottomLeftValue(TreeNode root) {
Queue<TreeNode> queue = new LinkedList();
int res = 0;
queue.add(root);
while (!queue.isEmpty()){
root = queue.poll();
if (root.right != null){
queue.add(root.right);
}
if (root.left != null){
queue.add(root.left);
}
res = root.val;
}
return res;
}
}
剑指 Offer II 046. 二叉树的右侧视图
难度:中等
给定一个二叉树的 根节点 root
,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
解题思路:
- 层序遍历:每一层的最右边的数
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> res = new ArrayList();
if (root == null)return res;
Queue<TreeNode> queue = new LinkedList();
queue.add(root);
while (!queue.isEmpty()){
int size = queue.size();
int num = 0;
for (int i=0;i<size;i++){
root = queue.poll();
num = root.val;
if (root.left != null){
queue.add(root.left);
}
if (root.right != null){
queue.add(root.right);
}
}
res.add(num);
}
return res;
}
}
剑指 Offer II 047. 二叉树剪枝
难度:中等
给定一个二叉树 根节点 root
,树的每个节点的值要么是 0
,要么是 1
。请剪除该二叉树中所有节点的值为 0
的子树。
节点 node
的子树为 node
本身,以及所有 node
的后代。
解题思路:
- 剪枝的条件是子节点全是0,所以dfs遍历每个节点,获取当前节点的左节点和右节点的值,哪个节点为0,表示可以剪枝,当前节点的结果就是左节点的值+右节点的值+当前节点的值。最终返回到最开始root,如果root也为0,则表示整棵数都可以剪
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode pruneTree(TreeNode root) {
int r = dfs(root);
return r == 0 ? null : root;
}
public int dfs(TreeNode root){
if (root == null)return 0;
int left = dfs(root.left);
if (left == 0){
root.left = null;
}
int right = dfs(root.right);
if (right == 0){
root.right = null;
}
return left + right + root.val;
}
}
剑指 Offer II 048. 序列化与反序列化二叉树
难度:困难
序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
解题思路:
- 应用题
- 按照前序遍历,把val都拼接成一个字符串,遇到null,则填入None 返回
- 解码:将字符串分割,存到list中,从list头部开始遍历,为了方便,遍历一个删除一个,遇到None,直接返回null。剩下的就是按照前序遍历的样子就构造树
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
return serializeToString(root,"");
}
public String serializeToString(TreeNode root,String str){
if (root == null){
str += "None,";
return str;
}
str += String.valueOf(root.val) + ",";
str = serializeToString(root.left,str);
str = serializeToString(root.right,str);
return str;
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
String[] temp = data.split(",");
List<String> res = new ArrayList(Arrays.asList(temp));
return deserializeToTree(res);
}
public TreeNode deserializeToTree(List<String> list){
if (list.get(0).equals("None")){
list.remove(0);
return null;
}
TreeNode node = new TreeNode(Integer.parseInt(list.get(0)));
list.remove(0);
node.left = deserializeToTree(list);
node.right = deserializeToTree(list);
return node;
}
}
// Your Codec object will be instantiated and called as such:
// Codec ser = new Codec();
// Codec deser = new Codec();
// TreeNode ans = deser.deserialize(ser.serialize(root));
剑指 Offer II 049. 从根节点到叶节点的路径数字之和
难度:中等
给定一个二叉树的根节点 root
,树中每个节点都存放有一个 0
到 9
之间的数字。
每条从根节点到叶节点的路径都代表一个数字:
- 例如,从根节点到叶节点的路径
1 -> 2 -> 3
表示数字123
。
计算从根节点到叶节点生成的 所有数字之和 。
叶节点 是指没有子节点的节点。
解题思路:
- 正常的二叉树遍历,最终当左右子树都为null的时候,代表已经到了叶节点,可以进行累加的计算。遍历的同时要带着经过的数值,每次都是进一位再加上当前节点值
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int res = 0;
public int sumNumbers(TreeNode root) {
dfs(root,0);
return res;
}
public void dfs(TreeNode root,int sum){
if (root == null){
return;
}
int curr_sum = sum*10 + root.val;
dfs(root.left,curr_sum);
dfs(root.right,curr_sum);
if (root.left == null && root.right == null){
res += curr_sum;
}
}
}
剑指 Offer II 050. 向下的路径节点之和
难度:中等
给定一个二叉树的根节点 root
,和一个整数 targetSum
,求该二叉树里节点值之和等于 targetSum
的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
解题思路:
- 如果求从根节点出发,找到一条路径和为targetSum。遍历二叉树的同时,targetSum减去当前值,如果下一个值等于这个结果表示找到一条路径,然后还得继续往下查找
- 而题目所说,每个节点都可能是父节点,所以增加的一步从每个节点出发
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int res = 0;
public int pathSum(TreeNode root, int targetSum) {
if (root == null)return 0;
int count = dfs(root,targetSum);
// 每个节点都可以作为父节点
count += pathSum(root.left,targetSum);
count += pathSum(root.right,targetSum);
return count;
}
public int dfs(TreeNode root, int targetSum){
if (root == null)return 0;
int sum = 0;
if (root.val == targetSum)sum++;
sum += dfs(root.left,targetSum-root.val);
sum += dfs(root.right,targetSum-root.val);
return sum;
}
}
剑指 Offer II 051. 节点之和最大的路径
难度:困难
路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给定一个二叉树的根节点 root
,返回其 最大路径和,即所有路径上节点值之和的最大值。
解题思路:
- 路径之和,两种情况:
- 左边一条路或者右边一条路
- 左边到中间到右边
- 从这个思路出发,每个节点就看左子树的大小和右子树的大小,那么当前的路径最大要么选左边要么选右边,就看哪个大选哪个,这就是左右子树对当前节点的贡献度。但是实际路径的话,有可能是从左至右贯穿的,所以也要考虑这种情况,算一下最大值
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int max = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
dfs(root);
return max;
}
public int dfs(TreeNode root){
if (root == null)return 0;
// 递归左右子节点,如果为负数,就当0处理,等于不加上当前路径
int left = Math.max(dfs(root.left),0);
int right = Math.max(dfs(root.right),0);
// 路径也有可能是直接 左到根到右,所以要把整个都加起来
int p = root.val + left + right;
// 比较下最大值
max = Math.max(max,p);
// 每个节点就留最大的那条路,即左边右边哪个大留哪个
return root.val + Math.max(left,right);
}
}
剑指 Offer II 052. 展平二叉搜索树
难度:简单
给你一棵二叉搜索树,请 按中序遍历 将其重新排列为一棵递增顺序搜索树,使树中最左边的节点成为树的根节点,并且每个节点没有左子节点,只有一个右子节点。
解题思路:
- 按照中序遍历,将节点放入队列中
- 新增一个树,依次从列表中取数,只连接到右节点上
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode increasingBST(TreeNode root) {
Queue<Integer> queue = new LinkedList();
dfs(root,queue);
TreeNode res = new TreeNode(queue.poll());
TreeNode node = res;
while (!queue.isEmpty()){
node.right = new TreeNode(queue.poll());
node = node.right;
}
return res;
}
public void dfs(TreeNode root,Queue<Integer> queue){
if (root == null)return;
dfs(root.left,queue);
queue.add(root.val);
dfs(root.right,queue);
}
}
剑指 Offer II 053. 二叉搜索树中的中序后继
难度:中等
给定一棵二叉搜索树和其中的一个节点 p
,找到该节点在树中的中序后继。如果节点没有中序后继,请返回 null
。
节点 p
的后继是值比 p.val
大的节点中键值最小的节点,即按中序遍历的顺序节点 p
的下一个节点。
解题思路:
- 二叉搜索树,如果当前节点比p大,p肯定在左子树,反之在右子树
- 记录最近一个比p大的节点,所以每当大于的时候都记录一下,直到最终比它小的时候,就证明找到了
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
TreeNode res = null;
while (root != null){
if (root.val > p.val){
res = root;
root = root.left;
}else{
root = root.right;
}
}
return res;
}
}
给定一个二叉搜索树,请将它的每个节点的值替换成树中大于或者等于该节点值的所有节点值之和。
提醒一下,二叉搜索树满足下列约束条件:
- 节点的左子树仅包含键 小于 节点键的节点。
- 节点的右子树仅包含键 大于 节点键的节点。
- 左右子树也必须是二叉搜索树。
解题思路:
- 右根左,中序遍历,从右扫到左,进行节点值得累加,替换value
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int sum = 0;
public TreeNode convertBST(TreeNode root) {
// 二叉搜索树 左<根<右
// 从右加到左
dfs(root);
return root;
}
public void dfs(TreeNode root){
if (root == null)return;
dfs(root.right);
sum += root.val;
root.val = sum;
dfs(root.left);
}
}
剑指 Offer II 055. 二叉搜索树迭代器
难度:中等
实现一个二叉搜索树迭代器类BSTIterator
,表示一个按中序遍历二叉搜索树(BST)的迭代器:
BSTIterator(TreeNode root)
初始化BSTIterator
类的一个对象。BST 的根节点root
会作为构造函数的一部分给出。指针应初始化为一个不存在于 BST 中的数字,且该数字小于 BST 中的任何元素。boolean hasNext()
如果向指针右侧遍历存在数字,则返回true
;否则返回false
。int next()
将指针向右移动,然后返回指针处的数字。
注意,指针初始化为一个不存在于 BST 中的数字,所以对 next()
的首次调用将返回 BST 中的最小元素。
可以假设 next()
调用总是有效的,也就是说,当调用 next()
时,BST 的中序遍历中至少存在一个下一个数字。
解题思路:
- 中序遍历的过程中拿个list存下节点的 value值,得到顺序
- 指针初始指向第一位,每调用一次,指针后移一位,
- 最终移动超出长度返回fasle
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class BSTIterator {
List<Integer> list = new ArrayList();
int index = 0;
public BSTIterator(TreeNode root) {
dfs(root,list);
}
public void dfs(TreeNode root,List<Integer> list){
if (root == null)return;
dfs(root.left,list);
list.add(root.val);
dfs(root.right,list);
}
public int next() {
return list.get(index++);
}
public boolean hasNext() {
if (index >= list.size())return false;
return true;
}
}
/**
* Your BSTIterator object will be instantiated and called as such:
* BSTIterator obj = new BSTIterator(root);
* int param_1 = obj.next();
* boolean param_2 = obj.hasNext();
*/
剑指 Offer II 056. 二叉搜索树中两个节点之和
难度:简单
给定一个二叉搜索树的 根节点 root
和一个整数 k
, 请判断该二叉搜索树中是否存在两个节点它们的值之和等于 k
。假设二叉搜索树中节点的值均唯一。
解题思路:
- 两数之和的二叉树版本,遍历的同时去集合中找对应的,找得到返回true,找不到false
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
Set<Integer> set = new HashSet();
public boolean findTarget(TreeNode root, int k) {
if (root == null){
return false;
}
if (set.contains(k-root.val)){
return true;
}
set.add(root.val);
return findTarget(root.left,k) || findTarget(root.right,k);
}
}
剑指 Offer II 057. 值和下标之差都在给定的范围内
难度:中等
给你一个整数数组 nums
和两个整数 k
和 t
。请你判断是否存在 两个不同下标 i
和 j
,使得 abs(nums[i] - nums[j]) <= t
,同时又满足 abs(i - j) <= k
。
如果存在则返回 true
,不存在返回 false
。
解题思路:
- 两个数相差在 t 以内,所以按照 t 分割,对应的数就会在对应的桶里面
- 例如:3、7、11,t = 3
- 所以 3 被分在了桶1
- 7被分在了桶2
- 11被分在了桶3
- 当来了一个新值5,就插入了1,而一个桶的数的差值肯定在 t 内,所以就找到了
- 或者说没有3,5自己在一个桶里,但是7和5相差也在 t 内,所以当前桶没有值的时候,也可以去临边的桶去查看,它们的差值有没有满足的情况
- 都没有满足的时候,证明是个新值,需要插入对应的桶里面
- 因为还要保证相差最多为k,所以就需要个滑动窗口k,移动超过k后把最开始的剔除
- 这里桶的分类情况要注意:
- 当桶大小为4,希望0,1,2,3 在一个桶里面,这个直接 = 值/桶大小
- 相等的,希望 -1,-2,-3,-4 在一个桶里面,这个如果直接 = 值/桶小,-1,-2,-3 都在 0 这个桶里,显然和上面0、1、2、3 冲突了,所以希望负数初始的从 -1 桶开始,所以需要额外 -1,而一开始这么除把-4给移除出去了,为了能移动进去,所以在原来的值上+1,这样-4变-3就到了-1桶里面了
class Solution {
public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
long w = (long)t + 1;
HashMap<Long,Long> map = new HashMap();
for (int i=0;i<nums.length;i++){
long id = getID(nums[i],w);
if (map.containsKey(id)){
return true;
}
if (map.containsKey(id-1) && Math.abs(map.get(id-1)-nums[i]) <= t){
return true;
}
if (map.containsKey(id+1) && Math.abs(map.get(id+1)-nums[i]) <= t){
return true;
}
map.put(id,(long)nums[i]);
if (i >= k){
map.remove(getID(nums[i-k],w));
}
}
return false;
}
public long getID(long x, long w) {
if (x >= 0) {
return x / w;
}
return (x + 1) / w - 1;
}
}
剑指 Offer II 058. 日程表
难度:中等
请实现一个 MyCalendar
类来存放你的日程安排。如果要添加的时间内没有其他安排,则可以存储这个新的日程安排。
MyCalendar
有一个 book(int start, int end)
方法。它意味着在 start 到 end 时间内增加一个日程安排,注意,这里的时间是半开区间,即 [start, end)
, 实数 x
的范围为, start <= x < end
。
当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生重复预订。
每次调用 MyCalendar.book
方法时,如果可以将日程安排成功添加到日历中而不会导致重复预订,返回 true
。否则,返回 false
并且不要将该日程安排添加到日历中。
请按照以下步骤调用 MyCalendar
类: MyCalendar cal = new MyCalendar();
MyCalendar.book(start, end)
解题思路:
毁灭吧、暴力吧
class MyCalendar {
List<int[]> booked;
public MyCalendar() {
booked = new ArrayList<int[]>();
}
public boolean book(int start, int end) {
for (int[] arr : booked) {
int l = arr[0], r = arr[1];
if (l < end && start < r) {
return false;
}
}
booked.add(new int[]{start, end});
return true;
}
}
剑指 Offer II 059. 数据流的第 K 大数值
难度:简单
设计一个找到数据流中第 k
大元素的类(class)。注意是排序后的第 k
大元素,不是第 k
个不同的元素。
请实现 KthLargest
类:
KthLargest(int k, int[] nums)
使用整数k
和整数流nums
初始化对象。int add(int val)
将val
插入数据流nums
后,返回当前数据流中第k
大的元素。
解题思路:
- 优先队列,维护长度为k
- 每次插入新值后判断,超过k就弹出一个,剩下的就是长度为k的队列
- 队列顶部元素就是最小的,也就是第k大的元素
class KthLargest {
PriorityQueue<Integer> queue;
int k;
public KthLargest(int k, int[] nums) {
this.k = k;
queue = new PriorityQueue<Integer>();
for (int x : nums) {
add(x);
}
}
public int add(int val) {
queue.offer(val);
if (queue.size() > k) {
queue.poll();
}
return queue.peek();
}
}
/**
* Your KthLargest object will be instantiated and called as such:
* KthLargest obj = new KthLargest(k, nums);
* int param_1 = obj.add(val);
*/
剑指 Offer II 060. 出现频率最高的 k 个数字
难度:中等
给定一个整数数组 nums
和一个整数 k
,请返回其中出现频率前 k
高的元素。可以按 任意顺序 返回答案。
解题思路:
- map存储每个数字出现的次数
- 遍历map存入优先队列,按从大到小排序
- 队列前k个元素就是答案
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
map.put(num, map.getOrDefault(num,0)+1);
}
PriorityQueue<Integer> queue = new PriorityQueue<>((a, b) -> map.get(b) - map.get(a));
for (int key : map.keySet()) {
queue.offer(key);
}
int[] res = new int[k];
for (int i = 0; i < k ; i++) {
res[i] = queue.poll();
}
return res;
}
}
剑指 Offer II 061. 和最小的 k 个数对
难度:中等
给定两个以升序排列的整数数组 nums1
和 nums2
, 以及一个整数 k
。
定义一对值 (u,v)
,其中第一个元素来自 nums1
,第二个元素来自 nums2
。
请找到和最小的 k
个数对 (u1,v1)
, (u2,v2)
... (uk,vk)
。
解题思路:
- 由于是两个升序排列的数组
- 所以第一个最小值产生在了 0,0,都是取得第一个元素,用数组表示 [ 0 , 0 ]
- 第二个最小值,坐标可能是 [0,1]或者[1,0],以此类推
- 当次取到最小值后,就会有两种情况产生,需要将它们排序后得到最小得那一个,然后再循环往复直到完成k对,或者数都取完为止
- 这里会有重复情况产生:
- 在[0,1]和[1,0]中,假设取了[0,1],然后插入了[1,1],[0,2]
- 在[1,0],[1,1],[0,2],假设取了 [1,0],然后插入了 [1,1],[2,0]
- 这时候队列就存在[1,1],[0,2],[1,1],[2,0],存在重复数
- 所以插入得时候就需要去重复,用HashSet存放坐标,坐标用字符串表示
class Solution {
public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
PriorityQueue<int[]> queue = new PriorityQueue<int[]>((a,b)-> nums1[a[0]] + nums2[a[1]] - nums1[b[0]] - nums2[b[1]]);
HashSet<String> set = new HashSet();
List<List<Integer>> res = new ArrayList();
queue.offer(new int[]{0,0});
while (k-- > 0 && !queue.isEmpty()){
// 取出最小的点
int[] p = queue.poll();
// 最小点对应的数,存入list
ArrayList<Integer> list = new ArrayList();
list.add(nums1[p[0]]);
list.add(nums2[p[1]]);
res.add(list);
// 将后一个最小点放入
// 可能是 p[0]+1 p[1]
// 可能是 p[0] p[1]+1
if (p[0] + 1 <nums1.length){
// 去重复
if (set.add(String.valueOf(p[0]+1)+"_"+String.valueOf(p[1]))){
queue.offer(new int[]{p[0]+1,p[1]});
}
}
if (p[1] + 1 <nums2.length){
// 去重复
if (set.add(String.valueOf(p[0])+"_"+String.valueOf(p[1]+1))){
queue.offer(new int[]{p[0],p[1]+1});
}
}
}
return res;
}
}
剑指 Offer II 062. 实现前缀树
难度:中等
Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie()
初始化前缀树对象。void insert(String word)
向前缀树中插入字符串word
。boolean search(String word)
如果字符串word
在前缀树中,返回true
(即,在检索之前已经插入);否则,返回false
。boolean startsWith(String prefix)
如果之前已经插入的字符串word
的前缀之一为prefix
,返回true
;否则,返回false
。
解题思路:
- 数组中每个位置代表一个字母,正好26个位置
- 每个单词的第一位就存放在数组对应的位置上,第二个字母就是在第一个的位置上再链接上一个数组,以此类推,一个单词就可以顺着存下去
- 题目有要求,求完整的单词和前缀的单词,所以每个单词如果在当前位置结束了,就需要标记一下。查找完整单词的时候,虽然按照顺序都找到了,但是最后的标记位表示false的话也仅仅代表有该单词的前缀,但是并没有完整的这个单词存放在里面
class Trie {
private Trie[] children;
private boolean isEnd;
/** Initialize your data structure here. */
public Trie() {
children = new Trie[26];
isEnd = false;
}
/** Inserts a word into the trie. */
public void insert(String word) {
Trie node = this;
for (int i=0;i<word.length();i++){
int index = word.charAt(i)-'a';
if (node.children[index] == null){
node.children[index] = new Trie();
}
node = node.children[index];
}
node.isEnd = true;
}
/** Returns if the word is in the trie. */
public boolean search(String word) {
Trie node = searchPrefix(word);
return node != null && node.isEnd;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
public boolean startsWith(String prefix) {
Trie node = searchPrefix(prefix);
return node != null;
}
public Trie searchPrefix(String word){
Trie node = this;
for (int i=0;i<word.length();i++){
int index = word.charAt(i)-'a';
if (node.children[index] == null){
return null;
}
node = node.children[index];
}
return node;
}
}
/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* boolean param_2 = obj.search(word);
* boolean param_3 = obj.startsWith(prefix);
*/
剑指 Offer II 063. 替换单词
难度:中等
在英语中,有一个叫做 词根(root)
的概念,它可以跟着其他一些词组成另一个较长的单词——我们称这个词为 继承词(successor)
。例如,词根an
,跟随着单词 other
(其他),可以形成新的单词 another
(另一个)。
现在,给定一个由许多词根组成的词典和一个句子,需要将句子中的所有继承词
用词根
替换掉。如果继承词
有许多可以形成它的词根
,则用最短的词根替换它。
需要输出替换之后的句子。
解题思路:
- 将每个词根插入到set中
- 将句子分割成单词,每个单词逐步开始截取,不停得去set中寻找有没有相同得,有就返回到结果中去
class Solution {
public String replaceWords(List<String> dictionary, String sentence) {
Set<String> dictionarySet = new HashSet<String>();
for (String root : dictionary) {
dictionarySet.add(root);
}
String[] words = sentence.split(" ");
for (int i = 0; i < words.length; i++) {
String word = words[i];
for (int j = 0; j < word.length(); j++) {
if (dictionarySet.contains(word.substring(0, 1 + j))) {
words[i] = word.substring(0, 1 + j);
break;
}
}
}
return String.join(" ", words);
}
}
剑指 Offer II 064. 神奇的字典
难度:中等
设计一个使用单词列表进行初始化的数据结构,单词列表中的单词 互不相同 。 如果给出一个单词,请判定能否只将这个单词中一个字母换成另一个字母,使得所形成的新单词存在于已构建的神奇字典中。
实现 MagicDictionary
类:
MagicDictionary()
初始化对象void buildDict(String[] dictionary)
使用字符串数组dictionary
设定该数据结构,dictionary
中的字符串互不相同bool search(String searchWord)
给定一个字符串searchWord
,判定能否只将字符串中 一个 字母换成另一个字母,使得所形成的新字符串能够与字典中的任一字符串匹配。如果可以,返回true
;否则,返回false
。
解题思路:
- 字典树的运用,先将初始化的字符串转换成字典树
- 后面的字符串匹配就是查找的过程,如果当前值查询不到,就可替换其他字母,前提是其他字母有后续的子节点,而这种替换只能一次,所以用个标志位表示一下,最终查看这个字符串是不是真的有,且当前修改过了一次
class MagicDictionary {
class Tric{
Tric[] children;
boolean isEnd;
public Tric(){
children = new Tric[26];
isEnd = false;
}
}
Tric tric;
/** Initialize your data structure here. */
public MagicDictionary() {
tric = new Tric();
}
public void buildDict(String[] dictionary) {
for (String dir : dictionary){
Tric node = tric;
for (int i=0;i<dir.length();i++){
int index = dir.charAt(i)-'a';
if (node.children[index] == null){
node.children[index] = new Tric();
}
node = node.children[index];
}
node.isEnd = true;
}
}
public boolean search(String searchWord) {
return dfs(searchWord,0,tric,false);
}
public boolean dfs(String searchWord,int index,Tric node,boolean modified){
if (index == searchWord.length()){
return node.isEnd && modified;
}
int idx = searchWord.charAt(index) - 'a';
// 如果当前匹配上了,可以递归查看后面是否匹配
if (node.children[idx] != null){
if (dfs(searchWord,index + 1,node.children[idx] , modified)){
return true;
}
}
// 上面没匹配上,那就可以修改当前值
// 没有修改过,那就可以修改一次
if (!modified){
// 当前值可以修改成别的任意一个字母
for (int i = 0; i < 26; ++i) {
// 如果其他字母有后续节点,可以去查看下接下来匹配不匹配,注意已经修改过当前字母了,下面就不能再修改了
if (i != idx && node.children[i] != null) {
if (dfs(searchWord,index + 1, node.children[i], true)) {
return true;
}
}
}
}
return false;
}
}
/**
* Your MagicDictionary object will be instantiated and called as such:
* MagicDictionary obj = new MagicDictionary();
* obj.buildDict(dictionary);
* boolean param_2 = obj.search(searchWord);
*/
剑指 Offer II 065. 最短的单词编码
难度:中等
单词数组 words
的 有效编码 由任意助记字符串 s
和下标数组 indices
组成,且满足:
words.length == indices.length
- 助记字符串
s
以'#'
字符结尾 - 对于每个下标
indices[i]
,s
的一个从indices[i]
开始、到下一个'#'
字符结束(但不包括'#'
)的 子字符串 恰好与words[i]
相等
给定一个单词数组 words
,返回成功对 words
进行编码的最小助记字符串 s
的长度 。
解题思路:
- words可能存在重复,先用set过滤一遍
- 因为后缀不能重复,所以遍历字符串,不停截取后缀,看set中是否有重复,重复的剔除,最终留下的就是都是有效的字符串,每个留下的字符串都要加上
'#'
class Solution {
public int minimumLengthEncoding(String[] words) {
Set<String> set = new HashSet();
for(String s : words){
set.add(s);
}
for (String s : words){
for (int k=1;k<s.length();k++){
set.remove(s.substring(k));
}
}
int res = 0;
for (String s : set){
res += s.length() + 1;
}
return res;
}
}
剑指 Offer II 066. 单词之和
难度:中等
实现一个 MapSum
类,支持两个方法,insert
和 sum
:
MapSum()
初始化MapSum
对象void insert(String key, int val)
插入key-val
键值对,字符串表示键key
,整数表示值val
。如果键key
已经存在,那么原来的键值对将被替代成新的键值对。int sum(string prefix)
返回所有以该前缀prefix
开头的键key
的值的总和。
解题思路:
- 字典树存储key,实际值用map来存
- 查找的的时候沿着字典树向下查找,当找到对应的前缀后,开始遍历接下来的字母,存在字母的位置继续向下寻找,每一层都是这样,直到到底 isEnd = true,将对应的key去map中查找拿到value后,累加到结果中去
class MapSum {
class Tire {
Tire[] children;
boolean isEnd;
public Tire(){
children = new Tire[26];
isEnd = false;
}
}
HashMap<String,Integer> map;
Tire tire;
char[] x = new char[]{'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'};
/** Initialize your data structure here. */
public MapSum() {
map = new HashMap();
tire = new Tire();
}
public void insert(String key, int val) {
map.put(key,val);
Tire node = tire;
for (int i=0;i<key.length();i++){
int index = key.charAt(i) - 'a';
if (node.children[index] == null){
node.children[index] = new Tire();
}
node = node.children[index];
}
node.isEnd = true;
}
public int sum(String prefix) {
Tire node = tire;
for (int i=0;i<prefix.length();i++){
int index = prefix.charAt(i) - 'a';
if (node.children[index] == null){
return 0;
}
node = node.children[index];
}
int sum = dfs(node,prefix);
return sum;
}
public int dfs(Tire node,String prefix){
int sum = 0;
if (node.isEnd){
sum += map.get(prefix);
}
for (int i=0;i<26;i++){
if (node.children[i] != null){
sum += dfs(node.children[i],prefix+String.valueOf(x[i]));
}
}
return sum;
}
}
/**
* Your MapSum object will be instantiated and called as such:
* MapSum obj = new MapSum();
* obj.insert(key,val);
* int param_2 = obj.sum(prefix);
*/
剑指 Offer II 067. 最大的异或
难度:中等
给你一个整数数组 nums
,返回 nums[i] XOR nums[j]
的最大运算结果,其中 0 ≤ i ≤ j < n
。
解题思路:
- 构建字典树,二进制,所以只有两位0,1
- 异或最大,所以最好两个数,每一位都不相同,且异或要大,首先数要大,1在高位的多
- 所以字典树反向插入二进制num,先比较高位不一样的,高位不一样,最终结果大
- 将所有数构建完字典树后,再将每个树进去走一遍,还是从后往前走,每次选择的都是和自己相反的路线,这样才能异或最大,所以本来是0,走1。本来是1,走0。但如果当前位置没人走过,那只能走自己原来的路线
class Solution {
class TrieNode {
TrieNode[] next;
public TrieNode(){
next = new TrieNode[2];
}
}
TrieNode trie;
public int findMaximumXOR(int[] nums) {
int max = Integer.MIN_VALUE;
buildTrie(nums);
for (int n : nums){
max = Math.max(max,searchMax(n));
}
return max;
}
public void buildTrie(int[] nums){
trie = new TrieNode();
for (int num : nums){
TrieNode node = trie;
// 从后往前存,所以是右移
for(int i = 30; i >= 0; i--){
int d = (num >> i) & 1;
if(node.next[d] == null){
node.next[d] = new TrieNode();
}
node = node.next[d];
}
}
}
public int searchMax(int num){
TrieNode node = trie;
int max = 0;
for (int i=30;i>=0;i--){
int d = (num >> i) & 1;
int other = (d-1) * -1; // d = 1 ,other = 0 . d = 0 ,other = 1
// 另外一条路走不通只能走原路
if (node.next[other] == null){
node = node.next[d];
max = max*2 + d;
}else{
node = node.next[other];
max = max*2 + other;
}
}
return num ^ max;
}
}
剑指 Offer II 068. 查找插入位置
难度:简单
给定一个排序的整数数组 nums
和一个整数目标值 target
,请在数组中找到 target
,并返回其下标。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n)
的算法。
解题思路:
- 一个简单的二分查找,结束条件是left往右越界,所以left所在位置就是答案
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0;
int right = nums.length;
while (left < right){
int mid = left + (right - left)/2;
if (nums[mid] == target){
return mid;
}
if (nums[mid] < target){
left = mid + 1;
}
if (nums[mid] > target){
right = mid;
}
}
return left;
}
}
剑指 Offer II 069. 山峰数组的顶部
难度:简单
符合下列属性的数组 arr
称为 山峰数组(山脉数组) :
arr.length >= 3
- 存在
i
(0 < i < arr.length - 1
)使得:arr[0] < arr[1] < ... arr[i-1] < arr[i]
arr[i] > arr[i+1] > ... > arr[arr.length - 1]
给定由整数组成的山峰数组 arr
,返回任何满足 arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1]
的下标 i
,即山峰顶部。
解题思路:
- 二分查找
- 中间值要比两边大,找到即返回
- 如果出现一边倒的情况,就要移动边界往大的那边移,最终条件是left>right跳出
- left < mid < right,升序,代表峰顶在右侧,直接缩小左边界 left = mid + 1
- left > mid > right,降序,代表峰顶在左侧,直接缩小右边界 right = mid
class Solution {
public int peakIndexInMountainArray(int[] arr) {
int left = 0;
int right = arr.length;
while (left < right){
int mid = left + (right-left)/2;
// 中间大两头小
if (arr[mid] > arr[mid+1] && arr[mid] > arr[mid-1])return mid;
// 左 < mid < right
if (arr[mid] < arr[mid+1] && arr[mid] > arr[mid-1])left = mid + 1;
// 左 > mid > right
if (arr[mid] < arr[mid-1] && arr[mid] > arr[mid+1])right = mid;
}
return left;
}
}
剑指 Offer II 070. 排序数组中只出现一次的数字
难度:中等
给定一个只包含整数的有序数组 nums
,每个元素都会出现两次,唯有一个数只会出现一次,请找出这个唯一的数字。
你设计的解决方案必须满足 O(log n)
时间复杂度和 O(1)
空间复杂度。
解题思路:
- 异或:两个相同值异或结果为0,所以最终相同的都抵消了,剩下的就是结果
class Solution {
public int singleNonDuplicate(int[] nums) {
int res = 0;
for (int num : nums){
res ^= num;
}
return res;
}
}
剑指 Offer II 071. 按权重生成随机数
难度:中等
给定一个正整数数组 w
,其中 w[i]
代表下标 i
的权重(下标从 0
开始),请写一个函数 pickIndex
,它可以随机地获取下标 i
,选取下标 i
的概率与 w[i]
成正比。
例如,对于 w = [1, 3]
,挑选下标 0
的概率为 1 / (1 + 3) = 0.25
(即,25%),而选取下标 1
的概率为 3 / (1 + 3) = 0.75
(即,75%)。
也就是说,选取下标 i
的概率为 w[i] / sum(w)
。
解题思路:
- 既然会根据总和算出占的权重,那干脆就按照权重分区域,数字代表出现的次数
- 例如:[1,3,5,7],总和为16
- 0~1 代表第一个,2~4 代表第二个,5~9 代表第三个,10到16代表第四个
- 原数组可被替换成右边界 [1,4,9,16],正好是前缀和
- 在0~16中取随机数就行,然后数组是升序的就可以二分查找,看最终点落在那边
class Solution {
int total;
int[] p;
public Solution(int[] w) {
int n = w.length;
p = new int[n];
p[0] = w[0];
total = w[0];
for (int i=1;i<n;i++){
total += w[i];
p[i] = p[i-1] + w[i]; // 存的是边界
}
}
public int pickIndex() {
int index = (int)(Math.random()*total) + 1;
return search(index);
}
public int search(int index){
int left = 0;
int right = p.length;
while (left < right){
int mid = left + (right - left)/2;
if (p[mid] >= index){
right = mid;
}else{
left = mid + 1;
}
}
return left;
}
}
剑指 Offer II 072. 求平方根
难度:简单
给定一个非负整数 x
,计算并返回 x
的平方根,即实现 int sqrt(int x)
函数。
正数的平方根有两个,只输出其中的正数平方根。
如果平方根不是整数,输出只保留整数的部分,小数部分将被舍去。
解题思路:
- 二分法求解,mid*mid 和 x 比较,不断缩小边界
class Solution {
public int mySqrt(int x) {
int left = 0;
int right = x;
int ans = 0;
while (left <= right){
int mid = left + (right-left)/2;
if ((long)mid*mid <= x){
ans = mid;
left = mid + 1;
}else{
right = mid - 1;
}
}
return ans;
}
}
剑指 Offer II 073. 狒狒吃香蕉
难度:中等
狒狒喜欢吃香蕉。这里有 n
堆香蕉,第 i
堆中有 piles[i]
根香蕉。警卫已经离开了,将在 h
小时后回来。
狒狒可以决定她吃香蕉的速度 k
(单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k
根。如果这堆香蕉少于 k
根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉,下一个小时才会开始吃另一堆的香蕉。
狒狒喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 h
小时内吃掉所有香蕉的最小速度 k
(k
为整数)。
解题思路:
- 二分查找,只要是有序的,就可以
- 最小速度为1,最大速度为最大堆香蕉数量
- 二分查找这个区间,看不同速度下所花的时间最接近h,且不超过h,所需要的最小速度
class Solution {
public int minEatingSpeed(int[] piles, int h) {
int n = piles.length;
Arrays.sort(piles);
int max = piles[n-1];
if (n == h)return max;
// 二分查找最小速度
int left = 1;
int right = max;
int k = max;
while (left < right){
int mid = left + (right-left)/2;
long time = getTime(mid,piles);
if (time > h){
left = mid + 1;
}else{
k = mid;
right = mid;
}
}
return k;
}
public long getTime(int speed,int[] piles){
long time = 0;
for (int p : piles){
time += p/speed;
if (p%speed != 0){
time++;
}
}
return time;
}
}
剑指 Offer II 074. 合并区间
难度:中等
以数组 intervals
表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
解题思路:
- 合并区间,用个优先队列先按照开始时间排个序,开始时间从小到大
- 队列头部就是最小的区间,之后循环从队列中取和前一个做比较
- 因为开始时间已经排好了序,所以只要关心后一个的开始时间是否大于前一个的结束时间。如果大于结束时间,表示是一个重新开始的区间,它的前一个就可以插入到结果集中;如果小于等于结束时间,则合并两个区间,结束时间取两个区间结束时间的最大值
class Solution {
public int[][] merge(int[][] intervals) {
if (intervals.length == 1)return intervals;
PriorityQueue<int[]> queue = new PriorityQueue<int[]>((a,b) -> a[0]-b[0]);
for (int i=0;i<intervals.length;i++){
queue.add(intervals[i]);
}
List<int[]> list = new ArrayList();
int[] pre = queue.poll();
while (!queue.isEmpty()){
int[] next = queue.poll();
if (next[0] <= pre[1]){
pre[1] = Math.max(pre[1],next[1]);
}else{
list.add(pre);
pre = next;
}
}
list.add(pre);
int[][] res = new int[list.size()][2];
for (int i=0;i<list.size();i++){
res[i] = list.get(i);
}
return res;
}
}
剑指 Offer II 075. 数组相对排序
难度:简单
给定两个数组,arr1
和 arr2
,
arr2
中的元素各不相同arr2
中的每个元素都出现在arr1
中
对 arr1
中的元素进行排序,使 arr1
中项的相对顺序和 arr2
中的相对顺序相同。未在 arr2
中出现过的元素需要按照升序放在 arr1
的末尾。
解题思路:
- 统计arr1中arr2出现的每个数字的次数,用map保存
- 未出现的按升序,所以先给arr1排个序,剩下的先用list存起来
- 按照arr2的顺序,且按照出现次数逐个填充,最后将list中的值填入
class Solution {
public int[] relativeSortArray(int[] arr1, int[] arr2) {
HashMap<Integer,Integer> map = new HashMap();
Arrays.sort(arr1);
for (int i : arr2){
map.put(i,0);
}
List<Integer> list = new ArrayList();
for (int i : arr1){
if (map.containsKey(i)){
map.put(i,map.get(i)+1);
}else{
list.add(i);
}
}
int left = 0;
int[] res = new int[arr1.length];
for (int i=0;i<arr2.length;i++){
int size = map.get(arr2[i]);
while (size-- > 0){
res[left++] = arr2[i];
}
}
for (int i=left;i<res.length;i++){
res[i] = list.get(i-left);
}
return res;
}
}
剑指 Offer II 076. 数组中的第 k 大的数字
难度:中等
给定整数数组 nums
和整数 k
,请返回数组中第 k
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
解题思路:
- 用一个优先队列,始终维护k个元素,从小到大排序,队列头部为最小值,每次新元素要加入就和队头比较。大于则插入,弹出队头部元素;小于直接丢弃
- 最终遍历结束,队头部元素就是第k个最大的元素
class Solution {
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> queue = new PriorityQueue<Integer>((a,b) -> a-b);
int i = 0;
for (;i<k;i++){
queue.add(nums[i]);
}
for (;i<nums.length;i++){
if (queue.peek() < nums[i]){
queue.poll();
queue.add(nums[i]);
}
}
return queue.peek();
}
}
剑指 Offer II 077. 链表排序
难度:中等
给定链表的头结点 head
,请将其按 升序 排列并返回 排序后的链表 。
解题思路:
- 归并排序
- 分割节点到最小,分割用的就是快慢指针,当且仅当区间只有一个元素
- 再开始往上合并,合并过程就是合并两个有序链表
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode sortList(ListNode head) {
return getSortList(head,null);
}
public ListNode getSortList(ListNode head,ListNode tail){
if (head == null)return head;
if (head.next == tail){
head.next = null;
return head;
}
ListNode fast = head;
ListNode slow = head;
while (fast != tail){
fast = fast.next;
slow = slow.next;
if (fast != tail){
fast = fast.next;
}
}
ListNode mid = slow;
ListNode noed1 = getSortList(head,mid);
ListNode noed2 = getSortList(mid,tail);
return merge(noed1,noed2);
}
public ListNode merge(ListNode head1,ListNode head2){
ListNode node = new ListNode(0);
ListNode temp = node;
ListNode temp1 = head1;
ListNode temp2 = head2;
while (temp1 != null && temp2 != null){
if (temp1.val < temp2.val){
temp.next = temp1;
temp1 = temp1.next;
}else{
temp.next = temp2;
temp2 = temp2.next;
}
temp = temp.next;
}
if (temp1 != null){
temp.next = temp1;
}else if (temp2 != null){
temp.next = temp2;
}
return node.next;
}
}
剑指 Offer II 078. 合并排序链表
难度:困难
给定一个链表数组,每个链表都已经按升序排列。
请将所有链表合并到一个升序链表中,返回合并后的链表。
解题思路:
- 分治合并的思想,将ListNode数组划分到最小,然后两个链表开始合并
- 分割结束条件
- 头部index等于尾部表示只有一个元素
- 头部index大于尾部,表示分割后有一半是null,没有元素,因为mid+1的缘故
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
return getMerge(lists,0,lists.length-1);
}
public ListNode getMerge(ListNode[] lists,int begin,int end){
if (begin == end)return lists[begin];
if (begin > end)return null;
int mid = (begin + end) >>> 1;
return merge(getMerge(lists,begin,mid),getMerge(lists,mid+1,end));
}
public ListNode merge(ListNode node1,ListNode node2){
if (node1 == null || node2 == null) {
return node1 != null ? node1 : node2;
}
ListNode node = new ListNode(0);
ListNode temp = node;
ListNode temp1 = node1;
ListNode temp2 = node2;
while (temp1 != null && temp2 != null){
if (temp1.val < temp2.val){
temp.next = temp1;
temp1 = temp1.next;
}else {
temp.next = temp2;
temp2 = temp2.next;
}
temp = temp.next;
}
if (temp1 != null){
temp.next = temp1;
}
if (temp2 != null){
temp.next = temp2;
}
return node.next;
}
}
剑指 Offer II 079. 所有子集
难度:中等
给定一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
解题思路:
- 回溯:只能往前不能往后,所以每次传的都是 i + 1
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList();
dfs(nums,0,res,new ArrayList());
return res;
}
public void dfs(int[] nums,int begin,List<List<Integer>> res,List<Integer> list){
res.add(new ArrayList(list));
if (begin == nums.length)return;
for (int i=begin;i<nums.length;i++){
list.add(nums[i]);
dfs(nums,i+1,res,list);
list.remove(list.size()-1);
}
}
}
剑指 Offer II 080. 含有 k 个元素的组合
难度:中等
给定两个整数 n
和 k
,返回 1 ... n
中所有可能的 k
个数的组合。
解题思路:
- 回溯:只往前不往后,返回条件是限定了k个数的组合
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res = new ArrayList();
dfs(n,k,1,res,new ArrayList());
return res;
}
public void dfs(int n,int k,int begin,List<List<Integer>> res,List<Integer> list){
if (k == 0){
res.add(new ArrayList(list));
return;
}
for (int i=begin;i<=n;i++){
list.add(i);
dfs(n,k-1,i+1,res,list);
list.remove(list.size()-1);
}
}
}
剑指 Offer II 081. 允许重复选择元素的组合
难度:中等
给定一个无重复元素的正整数数组 candidates
和一个正整数 target
,找出 candidates
中所有可以使数字和为目标数 target
的唯一组合。
candidates
中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的唯一组合数少于 150
个。
解题思路:
- 回溯:每一位可以重复选取,但只能朝前不能朝后
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList();
dfs(candidates,target,0,res,new ArrayList());
return res;
}
public void dfs(int[] candidates,int target,int begin,List<List<Integer>> res,List<Integer> list){
if (target < 0)return;
if (target == 0){
res.add(new ArrayList(list));
return;
}
for (int i=begin;i<candidates.length;i++){
list.add(candidates[i]);
dfs(candidates,target-candidates[i],i,res,list);
list.remove(list.size()-1);
}
}
}
剑指 Offer II 082. 含有重复元素集合的组合
难度:中等
给定一个可能有重复数字的整数数组 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用一次,解集不能包含重复的组合。
解题思路:
- 回溯:先排序,每个数不能重复使用,所以递归的时候只能选取下一位
- 不能包含重复的组合,也就是说例如: 1、2、2、2,target = 5.只能有一个[1、2、2],如何来区分?这里用到一个boolean,使用过的就标记为 true。以例子来说,每当要选一个数时,就判断和上一个数是否相等,但相等就跳过显然不对,像[1、2、2],选第二个2就会直接被跳过,还得判断上一个数是否正在使用中,如果不在使用中,说明上一个数刚刚被用完,然后回溯,选到了当前,这种就是可以剪枝的情况
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList();
Arrays.sort(candidates);
boolean[] used = new boolean[candidates.length];
dfs(candidates,target,used,0,res,new ArrayList());
return res;
}
public void dfs(int[] candidates,int target,boolean[] used,int begin,List<List<Integer>> res,List<Integer> list){
if (target < 0)return;
if (target == 0){
res.add(new ArrayList(list));
}
for (int i=begin;i<candidates.length;i++){
if (i > 0 && !used[i-1] && candidates[i] == candidates[i-1]){
continue;
}
list.add(candidates[i]);
used[i] = true;
dfs(candidates,target-candidates[i],used,i+1,res,list);
list.remove(list.size()-1);
used[i] = false;
}
}
}
剑指 Offer II 083. 没有重复元素集合的全排列
难度:中等
给定一个不含重复数字的整数数组 nums
,返回其 所有可能的全排列 。可以 按任意顺序 返回答案。
解题思路:
- 回溯:每个元素都要用上,用过的不能再用,用boolean标记
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList();
boolean[] used = new boolean[nums.length];
dfs(nums,0,used,res,new ArrayList());
return res;
}
public void dfs(int[] nums,int depth,boolean[] used,List<List<Integer>> res,List<Integer> list){
if (depth == nums.length){
res.add(new ArrayList(list));
return;
}
for (int i=0;i<nums.length;i++){
if (!used[i]){
used[i] = true;
list.add(nums[i]);
dfs(nums,depth+1,used,res,list);
used[i] = false;
list.remove(list.size()-1);
}
}
}
}
剑指 Offer II 084. 含有重复元素集合的全排列
难度:中等
给定一个可包含重复数字的整数集合 nums
,按任意顺序 返回它所有不重复的全排列。
解题思路:
- 回溯:算是前面几种的合并,加上了去重
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> res = new ArrayList();
Arrays.sort(nums);
boolean[] used = new boolean[nums.length];
dfs(nums,0,used,res,new ArrayList());
return res;
}
public void dfs(int[] nums,int depth,boolean[] used,List<List<Integer>> res,List<Integer> list){
if (depth == nums.length){
res.add(new ArrayList(list));
return;
}
for (int i=0;i<nums.length;i++){
if (i > 0 && nums[i] == nums[i-1] && !used[i-1])continue;
if (!used[i]){
used[i] = true;
list.add(nums[i]);
dfs(nums,depth+1,used,res,list);
used[i] = false;
list.remove(list.size()-1);
}
}
}
}
剑指 Offer II 085. 生成匹配的括号
难度:中等
正整数 n
代表生成括号的对数,请设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
解题思路:
- 每一次可以是左括号,也可以是右括号
- 只要记住一点,左括号数肯定要小于等于右括号数,不然就不是有效括号
class Solution {
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList();
dfs(n,n,res,"");
return res;
}
public void dfs(int left,int right,List<String> res,String s){
if (left < 0 || right < 0)return;
if (left == 0 && right == 0){
res.add(s);
return;
}
if (left > right)return;
dfs(left-1,right,res,s+"(");
dfs(left,right-1,res,s+")");
}
}
剑指 Offer II 086. 分割回文子字符串
难度:中等
给定一个字符串 s
,请将 s
分割成一些子串,使每个子串都是 回文串 ,返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
解题思路:
- 先用dp把回文子串都标记出来,方便下面截取的时候直接运用
class Solution {
public String[][] partition(String s) {
int n = s.length();
boolean[][] dp = new boolean[n][n];
for (int i=0;i<n;i++){
for (int j=0;j<n;j++){
dp[i][j] = true;
}
}
for (int i=n-1;i>=0;i--){
for (int j=i+1;j<n;j++){
dp[i][j] = (s.charAt(i) == s.charAt(j) && dp[i+1][j-1]);
}
}
List<List<String>> res = new ArrayList<>();
dfs(res, new ArrayList<>(), s, n, dp, 0);
String[][] ans = new String[res.size()][];
for (int i = 0; i < res.size(); i++) {
ans[i] = new String[res.get(i).size()];
for (int j = 0; j < ans[i].length; j++) {
ans[i][j] = res.get(i).get(j);
}
}
return ans;
}
public void dfs(List<List<String>> res,List<String> list,String s,int length,boolean[][] dp,int begin){
if (begin == length){
res.add(new ArrayList<>(list));
return;
}
for (int i=begin;i<length;i++){
if (dp[begin][i]){
list.add(s.substring(begin, i + 1));
dfs(res, list, s, length, dp, i + 1);
list.remove(list.size() - 1);
}
}
}
}
剑指 Offer II 087. 复原 IP
难度:中等
给定一个只包含数字的字符串 s
,用以表示一个 IP 地址,返回所有可能从 s
获得的 有效 IP 地址 。你可以按任何顺序返回答案。
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0
),整数之间用 '.'
分隔。
例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。
解题思路:
- 分割,IP地址可以分成四部分,每部分可以有1位到3位的数字组成,
- 每次截取后需要判定截取的字符串是否合法,长度超过1的字符串转数字不能以0开头,数字要在0到255之间
- 每次判定截取剩下的字符串个数是否符合要求,[1*depth,3*depth]
class Solution {
public List<String> restoreIpAddresses(String s) {
List<String> res = new ArrayList();
dfs(res,s,0,4,new ArrayList());
return res;
}
public void dfs(List<String> res,String s,int begin,int depth,List<String> list){
if (depth == 1){
String cur = s.substring(begin,s.length());
if (isValid(cur)){
list.add(cur);
if (list.size() == 4){
StringBuffer str = new StringBuffer();
for (int i=0;i<list.size();i++){
str.append(list.get(i) + ".");
}
res.add(str.deleteCharAt(str.length()-1).toString());
}
list.remove(list.size()-1);
}
return;
}
if ((s.length() - begin) < depth || (s.length() - begin) > 3*depth)return;
for (int i=1;i<4;i++){
if (begin + i > s.length() -1)break;
String cur = s.substring(begin,begin+i);
if (isValid(cur)){
list.add(cur);
dfs(res,s,begin+i,depth-1,list);
list.remove(list.size()-1);
}
}
}
public boolean isValid(String s){
int n = Integer.parseInt(s);
if (n < 0 || n > 255)return false;
if (s.length() > 1 && s.charAt(0) == '0')return false;
return true;
}
}
剑指 Offer II 088. 爬楼梯的最少成本
难度:简单
数组的每个下标作为一个阶梯,第 i
个阶梯对应着一个非负数的体力花费值 cost[i]
(下标从 0
开始)。
每当爬上一个阶梯都要花费对应的体力值,一旦支付了相应的体力值,就可以选择向上爬一个阶梯或者爬两个阶梯。
请找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。
解题思路:
- 动态规划:每一层可由前一层或者前两层跳跃来,取花费少的即可,每一层要加上本层的花费才是最终的总花费
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
int[] dp = new int[n];
dp[0] = cost[0];
dp[1] = cost[1];
for (int i=2;i<n;i++){
dp[i] = Math.min(dp[i-1],dp[i-2]) + cost[i];
}
return Math.min(dp[n-1],dp[n-2]);
}
}
剑指 Offer II 089. 房屋偷盗
难度:中等
一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响小偷偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组 nums
,请计算 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
解题思路:
- 动态规划
- 每个房间都是可偷可不偷
- 题目规定相邻房间不能偷,所以状态转移方程就是
- 假设偷当前的,那隔壁的就不能偷,所以钱数是隔一个的dp[i-2] + 本屋子nums[i]
- 假设不偷当前的,就说明隔壁被偷了,所以钱数和之前比没变
class Solution {
public int rob(int[] nums) {
if (nums.length == 1)return nums[0];
int[] dp = new int[nums.length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
for (int i=2;i<nums.length;i++){
dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[nums.length-1];
}
}
剑指 Offer II 090. 环形房屋偷盗
难度:中等
一个专业的小偷,计划偷窃一个环形街道上沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组 nums
,请计算 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
解题思路:
- 环形房屋:如果不偷第一个,就可以偷最后一个;如果偷第一个,最后一个就不能偷
- 两种情况各自动态规划,最终取大的那种情况
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if (n == 1)return nums[0];
if (n == 2)return Math.max(nums[0],nums[1]);
int[] dp1 = new int[n-1];
int[] dp2 = new int[n-1];
// 不取第一个房屋
dp1[0] = nums[1];
dp1[1] = Math.max(nums[1],nums[2]);
for (int i=3;i<n;i++){
dp1[i-1] = Math.max(dp1[i-2],dp1[i-3]+nums[i]);
}
// 不取最后一个房屋
dp2[0] = nums[0];
dp2[1] = Math.max(nums[0],nums[1]);
for (int i=2;i<n-1;i++){
dp2[i] = Math.max(dp2[i-1],dp2[i-2]+nums[i]);
}
return Math.max(dp1[n-2],dp2[n-2]);
}
}
剑指 Offer II 091. 粉刷房子
难度:中等
假如有一排房子,共 n
个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。
当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个 n x 3
的正整数矩阵 costs
来表示的。
例如,costs[0][0]
表示第 0 号房子粉刷成红色的成本花费;costs[1][2]
表示第 1 号房子粉刷成绿色的花费,以此类推。
请计算出粉刷完所有房子最少的花费成本。
解题思路:
- 动态规划:每个房子都有三种方案,每种方案都是由隔壁房子另外两种方案得到,结果取最小
class Solution {
public int minCost(int[][] costs) {
int n = costs.length;
int[][] dp = new int[n][3];
dp[0][0] = costs[0][0];
dp[0][1] = costs[0][1];
dp[0][2] = costs[0][2];
for (int i=1;i<n;i++){
dp[i][0] = Math.min(dp[i-1][1],dp[i-1][2]) + costs[i][0];
dp[i][1] = Math.min(dp[i-1][0],dp[i-1][2]) + costs[i][1];
dp[i][2] = Math.min(dp[i-1][0],dp[i-1][1]) + costs[i][2];
}
return Math.min(Math.min(dp[n-1][0],dp[n-1][1]),dp[n-1][2]);
}
}
剑指 Offer II 092. 翻转字符
如果一个由 '0'
和 '1'
组成的字符串,是以一些 '0'
(可能没有 '0'
)后面跟着一些 '1'
(也可能没有 '1'
)的形式组成的,那么该字符串是 单调递增 的。
我们给出一个由字符 '0'
和 '1'
组成的字符串 s,我们可以将任何 '0'
翻转为 '1'
或者将 '1'
翻转为 '0'
。
返回使 s 单调递增 的最小翻转次数。
解题思路:
- 遍历s,记录1的个数
- 每次遇到0的时候有两种选择,因为遇到0才有可能不是单调递增的,比如110111;
- 当前0翻转成1
- 之前的1翻转0,一次性
- 比较这两种哪个代价小
- max 则是翻转次数,累加的过程
class Solution {
public int minFlipsMonoIncr(String s) {
int max = 0;
int one = 0;
for (int i=0;i<s.length();i++){
if (s.charAt(i) == '0'){
max = Math.min(one,max+1);
}else{
one++;
}
}
return max;
}
}
剑指 Offer II 093. 最长斐波那契数列
难度:中等
如果序列 X_1, X_2, ..., X_n
满足下列条件,就说它是 斐波那契式 的:
n >= 3
- 对于所有
i + 2 <= n
,都有X_i + X_{i+1} = X_{i+2}
给定一个严格递增的正整数数组形成序列 arr
,找到 arr
中最长的斐波那契式的子序列的长度。如果一个不存在,返回 0 。
(回想一下,子序列是从原序列 arr
中派生出来的,它从 arr
中删掉任意数量的元素(也可以不删),而不改变其余元素的顺序。例如, [3, 5, 8]
是 [3, 4, 5, 6, 7, 8]
的一个子序列)
解题思路:
- 动态规划
- 一个 斐波那契 数列,可以根据最后两个数往前倒推前面的数
- 外层循环 i ,表示数列最右侧的值,不断更换倒数第二位 j ,寻找到倒数第三位 k = arr[i] - arr[j],能找到则表示k、j、i 能构成一个序列,然后又可以根据 j , k 作为最后两个值往前取推,也许能找到 x,构成 j、k、x,以此类推
- 这样最开始的 i、j、k 就可以在 j、k、x基础上长度+1,但也许找不到 x,只有 j、k,那么只能 构成 i 、j、k,长度就为3
- 总体就是从头开始遍历,代表搜索的长度在增加,增加长度的同时计算不同长度下的以不同坐标值作为最后两个数 能够组成的最长斐波那契
- j 的范围 i - 1 开始到 0 ,还有个条件:数列是递增的,k + j = i ,k 比 j 小,j + j > i
- 遍历的同时每次计算最大值
class Solution {
public int lenLongestFibSubseq(int[] arr) {
HashMap<Integer,Integer> map = new HashMap();
for (int i=0;i<arr.length;i++){
map.put(arr[i],i);
}
int max = 0;
int n = arr.length;
int[][] dp = new int[n][n];
for (int i=0;i<n;i++){
for (int j=i-1;j >= 0 && arr[j] * 2 > arr[i];j--){
int k = map.getOrDefault(arr[i]-arr[j],-1);
if (k != -1){
dp[j][i] = Math.max(dp[k][j]+1,3);
}
max = Math.max(max,dp[j][i]);
}
}
return max;
}
}
剑指 Offer II 094. 最少回文分割
难度:困难
给定一个字符串 s
,请将 s
分割成一些子串,使每个子串都是回文串。
返回符合要求的 最少分割次数 。
解题思路:
- 先用动态规划把每个子字符串是否是回文标记出来
- 双重循环,i 表示字符串的长度,如果本身字符串就是回文,则当前长度字符串无需切割。否则需要截取,假设从j+1到 i 这个子字符串是回文,那么只需要看0到 j 这个字符串需要切几下,再这基础上+1,遍历寻找最小切割次数
class Solution {
public int minCut(String s) {
int n = s.length();
boolean[][] g = new boolean[n][n];
for (int i=0;i<n;i++){
Arrays.fill(g[i],true);
}
for (int i = n - 1; i >= 0; --i) {
for (int j = i + 1; j < n; ++j) {
g[i][j] = s.charAt(i) == s.charAt(j) && g[i + 1][j - 1];
}
}
int[] dp = new int[n];
Arrays.fill(dp, Integer.MAX_VALUE);
for (int i=0;i<n;i++){
// 如果本身就是回文,无需分割
if (g[0][i]){
dp[i] = 0;
}else{
for (int j=0;j<i;j++){
// 如果 j+1到i是回文,所以只要在0到j的基础上+1
if (g[j+1][i]){
dp[i] = Math.min(dp[i],dp[j]+1);
}
}
}
}
return dp[n-1];
}
}
剑指 Offer II 095. 最长公共子序列
难度:中等
给定两个字符串 text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,
"ace"
是"abcde"
的子序列,但"aec"
不是"abcde"
的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
解题思路:
- 动态规划
- 如果当前位置两个字符相匹配,就看去除这两个字符之前的状态最长的是多少,如今就是在这基础上+1
- 如果当前位置两个字符不匹配,则是延续之前的最大长度,延续的方式可以是从字符串text1往后延续一个,text2不动;也可以是从text2往后延续了一个,text1不动
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int n1 = text1.length();
int n2 = text2.length();
int[][] dp = new int[n1+1][n2+1];
for (int i=1;i<=n1;i++){
for (int j=1;j<=n2;j++){
if (text1.charAt(i-1) == text2.charAt(j-1)){
dp[i][j] = dp[i-1][j-1] + 1;
}else{
dp[i][j] = Math.max(dp[i][j-1],dp[i-1][j]);
}
}
}
return dp[n1][n2];
}
}
剑指 Offer II 096. 字符串交织
难度:中等
给定三个字符串 s1
、s2
、s3
,请判断 s3
能不能由 s1
和 s2
交织(交错) 组成。
两个字符串 s
和 t
交织 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:
s = s1 + s2 + ... + sn
t = t1 + t2 + ... + tm
|n - m| <= 1
- 交织 是
s1 + t1 + s2 + t2 + s3 + t3 + ...
或者t1 + s1 + t2 + s2 + t3 + s3 + ...
提示:a + b
意味着字符串 a
和 b
连接。
解题思路:
- 动态规划
- s1 和 s2 能交织成 s3
- 如果拿 i 表示 s1 的长度,j 表示 s2 的长度,s 表示 s3 的长度,i + j = s
- 所以状态转移假设 i + j 能表示成 s,最后一位 i + j - 1 可以是由 s1 的 i - 1字符表示也可以是由 s2 的 j - 1 字符表示,前提是和 i + j - 1 字符相等
class Solution {
public boolean isInterleave(String s1, String s2, String s3) {
int n1 = s1.length();
int n2 = s2.length();
int n3 = s3.length();
if (n1 + n2 != n3)return false;
boolean[][] dp = new boolean[n1+1][n2+1];
dp[0][0] = true;
for (int i=0;i<=n1;i++){
for (int j=0;j<=n2;j++){
int p = i + j - 1;
if (i > 0){
dp[i][j] = dp[i][j] || (dp[i-1][j] && s1.charAt(i-1) == s3.charAt(p));
}
if (j > 0){
dp[i][j] = dp[i][j] || (dp[i][j-1] && s2.charAt(j-1) == s3.charAt(p));
}
}
}
return dp[n1][n2];
}
}
剑指 Offer II 097. 子序列的数目
难度:困难
给定一个字符串 s
和一个字符串 t
,计算在 s
的子序列中 t
出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE"
是 "ABCDE"
的一个子序列,而 "AEC"
不是)
题目数据保证答案符合 32 位带符号整数范围。
解题思路:
- 动态规划
- t是空字符串,即可匹配任意长度的s
- 逐步扩大 s 的长度,不同长度下,能匹配上 t
- 如果当前两个字符相等:
- 可以是删除两个字符的结果的延续 [i-1][j-1]
- 也可以看作当前 i 不起作用,删除 i 后,看[i-1][j]
- 如果当前两个字符不相等:
- 则当前 i 不起作用,删除 i 后,看 [i-1][j]
class Solution {
public int numDistinct(String s, String t) {
int n = s.length();
int m = t.length();
if (n < m)return 0;
int[][] dp = new int[n+1][m+1];
// t是空字符串,就可以随意匹配s
for (int i=0;i<=n;i++){
dp[i][0] = 1;
}
for (int i=1;i<=n;i++){
for (int j=1;j<=m;j++){
if (s.charAt(i-1) == t.charAt(j-1)){
dp[i][j] = dp[i-1][j] + dp[i-1][j-1];
}else{
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][m];
}
}
剑指 Offer II 098. 路径的数目
难度:中等
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
解题思路:
- 动态规格:左边和上边因为是走直线,所以是1条路径。中间的每个节点都可以从上或者右走到当前格,所以是个累加的过程
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for (int i=0;i<m;i++){
dp[i][0] = 1;
}
for (int j=0;j<n;j++){
dp[0][j] = 1;
}
for (int i=1;i<m;i++){
for (int j=1;j<n;j++){
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
剑指 Offer II 099. 最小路径之和
难度:中等
给定一个包含非负整数的 m x n
网格 grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:一个机器人每次只能向下或者向右移动一步。
解题思路:
- 动态规划:总过的路径进行累加,左边和上边都是直线直接累加。中间的节点格子,都可以是从上边和左边进入,每次选择路径最小的那个,路径要累加
class Solution {
public int minPathSum(int[][] grid) {
int n = grid.length;
int m = grid[0].length;
int[][] dp = new int[n][m];
dp[0][0] = grid[0][0];
for (int i=1;i<n;i++){
dp[i][0] = dp[i-1][0] + grid[i][0];
}
for (int j=1;j<m;j++){
dp[0][j] = dp[0][j-1] + grid[0][j];
}
for (int i=1;i<n;i++){
for (int j=1;j<m;j++){
dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1]) + grid[i][j];
}
}
return dp[n-1][m-1];
}
}
剑指 Offer II 100. 三角形中最小路径之和
难度:中等
给定一个三角形 triangle
,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i
,那么下一步可以移动到下一行的下标 i
或 i + 1
。
解题思路:
- 从上往下找出最小路径,不如从下往上反推最小路径,因为越往上数量越少,每层的每个节点都可以从下一层两个节点中挑选一个最小的作为上升路径,这样往上就能找到最终的最小路径
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
// 从下往上 反推,得到到达当前节点的最短路径
for (int i=triangle.size()-2;i>=0;i--){
List<Integer> list = triangle.get(i);
List<Integer> list1 = triangle.get(i+1);
for (int j=0;j<list.size();j++){
int res = Math.min(list1.get(j),list1.get(j+1)) + list.get(j);
list.set(j,res);
}
}
return triangle.get(0).get(0);
}
}
剑指 Offer II 101. 分割等和子集
难度:简单
给定一个非空的正整数数组 nums
,请判断能否将这些数字分成元素和相等的两部分。
解题思路:
- 背包问题
- 分成一半一半,也就是从nums中挑选数字和为总和的一半
- 要想填满当前大小的背包,每个num都是可选可不选:
- 如果不选,按照上一个的状态dp[i][j]=dp[i−1][j]
- 如果选择,当前正好是背包容量,则直接返回true
- 如果选择,填不满背包容量,剩下的背包容量就是j−nums[i],就去差值被填满的情况,dp[i][j]=dp[i−1][j−nums[i]]
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for (int n:nums){
sum += n;
}
if ((sum%2) == 1)return false;
Arrays.sort(nums);
// 背包问题
boolean[][] dp = new boolean[nums.length][sum/2 +1];
for (int i=0;i<nums.length;i++){
int cur = nums[i];
for (int j=1;j<=sum/2;j++){
if (cur == j){
dp[i][j] = true;
}else if (i > 0 && cur > j){
dp[i][j] = dp[i-1][j];
}else if (i > 0 && cur < j){
dp[i][j] = dp[i-1][j] || dp[i-1][j-cur];
}
}
}
return dp[nums.length-1][sum/2];
}
}
剑指 Offer II 102. 加减的目标值
难度:中等
给定一个正整数数组 nums
和一个整数 target
。
向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1]
,可以在2
之前添加'+'
,在1
之前添加'-'
,然后串联起来得到表达式"+2-1"
。
返回可以通过上述方法构造的、运算结果等于 target
的不同 表达式 的数目。
解题思路:
- DFS:每个数字都可以加或者减,最终目标就是 target = 0,前提把所有数字用一遍
class Solution {
int res = 0;
public int findTargetSumWays(int[] nums, int target) {
dfs(nums,target,0);
return res;
}
public void dfs(int[] nums,int target,int begin){
if (begin == nums.length){
if (target == 0){
res++;
}
return;
}
dfs(nums,target-nums[begin],begin+1);
dfs(nums,target+nums[begin],begin+1);
}
}
剑指 Offer II 103. 最少的硬币数目
难度:中等
给定不同面额的硬币 coins
和一个总金额 amount
。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1
。
你可以认为每种硬币的数量是无限的。
解题思路:
- 动态规划
- 不同金额 i 下 dp[i],所需要的最小硬币数目
- 每种硬币都得试一次,用了当前硬币后,就去找当前金额 i - coins[j] 余下得金额所需的最少硬币,找到的结果上+1就是这次所需的硬币数目,每个硬币循环一次取最小
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount+1];
Arrays.fill(dp,amount+1);
dp[0] = 0;
for (int i=1;i<=amount;i++){
for (int j=0;j<coins.length;j++){
if (i - coins[j] >= 0){
dp[i] = Math.min(dp[i],dp[i-coins[j]]+1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
剑指 Offer II 104. 排列的数目
难度:中等
给定一个由 不同 正整数组成的数组 nums
,和一个目标整数 target
。请从 nums
中找出并返回总和为 target
的元素组合的个数。数组中的数字可以在一次排列中出现任意次,但是顺序不同的序列被视作不同的组合。
题目数据保证答案符合 32 位整数范围。
解题思路:
- 动态规划
- 不同targer下的方案数,当target为0时,不选任何数,就这一个方案,dp[0]=1
- 假设最后一个元素是num,那么dp[i-num]表示的就是选取这个值有多少个方案。不同的num,方案都是不同的,这些方案要进行累加。然后target从0开始逐步增加到目标值
class Solution {
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target+1];
dp[0] = 1;
for (int i=1;i<=target;i++){
for (int n : nums){
if (n <= i){
dp[i] += dp[i-n];
}
}
}
return dp[target];
}
}
剑指 Offer II 105. 岛屿的最大面积
难度:中等
给定一个由 0
和 1
组成的非空二维数组 grid
,用来表示海洋岛屿地图。
一个 岛屿 是由一些相邻的 1
(代表土地) 构成的组合,这里的「相邻」要求两个 1
必须在水平或者竖直方向上相邻。你可以假设 grid
的四个边缘都被 0
(代表水)包围着。
找到给定的二维数组中最大的岛屿面积。如果没有岛屿,则返回面积为 0
。
解题思路:
- 遍历二维数组,从1开始出发,遍历它的上下左右只要是1就继续递归,把走过的1变成0,这样就不会重复取走,同时记录走过的1的个数,最终全部走完返回该片岛屿的数量。然后开始下一个1,也就是开始下一个岛屿,最终维护个最大岛屿的面积
class Solution {
int res = 0;
public int maxAreaOfIsland(int[][] grid) {
int n = grid.length;
int m = grid[0].length;
for (int i=0;i<n;i++){
for (int j=0;j<m;j++){
if (grid[i][j] == 1){
res = Math.max(res,dfs(i,j,n,m,grid));
}
}
}
return res;
}
public int dfs(int i,int j,int n,int m,int[][] grid){
int sum = 0;
if (i < 0 || j < 0 || i > n -1 || j > m - 1 || grid[i][j] == 0)return 0;
if (grid[i][j] == 1){
grid[i][j] = 0;
}
sum += dfs(i+1,j,n,m,grid);
sum += dfs(i-1,j,n,m,grid);
sum += dfs(i,j+1,n,m,grid);
sum += dfs(i,j-1,n,m,grid);
return sum + 1;
}
}
剑指 Offer II 106. 二分图
难度:中等
存在一个 无向图 ,图中有 n
个节点。其中每个节点都有一个介于 0
到 n - 1
之间的唯一编号。
给定一个二维数组 graph
,表示图,其中 graph[u]
是一个节点数组,由节点 u
的邻接节点组成。形式上,对于 graph[u]
中的每个 v
,都存在一条位于节点 u
和节点 v
之间的无向边。该无向图同时具有以下属性:
- 不存在自环(
graph[u]
不包含u
)。 - 不存在平行边(
graph[u]
不包含重复值)。 - 如果
v
在graph[u]
内,那么u
也应该在graph[v]
内(该图是无向图) - 这个图可能不是连通图,也就是说两个节点
u
和v
之间可能不存在一条连通彼此的路径。
二分图 定义:如果能将一个图的节点集合分割成两个独立的子集 A
和 B
,并使图中的每一条边的两个节点一个来自 A
集合,一个来自 B
集合,就将这个图称为 二分图 。
如果图是二分图,返回 true
;否则,返回 false
。
解题思路:
- DFS
- 二分图,就是能把节点分为两个阵营,相邻节点肯定是两个阵营的
- 从0出发遍历一遍,看和0相邻的是哪些节点。假设0属于其中一个阵营,那和它相连的肯定是另一个阵营的。这里做个阵营标识随意都可,0作为一个阵营就赋值为1,相邻的节点作为另外阵营赋值为-1。然后递归遍历相邻节点,找到和它们相邻的节点,然后这些新的节点应该赋值1,相邻的相邻就是同一阵营。如果都能成功赋值一遍,表示找到了所求节点,如果在赋值阵营的时候发现它已经属于另一阵营,那就直接返回false
class Solution {
int[] color;
public boolean isBipartite(int[][] graph) {
int n = graph.length;
color = new int[n];
for (int i=0;i<n;i++){
if (color[i] == 0){
if (!dfs(i,graph,1)){
return false;
}
}
}
return true;
}
public boolean dfs(int i,int[][] graph,int col){
color[i] = col;
// 该点和别的点连接
int[] conn = graph[i];
for (int con : conn){
if (color[con] == 0){
// 赋值和当前数的相反数
if (!dfs(con,graph,-col)){
return false;
}
}else{
// 如果有值,那肯定不能相等,相等表示无法分一边
if (color[con] == col){
return false;
}
}
}
return true;
}
}
剑指 Offer II 107. 矩阵中的距离
难度:中等
给定一个由 0
和 1
组成的矩阵 mat
,请输出一个大小相同的矩阵,其中每一个格子是 mat
中对应位置元素到最近的 0
的距离。
两个相邻元素间的距离为 1
。
解题思路:
- 动态规划
- 每个点的选择都可以是:
- 向左,向上
- 向右,向上
- 向左,向下
- 向右,向下
- 可以归结为一条往上的路,和一条往下的路。也就是从左上到右下,从右下到左上,每次计算当前格子的最小值
class Solution {
public int[][] updateMatrix(int[][] mat) {
int n = mat.length;
int m = mat[0].length;
int[][] dp = new int[n][m];
for (int i = 0; i < n; i++) {
Arrays.fill(dp[i], Integer.MAX_VALUE / 2);
}
for (int i=0;i<n;i++){
for (int j=0;j<m;j++){
if (mat[i][j] == 0){
// 把0置为0
dp[i][j] = 0;
}
}
}
// 从左上遍历到右下
// 遍历的过程是左上先dp,然后慢慢右下,右下的dp依赖于左上
// 所以右下的dp的每一步,是往左,往上
for (int i=0;i<n;i++){
for (int j=0;j<m;j++){
if (i - 1 >= 0){
dp[i][j] = Math.min(dp[i][j],dp[i-1][j]+1);
}
if (j - 1 >= 0){
dp[i][j] = Math.min(dp[i][j],dp[i][j-1]+1);
}
}
}
// 从右下遍历到左上
for (int i=n-1;i>=0;i--){
for (int j=m-1;j>=0;j--){
if (i + 1 < n){
dp[i][j] = Math.min(dp[i][j],dp[i+1][j]+1);
}
if (j + 1 < m){
dp[i][j] = Math.min(dp[i][j],dp[i][j+1]+1);
}
}
}
return dp;
}
}
剑指 Offer II 108. 单词演变
难度:困难
在字典(单词列表) wordList
中,从单词 beginWord
和 endWord
的 转换序列 是一个按下述规格形成的序列:
- 序列中第一个单词是
beginWord
。 - 序列中最后一个单词是
endWord
。 - 每次转换只能改变一个字母。
- 转换过程中的中间单词必须是字典
wordList
中的单词。
给定两个长度相同但内容不同的单词 beginWord
和 endWord
和一个字典 wordList
,找到从 beginWord
到 endWord
的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0。
解题思路:
- 算是暴力吧
- 把每一位上,能有多少种变化都记录下来
- 例如:hot、dot
- 第一位存 h d
- 第二位存 o
- 第三位存 t
- 更改单词的时候每次变一位,比如从h变成了d,去查找有无存在该单词,有就可以从该单词再出发,循环直至找到endword,记得用过的单词避免重复查找
- 广度优先,每转换一次,count++
class Solution {
HashMap<Integer,HashSet<Character>> map = new HashMap();
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
// 按位置分类,"hot","dot"
// 第一位 h d
// 第二位 o o
// 第三位 t t
// 重复的不用放入
HashSet<String> hashSet = new HashSet();
for (String s : wordList){
hashSet.add(s);
for (int i=0;i<s.length();i++){
HashSet<Character> set = map.getOrDefault(i,new HashSet<Character>());
set.add(s.charAt(i));
map.put(i,set);
}
}
// 不存在endword
if (!hashSet.contains(endWord))return 0;
Queue<String> queue = new LinkedList();
HashSet<String> seen = new HashSet();
queue.add(beginWord);
seen.add(beginWord);
int count = 1;
while (!queue.isEmpty()){
int size = queue.size();
count++;
for (int i=0;i<size;i++){
String curStr = queue.poll();
for (String nextStr : next(curStr, hashSet, seen)) {
// 如果找到了,就返回转换次数
if (nextStr.equals(endWord)) {
return count;
}
queue.add(nextStr);
seen.add(nextStr);
}
}
}
return 0;
}
public List<String> next(String cur,HashSet<String> set,HashSet<String> seen){
List<String> res = new ArrayList();
char[] c = cur.toCharArray();
char temp = '0';
for (Integer i : map.keySet()){
temp = c[i];
for (Character x : map.get(i)){
c[i] = x;
if (set.contains(new String(c)) && !seen.contains(new String(c))){
res.add(new String(c));
}
c[i] = temp;
}
}
return res;
}
}
剑指 Offer II 109. 开密码锁
难度:中等
一个密码锁由 4 个环形拨轮组成,每个拨轮都有 10 个数字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
。每个拨轮可以自由旋转:例如把 '9'
变为 '0'
,'0'
变为 '9'
。每次旋转都只能旋转一个拨轮的一位数字。
锁的初始数字为 '0000'
,一个代表四个拨轮的数字的字符串。
列表 deadends
包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
字符串 target
代表可以解锁的数字,请给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1
。
解题思路:
- 广度优先遍历
- 从0000开始转到目标值
- 每次只能转一次,可以转四个位置,每个位置可上可下,就有八种新数字产生
- 新的数字不能是死亡数字,同时也不能之前出现过。满足条件的可以加入队列,下次从该位置再出发,直到找到目标值
class Solution {
public int openLock(String[] deadends, String target) {
// 如果目标值直接就是0000,无需转动
if (target.equals("0000"))return 0;
// 广度搜索
Queue<String> queue = new LinkedList();
// 装死亡数字
Set<String> deadSet = new HashSet();
// 装重复数字
Set<String> reSet = new HashSet();
// 死亡数字
for (String deadend : deadends){
deadSet.add(deadend);
}
// 死亡数字有0000,也不用转了
if (deadSet.contains("0000"))return -1;
// 从0000开始
queue.add("0000");
reSet.add("0000");
int step = 0;
// 广度遍历开始
while (!queue.isEmpty()){
int size = queue.size();
step++;
for (int i=0;i<size;i++){
String cur = queue.poll();
List<String> list = get(cur);
for (String next:list){
if (next.equals(target)){
return step;
}
// 当不在死亡数组中,也和之前不重复,那就可以进入下一次的遍历过程
if (!deadSet.contains(next) && !reSet.contains(next)){
queue.add(next);
reSet.add(next);
}
}
}
}
return -1;
}
// 0000 转一下, 有4个位置可以转,每个位置可上转可下转,都是可能性,都加入到集合中
public List<String> get(String s){
List<String> list = new ArrayList();
char[] c = s.toCharArray();
for (int i=0;i<c.length;i++){
char x = c[i];
c[i] = nextUp(x);
list.add(new String(c));
c[i] = nextDown(x);
list.add(new String(c));
c[i] = x;
}
return list;
}
public char nextDown(char x){
if (x == '9')return '0';
return (char)(x+1);
}
public char nextUp(char x){
if (x == '0')return '9';
return (char)(x-1);
}
}
剑指 Offer II 110. 所有路径
难度:中等
给定一个有 n
个节点的有向无环图,用二维数组 graph
表示,请找到所有从 0
到 n-1
的路径并输出(不要求按顺序)。
graph
的第 i
个数组中的单元都表示有向图中 i
号节点所能到达的下一些结点(译者注:有向图是有方向的,即规定了 a→b 你就不能从 b→a ),若为空,就是没有下一个节点了。
解题思路:
- DFS
- 从0开始,遍历index=0时,和0相连接点,然后循环继续找下去,直到找到末尾算一种
- 例如: [[1,2],[3],[3],[]]
- 0可以连接1,1找到3
- 0可以连接2,2找到3
class Solution {
public List<List<Integer>> allPathsSourceTarget(int[][] graph) {
int n = graph.length;
List<List<Integer>> res = new ArrayList();
List<Integer> list = new ArrayList();
list.add(0);
dfs(graph,n-1,0,res,list);
return res;
}
public void dfs(int[][] graph,int n,int index,List<List<Integer>> res,List<Integer> list){
int[] cur = graph[index];
for (int i=0;i<cur.length;i++){
if (cur[i] == n){
list.add(cur[i]);
res.add(new ArrayList(list));
list.remove(list.size()-1);
continue;
}
list.add(cur[i]);
dfs(graph,n,cur[i],res,list);
list.remove(list.size()-1);
}
}
}
剑指 Offer II 111. 计算除法
难度:中等
给定一个变量对数组 equations
和一个实数值数组 values
作为已知条件,其中 equations[i] = [Ai, Bi]
和 values[i]
共同表示等式 Ai / Bi = values[i]
。每个 Ai
或 Bi
是一个表示单个变量的字符串。
另有一些以数组 queries
表示的问题,其中 queries[j] = [Cj, Dj]
表示第 j
个问题,请你根据已知条件找出 Cj / Dj = ?
的结果作为答案。
返回 所有问题的答案 。如果存在某个无法确定的答案,则用 -1.0
替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用 -1.0
替代这个答案。
注意:输入总是有效的。可以假设除法运算中不会出现除数为 0 的情况,且不存在任何矛盾的结果。
解题思路:
- 根据题目已知条件,把a/b和b/a都放入到map中去,因为还有可能有a/c和c/a,所以以a为key,List存放那些关联数,又因为除了除数和被除数,还有对应的计算结果,所以用个class对象把它们封装起来
- DFS遍历
class Solution {
class Pair{
String couple;
double value;
public Pair(String couple,double value){
this.couple = couple;
this.value = value;
}
}
HashMap<String,List<Pair>> map = new HashMap();
HashSet<String> visited = new HashSet<>();
double[] res;
public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
res = new double[queries.size()];
Arrays.fill(res,Double.MAX_VALUE);
for (int i=0;i<equations.size();i++){
String word1 = equations.get(i).get(0);
String word2 = equations.get(i).get(1);
List<Pair> list1 = map.getOrDefault(word1,new ArrayList());
list1.add(new Pair(word2,values[i]));
map.put(word1,list1);
List<Pair> list2 = map.getOrDefault(word2,new ArrayList());
list2.add(new Pair(word1,1/values[i]));
map.put(word2,list2);
}
for (int i=0;i<queries.size();i++){
String word1 = queries.get(i).get(0);
String word2 = queries.get(i).get(1);
// 如果有数不存在,例如 x/x 直接返回-1
if (!map.containsKey(word1) || !map.containsKey(word2)){
res[i] = -1;
continue;
}
// 两个存在的数相等,例如 a/a
if (word1.equals(word2)){
res[i] = 1;
continue;
}
visited.add(word1);
dfs(word1, word2, 1.0,i);
visited.remove(word1);
if (res[i] == Double.MAX_VALUE){
res[i] = -1;
}
}
return res;
}
public void dfs(String curr,String target,double c,int i){
if (curr.equals(target)){
res[i] = c;
return;
}
List<Pair> list = map.get(curr);
for (Pair p : list){
if (visited.contains(p.couple))continue;
visited.add(p.couple);
dfs(p.couple,target,c*p.value,i);
visited.remove(p.couple);
}
}
}
剑指 Offer II 112. 最长递增路径
难度:困难
给定一个 m x n
整数矩阵 matrix
,找出其中 最长递增路径 的长度。
对于每个单元格,你可以往上,下,左,右四个方向移动。 不能 在 对角线 方向上移动或移动到 边界外(即不允许环绕)。
解题思路:
- 动态规划+DFS
- 每一格都可以上下左右的走,每走一步步长+1,这里记录走过的升序最长的步长
- 用dp记录下当前格子的最长步数,当别的格子走到当前格子后,如果比当前格子小就可以继续构成升序,也不用重复去走,直接加上该格子的最长步数即可
class Solution {
int[][] dp;
public int longestIncreasingPath(int[][] matrix) {
int n = matrix.length;
int m = matrix[0].length;
int ans = 0;
dp = new int[n][m];
for (int i=0;i<n;i++){
for (int j=0;j<m;j++){
ans = Math.max(ans,dfs(matrix,i,j,n,m,-1));
}
}
return ans;
}
public int dfs(int[][] matrix, int i,int j,int n,int m,int pre){
if (i < 0 || i > n-1 || j < 0 || j > m-1)return 0;
if (matrix[i][j] <= pre)return 0;
if (dp[i][j] != 0)return dp[i][j];
int up = dfs(matrix,i-1,j,n,m,matrix[i][j]);
int down = dfs(matrix,i+1,j,n,m,matrix[i][j]);
int left = dfs(matrix,i,j-1,n,m,matrix[i][j]);
int right = dfs(matrix,i,j+1,n,m,matrix[i][j]);
int max = Math.max(Math.max(up,down),Math.max(left,right));
dp[i][j] = max+1;
return max+1;
}
}
剑指 Offer II 113. 课程顺序
难度:中等
现在总共有 numCourses
门课需要选,记为 0
到 numCourses-1
。
给定一个数组 prerequisites
,它的每一个元素 prerequisites[i]
表示两门课程之间的先修顺序。 例如 prerequisites[i] = [ai, bi]
表示想要学习课程 ai
,需要先完成课程 bi
。
请根据给出的总课程数 numCourses
和表示先修顺序的 prerequisites
得出一个可行的修课序列。
可能会有多个正确的顺序,只要任意返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
解题思路;
- 把课程的依赖关系都变成map,完成课程A,接下来的课程B、C、D就可以上,以A作为key,B、C、D放进list中
- 把所有单独可以完成的课程挑出来,完成该课程,就可以去map找它的后项
- 例如:完成A,就表示可能可以完成B、C、D。还要看B、C、D是否只需要完成前置A就行,可能还需要完成前置E。这里就需要一个数组记录每个课程需要完成多少个前置课程,每完成一个数量减1,当为0的时候表示可以完成当前课程
class Solution {
public int[] findOrder(int numCourses, int[][] prerequisites) {
int[] res = new int[numCourses];
int index = 0;
Queue<Integer> queue = new LinkedList();
int[] r = new int[numCourses];
HashMap<Integer,List<Integer>> map = new HashMap();
// 记录不同课程需要完成的前置课程数量
for (int[] pre : prerequisites){
r[pre[0]]++;
List<Integer> list;
if (map.containsKey(pre[1])){
list = map.get(pre[1]);
}else{
list = new ArrayList();
}
list.add(pre[0]);
map.put(pre[1],list);
}
// 首先记录可以单独完成的课程
for (int i=0;i<numCourses;i++){
if (r[i] == 0){
queue.add(i);
}
}
while (!queue.isEmpty()){
int c = queue.poll();
res[index++] = c;
if (map.containsKey(c)){
List<Integer> list = map.get(c);
for (int n : list){
r[n]--;
if (r[n] == 0){
queue.add(n);
}
}
}
}
for (int i=0;i<numCourses;i++){
if (r[i] != 0)return new int[0];
}
return res;
}
}
剑指 Offer II 114. 外星文字典
难度:困难
现有一种使用英语字母的外星文语言,这门语言的字母顺序与英语顺序不同。
给定一个字符串列表 words
,作为这门语言的词典,words
中的字符串已经 按这门新语言的字母顺序进行了排序 。
请你根据该词典还原出此语言中已知的字母顺序,并 按字母递增顺序 排列。若不存在合法字母顺序,返回 ""
。若存在多种可能的合法字母顺序,返回其中 任意一种 顺序即可。
字符串 s
字典顺序小于 字符串 t
有两种情况:
- 在第一个不同字母处,如果
s
中的字母在这门外星语言的字母顺序中位于t
中字母之前,那么s
的字典顺序小于t
。 - 如果前面
min(s.length, t.length)
字母都相同,那么s.length < t.length
时,s
的字典顺序也小于t
。
解题思路:
- 拓扑排序
- 主要是读懂题目,例如 "wrt","wrf",首先前面的字符串的优先级比后面高,两个字符串第一个不一样的地方就在 t 和 f ,这里就可以构建关系,t 指向了 f,先有 t 再有 f。存的时候也是为了 t 建立一个集合,把类似于 f 的都存在这个集合里面,当 t 已经排好序后,这些排在 t 后的数才有可能跟在后面
- 总体来说就是从入度为0的字母出发,入度为0表示没别的字母排在它的前面,就可以排在前面,然后属于它集合的那些数就释放了当前指向,如果这些字母的入度也为0表示可以排在后面,循环这个过程,把入度为0的字母挨个排序,最后如果还存在有入度的字母则表示存在环
class Solution {
public String alienOrder(String[] words) {
int n = words.length;
List<List<Integer>> list = new ArrayList();
// 初始化好26个字母对应的list
for (int i=0;i<26;i++){
list.add(new ArrayList<Integer>());
}
// 用来装入度的数组
int[] temp = new int[26];
Arrays.fill(temp,-1); // 用来计算入度的
for (String s : words){
for (int i=0;i<s.length();i++){
temp[s.charAt(i)-'a'] = 0; //包含的字母先标记出来,置为0
}
}
// 比较两两字符串,有先后顺序,不同的字符就产生了先后顺序
for (int i=0;i<n;i++){
for (int j=i+1;j<n;j++){
char[] a = words[i].toCharArray();
char[] b = words[j].toCharArray();
int m = Math.min(a.length,b.length);
for (int k=0;k<m;k++){
if (a[k] == b[k]){
// 已经到达的最后一位,且a的长度大于b的长度,会出现环,例如 abc,ab ,c后面又是ab
if (k == m-1 && a.length > b.length)return "";
// 其他情况相同的就pass
continue;
}
// 比较不一样的,不一样的字符就会有指向,a的排在b的前面
// a的集合中就存有b
list.get(a[k]-'a').add(b[k]-'a');
// b[k]这个字符的入度+1
temp[b[k]-'a']++;
break;
}
}
}
StringBuffer str = new StringBuffer();
Queue<Integer> queue = new LinkedList();
// 找到入度为0的可以先入集合
for (int i=0;i<temp.length;i++){
if (temp[i] == 0){
queue.add(i);
str.append((char) (i + 'a'));
}
}
// 逐步消除入度
while (!queue.isEmpty()){
int x = queue.poll();
List<Integer> le = list.get(x);
for (int l : le){
if (--temp[l] == 0){
queue.add(l);
str.append((char) (l + 'a'));
}
}
}
// 如果最后还有入度的字母,表示有环,直接返回
for (int i=0;i<temp.length;i++){
if (temp[i] > 0)return "";
}
return str.toString();
}
}
剑指 Offer II 115. 重建序列
难度:中等
给定一个长度为 n
的整数数组 nums
,其中 nums
是范围为 [1,n]
的整数的排列。还提供了一个 2D 整数数组 sequences
,其中 sequences[i]
是 nums
的子序列。
检查 nums
是否是唯一的最短 超序列 。最短 超序列 是 长度最短 的序列,并且所有序列 sequences[i]
都是它的子序列。对于给定的数组 sequences
,可能存在多个有效的 超序列 。
- 例如,对于
sequences = [[1,2],[1,3]]
,有两个最短的 超序列 ,[1,2,3]
和[1,3,2]
。 - 而对于
sequences = [[1,2],[1,3],[1,2,3]]
,唯一可能的最短 超序列 是[1,2,3]
。[1,2,3,4]
是可能的超序列,但不是最短的。
如果 nums
是序列的唯一最短 超序列 ,则返回 true
,否则返回 false
。
子序列 是一个可以通过从另一个序列中删除一些元素或不删除任何元素,而不改变其余元素的顺序的序列。
解题思路:
- 要想使得排列固定,每个元素指向下一位元素,这个方向肯定是确定的
- 把元素指向的其他元素用一个set存起来,例如 4 —> 1,4 —> 2,这里 [1,2] 放一个集合
- 同时算出每个元素被指向了几次,可能会有一个元素被另一个元素重复指,记得剔除。因为给定的超短序列,不同的序列中会有重复。例如:[[5,2,6,3],[4,1,5,2]],5—>2两次
- 这样没有被指的肯定是序列的开头,例如:[[5,2,6,3],[4,1,5,2]],4就没有被指,就可以消除,同时得消除4的指向1,1就没有被指向,可以删除,1指向了5,然后重复这个过程。其中这个肯定是一一指向的关系,每次只会删除一个,如果有两个需要删除,则说明顺序可以颠倒,就不是超短序列了
class Solution {
public boolean sequenceReconstruction(int[] nums, int[][] sequences) {
int n = nums.length;
int[] r = new int[n+1];
Map<Integer,Set<Integer>> map = new HashMap();
for (int[] se : sequences){
int size = se.length;
for (int i=0;i<size-1;i++){
Set<Integer> set;
if (map.containsKey(se[i])){
set = map.get(se[i]);
}else{
set = new HashSet();
}
if (!set.contains(se[i+1])){
set.add(se[i+1]);
r[se[i+1]]++;
}
map.put(se[i],set);
}
}
Queue<Integer> queue = new LinkedList();
for(int i=1;i<=n;i++){
if (r[i] == 0)queue.add(i);
}
while (!queue.isEmpty()){
// 一次队列中只能有一个
if (queue.size() > 1)return false;
int x = queue.poll();
if (map.containsKey(x)){
Set<Integer> set = map.get(x);
for (int a : set){
if (--r[a] == 0){
queue.add(a);
}
}
}
}
for (int i=1;i<=n;i++){
if (r[i] != 0)return false;
}
return true;
}
}
剑指 Offer II 116. 省份数量
难度:中等
有 n
个城市,其中一些彼此相连,另一些没有相连。如果城市 a
与城市 b
直接相连,且城市 b
与城市 c
直接相连,那么城市 a
与城市 c
间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n
的矩阵 isConnected
,其中 isConnected[i][j] = 1
表示第 i
个城市和第 j
个城市直接相连,而 isConnected[i][j] = 0
表示二者不直接相连。
返回矩阵中 省份 的数量。
解题思路:
- 顺着找下去,把相关联的都弄成一个集合
- 不关联的肯定不在一个集合里面,所以可以用Set。从0开始遍历,把0有关的都放入set中,这样遍历的时候发现存在set中的就表示是之前集合里面的,只有不存在才属于一个新的集合
class Solution {
public int findCircleNum(int[][] isConnected) {
int n = isConnected.length;
Set<Integer> seen = new HashSet();
Queue<Integer> queue = new LinkedList();
int res = 0;
for (int i=0;i<n;i++){
if (!seen.contains(i)){
queue.add(i);
while (!queue.isEmpty()){
int index = queue.poll();
for (int j=0;j<n;j++){
if (isConnected[index][j] == 1 && !seen.contains(j)){
queue.add(j);
seen.add(j);
}
}
}
res++;
}
}
return res;
}
}
剑指 Offer II 117. 相似的字符串
难度:困难
如果交换字符串 X
中的两个不同位置的字母,使得它和字符串 Y
相等,那么称 X
和 Y
两个字符串相似。如果这两个字符串本身是相等的,那它们也是相似的。
例如,"tars"
和 "rats"
是相似的 (交换 0
与 2
的位置); "rats"
和 "arts"
也是相似的,但是 "star"
不与 "tars"
,"rats"
,或 "arts"
相似。
总之,它们通过相似性形成了两个关联组:{"tars", "rats", "arts"}
和 {"star"}
。注意,"tars"
和 "arts"
是在同一组中,即使它们并不相似。形式上,对每个组而言,要确定一个单词在组中,只需要这个词和该组中至少一个单词相似。
给定一个字符串列表 strs
。列表中的每个字符串都是 strs
中其它所有字符串的一个 字母异位词 。请问 strs
中有多少个相似字符串组?
字母异位词(anagram),一种把某个字符串的字母的位置(顺序)加以改换所形成的新词。
解题思路:
- 直白版本
- 先用一个Set把元素都存储下来
- 随便从一个字符串出发,找到它与set集合中相似的字符串,这些字符串就是属于一类的那种,同时这些字符串也需要再次去寻找与它们相似的字符串,循环找下去 ,寻找过的就从set集合中剔除,由于不要在遍历的同时删除元素,所以需要删除的元素都存起来,等待遍历结束后统一删除
- 每从一个字符串出发找到所有就是一个大的集合,count+1
- 比较两个字符串相似,就是它们只有两位是不一样的,如果超过2位,那就说明不是
解题思路:
- 并查集
- 相似的字符串就连通起来,利用位置替换的方式,连通的字符串都会有一个相同的根节点。如果两个字符串根节点是同一个,说明已经连通起来,如果两个字符串的根节点不是同一个,还要看是否相似,相似才能把两个集合合并。最终查看根节点的个数即可
class Solution {
Set<String> set = new HashSet();
int count = 0;
public int numSimilarGroups(String[] strs) {
for (String s : strs){
set.add(s);
}
Queue<String> queue = new LinkedList();
dfs(queue);
return count;
}
public void dfs(Queue<String> queue){
if (set.size() == 0)return;
for (String s : set){
queue.add(s);
break;
}
count++;
while (!queue.isEmpty()){
String ss = queue.poll();
List<String> list = new ArrayList();
for (String x : set){
if (isSimilar(ss,x)){
list.add(x);
}
}
for (String new_ss : list){
queue.add(new_ss);
set.remove(new_ss);
}
}
dfs(queue);
}
public boolean isSimilar(String s1, String s2) {
int n = s1.length();
int cnt = 0;
for(int i = 0; i < n; ++i) {
if(s1.charAt(i) != s2.charAt(i)) {
if(++cnt > 2) {
return false;
}
}
}
return true;
}
}
class Solution {
int[] f;
public int numSimilarGroups(String[] strs) {
// 并查集
int n = strs.length;
f = new int[n]; //相似的字符串连在一起
for (int i=0;i<n;i++){
f[i] = i; // 每一位的位置都先是自己
}
// 字符串两两比较
for (int i=0;i<n;i++){
for (int j=i+1;j<n;j++){
int a = find(i);
int b = find(j);
if (a == b)continue; // 已经属于一个集合
if (isSimilar(strs[i],strs[j])){ // 如果相似就要合并了
f[a] = b; // f[i] 存放的就是 j 的坐标
}
}
}
int res = 0;
for (int i=0;i<n;i++){
if (f[i] == i)res++; // 有多少个没有动过位置的,就有多少个父节点,就有多少个集合
}
return res;
}
public int find(int x){
if (f[x] != x){
f[x] = find(f[x]);
}
return f[x];
}
public boolean isSimilar(String s1, String s2) {
int n = s1.length();
int cnt = 0;
for(int i = 0; i < n; ++i) {
if(s1.charAt(i) != s2.charAt(i)) {
if(++cnt > 2) {
return false;
}
}
}
return true;
}
}
剑指 Offer II 118. 多余的边
难度:中等
树可以看成是一个连通且 无环 的 无向 图。
给定往一棵 n
个节点 (节点值 1~n
) 的树中添加一条边后的图。添加的边的两个顶点包含在 1
到 n
中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n
的二维数组 edges
,edges[i] = [ai, bi]
表示图中在 ai
和 bi
之间存在一条边。
请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n
个节点的树。如果有多个答案,则返回数组 edges
中最后出现的边。
解题思路:
- 查并集
- 一开始每个节点放的都是自己,一条边的出现使得两个节点关联起来了
- 所以正常情况下,节点都是不关联的,要组成一棵树,才会关联起来,它们都有同一个根节点。当插入的时候发现两节点已经有共同的根节点了,就说明已经组成了树的一部分,当前的输入就是无效的
class Solution {
public int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
int[] res = new int[n+1];
for(int i=0;i<=n;i++){
res[i]=i;
}
for (int[] edge : edges){
int a = find(res,edge[0]);
int b = find(res,edge[1]);
// 有共同祖先,是多余的边
if (a == b){
return edge;
}else{
// 不是一个祖先,祖先合并
union(res,a,b);
}
}
return new int[0];
}
public int find(int[] res,int i){
if (res[i] != i){
res[i] = find(res,res[i]);
}
return res[i];
}
public void union(int[] res,int i,int j){
res[i] = j;
}
}
剑指 Offer II 119. 最长连续序列
难度:中等
给定一个未排序的整数数组 nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
解题思路:
- 先把元素都塞入set中去个重,同时也是便于查找
- 连续的数字序列,要么从大数到小,要么从小数到大
- 本文给出的就是从小数到大
- 因为没有排过序,所以不知道最小一位在哪里
- 干脆每个元素判断有没有下一位,例如:当前4,看看3在不在。如果存在,表示还有更小的,直到遍历到的数字发现没有下一位,也就可以说明当前已经最小,就可以向上查找,从小到大从set中寻找,统计连续的个数
class Solution {
public int longestConsecutive(int[] nums) {
Set<Integer> set = new HashSet();
for (int n:nums){
set.add(n);
}
int res = 0;
for (int i=0;i<nums.length;i++){
// 如果包含下一位就直接跳过,最后是从下往上计算
if (set.contains(nums[i]-1)){
continue;
}else{
// 遇到不包含的就往上看有没有连号的
int n = nums[i]+1;
int count = 1;
while (set.contains(n)){
n++;
count++;
}
res = Math.max(res,count);
}
}
return res;
}
}