206.反轉連結串列(簡單)
題目連結:
題目描述
給你單鏈表的頭節點 head
,請你反轉連結串列,並返回反轉後的連結串列。
示例 1:
輸入:head = [1,2,3,4,5]
輸出:[5,4,3,2,1]
示例 2:
輸入:head = [1,2]
輸出:[2,1]
示例 3:
輸入:head = []
輸出:[]
提示:
-
連結串列中節點的數目範圍是 [0, 5000]
-
-5000 <= Node.val <= 5000
進階:連結串列可以選用迭代或遞迴方式完成反轉。你能否用兩種方法解決這道題?
題解
思路:如下圖,利用三個指標
程式碼(C++版本)
ListNode* reverseList(ListNode* head) {//連結串列中只有沒有節點和只有一個節點的情況 if (head == NULL || head->next ==NULL) { return head; } ListNode* fp = head; ListNode* sp = fp->next; while (sp->next != NULL) { ListNode* tp = sp->next; sp->next = fp; fp = sp; sp = tp; } sp->next = fp; head->next = NULL; head = sp; return head; }
分析:
-
時間複雜度:O(N)
-
空間複雜度:O(1)
官方題解
連結:
方法一:迭代 假設連結串列為 1→2→3→∅,我們想要把它改成 ∅←1←2←3。
在遍歷連結串列時,將當前節點的next 指標改為指向前一個節點。由於節點沒有引用其前一個節點,因此必須事先儲存其前一個節點。在更改引用之前,還需要儲存後一個節點。最後返回新的頭引用。
class Solution { public: ListNode* reverseList(ListNode* head) { ListNode* prev = nullptr; ListNode* curr = head; while (curr) { ListNode* next = curr->next; curr->next = prev; prev = curr; curr = next; } return prev; } };
複雜度分析
-
時間複雜度:O(n),其中 n 是連結串列的長度。需要遍歷連結串列一次。
-
空間複雜度:O(1)。
方法二:遞迴 遞迴版本稍微複雜一些,其關鍵在於反向工作。假設連結串列的其餘部分已經被反轉,現在應該如何反轉它前面的部分?
class Solution { public: ListNode* reverseList(ListNode* head) { if (!head || !head->next) { return head; } ListNode* newHead = reverseList(head->next); head->next->next = head; head->next = nullptr; return newHead; } };
複雜度分析
-
時間複雜度:O(n),其中 n 是連結串列的長度。需要對連結串列的每個節點進行反轉操作。
-
空間複雜度:O(n),其中 n 是連結串列的長度。空間複雜度主要取決於遞迴呼叫的棧空間,最多為 n 層。
程式碼隨想錄
思路
如果再定義一個新的連結串列,實現連結串列元素的反轉,其實這是對記憶體空間的浪費。
其實只需要改變連結串列的next指標的指向,直接將連結串列反轉 ,而不用重新定義一個新的連結串列,如圖所示:
之前連結串列的頭節點是元素1, 反轉之後頭結點就是元素5 ,這裡並沒有新增或者刪除節點,僅僅是改表next指標的方向。
雙指標法
那麼接下來看一看是如何反轉呢?
我們拿有示例中的連結串列來舉例,如動畫所示:
首先定義一個cur指標,指向頭結點,再定義一個pre指標,初始化為null。
然後就要開始反轉了,首先要把 cur->next 節點用tmp指標儲存一下,也就是儲存一下這個節點。
為什麼要儲存一下這個節點呢,因為接下來要改變 cur->next 的指向了,將cur->next 指向pre ,此時已經反轉了第一個節點了。
接下來,就是迴圈走如下程式碼邏輯了,繼續移動pre和cur指標。
最後,cur 指標已經指向了null,迴圈結束,連結串列也反轉完畢了。 此時我們return pre指標就可以了,pre指標就指向了新的頭結點。
程式碼(C++)
class Solution { public: ListNode* reverseList(ListNode* head) { ListNode* temp; // 儲存cur的下一個節點 ListNode* cur = head; ListNode* pre = NULL; while(cur) { temp = cur->next; // 儲存一下 cur的下一個節點,因為接下來要改變cur->next cur->next = pre; // 翻轉操作 // 更新pre 和 cur指標 pre = cur; cur = temp; } return pre; } };
遞迴法
遞迴法相對抽象一些,但是其實和雙指標法是一樣的邏輯,同樣是當cur為空的時候迴圈結束,不斷將cur指向pre的過程。
關鍵是初始化的地方,可能有的同學會不理解, 可以看到雙指標法中初始化 cur = head,pre = NULL,在遞迴法中可以從如下程式碼看出初始化的邏輯也是一樣的,只不過寫法變了。
具體可以看程式碼(已經詳細註釋),
class Solution { public: ListNode* reverse(ListNode* pre,ListNode* cur){ if(cur == NULL) return pre; ListNode* temp = cur->next; cur->next = pre; // 可以和雙指標法的程式碼進行對比,如下遞迴的寫法,其實就是做了這兩步 // pre = cur; // cur = temp; return reverse(cur,temp); } ListNode* reverseList(ListNode* head) { // 和雙指標法初始化是一樣的邏輯 // ListNode* cur = head; // ListNode* pre = NULL; return reverse(NULL, head); } };