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

链表理论基础

链表基础

链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,分别是数据域和指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。

链表的入口节点称为链表的头结点也就是head。

如图所示:

链表1

链表类型

单链表

上面介绍的就是单链表

双链表

单链表中的指针域只能指向节点的下一个节点。

双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。

双链表既可以向前查询也可以向后查询。

如图所示:

链表2

循环链表

循环链表,顾名思义,就是链表首尾相连。

循环链表可以用来解决约瑟夫环问题。

链表4

存储方式

数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。

链表是通过指针域的指针链接在内存中各个节点。

所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。

如图所示:

链表3

这个链表起始节点为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有自己的内存回收机制,不需要手动释放D节点的内存。

添加节点

如图所示:

链表-添加节点

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

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

性能分析

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

链表-链表与数据性能对比

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

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

203.移除链表元素

题目

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

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == 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
输出:[]

提示:

  • 列表中的节点数目在范围 [0, 104] 内
  • 1 <= Node.val <= 50
  • 0 <= val <= 50

思路

视频讲解:203.移除链表元素

代码随想录:移除链表元素

基本思路很简单,即遍历整个链表,当下一个结点的值等于val时,删除该元素。

需要额外考虑头节点的特殊情况:

  • 头节点为空

  • 头节点需要删除

此处涉及到链表的两种操作方式:

  1. 直接使用所给的链表进行操作
  2. 设置一个虚拟头节点后进行操作

操作1:

203_链表删除元素3

移除头节点和移除其他节点的操作不一样,因为移除链表的其他节点时,都是通过前一个节点来移除当前节点,而头结点没有前一个节点。

移除头节点时,只需要将头节点向后移动一位就可以:

203_链表删除元素4

使用该操作写代码时,需要单独写一段逻辑对头节点进行处理。

操作2:

203_链表删除元素6

在头节点之前设置一个虚拟头节点,就可以让链表中的所有元素都按统一的方式进行移除了。

最后返回值的时候需要返回dummyNode.next,即新的头节点

题解

不使用虚拟头节点:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 * int val;
 * ListNode next;
 * ListNode() {}
 * ListNode(int val) { this.val = val; }
 * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeElements(ListNode head, int val) {
        //先判断头节点是否需要删除
        //如果需要删除,将头节点后移
        while (head != null && head.val == val) {
            head = head.next;
        }
        //如果头节点为空,直接返回
        if (head == null) {
            return head;
        }
        //指针
        ListNode cur = head;
        //当指针的下一个节点不为空时,进入循环
        while (cur.next != null) {
            if (cur.next.val == val) {
                //删除元素
                cur.next = cur.next.next;
            } else {
                //指针后移
                cur = cur.next;
            }
        }
        return head;
    }
}

使用虚拟头节点:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 * int val;
 * ListNode next;
 * ListNode() {}
 * ListNode(int val) { this.val = val; }
 * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeElements(ListNode head, int val) {
        //创建虚拟头节点,指向head
        ListNode dummy = new ListNode();
        dummy.next = head;
        //当前指针
        ListNode cur = dummy;
        //所有节点统一操作
        while (cur.next != null) {
            if (cur.next.val == val) {
                cur.next = cur.next.next;
            } else {
                cur = cur.next;
            }
        }
        //返回虚拟头节点的下一个节点
        return dummy.next;
    }
}

707.设计链表

题目

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

你可以选择使用单链表或者双链表,设计并实现自己的链表。

单链表中的节点应该具备两个属性:valnextval 是当前节点的值,next 是指向下一个节点的指针/引用。

如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。

实现 MyLinkedList 类:

  • MyLinkedList() 初始化 MyLinkedList 对象。
  • int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1
  • void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
  • void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
  • void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。
  • void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。

示例:

输入
["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"]
[[], [1], [3], [1, 2], [1], [1], [1]]
输出
[null, null, null, null, 2, null, 3]

解释
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addAtHead(1);
myLinkedList.addAtTail(3);
myLinkedList.addAtIndex(1, 2);    // 链表变为 1->2->3
myLinkedList.get(1);              // 返回 2
myLinkedList.deleteAtIndex(1);    // 现在,链表变为 1->3
myLinkedList.get(1);              // 返回 3

代码模板:

class MyLinkedList {

    public MyLinkedList() {

    }
    
    public int get(int index) {

    }
    
    public void addAtHead(int val) {

    }
    
    public void addAtTail(int val) {

    }
    
    public void addAtIndex(int index, int val) {

    }
    
    public void deleteAtIndex(int index) {

    }
}

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * MyLinkedList obj = new MyLinkedList();
 * int param_1 = obj.get(index);
 * obj.addAtHead(val);
 * obj.addAtTail(val);
 * obj.addAtIndex(index,val);
 * obj.deleteAtIndex(index);
 */

思路

视频讲解:707.设计链表

代码随想录:设计链表

