本文参考labuladong算法笔记[如何判断回文链表 | labuladong 的算法笔记]
1、前言
首先,寻找回文串是从中间向两端扩展,判断回文串是从两端向中间收缩。对于单链表,无法直接倒序遍历,可以造一条新的反转链表,可以利用链表的后序遍历,也可以用栈结构倒序处理单链表。
具体到回文链表的判断问题,由于回文的特殊性,可以不完全反转链表,而是仅仅反转部分链表,将空间复杂度降到 O(1)。
# 在 s 中寻找以 s[left] 和 s[right] 为中心的最长回文串
def palindrome(s: str, left: int, right: int) -> str:
# 防止索引越界
while left >= 0 and right < len(s) and s[left] == s[right]:
# 双指针,向两边展开
left -= 1
right += 1
# 返回以 s[left] 和 s[right] 为中心的最长回文串
return s[(left + 1):right]
因为回文串长度可能为奇数也可能是偶数,长度为奇数时只存在一个中心点,而长度为偶数时存在两个中心点,所以上面这个函数需要传入 l
和 r
。
而判断一个字符串是不是回文串就简单很多,不需要考虑奇偶情况,只需要双指针技巧,从两端向中间逼近即可:
def isPalindrome(s: str) -> bool:
# 一左一右两个指针相向而行
left, right = 0, len(s) - 1
while left < right:
if s[left] != s[right]:
return False
left += 1
right -= 1
return True
因为回文串是对称的,所以正着读和倒着读应该是一样的,这一特点是解决回文串问题的关键。
二、判断回文单链表
看下力扣第 234 题「回文链表」:
这道题的关键在于,单链表无法倒着遍历,无法使用双指针技巧。
那么最简单的办法就是,把原始链表反转存入一条新的链表,然后比较这两条链表是否相同。关于如何反转链表,可以参见前文 递归翻转链表的一部分。
其实,借助二叉树后序遍历的思路,不需要显式反转原始链表也可以倒序遍历链表,下面来具体聊聊。
对于二叉树的几种遍历方式,我们再熟悉不过了:
def traverse(root: TreeNode) -> None:
# 前序遍历代码
traverse(root.left)
# 中序遍历代码
traverse(root.right)
# 后序遍历代码
在 学习数据结构的框架思维 中说过,链表兼具递归结构,树结构不过是链表的衍生。那么,链表其实也可以有前序遍历和后序遍历:
def traverse(head: ListNode):
# 前序遍历代码
traverse(head.next)
# 后序遍历代码
【算法思路】
这个框架有什么指导意义呢?如果我想正序打印链表中的 val
值,可以在前序遍历位置写代码;反之,如果想倒序遍历链表,就可以在后序遍历位置操作:
# 倒序打印单链表中的元素值
def traverse(head: ListNode):
if head is None:
return
traverse(head.next)
# 后序遍历代码
print(head.val)
说到这了,其实可以稍作修改,模仿双指针实现回文判断的功能:
【python】
class Solution:
# 从左向右移动的指针
left = None
# 从右向左移动的指针
right = None
# 记录链表是否为回文
res = True
def isPalindrome(self, head: ListNode) -> bool:
self.left = head
self.traverse(head)
return self.res
def traverse(self, right: ListNode):
if right is None:
return
# 利用递归,走到链表尾部
self.traverse(right.next)
# 后序遍历位置,此时的 right 指针指向链表右侧尾部
# 所以可以和 left 指针比较,判断是否是回文链表
if self.left.val != right.val:
self.res = False
self.left = self.left.next
实际上就是把链表节点放入一个栈,然后再拿出来,这时候元素顺序就是反的,只不过我们利用的是递归函数的堆栈而已。
三、优化空间复杂度
【算法思路】
1、先通过 链表双指针技巧 中的快慢指针来找到链表的中点:
2、如果fast
指针没有指向null
,说明链表长度为奇数,slow
还要再前进一步:
3、从slow
开始反转后面的链表,现在就可以开始比较回文串了:
left, right = head, reverse(slow)
while right:
if left.val != right.val:
return False
left, right = left.next, right.next
return True
至此,把上面 3 段代码合在一起就高效地解决这个问题了,其中 reverse
函数很容易实现:
【python】
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
# 寻找链表中点
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# 对于奇数链表,slow要走到中点下一位,方便后续反转链表
if fast:
slow = slow.next
left = head
right = self.reverse(slow)
# 反转后半段链表后,就可以顺序遍历left和right判断是否回文
while right:
if left.val != right.val:
return False
left = left.next
right = right.next
return True
def reverse(self, head: ListNode) -> ListNode:
pre = None
cur = head
while cur:
next = cur.next
cur.next = pre
pre = cur
cur = next
return pre
【算法动态图】
算法总体的时间复杂度 O(N),空间复杂度 O(1),已经是最优的了。
我知道肯定有读者会问:这种解法虽然高效,但破坏了输入链表的原始结构,能不能避免这个瑕疵呢?
其实这个问题很好解决,关键在于得到p, q
这两个指针位置:
这样,只要在函数 return 之前加一段代码即可恢复原先链表顺序:
p.next = reverse(q);