链表五大经典面试题详解:双指针与基础操作实战

链表五大经典面试题详解:双指针与基础操作实战


🎯 前言:为什么链表面试题如此重要?

链表是数据结构中最重要的基础结构之一,在各大公司的面试中,链表相关题目出现的频率极高!今天我们就来深入剖析五道经典的链表面试题,掌握解决这类问题的核心技巧------双指针、头插尾插、快慢指针

通过这五道题目,你不仅能学会具体的解题方法,更重要的是掌握链表操作的核心思维!


📋 题目概览

题目 难度 核心技巧 链接
1. 移除链表元素 🟢 简单 尾插法、内存管理 203. Remove Linked List Elements
2. 反转链表 🟢 简单 头插法、三指针法 206. Reverse Linked List
3. 链表的中间节点 🟢 简单 快慢指针 876. Middle of Linked List
4. 链表中倒数第k个节点 🟡 中等 快慢指针、边界处理 剑指 Offer 22
5. 合并两个有序链表 🟢 简单 归并思想、尾插法 21. Merge Two Sorted Lists

🎯 第一题:原地移除链表元素

📝 题目描述

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

要求:

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

💡 核心思路:尾插法构建新链表

使用尾插法创建新链表,只保留值不等于val的节点,同时正确释放要删除节点的内存。

🎨 图解过程

复制代码
原链表: 1 → 2 → 6 → 3 → 4 → 5 → 6 (val = 6)
新链表构建过程:
1 → 2 → 3 → 4 → 5 → NULL

⚡ 代码实现

c 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* removeElements(struct ListNode* head, int val) {
    struct ListNode* newHead = NULL, *tail = NULL;
    struct ListNode* cur = head;
    
    while(cur) {
        if(cur->val != val) {
            // 尾插到新链表
            if(tail == NULL) {
                newHead = tail = cur;
            } else {
                tail->next = cur;
                tail = tail->next;
            }
            cur = cur->next;
        } else {
            // 删除当前节点
            struct ListNode *next = cur->next;
            free(cur);
            cur = next;
        }
        // 确保新链表尾部指向NULL
        if(tail) tail->next = NULL;
    }
    return newHead;
}

🔍 代码解析

  • 尾插法构建 :创建newHeadtail指针构建新链表
  • 条件判断 :当前节点值不等于val时进行尾插
  • 内存管理 :遇到目标节点时使用free释放内存
  • 边界处理 :每次循环后确保链表尾部指向NULL

🎯 第二题:反转单链表

📝 题目描述

给你单链表的头节点 head,请你反转链表,并返回反转后的链表。

💡 核心思路:头插法反转

遍历原链表,逐个将节点插入到新链表的头部,实现反转效果。

🎨 图解过程

复制代码
原链表: 1 → 2 → 3 → 4 → 5 → NULL
反转过程:
第一步: 1 → NULL
第二步: 2 → 1 → NULL
第三步: 3 → 2 → 1 → NULL
最终: 5 → 4 → 3 → 2 → 1 → NULL

⚡ 代码实现

c 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode* cur = head, *newhead = NULL;
    
    while(cur) {
        struct ListNode* next = cur->next;
        
        // 头插操作
        cur->next = newhead;
        newhead = cur;
        cur = next;
    }
    return newhead;
}

🔍 代码解析

  • 三指针操作
    • cur:遍历原链表
    • next:保存下一个节点地址
    • newhead:新链表的头节点
  • 头插操作 :当前节点的next指向新链表头,然后更新链表头
  • 迭代前进:移动到下一个待处理节点

🎯 第三题:链表的中间节点

📝 题目描述

给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

💡 核心思路:快慢指针法

快指针每次走两步,慢指针每次走一步,当快指针到达末尾时,慢指针正好在中间位置。

🎨 图解过程

复制代码
链表: 1 → 2 → 3 → 4 → 5
slow: ↑   ↑   ↑
fast: ↑     ↑     ↑
中间节点: 3

链表: 1 → 2 → 3 → 4 → 5 → 6  
slow: ↑   ↑   ↑   ↑
fast: ↑     ↑     ↑     ↑
中间节点: 4

⚡ 代码实现

