分享一道leetcode上的題,固然,竟然不是放在刷題貼裏來說,意味着分享的這道題不只僅是教你怎麼來解決,更重要的是這道題引起出來的一些解題技巧或許能夠用在其餘地方,下面咱們來看看這道題的描述。算法
給定一個未排序的整數數組,找出其中沒有出現的最小的正整數。數組
示例 1:
輸入: [1,2,0]
輸出: 3
示例 2:
輸入: [3,4,-1,1]
輸出: 2
示例 3:
輸入: [7,8,9,11,12]
輸出: 1
複製代碼
說明: 你的算法的時間複雜度應爲O(n),而且只能使用常數級別的空間。bash
這道題在 leetcode 的定位是困難級別,或許你能夠嘗試本身作一下,而後再來看個人解答,下面面我一步步來分析,秒殺的大佬請忽略.....工具
對於一道題,若是不能第一時間想到最優方案時,我以爲能夠先不用那麼嚴格,能夠先採用暴力的方法求解出來,而後再來一步步優化。像這道題,我想,若是能夠你要先用快速排序先把他們排序,而後在再來求解的話,那是至關容易的,不過 O(nlogn) 的時間複雜度過高,其實咱們能夠先犧牲下咱們的空間複雜度,讓保證咱們的時間複雜度爲 O(n),以後再來慢慢優化咱們的空間複雜度。學習
咱們知道,若是數組的長度爲 n,那麼咱們要找的目標數必定是出於 1~n+1 之間的,咱們能夠先把咱們數組裏的全部數映射到集合裏,而後咱們從 1~n 開始遍歷判斷,看看哪一個數是沒有在集合的,若是不存在的話,那麼這個數即是咱們要找的數了。若是 1~n 都存在,那咱們要找的數就是 n+1 了。優化
不過這裏須要注意的是,在把數組裏面的數存進集合的時候,對於 小於 1 或者大於 n 的數,咱們是不須要存進集合裏的,由於他們不會對結果形成影響,這也算是一種優化吧。光說還不行,還得會寫代碼,代碼以下:ui
public int firstMissingPositive(int[] nums) {
Set<Integer> set = new HashSet<>();
int n = nums.length;
for (int i = 0; i < n; i++) {
if (nums[i] >= 1 && nums[i] <= n) {
set.add(nums[i]);
}
}
for (int i = 1; i <= n; i++) {
if (!set.contains(i)) {
return i;
}
}
return n + 1;
}
複製代碼
方法一的空間複雜度在最塊的狀況下是 O(n),不知道你們還記不記得位算法,其實咱們是能夠利用位算法來繼續優化咱們的空間的,若是不知道位算法的能夠看我直接寫的一篇文章:spa
一、什麼是bitmap算法。.net
二、本身用代碼實現bitmap算法;3d
經過採用位算法,咱們咱們把空間複雜度減小8倍,即從 O(n) -> O(n/32),但其實 O(n/32) 任然還算 O(n),不過,在實際運行的時候,它是確實可以讓咱們運行的更快的,在 Java 中,已經有自帶的支持位算法的類了,即 bitSet,若是你沒學過這個類,我相信你也是能一眼看懂的,代碼以下:
public int firstMissingPositive2(int[] nums) {
BitSet bitSet = new BitSet();
int n = nums.length;
for (int i = 0; i < n; i++) {
if (nums[i] >= 1 && nums[i] <= n) {
bitSet.set(nums[i]);
}
}
for (int i = 1; i <= n; i++) {
if (!bitSet.get(i)) {
return i;
}
}
return n + 1;
}
複製代碼
若是這個數組是有序的,那就好辦了,可是若是咱們要把它進行排序的話,又得須要 O(nlogn) 的時間複雜度,那咱們有沒有啥辦法把它進行排序,而後時間複雜度又不須要那麼高呢?
答是能夠,剛纔咱們說過,對於那些小於 1 或者大於 n 的數,咱們是實際上是能夠不理的,竟然咱們,咱們須要處理的這些數,他們都是處於 1~n 之間的,那要你給這些處於 1~n 之間的數排序,而且重複的元素咱們也是能夠忽略掉的,記錄一個就能夠了,那麼你能不能在 O(n) 時間複雜度排序好呢?
不知道你們是否還記得我之間寫過的下標法?
或者是否還記得計數排序?(計數排序其實就是下標法的一個應用了)
不過學過計數排序的朋友可能都知道,計數排序是須要咱們開一個新的數組的,不過咱們這裏不須要,這道題咱們能夠這樣作:例如對於 nums[i],咱們能夠把它放到數組下標位 nums[i] - 1 的位置上,這樣子一處理的話,全部 1<=nums[i]<=n 的數,就都是是處於相對有序的位置了。注意,我指的是相對,也就是說對於 1-n 這些數而言,其餘 小於 1 或者大於 n 的咱們不理的。例如對於這個數組 nums[] = {4, 1, -1, 3, 7}。
讓 nums[i] 放到數組下標爲 nums[i-1]的位置,而且對於那些 nums[i]<=0 或 nums > n的數,咱們是能夠不用理的,因此過程以下:從下標爲 0 開始向右遍歷
一、把 4 放在下標爲 3 的位置,爲了避免讓下標爲 3 的數丟失,把下標爲 3 的數與 4進行交換。
二、此時咱們還不能向右移動來處下標爲1的數,由於咱們當前位置的3還不是處於有序的位置,還得繼續處理,因此把 3 與下標爲 2 的數交換
三、當前位置額數爲 -1,不理它,前進到下標爲 1 的位置,把 1 與下標爲 0的數交換
四、當前位置額數爲 -1,不理它,前進到下標爲 2 的位置,此時的 3 處於有序的位置,不理它繼續前進,4也是處於有序的位置,繼續前進。
五、此時的 7 > n,不理它,繼續前進。
遍歷完成,此時,那些處於 1~n的數都是處於有序的位置了,對於那些小於1或者大於n的,咱們忽略它僞裝沒看到就是了。
這裏還有個要注意的地方,就是 nums[i] 與下標爲 nums[i]-1的數若是相等的話也是不須要交換的。
接下來,咱們再次從下標 0 到下標 n-1 遍歷這個數組,若是遇到 nums[i] != i + 1 的數,,那麼這個時候咱們就找到目標數了,即 i + 1。
好吧,我以爲我講的有點囉嗦了,還一步步話題展示過程給大家看,連我本身都感受有點囉嗦了,大佬勿噴哈。最後代碼以下:
public int firstMissingPositive(int[] nums) {
if(nums == null || nums.length < 1)
return 1;
int n = nums.length;
for(int i = 0; i < n; i++){
// 這裏還有個要注意的地方,就是 nums[i] 與下標爲 nums[i]-1的數若是相等的話
// 也是不須要交換的。
while(nums[i] >= 1 && nums[i] <= n && nums[i] != i + 1 && nums[i] != nums[nums[i]-1] ){
// 和下標爲 nums[i] - 1的數進行交換
int tmp = nums[i];
nums[i] = nums[tmp - 1];
nums[tmp - 1] = tmp;
}
}
for(int i = 0; i < n; i++){
if(nums[i] != i + 1){
return i + 1;
}
}
return n + 1;
}
複製代碼
這道題我以爲仍是由挺多值得學習的地方的,例如它經過這道原地交換的方法,使指定範圍內的數組有序了。
還有就是這種經過數組下標來解決問題的方法也是一種經常使用的技巧,例如給你一副牌,讓你打亂順序,以後分發給4我的,也是能夠採用這種方法的,詳情能夠看這道題:什麼是洗牌算法。
最後推廣下個人公衆號:苦逼的碼農:公衆號裏面已經有100多篇原創文件,也分享了不少實用工具,海量視頻資源、電子書資源,關注自提。點擊掃碼關注哦。 戳我便可關注,