49、面试高频题(二):链表反转、合并两个有序链表、判断链表是否有环、LRU缓存实现

说实话,链表这块是面试里的“钉子户”。

我面过不少人,前面聊得挺好,一到手写链表题就卡壳。其实不是不会,是紧张加上平时练得少。今天我把这四道高频题掰开揉碎讲清楚,你跟着走一遍,心里就有底了。

一、链表反转

这道题我当年第一次面大厂时就遇到了。面试官说“写个反转链表”,我脑子嗡一下——明明背过,手就是抖。

后来我总结了一个口诀:“三指针,步步移”

核心思路:用三个指针 prev、curr、next 依次反转每个节点的指向。

// 反转单链表
struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode* prev = NULL;
    struct ListNode* curr = head;
    struct ListNode* next = NULL;

    while (curr != NULL) {
        next = curr->next;   // 保存下一个节点
        curr->next = prev;   // 反转指向
        prev = curr;         // 前移
        curr = next;         // 前移
    }
    return prev;  // 新头节点
}

我的小技巧:画图!拿张纸画三个节点,箭头标来标去,比看十遍代码都管用。

注意:别忘了处理空链表和只有一个节点的情况。我见过有人一上来就解引用 NULL,直接崩了。

二、合并两个有序链表

这道题其实很生活化。你想想看,两个有序的数组怎么合并?链表也一样,只是操作的是指针。

我个人习惯用递归写,代码短,逻辑清晰。但面试时最好两种都会——递归和迭代。

// 递归版本
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2) {
    if (l1 == NULL) return l2;
    if (l2 == NULL) return l1;

    if (l1->val < l2->val) {
        l1->next = mergeTwoLists(l1->next, l2);
        return l1;
    } else {
        l2->next = mergeTwoLists(l1, l2->next);
        return l2;
    }
}

避坑指南:我曾经在迭代版本里忘了更新尾指针,结果链表成了死循环。调试了一下午才发现。所以每次更新节点后,记得把尾指针往后挪。

三、判断链表是否有环

这道题有个经典解法——快慢指针。说白了就是两个人跑步,一个快一个慢,如果有环,快的迟早会追上慢的。

// 快慢指针判断环
bool hasCycle(struct ListNode *head) {
    struct ListNode *slow = head;
    struct ListNode *fast = head;

    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) {
            return true;
        }
    }
    return false;
}

为什么快慢指针能相遇?因为每次移动,快指针比慢指针多走一步。如果有环,它们之间的距离每次减少1,最终一定会相遇。

注意:快指针移动时要检查 fast->next 是否为 NULL,否则会访问非法内存。嗯,这里一定要小心。

四、LRU缓存实现

LRU(Least Recently Used)缓存,说白了就是“最近最少使用”淘汰策略。我曾在项目中用LRU缓存来加速数据库查询,效果很明显。

实现LRU缓存,核心是哈希表 + 双向链表。哈希表保证O(1)查找,双向链表保证O(1)插入和删除。

// LRU缓存结构定义
typedef struct {
    int key;
    int value;
    struct Node *prev;
    struct Node *next;
} Node;

typedef struct {
    int capacity;
    int size;
    Node *head;  // 虚拟头节点
    Node *tail;  // 虚拟尾节点
    Node **hash; // 哈希表
} LRUCache;

操作逻辑很简单:

  • get(key):如果存在,把节点移到链表头部;否则返回-1。
  • put(key, value):如果存在,更新值并移到头部;如果不存在,插入头部。如果容量满了,删除尾部节点。

我的经验:用虚拟头尾节点可以避免很多边界判断。我曾经不用虚拟节点,结果每次都要检查 head 和 tail 是否为 NULL,代码又臭又长。

知识体系总览

下面这张图把这四道题的核心逻辑串起来了,你可以对照着看。

链表面试高频题知识体系 链表反转 三指针法 prev / curr / next 时间复杂度 O(n) 空间复杂度 O(1) 合并有序链表 递归 / 迭代 比较节点值 时间复杂度 O(n+m) 空间复杂度 O(1) 判断链表有环 快慢指针 fast 每次两步 slow 每次一步 相遇则有环 LRU缓存 哈希表 + 双向链表 get O(1) put O(1) 淘汰最近最少使用 核心思想总结 指针操作 + 画图辅助 + 边界检查 + 虚拟节点技巧 面试建议 先讲思路,再写代码。写完手动跑一个例子验证。 注意空链表、单节点、环等边界情况。

总结一下

这四道题其实都围绕一个核心——指针操作。你只要把指针的指向变化搞清楚了,剩下的就是熟练度的问题。

我建议你每天手写一遍这四道题,坚持一周。面试时就算紧张,肌肉记忆也能帮你写出来。

嗯,链表这块其实没那么可怕。多画图,多动手,你也能成为别人眼中的“链表高手”。


专注资料整理