Leetcode百题斩-链表

发布于:2025-06-18 ⋅ 阅读:(19) ⋅ 点赞:(0)

这个专题算是经典中的经典了,从之前的刷题记录就可以看出,一共14题,其中5题之前都刷过。还是本着时间有限的原则,刷过的题看一下之前的记录就自己思考了,专注冲新题

首先,和二叉树一样,先构造一个链表节点的数据结构。

/*
Author Owen_Q
*/
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;
    }
 
    /**
     * 构建有环链表
     * @param nodes 列表元素
     * @param circlePos 环位置
     * @return 有环列表
     */
    public static ListNode build(List<Integer> nodes, Integer circlePos) {
        ListNode list = build(nodes);
        int len = nodes.size();
        if (circlePos != null && circlePos < len && circlePos >= 0) {
            ListNode circle = list;
            for (int i = 0; i < circlePos; i++) {
                circle = circle.next;
            }
            ListNode tail = list;
            while (tail.next != null) {
                tail = tail.next;
            }
            tail.next = circle;
        }
        return list;
    }
 
    /**
     * 构建无环链表
     * @param nodes 列表元素
     * @return 无环列表
     */
    public static ListNode build(List<Integer> nodes) {
        if (nodes == null || nodes.isEmpty()) {
            return null;
        }
        return new ListNode(nodes.get(0), buildRecursion(nodes, 1));
    }
 
    private static ListNode buildRecursion(List<Integer> nodes, int pos) {
        int len = nodes.size();
        if (pos == len) {
            return null;
        }
        return new ListNode(nodes.get(pos), buildRecursion(nodes, pos + 1));
    }
 
    public static List<Integer> toList(ListNode node) {
        ListNode now = node;
        ListNode circle = circleEntrance(now);
        boolean hasCircle = circle != null;
        List<Integer> result = new ArrayList<>();
        while (now != null) {
            // 若链表有环,则用null隔开链与环
            if (now == circle) {
                if (hasCircle) {
                    hasCircle = false;
                } else {
                    break;
                }
                result.add(null);
            }
            result.add(now.val);
            now = now.next;
        }
        return result;
    }
    
    public static ListNode circleEntrance(ListNode head) {
        HashSet<ListNode> set = new HashSet<>();
        ListNode now = head;
        while (now != null) {
            if (set.contains(now)) {
                return now;
            }
            set.add(now);
            now = now.next;
        }
        return null;
    }
 
    @Override
    public String toString() {
        return toList(this).toString();
    }
}

历史题

历史题刷的原则是先看之前的文章,回忆之前的心得和技巧,然后根据思路自己去写,尽可能不要画过多的时间

链表经典问题

递归法求解面试三大经典链表问题_递归遍历链表题目-CSDN博客

来个承上启下,上个专题结束前,看了之前写的一篇二叉树和链表汇总在一起的博客,上次说了二叉树篇,这次就先以剩余的两道链表题开头

206. Reverse Linked List[Easy]

思路:分递归和非递归写法,非递归就是三指针操作断链重连的过程,递归只需要将当前节点断开,然后将下一个节点指向当前节点即可,后续都可以递归完成。

翻转链表

不得不说五年过去了,编译器变快了,但是貌似牺牲了一点内存空间

此处,为了节约时间我们只写一下递归做法,非递归做法看之前的博客即可

/*
Author Owen_Q
*/
class ListReversor {
    public static ListNode reverseList(ListNode head) {
        if (head == null) {
            return null;
        }
        if (head.next == null) {
            return head;
        }
        ListNode reversedNode = reverseList(head.next);
        head.next.next = head;
        head.next = null;
        return reversedNode;
    }
}

21. Merge Two Sorted Lists[Easy]

思路:也分为递归和非递归。非递归用一个单独的指针去比较两个链表进行拼接,而递归就更简单了,直接复用小的链表头进行操作,将其连到递归下一项上

合并链表

同样这里只做递归写法,非递归还是看原博客

