LeetCode每日一刷 --- 手撕单链表习题(1)

本文详细介绍了链表的常见操作,包括移除指定元素、反转链表、找到链表的中间节点、找到链表中倒数第k个节点、合并两个有序链表以及如何分割链表。针对每个问题,提供了清晰的思路和对应的C语言实现代码,涵盖了不同情况的处理方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1、移除链表元素

2、反转链表

3、链表的中间节点

4、链表中倒数第k个节点

5、合并两个有序链表

6、链表分割


1、移除链表元素

  • 链接直达:

移除链表元素

  • 题目:

  •  思路:

此题要综合考虑多种情况,常规情况就如同示例1,有多个节点,并且val不连续,但是非常规呢?当val连续呢?当头部就是val呢?所以要分类讨论

常规情况:

需要定义两个指针prev和cur,cur指向第一个数据,prev指向cur的前一个。依次遍历cur指向的数据是否为val,若是,则把prev的下一个节点指向cur的下一个节点上,cur=cur->next,prev跟着cur一起走,直到cur走到NULL

连续val:

当我们仔细观察下,不难发现,在常规情况下是可以解决连续val的,但是头部val就不可了

头部val:

此时除了刚才定义的两个指针prev和cur外,还要有个head指向头部,当头部是val时,将cur指向下一个位置,head跟着一起动,直到cur指向的数据不为val时,将head赋给prev。此时剩余的就按常规处理即可。

  • 代码如下:
struct ListNode* removeElements(struct ListNode* head, int val){
    struct ListNode*cur=head;
    struct ListNode*prev=NULL;
    while(cur)
    {
        if(cur->val!=val)
        {
            prev=cur;
            cur=cur->next;
        }
        else
        {
            struct ListNode*next=cur->next;
            if(prev==NULL)
            {
                free(cur);
                cur=next;
                head=next;
            }
            else
            {
                prev->next=cur->next;
                free(cur);
                cur=prev->next;
            }
        }
    }
    return head;
}

2、反转链表

  • 链接直达:

反转链表

  • 题目:

  • 思路:

法一:三指针翻转方向

定义三个指针n1,n2,n3分别用来指向NULL,第一个数据,第二个数据。让n2的next指向n1,把n2赋给n1,再把n3赋给n2,再执行n3=n3->next的操作,接下来重复上述操作,直到n2指向空即可。但是要注意,要先判断该链表是否为NULL,如果是,则返回NULL,此外,还要保证当n3为空时就不要动了,直接把n3赋给n2即可。

  • 代码如下:
struct ListNode* reverseList(struct ListNode* head){
    if(head==NULL)
    {
        return NULL;
    }
    struct ListNode*n1=NULL;
    struct ListNode*n2=head;
    struct ListNode*n3=n2->next;
    while(n2)
    {
        n2->next=n1;
        n1=n2;
        n2=n3;
        if(n3)
        {
            n3=n3->next;
        }
    }
    return n1;
}

法二:头插

此法就需要再创建一个链表了,创建一个新的头部newhead指向NULL,再定义一个指针cur指向原链表第一个数据,注意还得定义一个指针next指向cur的下一个节点。遍历原链表,把节点取下来头插到newhead所在的链表。每次更新newhead赋给cur,如图所示:

  •  代码如下:
struct ListNode* reverseList(struct ListNode* head){
    if(head==NULL)
    {
        return NULL;
    }
    struct ListNode*cur=head;
    struct ListNode*next=cur->next;
    struct ListNode*newhead=NULL;
    while(cur)
    {
        cur->next=newhead;
        newhead=cur;
        cur=next;
        if(next)
        {
            next=next->next;
        }
    }
    return newhead;
}

3、链表的中间节点

  • 链接直达:

链表的中间节点

  • 题目:

  •  思路:

快慢指针

这道题要注意奇偶数,如果为奇数,如示例1,那么中间节点值就是3,反之偶数如示例2,返回第二个中间节点。此题我们定义两个指针slow和fast都指向第一个数据的位置,区别在于让slow一次走1步,fast一次走2步。当fast走到尾指针时,slow就是中间节点

  •  代码如下:
struct ListNode* middleNode(struct ListNode* head){
    struct ListNode*slow=head;
    struct ListNode*fast=head;
    while(fast&&fast->next)
    {
        slow=slow->next;
        fast=fast->next->next;
    }
    return slow;
}

4、链表中倒数第k个节点

  • 链接直达:

链表中倒数第k个节点

  • 题目:

  •  思路:

快慢指针

定义两个指针slow和fast,让fast先走k步,再让slow和fast同时走,当fast走到尾部时,slow就是倒数第k个,因为这样的话slow和fast的差距始终是k个,当fast走到空时结束。此题同样可以走k-1步,不过当fast走到尾部时结束,也就是fast的下一个节点指向空时结束,都一样。先拿走k步举例,如图所示:

  •  代码如下:
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    // write code here
    struct ListNode*fast=pListHead;
    struct ListNode*slow=pListHead;
    while(k--)
    {
        //if判断,防止k大于链表的长度
        if(fast==NULL)
            return NULL;
        fast=fast->next;
    }
    while(fast)
    {
        fast=fast->next;
        slow=slow->next;
    }
    return slow;
}

5、合并两个有序链表

  • 链接直达:

合并两个有序链表

  • 题目:

  •  思路:

法一:归并(取小的尾插)--- 带头节点

假设新链表的头叫head并指向NULL,还需要定义一个指针tail来方便后续的找尾,依次比较list1和list2节点的值,把小的放到新链表head上,并更新tail,再把list1或list2更新一下。当list1和list2两个链表中一个走到空时,直接把剩下的链表所有剩下的元素拷进去即可

  •  代码如下:
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
    //检查list1或list2一开始就为NULL的情况
    if(list1==NULL)
    {
        return list2;
    }
    if(list2==NULL)
    {
        return list1;
    }
    struct ListNode*head=NULL;
    struct ListNode*tail=head;
    while(list1&&list2)
    {
        if(list1->val<list2->val)
        {
            if(tail==NULL)
            {
                head=tail=list1;
            }
            else
            {
                tail->next=list1;
                tail=list1;
            }
            list1=list1->next;
        }
        else
        {
            if(tail==NULL)
            {
                head=tail=list2;
            }
            else
            {
                tail->next=list2;
                tail=list2;
            }
            list2=list2->next;
        }
    }
    //当list1和list2其中一个走到空的情况
    if(list1==NULL)
    {
        tail->next=list2;
    }
    else
    {
        tail->next=list1;
    }
    return head;
}

法二:哨兵位的头节点

解释下带头节点:

比如说同样一个链表存1,2,3。不带头节点只有这三个节点,head指向1。而带头节点的同样存3个值,不过有4个节点,head指向头部这个节点,这个节点不存储有效数据

 带头结点有如下好处,不用判断head和tail是否为空了,也不用判断list1和list2是否为空了,会方便不少。和上述思路一样,取小的下来尾插,直接链接到tail后面即可。但是要注意返回的时候要返回head的next,因为题目给的链表是不带头的,而head本身指向的就是那个头,所以要返回下一个。

  • 代码如下:
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    struct ListNode* head = NULL, * tail = NULL;
    head = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
    head->next = NULL;
    while (list1 && list2)
    {
        if (list1->val < list2->val)
        {
            tail->next = list1;
            tail = list1;
            list1 = list1->next;
        }
        else
        {
            tail->next = list2;
            tail = list2;
            list2 = list2->next;
        }
    }
    //当list1和list2其中一个走到空的情况
    if (list1 == NULL)
    {
        tail->next = list2;
    }
    else
    {
        tail->next = list1;
    }
    struct ListNode* list = head->next;
    free(head);
    head = NULL
        return list;
}

6、链表分割

  • 链接直达:

链表分割

  • 题目:

  •  思路:

定义两个链表lesshead和greaterhead。遍历原链表,把 < x 的插入到链表1,把 > x 的插入到链表2,最后再把链表1和链表2链接起来。在定义两个尾指针以跟进链表1和2新增元素

  •  代码如下:
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        // write code here
        struct ListNode* lessHead, * lessTail, * greaterHead, * greaterTail;
        lessHead = lessTail = (struct ListNode*)malloc(sizeof(struct ListNode));
        greaterHead = greaterTail = (struct ListNode*)malloc(sizeof(struct ListNode));
        lessTail->next = greaterTail->next = NULL;
        struct ListNode* cur = pHead;
        while (cur)
        {
            if (cur->val < x)
            {
                lessTail->next = cur;
                lessTail = lessTail->next;
            }
            else
            {
                greaterTail->next = cur;
                greaterTail = greaterTail->next;
            }
            cur = cur->next;
        }
        //合并
        lessTail->next = greaterHead->next;
        greaterTail->next = NULL;
        struct ListNode* list = lessHead->next;
        free(lessHead);
        free(greaterHead);
        return list;
    }
};
<think>好的,用户之前问过C++客户端岗位的考察内容,现在又具体问到了“代码”的常见题型和建议,特别是希望更详细的解释。需要先理解用户的需求,他们可能在准备面试,想深入了解代码的具体题型和如何有效准备。 用户提到常见题型包括数据结构链表反转、二叉树遍历,算法如LRU缓存和生产者-消费者模型,建议LeetCode时优先实际场景题。需要详细展开每个题型,给出示例和解题思路,同时提供准备建议。 首先,确定用户需要的是每个题型的详细分析,包括代码示例和考察点。比如链表反转可能有迭代和递归两种方法,需要解释清楚。二叉树遍历要区分不同的遍历方式,并给出代码。LRU缓存涉及数据结构设计,生产者-消费者模型则涉及多线程同步机制。 然后,用户可能希望知道如何高效题,特别是实际场景题,比如日志系统设计,这类题目如何与代码结合。需要给出LeetCode题号例子,并说明为什么这些题目重要。 另外,用户可能需要知道面试中的注意事项,比如代码规范、测试用例的考虑,以及如何优化代码。要提醒他们注意这些细节,避免常见错误。 还要考虑用户可能的背景,他们可能对某些概念不太熟悉,比如互斥锁、条件变量,或者LRU缓存的双向链表和哈希表结合的实现方式,需要用简洁易懂的语言解释。 最后,总结时要强调练习方法和资源推荐,帮助用户系统化准备,同时提醒沟通和测试的重要性,确保面试时不仅代码正确,还能清晰解释思路。</think>好的!我将从**常见题型解析、代码示例、考察重点、题策略**等方面详细展开“代码”的备考要点,并结合实际场景题给出具体建议。 --- ### 数据结构类题目 #### 1. **链表反转** **题目示例**: "反转单链表,要求时间复杂度$O(n)$,空间复杂度$O(1)$。" **核心思路**- **迭代法**:维护三个指针(prev, curr, next),逐步修改节点指向。 - **递归法**:从链表尾部开始反转(需注意递归深度可能导致栈溢出)。 **代码示例(迭代法)**: ```cpp ListNode* reverseList(ListNode* head) { ListNode* prev = nullptr; ListNode* curr = head; while (curr) { ListNode* next = curr->next; // 保存下个节点 curr->next = prev; // 反转当前节点指向 prev = curr; // 移动prev和curr curr = next; } return prev; // prev最终指向新头节点 } ``` **考察重点**- 指针操作的边界条件(如空链表、单节点链表- 能否用迭代和递归两种方法实现 --- #### 2. **二叉树遍历** **题目示例**: "实现二叉树的层序遍历(BFS)和前序遍历(DFS)。" **代码示例(层序遍历)**: ```cpp vector<vector<int>> levelOrder(TreeNode* root) { vector<vector<int>> res; if (!root) return res; queue<TreeNode*> q; q.push(root); while (!q.empty()) { int levelSize = q.size(); vector<int> level; for (int i = 0; i < levelSize; ++i) { TreeNode* node = q.front(); q.pop(); level.push_back(node->val); if (node->left) q.push(node->left); if (node->right) q.push(node->right); } res.push_back(level); } return res; } ``` **考察重点**- BFS与DFS的应用场景区分(如最短路径用BFS,回溯用DFS) - 非递归实现的能力(如用栈模拟DFS) --- ### 二、算法设计类题目 #### 1. **LRU缓存** **题目示例**: "设计个LRU(最近最少使用)缓存,支持`get`和`put`操作,时间复杂度$O(1)$。" **核心思路**- **数据结构组合**- 哈希表(快速查找键值) - 双向链表(维护访问顺序,头部最新,尾部最旧) **代码框架**: ```cpp class LRUCache { private: struct Node { int key, value; Node *prev, *next; Node(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {} }; unordered_map<int, Node*> cache; Node *head, *tail; int capacity; void moveToHead(Node* node) { /* 将节点移到头部 */ } void removeNode(Node* node) { /* 删除节点 */ } void addToHead(Node* node) { /* 添加节点到头部 */ } Node* removeTail() { /* 删除尾部节点并返回 */ } public: LRUCache(int cap) : capacity(cap) { head = new Node(-1, -1); tail = new Node(-1, -1); head->next = tail; tail->prev = head; } int get(int key) { if (!cache.count(key)) return -1; Node* node = cache[key]; moveToHead(node); // 更新访问顺序 return node->value; } void put(int key, int value) { if (cache.count(key)) { Node* node = cache[key]; node->value = value; moveToHead(node); } else { Node* newNode = new Node(key, value); cache[key] = newNode; addToHead(newNode); if (cache.size() > capacity) { Node* tailNode = removeTail(); cache.erase(tailNode->key); delete tailNode; } } } }; ``` **考察重点**-数据结构的组合设计能力(哈希表+双向链表- 指针操作的细节(如头尾哨兵节点简化边界处理) --- #### 2. **生产者-消费者模型** **题目示例**: "用C++多线程实现生产者-消费者模型,缓冲区大小为N。" **核心思路**- **同步机制**:互斥锁(`std::mutex`) + 条件变量(`std::condition_variable`) - **缓冲区管理**:队列(`std::queue`) **代码框架**: ```cpp #include <queue> #include <mutex> #include <condition_variable> template<typename T> class BlockingQueue { private: std::queue<T> queue; std::mutex mtx; std::condition_variable not_full; std::condition_variable not_empty; int max_size; public: BlockingQueue(int size) : max_size(size) {} void push(const T& item) { std::unique_lock<std::mutex> lock(mtx); not_full.wait(lock, [this]() { return queue.size() < max_size; }); queue.push(item); not_empty.notify_one(); // 通知消费者 } T pop() { std::unique_lock<std::mutex> lock(mtx); not_empty.wait(lock, [this]() { return !queue.empty(); }); T item = queue.front(); queue.pop(); not_full.notify_one(); // 通知生产者 return item; } }; ``` **考察重点**- 条件变量的正确使用(避免虚假唤醒) - 线程安全的队列实现(锁的粒度控制) --- ### 三、实际场景题题策略 #### 1. **优先题方向** - **日志系统设计**LeetCode 359. Logger Rate Limiter) - 考察点:哈希表 + 时间窗口管理 - **文件路径匹配**LeetCode 44. Wildcard Matching) - 考察点:动态规划或回溯算法 - **聊天消息缓存**(类似LRU但需考虑时间戳) - 考察点:有序数据结构(红黑树/跳表) #### 2. **题技巧** - **代码模板化**:如二叉树遍历、DFS/BFS的代码框架固定,熟记模板。 - **场景迁移**:将抽象题目映射到实际功能(如LRU可类比浏览器缓存淘汰策略)。 - **边界测试**:针对空输入、极值(如链表只有个节点)设计测试用例。 --- ### 四、面试注意事项 1. **沟通先行**- 先明确需求(如“缓冲区是否线程安全?”) - 举例:设计日志系统时,询问“是否允许丢失部分日志?”以确定用内存队列还是持久化存储。 2. **代码规范**- 变量命名清晰(如`prev`、`curr`代替`p`、`q`) - 添加必要注释(如“此处需要原子操作防止竞态条件”) 3. **优化意识**- 完成后主动分析时间/空间复杂度 - 提出改进方向(如“若内存允许,可以用`unordered_map`替代双向链表加速LRU访问”) --- **总结**代码的关键在于**快速将问题抽象为数据结构+算法组合**,并通过大量练习固化常见模式。建议每天精2-3题,重点理解题目与实际工程场景的联系(如链表操作在内核链表中的应用)。
评论 74
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三分苦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值