数据结构--链表进阶面试题

        在链表题目开始之前我们来复习一道数组元素的逆序问题:

        给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

提示:

  • 1 <= nums.length <= 10^5
  • -2^31 <= nums[i] <= 2^31 - 1
  • 0 <= k <= 10^5

 思路1:旋转k次,每一次项右轮转一个元素。时间复杂度O(N^2)。空间复杂度O(1)。

 思路2:创建一个新的数组,现将原数组的后k个元素放置到新数组中,然后再把剩余元素依次放入新数组中,最后把新数组的元素按顺序放回原数组。时间复杂度O(N)。空间复杂度O(N)。

上面两种思路各有优势,也有缺点。我们再来看看思路三。

思路3:先逆序前n-k个元素,再逆序后k个元素,最后整体逆序。这种方法在时间、空间复杂度上都是最优的,但是思路不好想到,这就要大家多多积累,我们来看看代码:

void revese(int* nums,int left ,int right)
{
    while(left <= right)
    {
        int tmp = nums[left];
        nums[left] = nums[right];
        nums[right] = tmp;
        left++;
        right--;
    }

}

void rotate(int* nums, int numsSize, int k) 
{
    k %= numsSize;
    revese(nums,0,numsSize-k-1);
    revese(nums,numsSize-k,numsSize-1);
    revese(nums,0,numsSize-1);
}

例1:返回链表的倒数第k个节点

        实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。给定的 k 保证是有效的。

解:

        这到题的思路和之前的快慢指针相似,我称之为先后指针,我们先创建两个指针(fast、slow),既然是找倒数第k个节点,那我们就先让fast走k个节点,然后让两个指针同时向后走,当fast指针走到尾节点时,slow节点就是倒数第k个节点。代码如下:

typedef struct ListNode ListNode;
int kthToLast(struct ListNode* head, int k)
{
    ListNode* fast = head;
    ListNode* slow = head;
    while(k--)
    {
        fast = fast->next;
    }
    while(fast)
    {
        fast = fast->next;
        slow = slow->next;
    }
    return slow->val;
}

 例2:链表的回文结构

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

解:

        这道题有些难度,不过我们使用之前学的解法思路可以很轻松的过这道题。既然是回文结构,那我们就先找到他的中间节点,然后我们在逆序后半段链表,然后再比较原头结点和逆序后的头结点的值,如果不相等就返回false,反之继续向后遍历,直到其中有一个指针指向为空,返回true。

 

bool chkPalindrome(ListNode* A) 
    {
        //寻找中间节点
        struct ListNode * fast = A;
        struct ListNode * slow = A;
        while(fast && fast->next)
        {
            slow = slow->next;
            fast = fast->next->next;
        }
        //翻转后半链表:头插法
        struct ListNode* pcur = slow;
        struct ListNode* newhead = NULL;
        while(pcur)
        {
            struct ListNode* next = pcur->next;
            pcur->next = newhead;
            newhead = pcur;
            pcur = next;
        }
        //向后遍历
        while(A && newhead)
        {
            if(A->val != newhead->val)
            {
                return false;
            }
             A = A->next;
             newhead = newhead->next;
        }
        return true;
    }

例3:相交链表

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

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

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

提示:

  • listA 中节点数目为 m
  • listB 中节点数目为 n
  • 1 <= m, n <= 3 * 10^4
  • 1 <= Node.val <= 10^5
  • 0 <= skipA <= m
  • 0 <= skipB <= n
  • 如果 listA 和 listB 没有交点,intersectVal 为 0
  • 如果 listA 和 listB 有交点,intersectVal == listA[skipA] == listB[skipB]

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

