经典算法题总结:链表篇

链表的题目注意画图辅助思考防止断链;如果需要构造新的链表使用伪头结点(dummyHead)可以简化操作;双指针(快慢指针,左右指针)。

链表的递归算法:只要是递归形式的遍历,都可以有前序位置和后序位置,分别在递归之前和递归之后。所谓前序位置,就是刚进入一个节点(元素)的时候,后序位置就是即将离开一个节点(元素)的时候,那么进一步,你把代码写在不同位置,代码执行的时机也不同:

链表数据结构定义:

public class ListNode {
    public int val;
    public ListNode next;

    public ListNode() {}
    public ListNode(int val) {
        this.val = val;
    }
    public ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }

}


public class DoubleListNode {
    public int key;
    public int value;
    public DoubleListNode pre;
    public DoubleListNode next;

    public DoubleListNode() {}

    public DoubleListNode(int key, int value) {
        this.key = key;
        this.value = value;
    }

}

206. 反转链表(⭐️⭐️)

思路

递归实现反转链表常常用来考察递归思想,我这里就用纯递归来翻转链表。对于递归算法,最重要的就是明确递归函数的定义。具体来说,我们的 reverse 函数定义是这样的:输入一个节点 head,将「以 head 为起点」的链表反转,并返回反转之后的头结点。明白了函数的定义,再来看这个问题。比如说我们想反转这个链表:

那么输入 reverse(head) 后,会在这里进行递归:

ListNode last = reverse(head.next);

不要跳进递归(你的脑袋能压几个栈呀?),而是要根据刚才的函数定义,来弄清楚这段代码会产生什么结果:

这个 reverse(head.next) 执行完成后,整个链表就成了这样:

并且根据函数定义,reverse 函数会返回反转之后的头结点,我们用变量 last 接收了。

现在再来看下面的代码:

head.next.next = head;

接下来:

head.next = null;
return last;

代码

public class ReverseList {

    public ListNode reverseListIterationVersion(ListNode head) {
        if (head == null || head.next == null) {
            return null;
        }
        ListNode newHead = null;
        ListNode next = null;
        ListNode cur = head;
        while (cur != null) {
            next = cur.next;
            cur.next = newHead;
            newHead = cur;
            cur = next;
        }
        return head;
    }

    public ListNode reverseListRecursionVersion(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode last = reverseListRecursionVersion(head.next);
        head.next.next = head;
        head.next = null;
        return last;
    }

}

复杂度

  • 时间复杂度:O(N)
  • 空间复杂度:迭代:O(1),递归: O(N)

146. LRU 缓存(⭐️⭐️)

思路

LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。哈希表即为普通的哈希映射(HashMap),通过缓存数据的键快速映射到其在双向链表中的位置

这是一个LRU(Least Recently Used)缓存的实现,用于缓存键值对,保持最近访问的数据在缓存中,并在缓存容量不足时淘汰最久未使用的数据。

构造方法:

  • LRUCache(int capacity): 构造LRU缓存对象,传入缓存容量作为参数。

方法:

  • int get(int key): 获取缓存中指定键的值,如果不存在则返回-1,存在则返回对应的值,并将该节点移到链表头部,表示最近使用。
  • void put(int key, int value): 向缓存中插入或更新键值对。如果缓存中已存在该键,则更新对应值并将节点移到链表头部;如果不存在该键,则插入新节点到链表头部,并更新哈希表。如果插入节点后缓存容量超出限制,则移除链表尾部的节点,并从哈希表中删除对应键。

成员变量:

  • HashMap<Integer, Node> cache: 哈希表用于快速查找缓存中的节点。
  • int size: 当前缓存大小。
  • int capacity: 缓存容量。
  • Node head: 链表头部表示最近使用。
  • Node tail: 链表尾部表示最久未使用。

私有方法:

  • removeNode(Node node): 移除指定节点。
  • addToHead(Node node): 将节点添加到链表头部。
  • removeTail(): 移除链表尾部节点。
  • moveToHead(Node node): 将节点移到链表头部,表示最近使用。

内部类:

  • Node: 链表节点类,用于存储键值对及构建双向链表。包含键、值、前驱节点和后继节点。

代码

import java.util.HashMap;

