【TypeScript】【算法入门】回文链表

回文链表


前言

这是我在算法入门道路上的日常积累,记录自己的学习和思考过程,如有不对或欠缺的地方,欢迎大佬们指正或补充!
目前工作中没有要求,我自己也定KPI,做题周期随缘,以尽可能吃透每一道题为目标,搬砖之余进行自我提升


题目说明

说明

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false

示例
示例 1:
1
2
2
1
  输入:head = [1,2,2,1]
  输出:true
示例 2:
1
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近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近10次运行结果)

问题总结2
  • 利用单指针和递归的方式必须调用到最后一层函数,再逐层返回结果,哪怕中途已经发现结果为否,也必须返回到最外层才能终止程序。
  • 从递归的特性来看,虽然只相当于循环了一次完整链表,时间复杂度为 O(n) ,但是每递归一次函数,都相当于在线程中占用了同等的内存空间,递归完整的一次链表,空间复杂度也是 O(n)
优化思路2
  • 链表这种数据结构的特性使其利于增删修改,不利于查找,因此遍历查找是不可避免的,但通过其链式结构修改链表结构是很容易的,如果能找到链表中点,就可以直接切断,对比两侧
X
X
1
2
2
1
1
2
1
  • 利用快慢双指针可以找到一个链表的中间节点(快慢双指针:循环改变指针引用节点,慢指针每次指向自己的下一个节点,快指针每次指向自己的下下个节点,这样快指针的前进速率就是慢指针的两倍,当快指针到达最后节点时,慢指针将到达中间节点,注意偶数个节点的列表有两个中间节点);
  • 比较链表的两部分同样可以利用翻转的思想,比较前半段链表与后半段翻转后的链表。
代码(解法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次运行结果
(解法3近10次运行结果)


结语

一个简单的回文加链表的算法问题,思路从暴力翻转,到单指针递归,再到快慢双指针翻转链表。需要有对指针和引用类型的理解,链表的相关知识,双指针剪枝算法的实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值