简介
题目链接 Leetcode 234. Palinedrome Linked List
这篇文章的主要目的是借此题的递归解法来对递归的回溯特性做一个简单的探讨。递归解法是一个时间复杂度为O(N)
,空间复杂度也为O(N)
的解法。至于题目中的追问:“能否用O(1)
的空间和O(N)
的时间完成?”这里不做讨论。
注意:文章中一切注解皆为Python代码
理解题目
题目非常简单。给定一个单向链表,判断链表中的值按当前顺序是否形成一个回文串。
如何判断是否为回文串
有多种判断回文串的方式,这里用一个Python列表 (list
) 来举例,假设有列表 p = [1, 2, 2, 1]
,那么判断它是否为回文串的方法有:
- 判断正序的
p
和倒序的p
是否相等,即True if p == p[::-1] else False
- 取列表的首尾两处初始化指针,两两对比,如果相等,就向中间靠拢,否则返回
False
,两指针成功相遇即证明列表为回文串left, right = 0, len(p) - 1 while left < right: if p[left] == p[right]: left += 1 right -= 1 else: return False return True
本文要讲的解法,会使用到第二种判断回文串的方式
递归解法
直觉上的思考过程
使用第二种方法判断回文串,说实话,并不符合直觉。原因如下:
- 首先,单向链表只能向一个方向移动,没有办法做到取左右端点,并向相对方向移动
- 其次,递归一旦开始,需要层层嵌套,也就必然使得单向链表的当前节点向一个方向移动,这样一来也就没有办法作比较
进一步思考
我们从上一部分“直觉上的思考过程”就可以发现,想要用递归实现第二种判断回文串的方式,就必须要克服两个问题
- 能同时取得左右两个端点
- 能在某一时刻比较两个端点,并且向相对的方向移动,也就是说某一个指针需要以从链尾到链首的方向移动
递归的两个隐藏特性
递归的每一个层级,输入变量都会变化,最终到递归出口结束递归,这也就间接的说明了:
- 递归函数外面的变量,可以按照我们的需要保持不变
递归函数遇到递归出口时,不仅是结束当前的递归层级,而且会向上一个层级回溯,以帮助上一个层级的函数遇到递归出口。这样的顺序说明:
- 递归函数不仅有递归(由浅入深)的过程,也有回溯(由深出浅)的过程,可以利用这个过程实现链表的反向遍历
虽然第一个隐藏特性甚至有点过于明显,写出来甚至有些愚蠢,但在做题的时候,很容易先入为主认为递归方程与外界变量无关。
代码实现
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
left = head # 起初保持不变,只有在一次和cur的对比后才会向next挪一位
def recursion(cur):
nonlocal left # 声明使用外部变量
if cur: # 如果当前节点非空,则进行递归;否则直接返回True
if not recursion(cur.next): # 首先,利用递归进行到下一层,并且移动cur;同时判断,
return False # 下一层级能否形成回文串,如果不能,直接返回False
if left.val != cur.val: # 接着,递归出口后判断当前的两个指针是否相等
return False # 如果不等,直接返回False
left = left.next # 重点来了,left只有在和cur进行对比之后才会挪到下一位
return True # 最终,当一切递归结束,并且没有提前返回False的情况下,
# 可以确认原输入链表为回文串
return recursion(head) # 调用时,只需要传入链表的首节点
代码运行过程简述
- 一旦递归开始,递归会利用“深入”的过程不断把初始值为
head
的cur
变量推向链表的尾端 - 这也就帮助我们,为了将左端点和右端点进行比较,打下基础
- 接着递归,会进行当前左右端点的判断也就是
left.val != cur.val
这个过程 - 一旦判断结束,递归会自动回溯到前一个节点,但是我们需要手动挪动
left
节点,以便下一次判断 - 一直重复这个过程,直到递归完成
解题后的思考
尽管代码很简单,但是无论是理解代码,或是想出这个方法,都谈不上直接。有以下几点思考值得分享:
- 递归回溯过程和栈(
stack
)的退栈过程(pop
)很像,都是后进先出Last-In-First-Out(LIFO)
;- 对于递归:最深层的递归层(最后触发的递归层),最先结束
- 对于栈:最后加入的元素,最先被退出来
- 这题的递归过程和树的后序遍历(
Post-Order-Traversal
)非常类似,都是在深入过程中什么都不做,等到回溯过程中才进行相应的活动 - 利用递归函数的传入变量(
cur
),和外部的变量(left
)这样“一动一静”的组合,可以实现指针状态的初始化,可以称得上是一种另类的双指针解法。