文章目录题目解法选择单链表 dummy 虚拟头节点关键约定dummy 不算下标代码逐函数解析对应题目 5 个接口1get(index)2addAtHead(val)3addAtTail(val)4addAtIndex(index, val)5deleteAtIndex(index)复杂度分析常见易错点题目你可以选择使用单链表或者双链表设计并实现自己的链表。单链表中的节点应该具备两个属性val和next。val是当前节点的值next是指向下一个节点的指针/引用。如果是双向链表则还需要属性prev以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。实现MyLinkedList类MyLinkedList()初始化MyLinkedList对象。int get(int index)获取链表中下标为index的节点的值。如果下标无效则返回-1。void addAtHead(int val)将一个值为val的节点插入到链表中第一个元素之前。插入完成后新节点成为链表的第一个节点。void addAtTail(int val)将一个值为val的节点追加到链表末尾。void addAtIndex(int index, int val)将一个值为val的节点插入到链表中下标为index的节点之前。如果index 链表长度则追加到末尾如果index 链表长度则不插入。void deleteAtIndex(int index)如果下标有效则删除链表中下标为index的节点。示例输入[MyLinkedList, addAtHead, addAtTail, addAtIndex, get, deleteAtIndex, get] [[], [1], [3], [1, 2], [1], [1], [1]]输出[null, null, null, null, 2, null, 3]解释addAtHead(1)1addAtTail(3)1-3addAtIndex(1,2)1-2-3get(1)返回2deleteAtIndex(1)1-3get(1)返回3解法选择单链表 dummy 虚拟头节点这题可以用双链表但初学时最稳、最容易写对的是单链表节点只维护val和nextdummy虚拟头节点放在真实链表前面不算下标size记录真实节点数量这样做的最大好处是所有插入/删除都能统一成“先找到前驱节点再改 next”不需要对“删除头节点 / 头部插入”写额外特判。关键约定dummy 不算下标假设链表是dummy - A - B - C - None 0 1 2dummy是虚拟头节点不算第 0 个节点下标0的节点是dummy.next也就是 A因此会出现两个非常固定的走法找第 index 个节点从 dummy 走index 1步找第 index 个节点的前驱节点从 dummy 走index步后面get / addAtIndex / deleteAtIndex都靠这个规则。代码classListNode:def__init__(self,val0,nextNone):self.valval self.nextnextclassMyLinkedList:def__init__(self):self.size0self.dummyListNode(0)# 虚拟头节点不算下标defget(self,index:int)-int:# 1) 下标检查合法范围是 [0, size-1]ifindex0orindexself.size:return-1# 2) 从 dummy 出发走 index1 步到达第 index 个真实节点curself.dummyfor_inrange(index1):curcur.next# 3) cur 已经是目标节点returncur.valdefaddAtHead(self,val:int)-None:# 头插等价于在 index0 处插入self.addAtIndex(0,val)defaddAtTail(self,val:int)-None:# 尾插等价于在 indexsize 处插入题目规定index长度表示追加self.addAtIndex(self.size,val)defaddAtIndex(self,index:int,val:int)-None:# 1) 如果 index size直接不插入ifindexself.size:return# 2) 兼容写法index0 当作插到头部虽然题目保证 index0ifindex0:index0# 3) 找到“index 位置的前驱节点”# 因为 dummy 不算下标所以走 index 步就到前驱curself.dummyfor_inrange(index):curcur.next# 4) 插入新节点new_node.next 指向原后继再让前驱 cur.next 指向 new_node# 这一句等价于# new_node ListNode(val)# new_node.next cur.next# cur.next new_nodecur.nextListNode(val,cur.next)# 5) 更新长度self.size1defdeleteAtIndex(self,index:int)-None:# 1) 下标检查ifindex0orindexself.size:return# 2) 找到“index 位置的前驱节点”curself.dummyfor_inrange(index):curcur.next# 3) 删除跳过目标节点cur.next让 cur.next 指向 cur.next.nextcur.nextcur.next.next# 4) 更新长度self.size-1逐函数解析对应题目 5 个接口1get(index)为什么从 dummy 开始dummy 是统一入口。为什么走index1步因为dummy.next才是下标 0 的真实节点。时间复杂度最坏走到尾部 (O(n))2addAtHead(val)头插就是在下标 0 前插入 → 复用addAtIndex(0, val)3addAtTail(val)当前尾部的下标是size-1题目规定index size表示“插到末尾后面”所以复用addAtIndex(size, val)新节点的 next 是什么当index size时前驱会走到当前最后一个真实节点当前尾节点的next原本是None因此新节点会被创建成ListNode(val, None)自然成为新尾节点4addAtIndex(index, val)核心思想先找前驱再插入。找前驱从 dummy 走index步插入cur.next ListNode(val, cur.next)维护size5deleteAtIndex(index)同样是先找前驱再跳过。找前驱从 dummy 走index步删除cur.next cur.next.next维护size复杂度分析设链表长度为 (n)get最坏遍历 (O(n))addAtHead复用addAtIndex(0)定位前驱 (O(1))插入改指针 (O(1)) → (O(1))addAtTail最坏需要走到尾部因为没有尾指针→ (O(n))addAtIndex最坏走到 index → (O(n))deleteAtIndex最坏走到 index → (O(n))额外空间只用常数指针和计数 → (O(1))不算节点存储常见易错点dummy 是否算下标 0不算dummy.next才是下标 0。get 走几步走index1。插入/删除找前驱走几步走index。最后是否更新 size插入1删除-1。delete 时会不会cur.next为空不会因为提前检查了index size。