一個大小為n的陣列,裡面的數都屬於範圍[0, n-1],有不確定的重複元素,找到至少一個重複元素,要求O(1)空間和O(n)時間
轉自:點選開啟連結
這個題目要求用O(n)的時間複雜度,這意味著只能遍歷陣列一次。同時還要尋找重複元素,很容易想到建立雜湊表來完成,遍歷陣列時將每個元素對映到雜湊表中,如果雜湊表中已經存在這個元素則說明這就是個重複元素。因此直接使用C++ STL中的hash_set(參見《STL系列之六 set與hash_set》)可以方便的在O(n)時間內完成對重複元素的查詢。
但是題目卻在空間複雜度上有限制——要求為O(1)的空間。因此採用雜湊表這種解法肯定在空間複雜度上是不符合要求的。但可以沿著雜湊法的思路繼續思考,題目中陣列中所以數字都在範圍[0
下面以2,4,1,5,7,6,1,9,0,2這十個數為例,展示下如何用基數排序來查詢重複元素。
下標 |
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]也為2與a[0]相等,因此我們就找到了一個重複的元素——2。
下標 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
資料 |
2 |
1 |
2 |
5 |
4 |
6 |
1 |
7 |
0 |
9 |
有了上面的分析,程式碼不難寫出:
- //GOOGLE面試題
- //一個大小為n的陣列,裡面的數都屬於範圍[0, n-1],有不確定的重複元素,找到至少一個重複元素,要求O(1)空間和O(n)時間。
- //By MoreWindows (http://blog.csdn.net/MoreWindows)
- #include <stdio.h>
- const int NO_REPEAT_FLAG = -1;
- void Swap(int &x, int &y)
- {
- int t = x;
- x = y;
- y = t;
- }
- //類似於基數排序,找出陣列中第一個重複元素。
- int RadixSort(int a[], int n)
- {
- int i;
- for (i = 0; i < n; i++)
- {
- while (i != a[i])
- {
- if (a[i] == a[a[i]])
- return a[i];
- Swap(a[i], a[a[i]]);
- }
- }
- return NO_REPEAT_FLAG;
- }
- void PrintfArray(int a[], int n)
- {
- for (int i = 0; i < n; i++)
- printf("%d ", a[i]);
- putchar('\n');
- }
- int main()
- {
- printf(" 白話經典算法系列之十 一道有趣的GOOGLE面試題 \n");
- printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
- const int MAXN = 10;
- int a[MAXN] = {2, 4, 1, 5, 7, 6, 1, 9, 0, 2};
- //int a[MAXN] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
- printf("陣列為: \n");
- PrintfArray(a, MAXN);
- int nRepeatNumber = RadixSort(a, MAXN);
- if (nRepeatNumber != NO_REPEAT_FLAG)
- printf("該陣列有重複元素,此元素為%d\n", nRepeatNumber);
- else
- printf("該陣列沒有重複元素\n");
- return 0;
- }
外層迴圈是O(n),內層迴圈準確的說是O(k),k是一個常數, while(a[i]!=i) 這個迴圈是常熟次的,具體幾次是不一定的,當滿足不條件就會退出了,但也有一個範圍k在[1,n-1]上取。所以時間複雜度是O(kn),k是常數所以整個的時間複雜度是O(n);這道題關鍵在於優化了空間。
方法二:
- int Repeat(int *a, int n)
- {
- for(int i = 0; i < n; i++)
- {
- if(a[i] > 0) //判斷條件
- {
- if(a[ a[i] ] < 0)
- {
- return a[i];//已經被標上負值了,有重複
- }
- else
- {
- a[ a[i] ]= -a[a[i]]; //記為負
- }
- }
- else // 此時|a[i]|代表的值已經出現過一次了
- {
- if(a[-a[i]] < 0)
- {
- return -a[i];//有重複找到
- }
- else
- {
- a[ -a[i] ] = -a[ -a[i] ];
- }
- }
- }
- return -1;//陣列中沒有重複的數
- }
下面對這種以取負為訪問標誌的方法用個例項來說明下:
設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是個重複出現的元素。執行結果如下圖所示:
這個演算法雖然有缺陷,但我們可以沿著這個演算法的思路——這個演算法之所以用到了取負,是因此根據題目條件,陣列中資料範圍為[0,n-1],因此可以通過判斷元素是否大於0來決定這個元素是未訪問過的資料還是已訪問過的資料。但也正因為對0的取負是無效操作決定了這個演算法存在著缺陷。要改進一下也很簡單——不用取負,而用加n。這樣通過判斷元素是否大於等於n就能決定這個元素是未訪問過的資料還是已訪問過的資料。完整程式碼如下:
[cpp] view plain copy- //GOOGLE面試題
- //一個大小為n的陣列,裡面的數都屬於範圍[0, n-1],有不確定的重複元素,找到至少一個重複元素,要求O(1)空間和O(n)時間。
- //By MoreWindows (http://blog.csdn.net/MoreWindows)
- #include <stdio.h>
- const int NO_REPEAT_FLAG = -1;
- int FindRepeatNumberInArray(int *a, int n)
- {
- for(int i = 0; i < n; i++)
- {
- int nRealIndex = a[i] >= n ? a[i] - n : a[i];
- if (a[nRealIndex] >= n) //這個位置上的值大於n說明已經是第二次訪問這個位置了
- return nRealIndex;
- else
- a[nRealIndex] += n;
- }
- return NO_REPEAT_FLAG; //陣列中沒有重複的數
- }
- void PrintfArray(int a[], int n)
- {
- for (int i = 0; i < n; i++)
- printf("%d ", a[i]);
- putchar('\n');
- }
- int main()
- {
- printf(" 白話經典算法系列之十一 一道有趣的GOOGLE面試題解法2\n");
- printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
- const int MAXN = 10;
- //int a[MAXN] = {2, 4, 1, 5, 7, 6, 1, 9, 0, 2};
- int a[MAXN] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 0};
- printf("陣列為: \n");
- PrintfArray(a, MAXN);
- int nRepeatNumber = FindRepeatNumberInArray(a, MAXN);
- if (nRepeatNumber != NO_REPEAT_FLAG)
- printf("該陣列有重複元素,此元素為%d\n", nRepeatNumber);
- else
- printf("該陣列沒有重複元素\n");
- return 0;
- }
執行結果如圖所示:
如同上一篇《白話經典算法系列之十一道有趣的GOOGLE面試題》一樣,演算法的核心程式碼依然只有短短5行左右。在時間空間複雜度上也同樣滿足題目要求。
相信由這篇文章可以看出,思維的轉換性對尋找一個合適演算法是非常有用的。
另外,程式碼的書寫也要注意一下,對比一下文章中的Repeat()函式與FindRepeatNumberInArray()就能發現對程