链表相关问题的多种解法(最小堆、双指针、栈)
在本文中,我们将深入探讨几道经典的链表相关的算法题,这些题目涵盖了不同的技巧,包括双指针、虚拟头结点、栈以及最小堆的运用。
24. 两两交换链表中的节点(虚拟头结点)
题目描述
给定一个链表,我们需要两两交换其中相邻的节点,并且不能修改节点内部的值,只能通过节点交换来完成,最后返回交换后链表的头节点。
示例分析
- 示例 1:输入
head = [1,2,3,4]
,期望输出[2,1,4,3]
。这里我们可以看到,链表中的节点 1 和 2 进行了交换,3 和 4 也进行了交换。 - 示例 2:输入
head = []
,输出[]
。空链表的情况比较简单,直接返回空即可。 - 示例 3:输入
head = [1]
,输出[1]
。当链表只有一个节点时,无需进行交换,直接返回原节点。
代码解析
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
// 创建一个虚拟头结点,它的值为0,next指针指向原链表的头节点。
// 这样做的好处是可以方便地处理头节点可能发生变化的情况,避免了特殊情况的单独处理。
ListNode dummyNode(0);
dummyNode.next = head;
// 定义一个指针p,初始指向虚拟头结点。
ListNode* p = &dummyNode;
// 当p的下一个节点和下下个节点都不为空时,进入循环进行交换操作。
while (p->next!= nullptr && p->next->next!= nullptr) {
// 临时保存需要交换的节点。
ListNode* temp1 = p->next;
ListNode* temp2 = p->next->next;
ListNode* temp3 = p->next->next->next;
// 进行节点交换操作。
// 首先将p的next指针指向temp2,即原来的第二个节点。
p->next = p->next->next;
// 然后将temp2的next指针指向temp1,即原来的第一个节点。
temp2->next = temp1;
// 最后将temp1的next指针指向temp3,即原来的第三个节点(交换后的下一组的第一个节点)。
temp1->next = temp3;
// 将指针p移动到交换后的下一组的第一个节点的前一个节点,即temp2。
p = p->next->next;
}
// 返回交换后的链表的头节点,即虚拟头结点的下一个节点。
return dummyNode.next;
}
};
82. 删除排序链表中的重复元素 II(前后指针)
题目描述
给定一个已排序的链表的头 head
,需要删除原始链表中所有重复数字的节点,只留下不同的数字,最后返回已排序的链表。
示例分析
- 示例 1:输入
head = [1,2,3,3,4,4,5]
,输出[1,2,5]
。这里的 3 和 4 都有重复节点,需要将它们全部删除。 - 示例 2:输入
head = [1,1,1,2,3]
,输出[2,3]
。这里的 1 重复了多次,需要删除所有重复的 1。
代码解析
class Solution {//因为是有序的,所以直接遍历就行,不需要哈希
public:
ListNode* deleteDuplicates(ListNode* head) {
// 创建一个虚拟头结点,其next指针指向原链表的头节点。
ListNode dummyNode(0);
dummyNode.next = head;
// 定义两个指针,p1指向原链表的头节点(虚拟头结点的下一个节点),p2指向虚拟头结点。
ListNode* p1 = dummyNode.next;
ListNode* p2 = &dummyNode;
// 如果链表为空或者只有一个节点,直接返回原链表的头节点(即虚拟头结点的下一个节点)。
if (p1 == nullptr || p1->next == nullptr) {
return dummyNode.next;
}
// 当p1和p1的下一个节点都不为空时,进入循环。
while (p1!= nullptr && p1->next!= nullptr) {
// 如果p1的值和p1的下一个节点的值相等,说明找到了重复的节点。
if (p1->val == p1->next->val) {
// 继续向后遍历,直到找到下一个不相等的节点。
while (p1->next!= nullptr && p1->val == p1->next->val) {
p1 = p1->next;
}
// 将p2的next指针指向p1的下一个节点,跳过所有重复的节点。
p2->next = p1->next;
// 将p1移动到下一个不相等的节点。
p1 = p1->next;
} else {
// 如果p1和p1的下一个节点的值不相等,将p1和p2都向后移动一位。
p1 = p1->next;
p2 = p2->next;
}
}
// 返回处理后的链表的头节点,即虚拟头结点的下一个节点。
return dummyNode.next;
}
};
1836. 从未排序的链表中移除重复元素(哈希 + 双指针)
题目描述
给定一个链表的第一个节点 head
,找到链表中所有出现多于一次的元素,并删除这些元素所在的节点,最后返回删除后的链表。
示例分析
- 示例 1:输入
head = [1,2,3,2]
,输出[1,3]
。这里的 2 出现了两次,需要删除所有的 2。 - 示例 2:输入
head = [2,1,1,2]
,输出[]
。这里的 1 和 2 都出现了两次,需要删除所有的节点。 - 示例 3:输入
head = [3,2,2,1,3,2,4]
,输出[1,4]
。这里的 3 出现了两次,2 出现了三次,需要删除所有的 3 和 2。
代码解析
#include<iostream>
#include <limits>
#include <unordered_map>
using namespace std;
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){}
};
// 创建链表节点的函数
ListNode* creatNode() {
ListNode* tail = nullptr;
ListNode* head = nullptr;
int num;
while (cin >> num) {
ListNode* newnode = new ListNode(num);
if (head == nullptr) {
head = newnode;
tail = newnode;
} else {
tail->next = newnode;
tail = newnode;
}
}
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), '\n');
tail->next = nullptr;
return head;
}
// 打印链表节点的函数
void printNode(ListNode* head) {
ListNode* p = head;
while (p!= nullptr) {
if (p->next!= nullptr) {
cout << p->val << " -> ";
} else {
cout << p->val;
}
p = p->next;
}
cout << endl;
}
ListNode* deleteDuplicatesUnsorted(ListNode* head) {
// 使用哈希表来记录每个元素出现的次数。
unordered_map<int, int> map;
ListNode dummyNode(0);
dummyNode.next = head;
ListNode* p1 = dummyNode.next;
// 先遍历一遍链表,将每个元素出现的次数记录在哈希表中。
while (p1!= nullptr) {
map[p1->val]++;
p1 = p1->next;
}
ListNode* p2 = dummyNode.next;
ListNode* p3 = &dummyNode;
// 再次遍历链表,对于出现次数大于1的元素,删除其所在的节点。
while (p2!= nullptr) {
if (map[p2->val] > 1) {
p3->next = p2->next;
p2 = p2->next;
} else {
p2 = p2->next;
p3 = p3->next;
}
}
p3->next = nullptr;
return dummyNode.next;
}
int main() {
cout << "请按照头尾节点顺序依次输入节点值:" << endl;
ListNode* inNode = creatNode();
ListNode* deldnode = deleteDuplicatesUnsorted(inNode);
cout << "经过处理后的链表是:" << endl;
printNode(deldnode);
return 0;
}
378. 有序矩阵中第 K 小的元素(最小堆)
题目描述
给定一个 n x n
矩阵 matrix
,其中每行和每列元素均按升序排序,需要找到矩阵中第 k
小的元素。注意,这里是排序后的第 k
小元素,而不是第 k
个不同的元素,并且要找到一个内存复杂度优于 O(n2)
的解决方案。
示例分析
- 示例 1:输入
matrix = [[1,5,9],[10,11,13],[12,13,15]]
,k = 8
,输出13
。矩阵中的元素为[1,5,9,10,11,12,13,13,15]
,第 8 小元素是 13。 - 示例 2:输入
matrix = [[-5]]
,k = 1
,输出-5
。
代码解析
class Solution {
public:
int kthSmallest(vector<vector<int>>& matrix, int k) {
// 通过设置最小堆,来获取那一列内容最小值。
// 这里定义了一个比较函数,用于确定堆中元素的顺序。
auto cmp = [] (vector<int> left, vector<int> right) {return left[0] > right[0];};
// 设置为vector<int>来存放对应元素和其行号列号,方便传递给后一位节点。
priority_queue<vector<int>, vector<vector<int>>,decltype(cmp)> minheap(cmp);
// 将矩阵每一行的第一个元素加入最小堆。
for (int i = 0; i < matrix.size(); i++) {
minheap.push({matrix[i][0], i, 0});
}
int result = -1;
// 当堆不为空且还没有找到第k小的元素时,继续循环。
while (!minheap.empty() && k--) {
vector<int> min = minheap.top();
minheap.pop();
result = min[0];
int i = min[1], j = min[2];
// 如果当前元素所在列不是最后一列,则将其下一列的元素加入堆中。
if (j < matrix.size() - 1) {
minheap.push({matrix[i][j + 1], i, j + 1});
}
}
return result;
}
};
373. 查找和最小的 K 对数字(最小堆)
题目描述
给定两个以非递减顺序排列的整数数组 nums1
和 nums2
,以及一个整数 k
。定义一对值 (u,v)
,其中第一个元素来自 nums1
,第二个元素来自 nums2
。需要找到和最小的 k
个数对 (u1,v1)
,(u2,v2)
,…,(uk,vk)
。
示例分析
- 示例 1:输入
nums1 = [1,7,11]
,nums2 = [2,4,6]
,k = 3
,输出[1,2],[1,4],[1,6]
。这里返回的是和最小的前三对数。 - 示例 2:输入
nums1 = [1,1,2]
,nums2 = [1,2,3]
,k = 2
,输出[1,1],[1,1]
。
代码解析
class Solution {
public:
vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k) {
// 定义一个比较函数,用于确定堆中元素的顺序,这里按照数对的和来比较。
auto cmp = [](vector<int>& a, vector<int>& b) {
return (a[0] + a[1]) > (b[0] + b[1]);
};
// 创建一个最小堆。
priority_queue<vector<int>,vector<vector<int>>,decltype(cmp)> minheap(cmp);
// 将nums1里的所有元素和nums2[0]作为一对放入最小堆。
for (int i = 0; i < nums1.size() && i < k; i++) {
minheap.push({nums1[i],nums2[0],0});
}
vector<vector<int>> result;
// 当堆不为空且还没有找到k个数对时,继续循环。
while (!minheap.empty() && k--) {
vector<int> cur = minheap.top();
minheap.pop();
result.push_back({cur[0], cur[1]});
int j = cur[2] + 1;
// 如果nums2中还有未使用的元素,则将当前nums1中的元素和下一个nums2中的元素组成数对放入堆中。
if (j < nums2.size()) {
minheap.push({cur[0], nums2[j], j});
}
}
return result;
}
};
2. 两数相加(双指针)
题目描述
给定两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储一位数字。需要将两个数相加,并以相同形式返回一个表示和的链表。可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例分析
- 示例 1:输入
l1 = [2,4,3]
,l2 = [5,6,4]
,输出[7,0,8]
。这里表示的是 342 + 465 = 807。 - 示例 2:输入
l1 = [0]
,l2 = [0]
,输出[0]
。 - 示例 3:输入
l1 = [9,9,9,9,9,9,9]
,l2 = [9,9,9,9]
,输出[8,9,9,9,0,0,0,1]
。
代码解析
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
// 在两条链表上的指针,分别初始化为两条链表的头节点。
ListNode *p1 = l1, *p2 = l2;
// 虚拟头结点(构建新链表时的常用技巧),其值为 -1。
ListNode *dummy = new ListNode(-1);
// 指针p负责构建新链表,初始指向虚拟头结点。
ListNode *p = dummy;
// 记录进位,初始化为0。
int carry = 0;
// 开始执行加法,两条链表走完且没有进位时才能结束循环。
while (p1!= nullptr || p2!= nullptr || carry > 0) {
// 先加上上次的进位。
int val = carry;
if (p1!= nullptr) {
val += p1->val;
p1 = p1->next;
}
if (p2!= nullptr) {
val += p2->val;
p2 = p2->next;
}
// 处理进位情况,计算新的进位。
// 如果 val 大于等于 10,进位为 1,否则为 0。
carry = val / 10;
// 取个位数字作为新节点的值。
val = val % 10;
// 构建新节点,将其加入到结果链表中。
p->next = new ListNode (val);
// 移动指针 p 到新节点。
p = p->next;
}
// 返回结果链表的头结点(去除虚拟头结点)。
return dummy->next;
}
};
445. 两数相加 II (栈中转)
题目描述
给定两个非空链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。可以假设除了数字 0 之外,这两个数字都不会以零开头。
示例分析
- 示例 1:输入
l1 = [7,2,4,3]
,l2 = [5,6,4]
,输出[7,8,0,7]
。 - 示例 2:输入
l1 = [2,4,3]
,l2 = [5,6,4]
,输出[8,0,7]
。 - 示例 3:输入
l1 = [0]
,l2 = [0]
,输出[0]
。
代码解析
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
// 创建两个栈,分别用于存储两个链表中的数字。
stack<int> str1,str2;
ListNode* p1 = l1;
ListNode* p2 = l2;
// 将链表 l1 中的数字依次压入栈 str1。
while(p1!=nullptr){
str1.push(p1->val);
p1=p1->next;
}
// 将链表 l2 中的数字依次压入栈 str2。
while(p2!=nullptr){
str2.push(p2->val);
p2=p2->next;
}
// 创建一个虚拟头结点,值为 -1。
ListNode* dummy = new ListNode(-1);
int carry = 0;
// 当两个栈都不为空时,进行加法运算。
while(!str1.empty()||!str2.empty()){
// 如果栈 str1 为空,取 0 作为当前数字,否则取栈顶数字并弹出。
int a = str1.empty()? 0 : str1.top();
if (!str1.empty()) str1.pop();
// 如果栈 str2 为空,取 0 作为当前数字,否则取栈顶数字并弹出。
int b = str2.empty()? 0 : str2.top();
if (!str2.empty()) str2.pop();
// 计算两个数字和进位的和。
int sum = a + b + carry;
// 计算新的进位。
carry = sum / 10;
// 创建一个新节点,值为 sum % 10。
ListNode* newnode = new ListNode(sum % 10);
// 将新节点插入到虚拟头结点之后,新节点成为结果链表的头节点。
newnode->next = dummy->next;
dummy->next = newnode;
}
// 如果最后还有进位,创建一个新节点,值为进位,插入到结果链表头部。
if(carry>0){
ListNode* newnode = new ListNode(carry);
newnode->next = dummy->next;
dummy->next = newnode;
}
// 返回结果链表的头节点(虚拟头结点的下一个节点)。
return dummy->next;
}
};
25. K 个一组翻转链表(递归)
给你链表的头节点 head
,每 k
个节点一组进行翻转,请你返回修改后的链表。
k
是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k
的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
题目描述
给你链表的头节点 head
,每 k
个节点一组进行翻转,请你返回修改后的链表。
k
是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k
的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例分析
-
示例 1:
输入:head = [1,2,3,4,5], k = 2 输出:[2,1,4,3,5]
-
示例 2:
输入:head = [1,2,3,4,5], k = 3 输出:[3,2,1,4,5]
代码解析
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
if(head==nullptr) return nullptr;
ListNode *a=head, *b=head;
for(int i=0; i<k; i++){
if(b==nullptr) return head;//如果有空就return到递归上一级
b = b->next;//指向下一轮‘含k个元素子段’的第一个元素
}
ListNode* newhead = reverseNode(a, k);//先反转本轮前k个元素,因为反转后的a就变成了末尾元素
a->next=reverseKGroup(b, k);//递归调用,反转后的a再连接到下一轮元素中(返回的newhead)
return newhead;
}
//反转前n个节点
ListNode* reverseNode(ListNode* head, int n){
ListNode *pre=nullptr, *cur=head;
while(n-->0&&cur!=nullptr){
ListNode *temp=cur->next;
cur->next=pre;
pre=cur;
cur=temp;
}
return pre;//返回反转后的队头元素
}
};
通过对这些链表相关算法题的分析和代码讲解,我们可以看到不同的解题思路和技巧在处理链表问题中的应用。这些技巧包括利用虚拟头结点简化操作、使用双指针遍历链表、借助栈来处理逆序问题以及利用最小堆来优化查找过程等。希望这些内容能帮助大家更好地理解和解决链表相关的算法问题。