回文链表
前言
这是我在算法入门道路上的日常积累,记录自己的学习和思考过程,如有不对或欠缺的地方,欢迎大佬们指正或补充!
目前工作中没有要求,我自己也定KPI,做题周期随缘,以尽可能吃透每一道题为目标,搬砖之余进行自我提升
题目说明
说明
给你一个单链表的头节点 head
,请你判断该链表是否为回文链表。如果是,返回 true
;否则,返回 false
。
示例
示例 1:
输入:head = [1,2,2,1]
输出:true
示例 2:
输入:head = [1,2]
输出:false
提示
- 链表中节点数目在范围
[1, 105]
内 0 <= Node.val <= 9
进阶
- 能否用
O(n)
时间复杂度和O(1)
空间复杂度解决此题?
初始代码
/**
* Definition for singly-linked list.
* class ListNode {
* val: number
* next: ListNode | null
* constructor(val?: number, next?: ListNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
* }
*/
function isPalindrome(head: ListNode | null): boolean {
};
题目总结
解题思路
- 先考虑基本实现;
- 看到链表想到递归;
- 看到回文想到对称,暴力反转一下总没错;
- 综上,利用递归将所有节点存在新的数组里,利用API
Array.prototype.reverse()
和Array.prototype.join()
得到全部节点值拼接是字符串及其反转结果并返回其是否相同。
代码(解法1)
function isPalindrome(head: ListNode | null): boolean {
let allNodeVals: number[] = [];
// 合并全部节点的值
mergeAllNodeVals(head, allNodeVals);
// 比较合并后的值与其反转后结果
return allNodeVals.join() === allNodeVals.reverse().join();
}
/**
* 合并全部节点的值
* @param node 链表节点
* @param nodeVals 合并后数组
*/
function mergeAllNodeVals(node: ListNode | null, nodeVals: number[]): void {
if (node) {
nodeVals.push(node.val);
// 下一节点存在则继续递归
if (node.next) mergeAllNodeVals(node.next, nodeVals);
}
}
运行结果(解法1)
(解法1近10次运行结果)
优化
问题总结1
- 数组长度分配变化会有内存消耗;
- 数组合并字符串与数组翻转都增加了时间与空间复杂度。
优化思路1
- 利用指针记录节点,在递归中直接进行比较;
- 利用链表有向的特点从两端开始比较,向中间收拢。
代码(解法2)
/** 记录左节点 */
let leftNode: ListNode;
function isPalindrome(head: ListNode | null): boolean {
// 取得最左端节点
if (!leftNode) leftNode = head;
// 递归直到最右端节点
let result: boolean = true;
// 直到当前节点为空,则到达递归尽头
if (head) {
// 取得当前到两端节点的比较结果,递归尽头前一次就是最两端节点比较
result = isPalindrome(head.next) && (leftNode.val === head.val)
// 将最左端节点后移
leftNode = leftNode.next;
return result;
} else { // 只有一个或最后的节点没有下一个节点,直接返回true
return result;
}
}
运行结果(解法2)
(解法2近10次运行结果)
问题总结2
- 利用单指针和递归的方式必须调用到最后一层函数,再逐层返回结果,哪怕中途已经发现结果为否,也必须返回到最外层才能终止程序。
- 从递归的特性来看,虽然只相当于循环了一次完整链表,时间复杂度为
O(n)
,但是每递归一次函数,都相当于在线程中占用了同等的内存空间,递归完整的一次链表,空间复杂度也是O(n)
。
优化思路2
- 链表这种数据结构的特性使其利于增删修改,不利于查找,因此遍历查找是不可避免的,但通过其链式结构修改链表结构是很容易的,如果能找到链表中点,就可以直接切断,对比两侧;
- 利用快慢双指针可以找到一个链表的中间节点(快慢双指针:循环改变指针引用节点,慢指针每次指向自己的下一个节点,快指针每次指向自己的下下个节点,这样快指针的前进速率就是慢指针的两倍,当快指针到达最后节点时,慢指针将到达中间节点,注意偶数个节点的列表有两个中间节点);
- 比较链表的两部分同样可以利用翻转的思想,比较前半段链表与后半段翻转后的链表。
代码(解法3)
function isPalindrome(head: ListNode | null): boolean {
// 中心节点
let center: ListNode = getCenterNode(head);
// 切断链表并返回后半段反转的链表、对比两侧链表
return compareLeftAndRight(head, reverseNode(center));
}
/**
* 取得中心节点
* @param head 头节点
* @returns 中心节点
*/
function getCenterNode(head: ListNode | null): ListNode {
// 记录快慢双指针
let fast: ListNode = head;
let slow: ListNode = head;
// 遍历一半列表、快指针走完时慢指针停留在中间节点
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
/**
* 反转链表
* @param head 头节点
* @returns 反转后链表
*/
function reverseNode(head: ListNode | null): ListNode {
// 记录当前节点
let node: ListNode = head;
// 记录前节点
let pre: ListNode = null;
// 遍历后半段节点
while (node) {
// 临时记录原来的下一节点、将当前节点和下一节点交换
const temp: ListNode = node.next;
node.next = pre;
pre = node;
node = temp;
}
return pre;
}
/**
* 比较两侧链表是否相同
* @param left 左链表
* @param right 右链表
* @returns 判断结果
*/
function compareLeftAndRight(left: ListNode | null, right: ListNode | null): boolean {
// 奇数时左侧链表多出一位,但是无需判断,因此以右侧链表为基准遍历
while (right) {
if (left.val === right.val) {
left = left.next;
right = right.next;
} else { // 出现反例就返回
return false;
}
}
return true;
}
运行结果(解法3)
(解法3近10次运行结果)
结语
一个简单的回文加链表的算法问题,思路从暴力翻转,到单指针递归,再到快慢双指针翻转链表。需要有对指针和引用类型的理解,链表的相关知识,双指针剪枝算法的实现。