/*
Author Owen_Q
*/
class ListMerger {
    public static ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if (list1 == null) {
            return list2;
        }
        if (list2 == null) {
            return list1;
        }
        if (list1.val < list2.val) {
            list1.next = mergeTwoLists(list1.next, list2);
            return list1;
        } else {
            list2.next = mergeTwoLists(list1, list2.next);
            return list2;
        }
    }
}

链表交点问题

链表交点问题解析-CSDN博客

142. Linked List Cycle II[Medium]

思路:最简单的方法当然是哈希法,直接用哈希表存储已经遍历过的节点,若重复读取则可以直接找到入环口,该方法直接被写在了链表数据结构的 circleEntrance() 方法中

此外双指针法思路很巧妙,先快慢指针判环,再用双指针找入口,具体思路还是看之前的原文

链表判环寻找环入口

/*
Author Owen_Q
*/
public class CircleList2 {
    public static ListNode detectCycle(ListNode head) {
        ListNode slow = head, fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                slow = head;
                while (slow != fast) {
                    slow = slow.next;
                    fast = fast.next;
                }
                return slow;
            }
        }
        return null;
    }
}

160. Intersection of Two Linked Lists[Easy]

思路:直接将链表分别正反首位相连形成两个新链表,并同时遍历两个链表,相交处即为来拿表交点。

当然,为了方便起见,这里我们也直接用哈希法来实现,链表合并法还是看之前的原文这里不再实现

链表寻找交点

/*
Author Owen_Q
*/
public class IntersectionNode {
    public static ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) {
            return null;
        }
        Set<ListNode> set = new java.util.HashSet<>();
        ListNode now = headA;
        while (now != null) {
            set.add(now);
            now = now.next;
        }
        now = headB;
        while (now != null) {
            if (set.contains(now)) {
                return now;
            }
            now = now.next;
        }
        return null;
    }
}

哈希法实现按键值访问链表

LeetCodeGOGOGO刷题记01——链表优化(哈希法实现按键值访问链表)_链表 优化-CSDN博客

146. LRU Cache[Medium]

思路:这题的核心就是需要同时支持按位置取值和按键值取值,普通数组可以实现按位置取值,而哈希表则可以实现按键值取值,于是可以将两种数据结构结合一下。先用一个数组存储元素的位置,再用一个哈希表将键值映射到当前值在数组中的位置。每次读取的时候,将读出来的元素重新塞到数组尾并更新哈希表。插入的时候,判断数组元素是否满,若满了,则取出数组中最靠前的元素将其在哈希表中删除即可。为了方便数组的删除,用两个变量来存储数组的有效边界即可。

这么一想,其实用数组模拟了链表,完全不需要用真正的列表数据结构即可完成操作

回看一下当年的解题思路,不免发现这么多年的工作经验还是明显有进步有效果的,特别是针对这种非算法思维而是工程实现类题目,明显能力强了很多

哈希法实现按键值访问链表

/*
Author Owen_Q
*/
class LRUCache {
private int capacity;
 
    private int currentEnd;
 
    private int currentStart;
 
    private int currentCapacity;
 
    private List<Pair<Integer, Integer>> cacheValue;
 
    private Map<Integer, Integer> cachePos;
 
    public LRUCache(int capacity) {
        this.capacity = capacity;
        currentStart = 0;
        currentEnd = 0;
        currentCapacity = 0;
        cacheValue = new ArrayList<>();
        cachePos = new HashMap<>();
    }
 
    public int get(int key) {
        if (cachePos.containsKey(key)) {
            int value = cacheValue.get(cachePos.get(key)).getValue();
            cacheValue.set(cachePos.get(key), null);
            cacheValue.add(new Pair<>(key, value));
            cachePos.put(key, currentEnd++);
            return value;
        }
        return -1;
    }
 
