三/四数之和类型
一、三数之和
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请你返回所有和为 0
且不重复的三元组。
最接近的三数之和
给你一个长度为 n
的整数数组 nums
和 一个目标值 target
。请你从 nums
中选出三个整数,使它们的和与 target
最接近。
思路:
两道题的做法都是类似的,先固定一个起始位置,然后在剩余的范围里面使用双指针进行遍历(前提是要先对数组进行排序),遍历的过程中找符合条件的结果。
注意:在三数之和这道题中,涉及到结果去重。如何去重:当遍历到的下一个数字如果和上一个数字是相同的,就直接continue; 这个是外层for循环的去重逻辑
if (i > 0 && nums[i] == nums[i - 1])
continue;
内层双指针的去重逻辑:
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
right--; // 移动右指针并重新计算 sum
代码:
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
if (i > 0 && nums[i] == nums[i - 1])
continue;
int left = i + 1;
int right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) {
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
right--; // 移动右指针并重新计算 sum
} else if (sum < 0) {
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
left++; // 移动左指针并重新计算 sum
} else {
List<Integer> part = new ArrayList<>();
Collections.addAll(part, nums[i], nums[left], nums[right]);
res.add(part);
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
right--;
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
left++;
}
}
}
return res;
}
}
二、盛水最多的容器
给定一个长度为 n
的整数数组 height
。有 n
条垂线,第 i
条线的两个端点是 (i, 0)
和 (i, height[i])
。
找出其中的两条线,使得它们与 x
轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
思路:双指针
使用双指针指向容器的两边,此时围成面积的宽是最大的。在双指针不断向里面缩进的同时,宽变小了,要使面积最大,每次就要使两边的高最大。因此移动的时候,要选择高度小的那一方移动。
代码:
class Solution {
public int maxArea(int[] height) {
int max=Integer.MIN_VALUE;
int left=0;
int right=height.length-1;
while(left<right){
int area=(right-left)*Math.min(height[left],height[right]);
max=Math.max(area,max);
if(height[left]<height[right])left++;
else right--;
}
return max;
}
}
三、删除排序链表中的重复元素II
给定一个已排序的链表的头 head
, 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
思路:
重复元素一个也不保留,都删除掉。判断逻辑:因为该链表是排序好的,当该元素和前一个元素、后一个元素都不相等的时候,才说明这个元素不是重复的。因此我用了pre节点、slow节点、fast节点。分别表示前一个节点,当前节点,下一次节点。
如果出现了不重复的元素:
1.可能出现在链表中间:fast.next!=null&&fast.val!=pre.val&&fast.val!=fast.next.val;
2.可能出现这链表结尾:fast.next==null&&fast.val!=pre.val
满足这样的条件之后:slow.next=fast; slow=slow.next;fast=fast.next;pre=pre.next
代码:
class Solution {
public ListNode deleteDuplicates(ListNode head) {
ListNode dummyHead=new ListNode(-101);
dummyHead.next=head;
ListNode fast=head;
ListNode slow=dummyHead;
ListNode pre=dummyHead;
while(fast!=null){
if(fast.next!=null&&fast.val!=pre.val&&fast.val!=fast.next.val){
slow.next=fast;
slow=slow.next;
fast=fast.next;
pre=pre.next;
}else if(fast.next==null&&fast.val!=pre.val){
slow.next=fast;
slow=slow.next;
fast=fast.next;
}else{
fast=fast.next;
pre=pre.next;
}
}
slow.next=null;
return dummyHead.next;
}
}
四、有效的三角形个数(双指针)
给定一个包含非负整数的数组 nums
,返回其中可以组成三角形三条边的三元组个数。
类似于三数之和:只是这个题的条件改变一下。
思路:
1.将数组进行升序排好之后,然后使用双指针(固定right 移动left)
2.如果nums[left]+nums[right]>nums[i],这样的话就找到了可以构成三角形的最小下标,此时count+=right-left;然后将right--(向左移动 看新的情况)
注意:这里向左移动之后,left是不用重新归位的,因为左移之后的总和是<=之前的总和的,因此从left这个位置继续向右移动,寻找>nums[i]的最小下标
3.如果<=的话,left继续向右移动(因为是升序的,left移动的同时,总和肯定是会变大的)
代码:
class Solution {
public int triangleNumber(int[] nums) {
Arrays.sort(nums);
int count=0;
for(int i=nums.length-1;i>=2;i--){
int left=0;
int right=i-1;
while(left<right){
if(nums[left]+nums[right]<=nums[i]){
left++;
}else{
count+=right-left;
right--;
}
}
}
return count;
}
}
五、重复的DNA序列(Set集合)
DNA序列 由一系列核苷酸组成,缩写为 'A'
, 'C'
, 'G'
和 'T'
.。
- 例如,
"ACGAATTCCG"
是一个 DNA序列 。
在研究 DNA 时,识别 DNA 中的重复序列非常有用。
给定一个表示 DNA序列 的字符串 s
,返回所有在 DNA 分子中出现不止一次的 长度为 10
的序列(子字符串)。你可以按 任意顺序 返回答案。
示例 1:
输入:s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT" 输出:["AAAAACCCCC","CCCCCAAAAA"]
思路:
1.可以利用set集合元素唯一的特性去做这道题,遍历String字符串,每次截取10个添加到set集合中。如果add()方法返回的是true,说明是第一次添加;如果返回的false,说明是重复出现的;
2.使用哈希表map<String,Integer>。key:value字符串:出现的次数。遍历字符串,每十个添加到哈希表中,然后遍历一遍把出现次数>1的收集到list中。
代码:
class Solution {
public List<String> findRepeatedDnaSequences(String s) {
Map<String,Integer> map=new HashMap<>();
List<String> res=new ArrayList<>();
for(int i=0;i<=s.length()-10;i++){
String str=s.substring(i,i+10);
map.put(str,map.getOrDefault(str,0)+1);
}
Set<String> keys=map.keySet();
for(String str:keys){
if(map.get(str)>1)res.add(str);
}
return res;
}
}
六、无重复字符的最长子串()
给定一个字符串 s
,请你找出其中不含有重复字符的 最长 子串的长度
输入: s = "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 “abc"
思路:
使用双指针进行遍历,每次左边固定,右边寻找无重复子串的最长子串。
当遇到 map.containsKey(s.charAt(right))的时候,说明遇到重复子串了,此时更新一下当前子串的长度,然后将左边起始位置右移,继续遍历。
没有遇到重复字符的话,就把字符添加到map集合中,
代码:
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character,Integer> map=new HashMap<>();
int max=0;
int res=0;
int left=0;
int right=left;
while(left<=right&&right<s.length()){
char ch=s.charAt(right);
if(map.containsKey(ch)){
map=new HashMap<>();
left++;
right=left;
}else{
right++;
map.put(ch,map.getOrDefault(ch,0)+1);
}
res=Math.max(res,right-left);
}
return res;
}
}
滑动窗口(将顺序转换为数量和种类)
七、字符串的排列
给你两个字符串 s1
和 s2
,写一个函数来判断 s2
是否包含 s1
的排列。如果是,返回 true
;否则,返回 false
。
换句话说,s1
的排列之一是 s2
的 子串 。
输入:s1 = "ab" s2 = "eidbaooo" 输出:true 解释:s2 包含 s1 的排列之一 ("ba").
思路:
0.要判断s2是否包含s1的排列,顺序不一样也算包含,直观的想法:就是将s1所有的排序都列出来,然后判断在s2中是否存在,存在一种就返回true;但是这种方法会超时。
1.如何在s2中找出s1的排列?如果s2中有子串的长度和字符的个数都和s1相同,那么就意味着s2中有s1的排列。
2.如何判断s2中是否拥有连续的和s1中相等的字符和数量?可以借助数组,字母一共有26个,因此如果字符的种类和个数是一样的话,那么数组也一定相同。通过Arrays.equals(arr1,arr2);
3.arr1就是s1串中字符的种类和数量,那么arr2中应该放什么?利用滑动窗口
3.1滑动窗口的大小为s1串的长度时候,就要判断arr2和arr1是否相等;
3.2如果大于s1串长度的时候,左边界就要往左移动(也就是数组中左边界那个字符的次数减一);
3.3如果小于s1串长度的时候,右边界就要向右移动,每次在arr2中新增的字符次数+1;
代码:
class Solution {
public boolean checkInclusion(String s1, String s2) {
//赊账本
int[] arr1=new int[26];
int len1=s1.length();
for(int i=0;i<len1;i++){
int index=s1.charAt(i)-'a';
arr1[index]++;
}
//记账本
int[] arr2=new int[26];
int startIndex=0;
for(int i=0;i<s2.length();i++){
if(i>=len1){
arr2[s2.charAt(i-len1)-'a']--;
}
int index=s2.charAt(i)-'a';
arr2[index]++;
if(Arrays.equals(arr1,arr2))return true;
}
return false;
}
}