432,劍指 Offer-反轉鏈表的3種方式

Success is stumbling from failure to failure with no loss of enthusiasm. 
node

成功是在失敗中摸索,同時不失去熱情。web

問題描述面試



定義一個函數,輸入一個鏈表的頭節點,反轉該鏈表並輸出反轉後鏈表的頭節點。算法


示例:微信

輸入: 1->2->3->4->5->NULL數據結構

輸出: 5->4->3->2->1->NULLapp


限制:數據結構和算法

0 <= 節點個數 <= 5000編輯器


使用棧解決svg



鏈表的反轉是老生常談的一個問題了,同時也是面試中常考的一道題。最簡單的一種方式就是使用,由於棧是先進後出的。實現原理就是把鏈表節點一個個入棧,當所有入棧完以後再一個個出棧,出棧的時候在把出棧的結點串成一個新的鏈表。原理以下


代碼比較簡單,來看下

 1public ListNode reverseList(ListNode head) {
2    Stack<ListNode> stack = new Stack<>();
3    //把鏈表節點所有摘掉放到棧中
4    while (head != null) {
5        stack.push(head);
6        head = head.next;
7    }
8    if (stack.isEmpty())
9        return null;
10    ListNode node = stack.pop();
11    ListNode dummy = node;
12    //棧中的結點所有出棧,而後從新連成一個新的鏈表
13    while (!stack.isEmpty()) {
14        ListNode tempNode = stack.pop();
15        node.next = tempNode;
16        node = node.next;
17    }
18    //最後一個結點就是反轉前的頭結點,必定要讓他的next
19    //等於空,不然會構成環
20    node.next = null;
21    return dummy;
22}


雙鏈表求解



雙鏈表求解是把原鏈表的結點一個個摘掉,每次摘掉的鏈表都讓他成爲新的鏈表的頭結點,而後更新新鏈表。下面以鏈表1→2→3→4爲例畫個圖來看下。

他每次訪問的原鏈表節點都會成爲新鏈表的頭結點,最後再來看下代碼

 1public ListNode reverseList(ListNode head{
2    //新鏈表
3    ListNode newHead = null;
4    while (head != null) {
5        //先保存訪問的節點的下一個節點,保存起來
6        //留着下一步訪問的
7        ListNode temp = head.next;
8        //每次訪問的原鏈表節點都會成爲新鏈表的頭結點,
9        //其實就是把新鏈表掛到訪問的原鏈表節點的
10        //後面就好了
11        head.next = newHead;
12        //更新新鏈表
13        newHead = head;
14        //從新賦值,繼續訪問
15        head = temp;
16    }
17    //返回新鏈表
18    return newHead;
19}


遞歸解決



咱們再來回顧一下遞歸的模板,終止條件,遞歸調用,邏輯處理。

 1public ListNode reverseList(參數0) {
2    if (終止條件)
3        return;
4
5    邏輯處理(可能有,也可能沒有,具體問題具體分析)
6
7    //遞歸調用
8    ListNode reverse = reverseList(參數1);
9
10    邏輯處理(可能有,也可能沒有,具體問題具體分析)
11}

終止條件就是鏈表爲空,或者是鏈表沒有尾結點的時候,直接返回

if (head == null || head.next == null) return head;

遞歸調用是要從當前節點的下一個結點開始遞歸。邏輯處理這塊是要把當前節點掛到遞歸以後的鏈表的末尾,看下代碼

 1public ListNode reverseList(ListNode head) {
2    //終止條件
3    if (head == null || head.next == null)
4        return head;
5    //保存當前節點的下一個結點
6    ListNode next = head.next;
7    //從當前節點的下一個結點開始遞歸調用
8    ListNode reverse = reverseList(next);
9    //reverse是反轉以後的鏈表,由於函數reverseList
10    // 表示的是對鏈表的反轉,因此反轉完以後next確定
11    // 是鏈表reverse的尾結點,而後咱們再把當前節點
12    //head掛到next節點的後面就完成了鏈表的反轉。
13    next.next = head;
14    //這裏head至關於變成了尾結點,尾結點都是爲空的,
15    //不然會構成環
16    head.next = null;
17    return reverse;
18}

由於遞歸調用以後head.next節點就會成爲reverse節點的尾結點,咱們能夠直接讓head.next.next = head;,這樣代碼會更簡潔一些,看下代碼

1public ListNode reverseList(ListNode head) {
2    if (head == null || head.next == null)
3        return head;
4    ListNode reverse = reverseList(head.next);
5    head.next.next = head;
6    head.next = null;
7    return reverse;
8}

這種遞歸往下傳遞的時候基本上沒有邏輯處理,當往回反彈的時候纔開始處理,也就是從鏈表的尾端往前開始處理的。咱們還能夠再來改一下,在鏈表遞歸的時候從前日後處理,處理完以後直接返回遞歸的結果,這就是所謂的尾遞歸,這種運行效率要比上一種好不少

 1public ListNode reverseList(ListNode head) {
2    return reverseListInt(head, null);
3}
4
5private ListNode reverseListInt(ListNode head, ListNode newHead) {
6    if (head == null)
7        return newHead;
8    ListNode next = head.next;
9    head.next = newHead;
10    return reverseListInt(next, head);
11}

尾遞歸雖然也會不停的壓棧,但因爲最後返回的是遞歸函數的值,因此在返回的時候都會一次性出棧,不會一個個出棧這麼慢。但若是咱們再來改一下,像下面代碼這樣又會一個個出棧了

 1public ListNode reverseList(ListNode head) {
2    return reverseListInt(head, null);
3}
4
5private ListNode reverseListInt(ListNode head, ListNode newHead) {
6    if (head == null)
7        return newHead;
8    ListNode next = head.next;
9    head.next = newHead;
10    ListNode node = reverseListInt(next, head);
11    return node;
12}


總結



鏈表反轉使用棧雖然也能實現,但通常不是很推薦,下面兩種實現方式會好一些。使用棧能實現鏈表的反轉,那麼使用隊列呢,若是使用雙端隊列也是能夠的,從一端所有入隊,而後再從這一端所有出隊,說了半天這不仍是和棧同樣嗎……



431,劍指 Offer-鏈表中倒數第k個節點

429,劍指 Offer-刪除鏈表的節點

410,劍指 Offer-從尾到頭打印鏈表

352,數據結構-2,鏈表


長按上圖,識別圖中二維碼以後便可關注。


若是以爲有用就點個"贊"吧

本文分享自微信公衆號 - 數據結構和算法(sjjghsf)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索