public class LRUCache {
    private HashMap<Integer, DoubleListNode> cache = new HashMap<>();
    private int size = 0; // 当前缓存的大小
    private int capacity = 0; // 当前缓存的容量
    private DoubleListNode head = new DoubleListNode();
    private DoubleListNode tail = new DoubleListNode();

    public LRUCache(int capacity) {
        this.capacity = capacity;
        head.next = tail;
        tail.pre = head;
    }

    public int get(int key) {
        DoubleListNode node = cache.get(key);
        if (node == null) {
            return -1;
        } else {
            moveToHead(node);
            return node.value;
        }
    }

    public void put(int key, int value) {
        DoubleListNode node = cache.get(key);
        if (node == null) {
            DoubleListNode newNode = new DoubleListNode(key, value);
            cache.put(key, newNode);
            addToHead(newNode);
            size++;
            if (size > capacity) {
                cache.remove(tail.pre.key);
                removeTail();
                size--;
            }
        } else {
            node.value = value;
            moveToHead(node);
        }
    }

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

    private void addToHead(DoubleListNode node) {
        // 先改变 node 的指针
        node.pre = head;
        node.next = head.next;
        // 再从右到左改变节点的指针,避免断链
        head.next.pre = node;
        head.next = node;
    }

    private void removeTail() {
        DoubleListNode node = tail.pre;
        removeNode(node);
    }

    private void removeNode(DoubleListNode node) {
        // 从右往左边改变指针,避免断链
        node.next.pre = node.pre;
        node.pre.next = node.next;
    }

}

复杂度

  • 时间复杂度:对于 put 和 get 都是 O(1)
  • 空间复杂度:O(capacity)

升级:过 x 秒之后没有操作的元素会自动过期删除(开一个定时任务)。

25. K 个一组翻转链表(⭐️⭐️)

思路

迭代:

两个关键的函数定义:

private ListNode findEnd(ListNode head, int k)
private void reverse(ListNode start, ListNode end)

注意前后两个节点的 next 指针的处理。

递归:

代码

public class ReverseKGroup {

    // 解法一:迭代
    public ListNode reverseKGroupIterationVersion(ListNode head, int k) {
        ListNode end = findEnd(head, k);
        if (end == null) {
            return head;
        }
        ListNode start = head;
        reverseIterationVersion(start, end);
        head = end;
        ListNode preEnd = start;
        while (preEnd.next != null) {
            start = preEnd.next;
            end = findEnd(start, k);
            if (end == null) {
                return head;
            }
            reverseIterationVersion(start, end);
            preEnd.next = end;
            preEnd = start;
        }
        return head;
    }

    private ListNode findEnd(ListNode head, int k) {
        ListNode end = head;
        while (--k > 0 && end != null) {
            end = end.next;
        }
        return end;
    }
    
    private void reverseIterationVersion(ListNode start, ListNode end) {
        ListNode subStart = end.next;
        ListNode newHead = null;
        ListNode cur = start;
        ListNode next = null;

        while (cur != subStart) {
            next = cur.next;
            cur.next = newHead;
            newHead = cur;
            cur = next;
        }

        start.next = subStart;
    }
    
    // 解法二:递归
    public ListNode reverseKGroupRecursionVersion(ListNode head, int k) {
        if (head == null) {
            return null;
        }
        // 区间[a, b),包含 k 个待反转的元素
        ListNode a = head;
        ListNode b = head;
        for (int i = 0; i < k; i++) {
            // 不足 k 个,不需要反转
            if (b == null) {
                return head;
            }
            b = b.next;
        }
        ListNode newHead = reverseRecursionVersion(a, b);
        a.next = reverseKGroupRecursionVersion(b, k);
        return newHead;
    }

    private ListNode reverseRecursionVersion(ListNode a, ListNode b) {
        ListNode newHead = null;
        ListNode cur = a;
        ListNode next = null;

        while (cur != b) {
            next = cur.next;
            cur.next = newHead;
            newHead = cur;
            cur = next;
        }

        return newHead;
    }

}

复杂度

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

21. 合并两个有序链表(⭐️⭐)

思路

代码

public class MergeTwoLists {

    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dummyHead = new ListNode(-1);
        ListNode p = dummyHead;
        ListNode p1 = l1;
        ListNode p2 = l2;

