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

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

前言

这篇文章,我们继续来看几道题,今天是与链表相关的面试题。

每一道题,我们都要争取找出最优的算法来实现。

122791a601ac45f3a73a4b6d48521e8b.png

题目1:移除链表元素

题目链接先给大家:

链接: link

🆗,接下来我们一起来看一下:

3c103fc0336b4066bc0660492df9f0c0.png

题目分析

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

cc6e5deef2294ac398fd112db88d0906.png

只不过那道题是是在数组里,而今天这道题是移除链表中的某个元素。

题目给我们一个链表的头节点 head 和一个整数 val ,让我们删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点。

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

思路1:暴力求解

首先第一个思路,我们最容易想到的,还是这个比较暴力的方法:

就是遍历链表,找到值等于val 的结点,然后一一删除。

在链表中删除元素的好处在于,我们不需要像数组那样挪动数据

但是呢?

我们看到题目中给的是单链表

98c046d1ee674215ac0ac89102ad76c6.png

不过单链表的删除我们在之前单链表的文章里也讲了:

687d819c3a364a2f89c05a401e12668b.png那它的缺点在于

首先如果单链表不带头(哨兵位),且要删除的元素是首元素,即头删,我们是需要单独处理的,当然你可以选择带头的链表。

其次,我们删除链表中的元素还需要找到他的前一个,所以每删除一个元素,我们都需要遍历找它的前一个。

所以说呢?

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

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

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

那这种思路是什么呢?

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

思路讲解

怎么做呢?

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

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

大家要注意,数据结构的学习,画图是很重要的,图画清楚了,看着图去写代码就很简单了。

我们就来看题目给的示例:

af7a4a48959b4d0fb8b58be26fd63118.png

开始了:

初始状态:

8e685caf610c4bf68b27f7eaa5b3bad9.png

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

6e3af5c6883249f2ba950b0a2207c5e1.png

相信图大家都很容易理解。

但是要注意

新链表尾插结束一定要将尾结点的指针域置空,因为新链表的尾结点不一定是原链表的尾结点,如果不是的话,它作为新的尾,但是还指向原来在它后面的那个结点,而它后面的那个结点等于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;
}

不带哨兵位的实现,有一些地方需要单独判断,大家要注意:

比如:

5836acdb7fb84cacab5b2fd8c3749923.png其它的大家仔细看看应该都能明白。

带哨兵位

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;
}

6062670ed4874736af94e569422a7da3.png

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

46d1773e2f6c408b88edf81070237217.png

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

题目链接: link

我们一起看一下题:

bbc97130947442c6b34406602c9f9e63.png

我们一起来分析一下这道题:

题目分析

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

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

思路讲解

怎么搞呢?


还是利用双指针,分别从两个链表的第一个元素开始,两两比较大小,取小的那一个尾插到新链表(相等取任何一个都可以),因为最终要返回的还是升序链表。

这其实跟第一题的操作也有点像,第一题我们是去不等于val的结点尾插,这个题是取两个链表中较小的那个尾插。


我们来画一下图:

0a6f3c831f32404eb78e588a5c8f9804.png

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


🆗,是不是只要有其中一个链表遍历结束,整个循环就应该结束了。

循环结束就完了吗?

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

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

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

有空链表怎么处理?

是不是好办啊?

直接返回另一个链表就行了。

当然,如果我们选择的是带哨兵位的链表,这一步其实就不需要了。

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

代码实现

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

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

不带哨兵位

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;
}

采用带哨兵位的结构,就省去了很多的判断,但要注意最后要释放一下哨兵位的头结点。

8849d4b78c6b4ce0b89e6b7181003ba1.png

c545b87bdade417d9e7821523a8edc09.png

题目3:反转链表

题目链接: link

这道题呢也是一道非常经典的题目,一起来看一下:

ed3fd9c3d3504a80900e2754d2d36ec4.png

题目分析

🆗,题目的意思呢是让我们去反转链表,题意很好理解,比如一个链表原来的结点是1,2,3,4,5 ;那翻转之后就应该是5,4,3,2,1。

那这道题呢,我们提供两种比较好的思路,都会给大家一一实现。

思路1:取结点头插

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


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


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

就用题目给的例子:

a4763e6f00a6483197439be8fd525408.png

我们现在要完成这样一个翻转。

怎么做呢?大家来看图:

d59a33f559a2418daa905ad96d2f89f0.png

代码实现

思路理清了,写代码其实就很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)啊,因为反转之后,第一个结点就变成尾结点了,那尾结点的指针域理所当然要指向空了。

我们继续思考一下:

我们要改变指针指向,那这个指针肯定是联系着两个结点的。

那我们在操作时是不是也得需要两个指针,来保存相邻两个结点的地址啊

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

看这样一个例子:

dafe63fcd5ef4695b953967705fef892.png

如果两个指针的话:

185e76776fa34ef08d271ff042d1dd17.png

从图上看感觉好像可以啊,两个指针向后迭代,好像可以完成。

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

大家对比着图看一下,这是单链表,n2结点的指向一旦改变,还能找到下一个结点吗?

就不能了!!!

那就没法继续向后走了呀,所以呢?

我们还要增加一个指针,来保存n2的下一个。

画图分析

那我们来重新画一下图:

3d2a434e04f947b191d2f0734a04c9e5.png

那最好反转之后的链表的头就是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;
}

代码呢也好写,就是写代码过程中会出现一些空指针的问题,需要我们特别处理。

a4e46f9fe0574792877589c6614fde8d.png

2f7b22c82fbf4d91a32e07ba08fbbfb3.png

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

fbfba8eb2bba47bc9becd43f190f6817.png


目录
相关文章
|
18天前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
45 4
|
1月前
|
算法 前端开发 Java
数据结构与算法学习四:单链表面试题,新浪、腾讯【有难度】、百度面试题
这篇文章总结了单链表的常见面试题,并提供了详细的问题分析、思路分析以及Java代码实现,包括求单链表中有效节点的个数、查找单链表中的倒数第k个节点、单链表的反转以及从尾到头打印单链表等题目。
32 1
数据结构与算法学习四:单链表面试题,新浪、腾讯【有难度】、百度面试题
|
19天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
19天前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
1月前
|
算法 Java
数据结构与算法学习五:双链表的增、删、改、查
双链表的增、删、改、查操作及其Java实现,并通过实例演示了双向链表的优势和应用。
16 0
数据结构与算法学习五:双链表的增、删、改、查
|
18天前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
38 0
|
20天前
|
存储 NoSQL Redis
Redis常见面试题:ZSet底层数据结构,SDS、压缩列表ZipList、跳表SkipList
String类型底层数据结构,List类型全面解析,ZSet底层数据结构;简单动态字符串SDS、压缩列表ZipList、哈希表、跳表SkipList、整数数组IntSet
|
1月前
|
存储
[数据结构] -- 双向循环链表
[数据结构] -- 双向循环链表
22 0
|
1月前
|
存储
探索数据结构:便捷的双向链表
探索数据结构:便捷的双向链表
|
1月前
|
存储
探索数据结构:单链表的实践和应用
探索数据结构:单链表的实践和应用