NO.141 環形鏈表 簡單
思路一:HashSet p指針遍歷鏈表,將每個節點存入 set ,如果訪問的節點 set 中存在,說明有環。
時間複雜度:O(n) 空間複雜度:O(n)
思路二:快慢指針 其實就是 Floyd 算法,如果快指針走到了結尾,說明不是環形鏈表;如果是環形鏈表快慢指針一定會相遇,因爲入環之後每次移動快慢指針,就相當於讓兩個指針的距離 -1 。
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) return false;
ListNode fast = head.next, slow = head;
while (fast != null && fast.next != null) {
//相遇說明有環
if (fast == slow) return true;
fast = fast.next.next;
slow = slow.next;
}
return false;
}
時間複雜度:O(n) 空間複雜度:O(1)
NO.142 環形鏈表II 中等
和其姊妹題NO.141的解題思路一樣,稍微一些調整變化。
思路一:HashSet p指針遍歷鏈表,將每個節點存入 set ,如果訪問的節點 set 中存在,說明有環並返回此時的節點p。線性遍歷,第一次重複一定是入環的第一個節點。
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) return null;
HashSet<ListNode> set = new HashSet<>();
ListNode p = head;
while (p != null) {
if (set.contains(p)) {
return p;
}
set.add(p);
p = p.next;
}
return null;
}
時間複雜度:O(n) 空間複雜度:O(n)
思路二:快慢指針 快慢指針如何進行不在贅述。重點在於如何找到入環的第一個節點。
先看一下最終實現:如果快指針遍歷完成,沒有環;
如果有環,當快慢指針第一次相遇後,慢指針原地不動,重置快指針回到表頭節點,此時快慢指針都保持每次移動一個節點,當快慢指針第二次相遇的時候,此時相遇節點就是入環的第一個節點。
public ListNode detectCycle(ListNode head) {
ListNode fast = head, slow = head;
while (true) {
//無環
if (fast == null || fast.next == null) return null;
fast = fast.next.next;
slow = slow.next;
//第一次相遇
if (slow == fast) break;
}
//重置fast
fast = head;
//第二次相遇
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
時間複雜度:O(n) 空間複雜度:O(1)
爲什麼兩次相遇一定是入環的第一個節點?
設鏈表有環,且鏈表共有 a+b 個節點,其中 鏈表頭部到鏈表入口 有 a 個節點(不計鏈表入口節點), 鏈表環 有 b 個節點;設兩指針分別走了 f,s 步,則有:
fast
走的步數是slow
步數的 2 倍,即 f = 2s ;fast
比slow
多走了 n 個環的長度,即 f = s + nb ; ( 雙指針都走過一次非環的部分 a 步,然後在環內繞圈直到重合,重合時fast
比slow
多走 環的長度整數倍 n )- 以上兩式相減得:f = 2nb,s = nb,即
fast
和slow
指針分別走了 2n,n 個 環的周長 。
如果一個指針 k 遍歷這個有環鏈表一次需要走 k = a + nb 步 (此時 n=1),此時 k 會回到入環節點。
而目前,slow
指針走過的步數爲 nb 步。因此,我們只要想辦法讓 slow
再走 a 步停下來,就可以到環的入口節點。
但是此時我們並不知道參數鏈表的 a 是多少!
所以讓 fast
和 slow
第一次相遇後(此時 s = nb),fast
從頭節點開始每次走一步,同時 slow
也在環內每次走一步,當 fast
走 a 步到達入環節點時,slow
共走了 s = a + nb 步,即 slow
也到達入環節點。
所以只需要第二次 fast
和 slow
相遇即可,此時一定是在入環節點。
NO.202 快樂數 簡單
思路一:HashTable 用一個 HashSet 對每一個數字的每一位求平方相加,結果 num 如果是 1 則說明 n 是快樂數;如果不是 1,判斷 HashSet 中是否已經存在相同的 num,如果存在說明出現閉環,不存在則結果放入 Set 並繼續按位平方求和,判斷結果。
public boolean isHappy(int n) {
HashSet<Integer> set = new HashSet<>();
//還沒有證明n是快樂數,也沒有發生循環
while (n != 1 && !set.contains(n)) {
set.add(n);
n = helper(n);
}
return n == 1;
}
//按位平方求和
private int helper(int n) {
int num = 0;
while (n > 0) {
int temp = n%10;
num+=temp*temp;
n/=10;
}
return num;
}
不需要考慮平方求和之後的結果num超出範圍,因爲 999 的結果是 243 、9999 的結果 324 、、、 9999999999999 的結果 1053 。。。發現,三位數最大的按位平方求和結果還是個三位數,所以無論怎麼求下去,都是三位數。
時間複雜度:O(logn) 空間複雜度:O(n)
思路二:快慢指針 不斷地生成 num ,num 生成下一個 num ,這是一種鏈式的遞減,如果鏈上的某個節點出現了 1 ,說明 n 是快樂數,如果鏈上出現了環,說明 n 不是快樂數。
這種思路下,本問題就轉換成了判斷鏈表是否有環的問題,在判斷的途中順便檢測是否有節點爲 1 。
慢指針一次走一個節點,快指針一次走兩個節點,如果存在循環,那麼快慢一定會相遇。
public boolean isHappy(int n) {
int slow = n, fast = helper(n);
while (fast != 1 && slow != fast) {
slow = helper(slow);
fast = helper(helper(fast));
}
return fast == 1;
}
//按位平方求和
private int helper(int n) {
int num = 0;
while (n > 0) {
int temp = n % 10;
num += temp * temp;
n /= 10;
}
return num;
}
時間複雜度:O(logn) 空間複雜度:O(1)
NO.287 尋找重複數 中等
思路一:快慢指針 讀完題並不能立刻想到判環問題。如果將數組建圖,下標位置 i 都有一個連接 nums[i] 的邊。本題說有且只有一個重複元素 num,那麼 num 這個位置至少有兩條邊指向他,所以圖裏一定有環,而這個重複元素 num 就是環的入口。
問題變成了找環的入口問題,就變成了上面的 NO.142 環形鏈表 II。區別僅在於本題是數組而已。
依然快慢指針,雖然本題不是一個鏈表,但是根據前面所說的建圖後有了鏈表的性質(下標 i -> nums[i]),所以移動一步就是 slow = nums[slow]
,同樣的走兩步就是 fast = nums[nums[fast]]
。
public int findDuplicate(int[] nums) {
int slow = 0, fast = 0;
while (true) {
slow = nums[slow];
fast = nums[nums[fast]];
//第一次相遇
if (slow == fast) break;
}
//重置一個指針,快慢指針都每次一個步長
slow = 0;
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
//第二次相遇就是環入口
return slow;
}
時間複雜度:O(n) 空間複雜度:O(1)
本人菜鳥,有錯誤請告知,感激不盡!