代码随想录算法训练营day3|链表理论基础、203.移除链表元素、707.设计链表、206.反转链表

1 链表理论基础

1.1 单链表

链表是一种通过指针串联在一起的线性结构,每一个结点都由两部分组成,一个是数据域 一个是指针域(指针域用来存放指向下一个结点的指针),最后一个结点的指针域指向null(空指针)。
链表的入口结点称为链表的头节点也就是head。
在这里插入图片描述

1.2 双链表

单链表中的指针域只能指向节点的下一个节点。
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表 既可以向前查询也可以向后查询。
在这里插入图片描述

1.3 循环链表

循环链表,顾名思义,就是链表首尾相连。
循环链表可以用来解决约瑟夫环问题。
在这里插入图片描述

1.4 链表的存储方式

链表在内存中的存储方式:数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。链表是通过指针域的指针链接在内存中各个节点。所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
请添加图片描述
这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。

1.5 链表的定义

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

1.6 链表的操作

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

链表和数组特性对比:
请添加图片描述
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。、
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。

203、 移除链表元素

题意:删除链表中等于给定值 val 的所有节点。
示例 1: 输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]

示例 2: 输入:head = [], val = 1 输出:[]

示例 3: 输入:head = [7,7,7,7], val = 7 输出:[]

以链表 1 4 2 4 来举例,移除元素4。
请添加图片描述
请添加图片描述
在链表中,移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。
所以头结点如何移除呢,其实只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点
请添加图片描述
其实可以设置一个虚拟头结点,这样原链表的所有节点就都可以按照统一的方式进行移除了。
来看看如何设置一个虚拟头。依然还是在这个链表中,移除元素1。

请添加图片描述

removeElements 方法,用于从链表中删除所有值为val的节点。
这个方法:先处理链表头部可能存在的值为val的结点,然后遍历剩余的链表,删除所有匹配的结点。
步骤 1:处理链表头部可能的 val 节点

while (head != null && head.val == val) {
    head = head.next;
}

这部分的目的是 处理链表头部 如果头节点的值是 val 的情况。假设我们有一个链表:

val -> 1 -> 2 -> val -> 3

在这种情况下,如果头节点的值是 val,我们希望移除它。head = head.next 会将 head 移动到下一个节点,从而跳过当前的节点。如果头节点的值不是 val,while 循环会结束,进入下一步。
为什么需要这一步?
假设链表的头部有多个 val,例如 val -> val -> 1 -> 2,而 val 节点需要被删除。如果不先处理头部的 val 节点,遍历将会错过这些节点。
步骤 2:遍历链表删除所有 val 节点

ListNode curr = head;
while (curr != null && curr.next != null) {
    if (curr.next.val == val) {
        curr.next = curr.next.next;
    } else {
        curr = curr.next;
    }
}

  • 初始化 curr:curr 指针被初始化为 head,也就是说,我们从链表的第一个节点开始遍历。
  • 遍历链表:while (curr != null && curr.next != null) 这个条件判断保证了我们不会在遍历到链表尾部时发生空指针异常。
    判断 curr.next.val == val:如果当前节点 curr 的下一个节点 curr.next 的值是 val,那么我们就把 curr.next 指向 curr.next.next,这样就删除了当前节点的下一个节点。
    否则,继续遍历:如果当前节点 curr.next 的值不是 val,那么我们就将 curr 移动到下一个节点,继续检查下一个节点。
    为什么不直接删除 curr 节点?
    删除节点时,我们不能直接修改 curr,因为这样会使得 curr 失去对当前节点的引用,导致链表丢失部分内容。正确的做法是通过修改 curr.next 来删除后续节点。
    步骤 3:返回新链表的头节点
return head;

一旦我们完成了所有节点的删除操作,返回修改后的链表头节点。因为链表可能被修改了,尤其是当链表头节点被删除时(如第一个 val 被删除),返回新的头节点是非常重要的。
总结:
处理头部节点:首先确保链表的头节点不包含需要删除的值。
遍历链表:然后遍历链表,删除所有值为 val 的节点。如果发现下一个节点的值是 val,就跳过它,否则继续遍历。
返回新头节点:遍历结束后返回新的链表头节点。