自定义评测:

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

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

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

 解:

        本道题在初见可能会没有思路,因为我们没有办法确定他们是否相交了或者走到哪个节点了。但是仔细思考,我们还是有办法去处理这些情况的。

        首先,我们要确定两个链表是否真的相交了,那我们可以先遍历两个数组,如果它们的尾节点为同一个,说明它们是相交的,反之即为不想交,返回空指针。

        如果是相交的话,我们就要重新去寻找它们的第一个相交节点,我们可以在第一次遍历判断他们是否相交时,顺便计算一下两个链表的节点个数,这样不需要重新再去计算,降低了时间复杂度。如果A链表长,那就让A先走len(A) - len(B)和节点,然后让两个节点从所在位置继续向后遍历,当两个节点相等时,就找到了第一个相交节点,返回该节点即可。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{
        ListNode* cur1 = headA;
        ListNode* cur2 = headB;
        int len1 = 1;
        int len2 = 1;
        while(cur1->next)
        {
            cur1 = cur1->next;
            len1++;
        }

        while(cur2->next)
        {
            cur2 = cur2->next;
            len2++;
        }
        
        if(cur1 != cur2)
        {
            return NULL;
        }
        else
        {
            ListNode* pcur1 = headA;
            ListNode* pcur2 = headB;
            if(len1 >= len2)
            {
                int num = len1 - len2;
                while(num--)
                {
                    pcur1 = pcur1->next;
                }
            }
            else
            {
                int num = len2 - len1;
                while(num--)
                {
                    pcur2 = pcur2->next;
                }
            }

            while(pcur1 != pcur2)
            {
                pcur1 = pcur1->next;
                pcur2 = pcur2->next;
            }
            return pcur1;
        }
}

 例4:随机链表的复制

        给你一个长度为 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 作为传入参数。

 解:

        本题的难点在于节点的随机指针random难以确定指向的方位。所以需要我们另辟蹊径,我们可以直接在原链表的每个节点之后复制一个相同的节点,这样我们就可以直到该新节点的随机节点的位置在原节点的随机节点的下一个位置。怎么样,是不是很巧妙。

        首先我们还是先判断原链表是否为空,为空就返回NULL。反之,我们就要开始插入复制节点了,各位要注意,新复制的原点的随机节点在第一遍遍历的时候都赋值为NULL,因为,第一遍还没有创建好所有的节点,你可能找不到相应的随机节点。第二遍遍历就是单纯把所有的random都指向他们相应的节点。第三遍遍历为的是还原原链表并分离新链表。代码如下:

/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *next;
 *     struct Node *random;
 * };
 */
typedef struct Node Node;
Node* copyRandomList(Node* head) 
{
     if (head == NULL) 
            return NULL;

    // 第一遍遍历:在每个原节点后面插入复制节点
    struct Node *cur = head;
    while (cur != NULL) 
    {
        struct Node *copyNode = (struct Node *)malloc(sizeof(struct Node));
        //固定位置插入
        copyNode->val = cur->val;
        copyNode->next = cur->next;
        //随机指针先置为空,后续节点完全创建好之后再进行复制
        copyNode->random = NULL;
        cur->next = copyNode;
        cur = copyNode->next;
    }

    // 第二遍遍历:设置复制节点的 random 指针
    cur = head;
    while (cur != NULL) 
    {
        //NULL已经赋值过了,所以无需再次复制
        if (cur->random != NULL) 
        {
            cur->next->random = cur->random->next;
        }
        cur = cur->next->next;
    }

    // 第三遍遍历:将复制节点从原链表中分离出来
    //分离新链表的同时,还原原链表
    cur = head;
    struct Node *newHead = head->next;
    struct Node *newCur = newHead;
    while (cur != NULL)     
    {
        cur->next = cur->next->next;
        if (newCur->next != NULL) 
        {
            newCur->next = newCur->next->next;
        }
        cur = cur->next;
        newCur = newCur->next;
    }

    return newHead;
}

例5:环形链表

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

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

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

提示:

  • 链表中节点的数目范围是 [0, 10^4]
  • -10^5 <= Node.val <= 10^5
  • pos 为 -1 或者链表中的一个 有效索引 。

解:

         这道题不是很难,但是用到了很多数理逻辑思维。首先我们要确定链表中是否存在环,我们先假设有环,那如何确定环的存在也是一个问题。我们来看一张图:

        我们依旧使用快慢指针来解题,不了解的可以看前两篇文章。slow走一步,fast走两步,直到slow进入环中,fast开始追击slow,因为fast每次都比slow多走一步,那是不是说明fast早晚都可以追上slow呀。既然能追上,是不是就说明有环存在,这道题就解了。如果没有环,两个指针就不可能相遇,我们在里面判断一下就可以了。代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
