leetcode系列-链表

链表的理论基础

在这篇文章关于链表,你该了解这些!中,介绍了如下几点:

  • 链表的种类主要为:单链表,双链表,循环链表
  • 链表的存储方式:链表的节点在内存中是分散存储的,通过指针连在一起。
  • 链表是如何进行增删改查的。
  • 数组和链表在不同场景下的性能分析。

可以说把链表基础的知识都概括了,但又不像教科书那样的繁琐

链表经典题目

虚拟头结点

讲解了链表操作中一个非常总要的技巧:虚拟头节点。

每次对应头结点的情况都要单独处理,所以使用虚拟头结点的技巧,就可以解决这个问题

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

/**
 * 方法:哨兵节点。哨兵节点将被用于伪头。初始化两个指针 curr 和 prev 指向当前节点和前继节点。
 *
 * 算法:万一头节点==val,所以需要有一个虚拟的节点来辅助遍历。
 * 初始化哨兵节点为 ListNode(0) 且设置 sentinel.next = head。
 * 初始化两个指针 curr 和 prev 指向当前节点和前继节点。
 * 当 curr != nullptr;
 *    比较当前节点和要删除的节点:
 *        若当前节点就是要删除的节点:则 prev.next = curr.next。
 *        否则设 prve = curr。
 *    遍历下一个元素:curr = curr.next。
 * 返回 sentinel.next。
*/
class Solution {
    public ListNode removeElements(ListNode head, int val) {

        ListNode sentinel = new ListNode(0);
        sentinel.next = head;

        ListNode prev = sentinel, curr = head;

        while (curr != null) {

            if (curr.val == val) {
                prev.next = curr.next;
            }else {
                prev = curr;
            }
            curr = curr.next;
        }
        return sentinel.next;
    }

}

复杂度分析

  • 时间复杂度:O(N),只遍历了一次。
  • 空间复杂度:O(1)。

链表的基本操作

链表:一道题目考察了常见的五个操作!中,我们通设计链表把链表常见的五个操作练习了一遍。

这是练习链表基础操作的非常好的一道题目,考察了:

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

707-设计链表的实现

/**
 * 定义一个单链表:找到目标ListNode的前一个ListNode
 
 复杂度分析
    时间复杂度:
        addAtHead: O(1)
        addAtInder,get,deleteAtIndex: O(k),其中 k 指的是元素的索引。
        addAtTail:O(N),其中 N 指的是链表的元素个数。
    空间复杂度:所有的操作都是 O(1)

 */
public class ListNode{
        int val;
        ListNode next;
        ListNode(int x){
            val = x;
            next = null;
        }
}

class MyLinkedList {

    int size;
    ListNode head;  // sentinel node as pseudo-head

    /** Initialize your data structure here. */
    public MyLinkedList() {
        size = 0;
        head = new ListNode(0);
    }
    
    /** Get the value of the index-th node in the linked list. If the index is invalid, return -1. */
    public int get(int index) {

        // if index is invalid
        if (index < 0 || index >= size){
            return -1;
        }
        ListNode curr = head;
        // index steps needed
        // to move from sentinel node to wanted index
        for (int i = 0; i < index + 1 ;i++){
            curr = curr.next;
        }
        return curr.val;

    }
    
    /** Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. */
    public void addAtHead(int val) {
        addAtIndex(0,val);
    }
    
    /** Append a node of value val to the last element of the linked list. */
    public void addAtTail(int val) {
        addAtIndex(size,val);
    }
    
    /** Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. */
    public void addAtIndex(int index, int val) {
        // If index is greater than the length,
        // the node will not be inserted.
        if (index > size) return;

        // [so weird] If index is negative,
        // the node will be inserted at the head of the list.
        if (index < 0) index = 0;

        ++size;
        // find predecessor of the node to be added
        ListNode pred = head;
        for (int i = 0; i < index; i++) {
            pred = pred.next;
        }
        // node to be added
        ListNode toAdd = new ListNode(val);
        // insertion itself
        toAdd.next = pred.next;
        pred.next = toAdd;

    }
    
