Luna Tech

Tutorials For Dummies.

Singly linked list(单链表)- 4

2021-07-27


1. 回文串

Reference: 如何判断回文链表 - labuladong的算法小抄 (gitbook.io)

目标

  1. 搞懂回文串(Palindrome)是什么(从前向后读和从后向前读一样的字符串)
  2. 搞懂寻找回文串和判断回文串的核心思想
  3. 知道如何在单链表结构中判断回文串
  4. 优化单链表回文串判定的空间复杂度

相关题目

234. 回文链表


2. 如何判断一个「单链表」是不是回文

思路 1 - 反转链表

由于单链表无法从后往前遍历,所以无法使用双指针。

那么如何比较呢?我们之前学了反转单链表的算法,现在我们可以结合反转来创造一条新的单链表。

这样一来,我们就拥有了一正一反两条单链表,只要比较两者是否相同即可。

思路 2 - 二叉树后序遍历

由于链表具有递归结构,所以树结构其实是链表的衍生产物。

链表可以做前序遍历和后序遍历。

/**
 * 单链表节点的定义:
 * public class ListNode {
 *     int val;
 *     ListNode next;
 * }
 */

void traverse(ListNode head) {
    // 前序遍历代码
    traverse(head.next);
    // 后序遍历代码
}

后序遍历的时候,就把代码写在 traverse 的后面,实现从后往前打印。

/* 后序打印单链表中的元素值 */
void traverse(ListNode head) {
    if (head == null) return;
    traverse(head.next);
    // 后序遍历代码
    print(head.val);
}

思路二代码

// 左侧指针
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)。


3. 优化空间复杂度

双指针技巧

1. 慢指针找到中点

先用双指针技巧中的快慢指针来找到链表的中点。

ListNode slow, fast;
slow = fast = head;
while (fast != null && fast.next != null) {
    slow = slow.next;
    fast = fast.next.next;
}
// slow 指针现在指向链表中点

2. 奇偶数

在奇数的情况下,慢指针需要再向前一步(为啥?)

if (fast != null)
    slow = slow.next;

3. 反转链表

从慢指针的位置开始进行链表反转,反转之后得到 1->2->3->5->4

然后开始比较,左node = head = 1,右node = 5

当右node不为空的时候,假如左node value 和右node value相同,那么我们继续向前移动两个node;

否则返回 false。

也就是说,假设 1->2->3->2->1,反转之后是1->2->3->1->2,比较之后发现1->2重复,继续向前时right=null(因为是奇数),但这个链表还是满足回文链表的要求,所以返回true。

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;

// 迭代法进行链表反转
ListNode reverse(ListNode head) {
    ListNode previous = null, current = head;
    while (current != null) { // previous = last node
        ListNode next = current.next; 
        current.next = previous; // 反转
        previous = current; // 向前移动
        current = next;
    }
    return previous; // previous = new head
}

如何避免破坏链表的原始结构

我们只需要知道比较结束之后左右 node 的位置,再进行一次翻转就可以了。

left.next = reverse(right);

复杂度

这种双指针法的时间复杂度 O(N),空间复杂度 O(1)。

完整版代码

class Solution {
    public boolean isPalindrome(ListNode head) {
        ListNode left = head;
        // find the middle node and reverse
        ListNode right = reverse(findMiddle(head));
        // compare left with the reversed middle node
        while (right != null && left != null){
            if (right.val != left.val)
                return false;
            right = right.next;
            left = left.next;
        }
        return true;
    }

    public ListNode findMiddle(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        while (fast != null && fast.next != null){
            slow = slow.next;
            fast = fast.next.next;
        }
        if (fast != null)
            slow = slow.next;
        return slow;
    }

    public ListNode reverse(ListNode head){
        ListNode pre = null;
        ListNode cur = head;
        while (cur != null){
            ListNode nxt = cur.next;
            cur.next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }
}

4. 结语

「寻找」回文串的思路是从中间向两边扩展;「判断」回文串的思路是从两边向中间收缩。

但是由于单链表无法从后往前遍历,我们可以采取以下几个方法:

  1. 自己造一条反转链表,然后进行比较;
  2. 利用链表的递归属性进行后序遍历,也就是用递归函数的堆栈来处理;

如何提高效率?判断回文串的时候,我们可以只反转一部分链表,这样可以把空间复杂度降到 O(1)。

PS: 参考的原文中有一些动画图可以帮助理解~