1. 程式人生 > >一個大小為n的陣列,裡面的數都屬於範圍[0, n-1],有不確定的重複元素,找到至少一個重複元素,要求O(1)空間和O(n)時間

一個大小為n的陣列,裡面的數都屬於範圍[0, n-1],有不確定的重複元素,找到至少一個重複元素,要求O(1)空間和O(n)時間

轉自:點選開啟連結

這個題目要求用O(n)的時間複雜度,這意味著只能遍歷陣列一次。同時還要尋找重複元素,很容易想到建立雜湊表來完成,遍歷陣列時將每個元素對映到雜湊表中,如果雜湊表中已經存在這個元素則說明這就是個重複元素。因此直接使用C++ STL中的hash_set(參見《STL系列之六 sethash_set》)可以方便的在O(n)時間內完成對重複元素的查詢。

    但是題目卻在空間複雜度上有限制——要求為O(1)的空間。因此採用雜湊表這種解法肯定在空間複雜度上是不符合要求的。但可以沿著雜湊法的思路繼續思考,題目中陣列中所以數字都在範圍[0

 n-1],因此雜湊表的大小為n即可。因此我們實際要做的就是對n個範圍為0n-1的數進行雜湊,而雜湊表的大小剛好為n。對排序演算法比較熟悉的同學不難發現這與一種經典的排序演算法——基數排序非常類似。而基數排序的時間空間複雜度剛好符合題目要求!因此嘗試使用基數排序來解這道面試題。

 

    下面以2415761902這十個數為例,展示下如何用基數排序來查詢重複元素。

下標

  0

  1

  2

  3

  4

  5

  6

  7

  8

  9

資料

  2

  4

  1

  5

  7

  6

  1

  9

  0

  2

1)由於第0個元素a[0] 等於2不為0,故交換a[0]a[a[0]]即交換a[0]a[2]得:

下標

  0

  1

  2

  3

  4

  5

  6

  7

  8

  9

資料

  1

  4

  2

  5

  7

  6

  1

  9

  0

  2

2)由於第0個元素a[0] 等於1不為0,故交換a[0]a[a[0]]即交換a[0]a[1]得:

下標

  0

  1

  2

  3

  4

  5

  6

  7

  8

  9

資料

  4

  1

  2

  5

  7

  6

  1

  9

  0

  2

3)由於第0個元素a[0] 等於4不為0,故交換a[0]a[a[0]]即交換a[0]a[4]得:

下標

  0

  1

  2

  3

  4

  5

  6

  7

  8

  9

資料

  7

  1

  2

  5

  4

  6

  1

  9

  0

  2

4)由於第0個元素a[0] 等於7不為0,故交換a[0]a[a[0]]即交換a[0]a[7]得:

下標

  0

  1

  2

  3

  4

  5

  6

  7

  8

  9

資料

  9

  1

  2

  5

  4

  6

  1

  7

  0

  2

5)由於第0個元素a[0] 等於9不為0,故交換a[0]a[a[0]]即交換a[0]a[9]得:

下標

  0

  1

  2

  3

  4

  5

  6

  7

  8

  9

資料

  2

  1

  2

  5

  4

  6

  1

  7

  0

  9

6)由於第0個元素a[0] 等於2不為0,故交換a[0]a[a[0]]即交換a[0]a[2],但a[2]也為2a[0]相等,因此我們就找到了一個重複的元素——2

下標

  0

  1

  2

  3

  4

  5

  6

  7

  8

  9

資料

  2

  1

  2

  5

  4

  6

  1

  7

  0

  9

     有了上面的分析,程式碼不難寫出:

  1. //GOOGLE面試題
  2. //一個大小為n的陣列,裡面的數都屬於範圍[0, n-1],有不確定的重複元素,找到至少一個重複元素,要求O(1)空間和O(n)時間。
  3. //By MoreWindows (http://blog.csdn.net/MoreWindows)
  4. #include <stdio.h>
  5. const int NO_REPEAT_FLAG = -1;
  6. void Swap(int &x, int &y)
  7. {
  8. int t = x;
  9. x = y;
  10. y = t;
  11. }
  12. //類似於基數排序,找出陣列中第一個重複元素。
  13. int RadixSort(int a[], int n)
  14. {
  15. int i;
  16. for (i = 0; i < n; i++)
  17. {
  18. while (i != a[i])
  19. {
  20. if (a[i] == a[a[i]])
  21. return a[i];
  22. Swap(a[i], a[a[i]]);
  23. }
  24. }
  25. return NO_REPEAT_FLAG;
  26. }
  27. void PrintfArray(int a[], int n)
  28. {
  29. for (int i = 0; i < n; i++)
  30. printf("%d ", a[i]);
  31. putchar('\n');
  32. }
  33. int main()
  34. {
  35. printf(" 白話經典算法系列之十 一道有趣的GOOGLE面試題 \n");
  36. printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
  37. const int MAXN = 10;
  38. int a[MAXN] = {2, 4, 1, 5, 7, 6, 1, 9, 0, 2};
  39. //int a[MAXN] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  40. printf("陣列為: \n");
  41. PrintfArray(a, MAXN);
  42. int nRepeatNumber = RadixSort(a, MAXN);
  43. if (nRepeatNumber != NO_REPEAT_FLAG)
  44. printf("該陣列有重複元素,此元素為%d\n", nRepeatNumber);
  45. else
  46. printf("該陣列沒有重複元素\n");
  47. return 0;
  48. }

