一、題目描述
> 在一個長度為 n 的陣列 nums 裡的所有數字都在 0~n-1 的範圍內。陣列中某些數字是重複的,但不知道有幾個數字重複了,也不知道每個數字重複了幾次。請找出陣列中任意一個重複的數字。
二、思路分析
- 之前我們已經分析過了通過遞迴的方式解決此問題 。 遞迴將問題逐層細化已達到整體問題的解決
- 而今天我們將從另外一個角度去分析次問題--迭代。所謂迭代就是通過一次迴圈遍歷解決反轉問題。而遞迴不同的是他將是從左至右的方式解決問題
- 在範圍內的連結串列節點先將他指向一個預設前置節點
preNode
。然後將當前節點指標後移在重複next指標指向preNode
。就可以解決問題
- 這樣我們僅僅藉助於一個
preNode
就可以完成節點2的反轉。不過這裡節點已的next指標還是指向節點2的。這一步我們會在最後處理首尾問題。
- 最終將會是如下指向問題,對於節點3、節點4也是同樣的操作。
- 當指定範圍內資料全部掃描完成之後內部指標結構如上。圖中1、2、4、5被特殊標註出來因為這四個分別是外邊界和內邊界的節點。我們需要特殊將這些邊界進行連線 。 1指向4 、 2指向5就完成了最終的反轉
- 相信通過上面的動畫模擬,你應該可以輕鬆的理解迭代處理的方式。但是在我們實際處理中邊界我們需要特殊儲存處理。下面我們就通過程式碼層面來實現效果
三、AC 程式碼
bug
- 按照上面的邏輯,我嘗試實現了下
//外邊界左側節點
private static ListNode firstNode ;
//外邊界右側節點
private static ListNode lastNode ;
public ListNode reverseBetween(ListNode head, int left, int right) {
//preNode 作為接受反轉節點
ListNode preNode=null;
//用於指向當前操作節點 , 也是內部右側節點
ListNode currentNode = head;
//儲存下一節點,方便賦值
ListNode nextNode=null;
//內部左側節點
ListNode leftNode=head;
int index =1 ;
while (currentNode != null) {
nextNode = currentNode.next;
if (index == left-1) {
//捕獲外部邊界節點
firstNode = currentNode;
}
if (index >= left && index <= right) {
//指標修復
currentNode.next = preNode;
preNode = currentNode;
}
currentNode = nextNode;
if (index == right) {
//捕獲外部邊界節點
lastNode = nextNode;
break;
}
index++;
}
//因為是指定範圍但是有可能是全部連結串列這時候外部邊界都是null
if (firstNode != null) {
leftNode = firstNode.next;
firstNode.next = preNode;
}
if (lastNode != null) {
leftNode.next = lastNode;
return head;
} else {
return preNode;
}
}
- 上面這段程式碼本地執行是成功的。而且在leetcode官網上執行[3,5] ,left=1 , right=1 單獨執行輸出結果也是[3,5] 時沒有問題的。但是當提交執行全部測試用例的時候確保在【3,5】 1 ,1這個測試用例無法通過。我認為是leetcode官網執行測試程式碼的一個bug
新增頭結點
- 在我們上面程式碼中雖然leetcode沒有通過但是那是leetcode的bug導致的,在裡面我們不難發現有很多if else操作。這樣的程式碼很難看至少在程式碼潔癖面前是不能容忍的。
- 為什麼會有那麼的判斷,主要是因為我們的外部邊界和內部邊界可能會出現重合。所以我們在原有的連結串列中在頭部再新增一個預設節點。這樣做是為了避免外邊界空的情況。
- 同樣是left=2,right=4的情況,我們從紅色頭結點開始獲取到left=2之前的節點和right=4的節點 。 即node1是我們之前說的firstNode。node5是lastNode。
- leftNode=node2;rightNode=node4 。然後我們在將內部連結串列進行反轉。反轉的方法就是按照我們上面的邏輯藉助另外一個空節點作為preNode。
- 因為node1,和node5已經被我們記錄下來了。下面我們只需要將內部外部指標進行關聯就可以了
firstNode.next = rightNode;
leftNode.next = lastNode;
- 最終將頭結點後面部分返回就可以了
private static ListNode firstNode ;
private static ListNode lastNode ;
public ListNode reverseBetween2(ListNode head, int left, int right) {
ListNode visualNode = new ListNode(-1, head);
firstNode = visualNode;
for (int i = 1;i < left; i++) {
firstNode = firstNode.next;
}
ListNode rightNode = firstNode;
for (int i = 0; i < right - left + 1; i++) {
rightNode = rightNode.next;
}
ListNode leftNode = firstNode.next;
lastNode = rightNode.next;
rightNode.next = null;
ListNode wpre = null;
ListNode wcur = leftNode;
while (wcur != null) {
ListNode next = wcur.next;
wcur.next = wpre;
wpre = wcur;
wcur = next;
}
firstNode.next = rightNode;
leftNode.next = lastNode;
return visualNode.next;
}
- 多執行幾次看看最終的效果
- 速度依舊是那麼快,在記憶體使用上平均值是65%以上。和我們遞迴的方式進行對比不難發現。迭代的方式在時間和空間上都是最優的。
四、總結
- 迭代和遞迴是解決連結串列常用的兩種方式。迭代的優點就是不斷的迴圈下去
- 遞迴最大的問題就是容易導致死迴圈,在書寫的時候需要特殊注意遞迴的結束條件