不懂一点编程。
读题
给你一个单链表的头节点 ListNode* head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
有链表节点定义:
| 12
 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,之后再进行正反向遍历。
| 12
 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 时停止。
递归好像和循环不一样,没办法中途停止?
| 12
 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 了。
修修补补之后的完整代码:
| 12
 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。
| 12
 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 是可变长的。
官方代码如下:
| 12
 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;
 }
 };
 
 | 
赏心悦目,简单高效。
方法二:递归
官方题解说:
| 12
 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) 复杂度,不过多了很多多余的操作,导致时间空间上都远远多于官方写法。
有代码:
| 12
 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) 复杂度的神奇方法。具体思路是反转后半截链表,对比前后链表。步骤是:
- 找到前半部分链表的尾节点。
- 反转后半部分链表。
- 判断是否回文。
- 恢复链表。
- 返回结果。
其中第一步可以顺带统计链表长度,因此可以得知链表中点位置;不过,题解中使用的是快慢指针,快指针走两步,慢指针走一步,于是快指针走到尾的时候,慢指针恰好到达中点。于是有代码:
| 12
 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 的固定长度问题
- 快慢指针找中点