链表OJ题型讲解与总结

目录

一.引言

二.链表题型详细讲解

一.移除链表元素

二.反转单链表

三.链表的中间结点

四.链表返回倒数第k个节点

五.合并两个有序链表

六.链表分割

七.链表的回文结构

三.总结与提升


一.引言

   在我们学习完单链表与双链表后,真诚建议初学者能够掌握单双链表中:链表的初始化,尾插,头插,尾删,头删,固定位置插入以及删除等对应函数的编写,如果对这些内容仍然感到陌生,可以阅读本人上两篇博客对两种链表实现的详细讲解,如果已经掌握,可以来看看有关面试过程中可能会遇到的链表的OJ题型,经过整理如下:

二.链表题型详细讲解

一.移除链表元素

讲解:

        本题要求删除原链表中数据为val的节点,我们如何进行处理?如果直接用cur指针从原链表头节点进行判断,如果cur中val等于题目中给出的val那么将下一个节点连接到cur的next后面,这样的思路是否可以呢?其实是可以的,但是实现起来会很困难试想:如果是1->2->6->6->6->3中要删除数据6呢?那么找到不是6的节点进行连接会变得很复杂。因此,这里我们采用另一种思路:我们创建一个新的头节点newhead,和尾节点tail,用cur在原链表中去遍历,如果cur中的val不是要删除的数据,我们就进行尾插,这样每次判断一个cur就让cur指向next,时间复杂度会更小,最后在tail不为NULL的条件下将tail->next赋值NULL,返回newhead,尝试实现如下:

struct ListNode* removeElements(struct ListNode* head, int val) {
    struct ListNode* cur=head;
    struct ListNode* tail=NULL;
    struct ListNode* newhead=NULL;
    if(head==NULL)
    {
        return newhead;
    }
    else
    {
        while(cur)
    {
        if(cur->val!=val)
        {
            if(tail==NULL)
            {
                newhead=cur;
                tail=cur;
            }
            else
            {
                tail->next=cur;
                tail=cur;
            }
        }
        cur=cur->next;

    }
    if(tail)
    {
        tail->next=NULL;
    }
    return newhead;
    }
}

二.反转单链表

讲解:

        本题提供两种解法:让我们先来看第一种常规解法:先来定义一个newhead,这是我们最后要返回的头指针,随后是在原链表中负责遍历的cur指针,以及两个tmp和tmp地址备份指针tmp1,如果原链表为空,我们直接返回NULL,否则我们用tmp备份cur,tmp设为newhead,这里我使用了num来判断第一个尾插,如果num不为1那么就将新节点接在tmp后即可,直到cur走到了原链表的末尾,就返回newhead,代码:

//链表反转
struct ListNode* reverseList(struct ListNode* head)
{
    struct ListNode* newhead = NULL, * cur = head, * tmp = NULL, * tmp1 = NULL;
    int num = 0;
    if (head == NULL)
    {
        return NULL;
    }
    else
    {
        while (cur)
        {
            tmp = cur;
            newhead = tmp;
            cur = cur->next;
            num++;
            if (num == 1)
            {
                tmp->next = NULL;
                tmp1 = tmp;
            }
            else
            {
                tmp->next = tmp1;
                tmp1 = tmp;
            }
        }
        return newhead;
    }
}

        第二种解法要方便很多:我们使用三个指针来反转链表,如图:我们让n1为空,n2指针指向头节点,n3指针指向第二个节点如果n2为空,就说明链表为空,那么直接返回NULL,如果不为空,我们让n2的next指向n1,然后让n1向前走,n2向前走,n3如果不为空就往下走

n1,n2,n3往后走:

直到n2指向了末尾的NULL,这时n1就是反转链表的头指针,我们返回n1:

代码如下:

//三个指针来反转链表
struct ListNode* reverseList(struct ListNode* head)
{
    struct ListNode* n1 = NULL, * n2 = NULL, * n3 = NULL;
    n1 = NULL;
    n2 = head;
    if (head == NULL)
    {
        return NULL;
    }
    n3 = n2->next;
    while (n2)
    {
        n2->next = n1;
        n1 = n2;
        n2 = n3;
        if (n3)
            n3 = n3->next;
    }
    return n1;
}

三.链表的中间结点

讲解:

        两种解法:第一种是常规思路,我们可以用cur去遍历整个链表,来计算链表的长度,如果长度为奇数,我们返回cur从头节点经过(长度)/2个节点后指向的节点地址,如果长度为偶数,我们就返回cur从头节点经过(长度+1)/2个节点后指向的节点地址,代码如下:

