链表双指针
- 单链表分解
- 合并K个升序链表
- [单链表的倒数第 k 个节点](#单链表的倒数第 k 个节点)
- 删除链表第k个节点
- 找到链表的中点
- 判断链表是否包含环
- 环形链表2
- 两个链表是否相交
- 删除排序链表中的重复元素
- [有序矩阵中第 K 小的元素](#有序矩阵中第 K 小的元素)
- [查找和最小的 K 对数字](#查找和最小的 K 对数字)
- 两数相加
- [两数相加 II](#两数相加 II)
合并两个链表
先创建一个虚拟头结点,然后向拉拉链一样在这个新链表填入数据
cpp
class Solution{
public:
ListNode* mergeTwoLists(ListNode* list1,ListNode* list2)
{
ListNode dummy(-1), *p = &dummy;
ListNode* p1 = list1;
ListNode* p2 = list2;
while(p1!=nullptr&&p2!=nullptr)
{
if(p1->val>p2->val){
p->next = p2;
p2= p2->next;
}else{
p->next = p1;
p1= p1->next;
}
p = p->next;
}
if(p1!=nullptr) p->next = p1;
if(p2!=nullptr) p->next = p2;
return dummy.next;
}
};
单链表分解
cpp
class Solution {
public:
ListNode* partition(ListNode* head, int x) {
ListNode dummy1(-1), *p1 = &dummy1;
ListNode dummy2(-2), *p2 = &dummy2;
while(head != nullptr)
{
if(head->val < x){
p1->next = head;
p1 = p1->next;
}else{
p2->next = head;
p2 = p2->next;
}
// 【修改点】:一定要让 head 往后走!
head = head->next;
}
p2->next = nullptr; // 断开大链表防止成环
p1->next = dummy2.next; // 拼接两个链表
return dummy1.next;
}
};
合并K个升序链表
这道题目首先需要的知识点就是二叉堆(分为大堆和小堆)
大堆就是一个二叉树的最顶部元素是最大的数
小堆相反,
当顶部元素被取走的时候,新的最大或者最小会浮到最上边
在 C++ 中,我们通常使用 std::priority_queue 来实现堆。需要注意的是,C++ 的 priority_queue 默认是最大堆,实现最小堆需要指定比较器
cpp
class Solution {
public:
struct cmp
{
bool operator()(ListNode* a, ListNode* b){
return a->val>b->val;
}
};
ListNode* mergeKLists(vector<ListNode*>& lists)
{
priority_queue<ListNode*,vector<ListNode*>,cmp> pq;
for(auto node:lists){
if(node != nullptr){
pq.push(node);
}
}
ListNode dummy(-1);
ListNode* p = &dummy;
while(!pq.empty())
{
ListNode* minNode = pq.top();
pq.pop();
p->next = minNode;
p = p->next;
if(minNode->next != nullptr)
{
pq.push(minNode->next);
}
}
return dummy.next;
}
};
单链表的倒数第 k 个节点
正常思路可以便利两次链表,第一次找到链表长度n,第二次直接遍历到n-1+k的位置就是倒数第k个节点
但是有没有可以一次遍历链表就可以找到倒数第k各节点的方法
双指针:
先让一个指向链表头部的指针p1遍历k个位置,此时只要再走 n - k 步,p1就能走到链表末尾的空指针
这时候再来一个p2指向链表头部,p1和p2同时遍历,当p1走到末尾(走了n-k步)p2此时正好在n-k的地方,正好是倒数第k个节点
cpp
ListNode* findFromEnd(ListNode* head, int k) {
ListNode* p1 = head;
// p1 先走 k 步
for (int i = 0; i < k; i++) {
p1 = p1 -> next;
}
ListNode* p2 = head;
// p1 和 p2 同时走 n - k 步
while (p1 != nullptr) {
p2 = p2 -> next;
p1 = p1 -> next;
}
// p2 现在指向第 n - k + 1 个节点,即倒数第 k 个节点
return p2;
}
删除链表第k个节点
利用上一个算法找到第k-1的那个节点,然后跳过k节点连接就好
cpp
private:
ListNode* findfromend(ListNode* head, int k){
ListNode* p1 = head;
for(int i = 0;i<k;i++){
p1 = p1->next;
}
ListNode* p2 = head;
while(p1!=nullptr){
p1 = p1->next;
p2 = p2->next;
}
return p2;
}
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode dummy(-1), *p= &dummy;
p->next = head;
ListNode* x = findfromend(p,n+1);
x->next = x->next->next;
return p->next;
}
找到链表的中点
利用快慢指针,快指针一次前进两步,慢指针一次前进一步,当快指针到末尾时,慢指针到中间
cpp
class Solution {
public:
ListNode* middleNode(ListNode* head) {
// 快慢指针初始化指向 head
ListNode* slow = head;
ListNode* fast = head;
// 快指针走到末尾时停止
while (fast != nullptr && fast->next != nullptr) {
// 慢指针走一步,快指针走两步
slow = slow->next;
fast = fast->next->next;
}
// 慢指针指向中点
return slow;
}
};
判断链表是否包含环
利用快慢指针,如果快慢指针相遇,证明快指针在链表中转圈
cpp
class Solution {
public:
bool hasCycle(ListNode *head) {
// 快慢指针初始化指向 head
ListNode *slow = head, *fast = head;
// 快指针走到末尾时停止
while (fast != nullptr && fast->next != nullptr) {
// 慢指针走一步,快指针走两步
slow = slow->next;
fast = fast->next->next;
// 快慢指针相遇,说明含有环
if (slow == fast) {
return true;
}
}
// 不包含环
return false;
}
};
环形链表2
首先分为三段距离:
x x x (Head 到 Entry):从链表头节点到环入口节点的距离。
y y y (Entry 到 Meet):从环入口节点到快慢指针相遇节点的距离。
z z z (Meet 到 Entry):从相遇节点继续走,回到环入口节点的距离(即环的剩余部分)。
慢指针走的距离: S = x + y S = x + y S=x+y
快指针走的距离: F = x + y + n ( y + z ) F = x + y + n(y + z) F=x+y+n(y+z)快指针比慢指针快,它可能在环里转了 n n n 圈 ( n ≥ 1 n \ge 1 n≥1) 才追上慢指针)
F = 2 × S F = 2 \times S F=2×S
x + y + n ( y + z ) = 2 ( x + y ) x + y + n(y + z) = 2(x + y) x+y+n(y+z)=2(x+y)
n ( y + z ) = x + y n(y + z) = x + y n(y+z)=x+y
x = n ( y + z ) − y x = n(y + z) - y x=n(y+z)−y
把其中一圈拆分出来
x = ( y + z ) + ( n − 1 ) ( y + z ) − y x = (y + z) + (n-1)(y + z) - y x=(y+z)+(n−1)(y+z)−y
消掉y
x = z + ( n − 1 ) ( y + z ) x = z + (n-1)(y + z) x=z+(n−1)(y+z)
重要结论:当 n = 1 n=1 n=1 (最常见的情况,快指针只多跑了一圈) 时: x = z x = z x=z
这句话翻译成人话就是:
从"头节点"到"环入口"的距离 ( x x x) ,等于从"相遇点"继续往前走到"环入口"的距离 ( z z z) 。
基于以上结论 x = z x = z x=z 的结论,我们可以设计出找入口的算法:
相遇:让 fast 和 slow 均从头出发,fast 走两步,slow 走一步。直到两者相遇。如果不相遇直接返回 null。
重置:保持一个指针(比如 slow)在相遇点不动。将另一个指针(fast 或新建一个指针 ptr)重新放回链表头节点 head。
同步前进:让两个指针同时移动,这次两个都每次只走 1 步。
再次相遇:因为 x = z x = z x=z,它们必然会在环的入口点相遇。返回该节点。
cpp
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* slow = head;
ListNode* fast = head;
while(fast != nullptr && fast->next!=nullptr){
fast = fast->next->next;
slow = slow->next;
if(fast == slow){
ListNode* p1 = head;
while(p1 != fast){
p1 = p1->next;
fast = fast->next;
}
return fast;
}
}
return nullptr;
}
};
两个链表是否相交
原理就是让两个链表头尾相交,这样在两个链表头部的两个指针就可以通知到达相同的节点了
cpp
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if(headA == nullptr || headB == nullptr) return nullptr;
ListNode* p1 = headA;
ListNode* p2 = headB;
while(p1 != p2){
p1 = (p1 == nullptr)? headB : p1->next;
p2 = (p2 == nullptr)? headA : p2->next;
}
return p1;
}
};
删除排序链表中的重复元素
注意:创建好dummy要指向原来的head
dummy 是栈变量,函数结束自动销毁,不需要 delete dummy
只有new(在堆上申请空间)出来的需要delete
cpp
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
if(!head || !head->next) return head;
ListNode dummy(-1), *p = &dummy;
dummy.next = head;
while(p->next != nullptr && p->next->next!=nullptr){
if(p->next->val == p->next->next->val){
int x = p->next->val;
while(p->next != nullptr && p->next->val == x){
ListNode* temp = p->next;
p->next =p->next->next;
delete temp;
}
}else{
p = p->next;
}
}
return dummy.next;
}
};
有序矩阵中第 K 小的元素
出现第K个,想到堆
首先将每行的头放到小堆中,堆顶是整个矩阵最小的那个,然后放入哪一行的第二个元素,一次取出堆顶
第K次取出的哪一个就是第k小的元素
cpp
class Solution {
public:
struct cmp
{
bool operator()(const vector<int>& a,const vector<int>& b)
{
return a[0]>b[0];
}
};
int kthSmallest(vector<vector<int>>& matrix, int k) {
priority_queue<vector<int>,vector<vector<int>>,cmp> pq;
int n = matrix.size();
for(int i = 0; i<n; i++){
pq.push({matrix[i][0],i,0});
}
for(int i = 0;i<k-1;i++){
vector<int> cur = pq.top();
pq.pop();
int r = cur[1];
int c = cur[2];
if(c+1<n){
pq.push({matrix[r][c + 1], r, c + 1});
}
}
return pq.top()[0];
}
};
查找和最小的 K 对数字
和上一题几乎一样,将两个非递减数组当成矩阵的行和列,内部的元素当做对应的两个数组的和
第K小的数组对就是上一题中第K小的数值,唯一区别是我们要自己计算两个数组对应的和
cpp
#include <vector>
#include <queue>
using namespace std;
class Solution {
public:
// --- 0. 辅助结构与规则 ---
// 自定义比较器:我们需要一个小顶堆
// 堆中元素存储格式:vector<int>{sum, i, j}
// sum: 当前数对的和 (nums1[i] + nums2[j])
// i: nums1 中的下标
// j: nums2 中的下标
struct cmp {
bool operator()(const vector<int>& a, const vector<int>& b) {
// 小顶堆逻辑:如果 a 的和大于 b 的和,则 a 沉底,b 上浮
// return true (a > b) -> 意味着 sum 小的在堆顶
return a[0] > b[0];
}
};
vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k) {
// --- 1. 前置准备条件 (这里是你重点关注的部分) ---
// 【result】: 结果容器
// 为什么要准备:题目要求返回前 k 对数字,我们需要一个容器来收集这些答案。
// 以后哪里用:在 while 循环中,每次从堆顶弹出一个最小值时,就 result.push_back(...) 存进去。
vector<vector<int>> result;
// 【n】: nums1 的长度
// 为什么要准备:nums1 相当于矩阵的"行",我们需要知道有多少行。
// 以后哪里用:
// 1. 初始化堆时:防止 i 越界 (i < n)。
// 2. 优化逻辑:如果 k < n,我们只需要初始化前 k 行,后面的没必要放进堆。
int n = nums1.size();
// 【m】: nums2 的长度
// 为什么要准备:nums2 相当于矩阵的"列",我们需要知道每一行有多长。
// 以后哪里用:
// 在 while 循环寻找"接班人"时,我们需要判断 j + 1 < m。
// 如果 j+1 已经等于 m 了,说明这一行走到头了,不能再往后推了。
int m = nums2.size();
// 【特判】: 边界保护
// 如果任何一个数组为空,或者 k 为 0,根本凑不出对数,直接返回空结果。
if (n == 0 || m == 0 || k == 0) return result;
// --- 2. 初始化优先队列 (Min-Heap) ---
// 定义堆:存储类型是 vector<int>,底层容器是 vector,排序规则是 cmp
priority_queue<vector<int>, vector<vector<int>>, cmp> pq;
// 将 nums1 中前 k 个元素与 nums2[0] 组成的"排头兵"放入堆
// 这里用到了 n (防止越界) 和 k (作为优化边界)
for (int i = 0; i < min(n, k); i++) {
// 存入: {当前和, nums1的下标, nums2的下标}
pq.push({nums1[i] + nums2[0], i, 0});
}
// --- 3. 循环获取结果 ---
// 循环条件:
// 1. k > 0: 我们只需要找 k 对,找够了就停。
// 2. !pq.empty(): 万一 k 很大,但总共能凑出的对数不够 k 个,堆空了也要停。
while (k > 0 && !pq.empty()) {
// [取出当前最小值]
vector<int> cur = pq.top();
pq.pop();
// 解析数据:刚才存进去的 {sum, i, j}
int i = cur[1]; // 拿到 nums1 的下标
int j = cur[2]; // 拿到 nums2 的下标
// [收集结果] -> 这里用到了最开始定义的 result
result.push_back({nums1[i], nums2[j]});
// [寻找接班人]
// 这里用到了最开始定义的 m
// 如果 nums2 这一行还有下一个元素 (j + 1 < m)
// 就把 {nums1[i] + nums2[j+1], i, j+1} 放进堆里
if (j + 1 < m) {
pq.push({nums1[i] + nums2[j + 1], i, j + 1});
}
// 找完一对,k 减 1
k--;
}
// 返回最终收集好的结果
return result;
}
};
两数相加
cpp
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode dummy(-1), *p = &dummy;
int carry = 0;
while(l1 != nullptr || l2 != nullptr || carry!= 0 ){
int n1 = (l1!=nullptr)? l1->val: 0;
int n2 = (l2!=nullptr)? l2->val: 0;
int sum = n1+n2+carry;
carry = sum/10;
int digit = sum%10;
p->next = new ListNode(digit);
p = p->next;
if(l1!=nullptr) l1 = l1->next;
if(l2!=nullptr) l2 = l2->next;
}
return dummy.next;
}
};
两数相加 II
和上一题的区别就是利用栈的后进先出的特性完成个位和高位的转换
cpp
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
// --- 1. 前置准备条件 ---
// 【s1, s2】: 两个栈
// 为什么要准备:
// 链表无法倒着走,我们用栈把所有节点存起来。
// 栈顶就是个位,栈底是最高位。
stack<int> s1, s2;
// 将 l1 的所有值压入 s1
while (l1 != nullptr) {
s1.push(l1->val);
l1 = l1->next;
}
// 将 l2 的所有值压入 s2
while (l2 != nullptr) {
s2.push(l2->val);
l2 = l2->next;
}
// 【head】: 结果链表的头指针
// 为什么要准备:
// 这次我们用"头插法",新来的节点要排在最前面。
// 初始为 nullptr。
ListNode* head = nullptr;
// 【carry】: 进位
int carry = 0;
// --- 2. 循环处理 (出栈相加) ---
// 只要栈里还有数字,或者还有进位没处理完,就继续
while (!s1.empty() || !s2.empty() || carry != 0) {
// [提取数值]
// 如果栈不空,就取出栈顶(当前最低位);如果空了,就当 0
int a = 0;
if (!s1.empty()) {
a = s1.top();
s1.pop(); // 用完记得弹出
}
int b = 0;
if (!s2.empty()) {
b = s2.top();
s2.pop();
}
// [计算和与进位]
int sum = a + b + carry;
carry = sum / 10;
int digit = sum % 10;
// [构建链表 - 关键点:头插法]
// 上一题我们是往屁股后面接 (curr->next = node)
// 这一题我们要往脑袋前面插,因为我们先算出来的是个位,个位应该在最后面
ListNode* newNode = new ListNode(digit);
newNode->next = head; // 1. 新节点指向原本的头
head = newNode; // 2. 只有新节点变成了新的头
}
// --- 3. 返回结果 ---
return head;
}
};