《算法通关村第一关——链表经典问题之判断回文序列笔记》

目录

  1. 问题概述
  2. 入栈法及其优化
  3. 快慢指针法
  4. 递归法

一、问题概述

从前往后打印链表的值与从后往前打印链表的值相同,即称该链表为回文序列,例如:

输入:1->2->2->1
输出:true

输入:1->1->3->2->1
输出:false

 二、入栈法及其优化

        1、全部入栈

        对于回文串的判断,最容易想到的就是利用栈来解决。原因显而易见,栈具有先进后出的特点,即正序入栈,逆序出栈。因此,只需要一边正序遍历链表,一边出栈,将两者进行对比即可。源代码如下:

public static boolean isPalindromeByAllStack(ListNode head) {
        ListNode temp = head;
        Stack<Integer> stack = new Stack();
        
        while (temp != null) {
            stack.push(temp.val);
            temp = temp.next;
        }
        
        while (head != null) {
            if (head.val != stack.pop()) {
                return false;
            }
            head = head.next;
        }
        return true;
    }

        需要注意的是设置中间节点指针temp进行操作,否则在入栈时直接用head操作将会导致链表丢失。

        2、优化之一半入栈

        顾名思义,只需要将前一半的链表节点入栈,在出栈时,遍历后一半链表节点,两者相比较即可。不过此方法还需事前遍历整个链表从而获取链表长度。核心代码与上述代码类似,不再赘述。

        3、优化之一半出栈

        注意此处与第二个方法的不同的在于,第二个方法只需要入栈一半的链表节点,而此方法需要将全部链表节点入栈后,只出栈一半的节点。这是针对第二个方法需要遍历整个链表额外开销的问题作出的应对之策,即同时进行遍历链表与入栈操作。最终只需要比较出栈的一半节点(原链表后一半节点)与链表的前一半节点即可。同样,核心代码与上述代码类似,不再赘述。


三、快慢指针法

        为了减少申请栈的额外空间开销,我们可以利用双指针思想当中的快慢指针法(慢指针每次向后移动一个节点;快指针每次向后移动两个节点),可以帮助我们迅速定位到链表的中间位置(只需遍历链表的一半长度)。同时,在遍历的过程中,将慢指针指过的节点利用头插法(原链表前一半节点的逆序)创建出一个新的链表。最后通过遍历这个新的链表与慢指针遍历完原链表的后一半节点,两者进行判断即可。源代码如下:

bool isPalindrome(struct ListNode *head)
{
    if (head == NULL || head -> next == NULL)
    {
        return true;
    }
    
    ListNode *slow = head;
    ListNode *fast = head;
    ListNode *pre = head;
    ListNode *preNode = NULL;
    //快指针的作用在于控制慢指针走向链表的中间节点位置,避免为了获取链表长度而多一次遍历链表。
    while (fast != NULL && fast -> next != NULL)
    {
        pre = slow;
        slow = slow -> next;
        fast = fast -> next -> next;
        pre -> next = preNode;
        preNode = pre;
    }
    if (fast != NULL)//奇数的情况处理,对于正中间的那一个数字不做处理
    {
        slow = slow -> next;
    }
    while (pre != NULL && slow != NULL)//相当于从中间开始一个往前,一个往后
    {
        if (pre -> val != slow -> val)
        {
            return false;
        }
        pre = pre -> next;
        slow = slow -> next;
    }
    return true;
}

需要注意的易错点:

  1. preNode相当于创建链表时的头节点,初始化为NULL,也是为了作为之后遍历这个新链表的结束条件。
  2. 第一个while循环的结束判断条件,当fast == NULL时,此时说明此链表长度为偶数,slow处于中间节点的后一个(此时有两个中间节点);然而当fast -> next == NULL时,此时说明此链表长度为奇数,slow处于中间节点(此时只有一个中间节点)。原因也很简单,想想中位数的计算就知道了。
  3. 当链表长度为奇数时,我们发现需要从中间节点的下一个节点开始与新链表进行比较,例如:1->2->3->2->1,此时slow指向数字3,新链表内容为2->1,显然比较的位置需要从slow -> next开始。

下图为第一个while循环结束后,偶数链表的情况:

下图为第一个while循环结束后,奇数链表的情况:

根据上面两幅图可以清晰地发现这些易错点背后的原因 。


四、递归法

        递归法的使用思想类似于栈的思想,不过代码量更少,思维难度更高,先给出源代码如下所示:

    public static boolean isPalindromeByRe(ListNode head) {
        temp = head;
        return check(head);
    }

    private static boolean check(ListNode head) {
        if (head == null)
            return true;
        boolean res = check(head.next) && (temp.val == head.val);
        temp = temp.next;
        return res;
    }

        由于递归函数的特点,相当于head指针将一直往后移动,指向链表的尾部。此时不断返回的过程就相当于不断从后往前遍历链表的过程,此时只需要同时从前往后遍历链表,进行比对即可。注意,不要忘记递归的出口是当head为NULL时,返回true。返回过程中,一旦发现不匹配的链表节点那么res为false的结果将会一直保存知道递归返回结束。

        至此,几种核心的链表回文序列的判定方法全部介绍完毕,本文将讲义中的提到的没有详细展开方法进行了分析与讲解,相信能够对大家有帮助。

学号:15        昵称:秋容何暮

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值