代码随想录算法训练营第三天|LeetCode 203.移除链表元素、707.设计链表、206.反转链表

203. 移除链表元素

给你一个链表的头节点 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

虚拟头结点法:

虚拟头节点是一个指向原头节点的额外节点,这样可以统一处理头节点和非头节点的删除操作。

算法步骤

  1. 初始化虚拟头节点:创建一个虚拟头节点 dummyHead,它的 next 指向原头节点 head
  2. 遍历链表:初始化,使用一个指针 curdummyHead 开始遍历链表。
  3. 删除操作:如果 cur.next 存在且其值等于 val,则删除 cur.next 节点,即将 cur.next 指向 cur.next.next。否则,移动 cur 到下一个节点。
  4. 返回结果:遍历完成后,返回 dummyHead.next,即新链表的头节点。

代码如下:

// 定义链表节点类
class ListNode {
    int val;                  // 节点的值
    ListNode next;           // 指向下一个节点的引用

    ListNode(int x) {       // 构造函数
        val = x;
        next = null;
    }
}

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        // 创建一个虚拟头节点,这样可以简化头节点的删除操作
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;
        ListNode cur = dummyHead;     // 使用cur指针遍历链表

        // 遍历链表,直到cur节点为空
        while (cur.next != null) {
            // 检查cur.next节点的值是否等于val
            if (cur.next.val == val) {
                // 如果等于val,删除这个节点
                ListNode toRemove = cur.next;
                cur.next = cur.next.next;  // 将cur的下一个节点指向要删除节点的下一个节点
                toRemove = null;          // 帮助垃圾收集器回收内存
            } else {
                // 如果不等于val,移动到下一个节点
                cur = cur.next;
            }
        }

        // 返回新链表的头节点,即dummyHead的下一个节点
        return dummyHead.next;
    }

    public static void main(String[] args) {
        Solution solution = new Solution();

        // 创建链表 1 -> 2 -> 6 -> 3 -> 4 -> 5 -> 6
        ListNode head = new ListNode(1);
        head.next = new ListNode(2);
        head.next.next = new ListNode(6);
        head.next.next.next = new ListNode(3);
        head.next.next.next.next = new ListNode(4);
        head.next.next.next.next.next = new ListNode(5);
        head.next.next.next.next.next.next = new ListNode(6);

        // 目标值 val
        int val = 6;

        // 调用函数并打印结果
        ListNode newHead = solution.removeElements(head, val);
        printList(newHead);  // 应该打印出 1 -> 2 -> 3 -> 4 -> 5
    }

    // 辅助函数,用于打印链表
    public static void printList(ListNode node) {
        while (node != null) {
            System.out.print(node.val + " -> ");
            node = node.next;
        }
        System.out.println("null");
    }
}

思考

  1. 为什么使用虚拟头节点?

在不使用虚拟头结点的情况下,如果链表的头结点需要被删除,我们就需要特殊处理这种情况,因为头结点来帮助我们重新链接链表。而使用虚拟头结点之后,就可以统一处理所有结点的删除操作。无论是头结点还是其他结点,都可以看作是相似结点。

栗子

  1. 初始状态

链表:1 -> 2 -> 6 -> 3 -> 4 -> 5 -> 6

  1. 步骤 1: 初始化虚拟头节点

我们创建一个虚拟头节点 dummyHead,它的 next 指向真正的头节点 1

dummyHead -> 1 -> 2 -> 6 -> 3 -> 4 -> 5 -> 6
  1. 步骤 2: 遍历链表

使用一个指针 curdummyHead 开始遍历链表。

  1. cur 指向 dummyHeadcur.next 指向 1

  2. cur.next.val 不等于 val,移动 cur1

  3. cur 指向 1cur.next 指向 2

  4. cur.next.val 不等于 val,移动 cur2

  5. cur 指向 2cur.next 指向 6

  6. cur.next.val 等于 val,删除 6cur.next 指向 3

  7. cur 指向 3cur.next 指向 4

  8. cur.next.val 不等于 val,移动 cur4

  9. cur 指向 4cur.next 指向 5

  10. cur.next.val 不等于 val,移动 cur5

  11. cur 指向 5cur.next 指向 6

  12. cur.next.val 等于 val,删除 6cur.next 指向 null

  13. 最终状态

