Leetcode刷题笔记之链表篇234. 回文链表

Posted 大家好我叫张同学

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Leetcode刷题笔记之链表篇234. 回文链表相关的知识,希望对你有一定的参考价值。

😈博客主页:🐼大家好我叫张同学🐼
💖 欢迎点赞 👍 收藏 💗留言 📝 欢迎讨论! 👀
🎵本文由 【大家好我叫张同学】 原创,首发于 CSDN 🌟🌟🌟
精品专栏(不定时更新) 【数据结构+算法】 【做题笔记】【C语言编程学习】
☀️ 精品文章推荐
【C语言进阶学习笔记】三、字符串函数详解(1)(爆肝吐血整理,建议收藏!!!)
【C语言基础学习笔记】+【C语言进阶学习笔记】总结篇(坚持才有收获!)


前言

为什么要写刷题笔记
写博客的过程也是对自己刷题过程的梳理总结,是一种耗时有效的方法。
当自己分享的博客帮助到他人时,又会给自己带来额外的快乐和幸福。
(刷题的快乐+博客的快乐,简直是奖励翻倍,快乐翻倍有木有QAQ🙈)

题目内容

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

原题链接(点击跳转)

思路分析

回文结构
从前往后数和从后往前数均相同

从前往后:1 2 2 1
从后往前:1 2 2 1

具有对称性的链表就具有回文结构
如果是单数个结点,中间的结点无需考虑,如果其他结点对称肯定是回文结构
例如:1 2 3 1 2 也是回文结构

这里借助求链表倒数第k个结点的思路。
只要链表的

第1个结点=倒数第1个结点
第2个结点=倒数第2个结点

一直走到中间结点为止,都相同的话,就是回文结构。否则,就不是回文结构。


循环结束的进行/终止条件有很多,因为我们事先要求出链表的长度。

所以可以通过循环的步数:step <= 2/length
当然,也可以根据第k个和倒数第k个之间的关系: k <= length-k-1
此外,还可以通过结点指针的关系: cur->next != end || cur != end

函数实现
bool isPalindrome(struct ListNode* head)
    struct ListNode* tail = head;
    int length = 0;//求链表长度
    while(tail)
        tail = tail->next;
        length++;
     
    int k = 1;//顺数第k个,从1开始
    struct ListNode* cur = head,*end;
    while(k <= length-k)//倒数第k个就是顺序第length-k个
        end = head;
        for(int i = 0; i < length-k; i++)
            end = end->next;//通过end找到倒数第k个
        
        if(cur->val != end->val)//两者比较,不同就返回false
            return false;
        cur = cur->next;
        k++;
    
    return true;//所有结点均比较过,相同,返回true

注意:题目中给出了链表结点数量不为0,所以空链表不需要考虑。对于仅有一个结点的情况,程序依然能够覆盖到,所以也不需要作为一个单独的情况来处理。



