1.分析题目
原题目:Given a singly linked list, determine if it is a palindrome.
题意非常明确,即判断一个单项链表是否为回文。比如像“1 2 3 2 1”、 “1 2 2 1”为回文;而“1 2 3 4”、 “1 2 2 3”均不是回文。
2.寻找题目切入点
2-1:考虑特殊案例
在理解题意后,我们普遍都可以想出大致的思路。但是一般在下手之前,我们需要先多考虑些“特别的输入”。这样做,一来我们能够保障程序的准确性,二来我们有时能够大幅降低debug耗时(有时候案例过不去往往在于忽略了边界输入)。
所以,在本题中,主要有两种特殊输入。一是空表, 二是长度为1的链表。在我们编写solution时,我们可以先把特殊情况放在最前面。代码如图所示:
//先考虑特殊案例
//特例1:空链表
if(head == NULL){
return true;
}
//特例2:长度为1的链表
if (head->next == NULL) {
return true;
}
2-2: 算法的优化
其实本题目有许多算法可用,毕竟题意是归类为“Easy”难度。
最简单的想法是我们可以把链表一次遍历过去后存入一个数组(或者是栈),然后通过对数组的操作进行判断。这种方法投机取巧地避免了单项链表只能单向遍历的局限。然而这浪费了空间,此方法空间复杂的为O(n)。
而原题的follow up要求我们空间复杂度为O(1),因此上述方案不够精简。
那么我们可否节省空间呢?答案是显然的。
为了使得空间复杂度为O(1),我们必须要在遍历链表的时候同时完成判断。然而我们的链表是单向的,那么该如何做呢?
也许有些人对单向的理解就是链表只能向一个方向遍历,但事实并非那么局限,单向应该意味“每个节点只有一个后继节点”。所以,我们可以考虑,将后半部分的节点的next指向前一个节点,使得单向链表变为半个“双向链表”。
比如对于“1 2 3 2 1”, 我们访问只能从1访问到5,即 1 -> 2 -> 3 -> 2 -> 1。 而根据上面的思路,我们可以将其变为:1 -> 2 -> 3 2 <- 1(我们只需考虑后面length / 2 个节点的方向改变即可)
大体实现可以分为四步:
1. 遍历列表求得链表长度,获取尾节点
//遍历链表以便确定总长度
CurrentNode = head;
while (CurrentNode != NULL) {
ListLength++;
//记录尾节点
if (CurrentNode->next == NULL) {
Tail = CurrentNode;
}
CurrentNode = CurrentNode->next;
}
2.遍历链表获得中间节点地址
Middle = ListLength / 2 + 1;
int i = 1;
MiddleNode = head;
//寻找中间节点
while (i != Middle) {
MiddleNode = MiddleNode->next;
i++;
}
3.遍历中间节点以后的节点以便改变next方向
//改变从中间开始的节点next指针,使其反向链接
CurrentNode = MiddleNode;
PreviousNode = MiddleNode;
while (CurrentNode != NULL) {
TempNode = CurrentNode->next;
CurrentNode->next = PreviousNode;
PreviousNode = CurrentNode;
CurrentNode = TempNode;
}
4.从链表两端向中间遍历以判断回文
//从链表两端向中间遍历节点
i = 1;
CurrentNode = head;
CurrentNode2 = Tail;
while (i <= ListLength / 2) {
//寻找到不匹配点直接返回false
if (CurrentNode->val != CurrentNode2->val) {
return false;
}
CurrentNode = CurrentNode->next;
CurrentNode2 = CurrentNode2->next;
i++;
}
3.题目收获
1.我们在下手之前需要养成“特殊输入优先”的习惯,一来保证程序的正确性,二来降低调试耗时。
2.对于算法的优化,我们可以通过“是否做了重复性工作”的思想来降低时间复杂度和空间复杂度,从而对算法进行优化。
3.对于对象中只需内部需要的变(常)量,我们可以列为private,避免外部意外访问。如图所示:
private:
int ListLength = 0;//记录链表长度
ListNode *CurrentNode;//记录当前指针
ListNode *CurrentNode2;//记录当前指针
ListNode *MiddleNode;//若长度为奇数,则为正中间节点;若为偶数,则为第n/2+1个节点
ListNode *TempNode;//以便反向链接用
ListNode *PreviousNode;//以便反向链表使用
ListNode *Tail;//记录尾节点
int Middle;//以便记录中央节点