链表类型题
本博文仅用作个人学习的记录,包含个人学习过程的一些思考,想到啥写啥,因此有些东西阐述的很罗嗦,逻辑可能也不清晰,看不懂的且当作是作者的呓语,自行跳过即可。
单序链表
首先我们得清楚链表是什么,也不用把它想象的多高大上,多专业化,它其实就是一个结构,一个包含有两个部分的结构:
val | next |
---|
它包含当前的值和下一个链表结构的索引,从定义可以看出:
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
所以也就是说有一个链表 pHead
, 或许它很长,但是你只知道 pHead
此时的值以及它指向下一个的索引,对它进行赋值也只会影响当前这个节点的值以及它指向的下一个节点的索引,而不会改变原链表的其它节点,最多是当前节点 pHead
丢失了指向原链表的索引,不再能够由 pHead.next
将原链表遍历出来。
讲题吧,从题目入手好理解一些,敲敲小黑板!!
逆序打印链表
代表题型:剑指offer 第3题
-
题目描述
输入一个链表,按链表从尾到头的顺序返回一个ArrayList。 -
代码
class Solution:
# 返回从尾部到头部的列表值序列,例如[1,2,3]
def printListFromTailToHead(self, listNode):
# 新建一个列表存放遍历得到的链表值
l = []
head = listNode
while head:
# 在列表首部,即0位置处插入链表值
l.insert(0, head.val)
# 链表指向下一个值
head = head.next
return l
- 解析
这里是逆序打印链表,可以分成三步:
1.将链表遍历一遍,取出每个节点的值。
2.将值存放到列表中
3.将列表逆序
这里的代码是将2,3两步结合了,直接将值逆序存放到列表中。
倒数第k个结点
代表题型:剑指offer 第14题
-
题目描述
输入一个链表,输出该链表中倒数第k个结点。 -
代码
class Solution:
def FindKthToTail(self, head, k):
# 判断输入是否符合规范
if head==None or k<=0:
return None
# 选用两个链表结构
p1 = p2 = head
while p1.next!=None:
# p2要比p1晚遍历k-1个值
if k>1:
k-=1
else:
p2 = p2.next
p1 = p1.next
# 判断k值是否符合规范
if k>1:
return None
# 如果符合规范,就返回p2
else:
return p2
- 解析
本题难点在于链表无法预知长度,因此也无法从后往前取值。
方法一: 我们最容易想到的是,将链表整个完成遍历,然后将所有的值都顺序(或者逆序)存入列表中,然后取出倒数(顺数)第k个值。因为列表是可以知道长度的,并且可以按索引取值,这个也刚好在逆序打印的基础上添加代码即可。但是需要注意,它不是打印倒数第k个结点的值,而是需要返回一个链表:
class Solution:
def FindKthToTail(self, head, k):
if head==None or k<=0:
return None
l = []
while head:
# 顺序保存
l.append(head.val)
head = head.next
if k<=len(l):
# 定义两个相同的链表结点
p1 = p2 = ListNode(0)
# 取得倒数第k个结点的值,并将之后的所有值生成一个链表返回
for item in l[(len(l)-k):]:
p1.next = ListNode(item)
p1 = p1.next
# 返回链表头,p1已经丢失了链表头,p2依旧指向的是原链表
return p2.next
else:
return None
可以看出这个方法还是很麻烦的,需要链表->列表->链表,借助了多个中间量。
这里用到了python列表的可变性,下文单列一节来讲吧,私以为还是很重要的一个点。
方法二: 也就是最开始的那个代码,主要思路是利用快慢指针,同时进行两次遍历,快指针刚好比慢指针快 k-1
个结点,那么当快指针遍历完成时,慢指针刚好遍历到链表的倒数第 k
个结点。
链表反转
代表题型:剑指offer 第15题
这一题刚开始的时候我总是和逆序打印链表傻傻分不清楚。。。
链表反转和逆序打印最大的区别在于,链表反转的返回值是一个 linkedlist
,而逆序打印链表返回的是一个 list
。
-
题目描述
输入一个链表,反转链表后,输出新链表的表头。 -
代码
class Solution:
# 返回ListNode
def ReverseList(self, pHead):
p1 = pHead
p2 = None
while p1:
tem = p1.next
p1.next = p2
p2 = p1
p1 = tem
return p2
- 解析
那么同样可以有多种方法。
方法一: 在逆序打印的基础上,将list
转换为linkedlist
。
class Solution:
# 返回ListNode
def ReverseList(self, pHead):
l = []
while pHead:
# 逆序保存
l.insert(0, pHead.val)
pHead = pHead.next
# 定义两个相同的链表结点
p1 = p2 = ListNode(0)
# 取得倒数第k个结点的值,并将之后的所有值生成一个链表返回
for item in l:
p1.next = ListNode(item)
p1 = p1.next
# 返回链表头,p1已经丢失了链表头,p2依旧指向的是原链表
return p2.next
同样是很麻烦,需要链表->列表->链表,而且代码也不简洁。
方法二: 逐个遍历链表结点,调转结点的指向,代码如开始所示。
首先新建一个 None
值。
对于输入链表的第一个结点,把它指向的下一个结点由第二个结点转换为 None
。
p1.next = p2
但是这样你就丢失了第二个结点和之后结点的地址,因此在这个操作之前需要先将第二个节点赋值给另外一个临时变量 tem
。
tem = p1.next
那么此时你手里的三个变量:
p1
包含原链表的第一个结点,并且下一个节点指向 None
。
p2
依旧是一个 None
值。
tem
包含原链表的第二个结点,并且逐个指向之后的所有原链表结点。
通过这一次操作可以看出,我们已经将一个结点的指向调转了,目前存放在 p1
这个参数里,原链表之后的结点放在了 tem
参数里。而 p1
代表的是原链表,p2
才代表反转之后的链表,因此我们需要把参数再调整回来。
p2 = p1
p1 = tem
如此遍历完整个原链表,p2
代表的就是反转之后的链表表头。
排序链表合并
代表题型:剑指offer 第16题
这题的几个解法受益很多,让我辈渣渣唯有一句卧槽,心中生出只管磕头之念!
-
题目描述
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。(这里的不减应该就是与原来单调递增一样,单调性不改变,可以有相等的情况) -
代码
循环
def Merge(self, l1, l2):
# 同样是定义两个链表,一个存表头一个用来修改链表数据,防止改完之后找不到表头
dummy = cur = ListNode(0)
# 这个and用的好呀
while l1 and l2:
# 链表值小的就存入到cur中,并且往后走一节。
if l1.val < l2.val:
cur.next = l1
l1 = l1.next
else:
cur.next = l2
l2 = l2.next
cur = cur.next
# 这个or用的是真真好,和开头那个and一样,避免了写多行判断赋值语句
cur.next = l1 or l2
return dummy.next
递归
def Merge(self, l1, l2):
# 方法开头进行判断是否有空链表,并返回非空链表
if not l1 or not l2:
return l1 or l2
# 对于值小的结点,把它的下一个结点和值大的结点再次执行本方法,并且赋值给值小结点指向的下一个结点
if l1.val < l2.val:
l1.next = self.Merge(l1.next, l2)
# 返回值小结点的表头
return l1
else:
l2.next = self.Merge(l1, l2.next)
return l2
递归方法真是奇妙呀,这感觉妙不可言。
递归优化
def Merge(self, pHead1, pHead2):
if pHead1 and pHead2:
# p1更大就交换结点
if pHead1.val > pHead2.val:
pHead1, pHead2 = pHead2, pHead1
pHead1.next = self.Merge(pHead1.next, pHead2)
return pHead1 or pHead2
这里需要注意的一点是:
return pHead1 or pHead2
当有一个是空值的时候,返回的是另外一个非空值;当两个都是非空值时,返回的是第一个值。
这与 or
的判断机制有关系,当判断第一个非空后,就不会判断第二个值了。
因此,这里只能把 pHead1
写在 pHead2
前面,因为小值按顺序排放在 pHead1
这个结点里,所以最后要返回的必须是 pHead1
。
- 总结
链表一个节点一个节点顺序操作的时候,需要一个额外的参数记录表头。
正向的链表操作感觉更适合使用递归,递归便是一层层执行,然后再一层层返回数据,这样就不需要多余参数记录表头。
Python 的字符串不变性
不可变性值的是:变量在新建之后就不可以被修改。
在Python中,数字(number)、字符串(string)、元组(tuple)是不可变的,集合(set)、列表(list)和字典(dictionary)可变。
但是我们还是经常能够看到,对上面这些类型的参数进行修改呀,而且也并没有报错,这是为什么呢?
对不可变类型的参数进行赋值时,并不是直接修改这个参数内存区域指向的值,而是新开辟了另外一个内存区域,并将当前参数指向了这个新的内存区域。
看个对比:
a = b = 1
b = 2
print(a) # 此时a的值还是1,并没有被改变,而是b指向了2所在的内存区域
a = b = [0]
b[0] = 1
print(a) # 此时a的值已经变为了[1],因为列表可以被修改
在Python中,标准库并没有实现链表,链表是由列表来自定义实现的一种数据结构,因此它是具有可变性的,所以可以采用两个参数(一个记录表头,一个修改链表)对链表实行操作。
Java 的 String
也是不可变的。