【链表经典OJ-下】

★★★★★个人专栏《C语言》《数据结构-初阶》★★★★★

欢迎各位大佬交流!!!

通过对经典链表OJ题目的练习,不仅能加深对链表的理解,更能体会链表的精妙之处!

目录

9、相交链表

一、判断两个链表是否相交

二、找到相交节点

三、小优化:

10、环形链表

快慢指针:

证明:

延伸:

[11、环形链表 Ⅱ](#11、环形链表 Ⅱ)

思路一:

12、随机链表的复制


9、相交链表

题意是如果两个链表相交,那就返回相交节点;

如果不相交,直接返回NULL即可;

首要任务就是判断两个链表是否相交;怎样判断呢?

如果两个链表相交,会有什么特征?

答案是它们的尾节点相同

一、判断两个链表是否相交

因此我们分别定义两个指针,用来遍历两个链表,均让它们走到尾节点;

此时判断两个指针是否相等,相等即为有环,否则无环;

cpp 复制代码
    struct ListNode* curA = headA;
    struct ListNode* curB = headB;
    //先走到尾节点
    while(curA->next) curA = cur->next;
    while(curB->next) curB = curB->next;
    //判断是否相交
    if(curA != curB) return NULL;

二、找到相交节点

对于图片中这个例子,怎样才能找到相交节点?

最简单的方法就是定义一个指针指向a1,一个指针指向b2;简单说就是两个指针同步!

然后进入循环中,一步一步走;循环结束条件就是两个指针相等;

那么我们是怎样找到这两个指针的指向的呢?

答案是通过两个链表中节点的数量;

假设链表A的节点数量是cntA,链表B的节点数量是cntB;

那么cntB - cntA 就是两个链表之间的差距;

怎样让两个指针同步?仅需让指向链表B的指针往前走差距步即可!

此时让两个指针同时走;当循环结束时,共同指向的就是相交链表

下面我们来尝试写代码:

既然要知道两个链表的节点数量

不妨直接在判断是否有环时统计节点数量;

三、小优化:

判断完存在环之后,需要使用 if-else 来处理不同的情况;

那么不妨使用假设法;

假设长链表是A,短链表是B;然后根据节点数量增加 if 判断,修正结果;

此时直接使用长链表走差距步即可,无需顾虑A还是B;

cpp 复制代码
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
    struct ListNode* curA = headA;
    struct ListNode* curB = headB;
    int cntA = 0,cntB = 0;
    //先走到尾节点
    while(curA->next)
    {
        curA = curA->next;
        cntA++;
    } 
    while(curB->next)
    {
        curB = curB->next;
        cntB++;
    }
    //判断是否相交
    if(curA != curB) return NULL;

    //假设长链表是A
    struct ListNode* longList = headA;
    struct ListNode* shortList = headB;
    if(cntA < cntB)
    {
        //说明B是长链表
        longList = headB;
        shortList = headA;
    }
    int gap = abs(cntA - cntB);
    //长链表先走差距步
    while(gap--)
    {
        longList = longList->next;
    }
    //再同时走
    while(shortList != longList)
    {
        shortList = shortList->next;
        longList = longList->next;
    }
    return shortList;
}

10、环形链表

我觉得如果第一次做这道题,不太好想;

快慢指针:

这题我们可以通过快慢指针的方法来解,依旧是快指针走一步,慢指针走两步;

先给出结论:如果有环,两个指针最终会相遇;

我们先看这个思路能否AC

cpp 复制代码
bool hasCycle(struct ListNode *head)
{
    struct ListNode* slow = head;
    struct ListNode* fast = head;
    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow) return true;
    }
    return false;
}

证明:

下面我们来证明一下:

如果链表没环的话,但 fast 走到为空时循环结束,返回 false;

下图即为有环情况下,两个指针一定会相遇的证明;

延伸:

如果快指针一次走三步、四步呢?n步呢?是否还会相遇?

总结:无论快指针一次走多少步,最终均会相遇!

11、环形链表 Ⅱ

首先我们要判断链表中是否有环;

根据上一题经验,直接定义快慢指针;快指针一次走两步,慢指针一次走一步;

如果链表中有环,那么快慢指针一定会相遇;

相遇后,我们需要思考怎样能够找到进环时的节点?

