Palindrome Linked List

Question

Problem Statement

Implement a function to check if a linked list is a palindrome.

Example

Given 1->2->1, return true

Challenge

Could you do it in O(n) time and O(1) space?

题解1 - 使用辅助栈

根据栈的特性(FILO),可以首先遍历链表并入栈(最后访问栈时则反过来了),随后再次遍历链表并比较当前节点和栈顶元素,若比较结果完全相同则为回文。 又根据回文的特性,实际上还可以只遍历链表前半部分节点,再用栈中的元素和后半部分元素进行比较,分链表节点个数为奇数或者偶数考虑即可。由于链表长度未知,因此可以考虑使用快慢指针求得。

Python

  1. ## Definition for singly-linked list
  2. # class ListNode:
  3. # def __init__(self, val):
  4. # self.val = val
  5. # self.next = None
  6. class Solution:
  7. # @param head, a ListNode
  8. # @return a boolean
  9. def is_palindrome(self, head):
  10. if not head or not head.next:
  11. return True
  12. stack = []
  13. slow, fast = head, head.next
  14. while fast and fast.next:
  15. stack.append(slow.val)
  16. slow = slow.next
  17. fast = fast.next.next
  18. # for even numbers add mid
  19. if fast:
  20. stack.append(slow.val)
  21. curt = slow.next
  22. while curt:
  23. if curt.val != stack.pop():
  24. return False
  25. curt = curt.next
  26. return True

源码分析

注意, 在python code中, slow 和 fast pointer 分别指向head 和head.next。 这样指向的好处是:当linked-list 有奇数个数字的时候, 最终位置,slow会停在mid的位置, 而fast指向空。 当linked-list有偶数个node时, 最终位置,slow和slow.next为中间的两个元素, fast指向最后一个node。所以slow的最终位置总是mid 或者mid 偏左一点的位置。这样的位置非常方便分割linked-list,以及其他计算。推荐采用这种方法来寻找linked-list的mid位置。模版优势,请见solution2。

Java

  1. /**
  2. * Definition for singly-linked list.
  3. * public class ListNode {
  4. * int val;
  5. * ListNode next;
  6. * ListNode(int x) { val = x; }
  7. * }
  8. */
  9. public class Solution {
  10. /**
  11. * @param head a ListNode
  12. * @return a boolean
  13. */
  14. public boolean isPalindrome(ListNode head) {
  15. if (head == null || head.next == null) return true;
  16. Deque<Integer> stack = new ArrayDeque<Integer>();
  17. // find middle
  18. ListNode slow = head, fast = head;
  19. while (fast != null && fast.next != null) {
  20. stack.push(slow.val);
  21. slow = slow.next;
  22. fast = fast.next.next;
  23. }
  24. // skip mid node if the number of ListNode is odd
  25. if (fast != null) slow = slow.next;
  26. ListNode rCurr = slow;
  27. while (rCurr != null) {
  28. if (rCurr.val != stack.pop()) return false;
  29. rCurr = rCurr.next;
  30. }
  31. return true;
  32. }
  33. }

源码分析

注意区分好链表中个数为奇数还是偶数就好了,举几个简单例子辅助分析。

复杂度分析

使用了栈作为辅助空间,空间复杂度为 O(\frac{1}{2}n), 分别遍历链表的前半部分和后半部分,时间复杂度为 O(n).

题解2 - 原地翻转

题解 1 的解法使用了辅助空间,在可以改变原来的链表的基础上,可使用原地翻转,思路为翻转前半部分,然后迭代比较。具体可分为以下四个步骤。

  1. 找中点。
  2. 翻转链表的后半部分。
  3. 逐个比较前后部分节点值。
  4. 链表复原,翻转后半部分链表。

Python

  1. # class ListNode:
  2. # def __init__(self, val):
  3. # self.val = val
  4. # self.next = None
  5. class Solution:
  6. def is_palindrome(self, head):
  7. if not head or not head.next:
  8. return True
  9. slow, fast = head, head.next
  10. while fast and fast.next:
  11. fast = fast.next.next
  12. slow = slow.next
  13. mid = slow.next
  14. # break
  15. slow.next = None
  16. rhead = self.reverse(mid)
  17. while rhead:
  18. if rhead.val != head.val:
  19. return False
  20. rhead = rhead.next
  21. head = head.next
  22. return True
  23. def reverse(self, head):
  24. dummy = ListNode(-1)
  25. while head:
  26. temp = head.next
  27. head.next = dummy.next
  28. dummy.next = head
  29. head = temp
  30. return dummy.next

源码分析

对比Java code, 会发现,把slow 和fast pointer 放在head和head.next减少了对odd 或者even number的判断。因为slow总是在mid的位置或者mid偏左的位置上, 所以把mid assign 为slow.next总是对的。

