算法与数据结构_链表

2023-11-13

链表

一、理解指针或引用的含义

  1. 含义:将某个变量(对象)赋值给指针(引用),实际上就是就是将这个变量(对象)的地址赋值给指针(引用)。
  2. 示例:
    p—>next = q; 表示p节点的后继指针存储了q节点的内存地址。
    p—>next = p—>next—>next; 表示p节点的后继指针存储了p节点的下下个节点的内存地址。

二、警惕指针丢失和内存泄漏(单链表)

  1. 插入节点
    在节点a和节点b之间插入节点x,b是a的下一节点,,p指针指向节点a,则造成指针丢失和内存泄漏的代码:p—>next = x;x—>next = p—>next; 显然这会导致x节点的后继指针指向自身。
    正确的写法是2句代码交换顺序,即:x—>next = p—>next; p—>next = x;
  2. 删除节点
    在节点a和节点b之间删除节点b,b是a的下一节点,p指针指向节点a:p—>next = p—>next—>next;

三、利用“哨兵”简化实现难度

  1. 什么是“哨兵”?
    链表中的“哨兵”节点是解决边界问题的,不参与业务逻辑。如果我们引入“哨兵”节点,则不管链表是否为空,head指针都会指向这个“哨兵”节点。我们把这种有“哨兵”节点的链表称为带头链表,相反,没有“哨兵”节点的链表就称为不带头链表。

  2. 未引入“哨兵”的情况
    如果在p节点后插入一个节点,只需2行代码即可搞定:

     new_node—>next = p—>next;
     p—>next = new_node;
    

    但,若向空链表中插入一个节点,则代码如下:

    if(head == null){
    head = new_node;
    }
    

    如果要删除节点p的后继节点,只需1行代码即可搞定:

    p—>next = p—>next—>next;
    但,若是删除链表的最有一个节点(链表中只剩下这个节点),则代码如下:

    if(head—>next == null){
    head = null;
    }
    

    从上面的情况可以看出,针对链表的插入、删除操作,需要对插入第一个节点和删除最后一个节点的情况进行特殊处理。这样代码就会显得很繁琐,所以引入“哨兵”节点来解决这个问题。

  3. 引入“哨兵”的情况
    “哨兵”节点不存储数据,无论链表是否为空,head指针都会指向它,作为链表的头结点始终存在。这样,插入第一个节点和插入其他节点,删除最后一个节点和删除其他节点都可以统一为相同的代码实现逻辑了。

  4. “哨兵”还有哪些应用场景?
    这个知识有限,暂时想不出来呀!但总结起来,哨兵最大的作用就是简化边界条件的处理。

四、重点留意边界条件处理

经常用来检查链表是否正确的边界4个边界条件:

  1. 如果链表为空时,代码是否能正常工作?
  2. 如果链表只包含一个节点时,代码是否能正常工作?
  3. 如果链表只包含两个节点时,代码是否能正常工作?
  4. 代码逻辑在处理头尾节点时是否能正常工作?

五、举例画图,辅助思考

核心思想:释放脑容量,留更多的给逻辑思考,这样就会感觉到思路清晰很多。

习题

单链表反转(LC:206)

LC:206——反转链表

//首先手写一个链表结构体,多写多练,做到随时手写
/*
	struct ListNode{
	int val;
	ListNode* next;
	ListNode():val(0),next(nullptr){}
	ListNode(int x):val(x),next(nullptr){}
	ListNode(int x,ListNode *next):val(x),next(next){}
	};
*/
//简单迭代,调用两个指针锻逻辑关系
class Solution{
public:
    ListNode* reverseList(ListNode *head){
        ListNode *pre = head,*cur = nullptr;
        while(pre != nullptr){
            ListNode *temp = pre->next;
            pre->next = cur;
            cur = pre;
            pre = temp;
        }
    return cur;
    }
};

链表中环的检测(LC:141)

LC:141——环形链表