    /** Delete the index-th node in the linked list, if the index is valid. */
    public void deleteAtIndex(int index) {

        // if the index is invalid, do nothing
        if (index < 0 || index >= size) return;

        size--;

        // find predecessor of the node to be deleted
        ListNode pred = head;

        for(int i = 0; i < index; ++i){
            pred = pred.next;
        }

        // delete pred.next
        pred.next = pred.next.next;
    }
}
/**
 * 双向链表,主要找前后的两个ListNode
 
  复杂度分析
    时间复杂度:
        addAtHead,addAtTail:O(1)
		get,addAtIndex,delete:O(min(k,N−k)),其中 k 指的是元素的索引。
    空间复杂度:所有的操作都是 O(1)
 */
public class ListNode{
    int val;
    ListNode next;
    ListNode prev;
    ListNode(int x){
        val = x;
    }
}

class MyLinkedList {

    int size;
    ListNode head,tail; // sentinel nodes as pseudo-head and pseudo-tail


    /** Initialize your data structure here. */
    public MyLinkedList() {

        size = 0;
        ListNode head = new ListNode(0);
        ListNode tail = new ListNode(0);
        head.next = tail;
        tail.prev = head;

    }

    /** Get the value of the index-th node in the linked list. If the index is invalid, return -1. */
    public int get(int index) {

        if (index < 0 || index >= size )return -1;

        // choose the fastest way: to move from the head
        // or to move from the tail
        ListNode curr = head;

        //双向查找,类似于二分法
        if (index + 1 < size - index){

            for (int i = 0; i < index + 1; i++) {
                curr = curr.next;
            }else{
            curr = tail;
            for (int i = 0; i < size - index ; i++) {
                curr = curr.prev;
            }
        }
        return curr.val;
    }

    /** Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. */
    public void addAtHead(int val) {

        ListNode pred = head,succ = head.next;
        ++size;

        ListNode toAdd = new ListNode(val);

        toAdd.prev = pred;
        toAdd.next = succ;

        pred.next = toAdd;
        succ.prev = toAdd;

    }

    /** Append a node of value val to the last element of the linked list. */
    public void addAtTail(int val) {

        ListNode succ = tail, pred = tail.prev;

        ++size;
        ListNode toAdd = new ListNode(val);

        toAdd.prev = pred;
        toAdd.next = succ;

        succ.prev = toAdd;
        pred.next = toAdd;

    }

    /** Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. */
    public void addAtIndex(int index, int val) {
            // If index is greater than the length,
            // the node will not be inserted.
            if (index > size) return;

            // [so weird] If index is negative,
            // the node will be inserted at the head of the list.
            if (index < 0) index = 0;

            // find predecessor and successor of the node to be added
            ListNode pred, succ;

            if (index < size - index){
                pred = head;
                for(int i = 0; i < index; ++i) pred = pred.next;
                succ = pred.next;
            }else {
                succ = tail;
                for (int i = 0; i < size - index; ++i) succ = succ.prev;
                pred = succ.prev;
            }
            // insertion itself
            ++size;
            ListNode toAdd = new ListNode(val);
            toAdd.prev = pred;
            toAdd.next = succ;
            pred.next = toAdd;
            succ.prev = toAdd;

        }

    }

    /** Delete the index-th node in the linked list, if the index is valid. */
    public void deleteAtIndex(int index) {
        // if the index is invalid, do nothing
        if (index < 0 || index >= size) return;

        // find predecessor and successor of the node to be deleted
        ListNode pred, succ;
        if (index < size - index) {
            pred = head;
            for(int i = 0; i < index; ++i) pred = pred.next;
            succ = pred.next.next;
        }
        else {
            succ = tail;
            for (int i = 0; i < size - index - 1; ++i) succ = succ.prev;
            pred = succ.prev.prev;
        }

        --size;
        pred.next = succ;
        succ.prev = pred;

    }
}

反转链表

链表:听说过两天反转链表又写不出来了?中,讲解了如何反转链表。

206-反转一个单链表。

/**方法一:迭代 : 假设链表为 1→2→3→∅,我们想要把它改成 ∅←1←2←3。
 在遍历链表时,将当前节点的 next 指针改为指向前一个节点。
 由于节点没有引用其前一个节点,因此必须事先存储其前一个节点。
 在更改引用之前,还需要存储后一个节点。最后返回新的头引用

 */
