链表刷题总结

138. 随机链表的复制

  • 拷贝问题,此类问题图拷贝问题(133题)
  • 解题思路
    • 遍历两次
    • 第一次遍历,拷贝next,并且拷贝old和new到一个map中,映射关系
    • 第二次遍历,拷贝random,通过oldRandom找到newrandom,然后设置到new中

92. 反转链表 II

思路通链表的操作问题,通常都不难,技巧就是一定要先想清楚思路,并且在草稿纸上画图,然后编码。

技巧:

  • 使用虚拟头节点解决left=1的问题,这样可以找到prev

解题思路:

  • 穿针引线:使用curr,next,pre三个节点
    • 1、curr的下一个节点指向next的下一个节点
    • 2、next的下一个节点,指向curr节点啊
    • 3、pre的next节点指向next节点
  • 反转局部,在将prev的next,指向翻转后的头节点,翻反转后的尾节点,指向right的下一个节点即可

141. 环形链表

解题思路:使用快慢指针的方法来实现

注意边界条件:

1、head==null

2、head.size == 1

3、head.size == 2

public class Solution {
    public boolean hasCycle(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;

        while (fast != null && fast.next != null && slow != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                return true;
            }
        }

        return false;
    }
}

206. 反转链表

列表的操作总结:

1、ListNode node = xx,暂存

2、curr.next = xx,修改制向

3、prev = curr,移动节点,或者prev暂存curr

4、curr = next,移动节点


class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode next = curr.next;// 暂存后续节点
            curr.next = prev;// 修改引用指向
            prev = curr;// 暂存当前节点
            curr = next;// 访问下一个节点
        }

        return prev;
    }
}

19. 删除链表的倒数第 N 个结点

三个思路:

1、先计算size,再移动指针,然后修改指向

2、借助栈,来找到元素,然后移动指针

3、使用双指针的方式来计算,先移动n为firstNode,然后second从dummy开始移动,直到first=null的时候,那么就找到了要删除的前一个元素

82. 删除排序链表中的重复元素 II

class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        ListNode dummy = new ListNode();
        dummy.next = head;

        ListNode left = dummy.next;
        ListNode prev = dummy;
        while (left != null && left.next != null) {
            ListNode next = left.next;
            if ( next.val != left.val) {
                if (prev.next == left) {
                    prev = left;
                    left = next;
                } else {
                    prev.next = next;
                    left = next;
                }
            } else {
                left = next;
            }
        }

        if (prev.next != left) {
            prev.next = null;
        }

        return dummy.next;
    }
}

解题思路:双指针

1、判断相邻元素是否相等,如果相等,那么移动left指针

2、如果不相等,判断,prev和left是否相等

  • 如果相等,那么prev和left一起移动
  • 如果不等,那么删除元素,prev.next = next

3、每次,不管什么条件,都移动left指针

4、最后,判断prev.next是否等于left,如果不等,那么说明后面的元素重复,那么prev.next置为null即可

61. 旋转链表

解题思路:

1、找到size

2、求余,找到准确要走几步路

3、找到头尾,以及分开的时候的头尾

4、断开,重连

5、边界情况处理:k=0, head.size == 1, k%size == 0,都直接return即可

class Solution {
    public ListNode rotateRight(ListNode head, int k) {
        if (head == null) {
            return null;
        }
        if (k == 0 || head.next == null){
            return head;
        }

        ListNode first = head;
        ListNode last = head;
        int size = 0;
        while(last != null) {
            size++;
            if (last.next == null) {
                break;
            }
            last = last.next;
        }

        int moveSize = k % size;
        if (moveSize == 0) {
            return head;
        }

        ListNode prev = first;
        for(int i = 0; i < size - moveSize; i++) {
            prev = first;
            first = first.next;
        }

        prev.next = null;
        last.next = head;

        return first;

    }
}

86. 分隔链表