bool hasCycle(struct ListNode *head) 
{
    //判断是否为空链表
    if(head == NULL)
    {
        return false;
    }
    ListNode* fast = head;
    ListNode* slow = head;
    //循环中如果fast和slow相等说明成环,不成环在循环结束后会返回fasle
    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)
        {
            return true;
        }
    }
    return false;
}

例5拓展:

        我们要讨论的远远不止这些,如果slow固定为走一步,但是fast不是走两步,而是走三步、四步、五步……呢?下面我们来一个一个讨论:

        我们假设进环前的节点个数为L,环的节点个数为C,当slow进环时,fast与slow相距N。

        我们先来看fast走三步的情况, 每移动一次指针,fast和slow的距离就会减少2,如果N为偶数,过程为N、N-2、N-4、……、2、0,最后两个指针会相遇;如果N为奇数,过程为N、N-2、N-4、……、3、1、-1,我们会发现两个指针错过了,并且越来越远,我们就需要重新计算它们的距离。错过之后它们的距离变成了C-1,当C-1为偶数时,它们最终会相遇对吧;但是当C-1为奇数时,它们就永远不会相遇了。那这里是不是就无解了呢,我们继续往下去探讨:

        我们假设slow走的路程是L,那么fast走的就是L + x*C + C-N(x为正整数)。我们还知道fast走的路程是slow的三倍,那么我们就可以写出关系式:3L = L + x*C + C-N。化简之后我们可以得到2L = (x+1)*C -N。由于C-1是奇数,仔细看你会发现:偶数 = 任意整数 * 偶数 - 奇数。这是一个恒不成立的等式,也就是说这种情况不存在。那我们是不是就可以证明,fast必定可以追上slow。

        看完三步后我们再来看看四步的,每移动一次指针,fast和slow的距离就会减少3,这时我们要分三种情况去看:N%3==0时,过程为N、N-3、N-6、……、3、0,可以相遇;N%3==1时,过程N、N-3、N-6、……、4、1、C-2;当N%3==2时,过程为N、N-3、N-6、……、5、2、C-1。后面的过程和三步的相似,各位感兴趣可以自己去算一下,这里不过多着笔。

        从这两种情况中我们要学的是数理逻辑思维,在正式工作时很少用到,但是在面试时可能会被问,大家注意一下就可以了。

例6:环形链表二

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

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

不允许修改 链表。

提示:

  • 链表中节点的数目范围在范围 [0, 10^4] 内
  • -10^5 <= Node.val <= 10^5
  • pos 的值为 -1 或者链表中的一个有效索引

解:

        这道题用上面的思路可以很快就解决, 我们使用两步fast的快慢指针来解决。

 

        首先,我们让fast和slow走到相遇节点,记为meet。我们来算一下,假设slow走了L+N ,fast走了L + x*C + N。由关系可得,2(L+N) = L + x*C + N。化简得L = (x-1)*C + C-N。其中C是环的节点数,C-N就是meet到入环节点的距离,因为(x-1)是可以等于0的,我们惊喜地发现,L = C-N。也就是说在头指针和meet一起向后走,会同时到达我们要的节点。代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
