代码随想录算法训练营Day3|链表part01

78 阅读5分钟

链表理论基础

文档参考:programmercarl.com/%E9%93%BE%E…

  • 一种用指针串联在一起的线性结构
  • 节点 = 数据域 + 指针域
  • 分类
    • 单链表 Untitled 13.png

    • 双链表 Untitled 1 7.png

    • 循环链表 Untitled 2 5.png

  • 存储方式:散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理

链表的定义

public class ListNode {
    // 结点的值
    int val;

    // 下一个结点
    ListNode next;

    // 节点的构造函数(无参)
    public ListNode() {
    }

    // 节点的构造函数(有一个参数)
    public ListNode(int val) {
        this.val = val;
    }

    // 节点的构造函数(有两个参数)
    public ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
}

链表的操作

链表中增删节点操作的时间复杂度都为O(1)O(1)

删除节点

Untitled 3 3.png 删除D节点,只需要把C节点的next指针指向E Java有内存回收机制,不用手动释放D节点空间

添加节点

Untitled 4 3.png

  1. 要插入的节点F的next指针指向插入位置节点D
  2. 插入位置前的节点C的next指针指向要插入的节点F

链表与数组的比较

插入/删除查询适用场景
数组O(n)O(n)O(1)O(1)数据量固定,较少增删,频繁查询
链表O(1)O(1)O(n)O(n)数据量不固定,频繁增删,较少查询

LeetCode 203 移除链表元素

题目链接:leetcode.cn/problems/re…

文档讲解:programmercarl.com/0203.%E7%A7…

思路

题目要求移除值为val的所有节点,且要返回修改后链表的头节点。考虑到头节点也可能被更改,且删除操作与其他节点不同,可以引入虚拟头节点dummyHead作为头节点的前序节点,也是一个始终不变的头节点。

虚拟头节点

在链表中,操作当前节点必须找前一个结点,而头节点没有前序节点,这就造成了头结点的特殊性。设置虚拟头节点后,头节点操作与其他节点无异,简化代码。 返回头节点时,要返回dummyHead的后继,之前的head可能已经无效

难点

在删除某个节点后,指针会指向下一个新节点。此时两个指针都不应该再移动,需要继续检查当前节点。只有节点不需要被删除时,两个指针才都需要向前移动

LeetCode 707 设计链表

题目链接:leetcode.cn/problems/de…

文档讲解:programmercarl.com/0707.%E8%AE…

思路

由于之前练习过设计单链表,这次设计一下双链表 MyLinkedList类:属性head,记录链表的头节点 MyListNode类:属性val,prev,next记录节点的值与前后继

初始化链表MyLinkedList()

把属性head初始化为空

根据索引读get(int val)

读节点的值时,头节点操作与其他节点并无分别,无需使用虚拟头节点 只需把指针向后移动到索引位置读取

难点

题设说明可能存在无效的索引输入。读值需要操作的节点即为当前指针,所以循环中需要添加指针是否为空的判断条件 如果指针为空,直接返回。

在链表头部插入节点addAtHead(int val)

头插节点的操作并无特殊性,故不使用虚拟头节点

  1. 新建节点
  2. 把节点node的next指针指向现在的头节点
  3. 如果头节点不为空,将其prev节点指向新节点node
  4. 把新节点node置为头节点

在链表尾部插入节点addAtTail(int val)

在尾部插入节点,同样有可能插入的是头节点,故引入虚拟头节点以简化代码

⚠️注意:双链表引入虚拟头节点时,要检查头节点是否为空,再将头节点的prev指向虚拟头节点

  1. 引入虚拟头节点
  2. 找到链表的最后一个节点
  3. 把新节点插入在最后一个节点后
  4. 还原头节点

根据索引添加节点addAtIndex(int index, int val)

索引指定位置可能为头节点,故引入虚拟头节点

  1. 引入虚拟头节点
  2. 循环找到插入位置,注意空指针
  3. 把新节点插入
  4. 如果不是插入在末尾,需要改变后继节点的prev指针
  5. 还原头节点
难点

插入索引值可能无效,因此在寻找插入位置时要检查指针是否为空。

根据索引删除节点deleteAtIndex(int index)

索引指定位置可能为头节点,故引入虚拟头节点

  1. 引入虚拟头节点
  2. 循环找到要插入位置的前序节点
  3. 删除元素。注意元素的后继非空才需要修改prev指针
  4. 还原头节点

LeetCode 206 反转链表

题目链接:leetcode.cn/problems/re…

文档讲解:programmercarl.com/0206.翻转链表.h…

视频讲解:www.bilibili.com/video/BV1nB…

思路

太久没做,翻了代码随想录想起来只需要改变next指针指向 考虑把一个链表整个反转:

  1. 把第一个节点指向自己的前继
  2. 反转从第二个节点开始的子链表 故我们可以使用递归
  3. 递归函数的参数和返回值 链表的头节点,无需返回值 全局变量prev,保存当前head的前一个节点。
  4. 递归结束条件 head为空,直接返回(处理链表为空的特殊情况) 如果head.next为空,只需要把head.next指针置为prev,之后返回。(这样head最后是落在最后一个节点,这样返回的就是新链表的头节点)
  5. 每层递归的逻辑 先保存head.next为next 将head.next链表指向prev 更新prev为head 把next传入递归函数进行下一轮递归

难点

在递归中搞明白每一层修改了那些指针,要保存哪些信息。 每一层都修改的是当前head的next指针,那么就需要预留下一个节点和上一个节点。

今日收获总结

今天学习4小时,链表设计细节很多折磨了很久,反转链表的思路也没有及时想出来,以后要多练