本篇博客内容是为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 pheadNULL; listnode ptailNULL; while(head) { if(head-val!val) { //如果新链表为空初始化头尾指针 if(pheadNULL) { pheadhead; ptailhead; } else {// 否则接到尾部并更新尾指针 ptail-nexthead; ptailhead; } } // 无论是否删除原链表指针都要后移 headhead-next; } // 有当新链表不为空时才断开尾部连接 if(ptail!NULL) ptail-nextNULL; return phead; }这段代码的核心思路是**“筛选重建法”。它并没有直接在原链表上进行复杂的指针删除操作这容易导致断链或丢失节点而是选择遍历原链表将符合条件的节点“摘”出来按顺序拼接到一个新的链表中**。这种方法的优点是逻辑清晰、不易出错且能很好地处理头节点被删除的情况。以下是详细的编写思路解析核心策略双链表操作代码维护了两个逻辑上的链表原链表由head驱动作为数据源负责从头到尾扫描每一个节点。新链表由phead和ptail维护作为结果容器只接收那些值不等于val的节点。变量定义与初始化listnode phead NULL;新链表的头指针。初始化为空表示新链表还没开始建立。最终函数将返回这个指针。listnode ptail NULL;新链表的尾指针。始终指向新链表的最后一个节点用于快速在尾部追加新节点O(1) 复杂度。head作为游标负责在原链表中不断向后移动 (head head-next)。循环筛选逻辑这是代码的主体部分通过while(head)遍历原链表判断条件if (head-val ! val)检查当前节点是否需要保留。只有当节点的值不等于目标值val时才执行后续操作如果相等则直接跳过相当于“删除”了该节点因为它没有被接入新链表。接入新链表这里分两种情况处理保证了链表结构的完整性新链表为空第一个有效节点if (phead NULL) { phead head; // 确立头节点 ptail head; // 此时头也是尾 }这是起始状态需要同时初始化头和尾。新链表已有节点后续有效节点else { ptail-next head; // 将当前节点挂到旧尾巴后面 ptail head; // 更新尾巴为当前节点 }利用尾插法将节点串联起来。收尾工作循环结束后必须处理新链表的结尾防止野指针问题。if (ptail ! NULL) ptail-next NULL;为什么要做这一步因为我们是直接把原链表的节点拿过来用的原链表中这些节点的next指针可能还指向后面的节点甚至是被跳过的、值为val的节点。如果不手动置空新链表的尾部可能会拖着一条长长的“垃圾尾巴”导致后续遍历出错或死循环。为什么判空如果原链表所有节点都被删除了例如[6,6], val6ptail依然是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; }为什么这种方法更好消灭了if-else分支在普通方法中删除头节点需要head head-next而删除中间节点需要prev-next curr-next。这是两套逻辑。有了哨兵节点所有的删除操作都变成了prev-next curr-next。哪怕删除的是第一个真实节点它的前驱也是sentinel逻辑完全通用。避免了空指针判断你不需要担心head是否为空也不需要担心删除后链表变空的情况。sentinel.next会自然地处理这一切如果全删光了它就是NULL。代码更简洁不需要维护phead和ptail两个指针只需要一个prev指针就能搞定所有事情。图解执行过程假设链表为[1] - [2] - [6] - [3]我们要删除6。初始状态sentinel - [1] - [2] - [6] - [3]prev指向sentinel。检查 1不等于 6。prev移动到[1]。检查 2不等于 6。prev移动到[2]。检查 6等于 6执行prev-next curr-next。此时[2]的next直接连到了[3]。[6]被孤立断开连接。关键点prev不动依然指向[2]。下一轮循环curr变成了prev-next也就是[3]。检查 3不等于 6。prev移动到[3]。结束返回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是非常专业且优雅的优化手段。它的核心优势在于统一处理逻辑无论是插入第一个节点还是后续节点操作都变成在dummy.next之后插入彻底消除了if head is None这类边界判断。防止空指针异常始终有一个安全的起点不用担心修改头指针时丢失引用。代码更简洁主循环或逻辑块里只保留核心操作可读性大幅提升。优化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)仅复用原节点。核心逻辑解析初始化创建一个new_head指针初始化为NULL。这代表一个新的、空的链表。遍历与摘除使用while循环遍历原链表。在修改当前节点的指向之前必须先保存下一个节点的地址否则链表就断了找不到后面的路了。头插操作让当前节点的next指向new_head把它插到新链表的最前面。更新new_head为当前节点它现在变成了新的头。移动游标将原链表的遍历指针移向刚才保存的“下一个节点”。标准代码实现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 为例)初始状态head指向 1new_head为 NULL第一轮循环 (处理节点 1)保存next_node(即节点 2)1-next指向new_head(NULL) —— 此时链表变成1 - NULLnew_head更新为 1head移动到next_node(即节点 2)第二轮循环 (处理节点 2)保存next_node(即节点 3)2-next指向new_head(节点 1) —— 此时链表变成2 - 1 - NULLnew_head更新为 2head移动到next_node(即节点 3)...以此类推最终new_head就会指向完全反转后的链表头。优化但是这个方法实在是太普通了接下来我们引入一个非常常见与重要的方法三指针法这是面试和工程中最经典、最标准的解法它的核心在于“原地反转”不需要创建新链表空间复杂度为 O(1)。算法解析三指针法原地迭代1. 思路采用“局部反转”策略。我们可以把链表想象成一列火车我们不需要把车厢拆下来重装而是站在车厢连接处依次将每一节车厢的挂钩方向调转。为了保证在调转挂钩时不丢失后续车厢的连接我们需要三只手三个指针协同工作一只手抓住前一个节点作为新的指向目标一只手抓住当前节点正在操作的节点一只手提前抓住下一个节点防止断链后迷路。2. 核心逻辑实现初始化指针定义prev前驱指针初始化为NULL因为反转后原头节点将变为尾节点其next应指向空定义curr当前指针指向原链表头节点head。循环遍历当curr不为空时进入循环。保存后继关键步骤。在修改curr的指向之前必须先用临时指针next_temp保存curr-next。这是因为一旦修改了curr-next原链表后续部分就会断开若不保存将无法继续遍历。执行反转将curr-next指向prev。此时当前节点与前一个节点的连接方向完成逆转。指针推进将prev移动到curr的位置前驱指针后移。将curr移动到next_temp的位置当前指针后移处理下一个节点。返回结果当循环结束时curr为NULL而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 为 NULLprev 指向原链表尾部即新链表头部 return prev; }欧克到这里就优化完了但是就只有这两种解法吗当然不是接下来再来一个更有意思的解法递归算法解析递归法Recursion1. 思路采用“自底向上”的逆向思维策略。我们不从头开始反转而是假设当前节点之后的所有节点已经由“小弟递归子问题”完美反转完毕当前节点只需要处理“自己与后继节点”**之间的连接关系即可。整个递归过程分为两个阶段递推阶段下楼梯不断深入链表直到触达最末尾的节点。回归阶段上楼梯从最后一个节点开始逐层向上回溯在每一层完成当前节点的反转操作并将新链表的头节点一路向上返回。2. 核心逻辑实现递归三段论寻找终止条件Base Case当链表为空head NULL或到达最后一个节点head-next NULL时无需反转直接返回当前节点。执行递归调用Delegate调用自身处理下一个节点reverseList(head-next)并接收小弟返回的新链表头节点new_head。处理当前层逻辑Reverse反转指针让当前节点的“后继节点”指回自己即head-next-next head;。断开旧连接将当前节点原本的next指针置空防止链表成环即head-next NULL;。返回最终结果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)链表较短、追求代码优雅、考察递归思维在真实的面试或笔试中你可以这样应对首选直接写出三指针法展现扎实的基本功。备选如果面试官要求“能不能用递归写一下”或者“还有没有其他思路”你可以抛出递归法或头插法展现你思维的广度和对算法的深刻理解。第四题 寻找链表的中间结点876. 链表的中间结点 - 力扣LeetCode思路1使用while循环遍历链表求得链表长度再利用while循环求得中间结点的指针struct ListNode* middleNode(struct ListNode* head) { int size0; struct ListNode*pmidhead; struct ListNode*pcurhead; while(pcur) { size; pcurpcur-next; } int midsize/2; while(mid) { mid--; pmidpmid-next; } if(headNULL) return NULL; return pmid; }还是老样子这个方法容易想但是不好接下来使用一个又常见又重要的方法快慢指针法快慢指针法双指针法是解决链表“找中点”、“找倒数第K个节点”或“判断环”等问题的神器。它的核心在于利用速度差来转换距离关系。针对这道“链表的中间结点”题目以下是基于快慢指针法的详细解析思路想象两个人在操场跑步跑道长度就是链表的长度。慢指针Slow每次只跑 1 步移动一个节点。快指针Fast每次跑 2 步移动两个节点。由于快指针的速度是慢指针的2倍当快指针跑完全程到达链表末尾NULL时慢指针刚好跑了一半的路程也就正好停在链表的中间位置。这种方法将原本需要“先遍历算长度再遍历找中点”的两次遍历优化为一次遍历即可完成。核心逻辑实现初始化定义两个指针slow和fast同时指向链表的头节点head。循环条件只要快指针fast没走到头就继续跑。注意边界因为fast一次走两步所以必须保证fast和fast-next都不为空否则访问fast-next-next会报错。步进操作slow slow-next;慢指针走一步fast fast-next-next;快指针走两步终止与返回当循环结束时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 ! NULLfast ! 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; lessheadlesstail(ListNode*)malloc(sizeof(ListNode)); greatheadgreattail(ListNode*)malloc(sizeof(ListNode)); while(pHead) { if(pHead-valx) { lesstail-nextpHead; lesstailpHead; } else { greattail-nextpHead; greattailpHead; } pHeadpHead-next; } greattail-nextNULL; lesstail-nextgreathead-next; ListNode*retheadlesshead-next; free(lesshead); free(greathead); lessheadgreatheadNULL; return rethead; } };代码步骤详解让我们一步步拆解代码是如何实现这个思路的1. 创建两个“哨兵”链表ListNode* lesshead, *lesstail; ListNode* greathead, *greattail; lesshead lesstail (ListNode*)malloc(sizeof(ListNode)); greathead greattail (ListNode*)malloc(sizeof(ListNode));目的创建两个临时的、不存储有效数据的“哨兵节点”或称“虚拟头节点”。lesshead和greathead分别是两个新链表的“空头”它们的作用是提供一个固定的起点这样在后续插入节点时就不用判断链表是否为空简化了逻辑。lesstail和greattail是两个链表的“尾巴”始终指向链表的最后一个有效节点方便我们进行尾插操作。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;目的将分流后的两个链表连接成一个完整的链表。操作greattail-next NULL;这一步至关重要因为greater链表的最后一个节点在原链表中可能指向任何一个节点。我们必须手动将其next指针置为NULL以标记新链表的结束防止形成环或连接到错误的节点。lesstail-next greathead-next;将less链表的尾部 (lesstail) 连接到greater链表的第一个有效节点 (greathead-next)。注意我们跳过greathead这个哨兵节点本身。4. 确定返回值并清理内存ListNode* rethead lesshead-next; free(lesshead); free(greathead); lesshead greathead NULL; return rethead;目的返回结果并释放我们申请的临时空间。操作ListNode* rethead lesshead-next;新的链表头就是less链表的第一个有效节点。如果所有节点都大于等于x那么lesshead-next会是NULL这也是正确的。free(...)我们最初用malloc创建了两个哨兵节点现在任务完成需要将它们释放掉避免内存泄漏。return rethead;返回最终拼接好的链表的头节点。总而言之这段代码通过创建两个辅助链表将复杂的“分割”问题转化为了简单的“遍历-分类-拼接”三个步骤是一种非常经典且高效的链表处理技巧。第六题本来是要写成一篇博客的但是现在就已经有12000字了所以还是分成两篇把...... 写在最后代码写完了但我想对正在阅读这篇博客的你说几句心里话。最近身边有很多同学在迷茫、害怕大体原因是只能靠自己一个人在这个世界打拼无依无靠而世界又在不断变化进步不知道自己的出路在哪里。我把送给他们的话也送给正在屏幕前努力的你保持乐观悲观者正确乐观者前行。相信时间的力量就不必问何时才有回报。拥有坚持的毅力就不必在意暂时的苟且。保持每天的进步就不必对未来感到迷茫。愿你在代码的世界里披荆斩棘也在人生的道路上步履不停。————————————————版权声明本文为CSDN博主「东华万里」的原创文章遵循CC 4.0 BY-SA版权协议转载请附上原文出处链接及本声明。原文链接https://blog.csdn.net/2401_86187158/article/details/162606768
第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 pheadNULL; listnode ptailNULL; while(head) { if(head-val!val) { //如果新链表为空初始化头尾指针 if(pheadNULL) { pheadhead; ptailhead; } else {// 否则接到尾部并更新尾指针 ptail-nexthead; ptailhead; } } // 无论是否删除原链表指针都要后移 headhead-next; } // 有当新链表不为空时才断开尾部连接 if(ptail!NULL) ptail-nextNULL; return phead; }这段代码的核心思路是**“筛选重建法”。它并没有直接在原链表上进行复杂的指针删除操作这容易导致断链或丢失节点而是选择遍历原链表将符合条件的节点“摘”出来按顺序拼接到一个新的链表中**。这种方法的优点是逻辑清晰、不易出错且能很好地处理头节点被删除的情况。以下是详细的编写思路解析核心策略双链表操作代码维护了两个逻辑上的链表原链表由head驱动作为数据源负责从头到尾扫描每一个节点。新链表由phead和ptail维护作为结果容器只接收那些值不等于val的节点。变量定义与初始化listnode phead NULL;新链表的头指针。初始化为空表示新链表还没开始建立。最终函数将返回这个指针。listnode ptail NULL;新链表的尾指针。始终指向新链表的最后一个节点用于快速在尾部追加新节点O(1) 复杂度。head作为游标负责在原链表中不断向后移动 (head head-next)。循环筛选逻辑这是代码的主体部分通过while(head)遍历原链表判断条件if (head-val ! val)检查当前节点是否需要保留。只有当节点的值不等于目标值val时才执行后续操作如果相等则直接跳过相当于“删除”了该节点因为它没有被接入新链表。接入新链表这里分两种情况处理保证了链表结构的完整性新链表为空第一个有效节点if (phead NULL) { phead head; // 确立头节点 ptail head; // 此时头也是尾 }这是起始状态需要同时初始化头和尾。新链表已有节点后续有效节点else { ptail-next head; // 将当前节点挂到旧尾巴后面 ptail head; // 更新尾巴为当前节点 }利用尾插法将节点串联起来。收尾工作循环结束后必须处理新链表的结尾防止野指针问题。if (ptail ! NULL) ptail-next NULL;为什么要做这一步因为我们是直接把原链表的节点拿过来用的原链表中这些节点的next指针可能还指向后面的节点甚至是被跳过的、值为val的节点。如果不手动置空新链表的尾部可能会拖着一条长长的“垃圾尾巴”导致后续遍历出错或死循环。为什么判空如果原链表所有节点都被删除了例如[6,6], val6ptail依然是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; }为什么这种方法更好消灭了if-else分支在普通方法中删除头节点需要head head-next而删除中间节点需要prev-next curr-next。这是两套逻辑。有了哨兵节点所有的删除操作都变成了prev-next curr-next。哪怕删除的是第一个真实节点它的前驱也是sentinel逻辑完全通用。避免了空指针判断你不需要担心head是否为空也不需要担心删除后链表变空的情况。sentinel.next会自然地处理这一切如果全删光了它就是NULL。代码更简洁不需要维护phead和ptail两个指针只需要一个prev指针就能搞定所有事情。图解执行过程假设链表为[1] - [2] - [6] - [3]我们要删除6。初始状态sentinel - [1] - [2] - [6] - [3]prev指向sentinel。检查 1不等于 6。prev移动到[1]。检查 2不等于 6。prev移动到[2]。检查 6等于 6执行prev-next curr-next。此时[2]的next直接连到了[3]。[6]被孤立断开连接。关键点prev不动依然指向[2]。下一轮循环curr变成了prev-next也就是[3]。检查 3不等于 6。prev移动到[3]。结束返回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是非常专业且优雅的优化手段。它的核心优势在于统一处理逻辑无论是插入第一个节点还是后续节点操作都变成在dummy.next之后插入彻底消除了if head is None这类边界判断。防止空指针异常始终有一个安全的起点不用担心修改头指针时丢失引用。代码更简洁主循环或逻辑块里只保留核心操作可读性大幅提升。优化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)仅复用原节点。核心逻辑解析初始化创建一个new_head指针初始化为NULL。这代表一个新的、空的链表。遍历与摘除使用while循环遍历原链表。在修改当前节点的指向之前必须先保存下一个节点的地址否则链表就断了找不到后面的路了。头插操作让当前节点的next指向new_head把它插到新链表的最前面。更新new_head为当前节点它现在变成了新的头。移动游标将原链表的遍历指针移向刚才保存的“下一个节点”。标准代码实现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 为例)初始状态head指向 1new_head为 NULL第一轮循环 (处理节点 1)保存next_node(即节点 2)1-next指向new_head(NULL) —— 此时链表变成1 - NULLnew_head更新为 1head移动到next_node(即节点 2)第二轮循环 (处理节点 2)保存next_node(即节点 3)2-next指向new_head(节点 1) —— 此时链表变成2 - 1 - NULLnew_head更新为 2head移动到next_node(即节点 3)...以此类推最终new_head就会指向完全反转后的链表头。优化但是这个方法实在是太普通了接下来我们引入一个非常常见与重要的方法三指针法这是面试和工程中最经典、最标准的解法它的核心在于“原地反转”不需要创建新链表空间复杂度为 O(1)。算法解析三指针法原地迭代1. 思路采用“局部反转”策略。我们可以把链表想象成一列火车我们不需要把车厢拆下来重装而是站在车厢连接处依次将每一节车厢的挂钩方向调转。为了保证在调转挂钩时不丢失后续车厢的连接我们需要三只手三个指针协同工作一只手抓住前一个节点作为新的指向目标一只手抓住当前节点正在操作的节点一只手提前抓住下一个节点防止断链后迷路。2. 核心逻辑实现初始化指针定义prev前驱指针初始化为NULL因为反转后原头节点将变为尾节点其next应指向空定义curr当前指针指向原链表头节点head。循环遍历当curr不为空时进入循环。保存后继关键步骤。在修改curr的指向之前必须先用临时指针next_temp保存curr-next。这是因为一旦修改了curr-next原链表后续部分就会断开若不保存将无法继续遍历。执行反转将curr-next指向prev。此时当前节点与前一个节点的连接方向完成逆转。指针推进将prev移动到curr的位置前驱指针后移。将curr移动到next_temp的位置当前指针后移处理下一个节点。返回结果当循环结束时curr为NULL而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 为 NULLprev 指向原链表尾部即新链表头部 return prev; }欧克到这里就优化完了但是就只有这两种解法吗当然不是接下来再来一个更有意思的解法递归算法解析递归法Recursion1. 思路采用“自底向上”的逆向思维策略。我们不从头开始反转而是假设当前节点之后的所有节点已经由“小弟递归子问题”完美反转完毕当前节点只需要处理“自己与后继节点”**之间的连接关系即可。整个递归过程分为两个阶段递推阶段下楼梯不断深入链表直到触达最末尾的节点。回归阶段上楼梯从最后一个节点开始逐层向上回溯在每一层完成当前节点的反转操作并将新链表的头节点一路向上返回。2. 核心逻辑实现递归三段论寻找终止条件Base Case当链表为空head NULL或到达最后一个节点head-next NULL时无需反转直接返回当前节点。执行递归调用Delegate调用自身处理下一个节点reverseList(head-next)并接收小弟返回的新链表头节点new_head。处理当前层逻辑Reverse反转指针让当前节点的“后继节点”指回自己即head-next-next head;。断开旧连接将当前节点原本的next指针置空防止链表成环即head-next NULL;。返回最终结果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)链表较短、追求代码优雅、考察递归思维在真实的面试或笔试中你可以这样应对首选直接写出三指针法展现扎实的基本功。备选如果面试官要求“能不能用递归写一下”或者“还有没有其他思路”你可以抛出递归法或头插法展现你思维的广度和对算法的深刻理解。第四题 寻找链表的中间结点876. 链表的中间结点 - 力扣LeetCode思路1使用while循环遍历链表求得链表长度再利用while循环求得中间结点的指针struct ListNode* middleNode(struct ListNode* head) { int size0; struct ListNode*pmidhead; struct ListNode*pcurhead; while(pcur) { size; pcurpcur-next; } int midsize/2; while(mid) { mid--; pmidpmid-next; } if(headNULL) return NULL; return pmid; }还是老样子这个方法容易想但是不好接下来使用一个又常见又重要的方法快慢指针法快慢指针法双指针法是解决链表“找中点”、“找倒数第K个节点”或“判断环”等问题的神器。它的核心在于利用速度差来转换距离关系。针对这道“链表的中间结点”题目以下是基于快慢指针法的详细解析思路想象两个人在操场跑步跑道长度就是链表的长度。慢指针Slow每次只跑 1 步移动一个节点。快指针Fast每次跑 2 步移动两个节点。由于快指针的速度是慢指针的2倍当快指针跑完全程到达链表末尾NULL时慢指针刚好跑了一半的路程也就正好停在链表的中间位置。这种方法将原本需要“先遍历算长度再遍历找中点”的两次遍历优化为一次遍历即可完成。核心逻辑实现初始化定义两个指针slow和fast同时指向链表的头节点head。循环条件只要快指针fast没走到头就继续跑。注意边界因为fast一次走两步所以必须保证fast和fast-next都不为空否则访问fast-next-next会报错。步进操作slow slow-next;慢指针走一步fast fast-next-next;快指针走两步终止与返回当循环结束时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 ! NULLfast ! 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; lessheadlesstail(ListNode*)malloc(sizeof(ListNode)); greatheadgreattail(ListNode*)malloc(sizeof(ListNode)); while(pHead) { if(pHead-valx) { lesstail-nextpHead; lesstailpHead; } else { greattail-nextpHead; greattailpHead; } pHeadpHead-next; } greattail-nextNULL; lesstail-nextgreathead-next; ListNode*retheadlesshead-next; free(lesshead); free(greathead); lessheadgreatheadNULL; return rethead; } };代码步骤详解让我们一步步拆解代码是如何实现这个思路的1. 创建两个“哨兵”链表ListNode* lesshead, *lesstail; ListNode* greathead, *greattail; lesshead lesstail (ListNode*)malloc(sizeof(ListNode)); greathead greattail (ListNode*)malloc(sizeof(ListNode));目的创建两个临时的、不存储有效数据的“哨兵节点”或称“虚拟头节点”。lesshead和greathead分别是两个新链表的“空头”它们的作用是提供一个固定的起点这样在后续插入节点时就不用判断链表是否为空简化了逻辑。lesstail和greattail是两个链表的“尾巴”始终指向链表的最后一个有效节点方便我们进行尾插操作。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;目的将分流后的两个链表连接成一个完整的链表。操作greattail-next NULL;这一步至关重要因为greater链表的最后一个节点在原链表中可能指向任何一个节点。我们必须手动将其next指针置为NULL以标记新链表的结束防止形成环或连接到错误的节点。lesstail-next greathead-next;将less链表的尾部 (lesstail) 连接到greater链表的第一个有效节点 (greathead-next)。注意我们跳过greathead这个哨兵节点本身。4. 确定返回值并清理内存ListNode* rethead lesshead-next; free(lesshead); free(greathead); lesshead greathead NULL; return rethead;目的返回结果并释放我们申请的临时空间。操作ListNode* rethead lesshead-next;新的链表头就是less链表的第一个有效节点。如果所有节点都大于等于x那么lesshead-next会是NULL这也是正确的。free(...)我们最初用malloc创建了两个哨兵节点现在任务完成需要将它们释放掉避免内存泄漏。return rethead;返回最终拼接好的链表的头节点。总而言之这段代码通过创建两个辅助链表将复杂的“分割”问题转化为了简单的“遍历-分类-拼接”三个步骤是一种非常经典且高效的链表处理技巧。第六题本来是要写成一篇博客的但是现在就已经有12000字了所以还是分成两篇把...... 写在最后代码写完了但我想对正在阅读这篇博客的你说几句心里话。最近身边有很多同学在迷茫、害怕大体原因是只能靠自己一个人在这个世界打拼无依无靠而世界又在不断变化进步不知道自己的出路在哪里。我把送给他们的话也送给正在屏幕前努力的你保持乐观悲观者正确乐观者前行。相信时间的力量就不必问何时才有回报。拥有坚持的毅力就不必在意暂时的苟且。保持每天的进步就不必对未来感到迷茫。愿你在代码的世界里披荆斩棘也在人生的道路上步履不停。————————————————版权声明本文为CSDN博主「东华万里」的原创文章遵循CC 4.0 BY-SA版权协议转载请附上原文出处链接及本声明。原文链接https://blog.csdn.net/2401_86187158/article/details/162606768