【数据结构】7道经典链表面试题

news2024/10/17 15:59:27

目录

1.返回倒数第K个节点【链接】

代码实现

2.链表的回文结构【链接】

代码实现 

3.相交链表【链接】

代码实现

4.判断链表中是否有环【链接】

代码实现

常见问题解析 

5.寻找环的入口点【链接】

代码实现1

代码实现2

6.随机链表的复制【链接】

代码实现

7.顺序表和链表的区别


1.返回倒数第K个节点【链接】

题目描述:

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

思路快指针先走k步,然后快指针和慢指针同时走,直到快指针走到NULL,此时慢指针的节点即为所求。

解析: 

代码实现

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

int kthToLast(struct ListNode* head, int k){
   struct ListNode*fast=head,*slow=head;  
   //快指针先走k步
   while(k--)
   {
    fast=fast->next;
   }
   //快慢指针一起走
   while(fast)
   {
    slow=slow->next;
    fast=fast->next;
   }
   return slow->val;
}

2.链表的回文结构【链接】

题目描述:

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

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

思路首先找到中间节点,将中间节点后半部分倒置,再分别从头结点和尾节点向中间遍历,看对应值是否相等。

这里需要用到曾经写过的找链表的中间节点函数和反转链表函数可参照【单链表的应用】

代码实现 

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList {
  public:
    /**
     * Definition for singly-linked list.
     * struct ListNode {
     *     int val;
     *     struct ListNode *next;
     * };
     */

    struct ListNode* middleNode(struct ListNode* head) {
        //创建快慢指针
        struct ListNode* slow = head;
        struct ListNode* fast = head;//如果有两个中间节点,则返回第二个中间节点
        while (fast && fast->next) {
            slow = slow->next;
            fast = fast->next->next;
        }
        //此时slow刚好指向中间节点
        return slow;
    }
    struct ListNode* reverseList(struct ListNode* head) {
        // 重新创建一个链表,将之前的链表进行头插即可
        struct ListNode* rphead = nullptr;
        // 进行指针变换
        struct ListNode* cur = head;
        while (cur != nullptr) {
            // 用于保存下一个节点地址
            struct ListNode* newnode = cur->next;
            // 头插
            cur->next = rphead;
            rphead = cur;
            cur = newnode;
        }
        return rphead; //返回新链表的头rhead
    }

    bool chkPalindrome(ListNode* A) {
        struct ListNode* mid = middleNode(A);
        struct ListNode* rmid = reverseList(mid);
        while (rmid && A) {
            if (rmid->val != A->val) {
                return false;
            }
            rmid = rmid->next;
            A = A->next;
        }
        return true;
    }
};

3.相交链表【链接】

题目描述:

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

示例1相交节点不是1,而是8,注意这里不能比值,因为两个不同节点的值可能一样,要比较节点的地址。

思路1:暴力求解,对于链表A中的每个节点,我们都遍历一次链表B看B中是否有相同节点,第一个找到的就是第一个公共节点,假设A链表有M个节点,B链表有N个节点,时间复杂度太高了为O(M*N)即O(N^2)。

思路2:先判断两个链表是否相交,可以通过判断两个链表最后一个节点地址是否相同,如果尾节点相同,说明两个链表一定相交,如果不相等,直接返回空指针。计算出两个链表的长度,让长链表先走相差的长度,然后让两个链表同时走,直到遇到相同的节点就是第一个公共节点,返回指向这个节点的指针。

解析: 

 

代码实现

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    struct ListNode *curA=headA,*curB=headB;
    int lenA=0,lenB=0;
    while(curA->next)
    {
        curA=curA->next;      
        ++lenA; 
    }
     while(curB->next)
    {
        curB=curB->next;     
        ++lenB; //此时B链表的长度少算一个
    }
    //尾节点不相等就是不相交
    if(curA!=curB)
    {
        return NULL;
    }
    lenA+=1;
    lenB+=1;
    //长的先走差距步,再同时走,第一个相等的就是交点
    //假设法
    int gap=abs(lenA-lenB);//求绝对值
    struct ListNode *longList=headA,*shortList=headB;//longList指向长链表,shprtList指向短链表
    //如果B比A长,再修改一下指针的指向,让longList指向长链表B,shprtList指向短链表A
    if(lenB>lenA)
    {
        longList=headB;
        shortList=headA;
    }
    while(gap--)//走差距步
    {
        longList=longList->next;
    }
    while(longList!=shortList)
    {
        longList=longList->next;
        shortList=shortList->next;
    }
    return shortList;//返回哪一个都可以
}