链表:1 -> 2 -> 3 -> 4 -> 5

707. 设计链表

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

单链表中的节点应该具备两个属性: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

提示:

  • 0 <= index, val <= 1000
  • 请不要使用内置的 LinkedList 库。
  • 调用 getaddAtHeadaddAtTailaddAtIndexdeleteAtIndex 的次数不超过 2000

单链表法:每个节点包含两个属性:val(当前节点的值)和next(指向下一个节点的指针)

算法步骤

  1. 初始化:创建一个虚拟头节点 dummyHead,用于简化头节点的操作,同时记录链表的长度 size
  2. 获取节点值:通过遍历链表,检查给定的索引 index 是否有效,如果有效,则返回对应节点的值,否则返回 -1
  3. 在头部添加节点:在虚拟头节点 dummyHead 之后添加新节点,更新 dummyHead.next
  4. 在尾部添加节点:遍历链表找到最后一个节点,然后在其后添加新节点。
  5. 在指定位置添加节点:遍历链表找到指定位置的前一个节点,然后在其后添加新节点。
  6. 删除指定位置的节点:遍历链表找到指定位置的前一个节点,然后更新其 next 指针以跳过要删除的节点,并释放被删除节点的内存。

代码如下:

// 定义链表节点类
class ListNode {
    int val; // 节点存储的值
    ListNode next; // 指向下一个节点的引用

    // 构造函数,初始化节点的值和下一个节点
    ListNode(int x) {
        val = x;
        next = null;
    }
}

// 定义链表类
class MyLinkedList {
    private ListNode dummyHead; // 虚拟头节点,用于简化头节点的操作
    private int size; // 记录链表的长度

    // 初始化链表
    public MyLinkedList() {
        dummyHead = new ListNode(0); // 创建一个值为0的虚拟头节点
        size = 0; // 初始化链表长度为0
    }

    // 获取链表中第index个节点的值,如果索引无效,则返回-1
    public int get(int index) {
        if (index < 0 || index >= size) return -1; // 检查索引是否有效
        ListNode cur = dummyHead.next; // 从虚拟头节点的下一个节点开始遍历
        for (int i = 0; i < index; i++) {
            cur = cur.next; // 移动到指定索引的节点
        }
        return cur.val; // 返回找到的节点的值
    }

    // 在链表头部添加一个新节点,值为val
    public void addAtHead(int val) {
        ListNode newNode = new ListNode(val);
        newNode.next = dummyHead.next; // 新节点指向原来的第一个节点
        dummyHead.next = newNode; // 头节点指向新节点
        size++; // 链表长度加1
    }

    // 在链表尾部添加一个新节点,值为val
    public void addAtTail(int val) {
        ListNode newNode = new ListNode(val);
        ListNode cur = dummyHead; // 从虚拟头节点开始遍历
        while (cur.next != null) {
            cur = cur.next; // 移动到链表的最后一个节点
        }
        cur.next = newNode; // 将新节点添加到链表尾部
        size++; // 链表长度加1
    }

    // 在链表的第index个位置之前添加一个新节点,值为val
    public void addAtIndex(int index, int val) {
        if (index > size) return; // 如果索引大于链表长度,不执行操作
        ListNode newNode = new ListNode(val);
        ListNode cur = dummyHead; // 从虚拟头节点开始遍历
        for (int i = 0; i < index; i++) {
            cur = cur.next; // 移动到指定索引的位置
        }
        newNode.next = cur.next; // 新节点指向当前节点的下一个节点
        cur.next = newNode; // 当前节点指向新节点
        size++; // 链表长度加1
    }

    // 删除链表的第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.next = cur.next.next; // 将当前节点的下一个节点的下一个节点指向当前节点的下一个节点,实现删除操作
        size--; // 链表长度减1
    }
    public static void main(String[] args) {
        MyLinkedList linkedList = new MyLinkedList(); // 初始化链表
        linkedList.addAtHead(1); // 头部添加元素1
        linkedList.addAtTail(3); // 尾部添加元素3
        linkedList.addAtIndex(1, 2); // 在第1个位置之前添加元素2
        System.out.println(linkedList.get(1)); // 获取第1个位置的元素值,输出:2
        linkedList.deleteAtIndex(1); // 删除第1个位置的元素
        System.out.println(linkedList.get(1)); // 再次获取第1个位置的元素值,输出:3
    }
}