ListNode *detectCycle(struct ListNode *head) 
{
        ListNode* fast = head;
        ListNode* slow = head;
        ListNode* meet = NULL;
        //相遇
        while(fast && fast->next)
        {
            slow =  slow->next;
            fast = fast->next->next;
            if(fast == slow)
            {
                meet = fast;
                break;
            }
        }
        //判断链表是否无环
        if(fast == NULL || fast->next == NULL)
        {
            return NULL;
        }
        //返回入环的第一个节点
        ListNode* pcur = head;
        while(pcur != meet)
        {
            pcur = pcur->next;
            meet = meet->next;
        }
        return meet;
    
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/605895.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AlphaFold3(AF3)简单介绍:预测各种生物分子结构和它们之间相互作用的深度学习模型

参考: 文章地址: https://www.nature.com/articles/s41586-024-07487-w https://blog.google/technology/ai/google-deepmind-isomorphic-alphafold-3-ai-model/ AlphaFold3体验官网: https://golgi.sandbox.google.com/ 《Accurate structure prediction of biomolecula…

分享一个php常驻内存多进程任务的扩展

前言 最近在摸鱼的时候发现一个PHP常驻内存多进程任务扩展包&#xff1a;EasyTask: PHP常驻内存多进程任务管理器&#xff0c;支持定时任务(PHP resident memory multi-process task manager, supports timing tasks) (gitee.com)&#xff0c;支持php使用多线程处理任务。之前…

文心一言 VS 讯飞星火 VS chatgpt (252)-- 算法导论18.2 5题

五、因为叶结点无需指向孩子结点的指针&#xff0c;那么对同样大小的磁盘页面&#xff0c;可选用一个与内部结点不同的(更大的) t 值。请说明如何修改 B 树的创建和插人过程来处理这个变化。如果要写代码&#xff0c;请用go语言。 文心一言&#xff1a; 当我们在B树&#xff…

Kafk设计篇01(设计动机+持久化)

背景 本篇文章基于最新版本&#xff1a;kafka 3.7&#xff0c;其他版本的设计&#xff0c;请参考官网&#xff1a; https://kafka.apache.org/documentation/设计动机 任何组件都有它存在的必要&#xff0c;必然是要解决某一类问题的。我们来看看kafka设计的初衷如何。 kaf…

Python---Numpy万字总结(1)

NumPy的应用&#xff08;1&#xff09; Numpy 是一个开源的 Python 科学计算库&#xff0c;用于快速处理任意维度的数组。Numpy 支持常见的数组和矩阵操作&#xff0c;对于同样的数值计算任务&#xff0c;使用 NumPy 代码简洁&#xff0c;在性能上也远远优于原生 Python&#…

温度表程序里的公式推算

今天要改个温度表的程序&#xff0c;但是好几年没搞过了。所以程序里面的各种数字怎么算出来的都忘记了。花了半天才想起来&#xff0c;所以记录在这里&#xff0c;下次再忘记了就来翻一下。。 下次应该看到这个能想起来的把。

【论文笔记】KAN: Kolmogorov-Arnold Networks 全新神经网络架构KAN,MLP的潜在替代者

KAN: Kolmogorov-Arnold Networks code&#xff1a;https://github.com/KindXiaoming/pykan Background ​ 多层感知机&#xff08;MLP&#xff09;是机器学习中拟合非线性函数的默认模型&#xff0c;在众多深度学习模型中被广泛的应用。但MLP存在很多明显的缺点&#xff1a;…

nginx--系统参数优化telenct

系统参数 在生产环境中&#xff0c;根据自己的需求在/etc/sysctl.conf来更改内核参数 net.ipv4.ip_nonlocal_bind 1 允许非本地IP地址socket监听 net.ipv4.ip_forward 1 开启IPv4转发 net.ipv4.tcp_timestamps 0 是否开启数据包时间戳 net.ipv4.tcp_tw_reuse 0 端⼝口复⽤…

ctfshow之_萌新web9至web10

一、访问在线靶场ctfshow 1、web9 如下图所示&#xff0c;进入_萌新赛的web9问题&#xff0c;题目提醒flag在config.php中&#xff1a; 如上图所示&#xff0c;可以get传参&#xff0c;且传入的参数需要正则匹配system、exec、highlight&#xff0c;且不区分大小写&#xff0…

分类任务的基础学习

1.什么是分类&#xff1f; 2.局限性&#xff1a; 当样本量逐渐变大的时候&#xff0c;准确率会下降——>因为线性回归曲线距离我们的原点越远&#xff0c;预测就会开始不准确&#xff0c;因为 x前面的倍数就会越来越小&#xff0c;这就导致了样本量变大&#xff0c;但是那些…

安卓开发--环境配置

本次项目选择使用 Andrio Studio 进行开发。虽然这款软件版本更新也很快。不过开发一款APP的技术流程是大差不差的。我几年前的安卓笔记放到现在还是能用。 现在CSDN网上写一个笔记留作以后参考&#xff0c;开始吧&#xff01;&#xff01;&#xff01; 1 安装 Andrio Studio …

Jmeter性能测试(五)

一、Jmeter参数化常用方式 1、CSV 数据文件设置 2、查询数据库(JDBC Connection Configuration) 二、CSV 数据文件设置 1、准备一个txt文件(不需要写表头&#xff0c;直接写你要用的数据就行了&#xff0c;多个字段用英文逗号隔开) 2、添加一个CSV 数据文件设置(放全局最上…

Vue从入门到实战Day02

一、指令补充 1. 指令修饰符 通过 “.”指明一些指令后缀&#xff0c;不同后缀封装了不同的处理操作 -> 简化代码 键盘按键修饰符 如&#xff1a;keyup.enter -> 键盘回车监听 常用按键修饰符别名 别名修饰符键值修饰符对应按键.delete.8/.46回格 / 删除.tab.9制表.e…

01-单片机商业项目编程,从零搭建低功耗系统设计

一、引言 这是关于《单片机商业编程之从零搭建低功耗系统》的第一篇章&#xff0c;个人善忘&#xff0c;平常项目设计当中的一些思路&#xff0c;以前年轻的时候习惯性的录制成视频&#xff0c;也算是当作是自己的笔记&#xff0c;无奈现在喉咙实在扛不住&#xff0c;因此先尝试…

Linux下的I2C通信

I2C通信: 一.硬件初识: IIC(inter-intergrated-Circu):内部集成总线 四线通讯:SCL,SDA,GND,VCC,串行,半双工 I2C 总线是同步,串行,半双工通信总线。 I2C 总线由时钟线 SDA 和 SCL 两根信号线构成。并且都有上拉电阻。确保总线空闲状态为高电平。 I2C 总线支持多…

四川古力未来科技抖音小店:安全便捷购物新体验

在这个数字化快速发展的时代&#xff0c;网络购物已经成为人们生活中不可或缺的一部分。四川古力未来科技抖音小店以其高度的安全性&#xff0c;为广大消费者提供了一个值得信赖的购物平台。在这里&#xff0c;我们可以享受到安全便捷的购物体验&#xff0c;畅游科技的海洋。 一…

java回调机制

目录 一、简介二、示例2.1 同步回调2.2 异步回调2.3 二者区别 三、应用场景 一、简介 在Java中&#xff0c;回调是一种常见的编程模式&#xff0c;它允许一个对象将某个方法作为参数传递给另一个对象&#xff0c;以便在适当的时候调用该方法。 以类A调用类B方法为例: 在类A中…

CTF-reverse,逆向分析,对“左移4或右移4,即(x<<4) | (x >>4)的加密探讨

博主在刷题过程中遇上这样一个有意思的加密&#xff08;如下图&#xff09;&#xff0c;苦苦思索其逆向运算&#xff0c;被硬控了很久&#xff0c;也没搜到什么资料来解释这个问题&#xff08;也许是太简单&#xff1f;&#xff1f;蒟蒻博主怀疑人生……&#xff09; 经过博主不…

2024最新版JavaScript逆向爬虫教程-------基础篇之无限debugger的原理与绕过

目录 一、无限debugger的原理与绕过1.1 案例介绍1.2 实现原理1.3 绕过debugger方法1.3.1 禁用所有断点1.3.2 禁用局部断点1.3.3 替换文件1.3.4 函数置空与hook 二、补充2.1 改写JavaScript文件2.2 浏览器开发者工具中出现的VM开头的JS文件是什么&#xff1f; 一、无限debugger的…

正点原子Linux学习笔记(七)在 LCD 上显示 png 图片

在 LCD 上显示 png 图片 21.1 PNG 简介21.2 libpng 简介21.3 zlib 移植下载源码包编译源码安装目录下的文件夹介绍移植到开发板 21.4 libpng 移植下载源码包编译源码安装目录下的文件夹介绍移植到开发板 21.5 libpng 使用说明libpng 的数据结构创建和初始化 png_struct 对象创建…
最新文章