请判断一个链表是否为回文链表。
方法一:将链表转换为数组,用双指针头尾遍历比较是否是回文。
- 数组列表底层是使用数组存储值,我们可以通过索引在 O(1) 的时间访问列表任何位置的值,这是由基于内存寻址的方式。
- 链表存储的是称为节点的对象,每个节点保存一个值和指向下一个节点的指针。访问某个特定索引的节点需要 O(n)的时间,因为要通过指针获取到下一个位置的节点。
- 确定数组列表是否回文很简单,我们可以使用双指针法来比较两端的元素,并向中间移动。一个指针从起点向中间移动,另一个指针从终点向中间移动。这需要O(n)的时间,因为访问每个元素的时间是 O(1),而有 n个元素要访问。
- 然而同样的方法在链表上操作并不简单,因为不论是正向访问还是反向访问都不是 O(1)。而将链表的值复制到数组列表中是 O(n),因此最简单的方法就是将链表的值复制到数组列表中,再使用双指针法判断。
var isPalindrome = function(head) {
let arr = [];
while(head){
arr.push(head.val);
head = head.next;
}
for(let i = 0,j = arr.length - 1;i<j;i++,j--){
if(arr[i] !== arr[j]) return false;
}
return true;
};
复杂度分析
时间复杂度:O(n),其中 n 指的是链表的元素个数。
第一步: 遍历链表并将值复制到数组中,O(n)。
第二步:双指针判断是否为回文,执行了O(n/2) 次的判断,即O(n)。
总的时间复杂度:O(2n)=O(n)。
空间复杂度:O(n),其中 n指的是链表的元素个数,我们使用了一个数组列表存放链表的元素值。
方法二:
避免使用 O(n) 额外空间的方法就是改变输入。
我们可以将链表的后半部分反转(修改链表结构),然后将前半部分和后半部分进行比较。比较完成后我们应该将链表恢复原样。虽然不需要恢复也能通过测试用例,但是使用该函数的人通常不希望链表结构被更改。
该方法虽然可以将空间复杂度降到 O(1),但是在并发环境下,该方法也有缺点。在并发环境下,函数运行时需要锁定其他线程或进程对链表的访问,因为在函数执行过程中链表会被修改。
整个流程可以分为以下五个步骤:
- 找到前半部分链表的尾节点。
- 反转后半部分链表。
- 判断是否回文。
- 恢复链表。
- 返回结果。
执行步骤一,我们可以计算链表节点的数量,然后遍历链表找到前半部分的尾节点。我们也可以使用快慢指针在一次遍历中找到:慢指针一次走一步,快指针一次走两步,快慢指针同时出发。当快指针移动到链表的末尾时,慢指针恰好到链表的中间。通过慢指针将链表分为两部分。若链表有奇数个节点,则中间的节点应该看作是前半部分。
步骤二可以使用反转一个单链表问题中的解决方法来反转链表的后半部分。
步骤三比较两个部分的值,当后半部分到达末尾则比较完成,可以忽略计数情况中的中间节点。
步骤四与步骤二使用的函数相同,再把后半部分反转一次恢复链表本身。
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
//反转链表
const reverseList = function(list){
if(list == null) return null;
let cur = list,pre = null;
while(cur){
let temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
};
//找出前半段的尾节点,如果为奇数,则中间节点加入前半部分
const endOfFirstHalf = function(head){
let slow = head,fast = head;
while(fast.next && fast.next.next){
slow = slow.next;
fast = fast.next.next;
}
return slow;
};
var isPalindrome = function(head) {
if(head == null) return null;
let first = endOfFirstHalf(head);
let second = reverseList(first.next);
let p1 = head,p2 = second;
while(p1 && p2 && p1.val == p2.val){
p1 = p1.next;
p2 = p2.next;
}
//还原链表
first.next = reverseList(second);
return p2 == null ? true : false;
};
复杂度分析
时间复杂度:O(n),其中 n 指的是链表的大小。
空间复杂度:O(1)。我们只会修改原本链表中节点的指向,而在堆栈上的堆栈帧不超过 O(1)。