链表专题:
题目简介 | LeetCode题号 |
---|---|
1-删除链表中的倒数第N个节点 | LeetCode第19题 |
2-删除链表中的节点 | LeetCode第237题 |
3-删除排序链表中的重复元素 | LeetCode第83题 |
4-旋转链表 | LeetCode第61题 |
5-两两交换链表中的节点 | LeetCode第24题 |
6-翻转链表 | LeetCode第206题 |
7-反转链表 | LeetCode第92题 |
8-相交链表 | LeetCode第160题 |
9-环形链表 | LeetCode第142题 |
10排序链表 | LeetCode第148题 |
链表的题目一定画图,通过画图确定指针的指向
首先记录一下我之前的一个误区:
1.删除链表中的倒数第N个节点-LeetCode第19题
题目描述:
题目分析:
单向链表只有后继指针,要删除某一个节点,就要找到该要删除节点的前一个节点。我们要删除倒数第N个节点,就要先找到倒数第N+1个节点。这个题目有一个要求:只能对链表遍历一遍,这样就不能先遍历一遍求链表的长度,再遍历一遍去找到倒数第N+1个节点。对于可能删除头节点的题目,或者头节点可能会变化的这类题目就有一个技巧:创建一个虚拟的头节点。创建一个虚拟的头节点的好处就是:虚拟节点一定不会被删除,那么这样的话我们就不用去处理头节点被删除的情况了,少了一些判断。这里用了一个双指针算法避免在删除倒数第N个节点的时候需要去遍历两遍。
双指针算法的做法有:
- 建立虚拟头结点指向头节点;
- 第一个指针first向后走N步;
- 再创建一个指针second指向虚拟头节点,first、second同时向后走,当第一个指针走到最后一个节点的时候,第二个指针就指向了倒数第N+1个位置;
- 删除的方式是第N+1的指针指向N节点的后一个节点。
代码:
/**
* 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) {
auto dummy = new ListNode(-1);
dummy->next = head;
auto first = dummy,second = dummy;
while(n--) first = first->next;
while(first->next)
{
first = first->next;
second = second->next;
}
second->next = second->next->next;
return dummy->next;
}
};
2-删除链表中的节点 - LeetCode第237题
题目描述:
题目分析:
这个题目的思路非常的巧妙
- 看到题目传入的参数是ListNode* node,直接指向了要删除的结点,而我们在链表中删除某个节点的时候,一定要找被删除节点的前一个节点,这个题目没法这样做,换了一种思路;
- 首先把将要被删除节点的后一个结点覆盖将要被删除的结点(也就是用1去覆盖5);
- 然后把将要被删除节点的后一个结点删除掉。
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
void deleteNode(ListNode* node) {
node->val = node->next->val;
node->next = node->next->next;
}
};
第二种解法:
上面是这个题的第一种解法,那么接下来的方法,我们从C++的角度出发去理解这个题目:首先node是一个指针,那么他肯定是指向某一段地址的开头,而且node是一个结构体类型的,一段是存值的,另一段是存地址的。也就是说下面两行代码实现实现的是把node->next里面的东西复制到node里面去:
*node表示是整个val和next上的值,这个是C++的特性,所以:上面的那两句话就可以用一句话来代替:
*(node)= *(node->next);
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
void deleteNode(ListNode* node) {
//node->val = node->next->val;//覆盖掉要删除的结点
//node->next = node->next->next; //删除一个节点
*(node) = *(node->next);
}
};
3-删除排序链表中的重复元素 —LeetCode第83题
题目描述:
题目分析:
链表题的一个思路是非常重要的,拿到链表的题目的话,先不要想着去写代码,而是先考虑思路。因为链表是排号数序的,所以相同的元素都是挨在一起的。并且第一个节点,头节点是不会被删除的,对于连续的元素的话,我们只保留第一个元素。
用一个指针遍历一遍这个链表
- 情况1:如果下一个点和当前点相同,则删掉下一个点;
- 情况2:如果下一个点和当前点不同,指针移到下一个点。
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
auto cur = head;
while(cur)
{
if(cur->next && cur->val == cur->next->val)
cur->next = cur->next->next;
else cur = cur->next;
}
return head;
}
};
4-旋转链表—LeetCode第61题
题目描述:
题目分析:
我感觉这个题目也是思路非常巧妙的,一个容易忽视的点就是这个 K 可能会很大,为了避免做无用功,我们一般先取余:
首先是尾部节点要指向原来的head节点;然后第倒数第K+1个节点指向空节点(这两个需要的点用双指针方法就可以找出来);最后倒数第K个节点作为新的头节点。这个题目不需要虚拟头节点。需要步骤1、2、3:
- K%n;
- first指针从头往后走K步;
- second和first同时往后走,当first走到尾巴的时候,停止;(后面始终保持着相差K个数的)
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* rotateRight(ListNode* head, int k) {
if(!head) return NULL;
int n = 0;
for(auto p = head;p;p = p->next) n++;
k %= n;
auto first = head,second = head;
while(k--) first = first->next;
//因为下面的fist还要往下走一步,所以这里判断的时候取得的是first->next不为空
while(first->next){
first = first->next;
second = second->next;
}
first->next = head;
head = second->next;
second->next = NULL;
return head;
}
};
5-两两交换链表中的节点- LeetCode第24题
题目描述:
题目分析:
LeetCode第24题,两两交换链表中的节点是一个非常经典的一个题目,如果面试多了的话,就会发现这个是一个必然会问的题目。由于头节点是会变化的,所以说我们在这个题目中需要创建一个虚拟的头节点。这个题目的一个解法就是每次需要枚举一对节点,然后作如下的操作:创建指针p,a,b分别指向头节点和相邻的需要交换的两个节点。
- p->next = b;
- a->next = b->next;
- b-next = a;
- p = a 实际上这里因为有三个指针了,这四步不是那么的严格的顺序要求,顺序是可以变换的。
这个题目是用p这个指针往后移动,带动着a、b往后移动
代码:
/**
* 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) {
auto dummy = new ListNode(-1);
dummy->next = head;
for(auto p = dummy;p->next && p->next->next;)
{
auto a = p->next,b = p->next->next;
p->next = b;
a->next = b->next;
b->next = a;
p = a; //如果不明白这个地方的话,就画画图看看
}
return dummy->next;
}
};
6-反转链表 - LeetCode第206题
题目描述:
题目分析:
这个题目是先用一个指针保存了一下第三个结点我们要做的就是中间的箭头翻转过来,让head指向5,让1指向空。在实现这个想法的时候,最重要的就是中间节点的指针的翻转,这里是用两个指针来实现的。
这个题目最重要的是要想明白各个指针是怎么指向的,以及如果判断结束,这个我认为是重点。
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(!head) return NULL;
auto a = head,b = head->next;
while(b)
{
auto c = b->next;
b->next = a;
a = b;
b = c;
}
head->next = NULL;
head = a;
return head;
}
};
总结一下上面的几个代码的话,几乎就是用两个或三个指针来完成的。
7-反转链表- LeetCode第92题
题目描述:
题目分析:
上面就是一个解题的思路,做这个题目的话就是分两步去做,首先是把m->n之间的节点反转过来,然后再把反转之后的一段节点插到原来的序列中去,但是需要提前找到a、m、n、c这是个节点。经过整数值m,走m-1步可以找到指针a;通过整数n走n步到达指针d,然后接着next就可以找到指针b、c。 这个题目头节点时可能会被反转,因为头节点可能会变化,所以这里搞一个虚拟的头节点。指针的设置如上面,这个指针如果整明白了,那么这个题目就会好做很多,但是那个指针指向那个节点一定一定要好好的盯着,很容易出错,思路上都是不难的。
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int m, int n) {
if(m==n) return head;
auto dummy = new ListNode(-1);
dummy->next = head;
auto a = dummy,d = dummy;
for(int i = 0; i < m - 1; i++) a = a->next;
for(int j = 0; j < n; j++) d = d->next;
auto b = a->next,c = d->next;
for(auto p = b,q = p->next;q != c;)
{
auto o = q->next;
q->next = p;
p = q;
q = o;
}
b->next = c;
a->next = d;
return dummy->next;
}
};
/**
//这个代码是指针换的之后的,自己写的
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int m, int n) {
auto dummy = new ListNode(-1);
dummy->next = head;
auto a = dummy,c = dummy;
for(int i = 0;i < m-1;i++) a = a->next;
for(int j = 0;j < n; j++) c = c->next;
auto b = a->next,d = c->next;
for(auto p = b, q = b->next;q != d;)
{
auto o = q->next;
q->next = p;
p = q;
q = o;
}
a->next = c;
b->next = d;
return dummy->next;
}
};
8-相交链表 - LeetCode第160题
题目描述:
题目分析:
题解的情况有两种:
这个题目的思路很巧妙,首先看算法的步骤:
- 用两个指针分别从两个链表头部开始扫描,每次分别走一步;
- 如果指针走到NULL,则从另一个链表头部开始走;
- 当两个指针相同时,
(1) 如果指针不是NULL,则指针位置就是相遇点;
(2) 如果指针是NULL,则两个链表不相交;
(3) 访问链表的时间复杂度是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) {
auto p = headA,q = headB;
while(p != q)
{
if(p) p = p->next;
else p = headB;
if(q) q = q->next;
else q = headA;
}
return p;
}
};
题意很长,做起来也不是很容易,但是想到这个方法的话,就太容易了
9-环形链表 - LeetCode第142题
题目描述:
题目分析:
本题想象它是一个这样的形式,这个题目是快慢指针的一个经典题目,fast指针(红色)每次走两步,second指针(蓝色)每次走一步;当蓝色指针从a点走到b点的时候,它走过的距离是X,那么红色指针应该走的距离是2X,也就是从a点先走到b点,然后在这个圈上又走了X远的距离,我们假设红色指针在C’位置上,假设b点到C’位置的距离是y,那么我们在这个圆圈上找到对称的位置C,b到C的距离也是y,让蓝色指针从b到C点(走了y),那么红色指针要走2y,红蓝指针在C点相遇;相遇之后,把蓝颜色的指针放回到开头(此时红颜色依然在C点),这时候红色指针和蓝色指针每次走一步,然后当蓝颜色指针走到b的时候,红颜色也将走到b,即它们一定在b点相遇。
原因的话,我这么解释:红蓝指针在C点相遇之后,蓝色从a—>b走X到b,红色从C每次走一步,走距离X一定到达b,因为原来从b点出发走X步到了C’,那么现在从C出发(起点退后了y)走X应该到b
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
auto fast = head,slow = head;
while(fast){
fast = fast->next;
slow = slow->next;
if(fast) fast = fast->next;
else break;
if(fast == slow)
{
slow = head;
while(fast != slow)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
}
return NULL;
}
};
个人觉得代码的难度在于判断条天上面。
10排序链表 - LeetCode第148题
题目描述:
题目分析:
题解:这个题目因为它有各种要求,时间复杂度是O(nlogn),空间复杂度是常数(O(1))所以有很多不能用的方法,而是用了一个自底向上的归并排序。因为快速排序的话是要用递归的写法,递归的写法的话肯定是需要用到系统栈的,只要是用到系统栈的题目,如果是递归N层的,一般需要O(logn)空间,故快速排序的话,时间复杂度是满足要求的,但是空间复杂度是不满足要求的。同样的一般的归并排序,递归写法的话也是需要O(logn)空间复杂度的,也是不满足要求。所以这个题目只有一种写法,即自底向上的归并排序的写法,用一个循环的形式,这样的话就不会用到栈了。
自底向上的归并排序的算法:
代码:
/**
* 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* sortList(ListNode* head) {
int n = 0;
for(auto p = head;p;p = p->next) n++;
auto dummy = new ListNode(-1); //头节点可能会变,所以我们创建一个虚拟的头节点
dummy->next = head;
for(int i = 1;i < n; i *= 2) //每次隔得间隔
{
auto cur = dummy;
for(int j = 0; j + i < n; j += i * 2)//枚举每一段
{
auto left = cur->next, right = cur->next;
for(int k = 0;k < i; k++) right = right->next;
//下面要进行归并
int l = 0,r = 0;
while(l < i && r < i && right)
if(left->val <= right->val)//经典的归并排序
{
cur->next = left;
cur = left;
left = left->next;
l++;
}
else//否则把右边放过来,cur始终指向尾部的意思
{
cur->next = right;
cur = right;
right = right->next;
r++;
}
//下面将两段中没有循环的接起来
while(l < i)
{
cur->next = left;
cur = left;
left = left->next;
l++;
}
while(r < i && right)
{
cur->next = right;
cur = right;
right = right->next;
r++;
}
cur->next = right;
}
}
return dummy->next;
}
};//这个题可以当作一个模版了