【初阶数据结构】——链表常见面试题剖析

news2024/12/24 10:19:07

文章目录

    • 前言
    • 题目1:移除链表元素
      • 题目分析
      • 思路1:暴力求解
      • 思路2:取非val值尾插至新链表
        • 思路讲解
        • 思考
        • 代码实现
          • 不带哨兵位
          • 带哨兵位
    • 题目2:合并两个有序链表
      • 题目分析
      • 思路讲解
      • 代码实现
        • 不带哨兵位
        • 带哨兵位
    • 题目3:反转链表
      • 题目分析
      • 思路1:取结点头插
        • 代码实现
      • 思路2:改变指针指向
        • 思考
        • 画图分析
        • 代码实现

前言

这篇文章,我们继续来看几道题,今天是与链表相关的面试题。
每一道题,我们都要争取找出最优的算法来实现。在这里插入图片描述

题目1:移除链表元素

题目链接先给大家:
链接: link
🆗,接下来我们一起来看一下:
在这里插入图片描述

题目分析

大家如果看过我上一篇文章(链接: link )的话,会发现这道题跟上一篇文章中的第一道题 移除元素 是很像的。
在这里插入图片描述

只不过那道题是是在数组里,而今天这道题是移除链表中的某个元素。
题目给我们一个链表的头节点 head 和一个整数 val ,让我们删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点。

那这道题要怎么解决呢,下面我们一起来分析一下:

思路1:暴力求解

首先第一个思路,我们最容易想到的,还是这个比较暴力的方法:
就是遍历链表,找到值等于val 的结点,然后一一删除。

在链表中删除元素的好处在于,我们不需要像数组那样挪动数据
但是呢?
我们看到题目中给的是单链表:
在这里插入图片描述
不过单链表的删除我们在之前单链表的文章里也讲了:
在这里插入图片描述
那它的缺点在于
首先如果单链表不带头(哨兵位),且要删除的元素是首元素,即头删,我们是需要单独处理的,当然你可以选择带头的链表。
其次,我们删除链表中的元素还需要找到他的前一个,所以每删除一个元素,我们都需要遍历找它的前一个。

所以说呢?

这种方法虽然可行,但是实现起来其实是会比较麻烦的,我们就不实现这种方法了,大家有兴趣的话也可以试一试。

思路2:取非val值尾插至新链表

好,那么我们接下来提供另外一种思路:

那这种思路是什么呢?
其实跟我们上一篇文章移除元素那道题当时提供的第二种思路比较类似。

思路讲解

怎么做呢?

我们从头去遍历链表,如果链表结点的值等于val,我们就把当前结点删除并释放掉,如果结点的值不等于val,我就把它尾插到一个新的空链表上。

老规矩,接下来我们画图来带大家再梳理一遍思路:

大家要注意,数据结构的学习,画图是很重要的,图画清楚了,看着图去写代码就很简单了。
我们就来看题目给的示例:
在这里插入图片描述

开始了:

初始状态:
在这里插入图片描述

开始遍历,不等于就尾插,等于就删除:

在这里插入图片描述
相信图大家都很容易理解。

但是要注意

新链表尾插结束一定要将尾结点的指针域置空,因为新链表的尾结点不一定是原链表的尾结点,如果不是的话,它作为新的尾,但是还指向原来在它后面的那个结点,而它后面的那个结点等于val的话,会被我们删除释放掉,这时就存在野指针问题了。

思考

刚才的思路我们创建了一个新链表,将需要保留的结点尾插到了新链表里

那大家想一下,这是不是和上一篇文章那道题的第2种思路开辟一个新数组一样,是一种空间换时间的做法呢?

显然不是的!

为什么?

因为我们只是把原链表的结点拿了下来进行尾插,并没有创建新的结点。

另外,还有一个问题:

我们选择把要保留的结点尾插到一个新的链表,那这个新的链表,通过前面的学习,我们知道,有两种结构可以选择:
带哨兵位的和不带哨兵位的。

其实刚才我们的图里画的是不带哨兵位的。

那有什么区别呢?

其实这道题你选择这两种结构哪一个都可以,带哨兵位的好处就是插入第一个元素,即头插的时候,可以方便一点,如果不带哨兵,头插需要单独处理。
经过前面的学习,相信大家都明白这两者的区别,就不具体解释了。

