【链表】力扣234:回文链表
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
示例1:
输入:head = [1,2,2,1]
输出:true
示例2:
输入:head = [1]
输出:true
数组储存再反转比较
在 Python 中,很容易构造一个列表的反向副本,也很容易比较两个列表,所以没有必要用双指针法。
需要注意的是,比较的是结点值的大小,而不是结点本身。正确的比较方式是node1.val == node2.val
,而 node1 == node2
是错误的。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
if not head: # 特殊情况先行判断
return False
vals = []
while head:
vals.append(haed.val)
head = head.next
return vals == vals[::-1]
'''
1. 判断方式用双指针法
j = len(vals) - 1
for i in range(len(vals)):
if vals[i] != vals[j]: # 比较的是结点的值的大小
return False
j -= 1
return True
2. 判断方式用单指针法
n = len(vals)
for i in range(n // 2):
if vals[i] != vals[n -1 - i]:
return False
return True
'''
时间复杂度:O(n),其中 n 指的是链表的元素个数。
第一步: 遍历链表并将值复制到数组中,O(n)。
第二步:双指针判断是否为回文,执行了 O(n/2) 次的判断,即 O(n)。
总的时间复杂度:O(2n) = O(n)。
空间复杂度:O(n)。使用了一个数组列表存放链表的元素值。
栈储存再pop比较
遇到对称有关的问题应该首先想用stack能不能解决,如果链表元素个数为偶数,先把一半的元素放入stack,然后每次用栈顶元素和下一个节点比较,如果一直比较成功就是true,否则false。如果是奇数个,先将一半加一个元素入栈,然后先出栈一次在做上述比较。
整体堆栈
-
遍历链表,把每个节点都 push 到 stack
-
再次遍历链表,同时栈内结点依次 pop,二者进行比较
时间复杂度 O(N),空间复杂度 O(N)
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
stack = []
# step1: push
cur = head
while cur:
stack.append(cur)
cur = cur.next
# step2: pop and compare
node1 = head
while stack:
node2 = stack.pop()
if node1.val != node2.val:
return False
node1 = node1.next
return True
作者:coldme-2
链接:https://leetcode.cn/problems/palindrome-linked-list/solution/hui-wen-lian-biao-de-san-chong-fang-fa-by-coldme-2/
一半堆栈
-
利用快慢双指针 slow 和 fast 来遍历链表,以找到链表中间的位置
-
链表的前一半入栈,后一半与出栈的结点依次比较
需要考虑链表长度是奇数还是偶数,这取决于终止时 fast 为空还是 fast.next 为空。
-
head 位于第一个结点
-
对于奇数,如
1->2->3->2->1
,此时快指针 fast 会停在最后的 1 处(fast 不为空,而 fast.next 为空),慢指针 slow 停在中间的 3 处,这时需要对 slow.next 的链表进行翻转 -
对于偶数,结束循坏时 fast 和 fast.next 均为空,慢指针 slow 停在链表后半部分的第一个位置,则不需要在 pop比较 之前继续移动 slow
时间复杂度 O(N),空间复杂度 O(N),但比整体堆栈节省一半的空间。
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
stack = []
slow, fast = head, head # 快慢指针均指向头结点
# step1: push
while fast and fast.next:
stack.append(slow) # 链表前一半依次入栈
slow = slow.next
fast = fast.next.next
if fast:
slow = slow.next
# step2: pop and compare
while stack:
cur = stack.pop()
if cur.val != slow.val:
return False
slow = slow.next
return True
作者:coldme-2
链接:https://leetcode.cn/problems/palindrome-linked-list/solution/hui-wen-lian-biao-de-san-chong-fang-fa-by-coldme-2/
递归
使用递归反向迭代节点,同时使用递归函数外的变量向前迭代,就可以判断链表是否为回文。
-
建立 curr 指针先到尾节点,由于递归的特性再从后往前进行比较。建立 递归函数外的指针 prev。
-
若 curr.val != prev.val 则返回 false。否则,prev 向前移动,并返回 true。
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
self.prev = head
def recursively_check(curr = head):
if curr:
if not recursively_check(curr.next):
return False
if self.prev.val != curr.val:
return False
self.prev = self.prev.next
return True
return recursively_check()
作者:LeetCode-Solution
链接:https://leetcode.cn/problems/palindrome-linked-list/solution/hui-wen-lian-biao-by-leetcode-solution/
时间复杂度:O(n),其中 n 指的是链表的大小。
空间复杂度:O(n)。
要理解计算机如何运行递归函数,在一个函数中调用一个函数时,计算机需要在进入被调用函数之前跟踪它在当前函数中的位置(以及任何局部变量的值),通过运行时存放在堆栈中来实现(堆栈帧)。在堆栈中存放好了数据后就可以进入被调用的函数。在完成被调用函数之后,会弹出堆栈顶部元素,以恢复在进行函数调用之前所在的函数。在进行回文检查之前,递归函数将在堆栈中创建 n 个堆栈帧,计算机会逐个弹出进行处理。所以在使用递归时空间复杂度要考虑堆栈的使用情况。
在许多语言中,堆栈帧的开销很大(如 Python),并且最大的运行时堆栈深度为 1000(可以增加,但是有可能导致底层解释程序内存出错)。为每个结点创建堆栈帧极大的限制了算法能够处理的最大链表大小。
快慢指针(the best)
避免使用 O(n) 额外空间的方法就是改变输入。
可以将链表的后半部分反转(修改链表结构),然后将前半部分和后半部分的值进行比较。比较完成后应该将链表恢复原样。虽然不需要恢复也能通过测试用例,但是使用该函数的人通常不希望链表结构被更改。
-
找到前半部分链表的尾节点
- 设置快慢指针。每次快指针增加两个,慢指针增加一个。这样当快指针结尾时,慢指针指向了链表的中间
-
反转后半部分链表
- 用慢指针逆序链表的后半部分,利用Python交换的特性,不需要额外的temp
不懂就去看力扣206题:https://www.cnblogs.com/Jojo-L/p/16453891.html
-
判断是否回文
- 一个从头开始,一个从中间开始,判断两结点值是否相同。当后半部分到达末尾则比较完成,可以忽略计数情况中的中间节点
-
恢复链表,即再反转一次
-
返回结果
该方法虽然可以将空间复杂度降到 O(1),但是在并发环境下,该方法也有缺点。在并发环境下,函数运行时需要锁定其他线程或进程对链表的访问,因为在函数执行过程中链表会被修改。
没有恢复链表版本
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
if not head:
return False
slow, fast, pre = head, head, None
# 找到前半部分链表的尾节点
while fast:
slow = slow.next
fast = fast.next.next if fast.next else fast.next # 避免判断链表长度奇偶。循环结束时指针 slow 在链表后半部分第一个,fast 为空
# 反转后半部分链表
while slow:
slow.next, slow, pre = pre, slow.next, slow
# 判断是否回文
while head and pre:
if head.val != pre.val:
return False
head, pre = head.next, pre.next
return True
时间复杂度:O(n),其中 n 指的是链表的大小。
空间复杂度:O(1)。只修改原本链表中结点的指向,而在堆栈上的堆栈帧不超过 O(1)。
前半部分比较
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
# reverse front link when finding mid node
slow = None
fast = head
back = head
while fast and fast.next:
fast = fast.next.next
temp = back.next
back.next = slow
slow = back
back = temp
# modify back if odd link
if fast:
back = back.next
# compare and return answer
while back and slow.val == back.val:
slow = slow.next
back = back.next
return False if back else True
作者:thuliangtian
链接:https://leetcode.cn/problems/palindrome-linked-list/solution/by-thuliangtian-h8zd/
恢复了链表版本 前半部分比较
比较的同时顺便反转前半部分
@Java
public boolean isPalindrome(ListNode head) {
if(head == null || head.next == null) {
return true;
}
ListNode fast = head;
ListNode slow = head;
ListNode prev = null;
// 快慢指针,并同时反转链表前半部分
while (fast != null && fast.next != null) {
fast = fast.next.next;
// 反转
ListNode nextNode = slow.next;
slow.next = prev;
prev = slow;
slow = nextNode;
}
ListNode prepre = slow;
if (fast != null) {
slow = slow.next;
}
// 比较值并反转还原前半部分
boolean isPalindrome = true;
while (prev != null) {
if (slow.val != prev.val) {
isPalindrome = false;
}
slow = slow.next;
// 前半部分再次反转
ListNode nextNode = prev.next;
prev.next = prepre;
prepre = prev;
prev = nextNode;
}
return isPalindrome;
}
@ 蓝黑R9(LeetCode)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理