链表是一种单向搜索的数据结构,形式上像链子一样由许多节点串联形成,但是其搜索方式并不灵活,只能从头节点出发,依次前进,直到达到目标节点。
如果有一个链表,我们想要得到该链表中的第四个节点,python代码应为:
point1 = head # 指针初始化指向头指针
for i in range(1,4): # 执行3次循环
point1 = point1.next # 遍历链表
经过以上代码,point1指向了第四个节点。控制次数地遍历链表就可以实现具体节点的定位。
这里的题目较为经典,本质是定点删除链表中的节点。
在了解链表的节点定位法后,我们以此作为出发点去解决这个问题。
需要注意到,题目要求删除倒数第n个节点,在不知道链表长度的情况下,这个倒数第n是没法直接用的,因此需要先整体遍历链表一遍,计算其长度。
以下代码实现了计算链表长度的功能。
point1 = head
length = 1 # 因为遍历链表是从第一个节点往后开始的,所以需要先算上头结点
while point1.next != None:
point1 = point1.next # 遍历整个链表
length += 1 #计算出链表的长度
需要注意的是由于遍历是从头结点的下一个开始的,因此头结点在遍历前就应当计算在内,所以初始值是1不是0。
按照这个思路可以写出以下代码:
class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
if head.next == None:
return None
point1 = head
point2 = head
length = 1 # 因为遍历链表是从第一个节点往后开始的,所以需要先算上头结点
while point1.next != None:
point1 = point1.next # 遍历整个链表
length += 1 #计算出链表的长度
for i in range(1,length-n):
point2 = point2.next # 遍历链表,移动到需要删除的节点的上一个节点
if point2.next.next != None:
point2.next = point2.next.next # 令需要删除的节点的上一个节点指向需要删除的节点的下一个节点
else:
point2.next = None
return head #返回链表
经测试,以上代码存在一个bug:无法删除掉头结点。
因为在遍历链表时,第一次操作就将头结点给跳过去,这样我们只能删除头结点以外的节点。
按照这个改进思路,也应当将头结点纳入遍历范围内。
因此我们可以在头指针前面插入一个节点,称其为“哑节点”:
dummy = ListNode(0, head)
该节点指向了头结点,这样每次遍历就会从头结点开始了。
此外,该题目规定,节点内的值在0到100之间,该哑结点并没有帮助检索外的其他作用,因此赋值个0(当然赋值其他也可以)。
修改后的代码如下:
class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
if head.next == None:
return None
length = 1
point1 = head
while point1.next != None: # 先把长度算出来
point1 = point1.next
length += 1
dummy = ListNode(0, head) # 哑节点,指向头结点
cur = dummy
for i in range(1, length-n+1): # 可以直接遍历了
cur = cur.next
cur.next = cur.next.next # 删除需要删除的节点
return dummy.next # dummy.next指向的是新链表的头结点
考虑到该题涉及“取倒数第几的xx”,还可以考虑用栈来写:
class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
if head.next == None:
return None
stack = list() # 初始化栈列表
dummy = ListNode(0, head) # 哑节点,指向头结点
cur = dummy
while(cur != None):
stack.append(cur) # 将链表节点正序加入列表中,压栈处理
cur = cur.next
for i in range(n):
stack.pop() # 出栈操作,将包含需要删除的节点在内的倒数节点抛出
pre = stack[-1] # 栈里的最后一个元素就是需要删除的节点的前一个节点
pre.next = pre.next.next
return dummy.next
在python中使用栈较为简单:1.用一个空列表当栈使用。2.把数据都压进去。3.根据所需元素的倒数次序进行出栈。
此外,还可以使用快慢指针法来处理这个问题。
class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
dummy = ListNode(0, head) # 需要返回的链表指针一定要初始指向哑指针
point1 = head # 快指针
point2 = dummy # 慢指针
for i in range(n): # 让快指针先走,提前n步
point1 = point1.next
while point1 != None: # 然后前后指针再一起走,快指针到末尾,停住,慢指针也同时停下
point1 = point1.next
point2 = point2.next
point2.next = point2.next.next
return dummy.next
来个使用已有链表构建新链表的。
由题意可知,需要构建一个新链表返回,因此一共涉及到三个链表。从宏观的流程角度上讲,该程序实现三个链表的共同前进,在程序中需要处理的是三个链表之间前进的关系。
从示意图中可知,每一步都是从链表1或者链表2中找一个节点给链表3(新链表)装上,每次装上个新节点,也就意味着链表3需要前进一次,给予节点的链表前进一次,也就是说一次操作需要3个链表中的2个同时前进。
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
dummy = ListNode(0,None) #新建一个链表和哑指针
point1 = dummy
while l1 != None and l2 != None: # 如果其中一个链表走到尽头就结束
if l1.val <= l2.val: # 如果需要从链表1中取节点
point1.next = l1 # 将节点装给新链表
point1 = point1.next # 新链表前进
l1 = l1.next # 链表1前进
else: # 关于链表2的操作同理
point1.next = l2
point1 = point1.next
l2 = l2.next
if l1 != None: #剩余的某个链表直接连在新链表后面即可
point1.next = l1
if l2 != None:
point1.next = l2
return dummy.next
涉及多链表的问题要注意每次操作都是哪些链表在前进。
来个单链表的。
回文xx在算法题里还是挺常见的,一开始考虑到有这个“倒数”情况(因为回文是倒着和正着比的),所以我拿栈写了一个函数。
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
dummy = ListNode(0,head) # 先上哑指针
point1 = dummy
point2 = dummy
length = 0
while head != None: # 测量链表的长度
head = head.next
length += 1
if length % 2 != 0: #将链表的后一半放进栈中,由于链表的长度奇数偶数有差异,所以分了情况
for i in range(length//2 + 1):
point2 = point2.next
else:
for i in range(length//2):
point2 = point2.next
stack = list() # 建立栈,将链表的后半部分压入栈
for i in range(length//2):
point2 = point2.next
stack.append(point2.val)
for i in range(length//2):
point1 = point1.next
temp = stack.pop() # 每次从链表前面与链表末尾取相同位次的内容进行比较
if point1.val != temp:
return False
return True
拿栈写了之后感觉不是很对味(对胃 ),翻出官方答案,发现官方的答案确实有很简单的办法。
说本质的问题,链表的出现就是为了一定程度上替代数组,数组可是个方便操作的数据结构,美中不足就是只能连续存储,对内存空间的利用率有点堪忧。今天既然搞算法,将链表转成数组再操作,岂不是美汁汁。
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
vals = []
cur = head
while current_node is not None: # 将链表的节点依次加入空列表中
vals.append(current_node.val)
cur = cur.next
return vals == vals[::-1] #列表显然可以通过简单方法翻转,翻了之后直接比
直呼内行,可见数组算法是相当基础的,数组得学好。
翻转列表也是基本功:
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
pre = None
cur = head
while (cur != None):
temp = cur.next # 当前节点的下一个,存起来
cur.next = pre #将当前节点的下一个设成前一个
pre = cur # 前一个向后移
cur = temp # 当前的向后移
return pre #注意返回的不是cur,因为cur移到链表的最后,指向了None。链表最后一个元素是None。