链表理论基础
建议:了解一下链接基础,以及链表和数组的区别
文章链接:代码随想录
我个人也总结了一些相关概念:
203.移除链表元素 (Easy)
建议: 本题最关键是要理解 虚拟头结点的使用技巧,这个对链表题目很重要。
题目链接:https://leetcode.com/problems/remove-linked-list-elements/description/
文章讲解:代码随想录
视频讲解:手把手带你学会操作链表 | LeetCode:203.移除链表元素_哔哩哔哩_bilibili
👩💻思路
1.创建虚拟头节点:
这个技巧适用于需要对链表进行操作但又不想处理头节点特殊情况的情况。虚拟头节点是一个额外的节点,它的下一个节点指向真正的链表头节点。这样做的好处是可以统一对链表进行操作,无需单独处理头节点的特殊情况,简化了代码逻辑。
例如,在删除链表中的某个节点时,如果没有虚拟头节点,就需要分别处理删除头节点和删除其他节点的情况;而有了虚拟头节点,就可以始终将删除操作统一为删除下一个节点,无需单独处理头节点。
2.直接处理头节点:
这种方法则是直接操作头节点,不使用虚拟头节点。这在某些情况下可能更简洁直观,特别是对于一些简单的链表操作。但是需要注意的是,在处理头节点时要确保不会因为头节点为空或者操作导致链表断裂等问题。
例如,如果要在链表头部插入一个新节点,直接处理头节点就是将新节点的下一个节点指向当前的头节点,然后将新节点设为头节点;而使用虚拟头节点时,则直接在虚拟头节点之后插入新节点,无需单独处理头节点的情况。
3.总结:
选择哪种方法取决于具体的需求和代码逻辑,有时候虚拟头节点可以简化代码,而直接处理头节点则可能更直观。
📊视频讲解要点总结:
1.首先,要判断删除的是不是头节点还是非头节点,如果是头节点,有两种方法删除:第一,head = head —> next,将头节点指针指向下一个node即可删除。第二,也可以使用dummy head虚拟头节点的方式删除头节点;针对非头节点,我们可以直接使用原来列表,将指针指向下一个node来删除。(单链表)
2.删除元素必须有前驱,这就是虚拟头节点的魅力所在。因此,创建一个指针,current指针要指向虚拟头节点,而不是真的头节点。如果你想删除真的头节点,直接current.next = current.next.next。
3.遍历列表之前,先判断head是否为空。
4.最终返回的是什么?我们需要返回虚拟头节点的下一个而不是原先的head因为有可能head已经被我们删掉了。(return dummy_head-->next
5.使用虚拟头节点的便利性更高,因为可以适用于所有情况。
❌易错点/难点总结:
(以下是我写代码过程中出现的小错误,仅供参考)
1.比较节点本身而不是节点的值:
- 错误示例:if head == val
- 解释:在链表操作中,应比较节点的值(head.val)而不是节点对象本身。
- 正确示例:if head.val == val
2.错误的循环条件:
- 错误示例:while head
- 解释:在遍历链表时,如果使用 head 作为循环条件会导致无法正确处理链表中的节点。应该使用 curr.next 作为循环条件,以确保在遍历时正确跳过匹配的节点。
- 正确示例:while curr.next
3.指针移动位置错误:
- 错误示例:curr = curr.next 放置在错误的代码块中。
- 解释:在处理完当前节点后,正确地移动指针(curr = curr.next)非常重要。确保只在没有删除节点时移动指针。
- 正确示例:在处理完节点的删除逻辑后,再移动指针。
4.未初始化虚拟头节点:
- 错误示例:未创建虚拟头节点或虚拟头节点指向错误。
- 解释:虚拟头节点(dummy head)用于处理链表头节点被删除的情况。
- 正确示例:dummy_head = ListNode(0); dummy_head.next = head
📸解题代码:
1.创建虚拟头节点:
时间复杂度:遍历链表的时间复杂度为 O(n),其中 n 是链表的长度。
在每次遍历中,删除节点的操作是 O(1) 的。
空间复杂度:空间复杂度为 O(1),因为只使用了常数级别的额外空间
来存储虚拟头节点和虚拟指针 curr。
# Definition for singly-linked list.
class ListNode(object):
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution(object):
def removeElements(self, head, val):
"""
:type head: ListNode
:type val: int
:rtype: ListNode
"""
# 初始化虚拟头节点,使其变成第一位
dummy_head = ListNode(0)
# 设置虚拟头节点的位置保证下一位是真正的头节点
dummy_head.next = head
#创建一个虚拟指针,指向虚拟头节点
curr = dummy_head
#开始遍历链表,确保head不能为空
while curr.next:
if curr.next.val == val:
curr.next = curr.next.next
else:
curr = curr.next
return dummy_head.next
2.直接处理头节点(不创建新的):
class ListNode(object):
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution(object):
def removeElements(self, head, val):
"""
:type head: ListNode
:type val: int
:rtype: ListNode
"""
# 处理头节点为特殊情况
while head and head.val == val:
head = head.next
# 开始遍历链表
curr = head
while curr and curr.next:
if curr.next.val == val:
curr.next = curr.next.next
else:
curr = curr.next
return head
707.设计链表 (medium)
建议: 这是一道考察 链表综合操作的题目,不算容易,可以练一练 使用虚拟头结点
题目链接/文章讲解/视频讲解:代码随想录
👩💻思路:
- 使用虚拟头节点:简化头节点的插入和删除操作。
- 检查索引有效性:确保索引在操作前是有效的(不越界)。
- 遍历到目标节点或其前驱节点:从虚拟头节点开始遍历,找到目标位置。
- 正确链接节点:在插入或删除时,确保正确调整节点的
next
指针。- 更新链表大小:每次成功添加或删除节点后,更新链表的大小属性。
这个思路适用于各种链表操作,如获取节点值、在头部或尾部添加节点、在指定位置添加或删除节点等。
❌易错点/难点总结:
1.链表索引越界
错误描述:尝试访问或操作一个无效索引位置(负数或超出链表长度)。
避免方法:在所有需要索引的操作前,检查索引的有效性。例如:
if index < 0 or index >= self.size:
return -12.操作空链表
错误描述:在链表为空时尝试进行删除或访问操作。
避免方法:检查链表是否为空,在操作前确保链表有足够的元素。3.忘记更新链表大小
错误描述:添加或删除节点时,忘记更新链表的大小属性 size。
避免方法:每次成功添加或删除节点时,记得更新链表的大小。例如:
self.size += 1 # 添加节点后
self.size -= 1 # 删除节点后4.错误的节点链接
错误描述:在添加或删除节点时,错误地链接节点,导致链表断裂或环路。
避免方法:按正确的顺序进行节点链接操作。例如:
new_node.next = current.next
current.next = new_node
删除节点时:
current.next = current.next.next5.处理头节点的特殊情况
错误描述:直接操作头节点时没有处理好头节点的特殊情况,导致链表结构异常。
避免方法:使用虚拟头节点(dummy head)简化对头节点的操作,避免单独处理头节点。例如:
self.dummy_head = ListNode()6.插入或删除时未正确找到前驱节点
错误描述:在插入或删除操作中,未正确找到目标节点的前驱节点,导致错误的链表操作。
避免方法:从虚拟头节点开始遍历,确保准确找到前驱节点。例如:
current = self.dummy_head
for _ in range(index):
current = current.next7.忘记处理边界条件
错误描述:在插入或删除操作时,未处理好边界条件,比如在空链表中插入,在尾部插入等。
避免方法:在操作前,考虑所有可能的边界情况,并在代码中处理这些情况。
📊视频讲解要点总结:
本期视频,卡老师总结了很重要的两个点,个人认为只要熟知并掌握这两点,任何类型的操作都是换汤不换药。
1.临时指针current该指向哪?
单链表中,无论是添加还是删除,无论在哪里删除还是添加,我们都要遵循的一个原则是:第n个节点(就是题目中给定的n,例如,在第n个节点前插入一个新的node)一定指向current.next。为什么不是current?如果current指向n,那么cur.next是n的后一位,这和我们要执行的命令不一致(“我们要在n前插入新的node”)。所以,为了确保我们有机会插入新的节点,要保证current指向第n个节点之前的那个节点,这样我们才能在他之前操作。
2.代码写对了,但是顺序放错了,全盘皆输。
上一段,我们已经确定了这个要新插入在n th节点前插入新的节点位置,即current ->new node -> current.next。那么,顺序该怎么排?正确答案如下:假设我们先定义current的下一个节点是new node的话,那么我们的真正在current后面的节点就被改成了new.node,在我们执行第二行时,new node.next = current.next就相悖了,因为new node不能等于他自己。这个逻辑比较绕,需要花时间琢磨一下。
- new_node.next = current.next #插队的人站在你的前面
- current.next = new_node. #现在,插队的人的前面是原本站在你的前面的人
- 第一步:new_node 的 next 指针指向 current 的下一个节点。即,新节点 new_node 指向原来在 current 后面的那个节点。此时,new_node 插入到了 current 和 current.next 之间,但链表还没有完全链接。
- 第二步:current 的 next 指针指向 new_node,即将 new_node 正式插入到 current 后面。这样,整个链表的链接顺序是 current -> new_node -> current.next。
3.用一个小例子总结:
我想了一个简单的例子分享给大家。假设你在排队过安检,突然有人说赶不上飞机了要插队,你同意了,按照正常的逻辑,你肯定要后退一步给他让出来空间插进来,那这个时候这个人就顺理成章的插到你的前面来了,他总不能插到你后面吧。退一万步讲,如果他真的想站到你后面的话,那他大概率不会来问你了,而是ask for你后面人的同意吧。
到这里,大家思考一下,这个新插入的人的前面是【原本站你前面的人】,新插入的人后面是你。原本是你前面的人,变成了你【前面的前面】。你们三个紧密相邻。链表和现实还是有一点差别,链表在执行这个插入动作的时候还会先把指针指到原本在你前面的人,因为只有这样,新的人才有可能加到你的前面。切记一点,只要插队的站到了你的前面,永远都没有你后面的人的事儿,所以千万不要让临时指针涉及到毫不相关的人。
📸解题代码:
单链表解法:
class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next class MyLinkedList: def __init__(self): self.dummy_head = ListNode() # 使用虚拟头节点简化操作 self.size = 0 def get(self, index: int) -> int: if index < 0 or index >= self.size: # 检查索引有效性 return -1 current = self.dummy_head.next for i in range(index): current = current.next return current.val def addAtHead(self, val: int) -> None: self.dummy_head.next = ListNode(val, self.dummy_head.next) # 正确链接新节点 self.size += 1 # 更新链表大小 def addAtTail(self, val: int) -> None: current = self.dummy_head while current.next: # 遍历找到尾节点 current = current.next current.next = ListNode(val) # 添加新节点到尾部 self.size += 1 # 更新链表大小 def addAtIndex(self, index: int, val: int) -> None: if index < 0 or index > self.size: # 检查索引有效性 return current = self.dummy_head for i in range(index): current = current.next current.next = ListNode(val, current.next) # 正确链接新节点 self.size += 1 # 更新链表大小 def deleteAtIndex(self, index: int) -> None: if index < 0 or index >= self.size: # 检查索引有效性 return current = self.dummy_head for i in range(index): current = current.next if current.next: # 检查是否有下一个节点 current.next = current.next.next # 删除节点 self.size -= 1 # 更新链表大小