234. 回文链表
题目介绍
给你一个单链表的头节点 \(head\) ,请你判断该链表是否为回文链表。如果是,返回 \(true\) ;否则,返回 \(false\) 。
示例 1:
输入:head = [1,2,2,1]
输出:true
示例 2:
输入:head = [1,2]
输出:false
提示:
- 链表中节点数目在范围\([1, 10^{5}]\) 内
- \(0 <= Node.val <= 9\)
进阶:你能否用 \(O(n)\) 时间复杂度和 \(O(1)\) 空间复杂度解决此题?
题解
2.1 迭代-双指针(链表转换数组)
代码
class Solution {
public:
bool isPalindrome(ListNode* head) {
vector<int> arr;
int i = 0, j =0;
while (head){
arr.push_back(head->val);
head = head ->next;
}
j = arr.size() - 1;
while (i < j){
if (arr[i++] != arr[j--]) return false;
}
return true;
}
};
复杂度分析
时间复杂度:O(n),其中 nnn 指的是链表的元素个数。
第一步: 遍历链表并将值复制到数组中,O(n)。
第二步:双指针判断是否为回文,执行了 O(n/2) 次的判断,即 O(n)。
总的时间复杂度:O(2n)=O(n)O(2n) =O(n)。
空间复杂度:O(n),其中 n 指的是链表的元素个数,我们使用了一个数组列表存放链表的元素值。
2.2 递归
思路
回文链表最重要的是能同时拥有指向头尾的指针,方便进行比较。
我们联想到使用递归可以进入到最深层也就是尾结点,递归的回调也弥补了单向链表无法向前回溯的缺陷。然后在设置一个全局指向首节点的指针,使用双指针思路即可。
代码
class Solution {
ListNode *frontPointer;
public:
bool recursicvelyCheck(ListNode *currentNode){
if (currentNode != nullptr){ //回调返回条件:遍历到尾结点
if(!recursicvelyCheck(currentNode->next)) return false; //这里实现递归。如果出现一个不成立的情况,后续回调均返回false;
if(currentNode -> val != frontPointer -> val) return false; // 非回文链表情况。
frontPointer = frontPointer -> next; // 双指针的同步.
}
return true; //作为最深层的返回值
}
bool isPalindrome(ListNode* head) {
frontPointer = head;
return recursicvelyCheck(head);
}
};
复杂度分析
时间复杂度:O(n),其中 n 指的是链表的大小。
空间复杂度:O(n),其中 n 指的是链表的大小。我们要理解计算机如何运行递归函数,在一个函数中调用一个函数时,计算机需要在进入被调用函数之前跟踪它在当前函数中的位置(以及任何局部变量的值),通过运行时存放在堆栈中来实现(堆栈帧)。在堆栈中存放好了数据后就可以进入被调用的函数。在完成被调用函数之后,他会弹出堆栈顶部元素,以恢复在进行函数调用之前所在的函数。在进行回文检查之前,递归函数将在堆栈中创建 nnn 个堆栈帧,计算机会逐个弹出进行处理。所以在使用递归时空间复杂度要考虑堆栈的使用情况。
这种方法不仅使用了 O(n) 的空间,且比第一种方法更差,因为在许多语言中,堆栈帧的开销很大(如 Python),并且最大的运行时堆栈深度为 1000(可以增加,但是有可能导致底层解释程序内存出错)。为每个节点创建堆栈帧极大的限制了算法能够处理的最大链表大小。
2.3 快慢指针
思路
整个流程可以分为以下五个步骤:
- 找到前半部分链表的尾节点。
- 反转后半部分链表。
- 判断是否回文。
- 恢复链表。
- 返回结果。
执行步骤一,我们可以计算链表节点的数量,然后遍历链表找到前半部分的尾节点。
我们也可以使用快慢指针在一次遍历中找到:慢指针一次走一步,快指针一次走两步,快慢指针同时出发。当快指针移动到链表的末尾时,慢指针恰好到链表的中间。通过慢指针将链表分为两部分。
若链表有奇数个节点,则中间的节点应该看作是前半部分。
步骤二可以使用「206. 反转链表」问题中的解决方法来反转链表的后半部分。
步骤三比较两个部分的值,当后半部分到达末尾则比较完成,可以忽略计数情况中的中间节点。
步骤四与步骤二使用的函数相同,再反转一次恢复链表本身。
代码
class Solution {
public:
bool isPalindrome(ListNode *head) {
ListNode *firstHalfEnd = endOfFirstHalf(head);
ListNode *secondHalfEnd = reverseList(firstHalfEnd->next);
ListNode *p1 = head;
ListNode *p2 = secondHalfEnd;
while (p2 != nullptr) {
if (p1->val != p2->val) return false;
p1 = p1->next;
p2 = p2->next;
}
firstHalfEnd->next = reverseList(secondHalfEnd);
return true;
}
ListNode *reverseList(ListNode *head) {
ListNode *pre = nullptr, *curr = head;
while (curr != nullptr) {
ListNode *next = curr->next;
curr->next = pre;
pre = curr;
curr = next;
}
return pre;
}
ListNode *endOfFirstHalf(ListNode *head) {
ListNode *p1 = head, *p2 = head;
while (p2->next != nullptr && p2->next->next != nullptr) {
p1 = p1->next;
p2 = p2->next->next;
}
return p1;
}
};