背景
工作多年,語言經歷過C#,JAVA。但是做過的項目大多以業務系統為主,曾經做過一些基礎架構的工作,但算法一直在工作中應用的比較少,導致多年之后基本都忘記完了。上一次面試過程中就有一個算法題,我能做對,但是感覺不是最優方案就放棄了。最近想想做為一個程序員,算法還是有必要再補習補習。
案例
有兩個數組,int[] arrayA=new int[]{1,3,1.....},int[] arrayB=new int[]{11,3,10.....},數組元素無序且有可能存在重復元素,請輸出兩個數組的交集。原題大意是這樣,細節可能有出入。
面試時我的方案
不用想,采用兩個for循環基本就能解決問題,但我又想不出來其它優化方法,想來想去,時間白白浪費最后居然連能做對的答案都沒去寫。
public void testArrayIntersectionA() {
int[] arrayA = new int[]{1, 1, 2, 3, 4, 4, 5, 1, 1};
int[] arrayB = new int[]{11, 1, 22, 3, 43, 4, 5, 11, 1, 22};
Set<Integer> intersectionSet = new HashSet();
for (int i = 0; i < arrayA.length; i++) {
for (int j = 0; j < arrayB.length; j++) {
if (arrayA[i] == arrayB[j]) {
intersectionSet.add(arrayA[i]);
}
}
}
}
當時曾經想過將數組排序然后比較,但放棄了,感覺增加了排序之后性能會不一定比上面的兩層for要優化。思路如下:
- 排序原數組
- 選擇數組元素小的數組去與大數組做比較
驗證上面的指針比較法
比如有這樣的兩個數組:

具體的做法如下:
- 排序數組

- 初始化兩數組的指針,均從0開始
- 將小數組的指針做為外層循環,在大數組中以大數組指針位置開始比較
- 如果找到相等的,記錄結果,同時將大小數組的指針向后移動

- 如果在大數組中找到末尾都沒有找到,那么小數組的指針向后移動
- 當小數組的指針移動到最后一個元素后結束算法
public void testArrayIntersectionB() {
int[] arrayA = new int[]{1, 1, 2, 3, 4, 4, 5, 1, 1};
int[] arrayB = new int[]{11, 1, 22, 3, 43, 4, 5, 11, 1, 22};
Set<Integer> intersectionSet = new HashSet();
Arrays.sort(arrayA);
Arrays.sort(arrayB);
int indexArrayA = 0;
int indexArrayB = 0;
int sizeArrayA = arrayA.length;
int sizeArrayB = arrayB.length;
while (indexArrayA < sizeArrayA) {
for (int i = indexArrayB; i < sizeArrayB; i++) {
if (arrayA[indexArrayA] == arrayB[i]) {
intersectionSet.add(arrayA[indexArrayA]);
indexArrayA++;
indexArrayB++;
break;
} else if (i == sizeArrayB - 1) {
indexArrayA++;
}
}
}
}
為了測試的准確性,可以將數組的元素增多,文中只是示意的寫了幾個元素,實際測試過程中可以增大元素個數。同時將方法重復執行500次或者更多來測試。得到的結論是排序之后的指針方法要快於簡單的兩層for,具體的數據我就不貼了,因為與數組元素的組成有一定的關系。
指針比較法的優化
上面的邏輯是,從大數組的某個位置開始比較至到數組的最后一個元素,但因為我們的數組已經經過排序,實際上我們只需要比較到第一個大於的數就可以結束比較,因為后面的元素一定比前面的元素要大。

