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

2024-01-02 12:26:35


一、203.移除链表元素

题目描述: 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

1.递归

对除了头节点head以外的节点进行删除操作,然后判断head的节点值是否等于val
1.head == val: head需要被删除,新的头节点为head.next
2.head != val: head保留,新的头节点仍为head
3.递归的终止条件是head == null, 此时直接返回head

  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( n ) O(n) O(n)
/**
 * 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) {
        if (head == null) {
            return head;
        }
        head.next = removeElements(head.next, val);
        return head.val == val ? head.next : head;
    }
}

2.直接使用原来的链表来进行删除操作

  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( 1 ) O(1) O(1)

直接使用原来的链表来进行删除操作

class Solution {
    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;
        
    }
}

3.设置一个虚拟头结点在进行删除操作

  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( 1 ) O(1) O(1)

设置一个虚拟头节点进行删除操作

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        if (head == null) {
            return head;
        }
        // 因为删除可能涉及到头节点,所以设置dummy节点,统一操作
        ListNode dummy = new ListNode(-1, head);
        ListNode pre = dummy;
        ListNode cur = head;
        while (cur != null) {
            if (cur.val == val) {
                pre.next = cur.next;
            } else {
                pre = cur;
            }
            cur = cur.next;
        }
        return dummy.next;
    }
}

二、707.设计链表

题目描述: 使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。
如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
这道题目设计链表的五个接口:

  • 获取链表第index个节点的数值
  • 在链表的最前面插入一个节点
  • 在链表的最后面插入一个节点
  • 在链表第index个节点前面插入一个节点
  • 删除链表的第index个节点

1.单向链表

实现单向链表,即每个节点仅存储本身的值和后继节点。除此之外,我们还需要一个哨兵(sentinel)节点作为头节点,和一个size参数保存有效节点数。

  • 时间复杂度:初始化消耗 O ( 1 ) O(1) O(1)get消耗 O ( i n d e x ) O(index) O(index)addAtHead消耗 O ( 1 ) O(1) O(1)addAtTail 消耗 O ( n ) O(n) O(n),其中 n n n为链表当前长度,即addAtHeadaddAtTailaddAtIndex已调用次数之和,addAtIndex消耗 O ( i n d e x ) O(index) O(index)
  • 空间复杂度:所有函数单次调用均为 O ( 1 ) O(1) O(1),总体为 O ( n ) O(n) O(n),其中 n n naddAtHeadaddAtTailaddAtIndex已调用次数之和。

单向链表
实现get(index) 时,先判断有效性,再通过循环来找到对应的节点的值。
get
addAtIndex(index, val)实现过程如下图所示:

  1. 找前驱节点pred
  2. 创建新节点toAdd
  3. toAdd.next的设置为pred.next, 将pred.next设置为toAdd(注意: 这里的更新操作要先更新toAdd.next后更新pred.next,简单理解为:先后-后前);
  4. 更新size

中间插入
开头插入
实现deleteAtIndex(index),实现过程:

  1. 判断参数有效性;
  2. 找前驱节点pred
  3. 更新pred.next == pred.next.next 删除节点;
  4. 更新size
    删除
//单链表
class ListNode {
    int val;
    ListNode next;
    ListNode(){}
    ListNode(int val) {
        this.val=val;
    }
}
class MyLinkedList {
    //size存储链表元素的个数
    int size;
    //虚拟头结点
    ListNode head;

    //初始化链表
    public MyLinkedList() {
        size = 0;
        head = new ListNode(0);
    }

    //获取第index个节点的数值
    public int get(int index) {
        //如果index非法,返回-1
        if (index < 0 || index >= size) {
            return -1;
        }
        ListNode currentNode = head;
        //包含一个虚拟头节点,所以查找第 index+1 个节点
        for (int i = 0; i <= index; i++) {
            currentNode = currentNode.next;
        }
        return currentNode.val;
    }

    //在链表最前面插入一个节点
    public void addAtHead(int val) {
        addAtIndex(0, val);
    }

    //在链表的最后插入一个节点
    public void addAtTail(int val) {
        addAtIndex(size, val);
    }

    // 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
    // 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
    // 如果 index 大于链表的长度,则返回空
    public void addAtIndex(int index, int val) {
        if (index > size) {
            return;
        }
        if (index < 0) {
            index = 0;
        }
        size++;
        //找到要插入节点的前驱
        ListNode pred = head;
        for (int i = 0; i < index; i++) {
            pred = pred.next;
        }
        ListNode toAdd = new ListNode(val);
        toAdd.next = pred.next;
        pred.next = toAdd;
    }

    //删除第index个节点
    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) {
            return;
        }
        size--;
        ListNode pred = head;
        for (int i = 0; i < index; i++) {
            pred = pred.next;
        }
        pred.next = pred.next.next;
    }
}

/**
 * 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);
 */

2.双向链表

