#174 translated: interview/判断回文链表.md
211
interview/check_palindromic_linkedlist.md
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
**Translator: [natsunoyoru97](https://github.com/natsunoyoru97)**
|
||||||
|
|
||||||
|
**Author: [labuladong](https://github.com/labuladong)**
|
||||||
|
|
||||||
|
There are two previous articles mentioned the problems about palindromic strings and palindromic sequences.
|
||||||
|
|
||||||
|
The core concept to **FIND** the palindromic strings is expanding from the middle to the edges:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
string palindrome(string& s, int l, int r) {
|
||||||
|
// to prevent the indexes from getting out of range
|
||||||
|
while (l >= 0 && r < s.size()
|
||||||
|
&& s[l] == s[r]) {
|
||||||
|
// expand to two edges
|
||||||
|
l--; r++;
|
||||||
|
}
|
||||||
|
// return the longest palindromic in which the middle
|
||||||
|
// are both s[l] and s[r]
|
||||||
|
return s.substr(l + 1, r - l - 1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The length of the palindromic strings can be either odd or even: when the length is odd there is only one middle pivot, and when the length is even there are two middle pivots. So the function above needs to parse the arguments `l` and `r` in.
|
||||||
|
|
||||||
|
But to **CHECK** a palindromic string is much easier. Regardless of its length, we only need to do the double pointers trick, and move from two edges to the middle:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool isPalindrome(string s) {
|
||||||
|
int left = 0, right = s.length - 1;
|
||||||
|
while (left < right) {
|
||||||
|
if (s[left] != s[right])
|
||||||
|
return false;
|
||||||
|
left++; right--;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
So the code is cleaner and much easier to understand. **The palindromic strings are SYMMETRIC so it is same to write it in normal order as in reverse order, which is the key to solve the problems of the palindromic strings.**
|
||||||
|
|
||||||
|
We make expansion from this simple scenario, and try to solve the problem: how to check a palindromic singly linked list.
|
||||||
|
|
||||||
|
### 1. Check A Palindromic Singly Linked List
|
||||||
|
|
||||||
|
Given the head node of a singly linked list, and check if the values are palindromic:
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* The definition of nodes in a singly linked list:
|
||||||
|
* public class ListNode {
|
||||||
|
* int val;
|
||||||
|
* ListNode next;
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
boolean isPalindrome(ListNode head);
|
||||||
|
|
||||||
|
Input: 1->2->null
|
||||||
|
Output: false
|
||||||
|
|
||||||
|
Input: 1->2->2->1->null
|
||||||
|
Output: true
|
||||||
|
```
|
||||||
|
|
||||||
|
The two pointers DON'T do the trick because we can't traverse a singly linked list in reverse. The most straightforward way is to store the existed linked list in a new linked list REVERSELY, then to compare whether these two linked lists are the same. If you have no idea about how to reverse a linked list, you can look at _[Reverse Part of a Linked List via Recusion](https://github.com/labuladong/fucking-algorithm/blob/english/data_structure/reverse_part_of_a_linked_list_via_recursion.md)_.
|
||||||
|
|
||||||
|
But **similar to the postorder traversal in a binary tree, we can traverse a linked list reversely without doing the actual reverse**.
|
||||||
|
|
||||||
|
We are familiar with the ways to traverse a binary tree:
|
||||||
|
|
||||||
|
```java
|
||||||
|
void traverse(TreeNode root) {
|
||||||
|
// code to traverse in preorder
|
||||||
|
traverse(root.left);
|
||||||
|
// code to traverse in inorder
|
||||||
|
traverse(root.right);
|
||||||
|
// code to traverse in postorder
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As mentioned in _The Thinking Patterns in Data Structure_, the linked list is recursive and it is the derivation of the trees ADT. Thus, **the linked list also has preorder traversal and postorder traversal**:
|
||||||
|
|
||||||
|
```java
|
||||||
|
void traverse(ListNode head) {
|
||||||
|
// code to traverse in preorder
|
||||||
|
traverse(head.next);
|
||||||
|
// code to traverse in postorder
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
How do we apply such a pattern? If I want to print the value of `val` in a linked list in normal order, I will write the code in the position of preorder traversal; Meanwhile, if I want to print the value in reverse order, I will make the code in the position of postorder traversal:
|
||||||
|
|
||||||
|
```java
|
||||||
|
/* print the values in a linked list reversely */
|
||||||
|
void traverse(ListNode head) {
|
||||||
|
if (head == null) return;
|
||||||
|
traverse(head.next);
|
||||||
|
// code to traverse in postorder
|
||||||
|
print(head.val);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We can also make a slight modification to imitate the two pointers to check the palindromes:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// The left pointer
|
||||||
|
ListNode left;
|
||||||
|
|
||||||
|
boolean isPalindrome(ListNode head) {
|
||||||
|
left = head;
|
||||||
|
return traverse(head);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean traverse(ListNode right) {
|
||||||
|
if (right == null) return true;
|
||||||
|
boolean res = traverse(right.next);
|
||||||
|
// code to traverse in postorder
|
||||||
|
res = res && (right.val == left.val);
|
||||||
|
left = left.next;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
What is the essence of this way? It is all about **pushing the nodes in the linked list into a stack and then popping them out. At this time the elements are in reverse.** What we make in use is the queues and stacks in recursion.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Of course, both the time complexity and the space complexity are O(N) no matter you construct a reversed linked list or use postorder traversal. Can we solve it without using extra spaces?
|
||||||
|
|
||||||
|
### 2. Optimizing the Space Complexity
|
||||||
|
|
||||||
|
Here is how to optimize:
|
||||||
|
|
||||||
|
**2.1 Find the node in the middle by the fast and slow pointers**:
|
||||||
|
|
||||||
|
```java
|
||||||
|
ListNode slow, fast;
|
||||||
|
slow = fast = head;
|
||||||
|
while (fast != null && fast.next != null) {
|
||||||
|
slow = slow.next;
|
||||||
|
fast = fast.next.next;
|
||||||
|
}
|
||||||
|
// the slow pointer now points to the middle point
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**2.2 If the `fast` pointer doesn't point to `null`, the length of this linked list is odd, which means the `slow` pointer needs to forward one more step**:
|
||||||
|
|
||||||
|
```java
|
||||||
|
if (fast != null)
|
||||||
|
slow = slow.next;
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**2.3 Reverse the right half of the linked list and compare palindromes**:
|
||||||
|
|
||||||
|
```java
|
||||||
|
ListNode left = head;
|
||||||
|
ListNode right = reverse(slow);
|
||||||
|
|
||||||
|
while (right != null) {
|
||||||
|
if (left.val != right.val)
|
||||||
|
return false;
|
||||||
|
left = left.next;
|
||||||
|
right = right.next;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Till now, we only need to merge these 3 parts of code to cope with this problem. The `reverse` function is easy to build:
|
||||||
|
|
||||||
|
```java
|
||||||
|
ListNode reverse(ListNode head) {
|
||||||
|
ListNode pre = null, cur = head;
|
||||||
|
while (cur != null) {
|
||||||
|
ListNode next = cur.next;
|
||||||
|
cur.next = pre;
|
||||||
|
pre = cur;
|
||||||
|
cur = next;
|
||||||
|
}
|
||||||
|
return pre;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The time complexity of this algorithm is O(N) and the space complexity is O(1), which is fully optimized.
|
||||||
|
|
||||||
|
I know some readers may ask: It is efficient but it broke the structure of the linked list. Can we remain the structure in origin?
|
||||||
|
|
||||||
|
It is easy to deal with, and the key is to get the positions of the pointers `p, q`:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
We only need to add one line before the return function to get the original order of the linked list.
|
||||||
|
|
||||||
|
```java
|
||||||
|
p.next = reverse(q);
|
||||||
|
```
|
||||||
|
|
||||||
|
To avoid being wordy, my words will stop here. Readers can have a try by yourselves.
|
||||||
|
|
||||||
|
### 3. Summing Up
|
||||||
|
|
||||||
|
First, by extending **from the middle to the two edges** to FIND the palindromic strings, meanwhile by shrinking **from the middle to the two edges** to CHECK the palindromic strings. Traversing in reverse doesn't work for a singly linked list, the two alternatives are: to construct a new reversed linked list instead, or to apply the stack ADT.
|
||||||
|
|
||||||
|
Due to the feature of palindromes (they are SYMMETRIC), we can optimize the space complexity to O(1) by only **reversing half of the linked list**.
|
@ -1,210 +0,0 @@
|
|||||||
我们之前有两篇文章写了回文串和回文序列相关的问题。
|
|
||||||
|
|
||||||
**寻找**回文串的核心思想是从中心向两端扩展:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
string palindrome(string& s, int l, int r) {
|
|
||||||
// 防止索引越界
|
|
||||||
while (l >= 0 && r < s.size()
|
|
||||||
&& s[l] == s[r]) {
|
|
||||||
// 向两边展开
|
|
||||||
l--; r++;
|
|
||||||
}
|
|
||||||
// 返回以 s[l] 和 s[r] 为中心的最长回文串
|
|
||||||
return s.substr(l + 1, r - l - 1);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
因为回文串长度可能为奇数也可能是偶数,长度为奇数时只存在一个中心点,而长度为偶数时存在两个中心点,所以上面这个函数需要传入`l`和`r`。
|
|
||||||
|
|
||||||
而**判断**一个字符串是不是回文串就简单很多,不需要考虑奇偶情况,只需要「双指针技巧」,从两端向中间逼近即可:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
bool isPalindrome(string s) {
|
|
||||||
int left = 0, right = s.length - 1;
|
|
||||||
while (left < right) {
|
|
||||||
if (s[left] != s[right])
|
|
||||||
return false;
|
|
||||||
left++; right--;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
以上代码很好理解吧,**因为回文串是对称的,所以正着读和倒着读应该是一样的,这一特点是解决回文串问题的关键**。
|
|
||||||
|
|
||||||
下面扩展这一最简单的情况,来解决:如何判断一个「单链表」是不是回文。
|
|
||||||
|
|
||||||
### 一、判断回文单链表
|
|
||||||
|
|
||||||
输入一个单链表的头结点,判断这个链表中的数字是不是回文:
|
|
||||||
|
|
||||||
```java
|
|
||||||
/**
|
|
||||||
* 单链表节点的定义:
|
|
||||||
* public class ListNode {
|
|
||||||
* int val;
|
|
||||||
* ListNode next;
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
|
|
||||||
boolean isPalindrome(ListNode head);
|
|
||||||
|
|
||||||
输入: 1->2->null
|
|
||||||
输出: false
|
|
||||||
|
|
||||||
输入: 1->2->2->1->null
|
|
||||||
输出: true
|
|
||||||
```
|
|
||||||
|
|
||||||
这道题的关键在于,单链表无法倒着遍历,无法使用双指针技巧。那么最简单的办法就是,把原始链表反转存入一条新的链表,然后比较这两条链表是否相同。关于如何反转链表,可以参见前文「递归操作链表」。
|
|
||||||
|
|
||||||
其实,**借助二叉树后序遍历的思路,不需要显式反转原始链表也可以倒序遍历链表**,下面来具体聊聊。
|
|
||||||
|
|
||||||
对于二叉树的几种遍历方式,我们再熟悉不过了:
|
|
||||||
|
|
||||||
```java
|
|
||||||
void traverse(TreeNode root) {
|
|
||||||
// 前序遍历代码
|
|
||||||
traverse(root.left);
|
|
||||||
// 中序遍历代码
|
|
||||||
traverse(root.right);
|
|
||||||
// 后序遍历代码
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
在「学习数据结构的框架思维」中说过,链表兼具递归结构,树结构不过是链表的衍生。那么,**链表其实也可以有前序遍历和后序遍历**:
|
|
||||||
|
|
||||||
```java
|
|
||||||
void traverse(ListNode head) {
|
|
||||||
// 前序遍历代码
|
|
||||||
traverse(head.next);
|
|
||||||
// 后序遍历代码
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
这个框架有什么指导意义呢?如果我想正序打印链表中的`val`值,可以在前序遍历位置写代码;反之,如果想倒序遍历链表,就可以在后序遍历位置操作:
|
|
||||||
|
|
||||||
```java
|
|
||||||
/* 倒序打印单链表中的元素值 */
|
|
||||||
void traverse(ListNode head) {
|
|
||||||
if (head == null) return;
|
|
||||||
traverse(head.next);
|
|
||||||
// 后序遍历代码
|
|
||||||
print(head.val);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
说到这了,其实可以稍作修改,模仿双指针实现回文判断的功能:
|
|
||||||
|
|
||||||
```java
|
|
||||||
// 左侧指针
|
|
||||||
ListNode left;
|
|
||||||
|
|
||||||
boolean isPalindrome(ListNode head) {
|
|
||||||
left = head;
|
|
||||||
return traverse(head);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean traverse(ListNode right) {
|
|
||||||
if (right == null) return true;
|
|
||||||
boolean res = traverse(right.next);
|
|
||||||
// 后序遍历代码
|
|
||||||
res = res && (right.val == left.val);
|
|
||||||
left = left.next;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
这么做的核心逻辑是什么呢?**实际上就是把链表节点放入一个栈,然后再拿出来,这时候元素顺序就是反的**,只不过我们利用的是递归函数的堆栈而已。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
当然,无论造一条反转链表还是利用后续遍历,算法的时间和空间复杂度都是 O(N)。下面我们想想,能不能不用额外的空间,解决这个问题呢?
|
|
||||||
|
|
||||||
### 二、优化空间复杂度
|
|
||||||
|
|
||||||
更好的思路是这样的:
|
|
||||||
|
|
||||||
**1、先通过「双指针技巧」中的快慢指针来找到链表的中点**:
|
|
||||||
|
|
||||||
```java
|
|
||||||
ListNode slow, fast;
|
|
||||||
slow = fast = head;
|
|
||||||
while (fast != null && fast.next != null) {
|
|
||||||
slow = slow.next;
|
|
||||||
fast = fast.next.next;
|
|
||||||
}
|
|
||||||
// slow 指针现在指向链表中点
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
**2、如果`fast`指针没有指向`null`,说明链表长度为奇数,`slow`还要再前进一步**:
|
|
||||||
|
|
||||||
```java
|
|
||||||
if (fast != null)
|
|
||||||
slow = slow.next;
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
**3、从`slow`开始反转后面的链表,现在就可以开始比较回文串了**:
|
|
||||||
|
|
||||||
```java
|
|
||||||
ListNode left = head;
|
|
||||||
ListNode right = reverse(slow);
|
|
||||||
|
|
||||||
while (right != null) {
|
|
||||||
if (left.val != right.val)
|
|
||||||
return false;
|
|
||||||
left = left.next;
|
|
||||||
right = right.next;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
至此,把上面 3 段代码合在一起就高效地解决这个问题了,其中`reverse`函数很容易实现:
|
|
||||||
|
|
||||||
```java
|
|
||||||
ListNode reverse(ListNode head) {
|
|
||||||
ListNode pre = null, cur = head;
|
|
||||||
while (cur != null) {
|
|
||||||
ListNode next = cur.next;
|
|
||||||
cur.next = pre;
|
|
||||||
pre = cur;
|
|
||||||
cur = next;
|
|
||||||
}
|
|
||||||
return pre;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
算法总体的时间复杂度 O(N),空间复杂度 O(1),已经是最优的了。
|
|
||||||
|
|
||||||
我知道肯定有读者会问:这种解法虽然高效,但破坏了输入链表的原始结构,能不能避免这个瑕疵呢?
|
|
||||||
|
|
||||||
其实这个问题很好解决,关键在于得到`p, q`这两个指针位置:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
这样,只要在函数 return 之前加一段代码即可恢复原先链表顺序:
|
|
||||||
|
|
||||||
```java
|
|
||||||
p.next = reverse(q);
|
|
||||||
```
|
|
||||||
|
|
||||||
篇幅所限,我就不写了,读者可以自己尝试一下。
|
|
||||||
|
|
||||||
### 三、最后总结
|
|
||||||
|
|
||||||
首先,寻找回文串是从中间向两端扩展,判断回文串是从两端向中间收缩。对于单链表,无法直接倒序遍历,可以造一条新的反转链表,可以利用链表的后序遍历,也可以用栈结构倒序处理单链表。
|
|
||||||
|
|
||||||
具体到回文链表的判断问题,由于回文的特殊性,可以不完全反转链表,而是仅仅反转部分链表,将空间复杂度降到 O(1)。
|
|
||||||
|
|
||||||
坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章:
|
|
||||||
|
|
||||||

|
|
Before Width: | Height: | Size: 473 KiB After Width: | Height: | Size: 473 KiB |
BIN
pictures/palindromic_linkedlist/1.jpg
Normal file
After Width: | Height: | Size: 291 KiB |
BIN
pictures/palindromic_linkedlist/2.jpg
Normal file
After Width: | Height: | Size: 297 KiB |
BIN
pictures/palindromic_linkedlist/3.jpg
Normal file
After Width: | Height: | Size: 290 KiB |
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 148 KiB |
Before Width: | Height: | Size: 169 KiB |
Before Width: | Height: | Size: 171 KiB |
Before Width: | Height: | Size: 168 KiB |