    public void put(int key, int value) {
        if (get(key) == -1) {
            if (currentCapacity == capacity) {
                while (cacheValue.get(currentStart) == null) {
                    currentStart++;
                }
                int expiredCacheKey = cacheValue.get(currentStart).getKey();
                cacheValue.set(currentStart, null);
                cachePos.remove(expiredCacheKey);
            } else {
                currentCapacity++;
            }
        } else {
            cacheValue.set(cachePos.get(key), null);
        }
        cacheValue.add(new Pair<>(key, value));
        cachePos.put(key, currentEnd++);
    }
 
}
 
/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

新题

141. Linked List Cycle[Easy]

思路:这一题就是上面编号142题链表判环的简化版本,直接用上述代码即可

/*
Author Owen_Q
*/
public class CircleList {
    public static boolean hasCycle(ListNode head) {
        return CircleList2.detectCycle(head) != null;
    }
}

234. Palindrome Linked List[Easy]

思路:判断链表是否回文,方法有很多,主要有数组法,回溯法和链表翻转法

数组法

数组法就是将链表转化为数组,然后双指针进行判断回文

/*
Author Owen_Q
*/
public class PalindromeList() 
   public static boolean isPalindromeByArray(ListNode head) {
        List<Integer> nodes = ListNode.toList(head);
        int start = 0;
        int end = nodes.size() - 1;
        while (start < end) {
            if (!nodes.get(start).equals(nodes.get(end))) {
                return false;
            }
            start++;
            end--;
        }
        return true;
    }
}

回溯法

回溯法就是利用递归回溯从后向前的特性,再手动记录一个从前向后的链表,即可实现收尾进行判断

/*
Author Owen_Q
*/
public class PalindromeList{
    public static boolean isPalindromeRecursion(ListNode head) {
        frontNode = head;
        return checkPalindromeByRecursion(head);
    }
 
    private static ListNode frontNode;
 
    private static boolean checkPalindromeByRecursion(ListNode head) {
        if (head == null || frontNode == null) {
            return true;
        }
        if (!checkPalindromeByRecursion(head.next)) {
            return false;
        }
        if (head.val != frontNode.val) {
            return false;
        }
        frontNode = frontNode.next;
        return true;
    }
}

链表翻转法

思路: 首先用快慢指针找到链表中点位置,然后将后半个链表翻转,再利用双指针分别遍历链表的前后两个部分即可进行判断,最后记得将链表翻转回来即可,翻转链表可以用上面编号206题翻转链表的代码

/*
Author Owen_Q
*/
public class PalindromeList{
    public static boolean isPalindromeByReverseList(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        ListNode reverseList = ListReversor.reverseList(slow);
        ListNode back = reverseList;
        ListNode front = head;
        while (back != null) {
            if (back.val != front.val) {
                ListReversor.reverseList(reverseList);
                return false;
            }
            back = back.next;
            front = front.next;
        }
        ListReversor.reverseList(reverseList);
        return true;
    }
}

148. Sort List[Medium]

思路:说到排序,首先先复习一下排序基础

基础排序大赏

那么链表的排序,其实最方便的还是归并排序了,接下来就用递归和非递归两种方式来实现一下

自上而下递归法

递归法相比较于非递归法,一般思路都十分清晰。首先快慢指针找到链表中点,这个时候需要预备一个pre指针用于记录中点前的节点,用于将前后链表断链。断链后将前后链表分别递归排序,最后可以用上面编号21题合并链表的代码,将前后链表合并即可

/*
Author Owen_Q
*/
class SortList {
    public static ListNode sortListFromTop(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode slow = head, fast = head, pre = null;
        while (fast != null && fast.next != null) {
            pre = slow;
            slow = slow.next;
            fast = fast.next.next;
        }
        pre.next = null;
        ListNode left = sortListFromTop(head);
        ListNode right = sortListFromTop(slow);
        return ListMerger.mergeTwoLists(left, right);
    }
}

自下而上非递归法

