說了你可能不信leetcode刷題區域性連結串列反轉D92存在bug,你看了就知道了

煙花散盡13141發表於2021-05-21

一、題目描述

找出陣列中重複的數字

> 在一個長度為 n 的陣列 nums 裡的所有數字都在 0~n-1 的範圍內。陣列中某些數字是重複的,但不知道有幾個數字重複了,也不知道每個數字重複了幾次。請找出陣列中任意一個重複的數字。

二、思路分析

  • 之前我們已經分析過了通過遞迴的方式解決此問題 。 遞迴將問題逐層細化已達到整體問題的解決

image-20210520092044810

  • 而今天我們將從另外一個角度去分析次問題--迭代。所謂迭代就是通過一次迴圈遍歷解決反轉問題。而遞迴不同的是他將是從左至右的方式解決問題
  • 在範圍內的連結串列節點先將他指向一個預設前置節點preNode 。然後將當前節點指標後移在重複next指標指向preNode 。就可以解決問題

  • 這樣我們僅僅藉助於一個preNode就可以完成節點2的反轉。不過這裡節點已的next指標還是指向節點2的。這一步我們會在最後處理首尾問題。

image-20210520101924193

  • 最終將會是如下指向問題,對於節點3、節點4也是同樣的操作。

image-20210520102153449

  • 當指定範圍內資料全部掃描完成之後內部指標結構如上。圖中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

image-20210520104258995

新增頭結點

  • 在我們上面程式碼中雖然leetcode沒有通過但是那是leetcode的bug導致的,在裡面我們不難發現有很多if else操作。這樣的程式碼很難看至少在程式碼潔癖面前是不能容忍的。
  • 為什麼會有那麼的判斷,主要是因為我們的外部邊界和內部邊界可能會出現重合。所以我們在原有的連結串列中在頭部再新增一個預設節點。這樣做是為了避免外邊界空的情況。

image-20210520132414970

  • 同樣是left=2,right=4的情況,我們從紅色頭結點開始獲取到left=2之前的節點和right=4的節點 。 即node1是我們之前說的firstNode。node5是lastNode。
  • leftNode=node2;rightNode=node4 。然後我們在將內部連結串列進行反轉。反轉的方法就是按照我們上面的邏輯藉助另外一個空節點作為preNode。

image-20210520135606544

  • 因為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;
}
  • 多執行幾次看看最終的效果

image-20210520135905591

  • 速度依舊是那麼快,在記憶體使用上平均值是65%以上。和我們遞迴的方式進行對比不難發現。迭代的方式在時間和空間上都是最優的。

四、總結

  • 迭代和遞迴是解決連結串列常用的兩種方式。迭代的優點就是不斷的迴圈下去
  • 遞迴最大的問題就是容易導致死迴圈,在書寫的時候需要特殊注意遞迴的結束條件

相關文章