4.判断链表中是否有环【链接】

题目描述:

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

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

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

思路首先,让快慢指针fast、slow指向链表的头节点head, 让快指针fast一次向后移动两个节点,慢指针一次向后移动一个节点, 判断fast和slow是否走到同一个节点上数学上追击相遇问题,如果走到同一个节点上,就返回true。 

代码实现

bool hasCycle(struct ListNode *head) {
    struct ListNode*slow=head,*fast=head;
    while(fast&&fast->next)//当一个链表中没有环时,fast一定会移动到链表的结尾
    {
        slow=slow->next;
        fast=fast->next->next;
        if(slow==fast)
        {
            return true;
        }
    }
    return false;
}

常见问题解析 

1.快慢指针为什么会相遇,它们有没有可能错过,永远追不上?

 

不会错过。

假设slow进环时,fast 和 slow 的距离是N,追击过程中二者距离变化如下:

N->N-1->N-2->……->3->2->1->0

每追击一次,二者之间距离缩小1,距离为0即相遇。

2.慢指针slow一次移动一个节点,快指针一次移动多个节点(3,4,5……n)可行吗?

可行。

用快指针一次移动3个节点举例

假设slow进环时,fast 和 slow 的距离是N,追击过程中二者距离变化如下:

情况1:N->N-2->N-4->……->4->2->0(N为偶数)

情况2:N->N-2->N-4->……->5->3->1->-1(N为奇数)N为-1表示错过了,距离变成C-1(C为环的长度)

  • 如果C-1是偶数,新一轮追击就追上了
  • 如果C-1是奇数,那么就永远追不上

如果N是奇数并且C是偶数,那么就永远追不上,那这种条件存在吗?

假设slow进环前走的距离为L,设slow进环时,fast已经在环中转了x圈

slow走的距离:L

fast走的距离:L+x*C+C-N

由fast走的距离是slow的三倍产生的数学等式:3L=L+x*C+C-N

化简得:2L=(x+1)*C-N

我们发现偶数=(x+1)*偶数-奇数不成立,反证出当N是奇数时,C也一定是奇数

总结一下:一定能追上,N是偶数,第一轮就追上了;N是奇数,第一轮追不上,C-1是偶数,第二轮就追上了。

5.寻找环的入口点【链接】

题目描述:

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

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

不允许修改 链表。

思路1用两个指针head、meet分别指向链表的头节点和快慢指针相遇的节点,同时移动两个指针,当两个指针指向同一个节点时,该节点就是环的入口点 。

解析: 

假设环的长度为C,到达相遇点时,慢指针slow走过的距离为L+N(一圈之内肯定会被追上),快指针fast走过的距离为L+x*C+N (假设快指针走了x圈,N一定大于等于1),由fast走的距离是slow的2倍产生的数学等式:2*(L+N)=L+x*C+N  化简得:L=x*C-N

也就是说head到入口点的距离等于meet指针转x圈减去N的距离,head走到入口点,meet也刚好走到入口点,所以两个指针一起遍历,最终会同时到达入口点。

代码实现1

struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode *slow=head,*fast=head;
    while(fast&&fast->next)
    {
        slow=slow->next;
        fast=fast->next->next;
        if(slow==fast)
        {
         struct ListNode *meet=slow;
         while(meet!=head)
         {
            meet=meet->next;
            head=head->next;
         }   
         return meet;
        }
    }
    return NULL;
}

思路2:让快慢指针相遇节点与下一个节点断开,然后将问题转化为两个链表的相交,环的入口点其实就是两个链表的相交点。

解析: 

代码实现2

 struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    struct ListNode *curA=headA,*curB=headB;
    int lenA=0,lenB=0;
    while(curA->next)
    {
        curA=curA->next;      
        ++lenA; 
    }
     while(curB->next)
    {
        curB=curB->next;     
        ++lenB; //此时B链表的长度少算一个
    }
    //尾节点不相等就是不相交
    if(curA!=curB)
    {
        return NULL;
    }
    lenA+=1;
    lenB+=1;
    //长的先走差距步,再同时走,第一个相等的就是交点
    //假设法
    int gap=abs(lenA-lenB);//求绝对值
    struct ListNode *longList=headA,*shortList=headB;//longList指向长链表,shprtList指向短链表
    //如果B比A长,再修改一下指针的指向,让longList指向长链表B,shprtList指向短链表A
    if(lenB>lenA)
    {
        longList=headB;
        shortList=headA;
    }
    while(gap--)//走差距步
    {
        longList=longList->next;
    }
    while(longList!=shortList)
    {
        longList=longList->next;
        shortList=shortList->next;
    }
    return shortList;//返回哪一个都可以
}

struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode *slow=head,*fast=head;
    while(fast&&fast->next)
    {
        slow=slow->next;
        fast=fast->next->next;
        if(slow==fast)
        {
         struct ListNode *meet=slow;
         struct ListNode *newhead=meet->next;
         meet->next=NULL;        
         return getIntersectionNode(head,newhead);
        }
    }
    return NULL;
}

6.随机链表的复制【链接】

题目描述:

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

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

深拷贝就是拷贝一个值和指针指向都跟当前链表一模一样的链表。 

思路: 拷贝节点到原节点的后面,再寻找途径处理random指针,处理完后对复制的节点拿下来尾插成新链表,返回新链表的头。

解析: 

代码实现

/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *next;
 *     struct Node *random;
 * };
 */

//拷贝节点插入在原节点的后面
struct Node* copyRandomList(struct Node* head) {
	struct Node*cur=head;
    while(cur)
    {
        struct Node*copy=(struct Node*)malloc(sizeof(struct Node));
        copy->val=cur->val;

        copy->next=cur->next;//注意这里顺序不能颠倒
        cur->next=copy;

        cur=copy->next;
    }
    //控制random
    cur=head;
    while(cur)
    {
        struct Node*copy=cur->next;
        if(cur->random==NULL)
        {
            copy->random=NULL;
        }
        else 
        {            
            copy->random=cur->random->next;
        }
        cur=copy->next;        
    }
    //把拷贝节点取下来尾插成为新链表,然后恢复原链表
    struct ListNode*copyhead=NULL,*copytail=NULL;
    cur=head;
    while(cur)
    {
        struct ListNode*copy=cur->next;
        struct ListNode*next=copy->next;
        if(copytail==NULL)
        {
            copyhead=copytail=copy;
        }
        else
        {
            copytail->next=copy;
            copytail=copytail->next;
        }
        cur->next=next;//恢复原链表,有没有这句代码都行!
        cur=next;
    }
    return copyhead;
}

7.顺序表和链表的区别

 不同点顺序表链表
存储空间上底层存储空间连续不连续
任意位置插入删除元素需要搬运大量的元素,效率低,时间复杂度为O(N)不需要搬运大量的元素,只需要修改指针指向,效率高,时间复杂度为O(1)
随机访问数组中存储的元素可以直接通过下标访问,不需要遍历数组,时间复杂度最低可以达到 O(1)从头指针开始往后找,直至找到目标元素,访问目标元素的时间复杂度为 O(n)
扩容容量不足,则需要进行扩容(原地扩容和异地扩容),扩容有消耗,还可能存在空间浪费的问题由于链表独特的组成结构,按需申请,不需要考虑容量是否足够,即不需要考虑扩容
应用场景大多用在元素高效存储或随机访问频繁的场景大多用在任意位置插入和删除操作频繁的场景
缓存利用率

为什么CPU不直接访问内存?

当程序或数据存储在内存上时,CPU不能直接访问它们,因为内存的访问速度较慢。为了执行程序或访问数据,如果内存的数据比较小,通常就会加载到寄存器;如果数据比较大,它们首先需要被一级一级加载到缓存中,这样CPU才能快速访问它们。它们可以从缓存中快速读取,而不是从较慢的内存中读取。尽可能多的让CPU访问缓存,这大大减少了CPU直接访问内存的次数,从而大大提高了计算机的性能。

CPU是怎么读取缓存的?

缓存由快速且昂贵的存储器组成,其容量较小但速度较快。缓存中存储的数据是内存中即将被CPU访问的一小部分。当一个计算机程序需要访问内存中的数据时,它通常会首先查找缓存。如果有,则发生了缓存命中,CPU就直接从缓存中读取,这样可以大大提高数据的读取速度。如果没有,就会发生缓存未命中,程序需要从主存或其他更慢的存储设备中获取数据到缓存中。

如果我们要访问顺序表和链表里面的一部分数据例如1,2,3,4,5,6,7,怎么访问呢?

CPU不会直接访问内存,而是加载到缓存中去访问。

由于这些数据量大,这些数据不会被加载到寄存器中,会被加载到缓存中。

