冰冰学习笔记:这些链表练习题,你会吗?(中)

欢迎各位大佬光临本文章!!!

 

还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。

本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。

我的博客地址:bingbing~bang的博客_CSDN博客https://blog.csdn.net/bingbing_bang?type=blog

我的gitee:冰冰棒 (bingbingsupercool) - Gitee.comhttps://gitee.com/bingbingsurercool


系列文章推荐

冰冰学习笔记:这些链表练习题,你会吗?(上)


目录

系列文章推荐

前言

1.链表中倒数第K个结点

(1)暴力求解

(2)快慢指针变体形式,先走k步

2.合并两个有序链表

3.链表分割

4.链表相交

5.环形链表

6.链表的回文结构



前言

前面的一篇文章我们主要讲解了链表习题中的双指针思想,例如快慢指针,今天我们还要学一下其他的解题思想。正如杭哥所言,第一难的是找到解决问题的思路,然后是解决方案是否能普世运用,适应各种极端场景。下面我们接着盘那些链表题。

1.链表中倒数第K个结点

题目链接:链表中倒数第k个结点_牛客题霸_牛客网 (nowcoder.com)

题目描述:输入一个链表,输出该链表中倒数第k个结点。

该题目的意思就是接收两个参数,一个参数为K,代表返回数组的结点位置,另一个参数为链表。

例如,链表:1,2,3,4,5。K为2,则返回链表中的倒数第2个结点,即返回存储元素4的链表地址,输出即为4,5。

(1)暴力求解

暴力求解的方法是我们最先想到的方法,这也是我们的底线,当我们想不到其他更好的方法的时候,自己也能解决这道题目。

暴力求解就是我先遍历链表找到链表结点个数count,然后pos=count-k得到需要倒数第k个结点与头节点相差的步数,然后通过循环走到pos的位置,返回该节点的地址即可。

但是这个题还要注意一些特殊情况。

情况一:链表为NULL,链表是空链表,没有元素,怎么返回呢?返回NULL指针。

情况二:K过大,K大于元素个数count,无法返回,此时应该返回NULL指针。

情况三:K为0,返回倒数第0个结点?应该返回NULL指针。

剩下的情况才是我们一开始分析的状况。

全部考虑之后开始写代码:

先考虑情况一和情况三,这两个直接用if语句进行排查即可。由于在普通情况下我们需要得到count的大小 ,那时在判断第二种情况。

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k)
{
   
    struct ListNode* cur = pListHead;
    //排查链表是否为空
    if ( cur == NULL )
    {
        return cur;
    }
    //排查k是否为0
    else if ( k == 0 )
    {
        return NULL;
    }
    //普通情况
    else
    {
        int count = 0;
        while ( cur != NULL )
        {
            cur = cur->next;
            count++;
        }
        //排查k是否大于链表长度
        if ( k > count )
        {
            return NULL;
        }
        int pos = count - k;
        cur = pListHead;
        while ( pos )
        {
            cur = cur->next;
            pos--;
        }
        return cur;
    }

我们发现此种方式写完代码后,我们对代码遍历了两遍,能否遍历一遍就找到呢?答案是可以的,我们来看第二种解法。

(2)快慢指针变体形式,先走k步

这种方式也是快慢指针的形式,创建两个指针,一个指针先走k步,然后两个指针一起走,当先走的快指针走到链表最后的时候,慢指针则走到了倒数第k个结点的位置,然后返回慢指针指向的地址即为所求。此时会发现,我们对链表仅仅遍历了一遍就解决了问题。

当然,前面所讲的特殊情况我们也得考虑进去,在考虑情况二的时候我们放在fast指针先走k步的时候,如果循环还没跳出,fast已经指向NULL了,则说明k的长度是大于整体长度,直接返回NULL。

因此代码如下:

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k)
{
    struct ListNode* fast = pListHead;
    struct ListNode* slow = pListHead;
    //排查链表是否为空以及k是否为0
    if ( slow == NULL || k==0)
    {
        return NULL;
    }
    else
    {
        while ( k-- )
        {
            //排查k是否大于链表长度
            if ( fast == NULL )
            {
                return NULL;
            }
            fast = fast->next;
        }
        while ( fast != NULL )
        {
            fast = fast->next;
            slow = slow->next;
        }
        return slow;  
    }
}

2.合并两个有序链表

题目链接:21. 合并两个有序链表 - 力扣(LeetCode) (leetcode-cn.com)

题目描述:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

该题目和我们以前做的合并两个有序数组的题目类似,只不过此时合并的是链表。例如,链表A:1,3,5,7,9。链表B:2,4,6,8。将两个链表合并之后得到新链表:1,2,3,4,5,6,7,8,9。

在这里我们使用的思想就是归并,即从头开始比较两个链表,取较小值尾插到新链表中。当任一方指向NULL时结束循环,然后判断二者是否为空,将不为空的直接链接在新链表后方,由于本身就为有序链表,因此连接后一定是有序的链表。