通过Leetcode的执行代码和测试示例进行预提交,发现程序可以成功通过。但是当我们正式提交的时候,就会出现超出时间限制的问题。
一旦出现超出时间限制,我们通常可以考虑两种情况
1)程序中某些循环体结束的条件不对,导致程序进入死循环
2)程序算法的时间复杂度太高,没达到预期的要求,导致运行超时
(这时候可能有些同学会问:”妖怪吧,为什么你可以想到我却想不到呢?“张同学回答:”别问,问就是刷题刷多了,出错调试代码的次数多了,有经验了,我太难了…每天都是夜深人静刷力扣,夜静无人码代码”,额,开个玩笑,总之就是多实践,实践出真知实践是认识的源泉~


因为程序能通过测试用例,说明程序不可能显然死循环。我们可以点开超出时间限制的测试用例看一下,然后就可以看到…一大堆…数字,也就是测试输入量 n 很大的情况。
程序的时间复杂度为O(n^2),空间复杂度为O(1) 。当数据量很大的时候,因为O(n^2)的时间复杂度,程序运行的时间就需要很长,自然就无法通过测试用例。


思路分析

找到问题后,我们就要思考如何处理这个问题。要想优化时间复杂度,我们会想到以空间换时间的方式。也就是先遍历一遍原链表,将其内容复制头插新链表中,那么新链表的内容实际上就是原链表从后往前数的内容。然后通过比较两个链表内容是否相同,来判断是否为回文结构


我们在第一遍遍历链表复制结点的时候,还可以顺便求出链表长度,后面比较两个链表的时候,只需要比较前 2/length个结点即可。

函数实现
bool isPalindrome(struct ListNode* head)
    struct ListNode* cur1 = head;
    struct ListNode* newhead = NULL;
    int length = 0;
    while(cur1)
        //复制结点头插到newhead新链表中
        struct ListNode* node = (struct ListNode*)malloc(sizeof(struct ListNode));
        node->val = cur1->val;
        if(newhead == NULL)
            newhead = node;
            node->next = NULL;
        
        else
            node->next = newhead;
            newhead = node;
        
        length++;
        cur1 = cur1->next;
    
    //对比两个链表,判断回文结构
    cur1 = head;
    struct ListNode* cur2 = newhead;
    int step = length/2;
    while(step--)
        if(cur1->val != cur2->val)
           return false;
        cur1 = cur1->next;
        cur2 = cur2->next;
    
    return true;


提交程序后,Leetcode成功通过,但是我们可以看到程序的执行时间内存消耗都很大,原因如下:
(1)我们实际上遍历了两遍链表,但重点是我们用malloc开辟新结点构成新链表这个的耗时较长
(2)用malloc开辟新结点组成新链表的方式还会占用很多内存空间,导致内存消耗较大

注意,新链表newhead使用完后要将新链表中的结点都释放掉,因为这种结点都是malloc从堆上面申请的,不释放导致内存泄漏,如果开发程序使用这段代码。就会导致电脑或手机内存越用越少,程序运行越来越慢

bool isPalindrome(struct ListNode* head)
    struct ListNode* cur1 = head;
    struct ListNode* newhead = NULL;
    int length = 0;
    while(cur1)
        //复制结点头插到newhead新链表中
        struct ListNode* node = (struct ListNode*)malloc(sizeof(struct ListNode));
        node->val = cur1->val;
        if(newhead == NULL)
            newhead = node;
            node->next = NULL;
        
        else
            node->next = newhead;
            newhead = node;
        
        length++;
        cur1 = cur1->next;
    
    //对比两个链表,判断回文结构
    cur1 = head;
    struct ListNode* cur2 = newhead;
    int step = length/2;
    while(step--)
        if(cur1->val != cur2->val)
           return false;
        cur1 = cur1->next;
        cur2 = cur2->next;
    
    //释放newhead链表,防止内存泄漏
    cur2 = newhead;
    while(cur2)
        struct ListNode* next = cur2->next;
        free(cur2);
        cur2 = next;
    
    return true;

上面这种方法还可以进行小小的改进,原本是复制新结点到链表中,再比较两个链表内容是否一致。其实归根结底就是比较两个的值是否一样。因此,我们可以将原链表中的val值复制到一个数组中,数组的大小可以根据length来确定。malloc一次性开辟一个数组空间,可以减少消耗,然后再数组里面内部可以直接比较数值是否相同。(当然也可以用数组和链表比较,只是需要将链表前面结点的val和数组后面的val比较)


那有没有办法对其进行改进,以达到程序的运行时间很短,同时内存消耗也很小呢?

快慢指针法
对于回文结构相关的题目,有一个很常用的方法:将链表前半部分或者后半部分反转一下,然后进行比较,具体的过程是:

(1)找中点
(2)反转前半部分或者后半部分
(3)对比判断是否为回文结构
(4)还原链表

反转链表部分可参考:【Leetcode刷题笔记之链表篇】206. 反转链表

算法图解

函数实现
//迭代法
struct ListNode* reverseList(struct ListNode* head,struct ListNode* middle)
    if(!head)//先判断链表是否为空
      return NULL;
    struct ListNode* prev = NULL;
    struct ListNode* cur = head;
    while( cur != middle)
        struct ListNode* next = cur->next;
        cur->next = prev;
        prev = cur;
        cur = next;
    
    return prev;


bool isPalindrome(struct ListNode* head)
    struct ListNode* fast,*slow;
    fast = slow = head;
    while(fast && fast->next)
        slow = slow->next;
        fast = fast->next->next;
    
    head = reverseList(head, slow);//反转前半部分
    //通过fast是否为空来判断结点为单数还是偶数,确定后面比较的起点
    struct ListNode* cur1 = head,*cur2 = slow;
    if(fast != NULL)
        cur2 = cur2->next;
    
    while(cur1 && cur1 != slow)
        if(cur1->val != cur2->val)
           return false;
        cur1 = cur1->next;
        cur2 = cur2->next;
    
    //还原
    struct ListNode* mark = head;
    head = reverseList(head,NULL);
    if(mark && mark->next)
       mark->next = slow;
    return true;

以上是关于Leetcode刷题笔记之链表篇234. 回文链表的主要内容,如果未能解决你的问题,请参考以下文章

Leetcode刷题笔记之链表篇141. 环形链表

Leetcode刷题笔记之链表篇160. 相交链表

Leetcode刷题笔记之链表篇160. 相交链表

Leetcode刷题笔记之链表篇206. 反转链表

Leetcode刷题笔记之链表篇141. 环形链表

Leetcode刷题笔记之链表篇876. 链表的中间结点