一. 序
鏈表作爲一種基本的數據結構,本身理解起來,很簡單。它通過指針或者叫引用,將一組零散的內存空間(結點),串聯起來組成一個數據存儲結構。
鏈表根據其指針的指向和豐富程度,可以分爲單鏈表、雙向鏈表、循環鏈表、雙向循環鏈表。其差別就是,是否在單鏈表的基礎上爲結點,增加更豐富的指針,讓其實現更豐富的功能。
鏈表雖然很好理解,但是鏈表的代碼,寫起來卻並不是那麼容易,尤其上一些對單鏈表的操作,例如鏈表反轉、鏈表雙雙反轉、有序鏈表合併等。
你可以自己試試,放下手機拿起紙筆,來一場模擬面試,就是寫一個單鏈表兩兩反轉,看看能否一次通過。
寫鏈表代碼的時候,指針指來指去,很容易就把指針丟失,造成鏈表斷裂。所以在操作鏈表時,其操作順序就是我們着重關注的點。
雖然鏈表代碼寫起來不容易,但鏈表又是面試的常客,一些常見的算法實現,也是我們開發者必須要掌握的。
二. 單鏈表兩兩反轉
2.1 什麼是單鏈表兩兩反轉?
單鏈表反轉比較好理解,就是逆序嘛,但是兩兩反轉是什麼意思呢?
我們知道,單鏈表是由指針,將一個一個結點串聯起來的數據結構。那麼我們將這些結點,兩個爲一組,在組內進行反轉,就是兩兩反轉了。
單鏈表兩兩反轉這種題,非常適合用遞歸的思想來解決,將每一步操作都封閉在一個小單元內,然後重複操作。
通常遞歸能做的,循環也能做,所以我們就這兩種解法,分別講解。
2.2 循環解法
無論是使用循環還是遞歸,其實都是將鏈表結點交換的步驟拆解,放在一個個小循環(遞歸)中去處理。相對於遞歸,循環法在結點的使用步驟上更清晰,我們就以循環法作爲切入點。
遞歸或循環,其核心就是找到抽象模型,在每個調用步驟中,不斷重複相同的事情。
在單鏈表兩兩反轉中,看似是在處理兩個結點,但其實是在處理 4 個結點之間的關係。
除了待反轉的 A、B 兩個結點之外,還需要操作 A 的前驅結點 prev 結點和 B 的 Next 結點 b-next 結點。
我們每次反轉,其實就在操作這四個結點,其中的操作步驟很重要。
如圖所示,步驟 ① 操作有兩步操作,因爲其操作的指針互不影響,所以在寫代碼的時候不分先後,在保證 prev 指針和 b-next 指針的指向無錯後,就可以開始 A、B 結點的反轉,也就是步驟 ②。
最後我們只需要將我們關注的結點前移,就可以進入下一次循環。
在這個步驟中,我們在操作 4 個結點的指針,但是其實每次初始的結點,只有 A 的前驅結點 prev 結點。爲了保證循環內的操作一致,我們可以在鏈表前,加一個虛擬的頭結點,來輔助我們,讓代碼更簡潔。
到這裏,各個步驟就清晰了,每次反轉兩個結點,然後前移 dummy 指針。
最後,還需要再注意一些邊界條件,注意我們的循環,什麼時候停止。
在單鏈表兩兩反轉的場景下,鏈表的結點數,有單有雙,當結點數爲單數時,最後一個結點已經找不到可以反轉交換的結點了,此時保持不變即可。
接下來直接上循環代碼了,這裏使用我們熟悉的 Java 代碼來實現。
public ListNode swapPairs(ListNode head){
// 鏈表頭增加虛擬結點 dummy
ListNode dummy = new ListNode(-1);
dummy.next = head;
head = dummy;
// 循環退出條件,注意鏈表結點數單雙的情況
while(head.next != null && head.next.next != null){
// 開始反轉
ListNode a = head.next;
ListNode b = a.next;
head.next = b; // 步驟①
a.next = b.next; // 步驟①
b.next = a; // 步驟②
// dummy 指針前移
head = a;
}
return dummy.next;
}
代碼中的註釋已經很清晰了,首先在鏈表頭插入一個虛擬結點 dummy,之後開啓循環,循環退出的條件就是走到了鏈表尾部的邊界,需要注意結點數爲單、雙兩種情況。之後再按照前文中圖解的步驟,開始操作鏈表指針實現兩兩反轉,最後前移 dummy 指針。
2.3 遞歸解法
遞歸的解法,相對於循環解法,代碼量上就少很多,看着也清爽了。主要是因爲遞歸,通過一層層的調用,在方法棧上存儲了存儲了一些變量就是我們待操作的結點。
在這裏,在遞歸裏,我們依然關注三個問題,遞歸解決的小問題、終止條件以及返回值。
遞歸的解法,直接看代碼比較清晰。
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode next = head.next;
head.next = swapPairs(next.next);
next.next = head;
return next;
}
再結合之前操作步驟的圖解。
遞歸的方法比較繞,結合上圖,找到思路,循環是一次從前向後的移動操作窗口,而遞推是從後向前移動操作窗口。注意遞歸終止條件以及每次遞歸操作時,結點指針的輪轉,多想多練就清晰了。
三. 小結時刻
到這裏對單鏈表的兩兩反轉,就講解完畢。
寫鏈表代碼,除了考驗邏輯思維能力,還考驗編碼能力,多寫多練纔是核心。
注意其中的邊界條件以及每個操作單元中,結點指針的交換輪轉。這其中的每個步驟的操作順序,都通過圖解的方式講解清楚了,有疑問歡迎在留言去討論。
最後
如果你看到了這裏,覺得文章寫得不錯就給個讚唄!歡迎大家評論討論!如果你覺得那裏值得改進的,請給我留言。一定會認真查詢,修正不足,定期免費分享技術乾貨。感興趣的小夥伴可以點一下關注哦。謝謝!