//照样首先手写链表结构体,多练
/*
struct ListNode{
    int val;
    ListNode *next;
    ListNode(int x):val(x),next(next){}
};
*/
//第一种自己写的,边界判断有点混乱,存在很多不必要的判断,浪费时间。但好在可以AC
class Solution{
public:
    bool hasCycle(ListNode *head){
        if(head==nullptr || head->next==nullptr) return false;//判断单个元素,空链等必无环的情况,可以优化
        //if(!head || !head->next)跟上面的判断是一样的
        ListNode *pre=head,*last = head;//核心是快慢双指针,慢指针一次走一步,快指针一次走两步。只要存在环,二者必相见
        while(pre != nullptr){
            pre = pre->next;
            if(!pre) return false;//有一种情况是想让快指针一次走满两步 pre = pre->next->next。
            //但存在走过的情况,若链表无环,走到了nullptr又要往后走,但nullptr的后面是没有元素的,报错。
            //所以分开,快指针每走一步都进行一次判断。
            pre = pre->next;
            last = last->next;
            if(pre == last) return true;
        }
        return false;
    }
};

//第二种同样是快慢指针,但是快指针的推进考虑优化了很多,放在循环条件里,简洁易懂
class Solution{
public:
    bool hasCycle(ListNode *head){
        ListNode *fast = head,*slow = head;
        //这个判断条件我只能说666妙不可言。只有满足快指针不是空指针,且快指针的下一步也非空的时候才进入循环。
        //保证了快指针fast的下两跳一定可取。不会出现边界问题
        while(fast && fast->next){
            fast = fast->next->next;
            slow = slow->next;
            if(fast == slow) return true;
        }
        return false;
	}   
};

两个有序链表的合并(LC:21)

LC:21——合并两个有序链表

//照例 先写一个链表节点结构体
/*
struct ListNode{
    int val;
    ListNode *next;
    ListNode():val(0),next(nullptr){}
    ListNode(int x):val(x),next(nullptr){}
    ListNode(int x,ListNode *next):val(x),next(next){}
}
*/
//迭代方法,创建新的头节点,然后比较两条链表,将节点数值更小的那个放入新的链表下一位。
//直到某个节点被全部处理完。把剩下没处理完的另一条链表直接拼上
class Solution{
    ListNode* mergeTwoLists(ListNode *l1,ListNode *l2){
        ListNode *nhead = new ListNode(-1);//创建新的头节点
        ListNode *pre = nhead;// 创建新链表中的一个动点,维护新节点的末尾节点的
        //当l1 l2两个链表都非空时,进入循环,开始比较寻找两个链表中头部val更小的节点直接拼接到新链表的尾端pre->next
        while(l1 && l2){
            if(l1->val <= l2->val){
                pre->next = l1;
                l1 = l1->next;
            }
            else{
                pre->next = l2;
                l2 = l2->next;
            }
            //新链表尾部拼接新节点后始终将pre指向尾部,用以维护新链表的最后节点。
            pre = pre->next;
        }
        //判断最终哪个节点时非空的?非空的剩余链表同样满足升序排列,故直接拼上就可以了
        pre->next = l1?l1:l2;
        //记得满足要求的链表是从头节点后面一位开始的
        return nhead->next;
    }
};

删除链表倒数第n个节点(LC:19)

LC:19

//还是先写链表节点结构体
/*
struct ListNode{
    int val;
    ListNode *next;
    ListNode():val(0),next(nullptr){}
    ListNode(int x):val(x),next(nullptr){}
    ListNode(intx,ListNode *next):val(x),next(next){}
};
*/
class Solution{
    ListNode* removeNthFromEnd(ListNode *head,int n){
        //快慢双指针,中间错位n步,这样保证当快指针到达末尾时,慢指针的下一步指向希望删除的节点。
        ListNode *pre = head,*cur = head;
        while(n>0){
            pre = pre->next;
            n--;
        }
        //考虑特殊情况,当快指针先行n步后,已然到达末尾节点的下一跳空指针。此时希望删除的就是原链表中的头节点。
        //返回head->next即可
        if(!pre) return head->next;
        //试想,若此快指针到达了nullptr,那么相隔n步的cur指向的正是待删除节点。
        //但事实上单链表只能操作下一跳节点,因此获知待删除节点的上一跳节点比它本身更有意义。
        //若此时pre仍未走到尾节点,则将pre与cur同步推进直到pre走到尾节点,那么cur此时所处位置就是待删除节点的上一步。
        //删除此时cur指向节点的下一步节点即可。
        while(pre->next){
            pre = pre->next;
            cur = cur->next;
        }
        cur->next = cur->next->next;
     return head;
    }
};

