链表相关练习题以及题解

前言

        学习完了链表结构,不妨多加练习熟系这种数据结构。所以本篇论文列举出了一些和链表相关的练习题,并描述解题思路,相信对能够令读者对链表这种结构的掌握更加得心应手。

一、链表练习题

1. 输入一个链表,输出该链表中倒数第k个结点。 OJ链接

1.1. 题目描述

        实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。

注意:本题相对原题稍作改动

示例:

输入: 1->2->3->4->5 和 k = 2
输出: 4

说明:给定的 k 保证是有效的。

1.2. 题解思路

        从题目的描述中可以得知,我们需要返回倒数第k个节点,如果可以从后向前遍历,那么这个题目会简单不少,但是链表为单向无头不带环链表,结构如下所示:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

        因此我们需要用其他的方法来解决这个问题。

        在做这个题的时候如果做过找链表中间节点的题的话,我们可以借用这道题的思路做一个快慢指针出来,根据示例的图示如下:

图1-1快慢指针法

        思路如图1-1所示,先让快指针走k步,然后让快指针和慢指针一起走。当快指针走到NULL节点的时候,慢指针正好到达指定位置——倒数第k个节点。

1.3. 解题代码

        根据1.2.的思路,解题代码如下:

int kthToLast(struct ListNode* head, int k){
    struct ListNode* slow = head;
    struct ListNode* quick = head;
    //快指针先走k步
    while(k--)
    {
        quick = quick->next;
    }
    //快慢指针一起向前走,直到快指针到NULL节点
    while(quick)
    {
        quick = quick->next;
        slow = slow->next;
    }
    //返回慢指针
    return slow->val;
}

        代码提交结果如下:

        因此解题思路和代码都没有问题。

2. 链表的回文结构。OJ链接

2.1. 题目描述

描述:

        对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。

        给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。

测试样例:


  

2.2. 题解思路

        和第一题一样,这一题也是单向链表,那么我们就不能从后往前访问了。那么为了达到能够同时向前向后访问,有两种思路:(1)把链表后半段逆至。(2)把链表数据拿出来放到能够反向找数据的结构中,比如数组。

2.2.1. 把链表后半段逆至

        这个思路就是先用块慢指针,慢指针一次走一步,快指针一次走两步。找到中间节点,然后将后半段逆至,最后同时从前从后遍历并依此比较。全部相等就返回true,有一个不同就返回false。图解如下:

图2-2 找到中间节点

        如图1-2,能够找到中间节点,随后逆至后半段节点,用三个指针,图解如下:

图2-2 逆至节点

        如图1-3所示,逆至的后半段节点,剩下的就是同时向前向后遍历,比较节点的值了,如图1-3所示:

图2-3  同时遍历

2.2.2.  把链表数据拿出来放到能够反向找数据的结构中,比如数组。

        思路其实从题目中也能知道,首先创造一个数组,当然也可以用之前顺序表的数据结构。如果用的是数组还需要定义一个变量记录有多少个数字,每录入一个数字就++。图解如下:

图1-4 将链表中数据存入数组

        然后前后同时比较就行了。图解如下:

图1-5  两个节点比较并分别向对侧移动

2.3. 解题代码

        本题解题代码采用方法1,将三个步骤分别分装为三个函数,代码如下:

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/


class PalindromeList {
public:
    //找到中间节点
    ListNode* GetMid(ListNode* A)
    {
        ListNode* slow = A;
        ListNode* quick = A;
        //当快指针到末尾或者下一个节点是空就结束循环
        while(quick && quick->next)
        {
            slow = slow->next;
            quick = quick->next->next;
        }
        //返回中间节点
        return slow;
    }

    //翻转后半部分链表
    ListNode* GetReturnMid(ListNode* A)
    {
        ListNode* cur = nullptr;
        ListNode* prev = nullptr;
        ListNode* next = A;
        //三个指针,next到空就结束循环
        while(next)
        {
            prev = cur;
            cur = next;
            next = next->next;
            cur->next = prev;
        }
        //返回末尾的节点
        return cur;
    }

    bool chkPalindrome(ListNode* A) {
        //接收中间节点
        ListNode* mid = GetMid(A);
        //找到末尾节点
        ListNode* start2 = GetReturnMid(mid);
        //头结点
        ListNode* start1 = A;
        //当末尾节点到空就结束循环,后半段一定短于前半段
        while(start2)
        {
            if(start2->val != start1->val)
            {
                //比较有不同的数,链表不是回文
                return false;
            }
            start2 = start2->next;
            start1 = start1->next;
        }
        //全部相同,链表是回文
        return true;
    }
};

        最后提交代码结果无误,截图如下:

