不懂一点编程。
读题
给你一个单链表的头节点 ListNode* head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
有链表节点定义:
1 2 3 4 5 6 7
| struct ListNode { int val; ListNode *next; ListNode() : val(0), next(nullptr) {} ListNode(int x) : val(x), next(nullptr) {} ListNode(int x, ListNode *next) : val(x), next(next) {} };
|
有 next,有 val;没有 previous。
思路
考虑使用递归来实现遍历和反向遍历;使用一个全局的 list_length 对其进行计数;在反向遍历的时候启动下一次正向遍历,判断 val 是否相同。或者,首先计数一遍得到 list_length,之后再进行正反向遍历。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| int list_length = 0;
ListNode* cal_length(ListNode* root){ if (root == nullptr) return false; list_length++; cal_length(root->next); return NULL; }
ListNode* fun1(ListNode* root){ if (root == nullptr) return false; if () }
ListNode* fun2(ListNode* root){ }
|
难点应该在于反向遍历时如何获取正向的数据?
考虑使用哈希表存储,哈希表能将指针与数据对应起来;需要对应指针和序号;获取了指针就能得到 val,因此不需要单独对 val 做映射。使用 num 作为正向前半段的序号,num_reverse 作为后半段的序号;num 的最大值就是链表的长度了,反向遍历的时候 num_reverse 从 0 开始,对应到前半段的序号;到 num/2 时停止。
递归好像和循环不一样,没办法中途停止?
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int num = 0; int num_reverse = 0; bool ans = true; unordered_map<int, ListNode*> fa;
bool fun1(ListNode* root){ if (root == nullptr) return false; fa[num] = root; num++; ans = fun1(root->next); if (ans || (fa[num_reverse]->val != root->val)) return false; num_reverse++; return true; }
|
结果测试案例通过,提交未通过,56 / 93 个通过的测试用例,仔细看了看有几处明显错误,比如没有 num_reverse++ 就直接 return 了。
修修补补之后的完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class Solution { public: int num = 0; int num_reverse = 0; bool ans = true; unordered_map<int, ListNode*> fa;
bool fun1(ListNode* root){ if (root == nullptr) return ans; fa[num] = root; num++; ans = fun1(root->next); if (ans && (fa[num_reverse]->val == root->val)){ num_reverse++; return true; }else { num_reverse++; return false; } }
bool isPalindrome(ListNode* head) { if (head == nullptr) return false; fun1(head); return ans; } };
|
结果
执行用时 252 ms,消耗内存 199.84 MB,大爆笑了,又慢又大坨。
优化
考虑到只需要存储前半的数据即可,于是先计算链表长度;当 num 和 num_reverse 超过一半时,停止判断,直接 return ans。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| class Solution { public: int num = 0; int num_reverse = 0; bool ans = true; unordered_map<int, ListNode*> fa; int list_length = 0;
void cal_length(ListNode* root){ if (root == nullptr) return; list_length++; cal_length(root->next); }
bool fun1(ListNode* root){ if (root == nullptr) return true; if (num <= list_length/2) fa[num] = root; num++; ans = fun1(root->next); if (num_reverse > list_length/2){ num_reverse++; return ans; } if (ans && (fa[num_reverse]->val == root->val)){ num_reverse++; return true; }else { num_reverse++; return false; } }
bool isPalindrome(ListNode* head) { if (head == nullptr) return false; cal_length(head); fun1(head); return ans; } };
|
用时 141 ms,内存 163.47 MB,没好到哪去。
官方题解
方法一:将值复制到数组中后用双指针法
也就是将链表转化为列表来做;还是经典的老面孔双指针法。列表好处是可以任意访问其中指定位置元素。值得注意的是,使用的是 vector 而不是我熟悉的原味数组 array,区别于 array,vector 是可变长的。
官方代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Solution { public: bool isPalindrome(ListNode* head) { vector<int> vals; while (head != nullptr) { vals.emplace_back(head->val); head = head->next; } for (int i = 0, j = (int)vals.size() - 1; i < j; ++i, --j) { if (vals[i] != vals[j]) { return false; } } return true; } };
|
赏心悦目,简单高效。
方法二:递归
官方题解说:
1 2 3 4 5 6 7 8
| 递归为我们提供了一种优雅的方式来反向遍历节点。
function print_values_in_reverse(ListNode head) if head is NOT null print_values_in_reverse(head.next) print head.val
如果使用递归反向迭代节点,同时使用递归函数外的变量向前迭代,就可以判断链表是否为回文。
|
官方题解使用了两个指针,一内一外(而我是使用了一个指针和一个哈希表),当正向迭代完成后,自然会开始回溯/反向迭代;此时内部的指针在往回走,而外部的指针仍在 head 处正向走,于是可以轻易比较 val 的值,无需记录也无需统计长度。
值得注意的是,如果只是这样,仍然会
O(n) 时间和空间复杂度,因为递归使用了堆栈。
掐指一算,我的方法(也是递归)确实是
O(n) 复杂度,不过多了很多多余的操作,导致时间空间上都远远多于官方写法。
有代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class Solution { ListNode* frontPointer; public: bool recursivelyCheck(ListNode* currentNode) { if (currentNode != nullptr) { if (!recursivelyCheck(currentNode->next)) { return false; } if (currentNode->val != frontPointer->val) { return false; } frontPointer = frontPointer->next; } return true; } bool isPalindrome(ListNode* head) { frontPointer = head; return recursivelyCheck(head); } };
|
中间 return 减少计算量的部分我也想到了,但是远没有这么优雅。
方法三:快慢指针
可以避免 O(n) 复杂度的神奇方法。具体思路是反转后半截链表,对比前后链表。步骤是:
- 找到前半部分链表的尾节点。
- 反转后半部分链表。
- 判断是否回文。
- 恢复链表。
- 返回结果。
其中第一步可以顺带统计链表长度,因此可以得知链表中点位置;不过,题解中使用的是快慢指针,快指针走两步,慢指针走一步,于是快指针走到尾的时候,慢指针恰好到达中点。于是有代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| class Solution { public: bool isPalindrome(ListNode* head) { if (head == nullptr) { return true; }
ListNode* firstHalfEnd = endOfFirstHalf(head); ListNode* secondHalfStart = reverseList(firstHalfEnd->next);
ListNode* p1 = head; ListNode* p2 = secondHalfStart; bool result = true; while (result && p2 != nullptr) { if (p1->val != p2->val) { result = false; } p1 = p1->next; p2 = p2->next; }
firstHalfEnd->next = reverseList(secondHalfStart); return result; }
ListNode* reverseList(ListNode* head) { ListNode* prev = nullptr; ListNode* curr = head; while (curr != nullptr) { ListNode* nextTemp = curr->next; curr->next = prev; prev = curr; curr = nextTemp; } return prev; }
ListNode* endOfFirstHalf(ListNode* head) { ListNode* fast = head; ListNode* slow = head; while (fast->next != nullptr && fast->next->next != nullptr) { fast = fast->next->next; slow = slow->next; } return slow; } };
|
值得注意的是,这个方法只降低了空间复杂度,时间复杂度仍然是
O(n)。
收获
- 可以使用 vector 规避 array 的固定长度问题
- 快慢指针找中点