求链表的中间节点(LC:876)

LC:876——链表的中间节点

//照例先手写链表结构体
/*
struct ListNode{
    int cal;
    ListNode *next;
    ListNode():val(0),next(nullptr){}
    ListNode(int x):val(x),next(nullptr){}
    ListNode(int x,ListNode *next):val(x),next(next){}
};
*/
class Solution{
    ListNode *middleNode(ListNode * head){
        ListNode *cur = head,*pre = head;
        while(pre && pre->next){
            pre = pre->next->next;
            cur = cur->next;
        }
      return cur;
    }
}
  • 以上题解都是迭代方式,递归方式等以后精通了再来写,现在一知半解,写了也白写。。

心得(别人的)

  1. 函数中需要移动链表时,最好新建一个指针来移动,以免更改原始指针位置。
  2. 单链表有带头节点和不带头结点的链表之分,一般做题默认头结点是有值的。
  3. 链表的内存时不连续的,一个节点占一块内存,每块内存中有一块位置(next)存放下一节点的地址(这是单链表为例)。
  4. 链表找倒数第k个节点思想:创建两个指针,第一个先走k-1步然后两个在一同走。第一个走到最后时则第二个指针指向倒数第k位
  5. 反向链表思想:从前往后将每个节点的指针反向,即.next内的地址换成前一个节点的,但为了防止后面链表的丢失,在每次换之前需要先创建个指针指向下一个节点。
  6. 两个有序链表合并思想:这里用到递归思想。先判断是否有一个链表是空链表,是则返回两一个链表,免得指针指向不知名区域引发程序崩溃。然后每次比较两个链表的头结点,小的值做新链表的头结点,此节点的next指针指向本函数(递归开始,参数是较小值所在链表.next和另一个链表)。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