3. 相交链表

3.1. 题目描述

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

        图示两个链表在节点 c1 开始相交

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构 。

自定义评测:

评测系统 的输入如下(你设计的程序 不适用 此输入):

  • intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0
  • listA - 第一个链表
  • listB - 第二个链表
  • skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数
  • skipB - 在 listB 中(从头节点开始)跳到交叉节点的节点数

评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA 和 headB 传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案 。

3.2. 题解思路

        我们需要确认链表是否有交点那么但是却只能遍历链表,没有到交接处的长度也可能互不相等,那么就可以有以下多种状况:

图3-1 状况1

图3-2 状况2

图3-3 状况3

        有可能A长,有可能B长,也有可能一样长,最后也会不相交。所以没办法确认需要走的长度。所以我们就找相交链表的共通点,那就是相交链表的最后节点地址一定是相同的,所以可以分别遍历两个链表开头,如果最后节点地址相同就能证明两个链表相交。

        已经确认相交了,那么怎么找到链接的节点呢?这个时候就可以计数,看链表的长度如何,最后相减,然后让长的一方先走多出来的步数,这样就能够确认两个链表同时走到链表链接处了,也可以随时比较。最后返回链接的节点即可。

3.2. 解题代码

        分析如上所示,分装了三个函数,代码如下:

// 判断链表是否相交
bool GetListSize(int* Asize, int* Bsize, struct ListNode *headA, struct ListNode *headB)
{
    //遍历链表找到最后节点
    while(headA->next)
    {
        ++*Asize;
        headA = headA->next;
    }
    while(headB->next)
    {
        ++*Bsize;
        headB = headB->next;
    }
    //判断最后节点是否相同
    if(headA != headB)
    {
        return false;
    }
    else
    {
        return true;
    }
}
//计算长链表事先走的步数
int Abs(int a, int b)
{
    int dif = a - b;
    if(dif >= 0)
    {
        return dif;
    }
    else
    {
        return -dif;
    }
}

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    int listsizeA = 0;
    int listsizeB = 0;
    //进入函数,判断链表是否相交,相交返回true
    if(!GetListSize(&listsizeA, &listsizeB, headA, headB))
    {
        return NULL;
    }
    //假定A链表是短链表
    struct ListNode* shortlist = headA;
    struct ListNode* longlist = headB;
    //如果实际大小相反,则转换B链表为短链表
    if(listsizeA > listsizeB)
    {
        shortlist = headB;
        longlist = headA;
    }
    //计算长链表事先走的步数
    int dif = Abs(listsizeA, listsizeB);
    //移动指向长链表的节点
    while(dif--)
    {
        longlist = longlist->next;
    }
    //同时移动长短链表的节点,找到相同的节点
    while(longlist != shortlist)
    {
        longlist = longlist->next;
        shortlist = shortlist->next;
    }
    //返回节点
    return shortlist;
}

        代码执行结果如下:

        结果无误。

4. 拷贝链表, 返回链表的深度拷贝。OJ链接

4.1. 题目描述

        给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

        构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 

例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

  • val:一个表示 Node.val 的整数。
  • random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为  null 。

你的代码  接受原链表的头节点 head 作为传入参数。

示例 1:

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

示例 2:

输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]

示例 3:

输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]

4.2. 题解思路

4.2.1.  思路1——反复遍历找到随机指针的位置

        深度拷贝那么就不能在原来链表上下太多功夫,我们可以直接先复制出来一个链表,先不管随机指针指向的位置,先深度拷贝链表。然后再来解决随机指针的问题。

        因为原来的链表和直接深度拷贝的链表没有关系,所以随机指针的找法就是遍历。如何遍历呢?首先判断随机指针是否为空,为空就指向NULL,反之就定义两个指针指向两个链表的头,由原链表的节点来控制拷贝链表节点的位置,一起向后遍历,找到了就将拷贝链表的随机指针链接上指定位置。

图4-1 链表传统深拷贝

        这种方法的缺陷就是时间复杂度为O(N^2),链表节点假设有个百万级超时是妥妥的。

