圖解算法:單鏈表兩兩反轉 | 眼睛會了手就會系列

一. 序

鏈表作爲一種基本的數據結構,本身理解起來,很簡單。它通過指針或者叫引用,將一組零散的內存空間(結點),串聯起來組成一個數據存儲結構。

 

鏈表根據其指針的指向和豐富程度,可以分爲單鏈表、雙向鏈表、循環鏈表、雙向循環鏈表。其差別就是,是否在單鏈表的基礎上爲結點,增加更豐富的指針,讓其實現更豐富的功能。

鏈表雖然很好理解,但是鏈表的代碼,寫起來卻並不是那麼容易,尤其上一些對單鏈表的操作,例如鏈表反轉、鏈表雙雙反轉、有序鏈表合併等。

你可以自己試試,放下手機拿起紙筆,來一場模擬面試,就是寫一個單鏈表兩兩反轉,看看能否一次通過。

寫鏈表代碼的時候,指針指來指去,很容易就把指針丟失,造成鏈表斷裂。所以在操作鏈表時,其操作順序就是我們着重關注的點。

雖然鏈表代碼寫起來不容易,但鏈表又是面試的常客,一些常見的算法實現,也是我們開發者必須要掌握的。

二. 單鏈表兩兩反轉

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;
}

再結合之前操作步驟的圖解。

 

遞歸的方法比較繞,結合上圖,找到思路,循環是一次從前向後的移動操作窗口,而遞推是從後向前移動操作窗口。注意遞歸終止條件以及每次遞歸操作時,結點指針的輪轉,多想多練就清晰了。

三. 小結時刻

到這裏對單鏈表的兩兩反轉,就講解完畢。

寫鏈表代碼,除了考驗邏輯思維能力,還考驗編碼能力,多寫多練纔是核心。

注意其中的邊界條件以及每個操作單元中,結點指針的交換輪轉。這其中的每個步驟的操作順序,都通過圖解的方式講解清楚了,有疑問歡迎在留言去討論。

最後

如果你看到了這裏,覺得文章寫得不錯就給個讚唄!歡迎大家評論討論!如果你覺得那裏值得改進的,請給我留言。一定會認真查詢,修正不足,定期免費分享技術乾貨。感興趣的小夥伴可以點一下關注哦。謝謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章