访问这段数据,CPU首先到缓存中查看第一个元素的地址在不在缓存里,如果在,发生缓存命中,直接访问;如果不在,缓存不命中,数据会先从内存加载到缓存,再去访问。

其实在加载的过程中,对于CPU来说,它是不会一个字节一个字节的加载的,因为这非常没有效率,一般来说都是要一块一块的加载的(局部性原理),它会认为当前位置的数据被访问了,与之相邻的内存区域也有很大概率被访问。一般加载多少字节到缓存中跟CPU的字长有关,CPU从内存中捞数据上来的最小数据单位是64Bytes也就是16个32位的整型。

顺序表中由于第一个数据不命中,会从首地址开始的一长段数据都加载到缓存中去,再访问第2个位置的数据,在缓存中,就发生缓存命中。如果数据特别长,第一个不命中,后面连续一长段都命中,再发生不命中,就再加载数据到缓存中去,后面一长段都命中(如下图所示),以此类推。这就叫做缓存利用率高 

但是在链表中,节点与节点之间在内存中的位置不一定连续,第一次访问时,发生缓存不命中,会把第一个节点以及后面一连串的地址都加载到缓存中去,有可能会访问到后面的节点,最坏的情况就是后面的节点一个都访问不到,这就叫做缓存利用率低。同时,可能会因为加载进来的没用的数据把缓存空间中其它有用的数据挤走而导致缓存污染

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DS堆的特性和实现(9)

文章目录 前言一、堆的概念和结构二、堆的调整算法向下调整算法向上调整算法两种算法建堆的时间复杂度 三、堆的实现结构体定义初始化和销毁堆的插入堆的删除挪移数据覆盖删除首尾交换再删除 获取堆顶元素获取有效数据个数判断是否为空 总结 前言 继续,本篇较难   …

我的创作纪念日-365天的感悟

时光荏苒,岁月如梭。转眼间,自己在CSDN注册已经整整15个年头了。回想起当初,还是个满怀憧憬、对未来充满无限好奇的学生哥。如今,虽然身份和角色发生了诸多变化,但CSDN始终陪伴着我,见证了我的成长与蜕变。…

JavaWeb环境下的Spring Boot在线考试系统开发

1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及,互联网成为人们查找信息的重要场所,二十一世纪是信息的时代,所以信息的管理显得特别重要。因此,使用计算机来管理基于JavaWeb技术的在线考试系统设计与实现…

深入探讨C++多线程性能优化

深入探讨C多线程性能优化 在现代软件开发中,多线程编程已成为提升应用程序性能和响应速度的关键技术之一。尤其在C领域,多线程编程不仅能充分利用多核处理器的优势,还能显著提高计算密集型任务的效率。然而,多线程编程也带来了诸…

OpenAI的新功能Canvas,效果还不错

时隔两年,ChatGPT终迎来界面全新升级! 这一次,OpenAI官宣推出类似 Anthropic 的 Artifacts 的界面交互功能 canvas,并称这是一种使用 ChatGPT 写作和编程的新方式。不论是写作,还是编码,都可以开启全新的交…

什么!我上传的文件不见了?

