day03:一文完全吃透链表基础:单双链表(虚拟头尾节点)、关键问题总结

理论基础

什么是链表,链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向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节点就可以了。

那有同学说了,D节点不是依然存留在内存里么?只不过是没有在这个链表里而已。

是这样的,所以在C++里最好是再手动释放这个D节点,释放这块内存。

其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。

#添加节点

可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。

但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。

数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。

链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。

移除链表元素

203. 移除链表元素 - 力扣(LeetCode)

思路过程

1.利用虚拟头结点记录头结点的位置

2.利用前驱节点pre,避免再次遍历

在删除时,利用pre.next=node.next即可完美跳过node节点,达到删除node的作用

代码

public class remove_linked_list_elements {
    //链表定义:
    class ListNode{
        int val;
        ListNode next;
        public ListNode(){

        }
        //有参构造
        public ListNode(int val,ListNode next){
            this.val=val;
            this.next=next;
        }
    }
    //给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
    public ListNode removeElements(ListNode head, int val) {
        //如果链表为空
        if(head==null){
            return head;
        }
        //首先遍历链表咯?
        //首先来个虚拟头结点,防止丢失头结点
        ListNode dummy=new ListNode();
        dummy.next=head;
        //来个前节点
        ListNode pre=new ListNode();
        pre=dummy;
        while (head!=null){
            //判断当前节点 是否为val
            if(head.val==val){
                ListNode next=head.next;
                pre.next=next;
                head=head.next;
                continue;
            }
            head=head.next;
            pre=pre.next;
        }
        return dummy.next;
    }
}

设计链表

707. 设计链表 - 力扣(LeetCode)

单链表

代码

package LinkedList;

//设计链表
public class design_linked_list {
    //这里使用单链表
    public static class MyLinkedList {
        //定义节点 单链表使用ListNode
        class ListNode{
            int val;
            ListNode next;
            public ListNode(int val){
                this.val=val;
            }
        }
        //这里去记录虚拟头节点,注意这里并不是第一个元素,这个头结点是空点
        ListNode dummy;
        //记录单链表长度
        int size=0;
        public MyLinkedList() {
            this.size=0;
            //todo:1.这里去给虚拟头节点赋值,不然会导致不能使用dummy.next 因为dummy是null时不能使用.next
            this.dummy=new ListNode(0);
        }
        // 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。
        public int get(int index) {
            //先判断是不是超出链表
            //这里因为当链表有一个元素时,链表size=1,而index下标是0,所以index=size时,链表也没有该下标
            if(index>=size) {
                return -1;
            }
            //一般这里是先去获取一个值,避免对虚拟头节点直接进行操作,保护虚拟头结点
            ListNode cur=dummy; //todo:2、这里去copy dummy 而不是next
            //遍历达到index节点
            for (int i = 0; i <=index; i++) {
                cur=cur.next;
            }
            size++;
            return cur.val;
        }
        // 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
        public void addAtHead(int val) {
            ListNode node=new ListNode(val);
            //首先判断链表是否为空
            if(size==0){
                dummy.next=node;
                size++;
                return;
            }
            //非空
            //先记录原先的头结点
            ListNode head=dummy.next;
            dummy.next=node;
            node.next=head;
            size++;
            return;
        }
        // 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
        public void addAtTail(int val) {
            ListNode node=new ListNode(val);
            //首先判断链表是否为空
            if(size==0){
                dummy.next=node;
                size++;
                return;
            }
            //去寻找原先的尾节点
            ListNode cur=dummy;
            while (cur.next!=null){
                cur=cur.next;
            }
            //现在这里的cur就是尾节点
            cur.next=node;
            size++;
            return;
        }
        //将一个值为 val 的节点插入到链表中下标为 index 的节点之前。
        // 如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。
        // 如果 index 比长度更大,该节点将 不会插入 到链表中。
        public void addAtIndex(int index, int val) {
            //如果index=长度
            if(index==size){
                addAtTail(val);
                return;
            }
            if(index>size){
                return;
            }
            //现在下面是正常插入
            ListNode node=new ListNode(val);
            ListNode cur=dummy;
            //这里i是
            for (int i = 0; i < index; i++) {
                cur=cur.next;
            }
            //原先的下一节点
            ListNode curNext=cur.next;
            cur.next=node;
            node.next=curNext;
            size++;
            return;
        }
        //如果下标有效,则删除链表中下标为 index 的节点。
        public void deleteAtIndex(int index) {
            if(index<0||index>=size){
                return;
            }
            ListNode cur=dummy;

            //找到下标为index-1的节点 即index是他的下一节点
            for (int i = 0; i < index; i++) {
                cur=cur.next;
            }
            ListNode indexNext=cur.next.next;
            cur.next=indexNext;
            size--;
            return;
        }

    }

    public static void main(String[] args) {
          MyLinkedList obj = new MyLinkedList();
             int param_1 = obj.get(0);
             obj.addAtHead(1);
             obj.addAtTail(3);
             obj.addAtIndex(1,2);
             obj.deleteAtIndex(1);
    }
}

关键点

1.这里去给虚拟头节点赋值,不然会导致不能使用dummy.next 因为dummy是null时不能使用.next

public MyLinkedList() {
    this.size=0;
    //todo:1.这里去给虚拟头节点赋值,不然会导致不能使用dummy.next 因为dummy是null时不能使用.next
    this.dummy=new ListNode(0);
}

2.在进行操作时,这里直接去复制dummy即可

