LeetCode #234回文链表(快慢指针中,易引入的bug问题)

前言:个人LC刷题记录与心得分享。

     ~~~~          ~~~    题解之类的力扣社区的大佬们都有写,所以笔者在此主要谈的是自己在完成这道题目时,遇见的bug以及相应的解决办法。
题目描述
[解题思路]
step1. 利用快慢指针的技巧,先找到链表的中点(中点的划定与链表结点个数的奇偶有关)。
step2. 反转后半部分链表。
step3. 利用双指针的方式判断是否回文。
step4. 恢复链表至初始状态(对于本题不是必须的,但建议考虑此步骤)。
step5. 返回bool值。

[完整测试代码-正确的示范?]

#include <iostream>

using namespace std;
class ListNode
{
private:
	int val;
	ListNode* next;
public:
	ListNode* creatListR(int a[], int n);//尾插法建立单链表(无dummy结点)
	ListNode* reverseList(ListNode* A); //反转链表
	bool isPalindrome(LNode* head);  //回文链表判断
	void print();
}

void ListNode::print()
{
    ListNode* p = this;
    cout << "List: " << endl;
    while(p != NULL){
        cout << p->val << " ";
        p = p->next;
    }
    cout << endl;
}

ListNode* ListNode::creatListR(int a[], int n)
{   
	//s指向新生成的结点, r始终指向终端结点, preHead为哨兵结点
    ListNode *s, *r, preHead; 

    r = &preHead;  
    for(int i = 0; i < n; i++){
        s = new ListNode; 
        s->val = a[i];
        r->next = s;   //新结点接入链表尾部
        r = r->next;  //r指向终端,以便接纳下一个到来的结点
    }
    r->next = NULL;

    return preHead.next;  //返回dummy结点的单链表
}

ListNode* ListNode::reverseList(ListNode* A)
{
    if(A == NULL || A->next == NULL) return A;

    //q作为辅助结点来记录p的直接后继结点的位置
    ListNode preHead, *p, *q;
    p = A;  //p结点始终指向旧链表的开始结点
    preHead.next = NULL;  

    while(p != NULL){
        q = p->next;
        //将p所指的结点插入新的链表中(头插法)
        p->next = preHead.next;  
        preHead.next = p;
        p = q; //因后继结点已经存入q中,所以p仍然可以找到后继(此时新的开始结点)
    }
    return preHead.next;
}

bool ListNode::isPalindrome(ListNode* head)
{
    if(head == NULL || head->next == NULL) return true;
    
    //快慢指针找链表的中点  
    ListNode *fastNode = head;
    ListNode *slowNode = head;
    //奇数个结点时:fastNode->next最后为NULL, 偶数时:fastNode最后为NULL
    while(fastNode != NULL && fastNode->next!= NULL){
        fastNode = fastNode->next->next;
        slowNode = slowNode->next;
    }
    
    //开始对比
    ListNode *left = head, *right = reverseList(slowNode);
    ListNode *newHead = right;

    bool flag = true;
    while(right != NULL){
        if(left->val != right->val) {
            flag = false;
            break;
        }
        left = left->next;
        right = right->next;
    }
    //恢复链表
    slowNode->next = reverseList(newHead);
    return flag;
}

int main()
{
    int a[] = {1,2,3};

    ListNode* head = head->creatlistR(a, sizeof(a)/sizeof(int));
    cout << "head->print() - begin - ";
    head->print();

    if(head->isPalindrome(head)) cout << "isPalindrome" << endl;
    else cout << "Not Palindrome" << endl;

    return 0;
}

[运行结果-好像没问题?]

     ~~~~          ~~~    Vscode上的运行结果如下图所示,依次判断了1->2->3->Ø,1->2->3->2->1->Ø,两个链表的回文情况。从返回的布尔值以及打印情况来看,代码似乎没什么问题,但果真如此吗?
运行结果
     ~~~~          ~~~    当笔者将核心代码提交至LC平台上时,在运行测试用例时直接显示当前代码执行出错(heap-use-after-free on address…),错误代码提示当前程序使用了已经释放的堆空间。What?可自己的代码中并没有delete, free()以及析构函数相关的内容出现,怎么就出现了堆空间释放的相关提示呢?这恐怕是许多与我一样刚接触LC这类OJ做题网站的小白都会提出疑问吧!
LC提交情况

[问题的源头-Bug?]

     ~~~~          ~~~    在上面的解题思路中,有提到step4-恢复链表这一步骤。对于本题来说该步骤其实可以忽略,可从工程的角度来考虑,我们并不希望修改链表的结构,以影响其他用户的使用。
     ~~~~          ~~~    故 bool isPalindrome(ListNode* head)函数中,语句slowNode->next = reverseList(newHead);就是不可或缺的(实际上,该语句这样写是存在问题的,具体的原因请继续往下阅读吧,哈哈~~)。在恢复链表的过程中,最容易引入的一个bug人为地给链表制造一个环

[原因探究]

     ~~~~          ~~~    此处以链表:1->2->3->Ø为例,说明上面的代码在恢复链表时是如何制造了一个环,以及为何LC测试时出现heap-use-after-free on address…提示的原因。
初始状态
在这里插入图片描述
在这里插入图片描述
Bug1
在这里插入图片描述
     ~~~~          ~~~    上述图例完整的演示了链表变化情况,尤其是最后两张图展示了如何形成的环,以及内存泄漏问题。那这又与heap-use-after-free on address…这一提示有什么关系呢?需要知道的是:一般OJ平台所提供的数据输入和输出,交由OJ自动创建和自动释放。也就是说,测试时的链表数据是由OJ所提供的,测试完毕后OJ会释放该链表,但若coder恢复链表错误(形成了环),则也就无法通过正常的遍历来释放链表(正好也解释了通过正常遍历释放链表结点时,会导致重复释放堆空间,即上面的错误代码提示)。

[如何修改?]

此处对isPalindrome函数做了适当修改,具体如下:

bool ListNode::isPalindrome(ListNode* head)
{
    if(head == NULL || head->next == NULL) return true;
    
    //快慢指针找链表的中点  
    ListNode *fastNode = head;
    ListNode *slowNode = head;
    //奇数个结点时:fastNode->next最后为NULL, 偶数时:fastNode最后为NULL
    while(fastNode != NULL && fastNode->next!= NULL){
        fastNode = fastNode->next->next;
        slowNode = slowNode->next;
    }
    
    //开始对比
    ListNode *left = head, *right = reverseList(slowNode);
    ListNode *newHead = right;

    cout << "left->print(): ";
    left->print();
    cout << "right->print(): ";
    right->print();

    bool flag = true;
    while(right != NULL){
        if(left->val != right->val) {
            flag = false;
            break;
        }
        left = left->next;
        right = right->next;
    }
    //恢复链表
    slowNode->next = reverseList(newHead)->next;
    cout << "head->print() - reset - ";
    head->print();

    return flag;
}

[正确的运行结果]
在这里插入图片描述
参阅资料:
LeetCode题解
天勤408数据结构
Python数据结构与算法分析

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值