代码实现

至于代码的实现,这里两种结构,我都给大家写了:

不带哨兵位
struct ListNode* removeElements(struct ListNode* head, int val){
    //不带哨兵位
    if(head==NULL)
        return NULL;
    struct ListNode* cur=head;
    struct ListNode* newhead=NULL;
    struct ListNode* tail=NULL;
    while(cur)
    {
        if(cur->val!=val)
        {
            if(newhead==NULL)
            {
                newhead=tail=cur;
            }
            else
            {
                tail->next=cur;
                tail=cur;
            }
            cur=cur->next;
        }
        else
        {
            struct ListNode* tmpnext=cur->next;
            free(cur);
            cur=tmpnext;
        }
    }
    if(tail)
        tail->next=NULL;
    return newhead;
}

不带哨兵位的实现,有一些地方需要单独判断,大家要注意:
比如:
在这里插入图片描述
其它的大家仔细看看应该都能明白。

带哨兵位
struct ListNode* removeElements(struct ListNode* head, int val){
    //带哨兵位
    struct ListNode* headnode=(struct ListNode*)malloc(sizeof(struct ListNode));
    headnode->next=NULL;
    struct ListNode* cur=head;
    struct ListNode* tail=headnode;
    while(cur)
    {
        if(cur->val!=val)
        {
            tail->next=cur;
            tail=cur;
            cur=cur->next;
        }
        else
        {
            struct ListNode* tmpnext=cur->next;
            free(cur);
            cur=tmpnext;
        }
    }
    tail->next=NULL;
    struct ListNode* newhead=headnode->next;
    free(headnode);
    return newhead;
}

在这里插入图片描述

带哨兵位的实现就不需要那么多单独判断的情况了,但是要注意,哨兵位的头结点是我们自己申请出来的,最后最好把头结点释放掉。
在这里插入图片描述

题目2:合并两个有序链表

题目链接: link
我们一起看一下题:
在这里插入图片描述
我们一起来分析一下这道题:

题目分析

这个题是不是我们上一篇文章也做过类似的,那个是合并有序数组,而这道题是合并有序链表。

那既然是类似的题目,我们就可以用类似的方法来解决。

思路讲解

怎么搞呢?

还是利用双指针,分别从两个链表的第一个元素开始,两两比较大小,取小的那一个尾插到新链表(相等取任何一个都可以),因为最终要返回的还是升序链表。
这其实跟第一题的操作也有点像,第一题我们是去不等于val的结点尾插,这个题是取两个链表中较小的那个尾插。

我们来画一下图:

在这里插入图片描述

那大家再来思考一下,这些操作我们肯定要放在循环中进行,那循环结束的条件应该是什么?

🆗,是不是只要有其中一个链表遍历结束,整个循环就应该结束了。
循环结束就完了吗?
并不是,因为还有另一个链表没有处理完,循环结束之后我们只需要把另一个链表的剩余元素链接到尾插的新链表后面就行了。

另外,还需要注意什么呢?

这道题给出的测试用例有这样的情况,就是给的两个链表中可能会有空链表

有空链表怎么处理?

是不是好办啊?
直接返回另一个链表就行了。
当然,如果我们选择的是带哨兵位的链表,这一步其实就不需要了。

好的,这就是整体的一个思路和一些需要注意的地方。

代码实现

那具体实现呢,就还是两种方式

即尾插的链表我们可以选择带头或是不带头(哨兵位)。

不带哨兵位

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
    if(list1==NULL)
        return list2;
    if(list2==NULL)
        return list1;
    struct ListNode* newhead=NULL;
    struct ListNode* tail=NULL;
    while(list1&&list2)
    {
        if(list1->val<list2->val)
        {
            if(newhead==NULL)
                newhead=tail=list1;
            else
            {
                tail->next=list1;
                tail=list1;
            }
            list1=list1->next;
        }
        else
        {
            if(newhead==NULL)
                newhead=tail=list2;
            else
            {
                tail->next=list2;
                tail=list2;
            }
            list2=list2->next;
        }
    }
    if(list1)
        tail->next=list1;
    if(list2)
        tail->next=list2;
    return newhead;
}

