本文还是继续介绍链表经典题,中间会穿插两道中等难度的题,拓展一下解题思路。只有见的题多了,做的题多了,才能游刃有余。
题目链接直接点击
这道题LeetCode标注的中等题,但是实际上只要理清思路,还是很好做的。这道题其实和移除链表指定元素有点像,只不过这个是移动节点。我们可以将原链表拆分成两条链表,一条链表所有节点的val值都比X小,另一条链表所有节点的val大于等于X,最后将两条链表连接起来组成一条新链表,这个新链表就是所求的链表。我们用什么方法来拆分原链表的节点呢?因为移动节点后要保持原链表的相对位置,所有就是顺序移动节点,即为尾插。所以,我们采用尾插的方法从原链表上拿下节点进行拆分。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* partition(struct ListNode* head, int x){
struct ListNode* head1=
(struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode*tail1=head1;
head1->next=NULL;
struct ListNode* head2=
(struct ListNode*)malloc(sizeof(struct ListNode));
head2->next=NULL;
struct ListNode* tail2=head2;
struct ListNode* cur=head;
while(cur)
{
if(cur->val<x)
{
tail1->next=cur;
tail1=cur;
cur=cur->next;
}
else
{
tail2->next=cur;
tail2=cur;
cur=cur->next;
}
}
tail1->next=head2->next;
tail2->next=NULL;
struct ListNode* new_head=head1->next;
free(head1);
free(head2);
return new_head;
}
之前提到过在需要尾插处理时,可以适当考虑一下带哨兵位尾插,这道题就可以采用带哨兵位尾插,不然需要判断两个链表头节点为空的情况,这种情况需要单独处理。这样就这样就显得略微麻烦了,我们直接带哨兵位尾插,就可以直接尾插,好处多多。只要最后将哨兵位给释放就行了,表示节点vax小于x的链表的尾节点next指向另一个链表的头节点,就相当于将两条链表连接起来了。因为每个节点next指向都是改变了的,所以也就是不确定新链表的尾节点的next是否指向空,这就需要最后手动置为空,不然就有可能形成带环的链表引发错误。这个新链表的尾节点,就是另一条表示节点的val值大于等于x的链表的尾节点。只要画图理清关系了这都很容易想到。
题目链接直接点击
这道题我介绍两种做法。首先第一种:环形链表在遍历过程中是会出现类似于死循环的状况,因为它的尾节点next不在指向空,而是指向链表中的某个节点。基于这个特性,如果链表在遍历时出现了这样的情况,那么它就是循环链表。那么这个死循环状态怎么表示出来呢?我们看到题目给了节点个数是0到10000,我们直接定义一个变量来计数,如果每循环一次就计数一次,如果在某次循环中计数到达了100001就说明这个链表是循环链表。因为循环链表会一直计数。如果不是循环链表,在遍历时用来遍历的指针变量会指向尾节点的空指针,从而跳出循环,这个时候计数肯定也是小于10000的,所以这个链表肯定不是带环的。
第二种方法就是双指针也就是快慢指针处理。快慢指针遍历链表,快指针走两步,慢指针走一步。如果链表带环快慢指针会相遇也就是出现快指针等于慢指针的情况,如果不带环快指针就会遍历完整个链表最后结束遍历。
节点计数
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
bool hasCycle(struct ListNode *head) {
int count=0;
struct ListNode* cur=head;
while(cur)
{
if(count==100001)
{
return true;
}
cur=cur->next;
count++;
}
return false;
}
快慢指针
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
bool hasCycle(struct ListNode *head) {
struct ListNode* fast=head;
struct ListNode* slow=head;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
if(slow==fast)
{
return true;
}
}
return false;
}
我们扩展一下,为啥快指针走2步就可以相遇,快指针走 3步 走4步 走n步能相遇吗?我们来分析一下。
其实最好的情况就是当慢指针一入环两者刚好相遇,这是环最小的时候,也就是环只有两个节点时候。就是n直接等于0,fast在环中转了一圈相当于等了慢指针1步。
那如果不是快指针走2步呢?
当n=4时可以带进去和上面一样的推导,因此只有快指针走2步慢指针走1步,才能适用于所有的情况。
题目链接直接点击
这道题是刚才那道带环链表的升级版,这道题需要这找到这个带环的入口。这个找环入口我提供两种思路。第一种思路:刚才我们知道了如果一条链表带环,快慢指针入环一定会相遇,我们从这个相遇处开始将这个环断开,相遇处的节点成为一条链表的头节点,相遇处节点的next指向的节点是另一条链表的头节点,然后求它俩的相交处,这样求入环口问题就转换求两个链表的相交节点问题。
这样的思路等于是将之前判断链表是否带环和求两个链表相交节点问题结合在一起了。这也侧面说明了当做的题多了,总结复盘多了,做题的思路就会更加开阔。
第二种方法:这个方法需要用到数学推导了,先给出一个结论,当快慢指针入环相遇后,这个时候从整个链表头节点开始向前遍历,然后环中 从相遇的节点开始向前遍历,最后两者一定会相遇,这个相遇的节点就是入环口。
这个怎么推导呢?我们画图分析一下。
其实这个所谓的距离路程就是相差的节点个数
快慢指针求相交节点
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode* fast=head;
struct ListNode*slow=head;
struct ListNode* meet;
struct ListNode* head1;
int flag=0;//标记判断是否有环
while(fast&&fast->next)
{
fast=fast->next->next;
slow=slow->next;
if(slow==fast)
{
meet=fast;
head1=meet->next;
meet->next=NULL;
flag=1;
}
}
if(flag==0)//没环直接返回空
{
return NULL;
}
struct ListNode* curA=head;
struct ListNode* curB=head1;
int lenA=0;
int lenB=0;
while(curA)
{
++lenA;
curA=curA->next;
}
while(curB)
{
++lenB;
curB=curB->next;
}
if(curB!=curA)
{
return NULL;
}
int gap=abs(lenA-lenB);
struct ListNode* longlist=head;
struct ListNode* shortlist=head1;
if(lenA<lenB)
{
longlist=head1;
shortlist=head;
}
while(gap--)
{
longlist=longlist->next;
}
while(longlist!=shortlist)
{
longlist=longlist->next;
shortlist=shortlist->next;
}
return longlist;
}
这样处理方式不用数学推理,但是写起来繁琐一点。代码中我引入一个flag变量来标记判断是否有环。
双指针推理
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode* fast=head;
struct ListNode*slow=head;
struct ListNode* meet;
while(fast&&fast->next)
{
fast=fast->next->next;
slow=slow->next;
if(slow==fast)
{
meet=fast;
while(meet!=head)
{
head=head->next;
meet=meet->next;
}
return meet;
}
}
return NULL;
}
这段代码虽然写起来比较简单,但是需要推理结论比较考验人。最里面的while循环如果不想写到里面,单独写到外面也是可以的。
题目链接直接点击
这道题题目看起来虽然很长但是实际上就是要求我们根据原链表复制出一条新链表,虽然复制链表不是难事,但是有个很麻烦问题就是每个节点除了next指针指向下一个节点外,还有一个指针指向任意一个节点或者空。要保证这个指针指向和原来链表节点中的指向一样。因为这个指针指向是随机的,当复制节点时,这个指针可能指向你还没有复制出来的节点。这是个很棘手的问题不好处理。
为了解决这个问题,我们可以把原链表的每个节点后增加一个节点,节点的val保持一样,这就相当于复制节点。但是只是复制了一部分,还有rondom的指向没有搞定,我们接着搞定rondom。我们将复制的节点的rondom指向原链表节点rondom指向的节点的nex指向处,这个听起来比较拗口。简单来说就是:复制节点->random=原链表节点->random->next; 最后,在将复制的节点从原链表上拿下来组成一条新链表,就是最后所求的复制链表。具体如何做呢?我们画图分析一下吧。
复制链表
struct Node* copyRandomList(struct Node* head) {
struct Node* cur=head;
struct Node* capy=NULL;
while(cur)
{
struct Node* newnode=(struct Node*)malloc(sizeof (struct Node));
newnode->val=cur->val;
struct Node* Next=cur->next;
cur->next=newnode;
newnode->next=Next;
cur=Next;
}
cur=head;
while(cur)
{
capy=cur->next;
if(cur->random==NULL)
{
capy->random=NULL;
}
else
{
capy->random=cur->random->next;
}
cur=cur->next->next;
}
struct Node* Head=NULL;
struct Node* tail=NULL;
cur=head;
while(cur)
{
if(Head==NULL)
{
Head=cur->next;
tail=cur->next;
}
else
{
tail->next=cur->next;
tail=cur->next;
}
cur=cur->next->next;
}
return Head;
}
这道题大致分为3步:第1步复制节点,第2步搞定随机指针的指向,第3步取下复制节点组成新链表。因为复制节点是紧跟着原链表节点的,所以cur->next就是复制节点,cur->next->next是原链表节点。这一点可以从上面的图中看出来,其次最后一个复制节点的next是指向空的,所以在最后尾插复制节点时,不用将复制链表的尾节点next手动指空,因为尾节点next就是指向空的,这道题关键在于处理随机指针,画图很重要,有助于理解节点之间的指向关系。
题目链接直接点击
这道题虽然标注的中等难度的题,但实际上更像一个脑筋急转弯。我们简单分析一下吧:这道没有没给头节点,只给了要删除的节点。一般链表节点的删除我们都是通过头节点找到要删除的节点的前驱节点,将前驱节点和要删除节点的后继节点相连,最后删除这个节点。
这道题就只有要删除的节点怎么做呢?我们换个思路,将后继结点val值和这个节点交换,在将这个节点的next指向后继节点的next,这不就相当于删除了节点了。
void deleteNode(struct ListNode* node) {
node->val=node->next->val;
node->next=node->next->next;
}
因为题目说了不是空间上的删除,也就不用free释放后继节点的空间,如果你释放了,Lettcode会报错。因为题目说了不会传最后一个节点,也就不用担心空指针解引用的问题。题目也说了传入的节点是链表中的节点,也就不用考虑传入空指针的问题。
这道题不太像个中等题,因为到了本文结尾,所以就介绍了这道题,就当作放松吧!