        while (p1 != null && p2 != null) {
            if (p1.val < p2.val) {
                p.next = p1;
                p1 = p1.next;
            } else {
                p.next = p2;
                p2 = p2.next;
            }
            p = p.next;
        }
        if (p1 != null) {
            p.next = p1;
        }
        if (p2 != null) {
            p.next = p2;
        }
    }

    return dummyHead.next;
}

复杂度

  • 时间复杂度:O(M + N)
  • 空间复杂度:O(1)

141. 环形链表(⭐️⭐️)

思路

代码

public class Solution {

    public boolean hasCycle(ListNode head) {
        // 如果链表为空或只有一个节点,肯定不存在循环,直接返回false
        if (head == null || head.next == null) {
            return false; 
        }
        ListNode slow = head;
        ListNode fast = head;

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

        return false;
    }

}

复杂度

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

142. 环形链表 II(⭐️⭐️)

思路

代码

public class Solution {

    public ListNode detectCycle(ListNode head) {
        if (head == null || head.next == null) {
            return null;
        }
        ListNode fast = head;
        ListNode slow = head;

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

        slow = head;

        while (slow != fast) {
            slow = slow.next;
            fast = fast.next;
        }

        return fast;
    }

}

复杂度

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

23. 合并 K 个升序链表(⭐️⭐️)

思路

代码

import java.util.PriorityQueue;

public class MergeKLists {

    public ListNode mergeKLists(ListNode[] lists) {
        if (lists == null || lists.length == 0) {
            return null;
        }

        PriorityQueue<ListNode> priorityQueue = new PriorityQueue<>(
                Comparator.comparingInt(o -> o.val)
        );

        ListNode dummyHead = new ListNode(-1);
        ListNode cur = dummyHead;

        for (ListNode list : lists) {
            if (list == null) {
                continue;
            }
            priorityQueue.add(list);
        }

        while (!priorityQueue.isEmpty()) {
            ListNode node = priorityQueue.poll();
            cur.next = node;
            cur = cur.next;
            if (node.next != null) {
                priorityQueue.add(node.next);
            }
        }


        return dummyHead.next;
    }

}

复杂度

  • 时间复杂度:O(K * N * log K),其中 K 为最小堆的最大的元素数量
  • 空间复杂度:O(K)

160. 相交链表(⭐️⭐️)

思路

pA 和 pB一同走过 a + b + c 路径长度时相遇在相交点。

代码

public class Solution {
    
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) {
            return null;
        }
        ListNode pA = headA;
        ListNode pB = headB;
        while (pA != pB) {
            pA = pA == null ? headB : pA.next;
            pB = pB == null ? headA : pB.next;
        }
        return pA;
    }
    
}

复杂度

  • 时间复杂度:O(M + N)
  • 空间复杂度:O(1)

143. 重排链表(⭐️⭐️)

思路

代码

import java.util.ArrayList;

public class ReorderList {

    // 方法一:利用线性表,双指针遍历
    public void reorderListVerison1(ListNode head) {
        if (head == null) {
            return;
        }
        ArrayList<ListNode> list = new ArrayList<>();
        ListNode cur = head;
        while (cur != null) {
            list.add(cur);
            cur = cur.next;
        }
        int left = 0;
        int right = list.size() - 1;
        while (left < right) {
            list.get(left).next = list.get(right);
            left++;
            if (left == right) {
                break;
            }
            list.get(right).next = list.get(left);
            right--;
        }
        list.get(right).next = null;
    }

    // 方法二:快慢指针找到中间节点,再将后面半个链表反转,然后再插入
    public void reorderListVersion2(ListNode head) {
         if (head == null) {
             return;
         }
         ListNode mid = findMiddleNode(head);
         ListNode l1 = head;
         ListNode l2 = mid.next;
         mid.next = null;
         l2 = reverseList(l2);
         mergeList(l1, l2);
    }

    private void mergeList(ListNode l1, ListNode l2) {
        ListNode l1Next = null;
        ListNode l2Next = null;

        while (l1 != null && l2 != null) {
            l1Next = l1.next;
            l2Next = l2.next;

            l1.next = l2;
            l1 = l1Next;
            l2.next = l1;

            l2 = l2Next;
        }
    }