带哨兵位

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
    struct ListNode* guardnode=(struct ListNode* )malloc(sizeof(struct ListNode));
    guardnode->next=NULL;
    struct ListNode* tail=guardnode;
    while(list1&&list2)
    {
        if(list1->val<list2->val)
        {
 
            tail->next=list1;
            tail=list1;
            list1=list1->next;
        }
        else
        {
            tail->next=list2;
            tail=list2;
            list2=list2->next;
        }
    }
    if(list1)
        tail->next=list1;
    if(list2)
        tail->next=list2;
    struct ListNode* newhead=guardnode->next;
    free(guardnode);
    return newhead;
}

采用带哨兵位的结构,就省去了很多的判断,但要注意最后要释放一下哨兵位的头结点。
在这里插入图片描述
在这里插入图片描述

题目3:反转链表

题目链接: link
这道题呢也是一道非常经典的题目,一起来看一下:
在这里插入图片描述

题目分析

🆗,题目的意思呢是让我们去反转链表,题意很好理解,比如一个链表原来的结点是1,2,3,4,5 ;那翻转之后就应该是5,4,3,2,1。
那这道题呢,我们提供两种比较好的思路,都会给大家一一实现。

思路1:取结点头插

思路1是取原链表的结点进行头插,什么意思呢?

还是搞一个新链表,但是这个新链表的结点不是要我们自己再去创建,而是,我们去遍历原链表,然后,依次把每个结点取下来头插到新链表中。

好的,光说大家可能不太好理解,还是老规矩哈,我把图给大家好好画一下:

就用题目给的例子:
在这里插入图片描述
我们现在要完成这样一个翻转。

怎么做呢?大家来看图:

在这里插入图片描述

代码实现

思路理清了,写代码其实就很easy了。

struct ListNode* reverseList(struct ListNode* head){
    struct ListNode* newhead=NULL;
    struct ListNode* cur=head;
    struct ListNode* tmp=NULL;
    while(cur)
    {
        //保存下一个的地址
        tmp=cur->next;
        //头插
        cur->next=newhead;
        newhead=cur;
        cur=tmp;
    }
    return newhead;
}

对于代码就不过多解释了,相信大家都能看懂。

那接下来我们看另一种思路:

思路2:改变指针指向

那解这道题呢?还有另外一种想法,就是什么呢:

我们去改变每个结点指针的指向,原来它是指向后一个的,我们现在让它指向前一个,这样是不是也能完成链表的反转。

思考

那想要这样做,就会有一些问题值得我们思考一下:

首先第一个问题,我们说要改变指针的指向,那处在后面的结点,我们都让它指向前面的结点就行了。
那第一个结点呢?它前面啥也没有,我们应该让它指向哪里呢?
🆗,是不是应该让它指向空(NULL)啊,因为反转之后,第一个结点就变成尾结点了,那尾结点的指针域理所当然要指向空了。

我们继续思考一下:

我们要改变指针指向,那这个指针肯定是联系着两个结点的。
那我们在操作时是不是也得需要两个指针,来保存相邻两个结点的地址啊

两个指针够不够?我们来尝试画一下图:

看这样一个例子:
在这里插入图片描述

如果两个指针的话:

在这里插入图片描述
从图上看感觉好像可以啊,两个指针向后迭代,好像可以完成。

但是我们仔细分析一下,其实是有的问题的。

大家对比着图看一下,这是单链表,n2结点的指向一旦改变,还能找到下一个结点吗?
就不能了!!!
那就没法继续向后走了呀,所以呢?
我们还要增加一个指针,来保存n2的下一个。

画图分析

那我们来重新画一下图:

在这里插入图片描述
那最好反转之后的链表的头就是n1了,所以我们最好返回n1就行了。

代码实现

struct ListNode* reverseList(struct ListNode* head){
    if(head==NULL)
        return NULL;
    struct ListNode* n1=NULL;
    struct ListNode* n2=head;
    struct ListNode* n3=n2->next;
    while(n2)
    {
        n2->next=n1;

        n1=n2;
        n2=n3;
        if(n3)
            n3=n3->next;
    }
    return n1;
}

代码呢也好写,就是写代码过程中会出现一些空指针的问题,需要我们特别处理。
在这里插入图片描述
在这里插入图片描述

