当前位置: 首页 > news >正文

第33篇 10道题加深单链表理解(上)

本篇博客内容是为10个单链表的题目:

第一题 移除链表元素

203. 移除链表元素 - 力扣(LeetCode)

针对 LeetCode 203. 移除链表元素这道经典题目,我们可以从算法复杂度优化的角度进行深度剖析。

在初步分析时,一个直观的思路是复用基础的查找与删除操作。然而,这种组合方式存在显著的性能瓶颈:在单链表中执行一次查找的时间复杂度为 O(N),而由于单链表缺乏前驱指针,每次定位到目标节点后,都需要从头节点开始重新遍历以寻找其前驱节点,这使得单次删除操作的时间复杂度同样为 O(N)。若对链表中所有符合条件的节点执行此操作,整体时间复杂度将急剧退化至 O(N²),这在处理大规模数据时是不可接受的。

为了突破这一性能瓶颈,我们可以采用“尾插法构建新链表”的策略来进行算法优化。具体而言,我们在遍历原链表的过程中,仅将值不等于目标值的节点依次链接到一个新的链表中。

这里需要澄清一个常见的认知误区:虽然逻辑上我们“创建”了一个新链表,但在底层实现中,我们并没有像动态数组那样开辟额外的连续内存空间来拷贝数据。我们仅仅是复用原链表的节点,通过改变指针的指向来重新组织节点间的逻辑关系。因此,该算法的空间复杂度依然保持在 O(1)。与此同时,由于整个处理过程仅需对原链表进行一次线性扫描,时间复杂度被成功优化至 O(N)。这种以指针操作代替数据拷贝的思路,完美地兼顾了时间效率与空间开销,是解决此类链表过滤问题的最优解。

