Day03~203.移除链表元素、707.设计链表、206.反转链表

205 阅读7分钟

摘要

本文主要介绍了链表的基本概念与常见操作,并附带了LeetCode上的几道题目,包括203.移除链表元素、707.设计链表、206.反转链表的解题思路与示例代码。

1、链表理论基础

1.1 概念

链表(Linked List)是一种常见的线性数据结构,它由节点(Node)构成,每个节点包含两部分:数据元素和指向下一个节点的引用(或指针)。链表的基本组成部分如下:

  1. 节点(Node): 链表的基本单元,包含两个字段,一个用于存储数据,另一个用于指向下一个节点。
  2. 头节点(Head): 链表的第一个节点,通常用来表示链表的起始点。
  3. 尾节点(Tail): 链表的最后一个节点,其指针通常为空(null)或者指向一个特殊的终结节点。

链表可以分为多种类型,其中常见的包括:

  • 单链表(Singly Linked List): 每个节点只有一个指针,指向下一个节点。
  • 双链表(Doubly Linked List): 每个节点有两个指针,分别指向前一个节点和后一个节点。
  • 循环链表(Circular Linked List): 最后一个节点的指针指向第一个节点,形成一个闭环。

链表相对于数组的优点包括:

  • 动态大小: 链表可以根据需要动态增长或缩小,不需要预先分配固定大小的空间。
  • 插入和删除效率高: 在链表中插入或删除节点的操作相对容易,只需要调整节点的指针,不需要像数组那样移动大量元素。
  • 内存利用率高: 链表可以根据需要分配内存,不会造成内存浪费。

然而,链表也有一些缺点,主要包括:

  • 随机访问效率低: 链表中的元素不是按照连续的内存地址存储的,因此随机访问的效率较低。
  • 占用额外的空间: 每个节点需要额外的空间来存储指针信息,相对于数组,链表可能占用更多的内存。

链表在计算机科学中被广泛应用,常用于实现其他数据结构,例如栈、队列和图等。它们也在各种算法和编程问题中发挥着重要的作用。理解链表的基本原理和操作是编写高效算法的关键。

1.2 链表的操作

链表是一种常见的数据结构,它支持一系列基本的操作,包括:

  1. 插入(Insertion): 向链表中添加新节点。
  2. 删除(Deletion): 从链表中移除节点。
  3. 查找(Search): 查找链表中特定值的节点。
  4. 遍历(Traversal): 遍历整个链表,访问每个节点。

下面是这些操作的详细说明:

1. 插入操作:

  • 在头部插入(Insert at the Beginning): 在链表的头部插入一个新节点,将其指针指向原来的头节点。
  • 在尾部插入(Insert at the End): 在链表的尾部插入一个新节点,将原来的尾节点的指针指向新节点。
  • 在指定位置插入(Insert at a Given Position): 在链表的指定位置插入一个新节点,调整相邻节点的指针。

2. 删除操作:

  • 删除头节点(Delete at the Beginning): 移除链表的头节点,将头指针指向下一个节点。
  • 删除尾节点(Delete at the End): 移除链表的尾节点,将倒数第二个节点的指针置为空。
  • 删除指定节点(Delete a Given Node): 移除链表中指定值的节点,调整前后节点的指针。

3. 查找操作:

  • 按值查找(Search by Value): 从链表中查找具有特定值的节点,返回节点或节点的位置。

4. 遍历操作:

  • 遍历整个链表(Traverse the Entire List): 从链表的头节点开始,依次访问每个节点,通常使用循环进行遍历。

链表的具体实现可以是单链表、双链表、循环链表等,操作的复杂度取决于链表类型和具体实现方式。链表是许多其他数据结构的基础,如栈和队列,因此对链表的操作和理解对于编写高效的算法非常重要。

2、203.移除链表元素

2.1 思路

(删除)使用虚拟头节点, 比较当前节点的next节点的值,满足条件则删除(cur.next = cur.next.next)

2.2 代码

error-1

    public ListNode removeElements(ListNode head, int val) {
        ListNode dummy = new ListNode(-1, head);
        ListNode cur = dummy;
​
        while(cur != null && cur.next != null) {
            if(cur.next.val == val) {
                cur.next = cur.next.next; 
            }
            
            cur = cur.next;
        }
        return dummy.next;
    }

测试用例:输入:head = [7,7,7,7], val = 7 ;输出:[7,7];预期结果:[]

原因:

  • 当使用虚拟头节点,删除第一个 7 后,当前节点指向第二个 7,所以第二个 7 没有删除;删除第三个 7 后,当前节点指向第四个 7,所以第四个 7 没有删除

解决方式:

  • 删除next节点后,当前节点不用指向next节点,可以继续判断当前节点的next节点