Java

  1. /**
  2. * Definition for singly-linked list.
  3. * public class ListNode {
  4. * int val;
  5. * ListNode next;
  6. * ListNode(int x) { val = x; }
  7. * }
  8. */
  9. public class Solution {
  10. /**
  11. * @param head a ListNode
  12. * @return a boolean
  13. */
  14. public boolean isPalindrome(ListNode head) {
  15. if (head == null || head.next == null) return true;
  16. // find middle
  17. ListNode slow = head, fast = head;
  18. while (fast != null && fast.next != null) {
  19. slow = slow.next;
  20. fast = fast.next.next;
  21. }
  22. // skip mid node if the number of ListNode is odd
  23. if (fast != null) slow = slow.next;
  24. // reverse right part of List
  25. ListNode rHead = reverse(slow);
  26. ListNode lCurr = head, rCurr = rHead;
  27. while (rCurr != null) {
  28. if (rCurr.val != lCurr.val) {
  29. reverse(rHead);
  30. return false;
  31. }
  32. lCurr = lCurr.next;
  33. rCurr = rCurr.next;
  34. }
  35. // recover right part of List
  36. reverse(rHead);
  37. return true;
  38. }
  39. private ListNode reverse(ListNode head) {
  40. ListNode prev = null;
  41. while (head != null) {
  42. ListNode after = head.next;
  43. head.next = prev;
  44. prev = head;
  45. head = after;
  46. }
  47. return prev;
  48. }
  49. }

C++

  1. class Solution {
  2. public:
  3. bool isPalindrome(ListNode* head) {
  4. if (!head || !head->next) return true;
  5. // find middle
  6. ListNode* slow = head, *fast = head;
  7. while (fast && fast->next) {
  8. slow = slow->next;
  9. fast = fast->next->next;
  10. }
  11. // skip mid node if the number of ListNode is odd
  12. if (fast) slow = slow->next;
  13. // reverse right part of List
  14. ListNode* rHead = reverse(slow);
  15. ListNode* lCurr = head, *rCurr = rHead;
  16. while (rCurr) {
  17. if (rCurr->val != lCurr->val) {
  18. reverse(rHead);
  19. return false;
  20. }
  21. lCurr = lCurr->next;
  22. rCurr = rCurr->next;
  23. }
  24. // recover right part of List
  25. reverse(rHead);
  26. return true;
  27. }
  28. ListNode* reverse(ListNode* head) {
  29. ListNode* prev = NULL;
  30. while (head) {
  31. ListNode* after = head->next;
  32. head->next = prev;
  33. prev = head;
  34. head = after;
  35. }
  36. return prev;
  37. }
  38. }

源码分析

连续翻转两次右半部分链表即可复原原链表,将一些功能模块如翻转等尽量模块化。

复杂度分析

遍历链表若干次,时间复杂度近似为 O(n), 使用了几个临时遍历,空间复杂度为 O(1).

题解3 - 递归(TLE)

递归需要两个重要条件,递归步的建立和递归终止条件。对于回文比较,理所当然应该递归比较第 i 个节点和第 n-i 个节点,那么问题来了,如何构建这个递归步?大致可以猜想出来递归的传入参数应该包含两个节点,用以指代第 i 个节点和第 n-i 个节点。返回参数应该包含布尔值(用以提前返回不是回文的情况)和左半部分节点的下一个节点(用以和右半部分的节点进行比较)。由于需要返回两个值,在 Java 中需要使用自定义类进行封装,C/C++ 中则可以使用指针改变在递归调用后进行比较时节点的值。

Python

  1. class Solution:
  2. def is_palindrome(self, head):
  3. result = [head, True]
  4. self.helper(head, result)
  5. return result[1]
  6. def helper(self, right, result):
  7. if right:
  8. self.helper(right.next, result)
  9. is_pal = result[0].val == right.val and result[1]
  10. result = [result[0].next, is_pal]

Java

  1. /**
  2. * Definition for singly-linked list.
  3. * public class ListNode {
  4. * int val;
  5. * ListNode next;
  6. * ListNode(int x) { val = x; }
  7. * }
  8. */
  9. class Result {
  10. ListNode lNode;
  11. boolean isP;
  12. Result(ListNode node, boolean isP) {
  13. this.lNode = node;
  14. this.isP = isP;
  15. }
  16. }
  17. public class Solution {
  18. /**
  19. * @param head a ListNode
  20. * @return a boolean
  21. */
  22. public boolean isPalindrome(ListNode head) {
  23. Result result = new Result(head, true);
  24. helper(head, result);
  25. return result.isP;
  26. }
  27. private void helper(ListNode right, Result result) {
  28. if (right != null) {
  29. helper(right.next, result);
  30. boolean equal = (result.lNode.val == right.val);
  31. result.isP = equal && result.isP;
  32. result.lNode = result.lNode.next;
  33. }
  34. }
  35. }

源码分析

核心代码为如何在递归中推进左半部分节点而对右半部分使用栈的方式逆向获取节点。左半部分的推进需要借助辅助数据结构Result.

复杂度分析

递归调用 n 层,时间复杂度近似为 O(n), 使用了几个临时变量,空间复杂度为 O(1).

Bonus - Fancy Python Solution

  1. class Solution:
  2. def is_palindrome(self, head):
  3. nodes = []
  4. while head:
  5. nodes.append(head.val)
  6. head = head.next
  7. return nodes == nodes[::-1]

源码分析

将linked-list问题,转化成判断一个array是否为palindrome的问题。

复杂度分析

时间复杂度 O(n), 空间复杂度也是 O(n)

Reference