文章目录
- 一、过滤保序
- 27.移除元素
- 题解
- 题解1:暴力解法
- 题解2:双指针法
- [26. 删除有序数组中的重复项](https://leetcode.cn/problems/remove-duplicates-from-sorted-array/)
- 题解
- [283. 移动零 ](https://leetcode.cn/problems/move-zeroes/description/)
- 题解
- [844. 比较含退格的字符串-易错](https://leetcode.cn/problems/backspace-string-compare/description/)
- 题解
- 题解1:重构字符串
- 题解2:双指针法
- [977. 有序数组的平方](https://leetcode.cn/problems/squares-of-a-sorted-array/)
- 题解
- 二、重构字符串
- [344. 反转字符串](https://leetcode.cn/problems/reverse-string/)
- 题解
- [剑指 Offer 05. 替换空格 ](https://leetcode.cn/problems/ti-huan-kong-ge-lcof/)
- 题解
- 解法1:开额外空间,进行拼接
- 题解2:不开额外空间,双指针法
- [151. 反转字符串中的单词](https://leetcode.cn/problems/reverse-words-in-a-string/)
- 题解
- 方式1:直接用库函数,
- 方式2:不使用库函数
- 自定义trim函数-使用额外空间
- 先去除多余空格在反转-不开额外空间
- 三、重构链表
- [206. 反转链表 ](https://leetcode.cn/problems/reverse-linked-list/)
- 题解
- 双指针法
- 递归法
- [19. 删除链表的倒数第 N 个结点](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/)
- 题解
- [面试题 02.07. 链表相交](https://leetcode.cn/problems/intersection-of-two-linked-lists-lcci/)
- 题解
- 题解1:O(n)空间
- 题解2:双指针法-O(1)空间
- [141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/)
- 题解
- [142. 环形链表 II](https://leetcode.cn/problems/linked-list-cycle-ii/)
- 题解
- 四、几个数之和
- 1.两数之和
- 题解
- 用HashMap
- 代码优化:
- [454. 四数相加 II](https://leetcode.cn/problems/4sum-ii/)
- 题解
- 15.三数之和
- 题解
- 题解1:排序+滑动窗口-三层模板
- 题解2:排序+滑动窗口-利用两数之和模板
- [18. 四数之和-一个数组元素](https://leetcode.cn/problems/4sum/)
- 题解
- **同样,使用排序加双指针**
- **还可以进行一些剪枝操作:**
一、过滤保序
27.移除元素
给你一个数组 nums
和一个值 val
,你需要 原地 移除所有数值等于 val
的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1)
额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以**「引用」**方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
提示:
0 <= nums.length <= 100
0 <= nums[i] <= 50
0 <= val <= 100
题解
题解1:暴力解法
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size = nums.size(); // 记录维护的有效元素大小
// 1.遍历每个元素,进行处理
for(int i = 0; i < size; i++) {
// 2.遇到目标元素,后面所有元素进行移动覆盖
if(nums[i] == val) {
for(int j = i + 1; j < size; j++) {
nums[j - 1] = nums[j];
}
// 3.注意,后面元素移动后,i位置也要向前移动,继续处理i位置新的数
i--;
size--;
}
}
return size; // 不能返回nums.size(),只是覆盖,元素个数没有改变
//nums.resize(size); // 可以重构nums数组实际容量
//return nums.size();
}
};
题解2:双指针法
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
// 1.定义左右指针,右指针寻找元素,左指针保存满足条件的元素
int l = 0, r = 0;
for(; r < nums.size(); r++) {
if(nums[r] != val) { // 排除等于val的元素
nums[l++] = nums[r];
}
}
return l;
}
};
26. 删除有序数组中的重复项
给你一个 升序排列 的数组 nums
,请你** 原地** 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。
由于在某些语言中不能改变数组的长度,所以必须将结果放在数组nums的第一部分。更规范地说,如果在删除重复项之后有 k
个元素,那么 nums
的前 k
个元素应该保存最终结果。
将最终结果插入 nums
的前 k
个位置后返回 k
。
不要使用额外的空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
判题标准:
系统会用下面的代码来测试你的题解:
int[] nums = [...]; // 输入数组
int[] expectedNums = [...]; // 长度正确的期望答案
int k = removeDuplicates(nums); // 调用
assert k == expectedNums.length;
for (int i = 0; i < k; i++) {
assert nums[i] == expectedNums[i];
}
如果所有断言都通过,那么您的题解将被 通过。
示例 1:
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
示例 2:
输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
提示:
1 <= nums.length <= 3 * 104
-104 <= nums[i] <= 104
nums
已按 升序 排列
题解
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
// 1.定义左右指针
int l = 0; // 左指针指向新数组下标
int r = 1; // 右指针寻找原数组元素,过滤重复元素
// 2.过滤保序,防止数组越界,先特殊处理下标为0的元素
nums[l++] = nums[0];
for(; r < nums.size(); r++) {
if(nums[r] != nums[r - 1]) nums[l++] = nums[r];
}
return l;
}
};
283. 移动零
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]
提示:
1 <= nums.length <= 104
-231 <= nums[i] <= 231 - 1
**进阶:**你能尽量减少完成的操作次数吗?
题解
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int l = 0, r = 0;
for(; r < nums.size(); r++) {
if(nums[r] != 0) nums[l++] = nums[r];
}
while(l < nums.size()) nums[l++] = 0;
}
};
844. 比较含退格的字符串-易错
给定 s
和 t
两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true
。#
代表退格字符。
**注意:**如果对空文本输入退格字符,文本继续为空。
示例 1:
输入:s = "ab#c", t = "ad#c"
输出:true
解释:s 和 t 都会变成 "ac"。
示例 2:
输入:s = "ab##", t = "c#d#"
输出:true
解释:s 和 t 都会变成 ""。
示例 3:
输入:s = "a#c", t = "b"
输出:false
解释:s 会变成 "c",但 t 仍然是 "b"。
提示:
1 <= s.length, t.length <= 200
s
和t
只含有小写字母以及字符'#'
进阶:
- 你可以用
O(n)
的时间复杂度和O(1)
的空间复杂度解决该问题吗?
题解
题解1:重构字符串
class Solution {
public:
bool backspaceCompare(string s, string t) {
return build(s) == build(t);
}
private:
string build(string& s) {
string res;
for(char c : s) {
if(c != '#') res.push_back(c);
else if(!res.empty()) res.pop_back();
}
return res;
}
};
题解2:双指针法
- 其实,只要遇到空格字符,会删除空格前面的字符,与空格后面字符无关,每次逆序遍历即可
- 难点是:当有多个空格时,如何将对应的前面普通字符删干净再比较
- 因此,需要维护个变量,记录普通字符后面的空格,
- 只有将后面的空格都抵消完后,才比较前面有效的字符
注意:不能是s[l--] == '#'
,或skipS-- > 0
- 因为,
if else
判断,有可能不走这个分支,但在判断条件时,将变量改了 - 因此,在判断条件里改变变量的习惯不好,除非确定一定走这个分支才行
class Solution {
public:
bool backspaceCompare(string s, string t) {
int l = s.size() -1, r = t.size() -1; // 逆序遍历的左右指针
int skipS = 0, skipT = 0; // 记录有效字符前面的空格数
while(l >= 0 || r >= 0) { // 两组字符数量可以不相等,可能一方先走完
// 1.处理s字符串-有效字符前的退格符对普通字符的消除工作
while(l >= 0) {
if(s[l] == '#') { // 不能是s[l--] == '#'
l--; // 注意,判断时不能先减1,只有真遇到空格才行
skipS++; // 统计有效字符前空格数量
}else if(skipS > 0) {// 不能是skipS-- > 0
skipS--;
l--; // 消除普通字符,有多少空格消除多少
}else break;// 直到遇到有效字符,即前面没有空格的普通字符,跳循环
}
// 2.处理t字符串-有效字符前的退格符对普通字符的消除工作
while(r >= 0) {
if(t[r] == '#') { // 注意,判断时不能先减1,只有真遇到空格才行
r--;
skipT++;
}else if(skipT > 0) {
skipT--;
r--;
}else break;
}
// 3.比较两个消除后留下的有效字符是否相等
if(l >= 0 && r >= 0) { // 都没有走完,才能计较有效字符
if(s[l] != t[r]) return false;
} else if(l >= 0 || r >= 0) return false;//有效字符数目不同,定不等
// 4.即相等,也没有走完,继续下轮循环判断
l--; r--;
}
return true;
}
};
977. 有序数组的平方
给你一个按 非递减顺序 排序的整数数组 nums
,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例 1:
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]
示例 2:
输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums
已按 非递减顺序 排序
进阶:
- 请你设计时间复杂度为
O(n)
的算法解决本问题
题解
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
// 大的肯定在两边,因此,从两边向中间处理数据
int i = 0, j = nums.size() -1;// 原数组两边下标
int n = nums.size();
vector<int> res(n);
int k = n -1;
while(i <= j) {
if(nums[i] * nums[i] > nums[j] * nums[j]) {
res[k--] = nums[i] * nums[i];
i++;
} else {
res[k--] = nums[j] * nums[j];
j--;
}
}
return res;
}
};
二、重构字符串
344. 反转字符串
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s
的形式给出。
不要给另外的数组分配额外的空间,你必须**原地修改输入数组**、使用 O(1) 的额外空间解决这一问题。
示例 1:
输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]
示例 2:
输入:s = ["H","a","n","n","a","h"]
输出:["h","a","n","n","a","H"]
提示:
1 <= s.length <= 105
s[i]
都是 ASCII 码表中的可打印字符
题解
class Solution {
public:
void reverseString(vector<char>& s) {
for(int i = 0, j = s.size()-1; i < j; i++, j--) swap(s[i], s[j]);
}
};
剑指 Offer 05. 替换空格
请实现一个函数,把字符串 s
中的每个空格替换成"%20"。
示例 1:
输入:s = "We are happy."
输出:"We%20are%20happy."
限制:
0 <= s 的长度 <= 10000
题解
解法1:开额外空间,进行拼接
class Solution {
public:
string replaceSpace(string& s) {
string res;
for(char c : s) {
if(c == ' ') res += "%20";
else res += c;
}
return res;
}
};
题解2:不开额外空间,双指针法
- 注意:循环判断的终止条件:
while(i < j)
{ // 不能相等,否则,一直循环下去,跳不出循环- 或者:
while(i <= j) && i >= 0
class Solution {
public:
string replaceSpace(string& s) {
// 1.先记录多少个空格,扩容空间
int nOld = s.size();
int cnt = 0;
for(char c : s) {
if(c == ' ') cnt++;
}
s.resize(nOld + cnt * 2);
int nNew = s.size();
// 2.从后往前添加元素
int i = nOld -1, j = nNew -1; // 左指针指向旧字符,右指针指向新字符
while(i < j) { // 不能相等,否则,一直循环下去,跳不出循环
if(s[i] != ' ') s[j--] = s[i--];
else {
s[j--] = '0';s[j--] = '2';s[j--]= '%';
i--;
}
}
return s;
}
};
151. 反转字符串中的单词
给你一个字符串 s
,请你反转字符串中 单词 的顺序。
单词 是由非空格字符组成的字符串。s
中使用至少一个空格将字符串中的 单词 分隔开。
返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
**注意:**输入字符串 s
中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
示例 1:
输入:s = "the sky is blue"
输出:"blue is sky the"
示例 2:
输入:s = " hello world "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。
示例 3:
输入:s = "a good example"
输出:"example good a"
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。
提示:
1 <= s.length <= 104
s
包含英文大小写字母、数字和空格' '
s
中 至少存在一个 单词
**进阶:**如果字符串在你使用的编程语言中是一种可变数据类型,请尝试使用 O(1)
额外空间复杂度的 原地 解法。
题解
方式1:直接用库函数,
- 将字符串分割得到字符数组,然后进行拼接
- c++标准库中split函数,
- C/C++中的Split函数是
strtok()
其函数原型如下:
char * strtok (char * str, const char * delimiters);
class Solution {
public String reverseWords(String s) {
s = s.trim();
StringBuilder s2 = new StringBuilder();
String[] st = s.split(" ");
for(int i = st.length - 1; i >= 0; i--) {
if(st[i] != "") { // 去除重复的空格字符,空格字符被切分后变成空字符
if(i != 0) s2.append(st[i] + " ");
else s2.append(st[i]);
}
}
return s2.toString();
}
}
方式2:不使用库函数
自定义trim函数-使用额外空间
- 1.先去除首尾多余空格
- 2.去除多余空格,并将整个字符串反转
- 3.再将每个单词字符串反转
class Solution {
public:
string reverseWords(string s) {
// 1.去除首尾空格
s = trim(s); // 可以先去除首尾空格字符,调用erase函数,自动维护size大小
// 2.去除多余空格,反转整个字符串,使用双指针法
int i = 0;
string res; // 重构,会影响原字符串字符
for(int j = s.size() - 1; j >= 0; j--) {
if(s[j] != ' ') { // 过滤保序-不能直接加字符,还要加空格
while(j >= 0 && s[j] != ' ') res += s[j--];
if(j != -1) res += ' ';
}
}
// 2.反转每个单词字符串
int start = 0;
for(int end = 0; end <= res.size(); end++) {
if(end == res.size() || res[end] == ' ') {
reverse(res, start, end);// 左闭右开
start = end + 1;
}
}
return res;
}
private:
string& trim(string &s) {
if(s.empty()) return s;
s.erase(0, s.find_first_not_of(" "));
s.erase(s.find_last_not_of(" ") + 1);
return s;
}
void reverse(string& s, int start, int end) {
for(int i = start, j = end - 1; i < j; i++, j--) swap(s[i], s[j]);
}
};
先去除多余空格在反转-不开额外空间
- 1.先去除空格
- 2.将整个字符串反转
- 3.再将每个单词字符串反转
class Solution {
public:
string reverseWords(string s) {
// 1.先去除多余空格-使用双指针法,先不翻转,可以在原空间处理
//s = trim(s); // 可以先去除首尾空格字符,调用erase函数,自动维护size大小
int i = 0;
for(int j = 0; j < s.size(); j++) {
// 如果不先调用trim,就要先添加空格,最后一个空格不易找到
if(s[j] != ' ') {
if(i != 0) s[i++] = ' ';
// 循环进行条件,处理合法字符
while(j < s.size() && s[j] != ' ') s[i++] = s[j++];
}
}
s.resize(i);// 重构s的实际元素大小
// 2.翻转整个字符串
reverse(s, 0, s.size()); // 前闭后开
// 3.翻转每个单词
int start = 0;
for(int end = 0; end <= s.size(); end++) {
if(end == s.size() || s[end] == ' ') { // 终止边界,非法字符触发事件
reverse(s, start, end);
start = end + 1; // 左指针指向新的单词开始为止,跳过空格字符
}
}
return s;
}
private:
string& trim(string &s) {
if(s.empty()) return s;
s.erase(0, s.find_first_not_of(" "));
s.erase(s.find_last_not_of(" ") + 1);
return s;
}
void reverse(string& s, int start, int end) {
for(int i = start, j = end - 1; i < j; i++, j--) swap(s[i], s[j]);
}
};
三、重构链表
206. 反转链表
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2]
输出:[2,1]
示例 3:
输入:head = []
输出:[]
提示:
- 链表中节点的数目范围是
[0, 5000]
-5000 <= Node.val <= 5000
**进阶:**链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?
题解
双指针法
/**
* 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;
ListNode cur = head;
ListNode next;
while(cur != null) {
// 1.先记录下个节点
next = cur.next;
// 2.反转指向
cur.next = pre;
// 3.移动指针,进行下轮反转
pre = cur;
cur = next;
}
return pre;
}
}
递归法
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
return recur(nullptr, head);
}
private:
ListNode* recur(ListNode* pre, ListNode* cur) {
if(cur == nullptr) return pre;
ListNode* next = cur->next;
cur->next = pre;
return recur(cur, next);
}
};
19. 删除链表的倒数第 N 个结点
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
示例 1:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SuedzZdm-1674910076455)(assets/remove_ex1.jpg)]
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1
输出:[]
示例 3:
输入:head = [1,2], n = 1
输出:[1]
提示:
- 链表中结点的数目为
sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz
**进阶:**你能尝试使用一趟扫描实现吗?
题解
- 因为无法知道链表中一共有多少个元素,如果想使用一趟扫描,需要使用双指针
- 双指针可以利用差值,找到要删除节点的前置节点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
// 1.定义头节点,和左右指针
ListNode* dummyHead = new ListNode(0, head);
ListNode* left = dummyHead; // 左指针指向要删除节点的前一个位置
ListNode* right = head; // 右指针探路,要比左提前一步,左是前置节点
while(n-- && right != NULL) right = right->next;//差值就是n
// 2.右指针探路,直到遍历到链表末尾,左指针就是要删除的前一个节点
while(right != nullptr) {
left = left->next;
right = right->next;
}
// 3.left指向要删除节点前面,进行删除节点操作
left->next = left->next->next;// 因题中要求n>=1,故left一定不会越界
return dummyHead->next;
}
};
面试题 02.07. 链表相交
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null
。
图示两个链表在节点 c1
开始相交**:**
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rgBFHWj8-1674910076456)(assets/160_statement.png)]
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
示例 1:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TY543qlE-1674910076457)(assets/160_example_1.png)]
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例 2:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KAItMZgy-1674910076462)(assets/160_example_2.png)]
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tSIA8ZM1-1674910076463)(assets/160_example_3.png)]
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
提示:
listA
中节点数目为m
listB
中节点数目为n
0 <= m, n <= 3 * 104
1 <= Node.val <= 105
0 <= skipA <= m
0 <= skipB <= n
- 如果
listA
和listB
没有交点,intersectVal
为0
- 如果
listA
和listB
有交点,intersectVal == listA[skipA + 1] == listB[skipB + 1]
**进阶:**你能否设计一个时间复杂度 O(n)
、仅用 O(1)
内存的解决方案?
题解
题解1:O(n)空间
- 可以直接使用集合记录节点,重复的节点就是相交的节点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
unordered_set<ListNode *> visited;
// 1.遍历A
while(headA != NULL) {
visited.insert(headA);
headA = headA->next;
}
// 2.遍历B,判断有无环
while(headB != NULL) {
if(visited.find(headB) != visited.end()) return headB;
headB = headB->next;
}
return NULL;
}
};
题解2:双指针法-O(1)空间
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wGfBaHHE-1674910076464)(assets/image-20221120214525644.png)]
证明
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VDmug6sq-1674910076464)(assets/image-20221120214712154.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eaJ1E4xE-1674910076465)(assets/image-20221120214733231.png)]
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if (headA == nullptr || headB == nullptr) return NULL;
// 1.定义两个指针分别指向两个链表的头
ListNode* pA = headA;
ListNode* pB = headB;
// 2.a+c+b = b+c+a
while(pA != pB) {
pA = pA == nullptr ? headB : pA->next;
pB = pB == nullptr ? headA : pB->next;
}
return pA;
}
};
141. 环形链表
给你一个链表的头节点 head
,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos
不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true
。 否则,返回 false
。
示例 1:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eDTHWCQg-1674910076467)(assets/circularlinkedlist.png)]
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R5aDojfK-1674910076467)(assets/circularlinkedlist_test2.png)]
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IszcONgM-1674910076468)(assets/circularlinkedlist_test3.png)]
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
提示:
- 链表中节点的数目范围是
[0, 104]
-10^5 <= Node.val <= 10^5
pos
为-1
或者链表中的一个 有效索引 。
题解
-
1.可以用**哈希表记录链表遍历过的节点,**如果有重复,说明有环,空间O(n)
-
2.也可以原地解决,使用快慢指针,如果有环,快指针一定能与慢指针相遇
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OUJpFwM6-1674910076469)(assets/34c6bd80278a4c05a713f7aa279d4f31.png)]
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null || head.next == null) return false;
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if(fast == slow) return true;
}
return false;
}
}
142. 环形链表 II
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
示例 1:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N9r910gJ-1674910076470)(assets/circularlinkedlist-166895576765318.png)]
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RZDRPLBP-1674910076471)(assets/circularlinkedlist_test2-166895576765720.png)]
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZLfeVxxa-1674910076472)(assets/circularlinkedlist_test3-166895576765722.png)]
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。
提示:
- 链表中节点的数目范围在范围
[0, 10^4]
内 -10^5 <= Node.val <= 10^5
pos
的值为-1
或者链表中的一个有效索引
题解
-
1.此题若不开额外空间,需要找规律,列数学表达式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vTxVABAb-1674910076473)(assets/e384a94b6efa48cda6b361c9792a33f8.png)]
public class Solution {
public ListNode detectCycle(ListNode head) {
if(head == null || head.next == null) return null;
ListNode slow = head.next; // 先走一步,不然进不了循环
ListNode fast = head.next.next;
// 1. 先找到快慢指针的相遇点
while(fast != slow && fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 2. 判断是否有环
if(fast != slow) return null;
// 3. 如果有环,根据公式,计算出环的起点
while(head != slow) {
head = head.next;
slow = slow.next;
}
return head;
}
}
四、几个数之和
1.两数之和
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
提示:
2 <= nums.length <= 104
-109 <= nums[i] <= 109
-109 <= target <= 109
- 只会存在一个有效答案
**进阶:**你可以想出一个时间复杂度小于 O(n2)
的算法吗?
题解
用HashMap
- 注意,求下个位置时,不能包含当前的位置
class Solution {
public int[] twoSum(int[] nums, int target) {
// 1.开哈希记录每个数对应的小标
Map<Integer, Integer> map = new HashMap<>();
for(int i = 0; i < nums.length; i++) map.put(nums[i], i);
// 2.遍历一遍数值,查找答案
for(int i = 0; i < nums.length; i++) {
// 注意,排除自身的数值
int key = target - nums[i];
if(map.containsKey(key) && map.get(key) != i) {
return new int[]{i, map.get(target - nums[i])};
}
}
return new int[]{-1, -1};
}
}
代码优化:
- 不能先放入反向表,否则会对同一个位置进行判断,因此要先判断再加入map
- 即从后往前判断,每次从前面已经添加的数据中查找,这样可以避免对同一位置判断
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> map;
for(int i = 0; i < nums.size(); i++) {
// 1.先找之前满足条件的值
auto it = map.find(target - nums[i]);// 自动推断为std::map<int, int>::iterator类型,是一个指针
if(it != map.end()) return {it->second, i};
// 2.再将当前值创建反向表,避免找到当前值
map[nums[i]] = i;
}
return {};
}
};
454. 四数相加 II
给你四个整数数组 nums1
、nums2
、nums3
和 nums4
,数组长度都是 n
,请你计算有多少个元组 (i, j, k, l)
能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
示例 1:
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出:2
解释:
两个元组如下:
1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0
示例 2:
输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1
提示:
n == nums1.length
n == nums2.length
n == nums3.length
n == nums4.length
1 <= n <= 200
-2^28 <= nums1[i], nums2[i], nums3[i], nums4[i] <= 2^28
题解
- 因为用四个数值在四个数组中,因此,可以不用考虑重复的问题
- 可以将四数之和,两两分组,分组计算,两组处理的逻辑与两数之和类似
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
map<int, int> map;
// 1.先计算记录前两个元素和
for(int i : nums1) {
for(int j : nums2) map[i + j]++;
}
int ans = 0;
// 2.查询后面两个数和,并统计
for(int i : nums3) {
for(int j : nums4) {
// size_type count( const Key& key ) const,返回1或0,可用于判断
if(map.count(-i-j)) {// size_type count( const Key& key ) const
ans += map[-i-j];
}
}
}
return ans;
}
};
15.三数之和
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请
你返回所有和为 0
且不重复的三元组。
**注意:**答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。
提示:
3 <= nums.length <= 3000
-105 <= nums[i] <= 105
题解
题解1:排序+滑动窗口-三层模板
- 其实这道题目使用哈希法并不十分合适,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug的代码。
1.先排序,优化三重循环比较的次数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qWvJBx6R-1674910076474)(assets/image-20221210155538055.png)]
2.双指针,一个指向二重循环,一个指向三重循环
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rKI10Tmu-1674910076476)(assets/image-20221210155828898.png)]
- 使用双指针,都只向一侧移动,复杂度降了一个等级
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lq6DpevU-1674910076477)(assets/image-20221210160204194.png)]
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
int n = nums.length;
// 1. 枚举a
for(int ia = 0; ia < n; ia ++) {
// 1.1 要保证枚举的数不重复
if(ia > 0 && nums[ia] == nums[ia - 1]) continue;
// 2. 创建b,c组成的滑动窗口的指针
int ic = n - 1; // 第三元数据,右指针
int target = -nums[ia]; // 滑动窗口的内容
for(int ib = ia + 1; ib < n; ib ++) { // 第二元数,左指针
// 同样,先保证枚举元素不重复
if(ib > ia + 1 && nums[ib] == nums[ib - 1]) continue;
// 维护滑动窗口, 左指针已经定了,移动右指针,注意边界
while(ib < ic && nums[ib] + nums[ic] > target) --ic;
if(ib == ic) break; // 后续b增加,更不会有等于target的值
// 如果找到满足条件的值,记录答案
if(nums[ib] + nums[ic] == target) {
List<Integer> list = new ArrayList<>();
list.add(nums[ia]);list.add(nums[ib]);list.add(nums[ic]);
res.add(list);
}
}
}
return res;
}
}
题解2:排序+滑动窗口-利用两数之和模板
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ans;
// 1.排序,为了去重
sort(nums.begin(), nums.end());
// 2.遍历第一层元素,后面两重可以用两数和模板求出
for(int i = 0; i < nums.size(); i++) {
// 3. 记住;每层循环都要进行去重截断处理
if(i > 0 && nums[i] == nums[i - 1]) continue;
// 4.将target-nums[i]的值作为两数之和模板参数,取出满足条件元素
for(vector<int> opt : twoSum(nums, i + 1, -nums[i])) {
ans.push_back({nums[i], opt[0], opt[1]});
}
}
return ans;
}
private:
// 两数之和模板-双指针解法-将两重循环优化成一层循环
vector<vector<int>> twoSum(vector<int>& nums, int start, int target) {
vector<vector<int>> ans;
// 1.定义左右指针
int i = start; // 左指针指向左边界(递增),向右移,和增大
int j = nums.size() -1; // 右指针指向右边界(递减),向左移,和减少
for(; i < nums.size(); i++) {
// 2.先去重-截断
if(i > start && nums[i] == nums[i - 1]) continue;
// 3.移动右指针,一直保存在左指针右边,直到<=target为止
while(i < j && nums[i] + nums[j] > target) j--;
// 4.截断-当停止条件是左右指针相遇时,左不能再往右走,一定没有合适元素了
if(i == j) break;
// 5.当停止条件是等于target时,记录满足条件的下标
if(nums[i] + nums[j] == target) ans.push_back({nums[i], nums[j]});
}
return ans;
}
};
18. 四数之和-一个数组元素
给你一个由 n
个整数组成的数组 nums
,和一个目标值 target
。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]]
(若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a
、b
、c
和d
互不相同nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]
提示:
1 <= nums.length <= 200
-109 <= nums[i] <= 109
-109 <= target <= 109
题解
同样,使用排序加双指针
- 最后两个数依旧使用双指针,可以减少复杂度
- 只是前面两个数需要两重循环
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xnYue3RF-1674910076477)(assets/image-20221218145450936.png)]
注意:int数据相加,会有可能越界,因此,要转为long类型
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> res = new ArrayList<>();
if(nums == null || nums.length < 4) return res;
int n = nums.length;
Arrays.sort(nums);
// 处理前两重循环
for(int i = 0; i < n; i ++) {
// 注意,每层循环,都要去重操作
if(i > 0 && nums[i] == nums[i - 1]) continue;
for(int j = i + 1; j < n; j ++) {
if(j > i + 1 && nums[j] == nums[j - 1]) continue;
// 寻找剩下的两个数,滑动窗口封装的结果
long two = (long)nums[i] + nums[j];
for(ArrayList<Integer> opt :
twoSum(nums, j + 1, target-two)) {
ArrayList<Integer> arr = new ArrayList<>();
arr.add(nums[i]); arr.add(nums[j]);
arr.add(opt.get(0)); arr.add(opt.get(1));
res.add(arr);
}
}
}
return res;
}
// 1.最后两重循环用双指针,封装好
private ArrayList<ArrayList<Integer>> twoSum(int[] nums, int start, long target) {
ArrayList<ArrayList<Integer>> res = new ArrayList<>();
// 1.确定右指针
int right = nums.length - 1;
// 2.循环遍历左指针
for(int i = start; i < nums.length; i ++) {
// 3. 先去重
if(i > start && nums[i] == nums[i - 1]) continue;
// 4. 维护滑动窗口内容,先移动右指针
while(i < right && (long)nums[i] + nums[right] > target) --right;
// 5. 截断:后续左边界右移,内容变大,一定找不到<=target的数
if(i == right) break; // 截断,直接结束循环判断
// 6. 找到满足条件,记录下来
if((long)nums[i] + nums[right] == target) {
ArrayList<Integer> arr = new ArrayList<>();
arr.add(nums[i]); arr.add(nums[right]);
res.add(arr);
}
}
return res;
}
}
还可以进行一些剪枝操作:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TUcvHtuW-1674910076479)(assets/image-20221218150229824.png)]
注意:int默认是有符号整型,可能会越界
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> ans;
int n = nums.size();
if(n < 4) return ans;
// 1.排序
sort(nums.begin(), nums.end());
// 2.遍历前两重循环
for(int i = 0; i < n - 3; i++) {
// 3. 截断
if(i > 0 && nums[i] == nums[i - 1]) continue;
if((long)nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target)
break;
if((long)nums[i] + nums[n - 1] + nums[n - 2] + nums[n - 3] < target)
continue; // 右边界不满足,往左移动会更小,当前i循环一定不满足
for(int j = i + 1; j < n - 2; j++) {
if(j > i + 1 && nums[j] == nums[j - 1]) continue;
if((long)nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target)
break;
if((long)nums[i] + nums[j] + nums[n - 1] + nums[n - 2] < target)
continue;
// 4.后两重用两数之和模板
long two = (long)target - nums[i] - nums[j];
for(vector<int>& opt : twoSum(nums, j + 1, two)) {
ans.push_back({nums[i], nums[j], opt[0], opt[1]});
}
}
}
return ans;
}
private:
vector<vector<int>> twoSum(vector<int>& nums, int start, long target) {
vector<vector<int>> ans;
// 1.定义左右指针
int i = start, j = nums.size() - 1;
// 2.遍历左指针,移动右指针
for(; i < nums.size(); i++) {
// 3.先去重,截断
if(i > start && nums[i] == nums[i - 1]) continue;
// 4.移动右指针
while(j > i && (long)nums[i] + nums[j] > target) --j;
// 5.截断
if(i == j) break;
// 6.记录
if((long)nums[i] + nums[j] == target) ans.push_back({nums[i], nums[j]});
}
return ans;
}
};