代码随想录算法训练营Day03 | 链表理论基础、leetcode203. 移除链表元素、leetcode707. 设计链表、leetcode206. 反转链表

编程入门 行业动态 更新时间:2024-10-22 18:45:08

Day03

  • 链表理论基础
    • 链表的类型
      • 单链表
      • 双链表
      • 循环链表
    • 链表的存储方式
    • 链表的定义
    • 链表的操作
      • 删除节点
      • 添加节点
    • 性能分析
  • leetcode203. 移除链表元素
  • leetcode707. 设计链表
  • leetcode206. 反转链表

链表理论基础

链表的类型

单链表

链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点也就是head。
如图所示:

双链表

双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表 既可以向前查询也可以向后查询。
如图所示:

循环链表

循环链表,就是链表首尾相连。
循环链表可以用来解决约瑟夫环问题。

链表的存储方式

数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。
所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
如图所示:

这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。

链表的定义

手写链表

// 单链表
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;
    }
}

链表的操作

删除节点

删除D节点,如图所示:

这里只需要将C节点的next指针指向E节点就可以了。
Java、Python有自己的内存回收机制,不用自己手动释放这个D节点内存。

添加节点

添加F节点,如图所示:

可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。
但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。

性能分析

链表的特性和数组的特性进行一个对比,如图所示:

数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。

leetcode203. 移除链表元素

这题没按照随想录的做法来实现,一开始不知道怎么在本地IDE上创建链表,然后问了下ChatGPT就知道怎么创建链表并赋值的,下面有我IDE的代码。
注意点:

  1. 构造并赋值一个单链表/双链表,这需要熟记于心;
  2. 遍历链表每个节点前,需要弄清楚所有特殊情况,提前做好特判操作(排除空链表、头结点值为val等特殊情况);
  3. 遍历链表时,比较的是当前节点的下一个节点的值(cur.next.val),类似于指针,目的是便于进行删除或添加的操作;
// 构造一个单链表
class ListNode {
    // 节点的值
    int val;
    // 下一个节点
    ListNode next;
    // 节点的构造函数(1个参数)
    public ListNode(int val) {
        this.val = val;
        this.next = null;
    }
}

public class leetcode203 {
    public static ListNode removeElements(ListNode head, int val) {
        // 特判:如果头节点的值等于待删除的值,则直接跳到下一个节点
        while (head != null && head.val == val) {
            head = head.next;
        }
        // 特判:如果头节点的值为空(即待遍历的链表为空链表),则直接返回null
        if (head == null) {
            return null;
        }
        // 然后再从头开始遍历,每次判断下一个节点的值是否与待删除值相等
        ListNode cur = head;
        while (cur != null && cur.next != null) {
            if (cur.next.val == val) {
                cur.next = cur.next.next;
            } else {
                cur = cur.next;
            }
        }
        return head;
    }

    // 在main方法里调用
    public static void main(String[] args) {
        // 定义题目示例的链表 head
        //int[] nums = {1,2,6,3,4,5,6};
        //int[] nums = {};
        int[] nums = {6,6,6,6};
        ListNode head = new ListNode(nums[0]);
        ListNode cur = head;
        for (int i = 1; i < nums.length; i++) {
            cur.next = new ListNode(nums[i]);
            cur = cur.next;
        }
        int val = 6;
        // 调用函数
        head = removeElements(head, val);
        // 输出链表
        cur = head;
        while (cur != null) {
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
    }
}

leetcode707. 设计链表

这题做得蛮久的,主要在于深入理解链表的各种基础操作,也就是设计链表的五个接口:

  • int get(int index):获取链表第 index 个节点的数值
  • void addAtHead(int val):在链表的最前面插入一个节点
  • void addAtTail(int val):在链表的最后面插入一个节点
  • void addAtIndex(int index, int val):在链表第index个节点前面插入一个节点
  • void deleteAtIndex(int index):删除链表的第index个节点

这里建议一边写代码的时候一边在草稿纸上画图来理解,比如我就在写获取、插入和删除操作的代码那时候画了一个链表的结构图来理解,方便理解怎么找到第index个节点以及其前驱节点。

还有在链表的头节点前画了个虚拟节点dummyHead = 0,可以时刻提醒我一开始构造链表时有个虚拟节点需要注意,并方便我在写插入操作的代码时能很好地理解如何在头节点前添加一个节点,形成新头节点;还在尾节点后画了个null“节点”,表示尾节点指向null“节点”,以便我在写插入操作的代码时能很好地理解如何在尾节点后添加一个节点,形成新尾节点。

注意点:

  1. 初始化链表的操作也要熟悉;size, head
  2. 虚拟头节点,时刻注意;
  3. 写一个接口时可以调用别的接口(比如addAtHead和addAtTail接口里调用了addAtIndex接口),能提高代码复用性,降低代码冗余;
  4. 前驱节点pre,插入和删除操作都要用到;
  5. 边写代码边画图,事半功倍。
// 单链表
class ListNode {
    int val;
    ListNode next;
    ListNode(int val) {
        this.val = val;
        this.next = null;
    }
}

class MyLinkedList {
    // 定义存储链表元素的个数
    int size;
    // 定义虚拟头结点(名字不重要,dummyHead或者head都行)
    ListNode head;