206. 反转链表

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

示例 1:

img

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

示例 2:

在这里插入图片描述

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

示例 3:

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

提示:

  • 链表中节点的数目范围是 [0, 5000]
  • -5000 <= Node.val <= 5000

解法1:迭代解法(双指针法)

迭代解法思路:使用三个指针:prevcurrnextprev 用于指向当前节点的前一个节点,curr 用于遍历链表,next 用于暂存 curr 的下一个节点。

  1. 初始化 prevnullcurr 为头节点 head
  2. 遍历链表,对于每个节点,先使用 next 暂存 curr.next
  3. 然后,将 curr.next 指向 prev,实现反转。
  4. 更新 prevcurrcurrnext
  5. currnull 时,遍历结束,此时 prev 指向新的头节点。

代码如下:

// 定义链表节点类
class ListNode {
    int val;                  // 节点存储的值
    ListNode next;           // 指向下一个节点的引用

    // 构造函数,初始化节点的值和下一个节点
    ListNode(int x) {
        val = x;
        next = null;
    }
}

// 定义链表类
class Solution {
    // 反转链表的方法
    public ListNode reverseList(ListNode head) {
        // 初始化两个指针,prev 和 curr,分别指向前一个节点和当前节点
        // 初始化为null的prev指针和头节点curr
        ListNode prev = null, curr = head;

        // 使用while循环遍历链表
        while (curr != null) {
            // 暂存当前节点的下一个节点
            ListNode next = curr.next;

            // 将当前节点的next指针指向前一个节点,实现反转
            curr.next = prev;

            // 前一个节点指针前移一位
            prev = curr;

            // 当前节点指针移动到下一个节点(原来是下一个节点)
            curr = next;
        }

        // 遍历结束后,prev指针指向新的头节点,返回prev
        return prev;
    }


// 主类,用于测试

    public static void main(String[] args) {
        // 创建链表 1 -> 2 -> 3 -> 4 -> 5
        ListNode head = new ListNode(1);
        head.next = new ListNode(2);
        head.next.next = new ListNode(3);
        head.next.next.next = new ListNode(4);
        head.next.next.next.next = new ListNode(5);

        // 创建Solution对象
        Solution solution = new Solution();

        // 调用reverseList方法反转链表
        ListNode newHead = solution.reverseList(head);

        // 打印反转后的链表
        printList(newHead); // 输出:5 -> 4 -> 3 -> 2 -> 1 -> null
    }

    // 辅助方法,用于打印链表
    public static void printList(ListNode node) {
        ListNode current = node;
        while (current != null) {
            System.out.print(current.val + " -> ");
            current = current.next;
        }
        System.out.println("null");
    }
}

示例:

1 -> 2 -> 3 -> 4 -> 5 -> null,我们的目标是将这个链表反转为:5 -> 4 -> 3 -> 2 -> 1 -> null

步骤0:初始状态

  • 头节点 head 指向数字 1。
  • prev 指针初始化为 null,因为它将指向反转后的链表的最后一个节点。
  • curr 指针初始化为 head,即指向数字 1。
  • next 指针用于暂存 curr.next