class Solution {
    public ListNode partition(ListNode head, int x) {
        if (head == null) {
            return head;
        }
        ListNode dummyNode1 = new ListNode();
        ListNode prev = dummyNode1;
        ListNode dummyNode2 = new ListNode();
        ListNode second = dummyNode2;

        ListNode pointer = head;

        while (pointer != null) {
            if (pointer.val < x) {
                prev.next = pointer;
                prev = prev.next;
            } else {
                second.next = pointer;
                second = second.next;
            }
            pointer = pointer.next;
        }
        prev.next = dummyNode2.next;
        return dummyNode1.next;
    }
}

下面这段代码真正的问题是:
把链表“拆”成两段以后,第二段(≥x 段)的尾节点 second 仍然指向原链表中它后面的某个节点,于是两段一接起来就形成了一个环,后面所有节点都被无限循环访问,程序可能超时、输出错误甚至死循环。

举个例子:
原链表 1 → 4 → 3 → 2 → 5 → 2,x = 3
循环结束后 second 停在最后一个 2,它的 next 仍然指向 5(原链表里的下一个节点)。
当你执行 prev.next = dummyNode2.next 把两段拼起来时,链表变成
1 → 2 → 2 → 4 → 3 → 5 → 2 → 4 → 3 → 5 → 2 → …
永远走不到 null,于是“不奏效”。


改正旧直觉,建立新直觉

  1. 旧直觉:“只要把节点按值拆成两段,再把第二段的头接到第一段的尾就完事了。”

  2. 问题根源:链表节点不是复制出来的新对象,它们原来的 next 指针还在。拆链时必须把“当前段”的尾节点的 next 显式断开,否则它会带着原来后面的整条子链一起留下来,造成环。

  3. 新直觉(以后写拆链代码时默念):
    “每拆完一段,立刻把尾巴的 next 置为 null。”
    也就是

    second.next = null;   // 关键一句
    

    放在拼接之前即可。


修正后的代码(只动一行)

class Solution {
    public ListNode partition(ListNode head, int x) {
        if (head == null) return head;

        ListNode dummy1 = new ListNode(), tail1 = dummy1;
        ListNode dummy2 = new ListNode(), tail2 = dummy2;

        for (ListNode p = head; p != null; p = p.next) {
            if (p.val < x) {
                tail1.next = p;
                tail1 = tail1.next;
            } else {
                tail2.next = p;
                tail2 = tail2.next;
            }
        }

        tail2.next = null;      // 断开第二段尾巴,防止成环
        tail1.next = dummy2.next;
        return dummy1.next;
    }
}

以后凡是“把一条链拆成多条再拼接”的场景,第一时间把每条链的尾节点 next 设成 null,就能彻底避免这一类隐蔽的成环 bug。

146. LRU 缓存

解题思路:

1、使用哈希表,来实现O(1)级别的get和put

2、使用一个双向链表来实现LRU,维护头节点和尾节点,实现基本操作比如addToHead, removeNode, moveToHead, removeTail;

public class LRUCache {
    class DLinkedNode {
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        public DLinkedNode() {}
        public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
    }

    private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
    private int size;
    private int capacity;
    private DLinkedNode head, tail;

    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        // 使用伪头部和伪尾部节点
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            return -1;
        }
        // 如果 key 存在,先通过哈希表定位,再移到头部
        moveToHead(node);
        return node.value;
    }

    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            // 如果 key 不存在,创建一个新的节点
            DLinkedNode newNode = new DLinkedNode(key, value);
            // 添加进哈希表
            cache.put(key, newNode);
            // 添加至双向链表的头部
            addToHead(newNode);
            ++size;
            if (size > capacity) {
                // 如果超出容量,删除双向链表的尾部节点
                DLinkedNode tail = removeTail();
                // 删除哈希表中对应的项
                cache.remove(tail.key);
                --size;
            }
        }
        else {
            // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
            node.value = value;
            moveToHead(node);
        }
    }

    private void addToHead(DLinkedNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void moveToHead(DLinkedNode node) {
        removeNode(node);
        addToHead(node);
    }

    private DLinkedNode removeTail() {
        DLinkedNode res = tail.prev;
        removeNode(res);
        return res;
    }
}
posted @ 2025-11-13 21:12  coder江  阅读(5)  评论(0)    收藏  举报