分析过后,代码如下所示:

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
    struct ListNode* n1 = list1;
    struct ListNode* n2 = list2;
    struct ListNode* tail = NULL;
    struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
    newnode->next = NULL;
    tail = newnode;
  
    while ( n1 && n2 )
    {
        if ( n1->val <= n2->val )
        {
            tail->next = n1;
            n1 = n1->next;
            tail = tail->next;
        }
        else
        {
            tail->next = n2;
            n2 = n2->next;
            tail = tail->next;
        }
    }
    //连接剩余元素
    if ( n1 )
    {
        tail->next = n1;
    }
    else
    {
        tail->next = n2;
    }
    struct ListNode* tmp = newnode;
    newnode = tmp->next;
    free(tmp);
    return newnode;

}

由于我们创建了头节点,因此不用去单独考虑链表有空的情况。我们创建tail指针保存每次尾插后的地址,方便我们下一次尾插。切记最后一定要释放开辟的头结点,避免照成内存泄漏。

值得注意的是,我们的连接剩余元素不需要像数组那样一个元素一个元素的连接,只需要一步连接即可。

3.链表分割

题目链接:链表分割_牛客题霸_牛客网 (nowcoder.com)

题目描述:现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。

什么意思呢?简单来说就是将一个链表分成两部分,一部分小于x一部分x。例如链表A:1,3,6,5,2,8。给定分割值x为4,那么链表就被分割成1,3,2和6,5,8两个部分,最终链表将变为1,3,2,6,5,8。但是相对顺序并没有改变,6本来就在5的前面,分割完还是在前面。只是将小于4的放在链表前面,大于等于4的放在了链表后面。

那么这个题怎么做呢?其实题目就给了我们提示,题目告知我们更改后链表前面是小于x的,后面是大于等于x的。那么我们就可以将链表分成两部分进行操作,遍历链表,将链表中小于x的结点尾插一个新链表,大于等于x的结点尾插一个新链表。

在cur指向NULL之后循环停止,最终将得到两个新链表:

1,3,2和6,5,8。

然后我们将head1的末尾与head2的开头相连接,则得到我们重新排序的链表。

这样就结束了吗?

我们看下面这种情况,当链表元素为1,3,8,5,6,2 的时候,通过我们上述的方法将会变成两个链表:1,3,2和8,5,6。然后将其链接到一起,结果却跑不过去。

为什么呢?

因为我们分割完后,head2中最后一个元素为6,6所在结点指向2所在的结点,无意之中链表形成了一个环。

所以我们要注意:在循环停止后,先对head2的尾部进行处理,再连接到一起,形成一整个链表。

代码如下:

struct ListNode* partition(struct ListNode* pHead, int x)
{
    // 判断是否为空链表
    if ( pHead == NULL )
    {
        return pHead;
    }
    struct ListNode* head1 = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* head2 = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* tail1 = head1;
    struct ListNode* tail2 = head2;
    head1->next = NULL;
    head2->next = NULL;
    struct ListNode *cur = pHead;
    //尾插链表
    while ( cur != NULL )
    {
        if ( cur->val < x )
        {
            tail1->next = cur;

            tail1 = tail1->next;
        }
        else//依据题意不用单独考虑相等情况
        {
            tail2->next = cur;

            tail2 = tail2->next;
        }
        cur = cur->next;
    }
    tail1->next = head2->next;
    //将大的一端尾部置空,避免形成环状
    tail2->next = NULL;

    struct ListNode* tmp = head1->next;
    free(head1);
    free(head2);
    return tmp;
}

4.链表相交

题目链接:160. 相交链表 - 力扣(LeetCode) (leetcode-cn.com)

题目描述:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 NULL。

我们可以将题目分解为两部分进行理解,首先判断链表是否相交,如何判断链表是否相交呢?

我们知道,如果两个链表相交,那么链表的后面一定是相同的,相交的链表不会是交叉型的,因为next只能指向一个结点。

那怎么判断链表相交呢?很简单,既然相交的链表后面是相同的,那我们直接遍历链表,找到两个链表的最后一个结点,判断结点的地址是否相同,如果相同则说明链表相交,如果不同则不相交。

然后我们在寻找链表的第一个相交的结点。我们最先想到的就是将两个链表的每个结点都进行一一比较,找到第一次相同的地址即可,但是,这种算法的复杂度是有可能达到O(N^2)的,不可取。

下面我们介绍一个O(N)的算法。

首先我们先找到List1和List2的长度len1,len2。然后求得两长度的差值gap,再让长的链表先走差值gap步 ,然后两个链表在一起遍历,逐个地址进行比较,第一次相同的即为第一个交点。

具体过程如下所示:

当然,链表为空的情况我们需要提前判断。

具体代码如下:

struct ListNode* getIntersectionNode(struct ListNode* headA, struct ListNode* headB)
{
    if ( headA == NULL || headB == NULL )
    {
        return NULL;
    }
    struct ListNode* curA = headA;
    struct ListNode* curB = headB;
    int lenA = 1, lenB = 1;
    //找尾结点并计算链表长度
    while ( curA->next != NULL )
    {
        curA = curA->next;
        lenA++;
    }
    while ( curB->next != NULL )
    {
        curB = curB->next;
        lenB++;
    }
    //判断尾结点是否相同,相同则为有交点
    if ( curA != curB )
    {
        return NULL;
    }
    //判断长短
    struct ListNode* shortlen = headA;
    struct ListNode* longlen = headB;
    if ( lenA > lenB )
    {
        shortlen = headB;
        longlen = headA;
    }
    int gap = abs(lenA - lenB);
    //让长的先走差值步
    while ( gap-- )
    {
        longlen = longlen->next;
    }
    //找第一个交点
    while ( longlen != shortlen )
    {
        longlen = longlen->next;
        shortlen = shortlen->next;
    }
    return shortlen;
}

 代码中我们在寻找尾结点的同时计算出了链表长度,由于我们寻找的是尾结点,而并非NULL,所以判定条件为cur->next不为NULL才进入循环内部,但是这样求的长度会比原链表少1。所以我们在定义len的时候直接将其初始化为1。

判断长短我们没有直接使用len的长度来判断,而是新定义两个变量shortlen和longlen进行后续的操作,使代码会变的更加易读。

5.环形链表

题目链接:141. 环形链表 - 力扣(LeetCode) (leetcode-cn.com)

题目描述:给你一个链表的头节点 head ,判断链表中是否有环。

看着题目描述很简单,就是去判断链表有没有环,简单我们只需要记录一下结点就行了,然后遍历链表,当指针再次回到记录的点时就证明链表有环。可以是可以,但是我们要记录的结点就多了,时间复杂度也会很大。

此时还会有人说,简单啊,有环的链表没有NULL,没环的有NULL,判断哪个有NULL不久完了!

 

额,没环的确实遇到NULL循环停下来了,有环的怎么停呢?这步死循环了吗?出不来呀,难不成程序崩溃了就证明他有环?这肯定不行。那有没有其他的办法呢?

有,采用快慢指针。

什么意思呢?我们设定两个指针,慢指针slow每次走一步,快指针fast每次走两步。循环条件就是fast以及fast->next不为NULL,如果遇到NULL则说明链表不为环状,跳出循环,返回false。

如果存在环状,则两个指针最终都会进入环状中,这时就演变为一个追及问题,快指针将会在若干步数后追到慢指针,然后两个指针相遇,返回ture。

具体过程如下:

代码其实很简单就是在快慢指针基础上加上判断即可。

bool hasCycle(struct ListNode *head) {

    struct ListNode *fast=head,*slow=head;
    while(fast&&fast->next)
    {
        fast=fast->next->next;
        slow=slow->next;
        if(fast==slow)
        {
            return true;
        }
    }
    return false;
}

6.链表的回文结构

题目链接:链表的回文结构_牛客题霸_牛客网 (nowcoder.com)

题目描述:

对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。

什么是回文结构,就是判断这个链表是否对称。例如一个链表1,2,3,2,1。链表对称是回文结构,链表,1,2,3,3,2。链表不对称,不是回文结构。

那怎么判断呢?有的人会将链表的val拷贝到数组中,然后利用双指针判断,但是我们需要额外开辟空间来存放这n个元素,所以便超过了空间复杂度O(1)。

所以我们想到了这样一个解法,先找到链表的中间结点,然后反转后半部分链表,最后一一对比。

反转链表以及寻找中间结点的办法我们之前都讲过了,此时直接拿来复用即可。

我们要注意什么时候停止比较?当指向链表开头的指针与指向链表中间的指针遍历都指向NULL的时候就停止遍历比较。后半部分好理解,反转后一定有指向NULL的结点,那前半部分呢?

我们发现,前半部分没有变化,中间结点的前一个结点的next还是指向中间结点的地址,即便原来的中间结点经过逆置已经到最后一个结点了。

那我们就好判断了,因为无论链表是奇数个结点还是偶数个结点都不会影响结果了,奇数个结点的中间结点都会被遍历判断不影响结果。

具体过程:

代码如下:

class PalindromeList
{
public:
    //找链表的中间结点
    struct ListNode* middleNode(struct ListNode* head)
    {

        struct ListNode* slow;
        struct ListNode* flast;
        slow = flast = head;
        while ( flast && flast->next )
        {
            slow = slow->next;
            flast = flast->next->next;
        }
        return slow;
    }
    //反转链表
    struct ListNode* reverseList(struct ListNode* head)
    {

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

        while ( pre != NULL )
        {
            pre->next = tmp;
            tmp = pre;
            pre = cur;
            if ( cur != NULL )
                cur = cur->next;

        }
        return tmp;
    }
    //判断是否为回文结构
    bool chkPalindrome(ListNode* A)
    {

        // write code here
        struct ListNode* head = A;
        struct ListNode* mid = middleNode(head);
        struct ListNode* newhead = reverseList(mid);
        while ( head && newhead )
        {
            if ( head->val != newhead->val )
            {
                return false;
            }
            else
            {
                head = head->next;
                newhead = newhead->next;
            }

        }
        return true;

    }
};

未完待续!!!

评论 37
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bingbing~bang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值