struct ListNode* middleNode(struct ListNode* head) {
    struct ListNode* cur=head;
    int count=0;
    if(head==NULL)
    {
        return NULL;
    }
    while(cur)
    {
        cur=cur->next;
        count++;
    }
    cur=head;
    if(count%2==0)
    {
        int num=(count+1)/2;
        while(num)
        {
            cur=cur->next;
            num--;
        }
        return cur;
    }
    else
    {
        int num=count/2;
        while(num)
        {
            cur=cur->next;
            num--;
        }
        return cur;
    }
}

        第二种解法使用快慢指针快指针一次向前走两个,慢指针一次往前面走一个,这样当快指针为空或是快指针的next为空时,返回慢指针,就是题目要求返回的地址,图解如下:

同理,如果链表长度为偶数也同样适用:

代码如下:

struct ListNode* middleNode(struct ListNode* head) {
    struct ListNode* fast=head;
    struct ListNode* slow=head;
    while(fast&&fast->next)
    {
        fast=fast->next->next;
        slow=slow->next;
    }
    return slow;
}

四.链表返回倒数第k个节点

讲解:

        这里也有两个方法:第一种就是用cur指针从头指针开始去计算链表的结点个数,然后再让cur从头指针向下走长度-k个节点,返回最终的地址,这里直接介绍第二种简便方法:

        我们使用快慢指针,让快指针先走k个节点,然后让慢指针和快指针同时向前走,每次走一个节点,直到fast为NULL,这时返回slow指针的地址即可,图解:

代码如下:

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

五.合并两个有序链表

讲解:

        这里可以定义两个指针,一个是最后要返回的newhead,一个是进行尾插时要用到的cur指针,然后再用cur1和cur2分别从list1和list2往下走,首先我们来判断list1和list2是否全部为空,或者其中有一个为空,前者直接返回NULL,后者返回另一个不为空的链表头指针即可,再就是对newhead的赋值,首先对两个链表的第一个节点数据进行比较,将较小的地址赋值newhead,在cur1和cur2都不指为空的时候对两个链表节点中的数据进行比较,每次将较小的节点进行尾插,如果相同,那么就进行两次尾插,直到cur1或是cur2中有一个指向空,直接将另一个不为空的链表接在cur后即可:

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

六.链表分割

举例:如果要将小于3的节点排在其他节点之前:

排序后:

讲解:

        解决本题最好的方法是使用哨兵位点,这样可以省去讨论链表头为空的情况,也可以防止很多对空指针进行解引用所引发的段错误,具体如何使用,请看图解:

我们使用malloc开辟两个头结点,用cur去遍历原链表,如果节点的val小于x,那么就把这个节点接到第一个头结点的尾上如果大于等于x,就接到第二个头结点尾上,同时定义tail1和tail2两个指针来更好的进行尾插,当cur指向空时,代表遍历完成,这时把第二个头结点的next接到第一个tail1后面:

这样就构成了新链表,记住一定要在tail2后接上NULL,否则tail2->next会一直指向原链表的某个节点,有极大概率会构成死循环,最后我们返回第一个头结点的next,释放掉两个头结点:

 ListNode* partition(ListNode* pHead, int x) {
        // write code here
        struct ListNode* gguard,*gtail,*lguard,*ltail,*cur=pHead;
        gguard=(struct ListNode*)malloc(sizeof(struct ListNode));
        gtail=gguard;
        lguard=(struct ListNode*)malloc(sizeof(struct ListNode));
        ltail=lguard;
        gtail->next=NULL;
        ltail->next=NULL;
        while(cur)
        {
            if(cur->val<x)
            {
                gtail->next=cur;
                gtail=gtail->next;
            }
            else 
            {
                ltail->next=cur;
                ltail=ltail->next;
            }
            cur=cur->next;
        }
        gtail->next=lguard->next;
        ltail->next=NULL;
        struct ListNode* head=gguard->next;
        free(gguard);
        free(lguard);
        return head;
    }

七.链表的回文结构

讲解:

        相信在数组中判断一个字符串或者一个数组是否构成回文一定没少遇到,但是链表这里要复杂很多如果分别从头从尾一个一个比较直到left下标大于right下标,这样会很复杂,因为链表没有下标,所以不能直接访问,如果要从尾部一个一个访问,不仅需要找尾,还需要记录尾结点的前一个结点地址,因此,这里我们可以换种思路,我们可以用快慢指针找到中间那个链表的节点地址,然后从那个节点开始,重新产生一个反转的新链表,将新链表每一个节点的数据与原链表每一个节点的数据比较,一旦在两个链表走到空之前发现数据不同,就返回false,不构成回文链表,否则就返回true,构成回文链表:(ps:这里反转链表函数以及使用快慢指针找到链表中间节点在前文已经提及到),代码实现:

