深入认识数据结构(四)---解决单链表问题

前言

单链表的特点就是只能由上一个节点找到下一个节点,这样会使得我们在解决单链表问题时需要格外细心。
在这里插入图片描述

这里有一些在解决单链表问题时需要注意的情况和一些解决方法。

1注意是否引用了空指针

我们在解决单向链表的问题的时候,由于只能通过上一个节点找到下一个节点,所以我们通常会设置多个指针来保存节点的地址,但是在使用这些指针的时候我们需要注意不能引用了空指针。

下面两个题目可以帮助我们认识到什么时候任意出现空指针,以及如何避免。

1.1删除链表中指定的节点

**题目链接:删除链表中指定的元素

在这里插入图片描述

思路:

这道题中,我们需要移除链表中特定的元素。我们可以利用两个指针:一个指针cur指向当前节点,一个指针pre指向前一个节点。

注意:怎样控制循环?

当我们只需要删除链表中的一个节点的时候,我们只需要判断当前节点是否满足条件,满足条件就执行删除,删除后就结束循环。

但是这道题不是只删除一个节点,也存在一个链表中的多个节点需要删除的情况:所以只有当检查完所有的节点后才能结束循环。

struct ListNode *cur = head;
while(cur)//遍历链表,遍历完后会退出循环。
{
    //执行操作
    cur = cur->next;
}

在这里插入图片描述

注意:这里我们使用pre指针指向前一个节点的时候,不能引用空指针。

当我们使用两个链表是遍历指针的时候,我们需要重点在意:不能在前一个指针为空指针的时候去访问它

即当我们访问到第一个节点的时候,由于该节点此时没有前一个节点,所以pre指针还是一个空指针。
如果我们要删除第一个节点的话,我们就只能改变头指针的值,不能改变pre指针的值。

解题:

struct ListNode* removeElements(struct ListNode* head, int val){
	struct ListNode* pre = NULL;
	struct ListNode* cur = head;

	while (cur)//当cur指针为NULL,就说明已经遍历完链表。
	{
		if (cur->val == val)
		{
			struct SListNode* next = cur->next;
            if(pre == NULL)//pre为空指针的情况,说明这是第一个节点
            {
                head = next;
            }
            else
            {    
			    pre->next = next;
            }
            free(cur);
            cur = next;
		}
		else
		{
			pre = cur;
		    cur = cur->next;//每一次cur都会指向后面一个节点
		}
	}
	return head;
}

1.2反转一个链表

这道题的链接:反转链表
这道题需要我们反转一个链表,并且返回这个新链表第一个节点的地址:
在这里插入图片描述

这道题有两个解法,但是实质都是相同的:改变各个节点中next指针的值。

方法一

让每一个节点中的指针指向前一个节点

我们可以利用三个指针,一个指针(命名为cur)用来遍历整个链表,一个指针(命名为prev)来指针前一个节点,

一个指针next来指向该节点的后一个节点。

每一次迭代到一个节点,就让这个节点的的next指针指向前一个节点的地址,然后让prev指针指向当前节点,当前节点指向后一个节点,依次迭代,直到cur指针的值为空指针就结束循环。

在这里插入图片描述

在这里插入图片描述

解题

struct ListNode* reverseList(struct ListNode* head){

     if(head == NULL)
    {
        return NULL;
    }
    struct ListNode *prev = NULL;
    struct ListNode *cur = head;
    struct ListNode *next = cur->next;

   

    while(cur)
    {
        cur->next = prev;
        prev = cur;
        cur = next;
        if(next)//如果next指针为空指针,那么久说明cur已经是最后一个节点了
        next = next->next;

    }
    return prev;
}

注意:
这个方法需要我们保存下一个节点的地址,每一次循环我们都会让next指针指向下一个节点*,但是当next已经是空指针的时候,访问next->next就会造成引用空指针,所以我们需要在next指针为空指针的时候就结束循环。

我们也可以利用方法二来避免引用空指针的情况

方法二

依次插入到一个新的链表中

创建一个新的链表头,然后遍历原链表,每一次把头节头插到新的链表中,然后返回新链表的头节点,

这个方法仍然需要两个指针,一个指针用来指向当前迭代的节点,一个指针用来指向当前节点的后一个节点。

struct ListNode* reverseList(struct ListNode* head){
    struct ListNode *newhead = NULL;

    if(head == NULL)
    {
        return NULL;
    }

    struct ListNode*cur = head;
    struct ListNode*next = NULL;
    while(cur)
    {
        next = cur->next;
        cur->next = newhead;
        newhead = cur;
        cur = next;
    }
    return newhead;


}

2.利用带哨兵位的链表(带头链表)

上面我们注意到,每一次如果我们要向一个位置的链表的尾部插入一个节点的时候,都需要判断这个链表是否是空链表。

向空链表尾部插入节点和向非空链表尾部插入节点的区别:

在这里插入图片描述

