leetcode剑指Offer专项版1-20
4、只出现一次的数字
1、题目:给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。
- 示例 1:
输入:nums = [2,2,3,2]
输出:3 - 示例 2:
输入:nums = [0,1,0,1,0,1,100]
输出:100 - 提示:
1 <= nums.length <= 3 * \(10^4\)
-\(2^{31}\) <= nums[i] <= \(2^{31}\) - 1
nums 中,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 - 进阶:你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
2、思路:如果我们可以将所有的数字都转化为二进制,然后根据每一位对3其余,如图所示:
最方便的是我们创建一个32位长度的数组,然后针对每个数字的二进制位对该数组对应下标进行累加,最终计算结果。
但为了严格按照题目要求,我们通过左位移配合二进制加法来实现累加操作,具体如图:
class Solution {
public int singleNumber(int[] nums) {
/*
所有的值>>2,输出1的数量并%3*2^0,
所有值>>2,输出1的数量并%3*2^1
*/
int res=0;
for(int i=0;i<32;i++){
int num=0;
for(int j=0;j<nums.length;j++){
num+=(nums[j]&1);
nums[j]>>=1;
}
num%=3;
if(num!=0)res=res|1<<i;
}
return res;
}
}
5. 单词长度的最大乘积
1、题目给定一个字符串数组 words,请计算当两个字符串 words[i] 和 words[j] 不包含相同字符时,它们长度的乘积的最大值。假设字符串中只包含英语的小写字母。如果没有不包含相同字符的一对字符串,返回 0。
- 示例 1
输入: words = ["abcw","baz","foo","bar","fxyz","abcdef"]
输出: 16
解释: 这两个单词为 "abcw", "fxyz"。它们不包含相同字符,且长度的乘积最大。 - 示例 2:
输入: words = ["a","ab","abc","d","cd","bcd","abcd"]
输出: 4
解释: 这两个单词为 "ab", "cd"。 - 示例 3:
输入: words = ["a","aa","aaa","aaaa"]
输出: 0
解释: 不存在这样的两个单词。
- 提示:
2 <= words.length <= 1000
1 <= words[i].length <= 1000
words[i] 仅包含小写字母
2、思路:位运算
我们可以用二进制来表示出现的字母,出现的字母记作1。
a可以用1左移0位表示,
b用1左移2位表示...
z用1左移25位表示。
例如:100101,表示出现过a、c和f
如果两个字符串不存在相同字母,那么这两个int值的且值 & 必定是0;
class Solution {
public static int maxProduct(String[] words) {
int[] num=new int[1000];
int res=0;
for (int i=0 ;i<words.length;i++) {
for (int j = 0; j < words[i].length(); j++) {
num[i]|=1<<(words[i].charAt(j)-'a');
}
}
for (int i=0 ;i<words.length;i++) {
for (int j=0 ;j<words.length;j++) {
if ((num[i]&num[j])==0)
res=Math.max(words[i].length()*words[j].length(),res);
}
}
return res;
}
}
7. 数组中和为 0 的三个数
1、题目
给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a ,b ,c ,使得 a + b + c = 0 ?
请找出所有和为 0 且 不重复 的三元组。
- 提示:
0 <= nums.length <= 3000
\(-10 ^ 5\) <= nums[i] <= \(10 ^ 5\) - 示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]] - 示例 2:
输入:nums = []
输出:[] - 示例 3:
输入:nums = [0]
输出:[]
2、思路
- 数组排序
- 确定一个固定的指针
- 使用双指针对数组进行遍历
- 注意去重 while (left < right && nums[left] == nums[left + 1]) left++;
class Solution {
public static List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> ans = new ArrayList();
int lg = nums.length;
Arrays.sort(nums);
for (int i = 0; i < lg; i++) {
if (nums[i] > 0) break;
if (i > 0 && nums[i] == nums[i - 1]) continue;
int left = i + 1;
int right = lg - 1;
while (left < right) {
int total = nums[i] + nums[left] + nums[right];
if (total == 0) {
ans.add(Arrays.asList(nums[i], nums[left], nums[right]));
while (left < right && nums[left] == nums[left + 1]) left++;
left++;
} else if (total < 0) left++;
else if (total > 0) right--;
}
}
return ans;
}
}
9. 乘积小于 K 的子数组
1、题目给定一个正整数数组 nums和整数 k ,请找出该数组内乘积小于 k 的连续的子数组的个数。
- 示例 1:
输入: nums = [10,5,2,6], k = 100
输出: 8
解释: 8 个乘积小于 100 的子数组分别为: [10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于100的子数组。 - 示例 2:
- 输入: nums = [1,2,3], k = 0
输出: 0 - 提示:
1 <= nums.length <= 3 *\(10^4\)
1 <= nums[i] <= 1000
0 <= k <= \(10^6\)
2、思路
本题可以使用「滑动窗口」的原因:
- 关键 1:如果一个连续子数组的所有元素的乘积都严格小于 k,那么这个 连续子数组的子集(同样也得保证是连续子数组)的乘积也一定严格小于 k。
原因我们在「关键字」里也向大家强调过,数组里的所有元素都是正整数。 - 关键 2:如果某个连续子数组的乘积大于等于 k,包含它的更长的子数组一定也不满足。
- 基于以上两点,我们可以设计「滑动窗口」算法。因此「滑动窗口」方法是「暴力解法」的优化。
public static int numSubarrayProductLessThanK(int[] nums, int k) {
if (k < 2) return 0;
int res = 0;
int left = 0;
int multi = 1;
int right = 0;
while (right<nums.length){
multi*=nums[right++];
while ( multi >= k) {
multi /= nums[left++];
}
res += right - left ;
}
return res;
}
10. 和为 k 的子数组
1、题目
- 给定一个整数数组和一个整数 k ,请找到该数组中和为 k 的连续子数组的个数。
- 提示:
- 1 <= nums.length <= 2 * 10 ^ 4
-1000 <= nums[i] <= 1000
-10 ^ 7 <= k <= 10 ^ 7
示例 - 示例 1 :
输入:nums = [1,1,1], k = 2
输出: 2
解释: 此题 [1,1] 与 [1,1] 为两种不同的情况 - 示例 2 :
输入:nums = [1,2,3], k = 3
输出: 2
2、分析:
滑动窗口的力所不及
在套模板的同时,大家是否考虑过,假设题目同样是求连续的子数组,但是在数组中出现了负数,那这种情况下还可以使用滑动窗口么?
答案是不行的,为什么?
我们窗口滑动的条件是什么,while窗口内元素超过或者不满足条件时移动,但如果数组存在负数,遇到不满足题意的时候,我们应该移动窗口左边界,还是扩大窗口右边界从而寻找到符合条件的情况呢?
3、如果这道题的取值没有负数,那就是标准的滑窗问题,但因为有了负数,滑窗思想不能用了。
通过分析,这道题应该属于我们上面列举四种情况的最后一种。具体思路如下:
- 初始化一个空的哈希表和pre_sum=0的前缀和变量
- 设置返回值ret = 0,用于记录满足题意的子数组数量
- 循环数组的过程中,通过原地修改数组的方式,计算数组的累加和
- 将当前累加和减去整数K的结果,在哈希表中查找是否存在
- 如果存在该key值,证明以数组某一点为起点到当前位置满足题意,ret加等于将该key值对应的value
- 判断当前的累加和是否在哈希表中,若存在value+1,若不存在value=1
- 最终返回ret即可
- 但在这里要注意刚才说到的前缀和边界问题。
- 我们在计算这种场景时,需要考虑如果以数组nums[0]为开头的连续子数组就满足题意呢?
- 此时候我们的哈希表还是空的,没办法计算前缀和!所以遇到这类题目,都需要在哈希表中默认插入一个{0:1}的键值对,
- 用于解决从数组开头的连续子数组满足题意的特殊场景。
class Solution {
public int subarraySum(int[] nums, int k) {
int pre_sum = 0;
int ret = 0;
HashMap<Integer, Integer> map = new HashMap<>();
map.put(0, 1);
for (int i : nums) {
pre_sum += i;
ret += map.getOrDefault(pre_sum - k, 0);
map.put(pre_sum, map.getOrDefault(pre_sum, 0) + 1);
}
return ret;
}
}
11. 0 和 1 个数相同的子数组
1、问题:给定一个二进制数组 nums , 找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。
- 示例 1:
输入: nums = [0,1]
输出: 2
说明: [0, 1] 是具有相同数量 0 和 1 的最长连续子数组。 - 示例 2:
输入: nums = [0,1,0]
输出: 2
说明: [0, 1] (或 [1, 0]) 是具有相同数量 0 和 1 的最长连续子数组。 - 提示:
1 <= nums.length <= \(10^5\)
nums[i] 不是 0 就是 1
2、分析
我们不妨将所有0转化为-1,那么如果遇到了相同数量的0和1,累加之后的结果就为0,不是就又转化为前缀和的思想了么
解题思路如下:
- 初始化哈希表,并添加{0:-1}
- 声明前缀和变量pre_sum = 0,最大子数组长度返回值ret = 0
- 循环数组,若元素为0,pre_sum - 1,反之pre_sum + 1
- 判断哈希表中是否存在值为pre_sum的key
- 若存在pre_sum的key,使用max函数更新ret。
- 最终返回ret即可
class Solution {
public int findMaxLength(int[] nums) {
HashMap<Integer, Integer> map = new HashMap<>();
map.put(0, -1);
int pre_sum = 0;
int ret = 0;
for (int i = 0; i < nums.length; i++) {
pre_sum += nums[i] == 0 ? -1 : 1;
if (map.containsKey(pre_sum)) {
ret = Math.max(ret, i - map.get(pre_sum));
} else {
map.put(pre_sum, i);
}
}
return ret;
}
}
17. 含有所有字符的最短字符串
1、题目:
给定两个字符串 s 和 t 。返回 s 中包含 t 的所有字符的最短子字符串。如果 s 中不存在符合条件的子字符串,则返回空字符串 "" 。
如果 s 中存在多个符合条件的子字符串,返回任意一个。
注意: 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
- 示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最短子字符串 "BANC" 包含了字符串 t 的所有字符 'A'、'B'、'C' - 示例 2:
输入:s = "a", t = "a"
输出:"a" - 示例 3:
输入:s = "a", t = "aa"
输出:""
解释:t 中两个字符 'a' 均应包含在 s 的子串中,因此没有符合条件的子字符串,返回空字符串。 - 提示:
1 <= s.length, t.length <= \(10^5\)
s 和 t 由英文字母组成
2、分析
- 滑动窗口+临界统计
- 记录好每次符合条件的起始点和长度即可
算法
同样使用两个指针定位字符串 s 的子字符串,其中左指针指向子字符串的第 一个字符,右指针指向最后一个字符。若某一时刻两个指针之间的字符串还没有包含字符串 t 中所有的字符,则右指针右移一位添加字符,若该时刻两个指针之间的字符串已包含字符串 t 中所有的字符,因为目标需要求得符合要求的最短子字符串,则需要右移动左指针从子字符串中删去第一个字符。
class Solution {
public String minWindow(String s, String t) {
int lens = s.length();
int lent = t.length();
if (lent > lens) return "";
int left = 0;
int right = 0;
int minstart = 0;
int minend = lens;
char[] s1 = s.toCharArray();
char[] t1 = t.toCharArray();
HashMap<Character, Integer> hashMap = new HashMap<>();
for (char ch : t1) {
hashMap.put(ch, hashMap.getOrDefault(ch, 0) + 1);
}
int numch = hashMap.size();//字符种类数目
int flag=0;
while (right < lens || numch == 0) {
if (numch != 0) {
if (hashMap.containsKey(s1[right])) {
hashMap.put(s1[right], hashMap.get(s1[right]) - 1);
if (hashMap.get(s1[right]) == 0) {
numch--;
}
}
right++;
} else {
flag=1;
if (minend - minstart > right - left ) {
minend = right;
minstart = left;
}
if (hashMap.containsKey(s1[left])) {
hashMap.put(s1[left], hashMap.get(s1[left]) + 1);
if (hashMap.get(s1[left]) == 1) {
numch++;
}
}
left++;
}
}
if (flag==0)return "";
return s.substring(minstart, minend);
}
}