步骤 1: 遍历链表并反转

  1. 第一次迭代

    • curr 指向 1,next 指向 2。
    • curr.next 设置为 prev(此时为 null),所以 1 的下一个节点现在是 null
    • prev 移动到 curr(1),curr 移动到 next(2)。
    原链表: 1 -> 2 -> 3 -> 4 -> 5 -> null
    反转后: null <- 1 -> 2 -> 3 -> 4 -> 5 -> null
    
  2. 第二次迭代

    • curr 指向 2,next 指向 3。
    • curr.next 设置为 prev(此时为 1),所以 2 的下一个节点现在是 1。
    • prev 移动到 curr(2),curr 移动到 next(3)。
    原链表: 1 -> 2 -> 3 -> 4 -> 5 -> null
    反转后: null <- 1 <- 2 -> 3 -> 4 -> 5 -> null
    
  3. 第三次迭代

    • curr 指向 3,next 指向 4。
    • curr.next 设置为 prev(此时为 2),所以 3 的下一个节点现在是 2。
    • prev 移动到 curr(3),curr 移动到 next(4)。
    原链表: 1 -> 2 -> 3 -> 3 -> 4 -> 5 -> null
    反转后: null <- 1 <- 2 <- 3 -> 4 -> 5 -> null
    
  4. 第四次迭代

    • curr 指向 4,next 指向 5。
    • curr.next 设置为 prev(此时为 3),所以 4 的下一个节点现在是 3。
    • prev 移动到 curr(4),curr 移动到 next (5)。
    原链表: 1 -> 2 -> 3 -> 4 -> 5 -> null
    反转后: null <- 1 <- 2 <- 3 <- 4 -> 5 -> null
    
  5. 第五次迭代

    • curr 指向 5,nextnull
    • curr.next 设置为 prev(此时为 4),所以 5 的下一个节点现在是 4。
    • prev 移动到 curr(5),curr 移动到 next(null)。
    原链表: 1 -> 2 -> 3 -> 4 -> 5 -> null
    反转后: null <- 1 <- 2 <- 3 <- 4 <- 5 -> null
    

步骤 2: 返回新的头节点

  • currnull 时,遍历结束。
  • prev 现在指向新的头节点,即数字 5。

补充:链表理论基础

在Java中,链表是一种常见的数据结构,它由一系列节点组成,每个节点包含数据部分和指向下一个节点的引用。Java提供了LinkedList类来实现链表,这个类继承自AbstractSequentialList类,实现了List接口、Queue接口、Deque接口、Cloneable接口和java.io.Serializable接口。

一、链表的基本概念

链表由节点组成,每个节点包含数据和指向下一个节点的引用。在Java中,可以通过创建一个节点类来表示链表的节点,如下所示:

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

二、链表的分类

  1. 单向链表:每个节点只包含一个指向下一个节点的指针。
  2. 双向链表:每个节点包含两个指针,一个指向下一个节点,一个指向前一个节点。
  3. 循环链表:最后一个节点的下一个节点指向头节点,形成一个环。

三、链表的操作

链表的基本操作包括插入、删除、查找和遍历。以下是一些基本操作的示例代码:

  1. 插入操作
  • 头插法:在链表头部插入新节点。

    public void addFirst(int data) {
        Node newNode = new Node(data);
        newNode.next = head;
        head = newNode;
    }
    
  • 尾插法:在链表尾部插入新节点。

    public void addLast(int data) {
        Node newNode = new Node(data);
        if (head == null) {
            head = newNode;
        } else {
            Node current = head;
            while (current.next != null) {
                current = current.next;
            }
            current.next = newNode;
        }
    }
    
  1. 删除操作
  • 删除指定值的节点:遍历链表,找到并删除指定值的节点。

    public void delete(int data) {
        if (head == null) return;
        if (head.data == data) {
            head = head.next;
            return;
        }
        Node current = head;
        while (current.next != null && current.next.data != data) {
            current = current.next;
        }
        if (current.next != null) {
            current.next = current.next.next;
        }
    }
    
  1. 查找操作
  • 查找指定值的节点:遍历链表,返回指定值的节点。

    public Node find(int data) {
        Node current = head;
        while (current != null && current.data != data) {
            current = current.next;
        }
        return current;
    }
    
  1. 遍历操作
  • 打印链表:遍历链表并打印每个节点的值。

    public void printList() {
        Node current = head;
        while (current != null) {
            System.out.print(current.data + " ");
            current = current.next;
        }
        System.out.println();
    }
    

复杂度分析

  • 插入和删除操作的时间复杂度为O(1),因为这些操作只需要修改指针。
  • 查找操作的时间复杂度为O(n),因为最坏情况下需要遍历整个链表。

总结

链表是一种灵活的数据结构,特别适合于插入和删除操作频繁的场景。通过实现链表的基本操作,可以解决许多与链表相关的算法问题。在Java中,LinkedList类提供了丰富的方法来操作链表,使得链表的使用更加方便和高效。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值