c 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* middleNode(struct ListNode* head) {
    struct ListNode* slow = head, *fast = head;
    
    while(fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

🔍 代码解析

  • 快慢指针初始化:都从链表头开始
  • 移动规则
    • 慢指针slow:每次移动1步
    • 快指针fast:每次移动2步
  • 循环条件fast && fast->next确保快指针可以安全移动
  • 返回值:快指针到末尾时,慢指针即为中间节点

🎯 第四题:链表中倒数第k个节点

📝 题目描述

输入一个链表,输出该链表中倒数第k个结点。

💡 核心思路:快慢指针 + 先后出发

快指针先走k步,然后快慢指针同步移动,当快指针到达末尾时,慢指针即为倒数第k个节点。

🎨 图解过程

复制代码
链表: 1 → 2 → 3 → 4 → 5, k = 2
步骤1: fast先走2步: fast在3, slow在1
步骤2: 同步移动: fast到5时, slow到4
结果: 节点4

⚡ 代码实现

c 复制代码
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k) {
    struct ListNode* slow = pListHead, *fast = pListHead;
    
    if(pListHead == NULL) {
        return NULL;
    }
    
    // 快指针先走k步
    while(k--) {
        if(fast == NULL)
            return NULL;
        fast = fast->next;
    }
    
    // 快慢指针同步移动
    while(fast) {
        slow = slow->next;
        fast = fast->next;
    }
    return slow;
}

🔍 代码解析

  • 边界检查 :链表为空直接返回NULL
  • 快指针先行:快指针先移动k步,检查链表长度是否足够
  • 同步移动:两个指针以相同速度前进
  • 终止条件:快指针到达末尾时,慢指针位置即为所求

🎯 第五题:合并两个有序链表

📝 题目描述

将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

💡 核心思路:归并思想 + 尾插法

比较两个链表当前节点值,取较小的进行尾插,直到某个链表遍历完毕。

🎨 图解过程

复制代码
链表1: 1 → 3 → 5
链表2: 2 → 4 → 6
合并过程:
1 → 2 → 3 → 4 → 5 → 6

⚡ 代码实现

c 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    if(list1 == NULL) return list2;
    if(list2 == NULL) return list1;
    
    struct ListNode *cur1 = list1, *cur2 = list2;
    struct ListNode *head = NULL, *tail = NULL;

    while (cur1 && cur2) {
        if (cur1->val <= cur2->val) {
            if (head == NULL) {
                head = tail = cur1;
            } else {
                tail->next = cur1;
                tail = tail->next;
            }
            cur1 = cur1->next;
        } else {
            if (head == NULL) {
                head = tail = cur2;
            } else {
                tail->next = cur2;
                tail = tail->next;
            }
            cur2 = cur2->next;
        }
    }
    
    // 连接剩余部分
    if(cur1) {
        tail->next = cur1;
    }
    if(cur2) {
        tail->next = cur2;
    }
    return head;
}

🔍 代码解析

  • 边界处理:处理任一链表为空的情况
  • 归并比较:比较两个链表当前节点,选择较小值
  • 尾插构建:维护头尾指针构建新链表
  • 剩余处理:将未遍历完的链表剩余部分直接连接

💎 核心技巧总结

技巧 适用场景 关键点
尾插法 删除元素、合并链表 维护尾指针,确保尾部指向NULL
头插法 反转链表 当前节点指向新链表头,更新链表头
快慢指针 中间节点、倒数第k个节点 快指针速度是慢指针的2倍
归并思想 合并有序链表 比较两个链表当前节点,取较小值

🚀 进阶思考

  1. 环形链表检测:如何判断链表是否有环?找到环的入口?
  2. 复杂链表复制:包含随机指针的链表如何深拷贝?
  3. 链表排序:如何对链表进行O(nlogn)的排序?

💡 提示 :链表问题的核心在于指针操作和边界情况处理,多加练习才能熟练掌握!

后续逐渐提升难度,先理解单链表推荐文章:
数据结构实战:从顺序表到单链表,手把手实现C语言通讯录

相关推荐
Sɪʟᴇɴᴛ໊ོ23528 分钟前
Anyview数据结构第一章(按需自取)
c语言·开发语言·数据结构·算法
橘颂TA41 分钟前
【剑斩OFFER】算法的暴力美学——丢失的数字
数据结构·算法·leetcode·结构与算法
xiaoye-duck2 小时前
归并排序:递归与非递归全解析
数据结构·排序算法
@卞2 小时前
高阶数据结构 --- 单调队列
数据结构·c++·算法
Live&&learn10 小时前
算法训练-数据结构
数据结构·算法·leetcode
胡萝卜3.011 小时前
掌握C++ map:高效键值对操作指南
开发语言·数据结构·c++·人工智能·map
风筝在晴天搁浅12 小时前
代码随想录 509.斐波那契数
数据结构·算法
落落落sss12 小时前
java实现排序
java·数据结构·算法
fei_sun13 小时前
【数据结构】2018年真题
数据结构