4.2.2. 思路2——让拷贝链表和原链表建立联系

        这种思路就是先把拷贝节点插入到原链表对应节点之后,如图所示:

图4-2 链表分别拷贝节点并链接

        这样拷贝的好处就是能够更快的找到随机指针,原链表的节点一定是隔一个在中间,所以先分别找到原节点对应的拷贝节点,通过找到原节点的随机指针,那么下一个就是该拷贝节点所指向的位置。

图4-3 拷贝链表找到随机指针

        当然还需要先判断随机指针是否是空指针,不然去找空指针的下一个位置是会报错的。最后就将链表恢复原样即可, 时间复杂度是O(N).

4.3. 解题代码

        这里代码采用思路2的思路,代码如下:

// 建立一个新节点
struct Node* BuyNode(int x)
{
    struct Node* newnode = (struct Node*)malloc(sizeof(struct Node));
    if(newnode == NULL)
    {
        perror("malloc fail");
        return NULL;
    }

    newnode->val = x;
    newnode->next = newnode->random = NULL;
    return newnode;
}

struct Node* copyRandomList(struct Node* head) {
    //为空直接返回
    if(head == NULL)
    {
        return NULL;
    }
    //在原节点的下一刻创造拷贝节点
    struct Node* cur = head;
	while(cur)
    {
        struct Node* newnode = BuyNode(cur->val);//创立新节点的函数
        newnode->next = cur->next;
        cur->next = newnode;
        cur = newnode->next;
    }
    //重置cur指针
    cur = head;
    //让curnext指向拷贝的节点
    struct Node* curnext = head->next;
    // 找到拷贝节点随机指针指向的位置
    while(cur)
    {
        //随机指针为空就直接赋为NULL
        if(cur->random == NULL)
        {
            curnext->random = NULL;            
        }
        else//反之就赋值为原链表随机指针的下一个
        {
            curnext->random = cur->random->next;
        }
        //原指针向下遍历
        cur = curnext->next;
        //如果原指针已经为空了就没有下一个拷贝指针了
        if(cur)
        {
            curnext = cur->next;
        }
    }
    //重置节点
    curnext = head->next;
    //这个节点用来记录拷贝节点的头结点
    struct Node* list = curnext;
    //将原链表和拷贝链表分别链接
    for(cur = head; cur; )
    {
        //链接原链表
        cur->next = curnext->next;
        //移动指针到原链表下一个节点
        cur = curnext->next;
        if(cur)
        {
            //如果原链表之后还有节点,就链接拷贝链表
            curnext->next = cur->next;
            //移动到下一个位置
            curnext = cur->next;
        }
        else
        {
            //如果原链表指针到末尾了,直接将拷贝链表最后节点的下一位置空
            curnext->next = NULL;
        }
    }
    //返回拷贝链表头结点
    return list;
}

        代码执行结果如下,结果无误:

二. 带环链表练习题

5. 给定一个链表,判断链表中是否有环。 OJ链接

5.1. 题目描述

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

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

5.2. 题解思路

        首先判断没有环很简单,如果节点遍历到最后能够为空,就说明链表节点中没有环。相反,如果有环的话就遍历不到空节点。在有环的时候也伴随着一个问题,那就是如果找到空节点才结束遍历,那么带环的链表就结束不了循环。所以本体的难点在于如何结束带环链表的遍历。

        一个指针解决不了,就两个指针。采用快慢指针的思路,快指针一次走两步,慢指针一次走1步,这样根据1步的速度差,进如环内快指针总会追上慢指针。

图5-1 带环节点遍历举例

        还有一些额外的问题。

5.2.1. 快指针会不会碰不到慢指针

        从结论来说,在带环链表中如果慢指针走一步,快指针走两步是一定会相遇的。因为快指针比慢指针快一步,假设快指针需要N步追上慢指针,一次追一步根本不会错过。

图5-2 快指针追慢指针1

5.2.1. 步数改变呢?

        如果快指针一次走3步那么也一定会遇到吗?

        答案是不一定,分析如下:

        首先,快指针与慢指针的步差为2。那么实际问题就可以被简化为在一个圆环里,单个指针要到达距离N个位置的节点,但是由于一次走两步,所以当N为奇数的时候会在第一圈的时候错过该位置。第一圈已经到不来指定节点了,那么第2圈以后呢?

        第二圈以后到达指定节点的距离为n*C+N,也就是说如果步差能够将距离整除那么之后就还是能遇到,回到假设的情况。如果C为偶数,偶数乘以一个数加上一个奇数仍然是奇数,那么奇数永远都不会被2整除,这样每次都会错过慢指针。所以如果改变步差是可能会导致快慢指针遇不到的。

