在总结链表题前,先记录一个误区:
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
tmp = ListNode(0)
res = temp
问题是:tmp和res是代表什么?两个相同链表的头结点?指向两个相同链表的头结点?
解答:tmp和res指向的是存储 哑节点0 的位置,此时tmp和res存储的是同一内存地址,可以理解成tmp和res是两个指针。当对res进行操作时,会在res中存储内容(节点)即为这个内存放内容。而res不断移动,tmp还是指向原来的 哑节点的位置。
但是我们之前用python列表时,情况如下
a = [1, 2]
b = a
b += [3]
print(a) #a = [1, 2]
print(b) #b = [1, 2, 3]
为什么改变b却不能改变a呢?那是因为列表是可变对象,对于可变对象,地址是不可以共享的,也就是a,b指向的地址是不一样的。
总结:在python中,不可变对象是共享的(如节点对象),创建可变对象永远是分配新地址(如列表)。
常见的链表题错误
在quick移两个指针的时候,要这样写,要不quick为None时,quick.next会报错。多使用哑节点。
while quick and quick.next:
quick = quick.next.next
链表题中,能用一个指针用一个指针,能用两个不用三个,而且都要建立哑节点,并对需要的指针进行初始化。
如删除链表重复的节点的题目。
在链表题中,常用的函数有:
def to_kth_node(head, k):#head为第一个结点,k为第k个结点
k -= 1
p = head
while k > 0 and p:
p = p.next
k -= 1
return p
接下来进入链表的题目,做题顺序:2、19、21、23、24、25、61、83、82、86、92
【leetcode2两数相加】
题目:给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
思路:这道题的关键在于进位问题,即当两个数相加大于10,需要向高位进一位。因此,设置进位变量为flag。
但是这道题要想完全做对的关键在于,l1和l2不一样长,而且要判断while循环完后是否有进位的情况。自己在写完常规思路以后一定要想一下是否满足特殊情况,把特例测试一下。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
if l1 is None:
return l2
if l2 is None:
return l1
# tmp是暂存(temporal)
tmp = ListNode(0) # tmp指向哑节点
# res是重置(reset)
res = tmp # res指向哑节点(此时和tmp指向同一位置)
# flag 进位标志
flag = 0 # 初始化 (只可能是0或1)
while l1 or l2: # l1或l2不为空就持续执行
tmp_sum = 0 # 链表节点值的和
if l1: # 如果l1不为空,把l1的某个节点值的和赋给tmp_sum
tmp_sum = l1.val # 把l1的某个节点的值赋给tmp_sum
l1 = l1.next
if l2: # 如果l2不为空,把l2中和l1对应的节点的值加到tmp_sum
tmp_sum += l2.val
l2 = l2.next # 指向下一个节点,为下一次的加和做准备
tmp_res = ((tmp_sum + flag) % 10) # 个位数字
flag = ((tmp_sum + flag) // 10) # 进位的数
res.next = ListNode(tmp_res) #将个位数字变为节点
res = res.next # res后移
#!!!设置很巧妙,要注意
if flag: # 如果flag不为0,就是对应位置相加后有进位
res.next = ListNode(1) # res的下一节点设为1
res = tmp.next # 找到头结点,并赋给res
del tmp # 删除tmp变量
return res # 返回res链表(链表返回只需要返回头节点)
复杂度分析:
时间复杂度:O(max(m,n)),其中m表示l1链表的长度,n表示l2链表的长度
空间复杂度:O(max(m,n)),新链表的长度最多为max(m,n)+1
总结:1、请注意,我们使用哑结点来简化代码。如果没有哑结点,则必须编写额外的条件语句来找到表头,而使用哑节点,可以很快找到表头,因为此时res已经在链表尾部了。2、在写代码的过程中,考虑一下特殊情况,如下表
参考:leetcode题解https://leetcode-cn.com/problems/add-two-numbers/solution/liang-shu-xiang-jia-by-leetcode/
哑节点除了能快速找到新链表表头以外,还能简化某些极端情况,例如列表中只含有一个结点,或需要删除列表的头部。
【Leetcode19删除列表的倒数第N个节点】
题目:给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
说明:
给定的 n 保证是有效的。
进阶:
你能尝试使用一趟扫描实现吗?
给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.
思路:本质是删除题,属于链表的基本操作。当时想到的方法是用两次遍历,第一次遍历先知道链表一共有多少个节点,第二次遍历找到需要删除节点的前一节点。该题我写了三个版本的代码,累死个人。
代码1:没有用到哑节点,把删除分成三种情况,头节点,中间节点,尾节点,两次遍历
代码2:用到哑节点,把删除分成两种情况,尾节点和其它节点,两次遍历
代码3:用到哑节点,两个指针,一次遍历:一次遍历的原因是让两个指针之间间隔n个结点,这样让一个指针到尾节点结束,另一个指针即为要删除结点的上一结点。特别注意的是两个指针间隔n个结点,之前一直弄错成n-1了!!!
贴出代码:
代码1:没有用到哑节点,把删除分成三种情况,头节点,中间节点,尾节点,两次遍历
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
p = head
tmp = p
listnode_n = 0
while p:
listnode_n += 1
p = p.next
p = tmp
if n == 1: #删除尾节点的情况
if listnode_n == 1:
return None
else:
for i in range(listnode_n-2):
p = p.next