目录
链表图文学 - LeetBook - 力扣(LeetCode)全球极客挚爱的技术成长平台https://leetcode.cn/leetbook/read/linked-list/jy291/
本文为个人记录学习及思考过程所用,如文中有任何错误,望各位大佬海涵,并指出错误。
注:连接为LeetCode内部《链表图文学》一书中的《设计链表》题目,题库中也包含该题。
笔者的代码属菜鸟级别,提交后运行速度不佳,内存占用较少,说明优化空间较大,但代码简单易懂,适合新手。
什么是链表?
链表的输出和列表(数组)相似,打个比方:
数组:
将内存比作鸡蛋盒,储存内容为鸡蛋,那么数组的储存方式为:顺序摆放,从0开始,依次标号(索引)。这样索引非常容易,我只需要找出对应标号(索引)即可取出“鸡蛋”。但如果我想在已经排好序的“鸡蛋”中删除或添加一个或多个“鸡蛋”,就会非常困难。(最末尾的添加非常简单)因为我需要挪动一部分“鸡蛋”,给新来的腾位置。所以数组添加元素较难,但索引容易。
链表:
将链表(包括内存的值和指针)比作别针,链表则为一个一个单独的别针散乱分布,(拿上个例子来说,就是鸡蛋盒内散乱分布着的鸡蛋)别针可以随意解开、连接、添加,当我将多个别针连在一起的时候,从头拎起,捏住的“别针”,就是链表头,“指针”则指向下一个链表头——可以理解为别针与别针相连处。因此,使用链表添加、删除元素很容易,但是索引、遍历长度较为繁琐。
新手注意:
链表并不是python内部自带的内容,因此我们无法像创建数组一样,随时随地创建一个链表。一般都需要创建一个“链表类”。
接触链表之初,我一直无法理解链表。因为有的教程创建了一个类,即为“链表类”,而有的教程则创建了两个类,“链表类”,“表头类”。一度让我十分混乱——当我将链表一个一个连接在一起的时候,我自然而然不就知道谁是链表头了吗?一度让我怀疑大佬们所言:“链表超级简单的哩。”
其实不然,这时候的我思维很乱。我理解了链表的作用机制以及链表的利弊。但我依然无法理解链表的具体操作是什么,如何遍历链表。
链表类,其实是“链表节点类”:即,我使用一个类,在类中应用一个init函数,函数内部参数有三个,分别是:self, val, next。相当于创建了一个“别针”,而不是连成一串的别针。
解题思路:
题目要求设计一个链表。给出了一个大类,内部封装了五个函数。
在做题目的时候,切勿按照函数顺序来做,应该先读题目对于每个函数的要求,并且清楚题目中所给类的用意——连接链表。
虽然题目没有给出init函数,但是设计“链表节点类”的函数不应该和“连接链表类”放在一起,所以第一步——创立“节点类”。
创立链表节点类:
class ListNode:
# self.val = 0
# self.next = listNode()
def __init__(self, value=0, node=None):
self.val = value
self.next = node
这样,我就创建了一个链表节点(别针),其值为我们传入的参数value,指针指向的None。
注意:在调用第二个类中函数的时候,我们每传入一个参数(下文中传入的参数都为int类型,而非链表节点),都要调用节点类,将int转为链表节点。
(插一个题外话,作为纯纯的小白,我在做这个题目的时候,一直都按照顺序来做,所以,在做到get函数的时候,(没错,就是第一个函数),我就做不下去了。因为问题很多:1.我需要从表头遍历,可是表头在那里?2. 我还要知道链表长度,长度怎么算? 所以,可以从“加尾”函数开始)
先写加尾函数:
在加尾的时候需要考虑:
1. 链表是否为空?如果为空,加尾就相当于是链表头。
因此,我在第二个类中写了一个init函数:
class MyLinkedList:
def __init__(self):
self.root = None
self.size = 0
(这里我维护了一个self.size,表示的是整个链表的长度,方便调用get函数时使用,注意,这里初始值为0,在进行:加、添、删等操作的时候需要更新self.size的值)
该类我只调用一次,所以只生成一个self.root,是空的。
加尾的时候,我只需要判断self.root是否为None,如果是,则直接将尾节点作为self.root,如果不为空,则设置一个单指针cur,从self.root开始,依次向后遍历,直到cur.next == None时,将尾节点加上即可。
def addAtTail(self, val: int) -> None:
newNode = ListNode(val, None)
if self.root == None:
self.root = newNode
else:
cur = self.root
while cur.next != None:
cur = cur.next
cur.next = newNode
self.size += 1
再写加头函数:
加头函数相对容易,我们已知最初类中设置了一个隐藏的链表头:self.root,(注意,不是虚拟链表头)
我只需要先调用第一个类,将val转为链表节点node,再将指针指向self.root,并把node设为self.root。因为无论怎么变,我都需要self.root来标记链表的头结点,方便添加、删除。
依然不要忘记self.size的维护。
def addAtHead(self, val: int) -> None:
node = ListNode(val, None)
node.next = self.root
self.root = node
self.size += 1
其次get函数:
在get的时候注意题目的要求:
- get(index):获取链表中第
index
个节点的值。如果索引无效,则返回-1。
什么叫索引无效?
index < 0; index >= self.size
注意,这里的index从0开始,和列表的索引值一样。
在判断完index在范围内,利用cur来遍历,输出index == 0时,对应的链表值。
def get(self, index: int) -> int:
# cur = self.root
# if self.root == None:
# print("root == None")
# while cur != None:
# print(cur.val, end=' ')
# cur = cur.next
# print("size=", self.size)
# print(" ")
if index < 0 or index >= self.size:
# print("lelelelel")
return -1
cur = self.root
while cur != None:
if index == 0:
return cur.val
cur = cur.next
index -= 1
注意:if语句一定要在index运算之前进行,避免出错。
另外,我在该函数中设置了检测get函数是否正确的代码段。
在index的位置添加节点的函数:
首先还是读题:
在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
头尾只需要调用之前的函数即可(但要注意,self.size已经在之前的函数中加过了,所以不需要再加一次)
如果符合条件,遍历到index == 1时,直接添加节点,但要注意,先将要添加节点的指针指向cur后面的节点,再将遍历的cur的指针指向添加节点,否则后面的指针会丢。
def addAtIndex(self, index: int, val: int) -> None:
if index <= 0:
self.addAtHead(val)
elif index == self.size:
self.addAtTail(val)
elif index > self.size:
return
else:
node = ListNode(val, None)
cur = self.root
while(index):
if index == 1:
break
cur = cur.next
index -= 1
node.next = cur.next
cur.next = node
self.size += 1
删除节点函数:
依然是读题:
- deleteAtIndex(index):如果索引
index
有效,则删除链表中的第index
个节点。
步骤为:
1. 判断index是否有效;
2. 遍历,在index == 1时停下,执行删除操作。
def deleteAtIndex(self, index: int) -> None:
if index >= self.size or index < 0:
return
elif index == 0:
self.root = self.root.next
self.size -= 1
return
else:
cur = self.root
while(index):
if index == 1:
cur.next = cur.next.next
self.size -= 1
return
index -= 1
cur = cur.next
别忘了维护self.size!
最后:给链表debug是真的慢啊……