外層迴圈是O(n),內層迴圈準確的說是O(k),k是一個常數, while(a[i]!=i) 這個迴圈是常熟次的,具體幾次是不一定的,當滿足不條件就會退出了,但也有一個範圍k在[1,n-1]上取。所以時間複雜度是O(kn),k是常數所以整個的時間複雜度是O(n);這道題關鍵在於優化了空間。


方法二:

  1. int Repeat(int *a, int n)
  2. {
  3. for(int i = 0; i < n; i++)
  4. {
  5. if(a[i] > 0) //判斷條件
  6. {
  7. if(a[ a[i] ] < 0)
  8. {
  9. return a[i];//已經被標上負值了,有重複
  10. }
  11. else
  12. {
  13. a[ a[i] ]= -a[a[i]]; //記為負
  14. }
  15. }
  16. else // 此時|a[i]|代表的值已經出現過一次了
  17. {
  18. if(a[-a[i]] < 0)
  19. {
  20. return -a[i];//有重複找到
  21. }
  22. else
  23. {
  24. a[ -a[i] ] = -a[ -a[i] ];
  25. }
  26. }
  27. }
  28. return -1;//陣列中沒有重複的數
  29. }

下面對這種以取負為訪問標誌的方法用個例項來說明下:

    設int a[] = {1, 2, 1}

    第一步:由於a[0]等於1大於0,因此先判斷下a[a[0]]a[1]是否小於0,如果小於,說明這是第二次訪問下標為1的元素,表明我們已經找到了重複元素。不是則將a[a[0]]取負,a[1]=-a[1]=-2

    第二步:由於a[1]等於-2,因此先判斷下a[-a[1]]取出a[2]是否小於0,如果小於,說明這是第二次訪問下標為2的元素,表明我們已經找到了重複元素。不是則將a[-a[1]]取負,a[2]=-a[2]=-1

    第三步:由於a[2]等於-1,因此判斷下a[-a[2]]a[1]是否小於0,由於a[1]在第一步中被取反過了,因此證明這是第二次訪問下標為1的元素,直接返回-a[2]即可。

 

這種通過取負來判斷元素是否重複訪問的方法正如網友jwfeng002所言,當陣列第0個元素為0且資料中只有0重複時是無法找出正確解的。只要用:

       const int MAXN = 5;

       int a[MAXN] = {0, 1, 2, 3, 0};

這組資料來測試,就會發現該方法無法判斷0是個重複出現的元素。執行結果如下圖所示:

 

這個演算法雖然有缺陷,但我們可以沿著這個演算法的思路——這個演算法之所以用到了取負,是因此根據題目條件,陣列中資料範圍為[0n-1],因此可以通過判斷元素是否大於0來決定這個元素是未訪問過的資料還是已訪問過的資料。但也正因為對0的取負是無效操作決定了這個演算法存在著缺陷。要改進一下也很簡單——不用取負,而用加n。這樣通過判斷元素是否大於等於n就能決定這個元素是未訪問過的資料還是已訪問過的資料。完整程式碼如下:

[cpp]  view plain copy
  1. //GOOGLE面試題  
  2. //一個大小為n的陣列,裡面的數都屬於範圍[0, n-1],有不確定的重複元素,找到至少一個重複元素,要求O(1)空間和O(n)時間。  
  3. //By MoreWindows (http://blog.csdn.net/MoreWindows)  
  4. #include <stdio.h>  
  5. const int NO_REPEAT_FLAG = -1;  
  6. int FindRepeatNumberInArray(int *a, int n)  
  7. {  
  8.     for(int i = 0; i < n; i++)  
  9.     {  
  10.         int nRealIndex = a[i] >= n ? a[i] - n : a[i];  
  11.         if (a[nRealIndex] >= n) //這個位置上的值大於n說明已經是第二次訪問這個位置了  
  12.             return nRealIndex;  
  13.         else  
  14.             a[nRealIndex] += n;  
  15.     }  
  16.     return NO_REPEAT_FLAG; //陣列中沒有重複的數  
  17. }  
  18. void PrintfArray(int a[], int n)  
  19. {  
  20.     for (int i = 0; i < n; i++)  
  21.         printf("%d ", a[i]);  
  22.     putchar('\n');  
  23. }  
  24. int main()  
  25. {  
  26.     printf("    白話經典算法系列之十一 一道有趣的GOOGLE面試題解法2\n");        
  27.     printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");   
  28.   
  29.     const int MAXN = 10;  
  30.     //int a[MAXN] = {2, 4, 1, 5, 7,  6, 1, 9, 0, 2};  
  31.     int a[MAXN] = {0, 1, 2, 3, 4,  5, 6, 7, 8, 0};  
  32.       
  33.     printf("陣列為: \n");  
  34.     PrintfArray(a, MAXN);  
  35.   
  36.     int nRepeatNumber = FindRepeatNumberInArray(a, MAXN);  
  37.     if (nRepeatNumber != NO_REPEAT_FLAG)  
  38.         printf("該陣列有重複元素,此元素為%d\n", nRepeatNumber);  
  39.     else  
  40.         printf("該陣列沒有重複元素\n");  
  41.     return 0;  
  42. }  

執行結果如圖所示:

如同上一篇《白話經典算法系列之十一道有趣的GOOGLE面試題》一樣,演算法的核心程式碼依然只有短短5行左右。在時間空間複雜度上也同樣滿足題目要求。


相信由這篇文章可以看出,思維的轉換性對尋找一個合適演算法是非常有用的。

 

另外,程式碼的書寫也要注意一下,對比一下文章中的Repeat()函式與FindRepeatNumberInArray()就能發現對程