class Solution {
    public ListNode reverseList(ListNode head) {


        ListNode pre = null;
        ListNode curr = head;

        while (curr != null){

            ListNode temp = curr.next;
            curr.next = pre;
            pre = curr;
            curr = temp;
        }
        return pre;

    }
}

复杂度分析

  • 时间复杂度:O(n),其中 n是链表的长度。需要遍历链表一次。
  • 空间复杂度:O(1)。

环形链表

链表:环找到了,那入口呢?中,讲解了在链表如何找环,以及如何找环的入口位置。

这道题目可以说是链表的比较难的题目了。

很多同学关注的问题是:为什么一定会相遇,快指针就不能跳过慢指针么?

可以确定如下两点:

  • fast指针一定先进入环中,如果fast 指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。
  • fast和slow都进入环里之后,fast相对于slow来说,fast是一个节点一个节点的靠近slow的,注意是相对运动,所以fast一定可以和slow重合

如果fast是一次走三个节点,那么可能会跳过slow,因为相对于slow来说,fast是两个节点移动的。

确定有否有环比较容易,但是找到环的入口就不太容易了,需要点数学推理。

我在链表:环找到了,那入口呢?中给出了详细的推理,兼顾易懂和简洁了。

这是一位录友在评论区有一个疑问,感觉这个问题很不错,但评论区根本说不清楚,我就趁着总结篇,补充一下这个证明。

在推理过程中,为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?

了解这个问题一定要先把文章链表:环找到了,那入口呢?看了,即文章中如下的地方:

在这里插入图片描述

首先slow进环的时候,fast一定是先进环来了。

如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子:

在这里插入图片描述

可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。

重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图:

在这里插入图片描述

那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。

因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。

也就是说slow一定没有走到环入口3,而fast已经到环入口3了

这说明什么呢?

在slow开始走的那一环已经和fast相遇了

那有同学又说了,为什么fast不能跳过去呢? 在刚刚已经说过一次了,fast相对于slow是一次移动一个节点,所以不可能跳过去

141-给定一个链表,判断链表中是否有环。

/**
 * 方法一:哈希表
 * 最容易想到的方法是遍历所有节点,每次遍历到一个节点时,判断该节点此前是否被访问过。
 * 我们可以使用哈希表来存储所有已经访问过的节点。
 * 每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,
 * 否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。
 *
 */
public class Solution {
    public boolean hasCycle(ListNode head) {

        Set<ListNode> set = new HashSet<ListNode>();
        while (head != null){
            if (!set.add(head)){
                return true;
            }
            head = head.next;
        }
        return false;
    }
}

复杂度分析

时间复杂度:O(N),其中 N 是链表中的节点数。最坏情况下我们需要遍历每个节点一次。

空间复杂度:O(N),其中 N 是链表中的节点数。主要为哈希表的开销,最坏情况下我们需要将每个节点插入到哈希表中一次。

142 -给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

/**
 *方法一:哈希表
 * 我们遍历链表中的每个节点,并将它记录下来;一旦遇到了此前遍历过的节点,就可以判定链表中存在环。
 *  * 时间复杂度 O(N)
 *  * 空间复杂度 O(N)
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {

        ListNode pos = head;
        Set<ListNode> set = new HashSet<ListNode>();

        while (pos != null){
            if (set.contains(pos)){
                return pos;
            }else{
                set.add(pos);
            }
            pos = pos.next;
        }
        return null;

    }
}
/**
 * 方法二:快慢指针
 *我们使用两个指针,fast 与 slow。它们起始都位于链表的头部。
 * 随后,slow 指针每次向后移动一个位置,而 fast 指针向后移动两个位置。
 * 如果链表中存在环,则 fast 指针最终将再次与 slow 指针在环中相遇。
 *
 * 需要构造两次相遇,第一次相遇是检查是否有环,第二次是找到环的入口。
 * 时间复杂度 O(N)
 * 空间复杂度 O(1)  我们只使用了 slow,fast,ptr 三个指针
 */

public class Solution {
    public ListNode detectCycle(ListNode head) {

        ListNode fast = head,slow = head;
        while (true){
            if (fast == null || fast.next == null) return null;

            fast.next = fast.next.next;
            slow = slow.next;

            if (fast == slow) break;
        }
        fast = head;
        while (slow != fast){
            slow = slow.next;
            fast = fast.next;
        }
        return fast;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值