不带头节点:

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        ListNode dummy = new ListNode();//设置一个虚拟的头结点
        dummy.next =head;//讲头节点链接到该链表

        ListNode cur= dummy;//设置指针cur,方便后续移动指针  进行寻找value值为val的结点
        while(cur.next != null){ // 当cur.next为空时,说明遍历到链表的最后一个结点(cur指向最后一个结点)
            if(cur.next.val == val ){ //val值满足条件, 执行删除操作
                cur.next = cur.next.next;//删除操作通过使用修改指针
            }else{
                cur = cur.next;//不满足则移动cur指针,便于判断下一结点是否满足要求。
            }
        }
        return dummy.next;
    }
}

707.设计链表

在这里插入图片描述
这道题目设计链表的五个接口:
- 获取链表第index个节点的数值
- 在链表的最前面插入一个节点
- 在链表的最后面插入一个节点
- 在链表第index个节点前面插入一个节点
- 删除链表的第index个节点
链表操作的两种方式:

  • 直接使用原来的链表来进行操作。
  • 设置一个虚拟头结点在进行操作。

在这里插入图片描述
重点:第一个节点要从零开始,虚拟头结点不计数。即,在链表中,index = 0 对应的是 链表的第一个实际节点,即虚拟头节点的下一个节点。因为在 MyLinkedList 类中,使用了一个虚拟头节点,这个虚拟头节点的 val 为 0,实际上并不存储任何有效数据。
获取节点值:

public int get(int index) {
    if (index < 0 || index >= size) {//index >= size 的判断用于确定一个索引是否越界,     size 表示链表中当前有效节点的数量。
        return -1;  // 如果索引无效,返回 -1
    }
    ListNode currentNode = head;  // 从虚拟头节点开始
    for (int i = 0; i <= index; i++) {
        currentNode = currentNode.next;  // 遍历到指定节点
    }
    return currentNode.val;  // 返回该节点的值
}

当你使用 get(0) 时,返回的是 虚拟头节点后 的第一个节点的值,也就是链表的 第一个有效节点。虚拟头节点的 next 指向链表的第一个实际节点,因此 index = 0 对应的就是这个实际节点。
关于index >= size的判断:
假设我们有一个链表,包含 3 个有效节点:

head -> 1 -> 2 -> 3

此时,size = 3,意味着链表中有 3 个节点,索引范围是从 0 到 2(也就是 0 <= index <= 2)。如果我们尝试访问 index = 3,就会超出链表的有效范围,因为链表中并没有第 3 个节点。因此,index >= size 为 true 时,表示请求的索引无效,应该返回错误(例如返回 -1,或者直接返回不做任何操作)。

class ListNode{
    //定义单链表中的结点,每个结点有两个部分:val :存储结点的值;next:指向下一个结点的引用。
    int val;
    ListNode next;//指针
    //给该类设置了两个构造函数
    ListNode(){}//默认构造函数:没有参数,通常用来创建空结点。
    ListNode(int val){
        this.val=val;
    }//带参构造函数:用给定的val值初始化一个结点
}
class MyLinkedList {
    
    int size;// 记录链表的大小
    
    ListNode head;// 头节点

    public MyLinkedList() {
        //初始化该链表
        size = 0;
        head = new ListNode(0); // 使用虚拟头节点,值为0
    }
    
    public int get(int index) {// 获取第index个节点的数值,注意index是从0开始的,第0个节点就是虚拟头节点的下一个结点
        //判断index是否合法
        if(index < 0 || index >=size){
            return -1; // 如果索引无效,返回 -1
        }

        ListNode cur ;
        cur = head;
        for(int i = 0;i<=index;i++){//要获取的是第index结点的值,由于虚拟了一个头节点,所以实际要查找第index+1个结点。
            cur = cur.next;
        }
        return cur.val;
    }
    
    public void addAtHead(int val) {//在链表头部添加一个新节点
        ListNode newNode = new ListNode(val); 
        newNode.next = head.next;//新节点的 next 指向原链表的第一个有效节点
        head.next = newNode;// 虚拟头节点指向新节点
        size ++;//每当我们向链表添加新节点时,都需要增加链表的大小。size 记录了链表中有效节点的数量,因此需要将 size 增加 1。
    }
    
