链表相关leetcode题目总结
文章目录
持续更新中~
最近在总结重要的数据结构二叉树、链表、图、栈、队列的编程实现及LeetCode常见题目,这篇博客总结的是链表。
链表的数据结构较为简单,一个链表节点包含两个元素,节点的值和指向下一个节点的指针,借助leetcode上的定义为:
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(NULL) {}
ListNode(int x) : val(x), next(NULL) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
我画了个图,演示了一个链表,实质是几个结构体ListNode串在一起,每个ListNode结构体包含两个部分,一个是节点的值,一个是指向下一个节点的指针,一个节点只能有一个后继指针,但是可以有多个指针指向它。
链表的特点是只能向后访问链表节点,不能往前访问链表节点,因此在编程中,为了能够从链表的第一个元素开始访问链表,常常再定义一个头指针,指向链表的第一个链表节点,访问第一个节点的值(head->val),访问第一个节点的指针(head->next),如下图所示:
一个小贴士,C++11引入了新的指针空值nullptr,这是一个C++11的关键字,用来表示空指针,可以代替NULL来表示空指针,表示更加清晰明了。
1.翻转链表
1.1 翻转整个链表(迭代+递归)
LeetCode206.翻转链表
翻转整个链表的代码看着非常简单,但是每次到自己写的时候总是磕磕碰碰,在这里总结一下。
之前自己有几点总是理解得不是很清楚,这里明确以下:
1.用已有指针初始化另一个指针,例如下面的程序第3行(ListNode* cur=head;)含义是,cur指针和head指针是两个不同的指针(拥有独立的内存空间),但他们指向同一个节点。
2.将一个指向链表节点的指针赋值给另一个指向链表节点的指针,如下面的程序第7行( pre=cur;),含义是,pre指着指向的链表节点更新为cur指针指向的链表节点。
方法1:迭代
我画了个图演示了用迭代的方法翻转链表的这个过程:
代码实现也比较简单:
ListNode* reverseList(ListNode* head) {
ListNode* pre=NULL;
ListNode* cur=head;
while(cur){
ListNode* past=cur->next;
cur->next=pre;
pre=cur;
cur=past;
}
return pre;
}
方法2:递归
递归的思路相对难以理解,可以将链表简化,一层层理解,我将三个节点的链表反转的过程可以见下图所示,可以发现,递归的方法是从后往前翻转链表,而迭代是从前往后。
代码实现如下,注意每次递归的执行都是讲第二个节点指针指向第一个节点,再将第一个节点指空即可。
ListNode* reverseList(ListNode* head) {
if (!head || !head->next) {
return head;
}
ListNode* newHead = reverseList(head->next);
head->next->next = head;//第二个节点的指针(本来指空)指向第一个节点
head->next = NULL;//第一个节点指空
return newHead;
}
1.2 翻转链表的部分节点
LeetCode 92.反转链表2
这个题目要求我们只翻转链表中的部分节点,其他节点的位置保持不变,有个1.1的分析,这一题只需要找准第left个和第right个节点,将第left个节点到第right个节点反转,同时将头尾分别接上原来的链表,特殊情况时left=1,要着重考虑,过程如下图所示:
代码如下:
ListNode* reverseBetween(ListNode* head, int left, int right) {
ListNode* newhead=head;//newhead是指向第left个节点的指针
ListNode* benewhead=head;//benewhead是指向第left-1个节点的指针
for(int i=0;i<left-2;i++)benewhead=benewhead->next;
if(left==1)newhead=head;//left=1时,newhead和benewhead
else newhead=benewhead->next;
ListNode* cur=newhead;
ListNode* pre=NULL;
for(int i=0;i<right-left+1;i++){
ListNode* past=cur->next;
cur->next=pre;
pre=cur;
cur=past;
}
//中间段的头为pre节点,尾为
if(left!=1)benewhead->next=pre;//接头
newhead->next=cur;//接尾
if(left!=1)return head;
return pre;
}
这样看上去代码有点冗余,其实在解决链表的问题时,有一种常用的小技巧就是在链表头部在接一个假的头结点,这样就不需要考虑left=1特殊情况,代码也更加清晰简洁。
添加一个假的头结点的代码写法:
ListNode *reverseBetween(ListNode *head, int left, int right) {
ListNode *fakehead = new ListNode(-1);
fakehead->next = head;
ListNode *pre = fakehead;
for (int i = 0; i < left - 1; i++) {
pre = pre->next;
}
ListNode *cur = pre->next;
ListNode *next;
for (int i = 0; i < right - left; i++) {
next = cur->next;
cur->next = next->next;
next->next = pre->next;
pre->next = next;
}
return fakehead->next;
}
1.3 2个一组翻转链表(递归+迭代)
LeetCode24.两两交换链表节点
题目的要求是不能改变节点的值,因此需要交换节点本身。常见的方法有迭代和递归两种。
首先介绍递归的方法。
我们可以参考1.1递归的思路,递归终止条件相同,递归函数的返回值应该是原链表的第二个节点。
ListNode* swapPairs(ListNode* head) {
if(head==NULL||head->next==NULL)return head;//节点个数为0和1的特殊情况
ListNode* newHead = head->next;
head->next = swapPairs(newHead->next);
newHead->next = head;
return newHead;
}
迭代的思路,这里用到了之前说的小技巧,添加一个假的头结点:
ListNode* swapPairs(ListNode* head) {
ListNode* fakeHead = new ListNode(0);
fakeHead->next = head;
ListNode* temp = fakeHead;
while (temp->next != nullptr && temp->next->next != nullptr) {
ListNode* node1 = temp->next;
ListNode* node2 = temp->next->next;
temp->next = node2;
node1->next = node2->next;
node2->next = node1;
temp = node1;
}
return fakeHead->next;
}
1.4 k个一组翻转链表
2.环形链表
2.1 判断链表是否有环
LeetCode141.环形链表
关于环形链表的问题,一直是链表中非常重要的部分,这里要说明一个非常常见的方法,就是快慢指针,快指针一次走两个节点,慢指针一次走一个节点。
以下图的链表为例,我们来说明如何判断链表是否有环。
若链表无环,快慢指针无法相遇;若链表有环,快慢指针一定相遇。在写代码时一定要注意的就是空节点的判断,只要有下一个节点,首先是要判断是否为空。
代码如下:
bool hasCycle(ListNode *head) {
if(head==NULL||head->next==NULL)return false;
ListNode *slow=head;
ListNode *fast=head->next;
while(slow!=fast){
if(fast==NULL||fast->next==NULL)return false;//若无环,快指针一定先到链表末尾
slow=slow->next;
fast=fast->next->next;
}
return true;
}
2.2 找出含环链表的入环节点
LeetCode142.环形链表2
在上一题的数学基础上,解决这个问题会容易很多。
代码如下:只是在上题的代码上增加了ptr和slow指针的循环相遇
ListNode *detectCycle(ListNode *head) {
if(head==NULL||head->next==NULL)return NULL;
ListNode *slow=head;
ListNode *fast=head->next;
while(slow!=fast){
if(fast==NULL||fast->next==NULL)return NULL;
slow=slow->next;
fast=fast->next->next;
}
ListNode *ptr=head;
slow=slow->next;//前往别忘了slow要先走一步,因为ptr在head说明已经走了一步
while(ptr !=slow){
ptr=ptr->next;
slow=slow->next;
}
return ptr;
}
3.查找链表特定节点
3.找出两单链表的相交节点
LeetCode160.相交链表
这个题目的思路实际上是一个数学技巧,见下图:
按照这个思路,代码是这样的(注意不管是哪个指针到达末尾时,都要主要返回表头节点):
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if(headA==NULL||headB==NULL)return NULL;
ListNode* ptrA=headA;
ListNode* ptrB=headB;
while(ptrA!=NULL&&ptrB!=NULL){//第一阶段,短链表到达链表尾部
ptrA=ptrA->next;
ptrB=ptrB->next;
}
if(ptrA==NULL){//若是ptrA先到达链表尾部
ptrA=headB;
while(ptrB!=NULL){//ptrB后到达链表尾部
ptrA=ptrA->next;
ptrB=ptrB->next;
}
ptrB=headA;
while(ptrA!=ptrB){
ptrA=ptrA->next;
ptrB=ptrB->next;
}
}
else {//ptrB先到达链表尾部
ptrB=headA;
while(ptrA!=NULL){
ptrA=ptrA->next;
ptrB=ptrB->next;
}
ptrA=headB;
while(ptrA!=ptrB){
ptrA=ptrA->next;
ptrB=ptrB->next;
}
}
return ptrA;
}
虽然思路清晰,但是代码写法比较冗余,将判断条件合并,实际上代码可以写的更加简洁优雅:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if(headA==NULL||headB==NULL)return NULL;
ListNode* ptrA=headA;
ListNode* ptrB=headB;
while(ptrA!=ptrB){
ptrA = (ptrA == NULL) ? headB : ptrA->next;
ptrB = (ptrB == NULL) ? headA : ptrB->next;
}
return ptrA;
}
3.2链表的倒数第k个节点
offer22.链表倒数第k个节点
有了上面的思路,代码实现就比较简单了:
ListNode* getKthFromEnd(ListNode* head, int k) {
ListNode* fast=head;
for(int i=0;i<k;i++)fast=fast->next;//fast此时位于第k+1个节点
ListNode* slow=head;
while(fast!=NULL){
slow=slow->next;
fast=fast->next;
}
return slow;
}
4.删除链表节点
4.1 删除链表的特定节点
237.删除链表中的节点
不同于一般的删除节点,这道题目要求不能从表头开始访问,只能从待删除的节点开始访问,因此没法真正删掉这个节点,只能让这个节点“变成”待删除节点的下一个节点,顺便把指向变成下一个节点的下一个节点。
void deleteNode(ListNode* node) {
node->val=node->next->val;
node->next=node->next->next;
}
4.1 删除链表的确定值的节点
ListNode* removeElements(ListNode* head, int val) {
ListNode* newhead = new ListNode(0,head);
ListNode* temp = newhead;
while (temp->next != NULL) {
if (temp->next->val == val) {
temp->next = temp->next->next;
} else {
temp = temp->next;
}
}
return newhead->next;
}
4.3删除中间节点
ListNode* deleteMiddle(ListNode* head) {
if (head->next == nullptr) {
return nullptr;
}
ListNode* slow = head;
ListNode* fast = head;
ListNode* pre = nullptr;
while (fast && fast->next) {
fast = fast->next->next;
pre = slow;
slow = slow->next;
}
pre->next = pre->next->next;
return head;
}
5.链表排序
6.合并有序链表
6.1合并两个有序链表
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if (l1 == NULL) return l2;
else if (l2 == NULL) return l1;
else if (l1->val < l2->val) {
l1->next = mergeTwoLists(l1->next, l2);
return l1;
}
else {
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
}
6.2合并k个有序链表
ListNode* mergeTwoLists(ListNode *a, ListNode *b) {
if ((!a) || (!b)) return a ? a : b;
ListNode head, *tail = &head, *aPtr = a, *bPtr = b;
while (aPtr && bPtr) {
if (aPtr->val < bPtr->val) {
tail->next = aPtr; aPtr = aPtr->next;
} else {
tail->next = bPtr; bPtr = bPtr->next;
}
tail = tail->next;
}
tail->next = (aPtr ? aPtr : bPtr);
return head.next;
}