題目一 從尾到頭列印連結串列
來源:力扣(LeetCode)
連結:https://leetcode-cn.com/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof/
1.描述
輸入一個連結串列的頭節點,從尾到頭反過來返回每個節點的值(用陣列返回)。
2.示例
- 示例 1:
輸入:head = [1,3,2]
輸出:[2,3,1]
解法一 迭代+輔助棧
解題思路
看到題不難想到最簡單的辦法就是藉助一個輔助棧,順序遍歷將節點值入棧,然後再依次出棧,就能實現倒序列印。
程式碼
1 1 /** 2 2 * Definition for singly-linked list. 3 3 * struct ListNode { 4 4 * int val; 5 5 * ListNode *next; 6 6 * ListNode(int x) : val(x), next(NULL) {} 7 7 * }; 8 8 */ 9 9 class Solution { 10 10 public: 11 11 vector<int> reversePrint(ListNode* head) { 12 12 vector<int> ans;//儲存答案 13 13 if(head == nullptr) return ans;//如果節點為空,輸出空陣列 14 14 stack<int> tempAns;//輔助棧 15 15 ListNode* temp = head; 16 16 //順序遍歷,將節點值壓棧 17 17 while(temp != nullptr){ 18 18 tempAns.push(temp->val); 19 19 temp = temp->next; 20 20 } 21 21 while(!tempAns.empty()){//出棧直至棧為空 22 22 ans.push_back(tempAns.top()); 23 23 tempAns.pop(); 24 24 } 25 25 return ans;//返回答案 26 26 } 27 27 };
複雜度分析
時間複雜度: O(m)
。m為連結串列長度,遍歷連結串列和出棧各需要O(m)
時間。
空間複雜度: O(m)
。輔助棧的空間消耗。
解法二 遞迴
解題思路
也可以藉助遞迴一直遞推至連結串列尾部再層層回溯以實現反向輸出。這樣的方法程式碼簡潔,但是呼叫函式時間消耗較大。
程式碼
1 /** 2 * Definition for singly-linked list. 3 * struct ListNode { 4 * int val; 5 * ListNode *next; 6 * ListNode(int x) : val(x), next(NULL) {} 7 * }; 8 */ 9 class Solution { 10 public: 11 vector<int> reversePrint(ListNode* head) { 12 if(head==nullptr) return ans;//如果頭結點為空,返回空陣列 13 reversePrint(head->next);//遞推 14 ans.push_back(head->val);//回溯,持續存值 15 return ans;//返回答案 16 } 17 18 vector<int> ans; 19 };
複雜度分析
時間複雜度: O(m)
。遞迴的時間消耗
空間複雜度: O(m)
。遞迴的空間消耗。
題目二 反轉連結串列
來源:力扣(LeetCode)
連結:https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof/
1.描述
定義一個函式,輸入一個連結串列的頭節點,反轉該連結串列並輸出反轉後連結串列的頭節點。
2.示例
- 示例 1:
輸入: 1->2->3->4->5->NULL
輸出: 5->4->3->2->1->NULL
解法一 迭代+輔助棧
解題思路
參考題目一,我們可以寫出迭代+輔助棧的版本。首先遍歷連結串列將節點存入輔助棧,然後再倒序遍歷輔助棧並將每個節點的next
指標指向前一個節點。
程式碼
1 /** 2 * Definition for singly-linked list. 3 * struct ListNode { 4 * int val; 5 * ListNode *next; 6 * ListNode(int x) : val(x), next(NULL) {} 7 * }; 8 */ 9 class Solution { 10 public: 11 ListNode* reverseList(ListNode* head) { 12 if(head==nullptr || head->next==nullptr) return head;//如果頭結點為空或者只有一個節點,返回其本身 13 vector<ListNode*> aid;//輔助棧 14 while(head!=nullptr){//順序遍歷連結串列並壓棧 15 aid.push_back(head); 16 head = head -> next; 17 } 18 ListNode* ans = new ListNode(0);//假頭節點 19 ListNode* temp = ans; 20 for(int i = aid.size()-1; i >= 0; --i){//倒序遍歷節點並修改next指標 21 temp -> next = aid[i]; 22 temp = temp -> next; 23 } 24 temp -> next = nullptr; 25 return ans->next;//返回翻轉後的連結串列頭節點 26 } 27 };
複雜度分析
時間複雜度: O(m)
。m
為連結串列長度,遍歷連結串列和倒序遍歷修改next
指標各需要O(m)
時間。
空間複雜度: O(m)
。輔助棧的空間消耗。
解法二 迭代+三指標
解題思路
解法一的輔助棧耗費了大量空間,實際上只需要使用三指標即可。我們使用指標prePtr
,curPtr
,nextPtr
分別指向上一節點,當前節點,下一節點。初始時,prePtr
,curPtr
,nextPtr
分別為nullptr
,head
以及head->next
,然後順序遍歷連結串列,將curPtr
指向節點的next
指標修改,使其指向prePtr
指向的節點,之後更新三個指標,使其分別後移以為。遍歷完成後,返回當前curPtr
即為答案。
程式碼
1 /** 2 * Definition for singly-linked list. 3 * struct ListNode { 4 * int val; 5 * ListNode *next; 6 * ListNode(int x) : val(x), next(NULL) {} 7 * }; 8 */ 9 class Solution { 10 public: 11 ListNode* reverseList(ListNode* head) { 12 if(head==nullptr || head->next==nullptr) return head;//若頭結點為空或只有一個節點,直接返回其本身 13 ListNode* prePtr = nullptr;//上一節點 14 ListNode* curPtr = head;//當前節點 15 ListNode* nextPtr = head->next;//下一節點 16 while(nextPtr != nullptr){//依次遍歷翻轉 17 curPtr -> next = prePtr; 18 prePtr = curPtr; 19 curPtr = nextPtr; 20 nextPtr = nextPtr -> next; 21 } 22 curPtr -> next = prePtr; 23 return curPtr;//返回答案 24 } 25 };
複雜度分析
時間複雜度: O(m)
。遍歷連結串列的時間消耗。
空間複雜度: O(1)
。只需常數個額外變數即可。
解法三 遞迴
解題思路
除此之外,我們還可以使用遞迴方法解決本題。一般來說遞迴的程式碼比較簡潔,就是有的地方不容易想。這裡再提供遞迴版本程式碼,供大家加深對遞迴的理解。
程式碼
1 /** 2 * Definition for singly-linked list. 3 * struct ListNode { 4 * int val; 5 * ListNode *next; 6 * ListNode(int x) : val(x), next(NULL) {} 7 * }; 8 */ 9 class Solution { 10 public: 11 ListNode* reverseList(ListNode* head) { 12 if(head==nullptr) return head; 13 ListNode* temp = head -> next; 14 head -> next = ans; 15 ans = head; 16 reverseList(temp); 17 return ans; 18 } 19 ListNode* ans = nullptr; 20 };
複雜度分析
時間複雜度: O(m)
。遞迴的時間消耗
空間複雜度: O(m)
。遞迴的空間消耗。
題目三 複雜連結串列的深拷貝
來源:力扣(LeetCode)
連結:https://leetcode-cn.com/problems/fu-za-lian-biao-de-fu-zhi-lcof/
1.描述
給你一個長度為n
的連結串列,每個節點包含一個額外增加的隨機指標random
,該指標可以指向連結串列中的任何節點或空節點。
構造這個連結串列的深拷貝。 深拷貝應該正好由n
個全新節點組成,其中每個新節點的值都設為其對應的原節點的值。新節點的next
指標和random
指標也都應指向複製連結串列中的新節點,並使原連結串列和複製連結串列中的這些指標能夠表示相同的連結串列狀態。複製連結串列中的指標都不應指向原連結串列中的節點。
例如,如果原連結串列中有X
和Y
兩個節點,其中X.random-->Y
。那麼在複製連結串列中對應的兩個節點x
和y
,同樣有x.random-->y
。
返回複製連結串列的頭節點。
用一個由n
個節點組成的連結串列來表示輸入/輸出中的連結串列。每個節點用一個[val, random_index]
表示:
val:一個表示 Node.val 的整數
。random_index:隨機指標指向的節點索引(範圍從0到n-1);如果不指向任何節點,則為null
。
你的程式碼只接受原連結串列的頭節點head
作為傳入引數。
2.示例
- 示例 1:
輸入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
輸出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
- 示例2
輸入:head = [[1,1],[2,1]]
輸出:[[1,1],[2,1]]
- 示例 3:
輸入:head = [[3,null],[3,0],[3,null]]
輸出:[[3,null],[3,0],[3,null]]
- 示例 4:
輸入:head = []
輸出:[]
解釋:給定的連結串列為空(空指標),因此返回 null。
解法一 暴力遍歷
解題思路
可以立馬想到的暴力方法就是遍歷連結串列,使用雙指標對舊連結串列和新連結串列進行同步,先建立random
指標指向全為空的新連結串列;然後對新連結串列的每個節點,使用上一步的雙指標同步方法再次遍歷舊連結串列找到正確的random
指向。
程式碼
1 /* 2 // Definition for a Node. 3 class Node { 4 public: 5 int val; 6 Node* next; 7 Node* random; 8 9 Node(int _val) { 10 val = _val; 11 next = NULL; 12 random = NULL; 13 } 14 }; 15 */ 16 class Solution { 17 public: 18 Node* copyRandomList(Node* head) { 19 if(head == NULL || head->next == NULL)//若頭結點為空或只有頭結點 20 { 21 if(head == NULL)//如果頭結點為空 22 { 23 return NULL;//則直接返回頭結點 24 } 25 else//如果只有頭結點 26 { 27 Node* NewHead = new Node(0);//新連結串列的頭結點 28 NewHead->val = head -> val;//將頭結點進行復制 29 NewHead -> next = NULL;//因為只有一個節點,next指向NULL 30 if(head -> random == head) 31 { 32 NewHead -> random = NewHead; 33 } 34 else 35 { 36 NewHead -> random = NULL; 37 } 38 return NewHead; 39 } 40 } 41 else//如果不只頭結點 42 { 43 vector<int> randomPosition;//新建一個陣列,用於記憶各個節點random指標的指向節點,倆數字一組 44 //例如1 31就代表第一個節點的random指向31節點,25 67就代表節點25 45 //的random指向67節點 46 Node* NewHead = new Node(0);//新連結串列的頭結點 47 Node* Index = head;//採用雙指標技術,這個舊連結串列哨兵指向原連結串列的當前節點 48 Node* newIndex = NewHead;//這個指標指向新連結串列的當前節點 49 NewHead->val = head -> val;//將頭結點進行復制 50 NewHead -> next = NULL;//因為只有一個節點,next指向NULL 51 NewHead -> random = NULL;//random節點之後統一修改,因為現在新連結串列還未成型 52 Index = Index -> next;//頭結點複製完畢,舊連結串列哨兵向後位移指向下一個節點 53 Node* positionOfRandom = NULL;//用來遍歷舊連結串列確定random指向的遊標 54 if(head -> random == NULL)//如果舊連結串列頭結點的random指向NULL 55 { 56 // NewHead -> random = NULL;//則新連結串列頭結點random也指向NULL 57 randomPosition.push_back(1);//1入陣列,表示這是第一個節點 58 randomPosition.push_back(0);//0入陣列,表示該節點的random指標指向null 59 } 60 else//如果頭節點random指向不為空,尋找頭結點random指向位置 61 { 62 randomPosition.push_back(1);//1入陣列,表示這是第一個節點 63 int flag = 1;//用來記錄random指向節點位置 64 positionOfRandom = head;//遊標指向頭節點 65 while(positionOfRandom != head -> random)//遊標持續移動直到找到random指向位置 66 { 67 flag++; 68 positionOfRandom = positionOfRandom -> next; 69 } 70 randomPosition.push_back(flag);//記錄位置的整數入陣列 71 } 72 int count = 1;//記錄做到第幾個節點了 73 while(Index != NULL)//每複製完一個節點舊連結串列哨兵向後位移一個節點,直到全部複製完畢 74 { 75 /***************進行節點複製工作************************************************/ 76 count++;//處理的節點數+1 77 Node* temp = new Node(0);//新建一個節點 78 temp -> next = NULL;//先將next指向空 79 temp -> random = NULL;//先將random指向空 80 temp->val = Index->val;//將舊節點的值複製過來 81 newIndex->next = temp; //將新建節點鏈到新連結串列尾部 82 83 /**************確定該節點random指向****************************************/ 84 randomPosition.push_back(count);//count入陣列,表示這是第count個節點 85 if(Index -> random == NULL)//如果舊連結串列該結點的random指向NULL 86 { 87 randomPosition.push_back(0);//則用0記錄新連結串列該節點random也指向NULL 88 } 89 else//如果舊連結串列該節點random指向不為空,尋找該結點random指向位置 90 { 91 int flag = 1;//用來記錄random指向節點位置 92 positionOfRandom = head;//遊標指向頭節點 93 // Node* nonius = head -> random; 94 while(positionOfRandom != Index -> random)//遊標持續移動直到找到random指向位置 95 { 96 flag++; 97 positionOfRandom = positionOfRandom -> next; 98 } 99 randomPosition.push_back(flag);//記錄位置的整數入陣列 100 } 101 102 Index = Index -> next;//處理完一個節點後倆遊標向後位移一個位置 103 newIndex = newIndex -> next; 104 } 105 106 /***********根據之前建立的輔助陣列完成random指向工作*************************************/ 107 //該步驟同樣採用雙哨兵技術 108 newIndex = NewHead; 109 for(int i = 0, j = 2; i < count; i++, j = j + 2) 110 { 111 if(randomPosition[j-1] == 0) 112 { 113 newIndex -> random = NULL; 114 } 115 else 116 { 117 positionOfRandom = NewHead; 118 for(int k = 0; k < randomPosition[j-1]-1; k++) 119 { 120 positionOfRandom = positionOfRandom -> next; 121 } 122 newIndex -> random = positionOfRandom; 123 } 124 newIndex = newIndex -> next; 125 } 126 return NewHead; 127 } 128 } 129 };
複雜度分析
時間複雜度: O(m²)
。m
為連結串列長度,建新連結串列和修改random
指標的時間消耗分別為O(m)
和O(m²)
。
空間複雜度: O(1)
。只需要常數個額外變數。
解法二 遞迴+雜湊表
解題思路
對於普通連結串列的深拷貝,我們只需要順次遍歷然後新建對應節點就可以了。但本題的難點在於,我們新建一個節點並給random
指標確定指向時,它應該指向的那個節點可能還不存在。於是我們可以在新建一個節點時,遞迴的新建他的後繼節點和random
指標指向的節點。為了防止重複新建節點,我們使用雜湊表標記該節點是否已有,如果有直接取用該節點,如果沒有,則新建它。
遞迴程式碼通常比較簡潔,但是有的遞迴較難以理解,需要多加練習,多讀程式碼,以加深理解。
程式碼
1 /* 2 // Definition for a Node. 3 class Node { 4 public: 5 int val; 6 Node* next; 7 Node* random; 8 9 Node(int _val) { 10 val = _val; 11 next = NULL; 12 random = NULL; 13 } 14 }; 15 */ 16 class Solution { 17 private: 18 unordered_map<Node*, Node*> created;//已建節點 19 public: 20 Node* copyRandomList(Node* head) { 21 if(head==nullptr) return head;//如果該節點為空,返回其本身 22 if(!created.count(head)){//如果該節點未建立 23 Node* temp = new Node(head->val);//新建該節點 24 created[head] = temp;//標記該節點已建立 25 temp -> next = copyRandomList(head->next);//遞迴建立其後繼節點 26 temp -> random = copyRandomList(head->random);//遞迴建立其random指標指向的節點 27 } 28 return created[head];//返回新連結串列 29 } 30 };
複雜度分析
時間複雜度: O(m)
。遞迴建立新連結串列的時間消耗。
空間複雜度: O(m)
。雜湊表的空間消耗。
解法三 迭代 + 雜湊表
解題思路
除了遞迴+雜湊外,本題還可以使用迭代+雜湊的方式解決,該方法相較於上一方法更好理解。
與上一方法不同的是,我們只關注當前節點random
指標應指向的節點是否存在,並不關注當前節點的random
指標應該指向的節點的random
指標應指向的節點存不存在,也即不會直接遞迴建立一條鏈路上的全部節點(上一方法中,只要random
指標指向的節點不存在,就會遞迴建立。),如果不存在,建立之,並標記舊連結串列中對應節點的新節點已建立。
具體的,我們使用pre
指標指向上一個節點,cur
指標指向當前工作節點,初始時pre
指向一個新建的假頭節點,cur
指向舊連結串列頭節點。我們從頭節點開始依次遍歷各個節點,首先查表判斷該節點是否已存在,如果已存在,直接取用之,將pre
指標指向的節點的next
指標指向已新建本節點,否則建立之。之後判斷其random
指向是否為空或者已存在,已存在則呼叫之,不存在則建立之,並將本節點random
指標指向它。最後更新cur
指標指向本節點的下一節點繼續遍歷。
程式碼
1 ```cpp 2 class Solution { 3 private: 4 unordered_map<Node*, Node*> created; 5 public: 6 Node* copyRandomList(Node* head) { 7 if(head==nullptr) return head;//如果頭結點為空,返回其本身 8 Node* pre = new Node(0);//假頭節點,用於避免邊界判斷帶來的麻煩 9 Node* cur = head;//當前指標指向頭節點 10 while(cur!=nullptr){//依次遍歷連結串列各節點,同時構造新連結串列 11 if(!created.count(cur)){//如果當前指標指向的舊連結串列節點未被建立(其有可能在之前的節點完善random指標時被建立) 12 created[cur] = new Node(cur->val);//則建立之 13 }//如果已被建立則直接使用之 14 pre -> next = created[cur];//將前一節點的next指標指向本節點 15 pre = pre -> next;//更新pre指標指向本節點 16 if(cur->random == nullptr) created[cur] -> random = nullptr;//若該節點的random指標指向空,則新連結串列中的也指向空 17 else{//否則完善其random指標指向 18 if(!created.count(cur->random)){//如果其random應指向的節點還未建立 19 created[cur->random] = new Node(cur->random->val);//則建立之 20 }//並將random指標指向他 21 created[cur]->random = created[cur->random]; 22 } 23 cur = cur -> next;//更新當前工作指標指向本節點的下一節點 24 } 25 return created[head];//返回答案 26 } 27 };
複雜度分析
時間複雜度: O(m)
。遍歷一次連結串列的時間消耗
空間複雜度: O(m)
。雜湊表的空間消耗。
解法四 迭代
解題思路
在上述方法中,因為使用了雜湊表,所以空間複雜度比較高。這裡有另外的一種方法,可以實現O(1)
空間複雜度。
具體的,我們首先遍歷一遍舊連結串列,並新建複製節點,注意,這次我們將新建的節點直接連結在原節點的後邊,同時保持連結串列的完整性。
例如對於連結串列A→B→C
,我們可以將其拆分為A → A′→ B → B′→ C → C′
。對於任意一個原節點S
,其拷貝節點S'
即為其後繼節點。
通過這樣的方式,我們很容易就能找到新節點的random
節點所指向的正確節點,因為他們就是舊連結串列中原節點中random
指標指向節點的後繼結點或者空節點,通過一次遍歷我們就能完善所有節點的random
指標。最後,再通過一次遍歷將兩個連結串列斷開,即可完成深拷貝操作。注意,原連結串列也必須還原,不然無法通過。
程式碼
1 class Solution { 2 public: 3 Node* copyRandomList(Node* head) { 4 if(!head) return head;//如果頭結點為空,返回其本身 5 6 //第一次遍歷,將拷貝節點連結到原節點之後,使其成為原節點的後繼結點 7 Node* temp = head; 8 while(temp!=nullptr){ 9 Node* newNode = new Node(temp->val); 10 newNode -> next = temp -> next; 11 newNode -> random = temp -> random; 12 temp -> next = newNode; 13 temp = newNode -> next; 14 } 15 16 //第二次遍歷,完善各拷貝節點的random指向 17 temp = head; 18 Node* ans = head -> next; 19 while(temp!=nullptr){ 20 if(temp->next->random != nullptr){ 21 temp->next->random = temp->next->random->next; 22 } 23 temp = temp -> next -> next; 24 } 25 26 //第三次遍歷,將新舊連結串列斷鏈並恢復原連結串列 27 temp = head; 28 while(temp!=nullptr){ 29 Node* copy = temp -> next -> next; 30 if(!copy) break; 31 temp -> next -> next = copy -> next; 32 temp -> next = copy; 33 temp = copy; 34 } 35 temp -> next = nullptr; 36 return ans; 37 } 38 };
複雜度分析
時間複雜度: O(m)
。遍歷一次連結串列的時間消耗
空間複雜度: O(1)
。只需常數個額外變數。
Tips:
在上面的分析中可以看到,對於涉及連結串列的,包含增刪等操作時,使用假頭/尾節點可以避免很多邊界判斷帶來的麻煩。這一點可以在解題過程中多加應用。
更多知識內容分享:
力扣個人主頁https://leetcode-cn.com/profile/articles/
CSDN個人主頁https://blog.csdn.net/qq_39255924
牛客個人主頁https://blog.nowcoder.net/newcoderthewarrior