struct ListNode* find(struct ListNode* phead)
{
    struct ListNode* p1=phead;
    struct ListNode* p2=phead;
    while(p2&&p2->next)
    {
        p2=p2->next->next;
        p1=p1->next;
    }
    return p1;
}
struct ListNode* reverse(struct ListNode* phead)
{
    struct ListNode* n1=NULL;
    struct ListNode* n2=phead;
    struct ListNode* n3=n2->next;
    while(n2)
    {
        n2->next=n1;
        n1=n2;
        n2=n3;
        if(n3)
        {
            n3=n3->next;
        }
    }
    return n1;
}
    bool chkPalindrome(ListNode* phead) 
    {
        struct ListNode* head1=find(phead);
        struct ListNode* head2=reverse(head1);
        while(phead&&head2)
        {
            if(phead->val!=head2->val)
            {
                return false;
            }
            phead=phead->next;
            head2=head2->next;
        }
        return true;
    }

三.总结与提升

        在我们做链表Oj的时候,个人觉得要注意以下几点:

        1.注意判断头指针为空的情况;

        2.注意防止对空指针进行解引用;

        3.在进行尾插时,注意对尾指针指向空的处理;

        4.在实际编程中,注意对哨兵位点的返回值是否正确,以及对申请空间的释放;

        5.多动手画图分析,不要盲目做题,将原理剖析可以个更加高效的解题。

——————————————————    本文结束    ——————————————————

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

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

相关文章

深入理解 Fork/Join 并行计算框架

在并发编程领域&#xff0c;任务模型可以分为简单并行任务、聚合任务和批量并行任务。然而&#xff0c;还有一种广泛应用的任务模型——分治&#xff08;Divide and Conquer&#xff09;。分治是一种解决复杂问题的思维方法&#xff0c;它通过将复杂问题分解为多个相似的子问题…

小尺寸低功耗蓝牙模块在光伏清扫机器人上的应用

一、引言 随着可再生能源的迅速发展&#xff0c;光伏发电系统的清洁与维护变得越来越重要。光伏清扫机器人通过自动化技术提高了清洁效率&#xff0c;而蓝牙模组的集成为这些设备提供了更为智能的管理和控制方案。 二、蓝牙模组的功能与实现&#xff1a; 蓝牙模组ANS-BT103M…

沁恒CH32V208蓝牙串口透传例程:修改透传的串口

从事嵌入式单片机的工作算是符合我个人兴趣爱好的,当面对一个新的芯片我即想把芯片尽快搞懂完成项目赚钱,也想着能够把自己遇到的坑和注意事项记录下来,即方便自己后面查阅也可以分享给大家,这是一种冲动,但是这个或许并不是原厂希望的,尽管这样有可能会牺牲一些时间也有哪天原…

【触想智能】工业安卓一体机日常维护注意事项以及其应用领域分析

工业安卓一体机是一种集成了安卓操作系统的工业控制设备。它广泛应用于各种工业场景&#xff0c;为生产和管理提供了便利。 为了保证工业安卓一体机的正常运行和延长其寿命&#xff0c;日常维护工作是十分重要的。下面是一些工业安卓一体机日常维护的注意事项&#xff0c;以及其…

12.6深度学习_模型优化和迁移_模型移植

八、模型移植 1. 认识ONNX ​ https://onnx.ai/ ​ Open Neural Network Exchange&#xff08;ONNX&#xff0c;开放神经网络交换&#xff09;格式&#xff0c;是一个用于表示深度学习模型的标准&#xff0c;可使模型在不同框架之间进行转移。 ​ ONNX的规范及代码主要由微软…

2-2-18-14 QNX系统架构之 TCP/IP 网络

阅读前言 本文以QNX系统官方的文档英文原版资料为参考&#xff0c;翻译和逐句校对后&#xff0c;对QNX操作系统的相关概念进行了深度整理&#xff0c;旨在帮助想要了解QNX的读者及开发者可以快速阅读&#xff0c;而不必查看晦涩难懂的英文原文&#xff0c;这些文章将会作为一个…

实战:MyBatis适配多种数据库:MySQL、Oracle、PostGresql等

概叙 很多时候&#xff0c;一套代码要适配多种数据库&#xff0c;主流的三种库&#xff1a;MySQL、Oracle、PostGresql&#xff0c;刚好mybatis支持这种扩展&#xff0c;如下图所示&#xff0c;在一个“namespace”&#xff0c;判断唯一的标志是iddatabaseId&#xff0c;刚好写…

电子信息工程自动化 单片机彩灯控制

摘要 随着社会经济和科学技术的不断进步&#xff0c;人们在保持发展的同时&#xff0c;环境带给人类的影响已经不足以让我们忽视&#xff0c;所以城市的美化问题慢慢的进入了人们的眼帘&#xff0c;PLC的产生给带电子产品带来了巨大变革&#xff0c;彩灯的使用在城市的美化中变…