5.3. 解题代码

        根据解题思路,代码如下:

bool hasCycle(struct ListNode *head) {
    struct ListNode* quick = head;
    struct ListNode* slow = head;
    //快慢指针遍历所有节点
    while(quick && quick->next)
    {
        quick = quick->next->next;
        slow = slow->next;
        //两指针相遇,说明一定有环
        if(quick == slow)
        {
            return true;
        }
    }
    //遍历完成说明没有环
    return false;
}

        运行结果如下,代码很简捷:

6. 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL OJ链接

6.1. 题目描述

给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

6.2. 题解思路

        这一题相当于是练习题5的进阶,首先需要确认链表带坏,这部分的思路和代码直接沿用。问题就是怎么确认入环节点了。

图6-1 图示

        首先快指针比慢指针每次移动都快一步,假设移动了 t 次。

        那么快指针的移动总步数为 2 * t 步, 慢指针移动步数为 t 步。

        因为快指针会追上慢指针,所以快指针和慢指针之间的步差应该为 n * C ,其中 n 表示转的圈数。为什么步差为 n * C 呢?因为,快指针会比慢指针多绕 n 圈最后追上慢指针。

        也就是说 2*t - t = n * C ->  t = n * C。

        那么慢指针的步数可视化之后会如下图所示:

图6-2

        转换一下思路, n * C 还能等于什么。

图6-3

        如图6-3所示,n * C 还相当于转了n圈。

        好巧不巧,如果这个圈数再加上一个 L 那么 n * C + L就相当于从链表头结点开始进入环,然后转了 n 圈回到了环开头。

        回到快慢指针相遇得到情况,我们只需要从相遇节点开始再走上 L 步就能到入环结点。

6.3. 解题代码

        根据解题思路代码如下:

struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode* quick = head;
    struct ListNode* slow = head;
    //快慢指针遍历所有节点
    while(quick && quick->next)
    {
        quick = quick->next->next;
        slow = slow->next;
        //两指针相遇,说明一定有环
        if(quick == slow)
        {
            //有环找第一个入环节点
            struct ListNode* meet = slow;
            slow = head;
            //相遇节点走上链表未成环部分长度刚好到入环第一个节点
            while(1)
            {
                if(slow == meet)
                {
                    //遇到了返回节点
                    return meet;
                }
                else
                {
                    //没遇到就继续遍历
                    slow = slow->next;
                    meet = meet->next;
                }
            }
        }
    }
    //遍历完成说明没有环
    return NULL;
}

        根据解题思路,在循环while(1)中一定能够遇到。所以这么写也可以。

        那么结果如下:

作者结语

        多的就不说了,链表是真的好玩好吧。

相关推荐

  1. leetcode相关题目

    2024-05-01 21:24:05       61 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-05-01 21:24:05       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-05-01 21:24:05       100 阅读
  3. 在Django里面运行非项目文件

    2024-05-01 21:24:05       82 阅读
  4. Python语言-面向对象

    2024-05-01 21:24:05       91 阅读

热门阅读

  1. Ansible playbook之变量引用

    2024-05-01 21:24:05       22 阅读
  2. 聊聊服务器散热方案的演进

    2024-05-01 21:24:05       29 阅读
  3. 第15届蓝桥杯-蒟蒻の反思与总结

    2024-05-01 21:24:05       71 阅读
  4. Python实现的人脸识别系统

    2024-05-01 21:24:05       34 阅读
  5. pnpm设置全局存储路径

    2024-05-01 21:24:05       34 阅读
  6. 第三题——LCP 07. 传递信息

    2024-05-01 21:24:05       106 阅读
  7. 抽象类和接口的区别你知道吗

    2024-05-01 21:24:05       35 阅读
  8. web3以太坊开发,前后端交互中涉及到的合约

    2024-05-01 21:24:05       158 阅读
  9. C++中常见容器总结Array-Vector-List-Queue-Stack-Map-Set

    2024-05-01 21:24:05       32 阅读
  10. Linux 文件管理命令 tr col colrm fold iconv

    2024-05-01 21:24:05       30 阅读