我們之前有兩篇文章寫了迴文串和迴文序列相關的問題。
尋找迴文串的核心思想是從中心向兩端擴展:
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
。
PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,全部發布在 labuladong的算法小抄,持續更新。建議收藏,按照我的文章順序刷題,掌握各種算法套路後投再入題海就如魚得水了。
而判斷一個字符串是不是迴文串就簡單很多,不需要考慮奇偶情況,只需要「雙指針技巧」,從兩端向中間逼近即可:
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;
}
以上代碼很好理解吧,因爲迴文串是對稱的,所以正着讀和倒着讀應該是一樣的,這一特點是解決迴文串問題的關鍵。
下面擴展這一最簡單的情況,來解決:如何判斷一個「單鏈表」是不是迴文。
一、判斷迴文單鏈表
輸入一個單鏈表的頭結點,判斷這個鏈表中的數字是不是迴文:
/**
* 單鏈表節點的定義:
* public class ListNode {
* int val;
* ListNode next;
* }
*/
boolean isPalindrome(ListNode head);
輸入: 1->2->null
輸出: false
輸入: 1->2->2->1->null
輸出: true
這道題的關鍵在於,單鏈表無法倒着遍歷,無法使用雙指針技巧。那麼最簡單的辦法就是,把原始鏈表反轉存入一條新的鏈表,然後比較這兩條鏈表是否相同。關於如何反轉鏈表,可以參見前文「遞歸操作鏈表」。
其實,藉助二叉樹後序遍歷的思路,不需要顯式反轉原始鏈表也可以倒序遍歷鏈表,下面來具體聊聊。
對於二叉樹的幾種遍歷方式,我們再熟悉不過了:
void traverse(TreeNode root) {
// 前序遍歷代碼
traverse(root.left);
// 中序遍歷代碼
traverse(root.right);
// 後序遍歷代碼
}
在「學習數據結構的框架思維」中說過,鏈表兼具遞歸結構,樹結構不過是鏈表的衍生。那麼,鏈表其實也可以有前序遍歷和後序遍歷:
void traverse(ListNode head) {
// 前序遍歷代碼
traverse(head.next);
// 後序遍歷代碼
}
這個框架有什麼指導意義呢?如果我想正序打印鏈表中的val
值,可以在前序遍歷位置寫代碼;反之,如果想倒序遍歷鏈表,就可以在後序遍歷位置操作:
/* 倒序打印單鏈表中的元素值 */
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)。下面我們想想,能不能不用額外的空間,解決這個問題呢?
二、優化空間複雜度
更好的思路是這樣的:
1、先通過「雙指針技巧」中的快慢指針來找到鏈表的中點:
ListNode slow, fast;
slow = fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// slow 指針現在指向鏈表中點
2、如果fast
指針沒有指向null
,說明鏈表長度爲奇數,slow
還要再前進一步:
if (fast != null)
slow = slow.next;
3、從slow
開始反轉後面的鏈表,現在就可以開始比較迴文串了:
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
函數很容易實現:
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 之前加一段代碼即可恢復原先鏈表順序:
p.next = reverse(q);
篇幅所限,我就不寫了,讀者可以自己嘗試一下。
PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,全部發布在 labuladong的算法小抄,持續更新。建議收藏,按照我的文章順序刷題,掌握各種算法套路後投再入題海就如魚得水了。
三、最後總結
首先,尋找回文串是從中間向兩端擴展,判斷迴文串是從兩端向中間收縮。對於單鏈表,無法直接倒序遍歷,可以造一條新的反轉鏈表,可以利用鏈表的後序遍歷,也可以用棧結構倒序處理單鏈表。
具體到迴文鏈表的判斷問題,由於迴文的特殊性,可以不完全反轉鏈表,而是僅僅反轉部分鏈表,將空間複雜度降到 O(1)。