三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[[-1, 0, 1],[-1, -1, 2]]
解法一:排序+夹逼(推荐)
1)首先对数组进行排序(涉及数的大小一般都会进行排序处理),有助于去重的实现;
2)排序后固定一个数 nums[i],再使用左右指针,对后面的数组进行夹逼遍历(nums[L]和nums[R]),计算三个数的和 sum 判断是否满足为 0,满足则添加进结果集
3)如果 nums[i]大于 0,则三数之和必然无法等于 0,结束循环;
如果 nums[i] == nums[i-1],则说明该数字重复,会导致结果重复,所以应该跳过;
当 sum == 0 时,nums[L] == nums[L+1]则会导致结果重复,应该跳过,L++;
当 sum == 0时,nums[R] == nums[R-1] 则会导致结果重复,应该跳过,R--;
时间复杂度:,n 为数组长度
public static List<List<Integer>> threeSum2(int[] nums) {
List<List<Integer>> ans = new ArrayList();
int len = nums.length;
if (nums == null || len < 3) {
return ans;
}
Arrays.sort(nums); // 排序
for (int i = 0; i < len; i++) {
if (nums[i] > 0) {
break; // 如果当前数字大于0,则三数之和一定大于0,所以结束循环
}
if (i > 0 && nums[i] == nums[i - 1]) {
continue; // 去重
}
int L = i + 1;
int R = len - 1;
while (L < R) {
int sum = nums[i] + nums[L] + nums[R];
if (sum == 0) {
ans.add(Arrays.asList(nums[i], nums[L], nums[R]));
while (L < R && nums[L] == nums[L + 1]) L++; // 去重
while (L < R && nums[R] == nums[R - 1]) R--; // 去重
L++;
R--;
} else if (sum < 0){
L++;
} else if (sum > 0) {
R--;
}
}
}
return ans;
}
解法二:二重暴力+Hash(Slow)
1)暴力解法为三重for循环遍历所有情况,考虑到第三个数可以由前两个数得到,使用HashMap可以避免第三重循环,从而达到复杂度
2)用Set<List<Integer>>去重,前提是list里的元素的顺序一致;
private List<List<Integer>> hashSolution(int[] nums) {
if (nums == null || nums.length <= 2) {
return Collections.emptyList();
}
Set<List<Integer>> results = new LinkedHashSet<>();
for (int i = 0; i < nums.length - 2; i++) {
Set<Integer> hashSet = new HashSet<>();
for (int j = i + 1; j < nums.length; j++) {
int d = -nums[i] - nums[j];
if (hashSet.contains(d)) {
List<Integer> list = Arrays.asList(nums[i], d, nums[j]);
// 排序, 否则三个相同的数, 但位置不同, 无法去重
list.sort(Comparator.naturalOrder());
results.add(list);
} else {
hashSet.add(nums[j]);
}
}
}
return new ArrayList<>(results);
}
四数之和
双指针
1)注意去重的代码
2)剪枝可节省时间
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> ans = new ArrayList<>();
int n = nums.length;
Arrays.sort(nums);
for (int i = 0; i <= n - 4; i++) {
// 去重
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
// 剪枝
if(nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target){
return ans;
}
if(nums[n-1] + nums[n-2] + nums[n-3] + nums[n - 4] < target){
return ans;
}
for (int j = i + 1; j <= n - 3; j++) {
// 去重
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
// 剪枝
if(nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target){
continue;
}
if(nums[i] + nums[j] + nums[n - 2] + nums[n - 1] < target){
continue;
}
int left = j + 1;
int right = n - 1;
while (left < right) {
// 去重
while (left < right && left > j + 1 && nums[left] == nums[left - 1]) {
left++;
}
while (left < right && right < n - 1 && nums[right] == nums[right + 1]) {
right--;
}
if(left < right){
int sum = nums[i] + nums[j] + nums[left] + nums[right];
if (sum == target) {
ans.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
left++;
right--;
} else if (sum > target) {
right--;
} else {
left++;
}
}
}
}
}
return ans;
}
和为k的子数组
给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。
示例 1 :
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
解法一:滑动窗口
1)先固定一个数nums[start],然后移动另一个指针,求和即可
2)依次移动固定数的位置,重复上述过程
两个指针,两轮遍历,算法复杂度为
class Solution {
public int subarraySum(int[] nums, int k) {
int n = nums.length;
int count = 0;
int lastSum = 0;
for (int start = 0; start < n; start++) {
for (int end = start; end < n; end++) {
if(end == start){
if(nums[start] == k){
count ++;
}
lastSum = nums[end];
}else {
lastSum += nums[end];
if(lastSum == k){
count ++;
}
}
}
}
return count;
}
}
解法二:HashMap+前缀和(推荐)
1)考虑到此题只需给出满足条件的子序列的数量,而不需要具体的子序列:可使用一个map记录所有从0开始的子序列的sum值及其出现次数,通过长序列的sum减去短序列的sum可以得到中间子序列的sum值,所以就不需要两个指针了。
2)建立map表用于存储每个从零项开始的连续子数组的sum求和及其出现的次数,初始化为(0,1),表示和为0的连续子数组(空数组)出现1次。
3)sum的值是在对nums数组的遍历中不断累加当前元素的,res的值则需要查找map中是否已存在sum-k的元素,也就是在查找此前所有从0项开始累加的连续子项和中有没有sum-k。因为:如果有的话,则说明从该项到当前项的连续子数组和必定为k,那么res则可以和这个sum的对应值,即这个sum出现的次数,相加得到新的res。
算法复杂度为O(N)。
class Solution {
public int subarraySum(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
int sum = 0, ret = 0;
// sum为0的子序列出现一次,即空序列;
map.put(0, 1);
for(int i = 0; i < nums.length; ++i) {
sum += nums[i];
if(map.containsKey(sum-k))
ret += map.get(sum-k);
map.put(sum, map.getOrDefault(sum, 0)+1);
}
return ret;
}
}
和可被 K 整除的子数组
给定一个整数数组 A,返回其中元素之和可被 K 整除的(连续、非空)子数组的数目。
示例:
输入:A = [4,5,0,-2,-3,1], K = 5
输出:7
解释:
有 7 个子数组满足其元素之和可被 K = 5 整除:
[4, 5, 0, -2, -3, 1], [5], [5, 0], [5, 0, -2, -3], [0], [0, -2, -3], [-2, -3]
提示:
1 <= A.length <= 30000
-10000 <= A[i] <= 10000
2 <= K <= 10000
解法:前缀和+同余定理
1)通常涉及连续子数组的时候,可以使用前缀和来解决。连续子数组的求和,可以使用前缀和的差来得到。前缀和:,任意连续子数组的求和:。
2)同余定理:即,根据同余定理得到:。
3)根据上面的结论,我们使用一个数组记录前缀和对K取模的结果的数量。即取模结果为0~K-1的各个值的数量。假设对K取模值为r的前缀和共有n个,那么这n个前缀和中任意取2个,即得到一个满足条件的子数组,所以这种情况下,共有个满足条件的子数组。将所有的取模值r,计算求和即可得到最后的解。注意:对于取模值r为0的情况,比较特殊,除了任选两个前缀和的组合情况之外, 每个前缀和本身就是一个满足条件的子数组,所以假设有n个取模值r为0的前缀和,对应情况的子数组个数为:。
public static int subarraysDivByK(int[] A, int K) {
int ret = 0;
int[] counts = new int[K];
// 前缀和
int sum = 0;
for (int i = 0; i < A.length; i++) {
sum += A[i];
// 保证对K取模的结果都为正数0~K-1, Java中-2%6=-2, 我们希望的值是-2%6=4
int mod = sum % K;
if(mod < 0){
mod += K;
}
counts[mod] += 1;
}
for (int i = 0; i < K; i++) {
int count = counts[i];
if (i == 0) {
ret += count + count * (count - 1) / 2;
} else {
ret += count * (count - 1) / 2;
}
}
return ret;
}
无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
滑动窗口
依次递增地枚举子串的起始位置,那么子串的结束位置也是递增的!这里的原因在于,假设我们选择字符串中的第 k个字符作为起始位置,并且得到了不包含重复字符的最长子串的结束位置为 rk。那么当我们选择第 k+1 个字符作为起始位置时,首先从 k+1 到 rk的字符显然是不重复的,并且由于少了原本的第 k 个字符,我们可以尝试继续增大 rk ,直到右侧出现了重复字符为止。
这样以来,我们就可以使用「滑动窗口」来解决这个问题了:
我们使用两个指针表示字符串中的某个子串(的左右边界)。其中左指针代表着上文中「枚举子串的起始位置」,而右指针即为上文中的 rk;
在每一步的操作中,我们会将左指针向右移动一格,表示 我们开始枚举下一个字符作为起始位置,然后我们可以不断地向右移动右指针,但需要保证这两个指针对应的子串中没有重复的字符。在移动结束后,这个子串就对应着 以左指针开始的,不包含重复字符的最长子串。我们记录下这个子串的长度;在枚举结束后,我们找到的最长的子串的长度即为答案。
public static int lengthOfLongestSubstring2(String s) {
int n = s.length();
if (n <= 1) {
return n;
}
int maxLen = 1;
Set<Character> chars = new HashSet<>();
int R = 0;
for(int L = 0; L<n; L++){
if(L > 0){
chars.remove(s.charAt(L-1));
}
while (R < n && !chars.contains(s.charAt(R))){
chars.add(s.charAt(R));
R++;
}
maxLen = Math.max(maxLen, R-L);
}
return maxLen;
}
或者事实上L每次不止可以加1
public static int lengthOfLongestSubstring(String s) {
int n = s.length();
if (n <= 1) {
return n;
}
int maxLen = 1;
Set<Character> chars = new HashSet<>();
int L = 0;
int R = 0;
while (L < n && R < n){
char c = s.charAt(R);
if(chars.contains(c)){
maxLen = Math.max(maxLen, R-L);
while (s.charAt(L) != c){
chars.remove(s.charAt(L));
L++;
}
L++;
}
chars.add(c);
R++;
}
maxLen = Math.max(maxLen, R-L);
return maxLen;
}
数组中重复的数据(https://leetcode-cn.com/problems/find-all-duplicates-in-an-array/)
给定一个整数数组 a,其中1 ≤ a[i] ≤ n (n为数组长度), 其中有些元素出现两次而其他元素出现一次。
找到所有出现两次的元素。
你可以不用到任何额外空间并在O(n)时间复杂度内解决这个问题吗?
示例:
输入:
[4,3,2,7,8,2,3,1]
输出:
[2,3]
利用索引号+取反
利用索引号,因为所有数值是在1~n之间,那么我们可以用索引0表示数字1,索引1表示数字2...。每遍历一次数组,就将此数的index对应的原来正的数字取负,若此index对应的数为负,则说明已出现过;并且取负之后仍然可以取到正确的位置值(取绝对值即可)。
public static List<Integer> findDuplicates(int[] nums) {
List<Integer> ans = new ArrayList<>();
for (int num : nums){
int index = Math.abs(num)-1;
if(nums[index] < 0){
ans.add(Math.abs(num));
}
nums[index] = -nums[index];
}
return ans;
}
最长湍流子数组
当 A 的子数组 A[i], A[i+1], ..., A[j] 满足下列条件时,我们称其为湍流子数组:
若 i <= k < j,当 k 为奇数时, A[k] > A[k+1],且当 k 为偶数时,A[k] < A[k+1];
或 若 i <= k < j,当 k 为偶数时,A[k] > A[k+1] ,且当 k 为奇数时, A[k] < A[k+1]。
也就是说,如果比较符号在子数组中的每个相邻元素对之间翻转,则该子数组是湍流子数组。
返回 A 的最大湍流子数组的长度。
示例 1:
输入:[9,4,2,10,7,8,8,1,9]
输出:5
解释:(A[1] > A[2] < A[3] > A[4] < A[5])
示例 2:
输入:[4,8,12,16]
输出:2
示例 3:
输入:[100]
输出:1
滑动窗口
public static int maxTurbulenceSize(int[] A) {
if (A.length <= 1) {
return A.length;
}
int ans = 1;
int start = 0;
for(int i=1; i<A.length; i++){
int c = Integer.compare(A[i-1], A[i]);
if(i == A.length-1 || c * Integer.compare(A[i], A[i+1]) != -1){
if(c != 0){ // 避免所有数都一样时, 答案不为1
ans = Math.max(ans, i-start+1);
}
start = i;
}
}
return ans;
}
替换后的最长重复字符(https://leetcode-cn.com/problems/longest-repeating-character-replacement/)
给你一个仅由大写英文字母组成的字符串,你可以将任意位置上的字符替换成另外的字符,总共可最多替换 k 次。在执行上述操作后,找到包含重复字母的最长子串的长度。
注意:
字符串长度 和 k 不会超过 104。
示例 1:
输入:
s = "ABAB", k = 2
输出:
4
解释:
用两个'A'替换为两个'B',反之亦然。
示例 2:
输入:
s = "AABABBA", k = 1
输出:
4
解释:
将中间的一个'A'替换为'B',字符串变为 "AABBBBA"。
子串 "BBBB" 有最长重复字母, 答案为 4。
滑动窗口
本题是比较典型的滑动窗口问题
这类问题一般通过一个滑动窗口就能在O(N)的时间复杂度下求解
本题可以先退化成考虑K=0的情况,此时题目就变成了求解字符串中最长连续子串长度问题了
我们先可以通过这个特例先了解一下滑动窗口的求解过程
上图的求解过程展示中,窗口从左至右不断扩张/滑动,当窗口触达字符串末尾字符时,运算结束,窗口的宽度为最终结果。初始窗口的宽度为1,我们不断的通过向当前窗口覆盖的子串后面追加一个字符看是否能满足我们的要求,如果满足窗口扩张,如果不满足,窗口向右滑动。
当K>0时,子串的条件变成了允许我们变换子串中的K个字符使其变成一个连续子串
那么这个题的关键点就是我们如何判断一个字符串改变K个字符,能够变成一个连续串
如果当前字符串中的出现次数最多的字母个数 + K >= 串长度,那么这个串就是满足条件的
我们维护一个数组int[26]来存储当前窗口中各个字母的出现次数,left表示窗口的左边界,right表示窗口右边界
窗口扩张:left不变,right++
窗口滑动:left++, right++
public static int characterReplacement(String s, int k) {
int n = s.length();
if (n <= 1 || n <= k + 1) {
return n;
}
int[] charCount = new int[26];
int L = 0;
int R = 0;
charCount[s.charAt(R) - 'A']++;
int maxCount = 1;
while (R < n) {
if(maxCount + k >= R - L + 1){
// 窗口扩大
R++;
if (R < n) {
int index = s.charAt(R) - 'A';
charCount[index]++;
maxCount = Math.max(maxCount, charCount[index]);
}
}else {
// 窗口移动
charCount[s.charAt(L) - 'A']--;
L++;
R++;
if (R < n) {
int index = s.charAt(R) - 'A';
charCount[index]++;
maxCount = Math.max(maxCount, charCount[index]);// 真实值 <= 这里的值, 不过不影响结果的正确性
}
}
}
return R - L;
}
上述代码可以简化为
public static int characterReplacement(String s, int k) {
int n = s.length();
if (n <= 1 || n <= k + 1) {
return n;
}
int[] charCount = new int[26];
int L = 0;
int R = 0;
charCount[s.charAt(R) - 'A']++;
int maxCount = 1;
while (R < n) {
// 窗口移动
if(maxCount + k < R - L + 1){
charCount[s.charAt(L) - 'A']--;
L++;
}
// 窗口扩大
R++;
if (R < n) {
int index = s.charAt(R) - 'A';
charCount[index]++;
maxCount = Math.max(maxCount, charCount[index]);
}
}
return R - L;
}
16. 最接近的三数之和](https://leetcode-cn.com/problems/3sum-closest/)
给定一个包括 n 个整数的数组 nums
和 一个目标值 target
。找出 nums
中的三个整数,使得它们的和与 target
最接近。返回这三个数的和。假定每组输入只存在唯一答案。
示例:
输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。
提示:
3 <= nums.length <= 10^3
-10^3 <= nums[i] <= 10^3
-10^4 <= target <= 10^4
双指针
public static int threeSumClosest(int[] nums, int target) {
Arrays.sort(nums);
int ans = nums[0] + nums[1] + nums[2];
for(int fix = 0; fix < nums.length; fix ++){
int L = fix + 1;
int R = nums.length - 1;
while (L < R){
int sum = nums[fix] + nums[L] + nums[R];
if(Math.abs(sum - target) < Math.abs(ans - target)){
ans = sum;
}
if(sum - target > 0){
R--;
}else if(sum == target){
return target;
}else {
L++;
}
}
}
return ans;
}
长度最小的子数组
双指针(滑动伸缩窗口)
技巧总结:
1)求连续子数组长度,自然联想到双指针;
2)涉及连续子数组的和,可以联想到前缀数组(暴力解法中才会用到,双指针解法中不用);
3)双指针的实现:两个指针+while循环。(或者for循环中嵌套while循环)
时间复杂度为O(n),因为每个元素最多遍历两遍,左指针一遍,右指针一遍。
public int minSubArrayLen(int s, int[] nums) {
int n = nums.length;
if(n == 0){
return 0;
}
int minLen = 0;
int L = 0;
int R = 0;
int sum = nums[R];
while (L <= R && R < n){
if(sum >= s){
int len = R - L + 1;
minLen = minLen == 0 ? len : Math.min(len, minLen);
sum -= nums[L++];
}else{
R ++;
if(R < n){
sum += nums[R];
}
}
}
return minLen;
}
颜色分类
计数排序
public void sortColors(int[] nums) {
int[] count = new int[3];
for(int num : nums){
count[num] ++;
}
int index = 0;
for(int i=0; i<count.length; i++){
while (count[i]-- > 0){
nums[index++] = i;
}
}
}
多指针
- curr指针,遍历数组,将0元素放在最左边,将2元素放在最右边,1元素继续;通过交换元素操作;
- p0指针,指向左边0元素的最右边界;
- p2指针,指向右边2元素的最左边界;
public void sortColors(int[] nums) {
int curr = 0;
int p0 = 0;
int p2 = nums.length-1;
while (curr < p2){
if(nums[curr] == 0){
swap(nums, p0, curr);
p0 ++;
curr++;
}else if(nums[curr] == 1){
curr++;
}else {
swap(nums, p2, curr);
p2--;
}
}
}
public void swap(int[] nums, int index1, int index2){
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
字符串的排列
数组匹配
- 我们可以使用数组来存储各字符的出现频率,然后判断两个数组是否相等来判断两个长度相同的字符串是否互为排列。给定字符串仅包含小写字母('a'到'z')。因此我们需要采用大小为 26 的数组。
public boolean checkInclusion(String s1, String s2) {
int len = s1.length();
int[] count1 = getCount(s1);
for (int i = 0; i <= s2.length() - len; i++) {
String sub = s2.substring(i, i + len);
if (match(count1, getCount(sub))) {
return true;
}
}
return false;
}
public int[] getCount(String s) {
int[] count = new int[26];
for (char c : s.toCharArray()) {
count[c - 'a']++;
}
return count;
}
public boolean match(int[] count1, int[] count2) {
for (int i = 0; i < 26; i++) {
if (count1[i] != count2[i]) {
return false;
}
}
return true;
}
滑动窗口优化
- 在上述的过程中,在对s2进行滑动的时候,都重新计算了子串的计数数组,其实没有必要,因为相邻的数组之间只有微笑的区别,只需要计算第一个窗口的计数数组,后面每移动一步,做出相应改变即可。
public boolean checkInclusion(String s1, String s2) {
int len = s1.length();
int[] count1 = getCount(s1);
if (s2.length() < s1.length()) {
return false;
}
int[] count2 = getCount(s2.substring(0, len));
if (match(count1, count2)) {
return true;
}
for (int i = 1; i <= s2.length() - len; i++) {
count2[s2.charAt(i - 1) - 'a']--;
count2[s2.charAt(i + len - 1) - 'a']++;
if (match(count1, count2)) {
return true;
}
}
return false;
}
public int[] getCount(String s) {
int[] count = new int[26];
for (char c : s.toCharArray()) {
count[c - 'a']++;
}
return count;
}
public boolean match(int[] count1, int[] count2) {
for (int i = 0; i < 26; i++) {
if (count1[i] != count2[i]) {
return false;
}
}
return true;
}
字母异位词分组
跟上题类似
- 将count数组映射成一个字符串key:用"#"连接所有数字即可。
public List<List<String>> groupAnagrams(String[] strs) {
List<List<String>> ans = new ArrayList<>();
Map<String, List<String>> map = new HashMap<>();
for (String str : strs) {
int[] count = getCount(str);
String countKey = getCountKey(count);
if(map.containsKey(countKey)){
map.get(countKey).add(str);
}else {
List<String> list = new ArrayList<>();
list.add(str);
map.put(countKey, list);
}
}
for(List<String> list : map.values()){
ans.add(list);
}
return ans;
}
public String getCountKey(int[] count) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 26; i++) {
sb.append("#").append(count[i]);
}
return sb.toString();
}
public int[] getCount(String s) {
int[] count = new int[26];
for (char c : s.toCharArray()) {
count[c - 'a']++;
}
return count;
}