//一般这里是先去获取一个值,避免对虚拟头节点直接进行操作,保护虚拟头结点
ListNode cur=dummy; //todo:2、这里去copy dummy 而不是next

3.注意边界条件(注意模拟) 这里的i从0达到下标为index时,应该循环<=index,可以在纸上模拟

//遍历达到index节点
for (int i = 0; i <=index; i++) {
    cur=cur.next;
}

4.关于链表和节点的定义不要搞混

节点的定义:

class ListNode{
    int val;
    ListNode next;
    public ListNode(int val){
        this.val=val;
    }
}

链表的定义

public static class MyLinkedList {
    //定义节点 单链表使用ListNode
    class ListNode{
        int val;
        ListNode next;
        public ListNode(int val){
            this.val=val;
        }
    }
    //这里去记录虚拟头节点,注意这里并不是第一个元素,这个头结点是空点
    ListNode dummy;
    //记录单链表长度
    int size=0;
    //链表的无参构造
    public MyLinkedList() {
        this.size=0;
        //todo:1.这里去给虚拟头节点赋值,不然会导致不能使用dummy.next 因为dummy是null时不能使用.next
        this.dummy=new ListNode(0);
    }

双链表

代码

public static class MyLinkedList_double{
    //节点的定义[与单链表比多了一个前驱节点,单链表只有后继next节点]
    class ListNode{
        int val;
        // 这里展示不同 这里展示前驱节点 后继节点
        ListNode pre,next;
        public ListNode(int val){
            this.val=val;
            this.pre=null;
            this.next=null;
        }
    }
    //虚拟头节点
    ListNode dummyHead;
    //虚拟尾节点
    ListNode dummyTail;
    //todo:定义虚拟头尾节点的用处
    int size;
    //链表的空参构造
    public MyLinkedList_double(){
        this.size=0;
        this.dummyHead=new ListNode(0);
        this.dummyTail=new ListNode(0);
        this.dummyHead.next=dummyTail;
        this.dummyTail.pre=dummyHead;
    }
    // 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。
    public int get(int index) {
        if(index>=size){
            return -1;
        }
        ListNode cur=dummyHead;
        //这里最终得到的是下标为index的节点
        for (int i = 0; i <=index; i++) {
            cur=cur.next;
        }
        return cur.val;
    }
    // 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
    public void addAtHead(int val) {
        ListNode node=new ListNode(val);
        //原先的头节点
        ListNode head=dummyHead.next;
        dummyHead.next=node;
        node.pre=dummyHead;
        node.next=head;
        head.pre=node;
        size++;
    }
    // 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
    public void addAtTail(int val) {
        ListNode node=new ListNode(val);
        //原来的尾节点
        ListNode tail=dummyTail.pre;
        tail.next=node;
        node.pre=tail;
        node.next=dummyTail;
        dummyHead.pre=node;
        size++;

    }
    //将一个值为 val 的节点插入到链表中下标为 index 的节点之前。
    // 如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。
    // 如果 index 比长度更大,该节点将 不会插入 到链表中。
    public void addAtIndex(int index, int val) {
        ListNode node=new ListNode(val);
        if(size==index){
            addAtTail(val);
            return;
        }
        if(index>size){
            return;
        }
        ListNode cur=dummyHead;
        //todo:这里最终得到的是下标为index的节点
        for (int i = 0; i <= index; i++) {
            cur=cur.next;
        }
        //得到原先的index的前驱节点
        ListNode oldPre=cur.pre;
        oldPre.next=node;
        cur.pre=node;
        node.pre=oldPre;
        node.next=cur;
        size++;
    }
    //如果下标有效,则删除链表中下标为 index 的节点。
    public void deleteAtIndex(int index) {
        if(index<0||index>=size){
            return;
        }
        ListNode cur=dummyHead;
        for (int i = 0; i <= index; i++) {
            cur=cur.next;
        }
        cur.pre.next=cur.next;
        cur.next.pre=cur.pre;
        size--;
    }

}

关键点

特点:有虚拟头尾节点、ListNode中有前驱和后继节点

1.双链表的好处就是:在添加首节点和尾节点时,不用考虑size=0的特殊情况,因为已经定义了虚拟头尾节点,不会出现为null的情况

2.虚拟头尾节点(哨兵节点)的作用

  1. 简化边界操作:避免在链表头部或尾部插入/删除时需要特殊处理,统一操作逻辑。
  2. 防止空指针异常:确保链表始终有头尾节点,即使链表为空,操作也不会出错。
  3. 代码更简洁:减少条件判断,提升代码可读性和可维护性。

核心思想:用两个不存储实际数据的节点(dummyHeaddummyTail)作为链表的边界,让所有操作都在“内部节点”上进行。

各种节点的作用

1.虚拟头节点dummy

定义了这个后,方便对head进行操作,防止head移动后,找不到头结点的位置【比如在进行循环时,head=head.next】这样子就会导致原来的头结点找不到了,但是在操作前,让ListNode cur=dummy 将dummy节点复制给cur,由于dummy是虚拟头节点,所以当cur的next指针变了,dummy也变了

2.前驱节点pre

在单链表中定义了这个,在每次移动时记录这个前节点pre,当前节点node,在进行操作时就可以避免再遍历一次

3.在单链表代码中,记得不要直接对head进行操作,而是将head赋值给cur,再对cur进行操作即可

ListNode cur=head;
//后续对cur进行操作,保留head

你可能感兴趣的:(算法学习之旅,链表,数据结构)