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 快慢指针

思路

整个流程可以分为以下五个步骤:

  1. 找到前半部分链表的尾节点。
  2. 反转后半部分链表。
  3. 判断是否回文。
  4. 恢复链表。
  5. 返回结果。
    执行步骤一,我们可以计算链表节点的数量,然后遍历链表找到前半部分的尾节点。

我们也可以使用快慢指针在一次遍历中找到:慢指针一次走一步,快指针一次走两步,快慢指针同时出发。当快指针移动到链表的末尾时,慢指针恰好到链表的中间。通过慢指针将链表分为两部分。
若链表有奇数个节点,则中间的节点应该看作是前半部分。
步骤二可以使用「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;
    }
};
posted @ 2023-12-10 20:19  DawnTraveler  阅读(14)  评论(0编辑  收藏  举报