public void testArrayIntersectionC() {
int[] arrayA = new int[]{1, 1, 2, 3, 4, 4, 5, 1, 1};
int[] arrayB = new int[]{11, 1, 22, 3, 43, 4, 5, 11, 1, 22};
Set<Integer> intersectionSet = new HashSet();
Arrays.sort(arrayA);
Arrays.sort(arrayB);
int indexArrayA = 0;
int indexArrayB = 0;
int sizeArrayA = arrayA.length;
int sizeArrayB = arrayB.length;
while (indexArrayA < sizeArrayA) {
for (int i = indexArrayB; i < sizeArrayB; i++) {
if (arrayA[indexArrayA] == arrayB[i]) {
intersectionSet.add(arrayA[indexArrayA]);
indexArrayA++;
indexArrayB++;
break;
} else if (arrayA[indexArrayA] < arrayB[i]) {
indexArrayA++;
break;
} else if (i == sizeArrayB - 1) {
indexArrayA++;
}
}
}
}
測試結論是此方法優化有效,特別是在特定的數據場景下。
利用java已有結構Set如何?
繼承了Collection接口的,包含一個retainAll的方法,我們利用Set可以非常輕松的來完成兩個數組的交集。但它只能處理對象類型的Integer,所以我們先要將int[] 轉換成Integer[],然后利用addAll以及retailAll來計算數組的交集。
public void testArrayIntersectionD() {
int[] arrayA = new int[]{1, 1, 2, 3, 4, 4, 5, 1, 1};
int[] arrayB = new int[]{11, 1, 22, 3, 43, 4, 5, 11, 1, 22};
int sizeArrayA=arrayA.length;
int sizeArrayB=arrayB.length;
Integer[] arrayA2=new Integer[sizeArrayA];
Integer[] arrayB2=new Integer[sizeArrayB];
for(int i=0;i<sizeArrayA;i++){
arrayA2[i]=new Integer(arrayA[i]);
}
for(int i=0;i<sizeArrayB;i++){
arrayB2[i]=new Integer(arrayB[i]);
}
Set<Integer> intersectionSet = new HashSet<Integer>();
intersectionSet.addAll(Arrays.asList(arrayA2));
intersectionSet.retainAll(Arrays.asList(arrayB2));
}
同樣也是執行500次,利用Set求交集的性能最好。下面是retainAll的源碼:應該是利用了遍歷最快的迭代器的原因,后續再找時間求證下。
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
Iterator<E> it = iterator();
while (it.hasNext()) {
if (!c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}
利用隊列 (此方法有數量級的優勢,比較的數組元素擴大到隨機生成的10000個int)
將原數組進行排序,然后將數組加入到隊列中,拿元素個數較小的做為循環條件,比較兩個隊列peek數值。相等則輸出並出隊列,否則將較小值所在的隊列進行出隊列操作至到某個隊列為空結束循環。
public void testArrayIntersectionE(int[] arrayA,int[] arrayB) {
int sizeArrayA=arrayA.length;
int sizeArrayB=arrayB.length;
Arrays.sort(arrayA);
Arrays.sort(arrayB);
Queue<Integer> queueA=new ArrayBlockingQueue<Integer>(sizeArrayA);
Queue<Integer> queueB=new ArrayBlockingQueue<Integer>(sizeArrayB);
for(int i=0;i<sizeArrayA;i++){
queueA.add(arrayA[i]);
}
for(int i=0;i<sizeArrayB;i++){
queueB.add(arrayB[i]);
}
Set<Integer> intersectionSet = new HashSet<Integer>();
while (!queueA.isEmpty()){
Integer valueA=queueA.peek();
Integer valueB=queueB.peek();
if(null==valueA||null==valueB){
break;
}
if(valueA.equals(valueB)){
intersectionSet.add(valueA);
queueA.poll();
queueB.poll();
}
else if(valueA>valueB){
queueB.poll();
}
else if(valueA<valueB){
queueA.poll();
}
}
}
示意過程如下:

擴展問題,如果數組不是int[],而直接是Integer[],數據結果會有變化嗎?
上面有提到,當時面試時我考慮的是數組排序,經過測試int[]的排序要快於Integer[]排序,數組的復制也是一樣。這在一定程序上會引起測試結果的變化。同時數組內元素的內容也會影響測試結果。