什么!我上传的文件不见了? 前言: 最近在实现一个文件上传功能时使用了异步处理,但是在异步处理文件时,却提示NoSuchFileException错误。简化代码如下: PostMapping("/upload")void testFileUpload(Reques…

Flume抽取数据(包含自定义拦截器和时间戳拦截器)

flume参考网址:Flume 1.9用户手册中文版 — 可能是目前翻译最完整的版本了https://flume.liyifeng.org/?flagfromDoc#要求: 使用Flume将日志抽取到hdfs上:通过java代码编写一个拦截器,将日志中不是json数据的数据过滤掉&#xf…

学习文档10/16

MySQL 字符集: MySQL 支持很多种字符集的方式,比如 GB2312、GBK、BIG5、多种 Unicode 字符集(UTF-8 编码、UTF-16 编码、UCS-2 编码、UTF-32 编码等等)。 查看支持的字符集 你可以通过 SHOW CHARSET 命令来查看,支持…

一次性理清Environment体系

在Spring中,我们可以通过配置文件等方式去进行一些属性值的配置,比如通过Value注解去获取到对应的属性值,又或者说是想在程序运行时获取系统环境变量,类似的这些操作其实都是去获取一些配置数据,所以在Spring中对这些数…

C++ | Leetcode C++题解之第486题预测赢家

题目&#xff1a; 题解&#xff1a; class Solution { public:bool PredictTheWinner(vector<int>& nums) {int length nums.size();auto dp vector<int>(length);for (int i 0; i < length; i) {dp[i] nums[i];}for (int i length - 2; i > 0; i-…

基于SpringBoot+Vue+uniapp的在线招聘平台的详细设计和实现

详细视频演示 请联系我获取更详细的演示视频 项目运行截图 技术框架 后端采用SpringBoot框架 Spring Boot 是一个用于快速开发基于 Spring 框架的应用程序的开源框架。它采用约定大于配置的理念&#xff0c;提供了一套默认的配置&#xff0c;让开发者可以更专注于业务逻辑而不…

喜报丨财富通科技荣获ISO9001、ISO27001及ISO20000三项国际认证

近日&#xff0c;财富通科技成功通过ISO9001、ISO27001及ISO20000三项国际标准认证。这标志着公司在质量管理体系、信息安全管理体系以及信息技术服务管理体系建设方面达到了国际认可的标准。 ISO9001认证表明财富通科技在软件开发、技术服务和项目管理等方面建立了一套完善的…

带你学习如何编写一篇API详设文档以及给新人提点建议

文章目录 前言先认清一个问题详设文档如何写先看文档脉络详设文档分析需求背景方案概述API定义安全设计性能设计缓存与数据库 总结 前言 这篇文章带读者了解软件开发项目中一个需求的开发详设文档主要包括哪些内容&#xff0c;其中重点会给读者分析API设计的规范&#xff0c;相…

推荐算法的学习

文章目录 前言1、模型1.1 从本领域模型的发展历史中学习1.1.1 在历史中总结发展规律和趋势1.1.2 发现模型之间的共性&#xff0c;方便记忆 1.2 从其他领域的发展中学习1.2.1 注意力机制1.2.2 残差网络 1.3 实践该怎么办&#xff1f; 2、 特征2.1 数据源的选择与建立2.2 特征构造…

react18中实现简易增删改查useReducer搭配useContext的高级用法

useReducer和useContext前面有单独介绍过&#xff0c;上手不难&#xff0c;现在我们把这两个api结合起来使用&#xff0c;该怎么用&#xff1f;还是结合之前的简易增删改查的demo&#xff0c;熟悉vue的应该可以看出&#xff0c;useReducer类似于vuex&#xff0c;useContext类似…

AirServer2024你的手机投屏神器,轻松实现多屏互动!

&#x1f4a1;**开篇点题**&#x1f4a1; 说起现代科技的魔力&#xff0c;小伙伴们是否还记得那个让你在公司会议、家庭影院乃至游戏战场上都能大显身手的神奇软件——AirServer&#xff1f;没错&#xff0c;就是那个让你手机秒变超级大屏的投屏神器&#xff01;今天我要和大家…

WebGIS开发系列教程

WebGIS开发-00保姆级、零基础入门教程 WebGIS开发-01开发环境搭建 WebGIS开发-02vite搭建htmlcssjs开发框架 WebGIS开发-03在框架中引入地图 WebGIS开发-04.搭建Vue3jsscss框架开启编程之旅 B Zhan持续更新中....

机器学习数据标准化与归一化:提升模型精度的关键

&#x1f4d8;数据标准化与归一化&#xff1a;提升模型精度的关键 机器学习中的数据处理环节至关重要&#xff0c;其中&#xff0c;数据标准化与归一化是提高模型性能的关键步骤之一。数据的特征尺度往往不一致&#xff0c;直接影响模型的训练效果&#xff0c;因此对数据进行处…

用sdkman管理多个jdk切换

前言 最近项目前后端进行升级&#xff0c;需要在jdk8和jdk17两个版本切换。最简单的是通过手动切换&#xff0c;但切换过程太繁琐&#xff0c;修改环境变量&#xff0c;达到切换目的。于是尝试其它解决方案&#xff0c;最终确实使用sdkman工具。 sdkman 是一款面向Java开发者的…

十分钟掌握Ajax(jQuery封装的ajax)

Ajax是一种异步&#xff08;无需等待服务器返回数据就可以做别的工作&#xff09;无刷新&#xff08;做了一些操作之后&#xff0c;页面不会刷新&#xff09;技术&#xff0c;通常结合DOM一起操作。(不像超链接和表单一样一点就刷新) Jquery封装好的Ajax技术有四种&#xff0c…