【后台管理系统】-【组件封装】

目录 组件封装搜索组件table列表组件content组件自定义插槽定制modal组件动态获取options数据 组件封装 搜索组件 给页面写一个配置文件&#xff0c;将配置文件传入组件&#xff0c;可直接生成页面&#xff0c;以下面页面为例&#xff0c; 新建src/views/main/system/depart…

「嵌入式系统设计与实现」书评:学习一个STM32的案例

本文最早发表于电子发烧友论坛&#xff1a;【新提醒】【「嵌入式系统设计与实现」阅读体验】 学习一个STM32的案例 - 发烧友官方/活动 - 电子技术论坛 - 广受欢迎的专业电子论坛!https://bbs.elecfans.com/jishu_2467617_1_1.html 感谢电子发烧友论坛和电子工业出版社的赠书。 …

设计模式:20、状态模式(状态对象)

目录 0、定义 1、状态模式的三种角色 2、状态模式的UML类图 3、示例代码 0、定义 允许一个对象在其内部状态改变时改变它的行为&#xff0c;对象看起来似乎修改了它的类。 1、状态模式的三种角色 环境&#xff08;Context&#xff09;&#xff1a;环境是一个类&#xff0…

idea中新建一个空项目

目的&#xff0c;为了在同一个目录下有多个小的项目&#xff1a;使用IDE为idea2022。 步骤&#xff1a; 点击新建项目&#xff0c;点击创建空项目&#xff0c;这里选择空项目是将其作为其他项目的一个容器&#xff0c;如图所示&#xff1a; 然后点击文件->项目结构&#xf…

Java基础复习

“任何时候我也不会满足&#xff0c;越是多读书&#xff0c;就越是深刻地感到不满足&#xff0c;越感到自己知识贫乏。科学是奥妙无穷的。” ——马克思 目录 一、方法&方法重载 二、运算符 三、数据类型 四、面向对象 1. 面向对象思想 2. 引用传递 3. 访问权限修饰…

嵌入式里的“移植”概念

这里因为最近一年看到公司某项目很多代码上有直接硬件的操作&#xff0c;这里有感而发&#xff0c;介绍移植的概念。 一、硬件 先上一个图&#xff1a; 举个例子&#xff0c;大学里应该都买过开发板&#xff0c;例如st的&#xff0c;这里三个层次&#xff0c; 内核&#xff…

量子计算与商业转型之旅

近年来&#xff0c;各组织对量子计算的应用有所增加&#xff0c;并使全球商业运营发生了显著变化。《财富商业洞察》的一份报告显示&#xff0c;2022 年量子计算市场价值为 7.17 亿美元&#xff0c;预计到 2030 年将达到 65.28 亿美元。 从本质上讲&#xff0c;量子计算机与经…

周末和男朋友户外运动美好时光

周末的时光总是那么令人期待&#xff0c;仿佛一周的忙碌和疲惫都在这一刻得到了释放。这次&#xff0c;我和男朋友决定到户外去锻炼身体&#xff0c;享受一下大自然的馈赠。 清晨的阳光透过窗帘的缝隙洒进房间&#xff0c;我懒洋洋地睁开眼睛&#xff0c;看到男朋友已经在一旁整…

【Git】:标签管理

目录 理解标签 创建标签 操作标签 理解标签 标签的作用 标记版本&#xff1a;标签 tag &#xff0c;可以简单的理解为是对某次 commit 的⼀个标识&#xff0c;相当于起了⼀个别名。例如&#xff0c;在项目发布某个版本的时候&#xff0c;针对最后⼀次 commit 起⼀个 v1.0 这样…

QT的ui界面显示不全问题(适应高分辨率屏幕)

//自动适应高分辨率 QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);一、问题 电脑分辨率高&#xff0c;默认情况下&#xff0c;打开QT的ui界面&#xff0c;显示不全按钮内容 二、解决方案 如果自己的电脑分辨率较高&#xff0c;可以尝试以下方案&#xff1a;自…

【Elasticsearch】初始化默认字段及分词

1、添加分词插件 1&#xff09;在线安装 执行命令 需要指定相同的版本 bin/elasticsearch-plugin.bat install https://get.infini.cloud/elasticsearch/analysis-ik/7.17.24 2&#xff09;离线安装 将安装包解压到 /plugins 目录下 安装包可以从对应的资源处下载 启动成…

MATLAB直流电机模型,直流电机控制

直流电机控制简介 直流电机&#xff08;DC motor&#xff09;广泛应用于各种机械驱动和电力控制系统中&#xff0c;其运行性能的控制至关重要。为了精准地控制直流电机的输出特性&#xff0c;可以通过不同的控制方式进行调节。常见的控制方式包括电枢电流控制、速度控制、电机位…