🆗,那以上就是这篇文章给大家介绍的几个链表相关的题目题目,希望能帮助到大家,同时也欢迎大家指正!!!
在这里插入图片描述

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

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

相关文章

用ode45解微分方程遇到的实际问题

最近在用ode45解微分方程数值解&#xff0c;试图复现论文中的图。一般来说说微分方程&#xff08;组&#xff09;只要按照响应的条件去撰写好对应的回调函数即可&#xff0c;基本没什么难度&#xff0c;但对于本文遇到的的这个问题&#xff0c;可能还需要一些技巧去实现解法&am…

动态路由和导航守卫

一、动态路由1、什么是动态路由&#xff1f;将URL地址中可变的内容设置成参数&#xff0c;根据不同的参数渲染不同的组件。&#xff08;组件可以复用&#xff09;2、动态路由如何进行参数的传递&#xff1a;&#xff08;1&#xff09;如何设置URL地址中的参数&#xff1a;’/ur…

【手写 Vue2.x 源码】第十二篇 - 生成 ast 语法树-流程说明

一&#xff0c;前言 上篇&#xff0c;主要介绍了 vue 数据渲染核心流程&#xff0c;涉及以下几个点&#xff1a; 初次渲染时 template 模板被编译为 ast 语法树&#xff1b;通过 ast 语法树生成 render 函数&#xff1b;通过 render 函数返回 vnode 虚拟节点&#xff1b;使用…

基于K8s的DevOps平台实践(三)

文章目录前言1. Jenkins与k8s集成&#x1f351; 插件安装及配置&#x1f351; 演示动态slave pod&#x1f351; Pod-Template中容器镜像的制作&#x1f351; 实践通过Jenkinsfile实现demo项目自动发布到kubenetes环境2. Jenkins集成Sonarqube&#x1f351; sonarqube架构简介&a…

初阶产品经理必看:如何快速进阶B端产品经理

​从去年开始&#xff0c;大批的互联网企业开始转战B端&#xff0c;众多传统企业也早在几年前就开始向互联网转型。 产业互联网的兴起&#xff0c;让一个岗位的潜藏价值正在逐渐爆发&#xff0c;尤其是富有经验的背景&#xff0c;更加让身价越来越高。 这个岗位就是&#xff…

QProcess的非阻塞式用法以及QApplication::processEvents的使用

一、QProcess的阻塞模式QProcess的应用场景非常广泛。可以使用它在qt程序中执行其他进程&#xff0c;并与之进行通信。当使用它执行一些终端命令和操作时&#xff0c;命令和操作往往是需要一定的时间的&#xff0c;这时QProcess本身提供了方法如&#xff1a;waitForStarted() /…

神经网络自适应PID控制及其应用

神经网络自适应PID控制及其应用 总结来自重庆大学宋永瑞教授2022暑期校园行学术会议 1. 研究背景 目前人工智能的发展为很多领域里的研究提供了可延展性&#xff0c;提供了新的研究问题的思路&#xff0c;无人系统和人工智能正走向深度融合&#xff0c;无人系统里具有核心驱动作…

C语言及算法设计课程实验三:最简单的C程序设计——顺序程序设计(四)

C语言及算法设计课程实验三&#xff1a;最简单的C程序设计——顺序程序设计&#xff08;四&#xff09;一、实验目的二、 实验内容2.4、将"China”译成密码三、 实验步骤3.4、顺序程序设计实验题目4&#xff1a;将"China”译成密码的实验步骤3.4.1、变量的定义与赋初…

Android EventBus源码深入解析

前言 EventBus&#xff1a;是一个针对Android进行了优化的发布/订阅事件总线。 github对应地址&#xff1a;EventBus 大家肯定都已经比较熟悉了&#xff0c;这里重点进行源码分析&#xff1b; EventBus源码解析 我们重点从以下三个方法入手&#xff0c;弄清楚register、unre…

关于sql注入这一篇就够了(适合入门)

本文章根据b站迪总课程总结出来,若有不足请见谅 目录 存在sql注入条件 判断数据库类型 注入mysql思路 判断网站是否存在注入点 判断列名数量&#xff08;字段数&#xff09; 文件读写操作 网站路径获取方法 注入类型 按注入点数据类型来分类 根据提交方式分类 猜测查询方式 sql…