那么有没有一个办法可以让我们避免掉这个情况,让我们不用区分两种情况呢?

这个方法就是: 我们创建一个头节点(我们称为哨兵位),然后再向后插入新的节点,最后返回哨兵位后面一个节点的地址,这个地址就是我们需要得到的链表的头指针。

这里利用两个例题来使用带哨兵位的链表:

2.1.合并两个有序链表

该题的链接是:合并两个有序链表
在这里插入图片描述

思路:

这道理我们得到了两个有序的链表,我们需要把他们按大小顺序合并到一个链表中

我们可以先创建一个头节点和两个指针,这两个指针指向这个节点,然后再创建两个指针,分别指向两个链表的头节点。

在这里插入图片描述

比较list1和list2 指向的节点中的值得大小,我们将较小的节点插入到newhead节点的后面,然后让对应的cur指针指向连标志哦你的下一个节点,同时tail指针指向新插入的节点。
在这里插入图片描述

注意:

  1. 什么时候循环停止。
    当其中一个链表已经遍历完,循环就结束了。

  2. 我们返回的是newhead指向的下一个节点的地址

    newhead是我们创建的哨兵位

该题解法:

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){

    //创建一个新的链表节点和一个指向这个节点的指针
    struct ListNode*newhead = (struct ListNode *)malloc(sizeof(struct ListNode));
    newhead->next = NULL;
    //创建一个尾指针指向新的头节点

    struct ListNode*tail = newhead;

    while(list1 && list2)
    {
        if(list1->val <list2->val)
        {
            tail->next = list1;
            list1 = list1->next;
            tail = tail->next;
        }
        else
        {
            tail->next = list2;
            list2 = list2->next;
            tail = tail->next;
        }


    }
    if(list1)//如果其中一个链表已经遍历完,
   // 那么剩下的链表的全部节点可以直接插入到尾节点后面。
    {
        tail ->next = list1;
    }
    if(list2)
    {
        tail ->next = list2;
    }
    

    return newhead ->next;

}

2.2 将一个链表按某个条件分割成多个部分

该题的链接是:链表分割

在这里插入图片描述

思路:

我们可以根据他提供x= 5来将链表分为两类,然后将这个链表分割成为两个链表,一部分小于x,一部分大于x。

然后将两个链表收尾链接起来,就得到了答案:

在这里插入图片描述

这道题我们也可以利用通过添加哨兵位头节点来简化解题步骤:

先创建两个头节点,一个头节点用来作为小于x的节点集合的头节点,一个头节点用来作为大于等于X的节点集合的头节点。

下面是这道题的解法:

  
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        // write code here
        
    
        //创建两个带头的链表
        struct ListNode *greater = (struct ListNode*)malloc(sizeof(struct ListNode));
        struct ListNode *less = (struct ListNode*)malloc(sizeof(struct ListNode));
        
        greater->next=less->next = NULL;
        
        struct ListNode *tail_less = less;
        struct ListNode *tail_greater = greater;//创建了两个指针来保存两个新节点的地址。
        
        struct ListNode *cur = pHead;//一个指针来遍历原链表
        
        while(cur)
        {
            if(cur->val  <x)
            {
                tail_less->next = cur;
                tail_less = tail_less->next;
            }
            else
            {
                tail_greater->next = cur;
                tail_greater = tail_greater->next;
            }
            
            
            cur = cur->next;
        }
        
        
        tail_greater->next = NULL;//保证链表最后一个节点的next指针为空指针。
        tail_less->next = greater->next;
        struct ListNode *list = less->next;
        return list;
        
        
    }
};

注意:

当我们得到更新后的整个链表时,我们需要把最后一个节点的next指针赋为NULL
原因:我们不能保证新链表的最后一个节点时原链表中的最后一个节点。

3.找到链表中特殊位置的节点

有些情况下我们需要去寻找链表中比较特殊的位置的一个点:

比如:

在一个普通单向链表中,找到这个链表中间的那个节点,或者是找到倒数第K个节点(k小于链表的长度);

在相交链表中找到第一个相交节点。

什么是相交链表:

在这里插入图片描述

两个链表中的某两个节点指向的是同一个节点的地址。

解决之类问题的方法:

我们可以利用快慢指针的方法-----创建两个指针,通过改变两个指针之间的距离来找到特殊位置的节点

下面我们利用快慢指针来解决一些经典的问题:

3.1 找到链表的中间节点

该题的链接是:找到链表的中间节点

在这里插入图片描述

思路:这道题要求我们找到中间节点,我们可以设置快慢指针,快指针每次走两步,慢指针每次走一步,当快指针遍历完整个链表的时候,慢指针指向的刚好就是我们要找的中间位置的节点:

在这里插入图片描述

注意:

怎样控制循环条件?

由于每次fast都会向后移动两个节点,但是我们无法保证fast是最后一个节点(如果fast是最后一个节点,那么fast->next就是一个空指针,fast->next->next就是在访问一个空指针,会发生错误),所以我们判断退出循环的条件应该是while(fast && fast->next)这样可以保证遍历链表完后会退出循环。