/** * Definition for singly-linked list. * struct ListNode { * int val; * struct ListNode *next; * }; */ typedef struct ListNode* listnode; struct ListNode* removeElements(struct ListNode* head, int val) { listnode phead=NULL; listnode ptail=NULL; while(head) { if(head->val!=val) { //如果新链表为空,初始化头尾指针 if(phead==NULL) { phead=head; ptail=head; } else {// 否则接到尾部并更新尾指针 ptail->next=head; ptail=head; } } // 无论是否删除,原链表指针都要后移 head=head->next; } // 有当新链表不为空时,才断开尾部连接 if(ptail!=NULL) ptail->next=NULL; return phead; }

这段代码的核心思路是**“筛选重建法”。它并没有直接在原链表上进行复杂的指针删除操作(这容易导致断链或丢失节点),而是选择遍历原链表,将符合条件的节点“摘”出来,按顺序拼接到一个新的链表中**。

这种方法的优点是逻辑清晰、不易出错,且能很好地处理头节点被删除的情况。以下是详细的编写思路解析:

核心策略:双链表操作

代码维护了两个逻辑上的链表:

  1. 原链表(由head驱动):作为数据源,负责从头到尾扫描每一个节点。
  2. 新链表(由pheadptail维护):作为结果容器,只接收那些值不等于val的节点。

变量定义与初始化

  • listnode phead = NULL;新链表的头指针。初始化为空,表示新链表还没开始建立。最终函数将返回这个指针。
  • listnode ptail = NULL;新链表的尾指针。始终指向新链表的最后一个节点,用于快速在尾部追加新节点(O(1) 复杂度)。
  • head:作为游标,负责在原链表中不断向后移动 (head = head->next)。

循环筛选逻辑

这是代码的主体部分,通过while(head)遍历原链表:

判断条件
if (head->val != val)

检查当前节点是否需要保留。只有当节点的值不等于目标值val时,才执行后续操作;如果相等,则直接跳过(相当于“删除”了该节点,因为它没有被接入新链表)。

接入新链表

这里分两种情况处理,保证了链表结构的完整性:

  1. 新链表为空(第一个有效节点)

    if (phead == NULL) { phead = head; // 确立头节点 ptail = head; // 此时头也是尾 }

    这是起始状态,需要同时初始化头和尾。

  2. 新链表已有节点(后续有效节点)

    else { ptail->next = head; // 将当前节点挂到旧尾巴后面 ptail = head; // 更新尾巴为当前节点 }

    利用尾插法,将节点串联起来。

收尾工作

循环结束后,必须处理新链表的结尾,防止野指针问题。

if (ptail != NULL) ptail->next = NULL;
  • 为什么要做这一步?因为我们是直接把原链表的节点拿过来用的,原链表中这些节点的next指针可能还指向后面的节点(甚至是被跳过的、值为val的节点)。如果不手动置空,新链表的尾部可能会拖着一条长长的“垃圾尾巴”,导致后续遍历出错或死循环。
  • 为什么判空?如果原链表所有节点都被删除了(例如[6,6], val=6),ptail依然是NULL。此时不能执行ptail->next,否则会导致程序崩溃。

总结

这段代码通过“遍历 -> 筛选 -> 尾插”三步走,巧妙地避开了直接删除节点带来的复杂指针操作。它不仅代码简洁,而且时间复杂度为 O(N),空间复杂度为 O(1)(复用了原节点,未开辟新内存),是非常优秀的解法。

优化

其实这题用‘哨兵节点’写会更优雅,连最后的判空都能省掉:

使用哨兵节点(Sentinel Node),也叫“虚拟头节点”或“哑节点”,是处理链表问题最优雅、最常用的技巧。

它的核心作用是统一操作逻辑:让链表的每一个真实节点都拥有“前驱节点”。这样我们就不需要单独写代码去判断“是否正在删除头节点”x

哨兵节点法完整代码

/** * Definition for singly-linked list. * struct ListNode { * int val; * struct ListNode *next; * }; */ struct ListNode* removeElements(struct ListNode* head, int val) { // 1. 创建哨兵节点(通常分配在栈上即可) // 它的值无所谓,关键是它的 next 指向原链表的头 struct ListNode sentinel; sentinel.next = head; // 2. 定义一个游标指针 prev,初始指向哨兵节点 // prev 始终代表“当前检查节点的前一个节点” struct ListNode* prev = &sentinel; // 3. 开始遍历 while (prev->next != NULL) { struct ListNode* curr = prev->next; // curr 是当前要检查的节点 if (curr->val == val) { // 【情况A:发现目标,执行删除】 // 将 prev 的 next 直接指向 curr 的下一个节点 // 这样 curr 就被从链表中“摘除”了 prev->next = curr->next; // 注意:这里不需要移动 prev! // 因为新的 prev->next 变成了原来的 curr->next, // 下一轮循环我们需要检查这个新接上来的节点。 // (可选) 释放被删除节点的内存,防止内存泄漏 // free(curr); } else { // 【情况B:不是目标,继续向后走】 // 只有当没有发生删除时,prev 才向后移动 prev = curr; } } // 4. 返回哨兵节点的下一个节点作为新的头 // 如果原头节点被删了,sentinel.next 会自动指向第二个节点 // 如果原头节点没被删,sentinel.next 依然指向原头节点 return sentinel.next; }

为什么这种方法更好?

  1. 消灭了if-else分支

    • 在普通方法中,删除头节点需要head = head->next,而删除中间节点需要prev->next = curr->next。这是两套逻辑。
    • 有了哨兵节点,所有的删除操作都变成了prev->next = curr->next。哪怕删除的是第一个真实节点,它的前驱也是sentinel,逻辑完全通用。
  2. 避免了空指针判断

    • 你不需要担心head是否为空,也不需要担心删除后链表变空的情况。sentinel.next会自然地处理这一切(如果全删光了,它就是NULL)。
  3. 代码更简洁

    • 不需要维护pheadptail两个指针,只需要一个prev指针就能搞定所有事情。

图解执行过程

假设链表为[1] -> [2] -> [6] -> [3],我们要删除6

  1. 初始状态sentinel -> [1] -> [2] -> [6] -> [3]prev指向sentinel

  2. 检查 1:不等于 6。prev移动到[1]

  3. 检查 2:不等于 6。prev移动到[2]

  4. 检查 6等于 6!

    • 执行prev->next = curr->next
    • 此时[2]next直接连到了[3]
    • [6]被孤立(断开连接)。
    • 关键点prev不动,依然指向[2]
  5. 下一轮循环

    • curr变成了prev->next,也就是[3]
    • 检查 3:不等于 6。prev移动到[3]
  6. 结束:返回sentinel.next(即[1])。


这种解法是面试里的加分项。

第二题 合并两个有序链表

21. 合并两个有序链表 - 力扣(LeetCode)

核心解法采用双指针迭代合并策略:通过while循环同步遍历两条有序链表,逐节点比对两链表当前指针值域,优先选取数值较小的节点接入结果链表尾部;循环终止后,将剩余未遍历完毕的链表片段直接拼接至合并链表末尾,完成全量节点合并。

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) { // 处理边界情况 if (list1 == NULL) return list2; if (list2 == NULL) return list1; struct ListNode* newhead = NULL; struct ListNode* newtail = NULL; while (list1 != NULL && list2 != NULL) { if (list1->val < list2->val) { if (newhead == NULL) { // 初始化头节点 newhead = newtail = list1; } else { // 【关键修改】先连接,再移动尾巴 newtail->next = list1; newtail = list1; // 或者 newtail = newtail->next; } list1 = list1->next; // 移动 list1 指针 } else { if (newhead == NULL) { newhead = newtail = list2; } else { // 【关键修改】同上 newtail->next = list2; newtail = list2; } list2 = list2->next; // 移动 list2 指针 } } // 处理剩余节点 if (list1 != NULL) { newtail->next = list1; // 直接把剩下的整串接上去 } if (list2 != NULL) { newtail->next = list2; } return newhead; }

在上面的链表操作中,频繁判断头结点是否为空是导致代码冗余和逻辑复杂的原因。

因此引入哨兵节点(Sentinel Node / Dummy Node)是非常专业且优雅的优化手段。它的核心优势在于:

  1. 统一处理逻辑:无论是插入第一个节点还是后续节点,操作都变成在dummy.next之后插入,彻底消除了if head is None这类边界判断。
  2. 防止空指针异常:始终有一个安全的起点,不用担心修改头指针时丢失引用。
  3. 代码更简洁:主循环或逻辑块里只保留核心操作,可读性大幅提升。

优化

struct ListNode dummy; // 创建一个虚拟节点 struct ListNode* tail = &dummy; // 尾巴从虚拟节点开始 while (l1 && l2) { if (l1->val < l2->val) { tail->next = l1; l1 = l1->next; } else { tail->next = l2; l2 = l2->next; } tail = tail->next; // 统一移动尾巴 } // 接上剩余部分 tail->next = l1 ? l1 : l2; return dummy.next; // 返回虚拟节点的下一个,即真正的头

第三题 反转链表

206. 反转链表 - 力扣(LeetCode)

有一个很简单的方法:

头插法构建新链表

核心策略:采用“头插法”(Head Insertion)重构链表。该方法通过遍历原链表,依次摘取节点并插入到新链表的头部,利用“后进先出”的特性自然实现链表的逆序。算法的时间复杂度为 O(N),空间复杂度为 O(1)(仅复用原节点)。

核心逻辑解析

  1. 初始化:创建一个new_head指针,初始化为NULL。这代表一个新的、空的链表。
  2. 遍历与摘除:使用while循环遍历原链表。在修改当前节点的指向之前,必须先保存下一个节点的地址(否则链表就断了,找不到后面的路了)。
  3. 头插操作
    • 让当前节点的next指向new_head(把它插到新链表的最前面)。
    • 更新new_head为当前节点(它现在变成了新的头)。
  4. 移动游标:将原链表的遍历指针移向刚才保存的“下一个节点”。

标准代码实现

struct ListNode* reverseList(struct ListNode* head) { // 1. 定义一个新链表的头指针,初始为空 struct ListNode* new_head = NULL; // 2. 遍历原链表 while (head != NULL) { // 【关键步骤】先保存原链表的下一个节点,防止断链后迷路 struct ListNode* next_node = head->next; // 3. 执行“头插”:将当前节点插入到新链表的最前面 head->next = new_head; // 当前节点的next指向旧的新链表头 // 4. 更新新链表的头:当前节点变成了新的头 new_head = head; // 5. 移动原链表指针,继续处理下一个节点 head = next_node; } // 6. 返回新链表的头 return new_head; }

图解流程 (以 1->2->3 为例)

  1. 初始状态

    • head指向 1
    • new_head为 NULL
  2. 第一轮循环 (处理节点 1)

    • 保存next_node(即节点 2)
    • 1->next指向new_head(NULL) —— 此时链表变成1 -> NULL
    • new_head更新为 1
    • head移动到next_node(即节点 2)
  3. 第二轮循环 (处理节点 2)

    • 保存next_node(即节点 3)
    • 2->next指向new_head(节点 1) —— 此时链表变成2 -> 1 -> NULL
    • new_head更新为 2
    • head移动到next_node(即节点 3)

...以此类推,最终new_head就会指向完全反转后的链表头。

优化

但是这个方法实在是太普通了,接下来我们引入一个非常常见与重要的方法:三指针法

这是面试和工程中最经典、最标准的解法,它的核心在于“原地反转”,不需要创建新链表,空间复杂度为 O(1)。

算法解析:三指针法(原地迭代)

1. 思路

采用“局部反转”策略。我们可以把链表想象成一列火车,我们不需要把车厢拆下来重装,而是站在车厢连接处,依次将每一节车厢的挂钩方向调转。为了保证在调转挂钩时不丢失后续车厢的连接,我们需要三只手(三个指针)协同工作:一只手抓住前一个节点(作为新的指向目标),一只手抓住当前节点(正在操作的节点)一只手提前抓住下一个节点(防止断链后迷路)

2. 核心逻辑实现
  1. 初始化指针:定义prev(前驱指针)初始化为NULL,因为反转后原头节点将变为尾节点,其next应指向空;定义curr(当前指针)指向原链表头节点head
  2. 循环遍历:当curr不为空时进入循环。
  3. 保存后继关键步骤。在修改curr的指向之前,必须先用临时指针next_temp保存curr->next。这是因为一旦修改了curr->next,原链表后续部分就会断开,若不保存将无法继续遍历。
  4. 执行反转:将curr->next指向prev。此时,当前节点与前一个节点的连接方向完成逆转。
  5. 指针推进
    • prev移动到curr的位置(前驱指针后移)。
    • curr移动到next_temp的位置(当前指针后移,处理下一个节点)。
  6. 返回结果:当循环结束时,currNULL,而prev正好停留在原链表的最后一个节点上,即新链表的头节点。
3. 代码 (C)
struct ListNode* reverseList(struct ListNode* head) { // 1. 初始化三指针中的两个关键指针 struct ListNode* prev = NULL; // 前驱节点,初始为空(反转后的尾节点指向空) struct ListNode* curr = head; // 当前节点,从头开始 // 2. 遍历链表 while (curr != NULL) { // 【关键】先保存下一个节点,防止断链 struct ListNode* next_temp = curr->next; // 3. 核心操作:将当前节点的 next 指向前一个节点(实现反转) curr->next = prev; // 4. 双指针同步后移 prev = curr; // prev 前进到当前位置 curr = next_temp; // curr 前进到原链表的下一个位置 } // 5. 循环结束时,curr 为 NULL,prev 指向原链表尾部(即新链表头部) return prev; }
欧克,到这里就优化完了,但是就只有这两种解法吗?当然不是,接下来再来一个更有意思的解法:递归:

算法解析:递归法(Recursion)

1. 思路

采用“自底向上”的逆向思维策略。我们不从头开始反转,而是假设当前节点之后的所有节点已经由“小弟(递归子问题)”完美反转完毕,当前节点只需要处理“自己与后继节点”**之间的连接关系即可。整个递归过程分为两个阶段:

  • 递推阶段(下楼梯):不断深入链表,直到触达最末尾的节点。
  • 回归阶段(上楼梯):从最后一个节点开始,逐层向上回溯,在每一层完成当前节点的反转操作,并将新链表的头节点一路向上返回。
2. 核心逻辑实现(递归三段论)
  1. 寻找终止条件(Base Case):当链表为空(head == NULL)或到达最后一个节点(head->next == NULL)时,无需反转,直接返回当前节点。
  2. 执行递归调用(Delegate):调用自身处理下一个节点reverseList(head->next),并接收小弟返回的新链表头节点new_head
  3. 处理当前层逻辑(Reverse)
    • 反转指针:让当前节点的“后继节点”指回自己,即head->next->next = head;
    • 断开旧连接:将当前节点原本的next指针置空,防止链表成环,即head->next = NULL;
  4. 返回最终结果(Return):无论在哪一层,整条反转链表的新头节点始终是递归最深处找到的那个new_head,因此直接将其返回即可。
3. 代码 (C语言)
struct ListNode* reverseList(struct ListNode* head) { // 【第一步:终止条件】 // 链表为空或只剩最后一个节点时,直接返回该节点 if (head == NULL || head->next == NULL) { return head; } // 【第二步:递归调用】 // 将后续链表交给子问题处理,并接收反转后的新头节点 struct ListNode* new_head = reverseList(head->next); // 【第三步:当前层逻辑】 // 1. 让后继节点指回当前节点(实现反转) head->next->next = head; // 2. 断开当前节点原来的指向(防止死循环) head->next = NULL; // 【第四步:返回值】 // 始终返回新链表的头节点 return new_head; }

太棒了!能用三种方法解决同一道经典算法题,说明我们对链表指针操作的底层逻辑已经掌握得非常透彻了。

现在我们将这三种方法(头插法、三指针法、递归法)的核心思路与优缺点进行系统性的对比整理:

1. 头插法(构建新链表)

  • 核心思路:采用“头插法”重构链表。依次遍历原链表,将摘取下来的节点不断插入到新链表的最前端。利用“后进先出”的特性,自然实现链表的逆序。
  • 优点
    • 思维直观:完全符合人类的正向线性思维,不需要在脑海中模拟复杂的指针旋转,非常适合初学者理解和手写。
    • 不易出错:每一步操作都是将节点接向一个已知的新链表头,不容易产生死循环。
  • 缺点
    • 思维局限:本质上是一种“拆东墙补西墙”的搬运思维,没有真正触及“原地修改指针”的精髓。

2. 三指针法(原地迭代)

  • 核心思路:采用“局部反转”策略。在遍历链表的过程中,使用三个指针协同工作:prev(前驱)、curr(当前)、next_temp(后继)。在断开当前节点之前,先保存后继节点,然后将当前节点的next指向前驱,最后整体向后推进。
  • 优点
    • 极致高效:空间复杂度为严格的 O(1),不需要任何额外的辅助空间。
    • 工程首选:这是面试和实际工程中最标准、最优雅的解法,体现了对内存和指针的绝对掌控。
  • 缺点
    • 思维难度较高:需要同时维护三个指针的状态,对逻辑严密性要求高,一旦顺序写错(比如没保存next_temp)极易导致断链或死循环。

3. 递归法(自底向上)

  • 核心思路:采用“自底向上”的逆向思维。将问题分解为“反转后续链表”和“处理当前节点”两个子问题。先让递归深入到链表末尾,在回溯(上楼梯)的过程中,让后继节点指回当前节点,并断开当前节点的旧连接。
  • 优点
    • 代码极简:去除了所有显式的循环和临时指针,代码行数最少,结构最优美。
    • 深刻理解:能极大地锻炼对递归树和函数调用栈的理解。
  • 缺点
    • 空间开销大:空间复杂度为 O(N),因为需要依赖系统调用栈。
    • 栈溢出风险:当链表极长时,深层递归容易导致栈溢出(Stack Overflow)。
    • 调试困难:出错时很难像迭代法那样通过断点直观地看到指针的走向。

综合建议与实战选择

方法时间复杂度空间复杂度适用场景
头插法O(N)O(1)快速写出能跑通的代码
三指针法O(N)O(1)面试标准答案、追求极致性能的工程场景
递归法O(N)O(N)链表较短、追求代码优雅、考察递归思维

在真实的面试或笔试中,你可以这样应对:

  1. 首选:直接写出三指针法,展现扎实的基本功。
  2. 备选:如果面试官要求“能不能用递归写一下”,或者“还有没有其他思路”,你可以抛出递归法头插法,展现你思维的广度和对算法的深刻理解。

第四题 寻找链表的中间结点

876. 链表的中间结点 - 力扣(LeetCode)

思路1:使用while循环遍历链表求得链表长度,再利用while循环求得中间结点的指针:

struct ListNode* middleNode(struct ListNode* head) { int size=0; struct ListNode*pmid=head; struct ListNode*pcur=head; while(pcur) { size++; pcur=pcur->next; } int mid=size/2; while(mid) { mid--; pmid=pmid->next; } if(head==NULL) return NULL; return pmid; }
还是老样子,这个方法容易想,但是不好,接下来使用一个又常见又重要的方法:快慢指针法:

快慢指针法(双指针法)是解决链表“找中点”、“找倒数第K个节点”或“判断环”等问题的神器。它的核心在于利用速度差来转换距离关系

针对这道“链表的中间结点”题目,以下是基于快慢指针法的详细解析:

思路

想象两个人在操场跑步,跑道长度就是链表的长度。

  • 慢指针(Slow):每次只跑 1 步(移动一个节点)。
  • 快指针(Fast):每次跑 2 步(移动两个节点)。

由于快指针的速度是慢指针的2倍,当快指针跑完全程(到达链表末尾NULL)时,慢指针刚好跑了一半的路程,也就正好停在链表的中间位置

这种方法将原本需要“先遍历算长度,再遍历找中点”的两次遍历,优化为一次遍历即可完成。

核心逻辑实现

  1. 初始化:定义两个指针slowfast,同时指向链表的头节点head
  2. 循环条件:只要快指针fast没走到头,就继续跑。
    • 注意边界:因为fast一次走两步,所以必须保证fastfast->next都不为空,否则访问fast->next->next会报错。
  3. 步进操作
    • slow = slow->next;(慢指针走一步)
    • fast = fast->next->next;(快指针走两步)
  4. 终止与返回:当循环结束时,fast到了终点,此时slow所在的位置就是中间节点,直接返回slow

代码 (C语言)

struct ListNode* middleNode(struct ListNode* head) { // 1. 定义快慢指针,都从头开始 struct ListNode* slow = head; struct ListNode* fast = head; // 2. 循环条件: // fast != NULL 处理偶数长度情况(fast最终指向尾部节点的下一个空位) // fast->next != NULL 处理奇数长度情况(fast最终指向最后一个节点) while (fast != NULL && fast->next != NULL) { slow = slow->next; // 慢指针走一步 fast = fast->next->next; // 快指针走两步 } // 3. 返回慢指针,即为中点 return slow; }

这里还有一个很重要的点:while循环的判定条件是不能写成:while ( fast->next != NULL&&fast != NULL )这样的,因为如果fast指针为空的时候,fast->next就会报错,原本的不会报错是因为先判断其是否为空指针。


这个方法效率很高,但有个小坑:如果链表有环,快慢指针一定会相遇。不过不用急,我们下面就会讲解到带环链表了

第五题 链表分割

链表分割_牛客题霸_牛客网

这段代码的核心思路是“分而治之”,通过构建两个临时的链表,将原链表中的节点按值的大小进行分流,最后再将它们拼接起来。

核心思路:双链表分流法

想象一下,你面前有一条杂乱的队伍(原链表),你的任务是根据一个标准x,把队伍里的人分成两列:

  • 左队列 (less):所有数值小于x的人。
  • 右队列 (greater):所有数值大于或等于x的人。

等所有人都分好队后,你只需要把“左队列”的队尾和“右队列”的队首连接起来,就得到了一条符合要求的、有序的新队伍。

这个方法巧妙地避免了在原链表上进行复杂的节点移动和指针操作,逻辑非常清晰。

class Partition { public: ListNode* partition(ListNode* pHead, int x) { // write code here ListNode*lesshead,*lesstail; ListNode*greathead,*greattail; lesshead=lesstail=(ListNode*)malloc(sizeof(ListNode)); greathead=greattail=(ListNode*)malloc(sizeof(ListNode)); while(pHead) { if(pHead->val<x) { lesstail->next=pHead; lesstail=pHead; } else { greattail->next=pHead; greattail=pHead; } pHead=pHead->next; } greattail->next=NULL; lesstail->next=greathead->next; ListNode*rethead=lesshead->next; free(lesshead); free(greathead); lesshead=greathead=NULL; return rethead; } };

代码步骤详解

让我们一步步拆解代码是如何实现这个思路的:

1. 创建两个“哨兵”链表
ListNode* lesshead, *lesstail; ListNode* greathead, *greattail; lesshead = lesstail = (ListNode*)malloc(sizeof(ListNode)); greathead = greattail = (ListNode*)malloc(sizeof(ListNode));
  • 目的:创建两个临时的、不存储有效数据的“哨兵节点”(或称“虚拟头节点”)。
    • lessheadgreathead分别是两个新链表的“空头”,它们的作用是提供一个固定的起点,这样在后续插入节点时,就不用判断链表是否为空,简化了逻辑。
    • lesstailgreattail是两个链表的“尾巴”,始终指向链表的最后一个有效节点,方便我们进行尾插操作。
2. 遍历原链表,进行“分流”
while(pHead) { if(pHead->val < x) { // 节点值小于 x,加入 less 链表 lesstail->next = pHead; lesstail = pHead; } else { // 节点值大于等于 x,加入 greater 链表 greattail->next = pHead; greattail = pHead; } pHead = pHead->next; }
  • 目的:这是算法的核心。我们遍历原始的pHead链表。
  • 操作:对于遍历到的每一个节点,判断它的值val
    • 如果val < x,就把它“摘下来”,接到less链表的尾部 (lesstail->next),然后更新lesstail指针,让它指向新的尾节点。
    • 否则 (val >= x),就把它接到greater链表的尾部 (greattail->next),并更新greattail指针。
  • 注意:这里只是改变了节点之间的连接关系,并没有创建新的节点,也没有复制节点的值,所以效率很高。
3. 拼接两个链表并收尾
greattail->next = NULL; lesstail->next = greathead->next;
  • 目的:将分流后的两个链表连接成一个完整的链表。
  • 操作
    1. greattail->next = NULL;:这一步至关重要!因为greater链表的最后一个节点,在原链表中可能指向任何一个节点。我们必须手动将其next指针置为NULL,以标记新链表的结束,防止形成环或连接到错误的节点。
    2. lesstail->next = greathead->next;:将less链表的尾部 (lesstail) 连接到greater链表的第一个有效节点 (greathead->next)。注意,我们跳过greathead这个哨兵节点本身。
4. 确定返回值并清理内存
ListNode* rethead = lesshead->next; free(lesshead); free(greathead); lesshead = greathead = NULL; return rethead;
  • 目的:返回结果并释放我们申请的临时空间。
  • 操作
    1. ListNode* rethead = lesshead->next;:新的链表头就是less链表的第一个有效节点。如果所有节点都大于等于x,那么lesshead->next会是NULL,这也是正确的。
    2. free(...):我们最初用malloc创建了两个哨兵节点,现在任务完成,需要将它们释放掉,避免内存泄漏。
    3. return rethead;:返回最终拼接好的链表的头节点。

总而言之,这段代码通过创建两个辅助链表,将复杂的“分割”问题转化为了简单的“遍历-分类-拼接”三个步骤,是一种非常经典且高效的链表处理技巧。

第六题

本来是要写成一篇博客的,但是现在就已经有12000字了,所以还是分成两篇把......

🌟 写在最后
代码写完了,但我想对正在阅读这篇博客的你说几句心里话。
最近身边有很多同学在迷茫、害怕,大体原因是只能靠自己一个人在这个世界打拼,无依无靠;而世界又在不断变化进步,不知道自己的出路在哪里。
我把送给他们的话,也送给正在屏幕前努力的你:
保持乐观,悲观者正确,乐观者前行。
相信时间的力量,就不必问何时才有回报。
拥有坚持的毅力,就不必在意暂时的苟且。
保持每天的进步,就不必对未来感到迷茫。
愿你在代码的世界里披荆斩棘,也在人生的道路上步履不停。
————————————————
版权声明:本文为CSDN博主「东华万里」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/2401_86187158/article/details/162606768
http://www.jsqmd.com/news/1140556/

相关文章:

  • 3种点云BEV投影方案对比:PCL投影、栅格化与深度学习编码
  • 深度解析MediaCrawler:高效采集多平台数据的智能爬虫框架
  • WeChatMsg:3大突破性能力重塑你的数字记忆主权
  • GRAIL:视频+3D资产驱动的机器人行为理解与运动生成
  • 【Bug已解决】OpenClaw Gateway 报错 bind=tailnet 但未找到 tailnet 接口解决方案
  • IIM-20670与PIC18F87J60高精度运动跟踪方案解析
  • 【安卓逆向】Apk反编译和Frida定位签名校验
  • Hermes Agent 2.0与Harness Engineering:AI智能代理开发实战指南
  • 喵斯快跑 PC/手机双端 平台整合 中文全曲包 双端免装直启
  • find命令高级用法:动作执行与xargs协同实战
  • Windows 7终极兼容方案:让Blender 3.x+在老系统上完美运行
  • 终极指南:如何让经典游戏在现代Windows系统上复活联机功能
  • 3分钟让Android手机变身万能键盘鼠标:USB HID Client完全指南
  • PotPlayer字幕翻译插件:三步轻松实现外语视频无障碍观看终极指南
  • PHP 反序列化漏洞实战:NPUCTF2020 ReadlezPHP 利用 __destruct 执行任意代码
  • PvZ Tools终极指南:植物大战僵尸1.0.0.1051版本完整修改器教程
  • 前端配置 axios/fetch 请求特定海外公开数据接口隔膜报 504 连接超时踩坑记录
  • 终极IPX兼容方案:让90年代经典游戏在现代Windows上重获新生
  • HEIF Utility:Windows用户处理iPhone照片的终极免费解决方案
  • 京东方8.6代AMOLED产线量产,却在三星、苹果高端旗舰订单上接连折戟?
  • 3步配置IPXWrapper:让Windows 10/11完美运行经典游戏联机
  • 从技术展示到商业变现,奇点算力站上 AI 工厂新风口
  • 抖音无水印视频下载完整指南:douyin-downloader一键批量获取教程
  • Seedance2.5本地部署实战:免费AI生图与视频生成全流程
  • 指标告警降噪:报警多了,真正的问题反而没人看
  • 2026年天津统招专升本全日制培训来袭,封闭集训助你上岸!
  • 遥操作触觉引导模型选型:环境参数驱动的实战指南
  • 测试电流选多大才合适
  • Bilibili评论数据采集实战指南:构建完整用户反馈分析管道
  • 《HarmonyOS技术精讲-Core File Kit》第16篇:性能优化——批量操作与并发控制