思路一:

既然两个指针相遇了,不妨定义 meet 指针表示相遇指针;

如果我们把这个链表以 meet 为中间节点一分为二,即将 meet 的下一个节点设为newhead,并将meet的next置为空;

即被分割成了一个以meet为尾节点的链表;一个以newhead为头节点的链表;

而题意要求我们返回入环时的节点,不就相当于两个链表的第一个相交节点吗?

而这正是我们第9题的内容;

下面我们来完善一下代码:

首先判断是否有环

cpp 复制代码
struct ListNode *detectCycle(struct ListNode *head)
{
    //先判断是否有环
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;

        if(fast == slow)
        {
            //有环,相遇了
            struct ListNode* meet = slow;
            struct ListNode* newhead= slow->next;
            meet->next = NULL;
        }
    }    
    return NULL;

}

接着实现相交链表,不妨将其封装为一个函数直接在相遇后执行;

要注意的是,由于先前我们已经判断过有环,因此两个链表一定会相交!

但由于要知道节点的数量,还是需要利用两个指针遍历;

不过最终的判断可以省略掉;

cpp 复制代码
 struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
 {
    //先判断是否相交
    struct ListNode* curA = headA;
    struct ListNode* curB = headB;
    //看尾节点是否相等
    int lenA = 1,lenB = 1;
    while(curA->next)
    {
        curA = curA->next;
        lenA++;
    }
    while(curB->next)
    {
        curB = curB->next;
        lenB++;
    }
    //if(curA != curB) return NULL;

    //假设法
    struct ListNode* longlist = headA;
    struct ListNode* shortlist = headB;
    if(lenA < lenB)
    {
        longlist = headB;
        shortlist = headA;
    }
    //先走差距步
    int gap = abs(lenA - lenB);
    while(gap--)
    {
        longlist = longlist->next;
    }
    while(shortlist != longlist)
    {
        shortlist = shortlist->next;
        longlist = longlist->next;
    }
    return shortlist;
 }

最后来看一下完整代码

cpp 复制代码
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
 {
    //先判断是否相交
    struct ListNode* curA = headA;
    struct ListNode* curB = headB;
    //看尾节点是否相等
    int lenA = 1,lenB = 1;
    while(curA->next)
    {
        curA = curA->next;
        lenA++;
    }
    while(curB->next)
    {
        curB = curB->next;
        lenB++;
    }
    //if(curA != curB) return NULL;

    //假设法
    struct ListNode* longlist = headA;
    struct ListNode* shortlist = headB;
    if(lenA < lenB)
    {
        longlist = headB;
        shortlist = headA;
    }
    //先走差距步
    int gap = abs(lenA - lenB);
    while(gap--)
    {
        longlist = longlist->next;
    }
    while(shortlist != longlist)
    {
        shortlist = shortlist->next;
        longlist = longlist->next;
    }
    return shortlist;
 }
struct ListNode *detectCycle(struct ListNode *head)
{
    //先判断是否有环
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;

        if(fast == slow)
        {
            //有环,相遇了
            struct ListNode* meet = slow;
            struct ListNode* newhead= slow->next;
            meet->next = NULL;

            return getIntersectionNode(head,newhead);
        }
    }    
    return NULL;

}

思路二:

思路一中是在相遇后,将链表一分为二改成相交链表问题;

有没有不需要一分为二的办法呢?

我们先给出结论:当快慢指针相遇后,重新定义两个指针,一个从链表头节点出发,另一个从相遇点出发,两个指针相遇的位置就是入环节点

下面我们来证明一下:

在这种思路下,代码实现将会变得异常简单;

cpp 复制代码
struct ListNode *detectCycle(struct ListNode *head)
{
    //先判断是否有环
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;

        if(fast == slow)
        {
            //有环,相遇了
            struct ListNode* meet = slow;
            while(head != meet)
            {
                head = head->next;
                meet = meet->next;
            }
            return meet;
        }
    }    
    return NULL;

}

12、随机链表的复制

最后一道题可以说是链表的试金石;

不仅思路不好想,而且对链表的操控要求也比较高;

那么我们就直接给出思路:

首先在原链表的每个节点后面都尾插一个复制好的节点;复制好的节点只修改val 和 next;

当尾插完所有复制好的节点后,更改 random 的指向;

