【ps】本篇有 5 道 leetcode OJ。
一、算法简介
链表是一种常见的线性数据结构,是一种在物理结构上非连续、非顺序的存储结构,其中的数据元素的逻辑顺序由其中的指针链接次序实现,指针链接的每一个结构体都是一个节点。
链表的结构多种多样,有单向或双向、带头或不带头、循环或非循环之分。但实际中,最常用的是以下两种组合:
- 无头单向非循环链表(或称单链表)
- 带头双向循环链表(或称双向链表)
【Tips】链表的常用技巧
- 一定要画图!
- 引入一个虚拟的头节点,利于处理边界情况、对链表进行各种操作。
- 不要吝啬空间,大胆定义变量去使用,尤其涉及到插入操作的时候。
- 快慢双指针,可以对链表判环、找环的入口、找链表中倒数第 n 个节点。
【Tips】链表的常见操作
- 创建一个新节点
- 尾插
- 头插
二、相关例题
1)两数相加
.1- 题目解析
要将两个链表的每个节点中的数相加并放在一个新的链表中,那一定就要构建一个链表,我们可以对这个新链表引入一个头节点,然后直接遍历两个链表进行求和,然后将结果构建出一个新节点尾插入头节点即可。
特别的,我们单独定义一个变量 t,既保存相加的结果,又记录进位。
.2- 代码编写
/**
* 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* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* newhead=new ListNode(0);
ListNode*cur1=l1,*cur2=l2,*prev=newhead;
int t=0; //保存求和结果+记录进位
while(cur1||cur2||t)
{
//取两个链表的节点进行求和
if(cur1)
{
t+=cur1->val;
cur1=cur1->next;
}
if(cur2)
{
t+=cur2->val;
cur2=cur2->next;
}
prev->next=new ListNode(t%10);//取结果的个位
t/=10; //取进位
prev=prev->next;
}
prev=newhead->next;
delete newhead;
return prev;
}
};
2)两两交换链表中的节点
.1- 题目解析
以示例 1 为例。我们可以引入一个头节点,并定义四个指针变量来表示不同的节点。
由此,要实现题目要求的交换,仅需交换 cur 和 next 指向的节点即可,然后将这四个指针整体向后移动,继续完成下次交换,直到 cur 遇到了空节点,说明此时整个链表都完成了交换。
.2- 代码编写
/**
* 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* swapPairs(ListNode* head) {
if(head==nullptr||head->next==nullptr)
return head; //处理边界情况
ListNode* newhead=new ListNode(0);
newhead->next=head;
ListNode* prev=newhead,*cur=newhead->next;
ListNode* next=cur->next,*nnext=next->next;
while(cur && next)
{
//交换 cur 和 next
cur->next=nnext;
next->next=cur;
prev->next=next;
//继续遍历链表
prev=cur;
cur=nnext;
if(cur)next=cur->next;
if(next)nnext=next->next;
}
cur=newhead->next;
delete newhead;
return cur;
}
};
3)重排链表
.1- 题目解析
以示例 1 为例。如果将原始链表从中间一分为二,将后半部分链表逆序,然后将前半部分和后半部分的链表节点依此插入道一个新链表中(前半部分先插入,后半部分后插入),就能得到题目要求的链表了。
我们可以用快慢双指针来找到链表的中间位置,将后半部分的起始节点看作是慢指针的下一根节点,然后用头插法来对后半部分的链表进行逆序操作,这样无论原始链表的节点是单数还是复数,都不会影响操作的正确性。
逆序完后半部分的链表之后,依此将前后两部分的链表插入到一个新的头节点之后即可。
.2- 代码编写
/**
* 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:
void reorderList(ListNode* head) {
if(head==nullptr||head->next==nullptr||head->next->next==nullptr)
return;
//找到原始链表的中间位置
ListNode* fast=head,*slow=head;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
}
//头插法逆序后半部分
ListNode* newhead=new ListNode(0);
ListNode*cur=slow->next,*next=cur->next;
slow->next=nullptr; //断开前后两个链表
while(cur)
{
next=cur->next;
cur->next=newhead->next;
newhead->next=cur;
cur=next;
}
//合并两个链表
ListNode* ret=new ListNode(0);
ListNode* prev=ret;
ListNode* cur1=head,*cur2=newhead->next;
while(cur1)
{
prev->next=cur1;
cur1=cur1->next;
prev=prev->next;
if(cur2)
{
prev->next=cur2;
cur2=cur2->next;
prev=prev->next;
}
}
delete newhead;
delete ret;
}
};
4)合并 K 个升序链表
.1- 题目解析
要合并 K 个升序链表,可以先合并两个升序链表,再继续两两合并剩下的链表。我们可以用优先级队列(建小根堆排升序)来优化这个过程,将所有链表的头节点依此入队,即可找到最小的那个节点,只需将其尾插入一个新的头节点,然后将后续的节点入队,重复这个过程,就可以完成 K 个升序链表的合并了。
另外,先合并两个升序链表,再继续两两合并剩下的链表,也可以用归并的方式来解决。
.2- 代码编写
//法1:优先级队列
/**
* 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 {
struct cmp
{
bool operator()(const ListNode* l1,const ListNode* l2)
{
return l1->val > l2->val;
}
};
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
//创建一个小根堆
priority_queue<ListNode*,vector<ListNode*>,cmp> heap;
//让所有链表的头节点进行小根堆
for(auto l: lists)
if(l)
heap.push(l);
//合并链表
ListNode* ret=new ListNode(0);
ListNode* prev=ret;
while(!heap.empty())
{
auto t=heap.top();//每次取堆顶元素
heap.pop();
prev->next=t;//链接
prev=t;
if(t->next) //将后续的节点入堆
heap.push(t->next);
}
prev=ret->next;
delete ret;
return prev;
}
};
//法2:递归
/**
* 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* mergeKLists(vector<ListNode*>& lists) {
return merge(lists,0,lists.size()-1);
}
ListNode* merge(vector<ListNode*>&lists,int left,int right)
{
if(left>right)return nullptr;
if(left==right)return lists[left];
//平分数组
int mid=(left+right)>>1;
//递归处理左右区间
ListNode* l1=merge(lists,left,mid);
ListNode* l2=merge(lists,mid+1,right);
//合并两个有序链表
return mergeTowlist(l1,l2);
}
ListNode* mergeTowlist(ListNode* l1,ListNode* l2)
{
if(l1==nullptr)return l2;
if(l2==nullptr)return l1;
ListNode head;
ListNode* cur1=l1,*cur2=l2,*prev=&head;
head.next=nullptr;
while(cur1&&cur2)
{
if(cur1->val<=cur2->val)
{
prev->next=cur1;
prev=cur1;
cur1=cur1->next;
}
else
{
prev->next=cur2;
prev=cur2;
cur2=cur2->next;
}
}
if(cur1)prev->next=cur1;
if(cur2)prev->next=cur2;
return head.next;
}
};
5)K 个一组翻转链表
.1- 题目解析
.2- 代码编写
/**
* 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* reverseKGroup(ListNode* head, int k) {
//1.先求出需要逆序多少组
int n=0;
ListNode* cur=head;
while(cur)
{
cur=cur->next;
n++;
}
n/=k;
//2.重复n次,长度为k的链表逆序
ListNode* newHead=new ListNode(0);
ListNode* prev=newHead;
cur=head;
for(int i=0;i<n;i++)
{
ListNode* tmp=cur;
for(int j=0;j<k;j++)
{
ListNode* next=cur->next;
cur->next=prev->next;
prev->next=cur;
cur=next;
}
prev=tmp;
}
prev->next=cur;
cur=newHead->next;
delete newHead;
return cur;
}
};