非递归写法就会复杂一些了,核心思路就是从小到大排序链表,先将链表分割为长度为1的子链表,然后两两合并,再将合并后的链表两两合并,依次类推最终完成排序。当然,为了避免链表先后信息丢失,每次断链的操作会在合并链表前,合并链表完成后需要立刻将链表拼接。这就需要一个pre指针来记录上一个被合并的链表,并将当前被合并的链表与之前的链表拼接。分隔链表的同时,记录分割后的位置有利于循环的向后遍历,最后在开始的时候记录一下链表长度,作为循环结束的标志即可。

/*
Author Owen_Q
*/
class SortList {
    public static ListNode sortListFromBottom(ListNode head) {
        int length = 0;
        ListNode cur = head;
        while (cur != null) {
            length++;
            cur = cur.next;
        }
        for (int i = 1; i < length; i <<= 1) {
            ListNode pre = null;
            cur = head;
            while (cur != null) {
                ListNode left = cur;
                ListNode right = split(cur, i);
                cur = split(right, i);
                ListNode merge = ListMerger.mergeTwoLists(left, right);
                if (pre == null) {
                    head = merge;
                    pre = head;
                } else {
                    pre.next = merge;
                }
                while (pre.next != null) {
                    pre = pre.next;
                }
            }
        }
        return head;
    }
 
    private static ListNode split(ListNode head, int length) {
        ListNode cur = head;
        while (length > 1 && cur != null) {
            cur = cur.next;
            length--;
        }
        if (cur == null) {
            return null;
        }
        ListNode next = cur.next;
        cur.next = null;
        return next;
    }
}

138. Copy List with Random Pointer[Medium]

思路:随机链表,在普通链表基础上,包含一个新的指针指向链表中随机的元素,现在要求将这个链表复制一份。

首先先和普通链表一样,先构造一份随机链表

/*
Author Owen_Q
*/
private static class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }

    private static List<Node> nodeList = new ArrayList<>();
    private static Map<Node, Integer> nodePosMap = new HashMap<>();

    public static Node buildNode(List<List<Integer>> nodes) {
        if (nodes == null || nodes.isEmpty()) {
            return null;
        }
        nodeList.clear();
        for (List<Integer> node : nodes) {
            nodeList.add(new Node(node.get(0)));
        }
        int nodeSize = nodeList.size();
        for (int i = 0; i < nodeSize; i++) {
            Node node = nodeList.get(i);
            if (i != nodeSize - 1) {
                node.next = nodeList.get(i + 1);
            }
            if (nodes.get(i).get(1) != null) {
                node.random = nodeList.get(nodes.get(i).get(1));
            }
        }
        return nodeList.get(0);
    }

    private List<List<Integer>> toList() {
        nodePosMap.clear();
        List<List<Integer>> result = new ArrayList<>();
        Node cur = this;
        int pos = 0;
        while (cur != null) {
            List<Integer> node = new ArrayList<>();
            node.add(cur.val);
            nodePosMap.put(cur, pos);
            result.add(node);
            cur = cur.next;
            pos++;
        }
        cur = this;
        pos = 0;
        while (cur != null) {
            List<Integer> node = result.get(pos);
            node.add(nodePosMap.get(cur.random));
            cur = cur.next;
            pos++;
        }
        return result;
    }

    @Override
    public String toString() {
        return toList().toString();
    }
}

这一题相比较于传统链表,最大的问题就是,链表节点中的随机指针指向的节点不知道在何时会被创建出来,于是,如果能提前把链表模型建出来,再去补随机指针就是个核心思路。

哈希法

哈希法就是通过将新旧链表节点进行映射。第一轮映射完之后,第二轮遍历即可将随机指针补全

/*
Author Owen_Q
*/
public class RandomListCopyer {
    private static Map<Node, Node> copyMap = new HashMap<>();

    public static Node copyRandomListWithHash(Node head) {
        Node cur = head;
        while (cur != null) {
            copyMap.put(cur, new Node(cur.val));
            cur = cur.next;
        }
        cur = head;
        while (cur != null) {
            Node copyedNode = copyMap.get(cur);
            copyedNode.next = copyMap.get(cur.next);
            copyedNode.random = copyMap.get(cur.random);
            cur = cur.next;
        }
        return copyMap.get(head);
    }
}