AC

    // (删除)使用虚拟头节点, 比较当前节点的next节点的值,满足条件则删除(cur.next = cur.next.next)
    public ListNode removeElements(ListNode head, int val) {
        ListNode dummy = new ListNode(-1, head);
        ListNode cur = dummy;
​
        while (cur != null && cur.next != null) {
            if (cur.next.val == val) {
                cur.next = cur.next.next;
            } else {
                cur = cur.next;
            }
        }
        return dummy.next;
    }

3、707.设计链表

3.1 思路

定义虚拟头节点 head ,定义链表的长度 size,实现 MyLinkedList() 方法,初始化 MyLinkedList 对象;

定义核心方法 getNode(int index),该方法可以通过 index 获取相应的 node,index 的取值范围是 [-1, size -1],则MyLinkedList 类中的其他方法实现思路如下:

  • int get(int index):可以通过 getNode(index) 获取
  • void addAtHead(int val):等价于addAtIndex(0, val)
  • void addAtTail(int val):等价于addAtIndex(size, val)
  • void addAtIndex(int index, int val):可以通过 getNode(index-1) 获取前节点,然后插入节点
  • void deleteAtIndex(int index):可以通过 getNode(index-1) 获取前节点,然后删除节点

3.2 代码

error-1

错误的代码:逻辑非常混乱

    public ListNode getNode(int index) {
        index = index + 1;
​
        if (index < 0 || index > size -1) {
            return null;
        }
​
        ListNode cur = head;
        while (index > 0) {
            cur = cur.next;
            index--;
        }
​
        return cur;
    }

正确的代码

    public ListNode getNode(int index) {
        if (index < -1 || index > size - 1) {
            return null;
        }
​
        ListNode cur = head;
        while (index >= 0) {
            cur = cur.next;
            index--;
        }
        return cur;
    }

index 的取值范围?如果通过 index 正确获取相应的链表节点?

  • index 代表链表的中元素的下标,因为使用了虚拟链表,但对于使用者是未知的,取值范围应该在 [-1, size - 1]

    • -1 代表虚拟节点
    • 0 代表头节点
    • size - 1 代表尾节点
  • 所以在判断取值范围应该是 index < -1 || index > size - 1

  • 遍历链表取值时,while (index >= 0)

    • index = -1,不进入循环,返回 head
    • index = 0,进入循环,返回 head.next
    • index 等于其他值同理

error-2

错误的代码

    public void addAtTail(int val) {
        addAtIndex(size-1, val);
    }

正确的代码

    public void addAtTail(int val) {
        addAtIndex(size, val);
    }

void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。

void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。

error-3

错误的代码;在 prev.next == null 情况下 会导致空指针异常

    public void deleteAtIndex(int index) {
        ListNode prev = getNode(index - 1);
        if (prev == null) {
            return;
        }
​
        prev.next = prev.next.next;
        size--;
    }

正确的代码

    public void deleteAtIndex(int index) {
        ListNode prev = getNode(index - 1);
        if (prev == null || prev.next == null) {
            return;
        }
​
        prev.next = prev.next.next;
        size--;
    }

AC

class DesignLinkedList {
​
    private ListNode head;
    private int size;
​
    public DesignLinkedList() {
        head = new ListNode(-1);
        size = 0;
    }
​
    public int get(int index) {
        ListNode node = getNode(index);
        if (node == null) {
            return -1;
        }
        return node.val;
    }
​
    public void addAtHead(int val) {
        addAtIndex(0, val);
    }
​
    public void addAtTail(int val) {
        addAtIndex(size, val);
    }
​
    public void addAtIndex(int index, int val) {
        ListNode prev = getNode(index - 1);
        if (prev == null) {
            return;
        }
​
        prev.next = new ListNode(val, prev.next);
        size++;
    }
​
    public void deleteAtIndex(int index) {
        ListNode prev = getNode(index - 1);
        if (prev == null || prev.next == null) {
            return;
        }
​
        prev.next = prev.next.next;
        size--;
    }
​
    public ListNode getNode(int index) {
        if (index < -1 || index > size - 1) {
            return null;
        }
​
        ListNode cur = head;
        while (index >= 0) {
            cur = cur.next;
            index--;
        }
        return cur;
    }
​
    private static class ListNode {
        int val;
        ListNode next;
​
        ListNode() {
        }
​
        ListNode(int val) {
            this.val = val;
        }
​
        ListNode(int val, ListNode next) {
            this.val = val;
            this.next = next;
        }
    }
}

4、206.反转链表

4.1 思路

使用变量prev保存前节点,遍历链表,使得当前节点指向prev,最后返回prev就是反转后的链表

4.2 代码

    // 使用变量prev保存前节点,遍历链表,使得当前节点指向prev,最后返回prev就是反转后的链表
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode cur = head;
        while(cur != null) {
            ListNode next = cur.next;
            cur.next = prev;
            prev = cur;
            cur = next;
        }
        return prev;
    }

参考资料

代码随想录-链表理论基础

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

代码随想录-设计链表

代码随想录-反转链表