(Java高级教程)第三章Java网络编程-第四节:TCP流套接字(ServerSocket)编程

文章目录一&#xff1a;Java流套接字通信模型二&#xff1a;相关API详解&#xff08;1&#xff09;ServerSocket&#xff08;2&#xff09;Socket三&#xff1a;TCP通信示例一&#xff1a;客户端发送什么服务端就返回什么&#xff08;1&#xff09;代码&#xff08;2&#xff0…

量子计算(二十一):Deutsch-Josza算法

文章目录 Deutsch-Josza算法 Deutsch-Josza算法 量子算法是量子计算落地实用的最大驱动力&#xff0c;好的量子算法设计将更快速推动量子计算的发展。 Deutsch-Jozsa量子算法&#xff0c;简称D-J算法&#xff0c;DavidDeutsch和RichardJozsa早在1992年提出了该算法&#xff0…

分布式事务方案分析:两阶段和TCC方案(图+文)

1 缘起 补充事务相关知识过程中&#xff0c; 发现&#xff0c;默认的知识都是基于单体服务的事务&#xff0c;比如ACID&#xff0c; 然而&#xff0c;在一些复杂的业务系统中&#xff0c;采用微服务架构构建各自的业务&#xff0c; 就有了分布式事务的概念&#xff0c;比如&am…

一站式云原生体验|龙蜥云原生ACNS + Rainbond

关于 ACNS 龙蜥云原生套件 OpenAnolis Cloud Native Suite&#xff08;ACNS&#xff09;是由龙蜥社区云原生 SIG 推出的基于 Kubernetes 发行版本为基础而集成的套件能力&#xff0c;可以提供一键式部署&#xff0c;开箱即用&#xff0c;以及丰富的云原生基础能力&#xff0c;…

JProfiler的使用

一、安装 从https://www.ej-technologies.com/download/jprofiler/files获取&#xff0c;如果需要对服务器远程分析&#xff0c;注意服务器版本的jprofiler和windows版本一致。 二、监控一个本地进程 2.1 不使用idea 安装之后&#xff0c;打开jprofiler&#xff0c;点击红框…

电脑蓝屏并提示BAD_POOL_CALLER怎么办?

电脑蓝屏可以说是Windows的常见问题&#xff0c;各种各样的终止代码对应着不同的问题。如果你的蓝屏代码显示BAD_POOL_CALLER&#xff0c;这篇文章就是为你提供的。 可能导致BAD_POOL_CALLER蓝屏错误的原因&#xff1a; 1、硬件或软件不兼容 2、过时或错误的设备驱动程序 3…

DataWorks创建JavaUDF函数全流程

文章目录插件下载创建MaxCompute Studio项目创建MaxCompute Java Module编写Java UDF函数注意说明&#xff1a;这篇文章只是个人记录下&#xff0c;具体步骤都可以在官网找到。推荐看官网文档哈 插件下载 创建MaxCompute Studio项目 启动IntelliJ IDEA&#xff0c;在顶部菜单栏…

1806. 还原排列的最少操作步数

解法一&#xff1a; 根据题目的题目描述进行模拟&#xff0c;遇到偶数iii将arr[i]prem[i/2]arr[i] prem[i/2]arr[i]prem[i/2],遇到奇数iii,将arr[i]prem[(n−1i)/2]arr[i]prem[(n-1i)/2]arr[i]prem[(n−1i)/2] 时间复杂度: O(n2)O(n^2)O(n2), 最多会循环n次空间复杂度&#…

Nginx反向代理使用方法小总结

文章目录一、前言二、反向代理定义重申三、短网址方式代理四、多级域名方式代理五、通配符代理方式总结一、前言 本文只介绍代理转发到一个主机的方式&#xff0c;至于在代理时进行负载均衡大家需要自己尝试&#xff0c;也比较简单&#xff0c;在本专栏前面文章提到过&#xf…

(二)Redis概述与安装

目录 一、概述 1、特性 2、应用场景 二、安装 三、启动 1、前台启动&#xff08;不推荐&#xff09; 2、后台启动&#xff08;推荐&#xff09; 四、redis关闭 五、redis相关知识介绍 一、概述 1、特性 Redis是一个开源的key-value存储系统。和Memcached类似&#x…