链表拼接法

思路:相比于哈希法,将复制的新表节点存储在哈希映射表中。链表拼接法则直接借用了链表自身的结构,将复制表的每个节点直接连在原表的后方,依旧是两轮遍历后将随机指针补全,最后再加一轮遍历断链即可

/*
Author Owen_Q
*/
public class RandomListCopyer {

    public static Node copyRandomListAfterOld(Node head) {
        Node cur = head;
        while (cur != null) {
            Node copyedNode = new Node(cur.val);
            copyedNode.next = cur.next;
            cur.next = copyedNode;
            cur = copyedNode.next;
        }
        cur = head;
        while (cur != null) {
            Node copyedNode = cur.next;
            if (cur.random != null) {
                copyedNode.random = cur.random.next;
            }
            cur = copyedNode.next;
        }
        cur = head;
        Node copyedHead = head == null ? null : head.next;
        while (cur != null) {
            Node copyedNode = cur.next;
            cur.next = copyedNode.next;
            if (copyedNode.next != null) {
                copyedNode.next = cur.next.next;
            }
            cur = cur.next;
        }
        return copyedHead;
    }
}

24. Swap Nodes in Pairs[Medium]

思路:链表俩俩交换顺序。没什么说的,就是手撕代码,一定要细一定要细一定要细!还是分递归和非递归两种方案来写,直接上代码

递归法

/*
Author Owen_Q
*/
public class ListSwapper {
      if (head == null || head.next == null) {
            return head;
      }
      ListNode next = head.next;
      head.next = swapPairsWithRecursion(next.next);
      next.next = head;
      return next;
}

非递归法

/*
Author Owen_Q
*/
public class ListSwapper {
      if (head == null || head.next == null) {
          return head;
      }
      ListNode cur = head;
      ListNode result = head.next;
      while (cur != null && cur.next != null) {
          ListNode toSwap = cur.next;
          ListNode swapNext1 = toSwap.next;
          ListNode swapNext2 = swapNext1;
          if (swapNext2 != null && swapNext2.next != null) {
              swapNext2 = swapNext2.next;
          }
          cur.next = swapNext2;
          toSwap.next = cur;
          cur = swapNext1;
      }
      return result;  
}

19. Remove Nth Node From End of List[Medium]

思路:删除链表倒数第N个节点,由于链表前后有序,删除倒数需要两轮遍历,第一轮求链表长度,第二轮删除节点。但如果想要一轮遍历,双指针的优势就体现出来了。快指针先跑N格,慢指针用于删除

/*
Author Owen_Q
*/
public class ListEndRemover {

    public static ListNode removeNthFromEnd(ListNode head, int n) {
        if (head == null) {
            return head;
        }
        ListNode slow = head;
        ListNode fast = head;
        for (int i = 0; i < n; i++) {
            fast = fast.next;
        }
        if (fast == null) {
            return head.next;
        }
        while (fast.next != null) {
            slow = slow.next;
            fast = fast.next;
        }
        slow.next = slow.next.next;
        return head;
    }
}

2. Add Two Numbers[Medium]

思路:给定两个链表,表示数字的逆序存储,求两链表和所形成的链表

还好是逆序存储,那么从低位开始直接枚举求和然后记录一下进位就行了,否则还要判断一下链表长度进行位数配对

/*
Author Owen_Q
*/
public class ListSum {
    
    public static ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        if (l1 == null && l2 == null) {
            return null;
        }
        if (l1 == null) {
            return l2;
        }
        if (l2 == null) {
            return l1;
        }
        ListNode a1 = l1;
        ListNode a2 = l2;
        int now = a1.val + a2.val;
        boolean addOne = now >= 10;
        if (addOne) {
            now -= 10;
        }
        ListNode result = new ListNode(now);
        ListNode cur = result;
        a1 = a1.next;
        a2 = a2.next;
        while (a1 != null || a2 != null) {
            now = addOne ? 1 : 0;
            if (a1 != null) {
                now += a1.val;
                a1 = a1.next;
            }
            if (a2 != null) {
                now += a2.val;
                a2 = a2.next;
            }
            if (now >= 10) {
                now -= 10;
                addOne = true;
            } else {
                addOne = false;
            }
            cur.next = new ListNode(now);
            cur = cur.next;
        }
        if (addOne) {
            cur.next = new ListNode(1);
        }
        return result;
    }
}

