代码随想录第三天|移除链表元素|设计链表|反转链表

203. 移除链表元素

文档讲解:代码随想录
视频讲解:B站视频
状态:使用虚拟头节点AC,同时了解到还可以直接使用本节点进行删除。这道题可以说是之后好几道题的基础,在链表中使用虚拟头节点是十分常见的操作。

首先介绍使用虚拟头节点对链表进行操作,这样做的好处是头节点往往需要特殊的操作,使得编辑头节点时需要额外写一段逻辑,如果使用虚拟头节点的话就没有这个问题。下面首先介绍使用虚拟头节点的方法。

删除链表元素可以分解为以下步骤:

  • 索引移动到待删除节点的上一个节点处
  • 待删除节点的上一个节点指针指向待删除节点的下一个节点

注意:使用指针遍历节点时,应该注意指针递增的时机,在该题中,若下一个节点不是目标节点,指针递增。具体代码如下:

  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) {
        ListNode virhead = new ListNode(-1, head);
        ListNode result = head;
        ListNode index = virhead;
        while(index.next != null) {
            if (index.next.val != val) { // 下一个节点不是目标节点,索引+1
                index = index.next;
                
            }
            else index.next = index.next.next;  //下一个节点是目标节点,删除指向下一个节点的指针
        }
        result = virhead.next;
        return result;
    }
}

然后介绍不使用虚拟节点的方法,这种方法删除头节点以外的节点时逻辑相同,都是在待删除节点的前一个节点上进行指针操作,但是在删除头结点时,需要在待删除节点上进行操作,因此在这里应该分情况讨论。

  • 待删除节点不是头节点,指针正常遍历到待删除节点的上一个节点进行操作
  • 待删除节点是头节点,将头节点指向下一个节点。

如何判断待删除的节点为头节点?这需要在正常遍历流程开始之前先使用一个循环判断,直到头节点不是目标节点,再进行常规遍历操作。流程图如下:
在这里插入图片描述

具体实现代码如下:

public ListNode removeElements(ListNode head, int val) {
    while (head != null && head.val == val) { //操作头节点知道头节点不是目标元素
        head = head.next;
    }
    // 已经为null,提前退出
    if (head == null) {
        return head;
    }
    // 已确定当前head.val != val
    ListNode pre = head;
    ListNode cur = head.next;
    while (cur != null) {
        if (cur.val == val) {
            pre.next = cur.next;
        } else {
            pre = cur;
        }
        cur = cur.next;
    }
    return head;
}

707. 设计链表

文档讲解:代码随想录
视频讲解:B站视频
状态:多次debug后AC,这道题十分考验对于链表的操作能力,在做题时经常因为对链表操作掌握不熟练而疏忽删除节点,增加节点等操作中的一些细节。这道题需要以后回来复习。

这道题需要设计的有链表类的字段、节点类设计,get方法、insert方法以及delete方法。

  1. 链表类字段

    参考标准库中的LinkedList库,需要一个指向链表头部的指针和一个指向链表尾部的指针,以及一个表示链表长度的量:

    class MyLinkedList {
        private int size;
        private Node head;
        private Node tail;
    
        public MyLinkedList() {
            this.size = 0;
            this.head = null;
            this.tail = null;
        }
    }
    
  2. 节点内部类

    将节点类作为链表类的内部类,这样节点类对外隐藏,创建节点首先需要一个链表类的实例,保证了封装性。单链表的节点类设计如下:

        private class Node {
            private int val;
            private Node next;
    
            private Node(int val, Node next) {
                this.val = val;
                this.next = next;
            }
        }
    
  3. get方法

    这个方法需要得到指定索引处的链表元素,使用遍历得到元素,方法如下:

    public int get(int index) {
        if (index < 0 || index >= size) return -1;
        Node current = head;
        for (int i = 0; i < index; i++) {
            current = current.next;
        }
        return current.val;
    }
    
  4. insert方法设计

    这一类方法包含三个方法

    • addAtHead方法需要改变头节点,根据size大小也需要改变尾节点;
    • addAtTail方法需要改变尾节点,根据size大小也需要改变头节点;
    • addAtIndex方法需要插入元素,在插入到头部或者尾部时可以调用函数解决。

    具体方法实现如下:

        public void addAtHead(int val) {
            Node newHead = new Node(val, head);
            head = newHead;
            if (size == 0) {
                tail = newHead;
            }
            size++;
        }
    
        public void addAtTail(int val) {
            Node newTail = new Node(val, null);
            if (size == 0) {
                head = newTail;
                tail = newTail;
            } else {
                tail.next = newTail;
                tail = newTail;
            }
            size++;
        }
    
        public void addAtIndex(int index, int val) {
            if (index < 0 || index > size) return;
            if (index == 0) {
                addAtHead(val);
                return;
            }
            if (index == size) {
                addAtTail(val);
                return;
            }
            Node prev = head;
            for (int i = 0; i < index - 1; i++) {
                prev = prev.next;
            }
            Node newNode = new Node(val, prev.next);
            prev.next = newNode;
            size++;
        }
    
  5. deleteAtIndex方法

    这个方法删除指定索引处的节点,整体方法与第一道题移除节点相同,需要注意的是,如果移除的是头节点或者尾节点,相应的指针也需要改变。

        public void deleteAtIndex(int index) {
            if (index < 0 || index >= size) return;
            if (index == 0) {
                head = head.next;
                if (size == 1) {
                    tail = null;
                }
            } else {
                Node prev = head;
                for (int i = 0; i < index - 1; i++) {
                    prev = prev.next;
                }
                prev.next = prev.next.next;
                if (index == size - 1) {
                    tail = prev;
                }
            }
            size--;
        }
    

注意:操作链表时要特别留心链表的临界条件。

206. 反转链表

文档讲解:代码随想录
视频讲解:B站视频
状态:使用迭代法更容易让人理解,但是递归代码少,再尝试使用递归求解时碰壁,这道题需要以后回来复习。

使用迭代方法时,反转链表需要我们获取前一个节点和当前节点,使得当前节点指向前一个节点,并且也需要一个临时变量来存储后一个节点,因此流程如下:

  • 获得后一个节点并存储在临时变量中
  • 将当前节点指向上一个节点
  • 索引移动至临时变量所在的节点

直到当前节点为null,就可以返回前一个节点作为头节点。

代码如下:

class Solution {
    public ListNode reverseList(ListNode head) {
        // 迭代解决
        // 两个指针,一个指向当前节点,一个指向前一个节点
        var index = head;
        ListNode prev = null;
        // 迭代
        while(index != null) {
            var temp = index.next;
            index.next = prev;
            // 两个索引进行移动
            prev = index;
            index = temp;
        }
        return prev
 }

递归法只讨论给定函数声明的递归方法:

  • 递归每次传入参数不一致,函数栈的上一层传入参数是在下一层的基础上+1;
  • 递归的退出条件是遍历到链表的最后一个元素,返回当前元素作为头节点,并且一直返回到栈底
  • 每一个函数栈需要完成当前索引所在节点的下一个节点的链表反转,并负责将最顶层函数栈的节点返回到main函数中,这是因为最后一个节点不做反转直接返回,所以导致当前节点的反转操作是在前一个节点处完成的。

具体代码如下:

class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null) return null;
        if (head.next == null) return head;
        
        // 递归调用,翻转第二个节点开始往后的链表
        ListNode last = reverseList(head.next);
        // 翻转头节点与第二个节点的指向
        head.next.next = head;
        // 此时的 head 节点为尾节点,next 需要指向 NULL
        head.next = null;
        return last;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值