    private ListNode reverseList(ListNode head) {
        ListNode newHead = null;
        ListNode cur = null;
        ListNode next = null;

        while (cur != null) {
            next = cur.next;
            cur.next = newHead;
            newHead = cur;
            cur = next;
        }

        return newHead;
    }

    private ListNode findMiddleNode(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

}

复杂度

  • 时间复杂度:O(N)
  • 空间复杂度:方法一:O(N),方法二:O(1)

19. 删除链表的倒数第 N 个结点(⭐️⭐️)

思路

双指针。

代码

public class RemoveNthFromEnd {

    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummyHead = new ListNode(-1);
        dummyHead.next = head;
        ListNode x = findFromEnd(dummyHead, n + 1);
        x.next = x.next.next;
        return dummyHead.next;
    }

    // 双指针找到倒数第 n 个节点
    private ListNode findFromEnd(ListNode head, int n) {
        ListNode p1 = head;
        for (int i = 0; i < n; i++) {
            p1 = p1.next;
        }
        ListNode p2 = head;
        while (p1 != null) {
            p1 = p1.next;
            p2 = p2.next;
        }
        return p2;
    }

}

复杂度

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

82. 删除排序链表中的重复元素 II(⭐️⭐️)

思路

代码

public class DeleteDuplicates {

    public ListNode deleteDuplicates(ListNode head) {
        if  (head == null) {
            return null;
        }

        ListNode dummyHead = new ListNode(-1, head);
        ListNode cur = dummyHead;

        while (cur.next != null && cur.next.next != null) {
            if (cur.next.val == cur.next.next.val) {
                int x = cur.next.val;
                while (cur.next != null && cur.next.val == x) {
                    cur.next = cur.next.next;
                }
            } else {
                cur = cur.next;
            }
        }

        return dummyHead.next;
    }

}

复杂度

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

148. 排序链表(⭐️⭐️)

思路

方法一:自顶向下归并排序

方法二:自底向上归并排序

代码

public class SortList {

    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        // 使用快慢指针找到链表中点,分为左右两个部分
        ListNode slow = head;
        ListNode fast = head.next;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }

        ListNode mid = slow.next;
        slow.next = null;
        ListNode left = sortList(head);
        ListNode right = sortList(mid);

        // 合并左右两个有序链表
        ListNode dummyHead = new ListNode(-1);
        ListNode  cur = dummyHead;
        while (left != null && right != null) {
            if (left.val < right.val) {
                cur.next = left;
                left = left.next;
            } else {
                cur.next = right;
                right = right.next;
            }
            cur = cur.next;
        }
        cur.next = left == null ? right :left;

        return dummyHead.next;
    }

}

在这个代码片段中,快指针 (fast) 需要先走一步的原因是为了确保链表在长度为偶数时,slow 指针能够准确地指向链表的中间节点。

具体来说,如果 fastslow 指针都从链表头开始同步移动,每次 fast 移动两步,slow 移动一步,那么当 fast 到达链表末尾时,slow 指针会停在整个链表长度的中间位置(无论链表长度是奇数还是偶数)。

但是,如果不先让 fast 走一步,对于偶数长度的链表,slow 指针会在中间偏后的位置。通过让 fast 指针先走一步,当链表长度为偶数时,slow 指针会正好停在中间节点的前一个位置,从而使链表能够正确地分成两个部分。

复杂度

  • 时间复杂度:O(N * log(N))
  • 空间复杂度:O(log(N))

2. 两数相加(⭐️⭐)

思路

代码

public class AddTwoNumbers {

    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode dummyHead = new ListNode(-1);
        ListNode cur = dummyHead;
        int carry = 0; // 进位

        while (l1 != null || l2 != null) {
            int l1Value = l1 == null ? 0 : l1.val;
            int l2Value = l2 == null ? 0 : l2.val;
            int sum = l1Value + l2Value + carry;
            carry = sum / 10;
            sum = sum % 10;
            cur.next = new ListNode(sum);
            cur = cur.next;

            if (l1 != null) {
                l1 = l1.next;
            }
            if (l2 != null) {
                l2 = l2.next;
            }
        }

        if (carry == 1) {
            cur.next = new ListNode(carry);
        }

        return dummyHead.next;
    }

}

复杂度

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

  • 23
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值