实现双向链表,即每个节点要存储本身的值,后继节点和前驱节点。除此之外,需要一个哨兵节点作为头节点head和一个哨兵节点作为尾节点tail,还需要一个size参数保存有效节点数。

  • 时间复杂度:初始化消耗 O ( 1 ) O(1) O(1)get消耗 O ( i n d e x ) O(index) O(index)addAtHead消耗 O ( 1 ) O(1) O(1)addAtTail 消耗 O ( 1 ) O(1) O(1),其中 n n n为链表当前长度,即addAtHeadaddAtTailaddAtIndex已调用次数之和,addAtIndex消耗 O ( i n d e x ) O(index) O(index)
  • 空间复杂度:所有函数单次调用均为 O ( 1 ) O(1) O(1),总体为 O ( n ) O(n) O(n),其中 n n naddAtHeadaddAtTailaddAtIndex已调用次数之和。

双向链表
实现get(index)时,先判断有限性,然后再比较从head还是tail来遍历会比较快找到目标,然后进行遍历。
get双链表
addAtIndex(index, val)实现过程如下图所示:

  1. 找原来下标为index的节点succ
  2. 找前驱节点pred
  3. 创建新节点toAdd
  4. 通过各自prevnext变量的更新来增加toAdd
  5. 更新size
    add双链表
    实现deleteAtIndex(index),实现过程:
  6. 判断参数有效性;
  7. 找下标为index节点的前驱节点pred和后继节点succ
  8. 通过更新各自prednext变量的更新来删除节点;
  9. 更新size
    delete双链表
//双链表
class MyLinkedList {
    class ListNode {
        int val;
        ListNode next, pred;
        ListNode(int x) { val = x; }
    }

    int size;
    ListNode head, tail;

    public MyLinkedList() {
        size = 0;
        head = new ListNode(0);
        tail = new ListNode(0);
        head.next = tail;
        tail.pred = head;
    }

    public int get(int index) {
        if (index < 0 || index >= size) {
            return -1;
        }
        ListNode cur = head;
        // 通过判断 index < (size-1)/2 来决定是从head还是tail遍历,提高效率
        if (index < (size - 1) / 2) {
            for (int i = 0; i <= index; i++) {
                cur = cur.next;
            }
        } else {
            cur = tail;
            for (int i = 0; i <= size - index - 1; i++) {
                cur = cur.pred;
            }
        }
        return cur.val;
    }

    public void addAtHead(int val) {
        ListNode cur = head;
        ListNode newNode = new ListNode(val);
        newNode.next = cur.next;
        cur.next.pred = newNode;
        cur.next = newNode;
        newNode.pred = cur;
        size++;
    }

    public void addAtTail(int val) {
        ListNode cur = tail;
        ListNode newNode = new ListNode(val);
        newNode.next = tail;
        newNode.pred = cur.pred;
        cur.pred.next = newNode;
        cur.pred = newNode;
        size++;
    }

    public void addAtIndex(int index, int val) {
        if(index > size){return;}
        if(index < 0){index = 0;}
        ListNode cur = head;
        for(int i = 0; i < index; i++){
            cur = cur.next;
        }
        ListNode newNode = new ListNode(val);
        newNode.next = cur.next;
        cur.next.pred = newNode;
        newNode.pred = cur;
        cur.next = newNode;
        size++;
    }

    public void deleteAtIndex(int index) {
        if(index >= size || index < 0){return;}
        ListNode cur = head;
        for(int i = 0; i < index; i++){
            cur = cur.next;
        }
        cur.next.next.pred = cur;
        cur.next = cur.next.next;
        size--;
    }
}

三、206.反转链表

题目描述: 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
反转链表

1.双指针法

  1. 定义一个cur指针,指向头节点,再定义一个pred指针,初始化为null
  2. 反转:将cur.next节点用temp保存一下,即保存一下当前节点;
  3. 改变cur.next指向,将cur.next指向pred,即反转第一个节点;
  4. 循环走代码逻辑,继续移动predcur指针。
  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( 1 ) O(1) O(1)
/**
 * 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 pred = null;
        ListNode cur = head;
        ListNode temp = null;
        while(cur != null) {
            temp = cur.next; // 保存下一个节点
            cur.next = pred;
            pred = cur;
            cur = temp;
        }
        return pred;
    }
}

2.递归法

  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( n ) O(n) O(n)
// 递归
class Solution {
    public ListNode reverseList(ListNode head) {
        return reverse(null, head);
    }
    private ListNode reverse(ListNode pred, ListNode cur) {
        if (cur == null) {
            return pred;
        }
        ListNode temp = null;
        temp = cur.next;  // 先保存下一个节点
        cur.next = pred;  // 反转
        return reverse(cur, temp);
    }
}
 // 从后向前递归
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; 
    }
}

总结

以上就是今天要学习的内容,设计链表是重点,把这道题目做了,链表基本操作就OK了。

文章来源:https://blog.csdn.net/qq_41929830/article/details/135283177
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。