    public void addAtTail(int val) {
        ListNode newNode = new ListNode(val); 
        ListNode cur = head;
        while(cur.next != null){
            cur = cur.next;
        }
        cur.next =  newNode;
        size++;
    }//在遍历时我们从虚拟头节点开始,直到找到 next == null 的节点,即链表的尾部。
    
    public void addAtIndex(int index, int val) {
         if (index > size) {
            return;
        }
        if (index < 0) {
            index = 0;
        }
        size++;
        //找到要插入节点的前驱
        ListNode pred = head;
         for (int i = 0; i < index; i++) {
            pred = pred.next;
        }
         ListNode toAdd = new ListNode(val);
         toAdd.next = pred.next;
         pred.next = toAdd;
    }
    
    public void deleteAtIndex(int index) {
        if(index<0 || index >= size){
            return;// 如果索引无效,返回
        }
        size--;// 减少链表的大小
        //找到要删除结点所在位置的前一结点
        ListNode pred = head;
        for(int i=0;i<index;i++){
            pred = pred.next;
    
        }
        pred.next = pred.next.next;// 删除指定节点
    }
}

206.反转链表

题意:反转一个单链表。

示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL

如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。
其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:请添加图片描述

双指针解法

// 双指针
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode cur = head;
        ListNode temp = null;
        while (cur != null) {
            temp = cur.next;// 保存下一个节点
            cur.next = prev;
            prev = cur;
            cur = temp;
        }
        return prev;
    }
}

Java 中,必须显式地比较对象是否为 null。因此,while (cur) 会导致编译错误,正确的写法应该是 while (cur != null)。

递归解法

通过递归调用反转链表的剩余部分,将当前节点放置到已经反转链表的末尾,从而实现链表的反转。

// 递归 
class Solution {
    public ListNode reverseList(ListNode head) {
        return reverse(null, head);//结合双指针的这部分初始化信息,递归方法中的reverse,传递的形参分别是null(pre)和head(cur)
    }//调用reverse来翻转这个列表,考虑参数怎么写

    private ListNode reverse(ListNode prev, ListNode cur) {
        if (cur == null) {/结合双指针来看,循环终止条件:cur == null
            return prev;
        }
        ListNode temp = null;
        temp = cur.next;// 先保存下一个节点
        cur.next = prev;// 反转(改变方向)
        // 更新prev、cur位置
        // prev = cur;
        // cur = temp;
        //即进入下一层递归,传递形参   相当于双指针中的赋值操作  prev = cur; cur = temp;
        return reverse(cur, temp);//传递形参cur(pre) 和temp(cur)
    }
}
//双指针思路
cur=head;
pre=null;//结合这部分初始化信息,递归方法中的reverse,传递的形参分别是null(pre)和head(cur)
while(cur!=null){
temp=cur.next;
pre = cur;
cur = temp;
 }

总结:先熟练掌握双指针的解法,然后再去写递归代码,二者实际一一对应。

反转整个链表、反转链表中指定位置之间的节点。
反转结束后,从原来的链表上看:
pre指向反转这一段的末尾
cur指向反转这一段后续的下一个节点。

class Solution {
    public ListNode reverseBetween(ListNode head, int left, int right) {
        //根据反转整个链表
        //pre指向反转这一段的末尾
        //cur指向反转这一段后续的下一个节点

        //p0指的是要反转链表段的上一个节点
        //特殊情况:当left=1时,没有p0,所以这时需要考虑构建虚拟头结点
        ListNode dummyNode = new ListNode(0,head);
      
        ListNode p0 =  dummyNode;
        for(int i = 0;i < left - 1; i++){
            //到达要反转的这一链表的上一个节点
            p0 = p0.next;
        }
        ListNode pre = null;
        ListNode cur = p0.next;
        //接下来的过程就和上一题(反转整个链表一致了)
        for(int i = 0 ; i < right -left + 1; i++){
            ListNode temp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = temp;
        }
        
        p0.next.next = cur;
        p0.next= pre;
        return dummyNode.next;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值