题目内容
回文链表:题目链接
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2
输出: false
示例 2:
输入: 1->2->2->1
输出: true
进阶:
你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
引言–题目解读
我们之前肯定接触过类似的题目,leetcode中也有典型的题,比如:leetcode–9.回文数(回文数题目链接)、leetcode–125.验证回文串(验证回文串)等等。回文数这道题告诉我们什么是回文,回文的解题思想是什么,其实如果不考虑题目设置的附加因素(比如:验证回文串这道题,我们要排除特殊符号(’,’ 、’.’ 或是空格等等)的干扰;又如:本文的回文链表,我们需要考虑链表这种数据结构的特殊性),刨除这些题目的附加因素,回文的解决思路大体上可以认为是给定的用例,正着读和倒着读一模一样便是回文(但解决思路不只这一种,只在于通俗理解)。
一、链表的特殊性
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。1,链表的结构如下图所示(图片搜索于谷歌【图片链接】):
根据图片中链表的结构,接下来我们就用代码构建一个链表(单链表),代码如下:
#构建一个节点类,节点作为链表的结构单位,一个节点包括:节点内存的值、指向下一节点的后继
class ListNode:
def __init__(self , value):
self.val = value
self.next = None
#构建一个链表类,这里我们主要是利用列表初始化一个链表
class LinkList:
def __init(self):
self.head = None
def initList(self , data):
self.head = ListNode(data[0]) #初始化链表的头节点
#设置两个指针,r指向头节点,p用于连接由节点类构造出的节点
r = self.head
p = self.head
for i in data[1:]:
node = ListNode(i)
p.next = node
p = p.next
return r
二、回文链表解题思路
这里我们解决回文链表主要有两种思路,第一种思路:既然回文链表需要考虑链表数据结构的特殊性,那我们完全可以将链表先转换为列表,对比源列表与列表反转是否一致来判断链表是否为回文链表;第二种思路:我们可以利用寻找居中轴位置,将中轴到未节点的这一段链表进行反转,接下来再依次遍历节点内的值来判断该链表是否为回文链表。对于第一种思路来说实现起来比较简单,只需要在我们的链表类中添加一个链表转列表的函数进而写一个判断列表是否为回文列表的函数即可(代码在最后的完整代码部分LinkList类中的tolist函数部分);对于第二种思路而言我们这里涉及两个点:(1)中轴位置如何寻找?(2)如何实现链表的反转?,这两个点我们都可以在leetcode中各找一道题利用其解题思路解决这两个子问题。
中轴位置如何寻找
对于寻找中轴位置,我们的第一想法肯定是想去先获得链表的长度 “length”,然后设置一个计数变量去遍历链表找到(length//2)的位置便是我们要去寻找的中轴位置,但考虑到时间复杂度的因素,我们可以转变一下思路。这里我们先引入一道题:leetcode–19. 删除链表的倒数第N个结点,在这道题里我们的一开始的思想是不是也是先去获得链表的长度 “length”,然后第二遍去寻找 “legth-N+1” 这个位置的节点进行删除,回头看看我们都是在用一个指针去遍历链表,一个指针去把控什么时候指针指向的节点就是我们想要的,对于这一点我们解决问题就要考虑运用双指针(快慢指针),接下来将插入一组图片去说明一下如何利用快慢指针去寻找倒数第N个节点。
- 设置一个快指针 “fast” 和一个慢指针 “slow”,我们先让fast指针向后移动N次,如下图(本图中有小陷阱!):
注:此题中我们要删除倒数第二个节点(节点内部值为4)
理论上当fast指针比slow指针先走2步,接下来fast与slow指针同时走,当fast指针走到结尾时,slow指针指向的应该是倒数第2个节点,但按照上图所示,当fast走到结尾时,slow指向的确实 “3”。这就是一个小陷阱,就是说fast在走步的时候直接跳过了 “1”。因为单链表无法回头,我们不能通过索引简单地对指针回退一个位置,所以我们在链表头节点之前添加一个无用的节点做过渡,如下图所示:
由上图,我们在原链表头节点之前增加了一个节点用于辅助,一开始fast指针(蓝,由此位向前)前进2次(红,前进两个节点位置),这里要说明一下,我们设置一个节点在链表前只是为了将 “1” 这个节点也被指针访问,这样一来以slow与fast此时的相对位置(先让fast前进2个节点并不是让fast比slow多访问两个节点,这个很重要!)向前访问节点,当fast访问到尾节点时,slow整好指向链表的倒数第二个节点。这一部分的代码实现如下:
def removeNthFromEnd(head,N):
useful = ListNode(0)
useful.next = head
fast = head
slow = useful
n = 0
while fast != None:
if n < N:
fast = fast.next
print(fast.val)
n += 1
continue
slow = slow.next
fast = fast.next
slow.next = slow.next.next
#注意:因为在链表头节点前添加了一个节点,这里必须是useful。next才是原链表的头节点
return useful.next
基于上面寻找链表的倒数第N个节点,我们也可以利用快慢指针配合的方式找到链表的中枢位置。
- 设置一个快指针和一个慢指针,快指针每次前进两次,慢指针每次前进一次,当快指针访问到链表尾部(有可能是最后一个节点,也有可能是倒数第二个节点)为止,慢指针正好指向中轴位置。如下图所示:
如上图所示,我分别花了两个链表,一个是节点个数为奇数的链表,一个是节点个数为偶数的链表,如果我们对slow指针之后的链表节点进行反转,那么前后两部分为head->slow 与 slow.next->fast 或 fast.next(尾接点),如果遍历这两部分结果一摸一样,就证明该链表为回文链表。下面代码实现了寻找链表中轴位置:
def midnode(head):
fast = slow = head
while fast.next and fast.next.next:
fast = fast.next.next
slow = slow.next
实现链表反转
在我们找到链表中轴的位置后,接下来我们需要进行的是实现链表中轴位置后半部分的反转(leetcode–206.链表反转),一说到链表的反转,根据链表的特性,我们直接将各节点的指针指向翻过来便可以实现指针的反转(原链表头节点的后继指向空,原链表尾节点的后继指向原来它的前一个节点),这一步我们用迭代的方法,进行链表的反转(之后会再写篇反转链表的单独博文,里面会详细讲述一下递归实现链表反转),这里就画个草图简单说明一下:
图中的只有两个节点,做了一个最基本的反转操作,如果节点数量更多,我们要去依靠post指针进行迭代的向后移动,curr指针与pre指针负责对当前节点和前一个节点的指针指向进行修改,一旦修改完成,pre指针指向curr指向的节点位置,curr进而指向post指针指向的节点位置,post指针也向后移动,若post指向的节点已经是尾节点那么结束迭代,链表反转完成,这一部分代码如下:
def reverselist(head):
cur = head
pre = None
post = cur.next
while post:
cur.next = pre
pre = cur
cur = post
post = post.next
cur.next = pre
return cur
三、题目完整代码
class ListNode:
def __init__(self , value):
self.val = value
self.next = None
class LinkList:
def __init(self):
self.head = None
def initList(self , data):
self.head = ListNode(data[0])
r = self.head
p = self.head
for i in data[1:]:
node = ListNode(i)
p.next = node
p = p.next
return r
def tolist(self , head):
if head == None: return
p = head
print_res = []
while p != None:
print_res.append(p.val)
p = p.next
return print_res
def isPalindrome(head):
if head == None or head.next == None:
return True
if head.next.next == None:
return head.val == head.next.val
fast = slow = q = head
while fast.next and fast.next.next:
fast = fast.next.next
slow = slow.next
def reverselist(head):
cur = head
pre = None
post = cur.next
while post:
cur.next = pre
pre = cur
cur = post
post = post.next
cur.next = pre
return cur
#注意这里,我们前面说过反转部分是从中轴位置的后继节点开始的
p = reverselist(slow.next)
while p.next:
if p.val != q.val:
return False
p = p.next
q = q.next
return p.val == q.val
if __name__ == "__main__":
a = [1,2,3]
b = [1,2,2,1]
obj = LinkList()
obj = LinkList()
A = obj.initList(a)
B = obj.initList(b)
print(isPalindrome(A))
print(isPalindrome(B))
总结
个人认为回文链表这道题包含了挺多东西,对于指针的运用,比如:双指针、三指针,双指针里也有左右指针与快慢指针;对于链表的基础操作,它包括:遍历链表、利用当前节点对后继节点的指向对链表进行修改等等…代码小菜还在努力进步,文中如有不妥,请各位大神批评指正 0.0