移除链表元素
题意
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
法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 {
public:
ListNode* removeElements(ListNode* head, int val)
{
//删除头结点
while(nullptr!=head&&head->val==val)
{
ListNode* temp=head;
head=head->next;
delete temp;
}
//删除非头结点
ListNode* cur=head;
while(cur!=nullptr&&cur->next!=nullptr)
{
if(cur->next->val==val)
{
ListNode* temp = cur->next;
cur->next=temp->next;
delete temp;
}
else
{
cur=cur->next;
}
}
return head;
}
};
法2—设置一个虚拟头结点
这样原链表的所有节点就都可以按照统一的方式进行移除了。
最后呢在题目中,return 头结点的时候,别忘了 return dummyNode->next;
, 这才是新的头结点。
/**
* 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* removeElements(ListNode* head, int val)
{
ListNode* dummyHead = new ListNode(0,nullptr);
dummyHead->next=head;
ListNode* cur = dummyHead;
while(cur->next!=nullptr)
{
if(cur->next->val==val)
{
ListNode* del_node = cur->next;
cur->next=del_node->next;
delete del_node;
}
else
{
cur=cur->next;
}
}
head=dummyHead->next;
delete dummyHead;
return head;
}
};
设计链表
题意
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
在链表类中实现这些功能:
- get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
- addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
- addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
- addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
- deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
解题思路
使用虚拟头结点来实现这些方法会方便很多,都是一些链表基础性的操作,看代码就懂了。
class MyLinkedList
{
public:
//链表结点设计
struct LinkedNode
{
int val;
LinkedNode* next;
LinkedNode(int value):val(value),next(nullptr){}
};
private:
int _size;// 链表结点个数
LinkedNode* _dummyHead;//虚拟头结点
public:
//链表初始化操作
MyLinkedList()
{
_size=0;
_dummyHead=new LinkedNode(0);
}
//获取链表中第 index 个节点的值。如果索引无效,则返回-1。
//注意,index从0开始的。有_size个节点,则索引范围为:0~_size-1
int get(int index)
{
if(index<0||index>_size-1)
return -1;
LinkedNode* cur=_dummyHead->next;
while(index--)
{
cur=cur->next;
}
return cur->val;
}
//在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
void addAtHead(int val)
{
LinkedNode* add_node=new LinkedNode(val);
add_node->next=_dummyHead->next;
_dummyHead->next=add_node;
_size++;
}
//将值为 val 的节点追加到链表的最后一个元素。
void addAtTail(int val)
{
LinkedNode* add_node=new LinkedNode(val);
LinkedNode* tail_node=_dummyHead;
while(tail_node->next!=nullptr)
{
tail_node=tail_node->next;
}
tail_node->next=add_node;
_size++;
}
/*
在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
*/
void addAtIndex(int index, int val)
{
//index大于链表长度
if(index>_size)
{
cout<<"插入位置大于链表长度"<<endl;
return;
}
//如果index小于0,则在头部插入节点。
if(index<0)
{
LinkedNode* add_node = new LinkedNode(val);
add_node->next=_dummyHead->next;
_dummyHead->next=add_node;
_size++;
}
else // index范围:0~_size
{
//寻找第index-1个节点
LinkedNode* cur=_dummyHead;
while(index--)
{
cur=cur->next;
}
LinkedNode* add_node = new LinkedNode(val);
add_node->next=cur->next;
cur->next=add_node;
_size++;
}
}
void deleteAtIndex(int index)
{
// 如果索引 index 有效,则删除链表中的第 index 个节点。
if(index>=0&&index<_size)
{
LinkedNode* cur=_dummyHead;
while(index--)
{
cur=cur->next;
}
LinkedNode* del_node=cur->next;
cur->next=del_node->next;
delete del_node;
_size--;
}
}
};
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList* obj = new MyLinkedList();
* int param_1 = obj->get(index);
* obj->addAtHead(val);
* obj->addAtTail(val);
* obj->addAtIndex(index,val);
* obj->deleteAtIndex(index);
*/
反转链表
题意
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
法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
{
public:
ListNode* reverseList(ListNode* head)
{
if(nullptr==head || nullptr==head->next)
return head;
ListNode* temp=head->next;
head->next=nullptr;
while(nullptr!=temp)
{
//依次将temp所指向的节点插入到头部
ListNode* add_node=temp;
temp=temp->next;
add_node->next=head;
head=add_node;
}
return head;
}
};
法2—双指针
- 首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
- 然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下cur这个节点的下一个节点。
- 为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。
- 接下来,就是循环往下走,继续移动pre和cur指针。
- 最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。
/**
* 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* reverseList(ListNode* head)
{
if(nullptr==head || nullptr==head->next)
return head;
ListNode* temp;//保存cur的下一个节点
ListNode* pre = nullptr;
ListNode* cur=head;
while(nullptr!=cur)
{
temp=cur->next;
cur->next=pre;
pre=cur;
cur=temp;
}
return pre;
}
};
法3—递归
/**
* 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* reverse(ListNode* pre,ListNode* cur)
{
if(nullptr==cur)
return pre;
ListNode* temp=cur->next;
cur->next=pre;
return reverse(cur,temp);
}
ListNode* reverseList(ListNode* head)
{
if(nullptr==head || nullptr==head->next)
return head;
return reverse(nullptr,head);
}
};
两两交换链表的中的节点
题意
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
解题思路
这道题目正常模拟就可以了。 建议使用虚拟头结点,这样会方便很多,要不然每次针对头结点(没有前一个指针指向头结点),还要单独处理。
创建虚拟头结点dummyHead,令 dummyHead.next = head。令 cur 表示当前到达的节点,初始时 cur= dummyHead。每次需要交换 cur后面的两个节点。
如果 cur 的后面没有节点或者只有一个节点,则没有更多的节点需要交换,因此结束交换。否则,获得 cur 后面的两个节点 node1 和 node2,通过更新节点的指针关系实现两两交换节点。
具体而言,交换之前的节点关系是 cur-> node1 -> node2,交换之后的节点关系要变成 cur-> node2 -> node1,因此需要进行如下操作:
cur.next = node2;
node1.next = node2.next;//节点1的next指向节点2的next
node2.next = node1;//节点2的next指向节点1
完成上述操作之后,节点关系即变成 cur-> node2 -> node1->.....。再令 cur= node1,对链表中的其余节点进行两两交换,直到全部节点都被两两交换。
两两交换链表中的节点之后,新的链表的头节点是 dummyHead.next
,返回新的链表的头节点即可。
/**
* 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(nullptr==head)
return nullptr;
ListNode* dummyHead=new ListNode(0,head);
ListNode* cur=dummyHead;
while(cur->next!=nullptr&&cur->next->next!=nullptr)
{
ListNode* node1=cur->next,*node2=cur->next->next;
cur->next=node2;
node1->next=node2->next;
node2->next=node1;
cur=cur->next->next;
}
head=dummyHead->next;
delete dummyHead;
return head;
}
};
删除链表的倒数第N个节点
题意
给你一个链表,删除链表的倒数第 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)
{
if(!head)
return head;
ListNode* dummy_head=new ListNode(0,head);
stack<ListNode*> my_stack;
ListNode* cur=dummy_head;
while(cur)
{
my_stack.push(cur);
cur=cur->next;
}
for(int i=0;i<n;i++)
{
my_stack.pop();
}
ListNode* pre_delNode=my_stack.top();//待删除节点的上一个节点
ListNode* delNode=pre_delNode->next;
pre_delNode->next=delNode->next;
delete delNode;
head=dummy_head->next;
delete dummy_head;
return head;
}
};
解法2—双指针
双指针的经典应用。
由于我们需要找到倒数第 n 个节点,因此我们可以使用两个指针 fast 和 slow 同时对链表进行遍历,并且 fast 比 slow 超前 n 个节点。当fast 遍历到链表的末尾时(即 fast 为空指针),slow 就恰好处于倒数第 n 个节点。
具体地,初始时 fast 和 slow 均指向头节点。我们首先使用 fast 对链表进行遍历,遍历的次数为 n。此时,slow 和 fast之间间隔了 n−1 个节点,即 slow 比 fast 超前了 n 个节点。
在这之后,我们同时使用 slow 和 fast 对链表进行遍历。当 fast 遍历到链表的末尾(即 fast为空指针)时,slow 恰好指向倒数第 n 个节点。
如果我们能够得到的是倒数第 n 个节点的前驱节点而不是倒数第 n 个节点的话,删除操作会更加方便。因此我们可以考虑在初始时将slow指向虚拟头节点,其余的操作步骤不变。这样一来,当 fast遍历到链表的末尾时,slow 的下一个节点就是我们需要删除的节点。
/**
* 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)
{
if(!head)
return head;
ListNode* dummy_head=new ListNode(0,head);
ListNode* fast,*slow;
fast=head;
slow=dummy_head;
while(n--&&fast!=nullptr)
{
fast=fast->next;
}
while(fast!=nullptr)
{
fast=fast->next;
slow=slow->next;
}
ListNode* del_node=slow->next;
slow->next=del_node->next;
delete del_node;
head=dummy_head->next;
delete dummy_head;
return head;
}
};
链表相交
题意
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null
。
图示两个链表在节点 c1
开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
解法1
设「第一个公共节点」为 node ,「链表 headA」的节点数量为 a ,「链表 headB」的节点数量为 b ,「两链表的公共尾部」的节点数量为 cc ,则有:
- 头节点
headA
到node
前,共有 a - c 个节点; - 头节点
headB
到node
前,共有 b - c 个节点;
考虑构建两个节点指针 A
, B
分别指向两链表头节点 headA
, headB
,做如下操作:
- 指针 A 先遍历完链表 headA ,再开始遍历链表 headB ,当走到 node 时,共走步数为:
a + (b - c)
- 指针 B 先遍历完链表 headB ,再开始遍历链表 headA ,当走到 node 时,共走步数为:
b + (a - c)
如下式所示,此时指针 A
, B
重合:a+(b−c)=b+(a−c)。
- 若两链表 有 公共尾部 (即 c > 0 ) :指针 A , B 同时指向「第一个公共节点」node 。
- 若两链表 无 公共尾部 (即 c = 0 ) :指针 A , B 同时指向 null 。
因此返回 A
即可。
/**
* 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)
{
ListNode* A=headA,*B=headB;
while(A!=B)
{
A=A!=nullptr?A->next:headB;
B=B!=nullptr?B->next:headA;
}
return A;
}
};
解法2
简单来说,就是求两个链表交点节点的指针。我们求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置,如图:
此时我们就可以比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到交点。否则循环退出返回空指针。
/**
* 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)
{
ListNode* curA = headA;
ListNode* curB = headB;
int lenA = 0, lenB = 0;
while (curA != NULL)
{
// 求链表A的长度
lenA++;
curA = curA->next;
}
while (curB != NULL)
{
// 求链表B的长度
lenB++;
curB = curB->next;
}
curA = headA;
curB = headB;
// 让curA为最长链表的头,lenA为其长度
if (lenB > lenA)
{
swap (lenA, lenB);
swap (curA, curB);
}
// 求长度差
int gap = lenA - lenB;
// 让curA和curB在同一起点上(末尾位置对齐)
while (gap--)
{
curA = curA->next;
}
// 遍历curA 和 curB,遇到相同则直接返回
while (curA != NULL)
{
if (curA == curB)
{
return curA;
}
curA = curA->next;
curB = curB->next;
}
return NULL;
}
};
环形链表II
题意
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
思路
判断链表是否有环
可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢?
首先第一点:fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。
那么来看一下,为什么fast指针和slow指针一定会相遇呢?
可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针,会发现最终都是这种情况, 如下图:
fast和slow各自再走一步, fast和slow就相遇了。
这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。
如果有环,如何找到环的入口?
假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:
那么相遇时: slow指针走过的节点数为: x + y
, fast指针走过的节点数:x + y + n (y + z)
,n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数。
因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:(x + y) * 2 = x + y + n (y + z)
两边消掉一个(x+y): x + y = n (y + z)
因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。
所以要求x ,将x单独放在左面:x = n (y + z) - y
。
再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z
注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。
这个公式说明什么呢?
先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。
当 n为1的时候,公式就化解为 x = z;
这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。
让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。
那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。
其实这种情况和n为1的时候 效果是一样的,因为有环,所以他们总会遇到的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。
/**
* 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)
{
ListNode* fast,*slow;
fast=slow=head;
while(fast!=nullptr&&fast->next!=nullptr)
{
slow=slow->next;
fast=fast->next->next;
if(slow==fast)
{
//有环
ListNode* index1=fast;
ListNode* index2=head;
while(index1!=index2)
{
index1=index1->next;
index2=index2->next;
}
return index1;
}
}
return nullptr;
}
};
补充:
为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?
首先slow进环的时候,fast一定是先进环来了。
如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子:
可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。
重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图:
那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。
也就是说slow一定没有走到环入口3,而fast已经到环入口3了。
这说明什么呢?
在slow开始走的那一环已经和fast相遇了。
为什么fast不会跳过slow呢? 在上面已经说过一次了,fast相对于slow是一次移动一个节点,所以不可能跳过去。