【Python零基础教程】继承、多态与魔法函数:面向对象编程三大核心特性详解
2026/7/21 22:13:53
对前端开发者而言,学习算法绝非为了“炫技”。它是你从“页面构建者”迈向“复杂系统设计者”的关键阶梯。它将你的编码能力从“实现功能”提升到“设计优雅、高效解决方案”的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。
------ 算法:资深前端开发者的进阶引擎
给你链表的头结点head,请将其按升序排列并返回排序后的链表。
示例 1:
输入:head = [4,2,1,3] 输出:[1,2,3,4]示例 2:
输入:head = [-1,5,3,4,0] 输出:[-1,0,3,4,5]示例 3:
输入:head = [] 输出:[]进阶要求:你可以在O(n log n)时间复杂度和常数级空间复杂度下,对链表进行排序吗?
O(n log n)。这提示我们,像冒泡排序、插入排序等O(n^2)的算法不符合要求。符合此要求的经典算法有:归并排序、快速排序、堆排序。O(log n),不满足常数空间。O(log n)的栈空间开销。O(n log n)时间和O(1)空间,我们必须采用迭代、自底向上的归并排序。这是本题的最优解,也是考察的核心。为什么是归并排序?
归并排序是分治法的典型应用。对于链表,其合并两个有序链表的操作可以在O(n)时间和O(1)空间内完成(只需要调整指针),这比数组归并需要额外空间更具优势。
如何满足O(1)空间?—— 迭代法
subLength表示当前要归并的子链表长度,初始为1。subLength的子链表。subLength加倍,重复上述过程,直到subLength大于或等于整个链表的长度。关键步骤模拟:
假设链表为[4, 2, 1, 3]
subLength = 1: 链表视为[4], [2], [1], [3]-> 两两合并 ->[2,4], [1,3]subLength = 2: 链表视为[2,4], [1,3]-> 两两合并 ->[1,2,3,4]subLength = 4: 已排序完成。cut):从给定链表头开始,切下指定长度的子链表,并返回剩余部分的头节点。merge):合并两个有序链表,返回新链表的头节点。这是LeetCode 21. 合并两个有序链表的直接应用。subLength的增长,内层循环遍历整个链表,进行切分和合并操作。复杂度分析:
O(log n)次,内层循环每次遍历整个链表O(n)。/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } *//** * @param {ListNode} head * @return {ListNode} */varsortList=function(head){// 边界条件处理if(!head||!head.next)returnhead;// 1. 计算链表总长度letlength=0;letnode=head;while(node){length++;node=node.next;}// 2. 创建虚拟头节点,指向原链表constdummyHead=newListNode(0,head);// 3. 自底向上归并for(letsubLength=1;subLength<length;subLength<<=1){// subLength 每次翻倍letprev=dummyHead;// prev 用于连接合并好的子链表letcurr=dummyHead.next;// curr 是当前待处理部分的起点while(curr){// 3.1 切分出第一个子链表 head1lethead1=curr;// 走 subLength - 1 步,找到 head1 的尾部for(leti=1;i<subLength&&curr.next;i++){curr=curr.next;}// 3.2 切分出第二个子链表 head2lethead2=curr.next;curr.next=null;// 切断 head1 与后面的连接curr=head2;// 从 head2 开始,再走 subLength - 1 步,找到 head2 的尾部for(leti=1;i<subLength&&curr&&curr.next;i++){curr=curr.next;}// 3.3 记录剩余部分,并切断 head2 与后面的连接letnext=null;if(curr){next=curr.next;curr.next=null;}// 3.4 合并 head1 和 head2,并将结果连接到 prev 后面constmerged=mergeTwoLists(head1,head2);prev.next=merged;// 3.5 将 prev 移动到合并后链表的末尾,准备连接下一组合并结果while(prev.next){prev=prev.next;}// 3.6 curr 移动到剩余部分,继续处理下一对子链表curr=next;}}returndummyHead.next;};/** * 合并两个有序链表 (LeetCode 21) * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */functionmergeTwoLists(l1,l2){constdummy=newListNode(0);letcur=dummy;while(l1&&l2){if(l1.val<=l2.val){cur.next=l1;l1=l1.next;}else{cur.next=l2;l2=l2.next;}cur=cur.next;}// 连接剩余部分cur.next=l1?l1:l2;returndummy.next;}varsortList=function(head){// 递归终止条件if(!head||!head.next)returnhead;// 1. 使用快慢指针找到链表中点letslow=head,fast=head.next;while(fast&&fast.next){slow=slow.next;fast=fast.next.next;}// 2. 切断链表,分成左右两部分constmid=slow.next;slow.next=null;// 3. 递归排序左右两部分constleft=sortList(head);constright=sortList(mid);// 4. 合并两个有序链表returnmergeTwoLists(left,right);};// mergeTwoLists 函数同上varsortList=function(head){if(!head)returnnull;// 1. 链表转数组constarr=[];letcurr=head;while(curr){arr.push(curr.val);curr=curr.next;}// 2. 数组排序arr.sort((a,b)=>a-b);// 3. 数组转回链表constdummy=newListNode(0);curr=dummy;for(constvalofarr){curr.next=newListNode(val);curr=curr.next;}returndummy.next;};| 思路 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 | 是否满足进阶要求 |
|---|---|---|---|---|---|
| 迭代归并排序 | O(n log n) | O(1) | 满足所有进阶要求;纯指针操作,空间效率极致 | 代码实现相对复杂,边界条件需仔细处理 | 是 |
| 递归归并排序 | O(n log n) | O(log n) | 代码清晰,易于理解和实现;分治思想的经典体现 | 递归调用栈消耗额外空间,不满足常数空间要求 | 否 |
| 转为数组排序 | O(n log n) | O(n) | 实现极其简单,快速;利用语言原生API | 需要额外O(n)空间存储数组和新建链表,破坏了原链表节点 | 否 |
O(1)时间的插入/删除是其优势。排序算法需要适应这一特性。cut和merge过程中,对指针(next)的精确控制是正确实现的关键,也是前端开发者需要熟练掌握的核心技能之一。虽然前端中直接操作链表排序的场景不多,但本题所锻炼的能力具有广泛的迁移价值:
mergeTwoLists的变体。优化此合并过程能极大提升列表滚动的流畅度。