本文首發自公衆號「承香墨影(ID:cxmyDev)」,歡迎關注。
我又來說鏈表題了,這道題聽說是來自字節跳動的面試題。java
爲何說是「聽說」呢?由於我也是看來的,以爲題目仍是挺有意思,可是原做者給出的方案,我想了想以爲還有優化空間,就單獨拿出來說講。面試
就像本文的題目說的,這是一道關於鏈表翻轉的題。鏈表的翻轉,以前的文章也講了不少,例如:鏈表翻轉、鏈表兩兩翻轉、K 個一組翻轉鏈表。這些其實都是 leetcode 上的標準題,可是一般企業給出的面試題,多半會作一些變種,也就是加一些特殊的條件。算法
例現在天要講的這道題。函數
給定單鏈表的頭結點 head,實現一個調整鏈表的函數,從鏈表尾部開始,以 K 個結點爲一組進行逆序翻轉,頭部剩餘結點不足一組時,不須要翻轉。(不能使用隊列或者棧做爲輔助)學習
仔細讀題,像不像咱們以前講到的 leetcode 第 25 題:K 個一組翻轉鏈表。優化
leetcode-25 是從頭結點開始,以 K 個結點一組進行翻轉。而字節跳動這道題,是從尾結點開始。spa
只是多了一個從尾結點開始分組翻轉的條件,這道題的難度就增長了。code
前面也說到,這道題是我看來的,當時是以一篇文章的形式發佈出來。blog
文章我就不發了,不過先了解一下他的解題思路,有助於咱們思考。遞歸
他的思路很清晰,雖然這道題他不會解,可是 leetcode-25 這個標準的以 K 個一組翻轉鏈表的題他很熟悉。
那麼能夠先將原始鏈表,進行一次「鏈表翻轉」,再進行「K 個一組翻轉鏈表」,最後再作一次「鏈表翻轉」還原鏈表,就得出了須要的結果。
ListNode revserseKGroupPlus(ListNode head, int k) { // 翻轉鏈表 head = reverseList(head); // K 個一組翻轉鏈表 head = reverseKGroup(head, k); // 翻轉鏈表 head = reverseList(head); return head; }
把一個不熟悉的問題,通過簡單的轉換,變成熟悉的問題進行解決,這種思路是沒有錯的。
可是呢,有個問題--
在面試的場景中,一般來講,面試官的水平會高於面試者,那麼咱們能夠簡單的理解,面試就是一個不斷受挫的過程,這個過程總會被問到咱們知識的邊界纔會中止。
面試題只是起點,面試過程當中深挖的哪些問題,纔是觸摸到咱們談薪資本的核心。固然這扯遠了,繼續回到本文的內容。
此時就算面試者當場寫出瞭解題代碼,也逃不開一個經典問題。
面試官:「還有更優的方案嗎?」
那麼這道題,有沒有更優的方案?答案固然是有的。
將鏈表先翻轉後處理,再翻轉回去,這樣並不優雅,其實只需一次以 K 個一組翻轉鏈表就能夠。
再回憶一下 leetcode 第 25 題,它和這道題的差別,主要來自於,對不足一組的鏈表結點的處理。leetcode-25 是從頭結點開始處理,因此多出來的結點會在尾部,而字節跳動這道題則正好相反,餘下的結點會在頭部。
可是它們同時也有一種特殊狀況,就是 K 個一組進行分組時,這裏的 K 正好能夠完整的分組,一個很少,一個很多的分紅 N 組。
當鏈表結點數量正好爲 K * N 時,那麼又回到了咱們熟悉的 leetcode-25 題了。
若是咱們先將原始結點進行處理,找出它正好能夠整除 K 的起始結點 offset,將這個起始結點 offset 的子鏈表,再進行 K 個一組進行翻轉鏈表,最後把它拼接回原始鏈表,就完成了這道題。
這個過程,須要額外定義兩個結點,第一個知足 K 個分組條件的 offset 結點,以及 offset 的前驅結點 prev 結點,prev 結點主要是用來拼接翻轉後的兩個鏈表,讓其不會出現鏈表斷裂的問題。
它們的關係以下:
這其中還涉及到一些簡單的鏈表運算,例如求鏈表的長度,這裏就不展開說了,直接上核心代碼,邏輯都在註釋裏,咱們先定義一個 reverseKGroupPlus()
方法。
public ListNode reverseKGroupPlus(ListNode head, int k) { if (head == null || k <= 1) return head; // 計算原始鏈表長度 int length = linkedLength(head); if (length < k) return head; // 計算 offset int offsetIndex = length % k; // 原始鏈表正好能夠由 K 分爲 N 組,可直接處理 if (offsetIndex == 0) { return reverseKGroup(head, k); } // 定義並找到 prev 和 offset ListNode prev = head, offset = head; while (offsetIndex > 0) { prev = offset; offset = offset.next; offsetIndex--; } // 將 offset 結點爲起始的子鏈表進行翻轉,再拼接回主鏈表 prev.next = reverseKGroup(offset, k); return head; }
注意當鏈表長度正好能夠用 K 分爲 N 組時,咱們直接處理,否者才須要後續複雜的邏輯。
代碼的註釋足夠清晰了,在腦子裏過一遍代碼的執行流程應該能明白,爲了幫助你們理解,我又畫了個示意圖。
假設以 head 爲頭結點的鏈表長度是 10,K 爲 4 時,那麼計算下來 offset Index 就是 2。
找到 prev 和 offset 結點後,就能夠將以 offset 結點爲頭結點的子鏈表,進行 K 個一組翻轉鏈表的操做了。
此時,head 結點爲起始的鏈表,就是咱們計算後的結果。
這道題,還涉及到不少其餘的小算法,自己 leetcode-25 就已經被定級爲「困難」,字節跳動在這道題的基礎上,又增長了難度。
爲了保證解題的完整,這裏再補充一些相關代碼。
1. 計算鏈表長度
private int linkedLength(ListNode head) { int count = 0; while (head != null) { count++; head = head.next; } return count; }
沒什麼好說的,一個 while 循環搞定。
2. 以 K 個一組翻轉鏈表
這道題在以前的文章中詳細講解了,這裏直接貼代碼了。
public ListNode reverseKGroup(ListNode head, int k) { // 增長虛擬頭結點 ListNode dummy = new ListNode(0); dummy.next = head; // 定義 prev 和 end 結點 ListNode prev = dummy; ListNode end = dummy; while(end.next != null) { // 以 k 個結點爲條件,分組子鏈表 for (int i = 0; i < k && end != null; i++) end = end.next; // 不足 K 個時不處理 if (end == null) break; // 處理子鏈表 ListNode start = prev.next; ListNode next = end.next; end.next = null; // 翻轉子鏈表 prev.next = reverseList(start); // 將子連表先後串起來 start.next = next; prev = start; end = prev; } return dummy.next; } // 遞歸完成單鏈表翻轉 private ListNode reverseList(ListNode head) { if (head == null || head.next == null) return head; ListNode p = reverseList(head.next); head.next.next = head; head.next = null; return p; }
對於 leetcode-25 這道題,還不太瞭解的能夠看看以前的文章《K 個一組翻轉鏈表》。
以上就是我解這道題的思路,可能不是最高效的,但也算是比較清晰。
在面試過程當中,鏈表相關的題目能夠說是高頻題。雖然企業在出題時,爲了增長難度也會作一些變種,可是做爲面試者,不管如何都避不開多練多寫多想。
你有更好的方案嗎?你在面試中有碰到什麼奇葩的算法題嗎?歡迎在留言區討論。
本文對你有幫助嗎?留言、轉發、收藏是最大的支持,謝謝!
公衆號後臺回覆成長『 成長』,將會獲得我準備的學習資料,也能回覆『 加羣』,一塊兒學習進步。