前言:个人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做题网站的小白都会提出疑问吧!
[问题的源头-Bug?]
~~~~
~~~
在上面的解题思路中,有提到step4-恢复链表这一步骤。对于本题来说该步骤其实可以忽略,可从工程的角度来考虑,我们并不希望修改链表的结构,以影响其他用户的使用。
~~~~
~~~
故 bool isPalindrome(ListNode* head)函数中,语句slowNode->next = reverseList(newHead);
就是不可或缺的(实际上,该语句这样写是存在问题的,具体的原因请继续往下阅读吧,哈哈~~)。在恢复链表的过程中,最容易引入的一个bug是人为地给链表制造一个环。
[原因探究]
~~~~
~~~
此处以链表:1->2->3->Ø为例,说明上面的代码在恢复链表时是如何制造了一个环,以及为何LC测试时出现heap-use-after-free on address…提示的原因。
~~~~
~~~
上述图例完整的演示了链表变化情况,尤其是最后两张图展示了如何形成的环,以及内存泄漏问题。那这又与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数据结构与算法分析