使用虚拟头节点设计单向链表。

  1. 设计Node类,类中有属性 val 和 next,同时设计构造方法
  2. 链表类中设计两个属性:有效节点数 size 以及虚拟头节点 dummyHead
  3. MyLinkedList():构造方法,设置 size=0 和 dummyHead
  4. get(int index):首先判断下标是否有效,下标无效返回-1,创建指针cur,根据index将指针向链表尾端移动,移动到index位置后返回cur.val
  5. addAtHead(int val):通过虚拟头节点直接添加,注意要size++
  6. addAtTail(int val):先将指针移动到尾端,再添加元素,注意size++
  7. addAtIndex(int index, int val):先判断下标是否有效,再将指针移动到index前一个元素位置,再添加元素,注意size++
  8. deleteAtIndex(int index):先判断下标是否有效,再将指针cur移动到index前一个元素位置,使用cur.next = cur.next.next删除元素,注意size–

题解

独立题解:

做题时脑子有点糊,可能写得比较乱

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

class MyLinkedList {
    int size;
    Node dummy;

    public MyLinkedList() {
        size = 0;
        dummy = new Node(0);
    }

    public int get(int index) {
        Node cur = dummy;
        while (cur.next != null && index > 0) {
            cur = cur.next;
            index--;
        }
        return cur.next == null ? -1 : cur.next.val;
    }

    public void addAtHead(int val) {
        Node head = new Node(val);
        head.next = dummy.next;
        dummy.next = head;
        size++;
    }

    public void addAtTail(int val) {
        Node tail = new Node(val);
        Node cur = dummy;
        while (cur.next != null) {
            cur = cur.next;
        }
        cur.next = tail;
        size++;
    }

    public void addAtIndex(int index, int val) {
        if (index <= size) {
            Node cur = dummy;
            while (cur.next != null && index > 0) {
                cur = cur.next;
                index--;
            }
            Node tmp = new Node(val);
            tmp.next = cur.next;
            cur.next = tmp;
            size++;
        }
    }

    public void deleteAtIndex(int index) {
        if (index < size) {
            Node cur = dummy;
            while (index > 0) {
                cur = cur.next;
                index--;
            }
            cur.next = cur.next.next;
            size--;
        }
    }
}

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * MyLinkedList obj = new MyLinkedList();
 * int param_1 = obj.get(index);
 * obj.addAtHead(val);
 * obj.addAtTail(val);
 * obj.addAtIndex(index,val);
 * obj.deleteAtIndex(index);
 */

206.反转链表

题目

206. 反转链表 - 力扣(LeetCode)

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例1:
在这里插入图片描述

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例2:
在这里插入图片描述

输入:head = [1,2]
输出:[2,1]

示例3:

输入:head = []
输出:[]

思路

视频讲解:206.反转链表

代码随想录:反转链表

移动过程如图(图中pre和cur移动顺序有误):

img

双指针法:

首先定义一个cur指针,指向头节点,再定义一个pre指针,初始化为null。

然后开始反转,为了防止反转后找不到原来的 cur.next,需要先用 tmp 指针保存 cur.next 节点。

然后继续移动 pre 和 cur 指针。

最后,cur 指针指向 null 时,循环结束,链表反转完毕。 此时 pre 指针指向新的头节点,return pre 即可。

递归法:

与双指针法逻辑相同,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。

主要难点是理解递归思想:详解递归思想-CSDN博客

递归的三大要素:

  1. 明确该递归函数的目的
  2. 寻找函数的结束条件
  3. 找到函数的等价关系式

在该题中运用递归思想:

  1. 明确函数目的为反转链表
  2. 递归函数结束条件:当前指针 cur 为空时,返回 pre
  3. 根据双指针法的逻辑找到递归函数的等价条件

从后向前递归:

详细思路参考题解注释

递归.gif

题解

双指针法:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 * int val;
 * ListNode next;
 * ListNode() {}
 * ListNode(int val) { this.val = val; }
 * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;
        while (cur != null) {
            ListNode tmp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
}

递归:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 * int val;
 * ListNode next;
 * ListNode() {}
 * ListNode(int val) { this.val = val; }
 * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        return reverse(null, head);
    }

    ListNode reverse(ListNode pre, ListNode cur) {
        if (cur == null) {
            return pre;
        }
        ListNode tmp = cur.next;
        cur.next = pre;
        return reverse(cur, tmp);
    }
}

从后向前递归:

// 从后向前递归
class Solution {
    ListNode reverseList(ListNode head) {
        // 边缘条件判断
        if(head == null) return null;
        if (head.next == null) return head;
        
        // 递归调用,翻转第二个节点开始往后的链表
        ListNode last = reverseList(head.next);
        // 翻转头节点与第二个节点的指向
        //最后一层返回head = 5,倒数第二层last = 5, head = 4
        //此处head.next.next -> 4.next.next -> 5.next -> 5.next=4
        head.next.next = head;
        // 此时的 head 节点为尾节点,next 需要从指向5变为指向 NULL
        head.next = null;
        return last;
    } 
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值