最终将新节点连在一起,返回头指针即可;

下面我们来处理一些核心问题:

Q1:为什么要所有节点尾插完之后再更改 random 的指向呢?

因为 random 指向的可能是还未遍历到的节点!

Q2:怎样更改某个节点random的指向?

先找到原节点,接着将 random 指向原链表的random的next !

下面我们来尝试写代码:

一、复制并尾插

首先将将原链表中每个节点都复制一份并尾插在其后面;

cpp 复制代码
    struct Node* cur = head;
    while(cur)
    {
        struct Node* next = cur->next;
        //复制一个新的节点
        struct Node* newnode = (struct Node*)malloc(sizeof(struct Node));
        newnode->val = cur->val;
        newnode->next = cur->next;

        //尾插在cur后面
        newnode->next = cur->next;
        cur->next = newnode;

        cur = next;
    }

二、修改random的指向

cpp 复制代码
    //修改random的指向
    struct Node* pcur = head;
    while(pcur)
    {
        //pcur是原节点,next是复制节点
        struct Node* next = pcur->next;
        if(pcur->random == NULL) next->random = NULL;
        else next->random = pcur->random->next;
        //pcur只需遍历原节点
        pcur = pcur->next->next;
    }

三、串联新链表

cpp 复制代码
struct Node* newhead = NULL;
    struct Node* newtail = NULL;
    //串联复制链表
    cur = head;
    while(cur)
    {
        struct Node* ansnode = cur->next;
        
        if(newtail == NULL)
        {
            //既是头指针也是尾指针
            newhead = newtail = ansnode;
        }
        else
        {
            //尾插在newtail后面
            newtail->next = ansnode;
            newtail = ansnode;
        }
        cur = ansnode->next;

    }
    return newhead;

最终我们将代码整合在一起

cpp 复制代码
struct Node* copyRandomList(struct Node* head)
{
    struct Node* cur = head;
    while(cur)
    {
        struct Node* next = cur->next;
        //复制一个新的节点
        struct Node* newnode = (struct Node*)malloc(sizeof(struct Node));
        newnode->val = cur->val;
        newnode->next = cur->next;

        //尾插在cur后面
        newnode->next = cur->next;
        cur->next = newnode;

        cur = next;
    }
    //修改random的指向
    struct Node* pcur = head;
    while(pcur)
    {
        //pcur是原节点,next是复制节点
        struct Node* next = pcur->next;
        if(pcur->random == NULL) next->random = NULL;
        else next->random = pcur->random->next;
        //pcur只需遍历原节点
        pcur = pcur->next->next;
    }

    struct Node* newhead = NULL;
    struct Node* newtail = NULL;
    //串联复制链表
    cur = head;
    while(cur)
    {
        struct Node* ansnode = cur->next;
        
        if(newtail == NULL)
        {
            //既是头指针也是尾指针
            newhead = newtail = ansnode;
        }
        else
        {
            //尾插在newtail后面
            newtail->next = ansnode;
            newtail = ansnode;
        }
        cur = ansnode->next;

    }
    return newhead;
}

如有不足之处欢迎指出!!!

相关推荐
_日拱一卒2 小时前
LeetCode:随机链表的复制
算法·leetcode·链表
CPUOS20102 小时前
嵌入式C语言高级编程之接口隔离原则
c语言·网络·接口隔离原则
sghuter2 小时前
HTML头部元信息避坑指南
c语言·前端·html·cocoa
a里啊里啊2 小时前
软考-软件评测师:知识点整理(六)——数据结构与算法
数据结构·算法·链表·软考·软件评测师
想带你从多云到转晴2 小时前
06、数据结构与算法---二叉树
java·数据结构·算法
酉鬼女又兒2 小时前
Leetcode 26.删除有序数组中的重复项 双指针巧解有序数组去重:从快慢指针到原地修改算法的精髓
java·数据结构·算法·leetcode·职场和发展·蓝桥杯·排序算法
光电笑映2 小时前
Linux C/C++ 开发工具(下):make/Makefile、进度条小程序与 gdb 调试器
linux·c语言·c++
承渊政道2 小时前
【动态规划算法】(斐波那契数列模型详解)
数据结构·c++·学习·算法·leetcode·macos·动态规划