    // 初始化链表
    public MyLinkedList() {
        size = 0;
        head = new ListNode(0);
    }

    // 获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点
    public int get(int index) {
        // 如果 index 非法,返回 -1
        if (index < 0 || index >= size) {
            return -1;
        }
        ListNode cur = head;
        // 因为包含一个虚拟头节点head,所以是要查找第 index+1 个节点
        for (int i = 0; i <= index; i++) {
            cur = cur.next;
        }
        return cur.val;
    }

    // 在链表最前面插入一个节点,等价于在第0个元素前添加
    public void addAtHead(int val) {
        addAtIndex(0, val);
    }

    // 在链表最后面插入一个节点,等价于在(末尾+1)个元素前添加
    public void addAtTail(int val) {
        addAtIndex(size, val);
    }

    // 在第 index 个节点之前插入一个新节点
    // 如果 index 为 0,则说明新插入的节点为链表的新头节点
    // 如果 index 为链表的长度 size,则说明新插入的节点为链表的新尾结点
    public void addAtIndex(int index, int val) {
        // 如果 index 大于链表的长度,则返回空
        if (index > size) {
            return;
        }
        // 如果 index 小于 0,等同于在头部插入节点(先将index赋值为0)
        if (index < 0) {
            index = 0;
        }
        // 此时确定要插入一个节点了,链表长度先自增一个单位
        size++;
        // 找到要插入节点的前驱节点(记得有个虚拟头结点)
        ListNode pre = head;
        for (int i = 0; i < index; i++) {
            pre = pre.next;
        }
        // 插入操作:先让插入节点指向前驱节点的下一个节点,再让前驱节点指向插入节点
        ListNode toAdd = new ListNode(val);
        toAdd.next = pre.next;
        pre.next = toAdd;
    }

    // 删除第 index 个节点
    public void deleteAtIndex(int index) {
        // 如果 index 小于 0 或者大于等于链表的长度,则直接返回
        if (index < 0 || index >= size) {
            return;
        }
        // 此时确定要删除一个节点了,链表长度先自减一个单位
        size--;
        // 找到要插入节点的前驱节点(记得有个虚拟头结点)
        ListNode pre = head;
        for (int i = 0; i < index; i++) {
            pre = pre.next;
        }
        // 删除操作
        pre.next = pre.next.next;
    }
}

leetcode206. 反转链表

这题也是思路一下就有,但不知道具体如何实现,还是写代码少了,应该每天都写算法题的,前段时间由于实验室科研项目导致一直没看算法题。

注意点:
1.首先应该申请节点:pre(初始化为null) 和 cur(指向头结点),以及临时变量 tmp(保存cur.next);
2. 循环逻辑要弄清晰,最好在一旁画个草稿图帮助自己理解,运行一两个循环试试看;
3. 最后的循环结束条件以及返回值也要弄清楚,建议画图理解;
4. 个人感觉循环中的操作过程类似于数组中的交换元素,可以做类比学习。

class Solution {
    // 双指针法
    public ListNode reverseList(ListNode head) {
        // 申请节点: pre 和 cur, pre 指向 null
        ListNode pre = null;
        ListNode cur = head;
        ListNode tmp = null;
        // 循环遍历
        while (cur != null) {
            // 记录当前节点的下一个节点
            tmp = cur.next;
            // 然后将当前节点指向 pre
            cur.next = pre;
            // pre 和 cur 节点都前进一位
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
}

递归的两个条件:
终止条件是当前节点或者下一个节点==null
在函数内部,改变节点的指向,也就是 head 的下一个节点指向 head 递归函数那句。

递归法中,我们应该关心的主要有三点:
返回值:指向改变的节点,也就是每次递归的最后一个节点
调用单元f(x)做了什么:改变节点head的指向(调用单元f(x):head.next 指向 head,也就是最后一个节点cur指向其前一个节点head)
终止条件:head 为空指针或者 head.next 为空指针,也就是当前为空或者下一个节点为空;

class Solution {
    public ListNode reverseList(ListNode head) {
        // 递归终止条件: 当前为空,或者下一个节点为空
        if (head == null || head.next == null) {
            return head;
        }
        // 进行递归(cur是返回值:指向改变的节点)
        ListNode cur = reverseList(head.next);
        // 这里进行节点指向改变(调用单元f(x):head.next 指向 head,也就是最后一个节点cur指向其前一个节点head)
        head.next.next = head;
        // 防止链表循环,需要将head.next设置为空
        head.next = null;
        // 返回节点(每层递归函数都返回cur,也就是最后一个节点)
        return cur;
    }
}

更多推荐

代码随想录算法训练营Day03 | 链表理论基础、leetcode203. 移除链表元素、leetcode707. 设计链表、leetcode206. 反转链表

本文发布于:2023-06-13 18:30:00,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1389331.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:链表   算法   移除   训练营   理论基础

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!