25. Reverse Nodes in k-Group[Hard]

思路:这就是个纯手撕链表题,确实不难看出,leetcode的代码难度确实整体偏低,像这种稍微带一点逻辑的手撕题就能轻轻松松上hard了。断链,翻转,重连三步走,翻转链表可以用上面编号206题翻转链表的代码,别的也没啥好说的,直接上代码

/*
Author Owen_Q
*/
public class ListReversorForGroups {

    public static ListNode reverseKGroup(ListNode head, int k) {
        if (head == null || head.next == null || k <= 1) {
            return head;
        }
        ListNode result = head;
        ListNode cur = head;
        ListNode prev = null;
        while (cur != null) {
            ListNode groupEnd = cur;
            for (int i = 1; i < k && groupEnd != null; i++) {
                groupEnd = groupEnd.next;
            }
            if (groupEnd == null) {
                return result;
            }
            if (result == head) {
                result = groupEnd;
            }
            ListNode next = groupEnd.next;
            if (prev != null) {
                prev.next = null;
            }
            if (next != null) {
                groupEnd.next = null;
            }
            groupEnd = cur;
            cur = ListReversor.reverseList(cur);
            if (prev != null) {
                prev.next = cur;
            }
            if (next != null) {
                groupEnd.next = next;
            }
            cur = next;
            prev = groupEnd;
        }
        return result;
    }
}

23. Merge k Sorted Lists[Hard]

思路:合并多个有序链表,可以俩俩链表合并,也可以多链表同事合并

分组合并法

分组合并法就是将待合并链表俩俩分组合并,合并完后,再将合并后的链表俩俩分组合并,最终直到只剩下一个链表。合并链表的过程可以参考上面编号21题合并链表的代码

/*
Author Owen_Q
*/
public class ListMergerForMultitude {

    public static ListNode mergeKListsMerge2Lists(ListNode[] lists) {
        if (lists == null || lists.length == 0) {
            return null;
        }
        if (lists.length == 1) {
            return lists[0];
        }
        List<ListNode> nodeQueue = new ArrayList<>(Arrays.asList(lists));
        int st = 0;
        int en = lists.length - 1;
        while (st <= en - 1) {
            nodeQueue.add(ListMerger.mergeTwoLists(nodeQueue.get(st), nodeQueue.get(st + 1)));
            en++;
            st += 2;
        }
        return nodeQueue.get(en);
    }
}

优先队列法

当然也可以用优先队列,将每个列表的头节点加入优先队列,然后每次挑选最小元素出队列,再将该元素的后续节点加入队列

/*
Author Owen_Q
*/
public class ListMergerForMultitude {

    public static ListNode mergeKListsPriorityQueue(ListNode[] lists) {
        if (lists == null) {
            return null;
        }
        PriorityQueue<ListNode> queue = new PriorityQueue<>(Comparator.comparing(node -> node.val));
        for (ListNode list : lists) {
            if (list != null) {
                queue.add(list);
            }
        }
        if (queue.isEmpty()) {
            return null;
        }
        ListNode minNode = queue.poll();
        ListNode headNode = new ListNode(minNode.val);
        if (minNode.next != null) {
            queue.add(minNode.next);
        }
        ListNode currentNode = headNode;
        while (!queue.isEmpty()) {
            minNode = queue.poll();
            currentNode.next = new ListNode(minNode.val);
            currentNode = currentNode.next;
            if (minNode.next != null) {
                queue.add(minNode.next);
            }
        }
        return headNode;
    }
}

完结撒花


网站公告

今日签到

点亮在社区的每一天
去签到