算法与数据结构_链表 的相关文章

  • 为什么这个 Web api 控制器不并发?

    我有一个 Web API 控制器 里面有以下方法 public string Tester Thread Sleep 2000 return OK 当我调用它 10 次 使用 Fiddler 时 我预计所有 10 次调用都会在大约 2 秒后
  • 在 HKCR 中创建新密钥有效,但不起作用

    我有以下代码 它返回 成功 但使用两种不同的工具使用搜索字符串 3BDAAC43 E734 11D5 93AF 00105A990292 搜索注册表不会产生任何结果 RegistryKey RK Registry ClassesRoot C
  • 将类对象放置在向量中?

    我注意到我可以将一个类放置在一个向量中 这是我的程序 我收到以下错误 out blackjack exe blackjack obj blackjack obj error LNK2019 unresolved external symbo
  • 按扩展名过滤搜索文件返回太多结果

    我正在开发一个 C 控制台应用程序 它必须管理 Windows 操作系统上的文件 我需要获取具有特定扩展名的文件名 列表 我找到了很多解决方案 最建议的是以下一种 HANDLE hFind WIN32 FIND DATA data hFin
  • 前向声明类型和“已声明为类类型的非类类型”

    我对以下代码有问题 template
  • 有些有助于理解“产量”

    在我不断追求少吸的过程中 我试图理解 产量 的说法 但我不断遇到同样的错误 someMethod 的主体不能是迭代器块 因为 System Collections Generic List 不是迭代器接口类型 这是我被卡住的代码 forea
  • 如何将 .txt 文件中的数据转换为 xml? C#

    我在一个文本文件中有数千行数据 我想通过将其转换为更容易搜索的内容来轻松搜索 我希望 XML 或其他类型的大型数据结构 尽管我不确定它是否是最好的对于我的想法 每行的数据如下所示 第 31 册 托马斯 乔治 32 34 154 每本书都不是
  • 不同 C++ 文件中的相同类名

    如果两个 C 文件具有相同名称的类的不同定义 那么当它们被编译和链接时 即使没有警告也会抛出一些东西 例如 a cc class Student public std string foo return A void foo a Stude
  • 什么是空终止字符串?

    它与什么不同标准 字符串 http www cplusplus com reference string string 字符串 实际上只是一个数组chars 空终止字符串是指其中包含空字符的字符串 0 标记字符串的结尾 不一定是数组的结尾
  • 是否使用 C# 数据集? [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我对 C 中的数据集概念有点困惑 编码 ASP NET 站点 但这并不重要 在我的阅读中 我了解到它们 本质上 用作我的应用程序和我的
  • 如何将AVFrame转换为glTexImage2D使用的纹理?

    如您所知 AVFrame 有 2 个属性 pFrame gt data pFrame gt linesize 当我从视频 sdcard test mp4 android平台 读取帧后 并将其转换为RGB AVFrame副 img conve
  • 如何递归取消引用指针(C++03)?

    我正在尝试在 C 中递归地取消引用指针 如果传递一个对象 那就是not一个指针 这包括智能指针 我只想返回对象本身 如果可能的话通过引用返回 我有这个代码 template
  • 从 C# 使用 Odbc 调用 Oracle 包函数

    我在 Oracle 包中定义了一个函数 CREATE OR REPLACE PACKAGE BODY TESTUSER TESTPKG as FUNCTION testfunc n IN NUMBER RETURN NUMBER as be
  • 如何最好地以编程方式将 `__attribute__ ((unused))` 应用于这些自动生成的对象?

    In my makefile我有以下目标 它将文本 HTML 资源 编译 为unsigned char数组使用xxd i http linuxcommand org man pages xxd1 html 我将结果包装在匿名命名空间和标头保
  • Oauth2中如何同时撤销RefreshToken和使AccessToken失效

    我正在使用 Owin Oauth2 授权和资源服务器相同 开发单页面应用程序 AngularJS Net MVC Json Rest API 的身份验证流程 我选择了 Bearer Token 路由而不是传统的 cookie session
  • C++ 对象用 new 创建,用 free() 销毁;这有多糟糕?

    我正在修改一个相对较大的 C 程序 不幸的是 并不总是清楚我之前的人使用的是 C 还是 C 语法 这是在一所大学的电气工程系 我们 EE 总是想用 C 来做所有事情 不幸的是 在这种情况下 人们实际上可以逃脱惩罚 但是 如果有人创建一个对象
  • C++:为什么 numeric_limits 对它不知道的类型起作用?

    我创建了自己的类型 没有任何比较器 也没有专门化std numeric limits 尽管如此 由于某种原因 std numeric limits
  • C++:二叉树所有节点值的总和

    我正在准备面试 我被一个二叉树问题困住了 我们如何计算二叉树所有节点中存在的值的总和 优雅的递归解决方案 伪代码 def sum node if node NULL return 0 return node gt value sum nod
  • 为什么空循环使用如此多的处理器时间?

    如果我的代码中有一个空的 while 循环 例如 while true 它将把处理器的使用率提高到大约 25 但是 如果我执行以下操作 while true Sleep 1 它只会使用大约1 那么这是为什么呢 更新 感谢所有精彩的回复 但我
  • MySqlConnectionStringBuilder - 使用证书连接

    我正在尝试连接到 Google Cloud Sql 这是一个 MySql 解决方案 我能够使用 MySql Workbench 进行连接 我如何使用 C 连接MySqlConnectionStringBuilder 我找不到提供这三个证书的

