题目
解题
要解决这个问题,可以将回文 (palindrome) 定义为 0 -> 1 -> 2 -> 1 -> 0。显然,若链表是回文,不管正着看还是反着看,都是一样的。
解题一:反转并比较
做题目的时候可能是脑子出去神游了,竟然特开心地在反转链表时没有new node,直接拿的原链表节点,结果把原链表给改了,比较的时候就比较了个寂寞。以后解题之前应该先判断一下原链表是否可以被修改,如果可以便不用额外的空间,如果不可以那要确保操作不能改变原链表。
官解喜欢写子函数来完成一个一个小操作,我自己常把所有代码敲在一个函数里,代码少的话是没什么问题,但是平时开发中不易读和进行维护。所以,这一点要向官解学习!
在比较原始链表和反转链表时,其实只需比较链表的前半部分。若原始链表和反转链表的前半部分相同,那么,两者的后半部分肯定相同。即长度为5的链表只需要比较前2个节点,长度为4的链表也只用比较前2个节点。
// javascript
var ListNodeWithLen = function(node, len) {
this.node = node;
this.len = len;
};
// 反转链表
var reverseList = function(head) {
let rev = new ListNodeWithLen(null, 0);
while (head !== null) {
rev.len++;
let newNode = new ListNode(head.val);
newNode.next = rev.node;
rev.node = newNode;
head = head.next;
}
return rev;
};
// 比较两个链表是否相同
var isEqual = function(l1, l2, length) {
let count = length >> 1;
for (let i = 0; i < count; i++) { // [] 进不了 for loop
if (l1.val !== l2.val) return false;
l1 = l1.next;
l2 = l2.next;
}
return true;
};
var isPalindrome = function(head) {
let rev = reverseList(head);
return isEqual(head, rev.node, rev.len);
};
时间复杂度和空间复杂度都是 O ( N ) O(N) O(N)。从时间复杂度上来看,必须遍历链表的每一个节点后才可以判断是否是回文,所以应该尝试从空间角度去优化,题目也给了提示,问能不能用 O ( 1 ) O(1) O(1)的空间复杂度来实现。
解题二:迭代法
用栈来实现,栈是后进先出,可以想象一下堆碟子,后堆上去的碟子是可以先被拿出来,可以用一个数组去实现。
为了找到中间节点,用了快慢runner,慢runner走一步,快runner走两边,当快runner到尾部时,慢runner正好在中间位置。
// javascript
var isPalindrome = function(head) {
let fast = head, slow = head;
let arr = [];
// 将链表前半部分元素插入到栈中
// 当快指针 (2 倍速移动),移动到链表尾部,慢指针到达中点
while (fast !== null && fast.next !== null) {
arr.push(slow.val);
slow = slow.next;
fast = fast.next.next;
}
// 因为有奇数个节点,所以跳过中间节点
if (fast !== null) slow = slow.next;
while (slow !== null) {
// 如果值不同,则不是回文
if (slow.val !== arr.pop()) return false;
slow = slow.next;
}
return true;
};
但是这里面用栈来存储前半段链表节点值,所以还是用了额外的空间,避免使用
O
(
N
)
O(N)
O(N)额外空间的方法就是改变输入。
// javascript
const reverseList = (head) => {
let prev = null;
let curr = head;
while (curr !== null) {
let nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
};
const endOfFirstHalf = (head) => {
let fast = head, slow = head;
while (fast.next !== null && fast.next.next !== null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
};
var isPalindrome = function(head) {
if (head === null) return true;
// 找到前半部分链表的尾节点并反转后半部分链表
const firstHalfEnd = endOfFirstHalf(head);
const secondHalfStart = reverseList(firstHalfEnd.next);
// 判断是否回文
let p1 = head;
let p2 = secondHalfStart;
let res = true;
while (res && p2 !== null) {
if (p1.val !== p2.val) {
res = false;
}
p1 = p1.next;
p2 = p2.next;
}
// 还原链表并返回结果
firstHalfEnd.next = reverseList(secondHalfStart);
return res;
};
解法三:递归法
LeetCode官网上的解题思路会比书上的解法更容易理解。如果是数组,可以用头尾两个指针去遍历,链表只能从前往后遍历,不能从后往前遍历(不知道前一个节点),所以用递归的方法来实现尾指针的向前遍历。
// javascript
let frontPointer;
const reversivelyCheck = (currentNode) => {
if (currentNode === null) return true;
let cmp = reversivelyCheck(currentNode.next);
if (cmp === false || currentNode.val !== frontPointer.val) return false;
frontPointer = frontPointer.next;
return true;
};
var isPalindrome = function(head) {
frontPointer = head;
return reversivelyCheck(head);
};
如果不将 frontPointer 设置为全局变量,可以用下面的写法进行传递:
//javascript
var NodeWithCmp = function(node, cmp) {
this.node = node;
this.cmp = cmp;
};
var isEqual = function(head, curNode) {
if (curNode === null) return new NodeWithCmp(head, true);
let res = isEqual(head, curNode.next);
if (res.cmp === false || res.node.val !== curNode.val) return new NodeWithCmp(null, false);
res.node = res.node.next;
return res;
};
var isPalindrome = function(head) {
let res = isEqual(head, head);
return res.cmp;
};
书上花了比较长的篇幅来描述推演的思路,该方法预先计算链表的长度,只比较了链表的前半部分,而上面的解法其实进行了重复比较。递归是我的薄弱环节,但是在链表的遍历中它确实有天然的优势。
// javascript
var NodeWithCmp = function(node, cmp) {
this.node = node;
this.cmp = cmp;
};
var getLength = function (l) {
var length = 0;
while (l !== null) {
length++;
l = l.next;
}
return length;
};
var isPalindromeRecurse = function(head, length) {
if (length === 0) { // 偶数个节点
return new NodeWithCmp(head, true);
}
if (length === 1) { // 奇数个节点
return new NodeWithCmp(head.next, true);
}
// 在子链表上递归
let res = isPalindromeRecurse(head.next, length - 2);
if (res.cmp === false) { // 如果递归调用返回非回文,则向上传递失败信息
return new NodeWithCmp(null, false);;
}
res.cmp = (res.node.val === head.val); // 检查与另一侧的节点值是否匹配
res.node = res.node.next;
return res; // 返回对应的节点
};
var isPalindrome = function(head) {
let length = getLength(head);
let res = isPalindromeRecurse(head, length);
return res.cmp;
};