该题的解法:

struct ListNode* middleNode(struct ListNode* head){
    //利用快慢指针
    struct ListNode*fast = head;
    struct ListNode*slow = head;
    while(fast&&fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
    }

    return slow;//此时慢指针指向的节点就是中间位置的节点。
}

补充:用这个方法可以找到一些其他位置:
例如三分之一位置的节点(我们设置每次快指针走的距离是慢指针走的距离的三倍,当快指针遍历完链表的时候,慢指针就指向了第三分之一位置的节点,但是我们设置循环条件的时候就应该设置为:

while(fast && fast->next && fast->next->next)//控制循环的条件
{
    //循环的内容
    fast = fast->next->next->next;
    slow = slow->next;
}
//退出循环后,slow指针指向的节点就是三分之一位置的节点。

3.2找到链表中倒数第K个节点

该题的链接是:找到倒数第K个节点

在这里插入图片描述

思路:

这道题我们仍然使用快慢指针的方式:

现在我们确定了我们需要返回的节点和最后一个节点的相对距离为K,我们只需要让两个指针之间的距离也为K,即让快指针先走K步,然后两个指针一起以相同的速度向后移动,当快指针成为空指针的时候,慢指针指向的就是倒数第K个节点

在这里插入图片描述

注意:

  1. 在两个指针一起向后移动的循环中,循环一定要等到fast指向NULL的时候才能停止。

  2. 考虑K不合理的情况(K大于节点的个数)—这种情况下返回NULL 如果需要在之遍历一次链表的情况下判断出K是否合理,可以用这样的方法:

while(k--)
{
    if(fast->next)
        fast = fast->next;
    else 
        return NULL;
}
//如果K的值大于节点的个数,
//那么在多次迭代后fast->next一定会等于NULL,此时返回NULL即可。

完整的解法:

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    // write code here
    //用快慢指针:一个指针先走k步,然后两个指针一起向后迭代,
    //当快指针等于空指针的时候,慢指针指向的就是倒数第k个节点。
    
    //但是要注意不能让k 超过链表的长度
    struct ListNode*fast = pListHead;
    struct ListNode*slow = pListHead;
    while(k--)
    {
        if(fast != NULL)
            fast = fast->next;
        else
            return NULL;
    }
    while(fast)
    {
        fast = fast->next;
        slow = slow->next;
    }
    
    return slow;
}

3.3 相交链表

该题的链接是:找到相交链表的交点

在这里插入图片描述

在这里插入图片描述

思路:

首先要判断两个链表是否相交:如果两个链表的最后一个节点的地址相等,那么这两个节点就相交

反之,这两个链表不相交,所以我们需要创建两个尾指针,用来遍历这两个链表,最后比较这两个尾指针的地址

struct ListNode* tail_headA = headA;
struct ListNode* tail_headB = headB;
//找到链表A的最后一个节点
while(tail_headA)
{
    tail_headA = tail_headA->next;
}
//找到链表B的最后一个节点
 while(tail_headB)
{
   	 tail_headB = tail_headB->next;
}
//比较地址,如果地址不相等,就返回空指针。
if(tail_headA != tail_headB)
{
    return NULL;
}

如果地址相等,说明这两个链表相交,我们还需要求出第一个交点:

我们找出两个链表的长度,由于两个链表从第一个交点开始,后面的内容是相等的,所以我们可以通过求出两个链表的长度差K,然后设置快慢指针,快指针指向长链表的头节点,慢指针指向短链表的头节点,先让快指针向后移动K个节点,

在这里插入图片描述

然后两个指针一起向后移动,当两个指针相等的时候,这两个指针所指向的就是第一个节点的地址。

这道题的解法:

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
 //遍历链表判断是否相交
    struct ListNode* tail_headA = headA;
    struct ListNode* tail_headB = headB;
    
    int lenA = 1;
    int lenB = 1;

    while(tail_headA)//遍历A链表的时候计算出该链表的长度,同时找到该链表的为节点
        
    {
        ++lenA;
        tail_headA = tail_headA->next;
    }

    while(tail_headB)
    {
        ++lenB;
        tail_headB = tail_headB->next;
    }

    if(tail_headA != tail_headB)//判断两个链表是否相交
    {
        return NULL;
    }

    int sub = abs(lenA-lenB);//计算链表长度的差值
    struct ListNode*longNode = headA;
    struct ListNode*shortNode = headB;
    if(lenA < lenB)
    {
        shortNode = headA;
        longNode = headB;
    }//找到较长的链表

    while(sub--)
    {
        longNode = longNode->next;
    }

    while(longNode != shortNode)//两个指针一起向后移动,当循环退出的时候,指针所指向的地址就是第一个交点的地址。
    {
        longNode = longNode->next;
        shortNode = shortNode->next;
    }
    return longNode;
}
  • 20
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 32
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 32
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值