随机推荐

  • 关于Unity加载优化,你可能会遇到这些问题

    关键词 资源加载 卸载 实例化实例化 资源管理方法 一 资源加载 Q1 Shader 是独立打包的 如果我在开始游戏的时候加载一次 以后切换场景时就不用每次加载了吗 确切地说 要实现后续Shader不加载开销 需要满足以下两个条件 1 包含
  • Sophus安装踩坑

    装SLAM十四讲第二版提供的Sophus Eigen版本3 4 0 报错 home ch 下载 Sophus 13fb3288311485dc94e3226b69c9b59cd06ff94e test core test so2 cpp 9
  • JAVAfx11打包部署

    1 将默认打包工具删除 添加maven shade plugin依赖 如下
  • 有实力的人才能谈梦想

    我总是徘徊 在犹豫 觉得自己做不到 只是在苟延残喘摆了 所谓的目标也不可能实现 今天我发现我做到了 原来也不是那么遥不可及 是自己不够自信 不够淡定 有实力的人聊梦想 没理想的人就想想怎么混工作吧 有实力的人 自信 坚定的毅力 不怕失败 淡
  • Vue.directive指令(自定义指令)

    定义方式 html页面定义 Vue directive hello function el binding vnode el style color binding value 全局定义 Vue directive hello insert
  • 2.搭建Fabric区块链网络环境——前提条件和fabric的安装

    1 安装前提条件 这些前提条件的满足确保了你可以顺利地搭建和运行 Fabric 区块链网络 并进行链码的开发 部署和执行 安装 Docker 确保系统上已经安装了 Docker 并且 Docker 服务正在运行 Docker Fabric
  • [MySQL]MySQL内置函数

    MySQL MySQL内置函数 文章目录 MySQL MySQL内置函数 1 日期函数 2 字符串函数 3 数学函数 4 其他函数 1 日期函数 常用日期函数如下 函数名称 描述 current date 获取当前日期 current ti
  • Trick: QSplashScreen中设置其他控件,并控制其大小

    方案一 使用qss设置控件内容 无法自适应窗口大小 方案二 在代码中通过QFont等类 代码直接设置大小 大小可为变量 窗口跳转时出现控件闪动 大小变动 方案Final 在代码中使用setStyleSheet 设置控件 QFont font
  • css文本输入框的样式

    form control display block width 100 height 34px padding 6px 12px font size 14px line height 1 428571429 color 555555 ve
  • 怎样为std::map的自定义key提供比较操作(一)

    stl的关联容器 map set 的key一般要求提供 lt 比较操作 假设我们有一个结构SomeKey struct SomeKey int a b 要想以SomeKey作为std map的key 需要为这个结构提供operator lt
  • (1)海思Hi3531DV100开发环境搭建

    海思Hi3531DV100开发环境搭建 1 本方案在linkpi开发板Hi3531Dv100上测试 一 安装SDK 1 Hi3531DV100 SDK包位置 在 Hi3531DV100 V100R001 01 software board
  • the resource is not on the build path of a java project错误

    在eclipse中 使用mavenimport了一个工程 但是在对某一个类进行F3 F4 ctrl alt H操作的时候报错 the resource is not on the build path of a java project 这
  • Windows下libcurl的配置(笔记)

    第一步 下载curl源代码 在官网就能下到 第二部 解压文件 进入文件新建一个名为build的目录 在build目录中新建一个install目录用于存放安装文件 第三部 用CMAKE配置 没有可以去下载 第四部 点击Configure之后
  • 前端搭建猜数字游戏(内附源码)

    The sand accumulates to form a pagoda 写在前面 功能介绍 页面搭建 样式设置 逻辑部分 完整代码 写在前面 上周我们实通过前端基础实现了打字通 当然很多伙伴再评论区提出了想法 后续我们会考虑实现的 今天
  • 传值、传址、引用的介绍及各自的优缺点

    1 传值 1 1 概念 void Swap int left int right int temp left left right right temp int main int a 10 int b 20 cout lt lt a lt
  • Angular8 环境搭建

    Angular 环境搭建 无论使用什么前端框架 都必然使用到NodeJS工具 Angular也不例外 Angular采用的是 全家桶 式的设计思路 因此 angular cli脚手架工具集成了日常开发需要使用到的所有NodeJS模块 使用
  • Java实现压缩解压文件

    关键词 ZipOutputStream ZipInoutStream 最近在工作中有需求需要在浏览器中一次性下载多个文件 于是想到了使用压缩的功能 百度了一下 发现很多博客的内容都大致相同 不太方便使用 于是自己写了这么一个工具类 使用JD
  • 什么是视图,视图的创建、删除、使用?

    什么是视图 视图是一张虚拟的表 视图与数据库中存在的表不太相同 之前我们创建的表都是包含数据的 如用户信息订单信息 然而视图是不包含数据的 举例 查询王五的所有订单的情况 王五本身要从用户表user进行查找 王五有很多订单要用订单表中进行查
  • git文件存放结构

    该思维导图是自己在整理笔记的时候 发现内容不全 但属于有略微用处的 上传csdn作为笔记存档 主要包括git的几种数据类型blob tree commit 以及git中常用到的合并策略 pdf地址 https download csdn n
  • 算法与数据结构_链表

    链表 一 理解指针或引用的含义 含义 将某个变量 对象 赋值给指针 引用 实际上就是就是将这个变量 对象 的地址赋值给指针 引用 示例 p gt next q 表示p节点的后继指针存储了q节点的内存地址 p gt next p gt nex