面试题 02.04. 分割链表 - 题解与详细分析

面试题 02.04. 分割链表 - 题解与详细分析

题目描述

给你一个链表的头节点 head 和一个特定值 x,请你对链表进行分隔,使得所有小于 x 的节点都出现在大于或等于 x 的节点之前。

你不需要保留每个分区中各节点的初始相对位置。

示例

示例 1:

输入: head = [1,4,3,2,5,2], x = 3 输出: [1,2,2,4,3,5]

示例 2:

输入: head = [2,1], x = 2 输出: [1,2]

提示:

  • 链表中节点的数目在范围 [0, 200] 内
  • -100 <= Node.val <= 100
  • -200 <= x <= 200

解题思路

这道题要求将链表分割成两部分:所有小于 x 的节点在前,大于或等于 x 的节点在后。关键点在于不需要保持每个分区内节点的原始相对顺序

核心思想

使用双指针方法:

  • 对于小于 x 的节点,采用头插法插入到新链表头部
  • 对于大于等于 x 的节点,采用尾插法插入到新链表尾部
  • 这样就能保证所有小于 x 的节点都在大于等于 x 的节点之前

代码实现

/** * Definition for singly-linked list. * struct ListNode { * int val; * struct ListNode *next; * }; */ struct ListNode* partition(struct ListNode* head, int x) { struct ListNode* cur = head; struct ListNode* newhead = NULL; struct ListNode* newtail = NULL; struct ListNode* temp = NULL; while(cur) { temp = cur->next; // 保存下一个节点 if(cur->val < x) { // 小于x的节点:头插法 if(newhead == NULL) { newhead = newtail = cur; newhead->next = NULL; } else { cur->next = newhead; newhead = cur; } } else { // 大于等于x的节点:尾插法 if(newhead == NULL) { newhead = newtail = cur; newhead->next = NULL; } else { newtail->next = cur; newtail = cur; newtail->next = NULL; } } cur = temp; // 移动到下一个节点 } return newhead; }

代码详解

变量说明

  • cur:当前遍历的节点
  • newhead:新链表的头节点
  • newtail:新链表的尾节点
  • temp:临时保存下一个节点

核心逻辑

while(cur) { temp = cur->next; // 保存下一个节点 if(cur->val < x) { // 头插法:将节点插入到链表头部 if(newhead == NULL) { newhead = newtail = cur; newhead->next = NULL; } else { cur->next = newhead; newhead = cur; } } else { // 尾插法:将节点插入到链表尾部 if(newhead == NULL) { newhead = newtail = cur; newhead->next = NULL; } else { newtail->next = cur; newtail = cur; newtail->next = NULL; } } cur = temp; // 移动到下一个节点 }

执行过程可视化

以示例1 head = [1,4,3,2,5,2]x = 3 为例:

初始状态: 链表: 1->4->3->2->5->2->NULL newhead = NULL, newtail = NULL 第1步:节点1 (值1 < 3) 头插法:newhead -> 1 -> NULL, newtail -> 1 第2步:节点4 (值4 >= 3) 尾插法:newhead -> 1 -> 4 -> NULL, newtail -> 4 第3步:节点3 (值3 >= 3) 尾插法:newhead -> 1 -> 4 -> 3 -> NULL, newtail -> 3 第4步:节点2 (值2 < 3) 头插法:newhead -> 2 -> 1 -> 4 -> 3 -> NULL, newtail -> 3 第5步:节点5 (值5 >= 3) 尾插法:newhead -> 2 -> 1 -> 4 -> 3 -> 5 -> NULL, newtail -> 5 第6步:节点2 (值2 < 3) 头插法:newhead -> 2 -> 2 -> 1 -> 4 -> 3 -> 5 -> NULL, newtail -> 5 最终结果:2->2->1->4->3->5->NULL

优化与改进

方法二:双链表法(保持相对顺序)

如果我们希望保持每个分区内节点的相对顺序,可以使用两个链表:

struct ListNode* partition(struct ListNode* head, int x) { // 创建两个哑节点 struct ListNode less_dummy, ge_dummy; struct ListNode* less_tail = &less_dummy; struct ListNode* ge_tail = &ge_dummy; less_dummy.next = NULL; ge_dummy.next = NULL; struct ListNode* cur = head; while (cur != NULL) { if (cur->val < x) { // 添加到小于x的链表 less_tail->next = cur; less_tail = cur; } else { // 添加到大于等于x的链表 ge_tail->next = cur; ge_tail = cur; } cur = cur->next; } // 连接两个链表 less_tail->next = ge_dummy.next; ge_tail->next = NULL; return less_dummy.next; }

输出结果: [1,2,2,4,3,5](保持相对顺序)

方法三:原地分割

struct ListNode* partition(struct ListNode* head, int x) { struct ListNode* cur = head; struct ListNode* prev = NULL; struct ListNode* insert_pos = NULL; while (cur != NULL) { if (cur->val < x) { if (insert_pos != NULL) { // 将当前节点移动到insert_pos之后 if (prev != NULL) { prev->next = cur->next; } struct ListNode* temp = insert_pos->next; insert_pos->next = cur; cur->next = temp; insert_pos = cur; cur = prev->next; } else { insert_pos = cur; prev = cur; cur = cur->next; } } else { prev = cur; cur = cur->next; } } return head; }

复杂度分析

原方法

  • 时间复杂度:O(n),只需遍历链表一次
  • 空间复杂度:O(1),只使用常数级别的额外空间

双链表法

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

关键点总结

  1. 头插法与尾插法的结合:小于x的节点用头插法,大于等于x的节点用尾插法
  2. 边界处理:注意处理空链表和单节点链表的情况
  3. 指针操作:在修改指针前保存下一个节点,避免链表断裂
  4. 尾节点处理:确保尾节点的next为NULL,避免形成环

应用场景

这种链表分割技巧在以下场景中有应用:

  1. 快速排序的链表版本:分割操作是快速排序的核心
  2. 数据过滤:根据条件将数据分成两部分
  3. 负载均衡:将任务根据优先级分配到不同队列

总结

这道题考察了链表的基本操作和双指针技巧:

  1. 核心思路:通过头插法和尾插法的组合实现链表分割
  2. 灵活性:题目不要求保持相对顺序,给了我们更多的操作空间
  3. 指针操作:熟练掌握链表的插入、删除操作是关键
  4. 边界情况:注意处理空链表、单节点链表等特殊情况

掌握这种链表分割技巧对于理解更复杂的链表算法(如链表排序)很有帮助。

Read more

Hadoop 核心组件详解:HDFS、YARN、MapReduce 如何各司其职?

Hadoop 核心组件详解:HDFS、YARN、MapReduce 如何各司其职?

Hadoop 核心组件详解:HDFS、YARN、MapReduce 如何各司其职? * 一、Hadoop 核心组件全景图 * 二、HDFS:分布式文件系统 * 1. 架构角色 * 2. 读写流程(流程图) * HDFS 写数据流程 * HDFS 读数据流程 * 三、YARN:资源调度与管理系统 * 1. 架构角色 * 2. YARN 的任务提交流程 * 四、MapReduce:分布式计算框架 * 1. 核心思想 * 2. 经典 WordCount 代码示例 * 3. MapReduce 完整执行流程 * 五、总结:三驾马车的协同 🌺The Begin🌺点点关注,收藏不迷路🌺 大数据时代,面对海量数据的存储与计算,

By Ne0inhk
Flutter 组件 easter 适配鸿蒙 HarmonyOS 实战:天文学节气算法,构建全球化复活节周期与民俗历法治理架构

Flutter 组件 easter 适配鸿蒙 HarmonyOS 实战:天文学节气算法,构建全球化复活节周期与民俗历法治理架构

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 easter 适配鸿蒙 HarmonyOS 实战:天文学节气算法,构建全球化复活节周期与民俗历法治理架构 前言 在鸿蒙(OpenHarmony)生态迈向全球化部署、涉及跨区域文化适配(I18n)及复杂变动日期计算的背景下,如何精确推演具备“阴阳历混合特性”的全球性节日(如复活节),已成为决定跨国类应用“运营确定性”的核心技术难点。在鸿蒙设备这类强调 AOT 极致性能与低功耗常驻服务(AOD)的环境下,如果应用依然依赖手动配置的“节日死表”,由于由于复活节日期在全球范围内的复杂游移性,极易由于由于配置滞后导致海外营销活动的时序错乱。 我们需要一种能够实现高精度天文学推演、支持百年尺度计算且具备纯 Dart 离线运作能力的历法预判方案。 easter 为 Flutter 开发者引入了基于高斯算法(Gauss's algorithm)或曼氏算法(Meeus&

By Ne0inhk
力扣校招算法通关:双指针技巧全场景拆解 —— 从数组操作到环检测的高效解题范式

力扣校招算法通关:双指针技巧全场景拆解 —— 从数组操作到环检测的高效解题范式

文章目录 * 前言 * 双指针 * 例题讲解 * 移动零 力扣 * 复写零 力扣 * 快乐数 力扣 * 盛最多水的容器 力扣 * 有效三角形的个数 力扣 * 查找总价格为目标值的两个商品 力扣 * 三数之和 力扣 前言 在力扣校招算法题中,双指针技巧是一类高频且实用的解题方法。它并非真正的 “指针”,而是通过两个数组下标(或迭代器)的协同移动,在数组划分、区间求解、环检测等场景中实现高效遍历与逻辑处理,往往能将时间复杂度从暴力法的 O(n平方)优化至O(n),是校招笔试和面试中突破数组类难题的关键武器。 本专栏将围绕力扣校招高频的双指针题型展开,从 “移动零”“复写零” 的数组操作,到 “快乐数” 的环检测、“盛最多水的容器” 的区间优化,再到 “三数之和” 的多指针协同,逐一拆解双指针的核心逻辑、边界处理与去重技巧,

By Ne0inhk
单链表的实现(数据结构)

单链表的实现(数据结构)

一. 单链表的实现 我们在上一篇中简单的认识了链表的组成和结构,并打印出链表,那么今天就来具体实现一下单链表对于数据增加、删减、插入等。 接下来就是我们在链表中对于数据的增、删、插的实现,对于我们的链表来说在任何地方增加数据都需要来申请一个新的结点,首先呢,SLDatatype是我们更改int类型的名称之后得来的,SL是结构体的名称,我们先申请一个结构体大小的新结点node,然后再将新结点中的x的值赋给data,再让新结点的next指向NULL,申请新结点的函数写好之后我们就来写关于数据的增、删、插等,所以我们直接先来完成申请新结点的代码: SLDatatype* SLBuyNode(SLDatatype x) { SL* node = (SL*)malloc(sizeof(SL)); if (node == NULL) { perror("malloc fail!"); exit(1); } node->data = x; node->next = NULL; return

By Ne0inhk