題目描述(中等難度)
給一個鏈表,返回複製後的鏈表。鏈表節點相對於普通的多了一個 random
指針,會隨機指向鏈表內的任意節點或者指向 null
。
思路分析
這道題其實和 133 題 複製一個圖很類似,這裏的話就是要解決的問題就是,當更新當前節點的 random
指針的時候,如果 random
指向的是很後邊的節點,但此時後邊的節點還沒有生成,那麼我們該如何處理。
和 133 題 一樣,我們可以利用 HashMap
將節點提前生成並且保存起來,第二次遍歷到他的時候直接從 HashMap
裏邊拿即可。
這裏的話就有兩種思路,一種需要遍歷兩邊鏈表,一種只需要遍歷一遍。
2020.3.3 更新,leetcode 增加了樣例,之前沒有重複的數字所以
key
存的val
,現在有了重複數字,將key
修改爲Node
。此外Node
的無參的構造函數也被去掉了,也需要修改。
解法一
首先利用 HashMap
來一個不用思考的代碼。
遍歷第一遍鏈表,我們不考慮鏈表之間的相互關係,僅僅生成所有節點,然後把它存到 HashMap
中,val
作爲 key
,Node
作爲 value
。
遍歷第二遍鏈表,將之前生成的節點取出來,更新它們的 next
和 random
指針。
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
HashMap<Node, Node> map = new HashMap<>();
Node h = head;
while (h != null) {
Node t = new Node(h.val);
map.put(h, t);
h = h.next;
}
h = head;
while (h != null) {
if (h.next != null) {
map.get(h).next = map.get(h.next);
}
if (h.random != null) {
map.get(h).random = map.get(h.random);
}
h = h.next;
}
return map.get(head);
}
解法二
解法一雖然簡單易懂,但還是有可以優化的地方的。我們可以只遍歷一次鏈表。
核心思想就是延遲更新它的 next
。
1 -> 2 -> 3
用 cur 指向已經生成的節點的末尾
1 -> 2
^
c
然後將 3 構造完成
最後將 2 的 next 指向 3
1 -> 2 -> 3
^
c
期間已經生成的節點存到 HashMap 中,第二次遇到的時候直接從 HashMap 中拿
看下代碼理解一下含義吧
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
HashMap<Node, Node> map = new HashMap<>();
Node h = head;
Node cur = new Node(-1); //空結點,dummy 節點,爲了方便頭結點計算
while (h != null) {
//判斷當前節點是否已經產生過
if (!map.containsKey(h)) {
Node t = new Node(h.val);
map.put(h, t);
}
//得到當前節點去更新它的 random 指針
Node next = map.get(h);
if (h.random != null) {
//判斷當前節點是否已經產生過
if (!map.containsKey(h.random)) {
next.random = new Node(h.random.val);
map.put(h.random, next.random);
} else {
next.random = map.get(h.random);
}
}
//將當前生成的節點接到 cur 的後邊
cur.next = next;
cur = cur.next;
h = h.next;
}
return map.get(head);
}
解法三
上邊的兩種解法都用到了 HashMap
,所以額外需要 O(n)
的空間複雜度。現在考慮不需要額外空間的方法。
主要參考了這裏。主要解決的問題就是我們生成節點以後,當更新它的 random
的時候,怎麼找到之前生成的節點,前兩種解法用了 HashMap
全部存起來,這裏的話可以利用原來的鏈表的指針域。
主要需要三步。
- 生成所有的節點,並且分別插入到原有節點的後邊
- 更新插入節點的
random
- 將新舊節點分離開來
一圖勝千言,大家看一下下邊的圖吧。
代碼對應如下。
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
Node l1 = head;
Node l2 = null;
//生成所有的節點,並且分別插入到原有節點的後邊
while (l1 != null) {
l2 = new Node(l1.val);
l2.next = l1.next;
l1.next = l2;
l1 = l1.next.next;
}
//更新插入節點的 random
l1 = head;
while (l1 != null) {
if (l1.random != null) {
l1.next.random = l1.random.next;
}
l1 = l1.next.next;
}
l1 = head;
Node l2_head = l1.next;
//將新舊節點分離開來
while (l1 != null) {
l2 = l1.next;
l1.next = l2.next;
if (l2.next != null) {
l2.next = l2.next.next;
}
l1 = l1.next;
}
return l2_head;
}
解法四
不利用額外的空間複雜度還有一種思路,參考 這裏。
解法三利用原鏈表的 next
域把新生成的節點保存了起來。類似的,我們還可以利用原鏈表的 random
域把新生成的節點保存起來。
主要還是三個步驟。
- 生成所有的節點,將它們保存到原鏈表的
random
域,同時利用新生成的節點的next
域保存原鏈表的random
。 - 更新新生成節點的
random
指針。 - 恢復原鏈表的
random
指針,同時更新新生成節點的next
指針。
一圖勝千言。
相應的代碼如下。
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
Node l1 = head;
Node l2 = null;
//生成所有的節點,講它們保存到原鏈表的 random 域,
//同時利用新生成的節點的 next 域保存原鏈表的 random。
while (l1 != null) {
l2 = new Node(l1.val);
l2.next = l1.random;
l1.random = l2;
l1 = l1.next;
}
l1 = head;
//更新新生成節點的 random 指針。
while (l1 != null) {
l2 = l1.random;
l2.random = l2.next != null ? l2.next.random : null;
l1 = l1.next;
}
l1 = head;
Node l2_head = l1.random;
//恢復原鏈表的 random 指針,同時更新新生成節點的 next 指針。
while (l1 != null) {
l2 = l1.random;
l1.random = l2.next;
l2.next = l1.next != null ? l1.next.random : null;
l1 = l1.next;
}
return l2_head;
}
總
解法一、解法二是比較直接的想法,直接利用 HashMap
存儲之前的節點。解法三、解法四利用原有鏈表的指針,通過指來指去完成了賦值。鏈表操作的核心思想就是,在改變某一個節點的指針域的時候,一定要把該節點的指針指向的節點用另一個指針保存起來,以免造成丟失。
更多詳細通俗題解詳見 leetcode.wang 。