1. 程式人生 > >一道能做出來就腳踢BAT的高難度演算法題:在元素重複三次的陣列中查詢重複一次的元素

一道能做出來就腳踢BAT的高難度演算法題:在元素重複三次的陣列中查詢重複一次的元素

我們看一道難度很高的查詢類演算法題,如果你真能在一小時內給出正確的演算法和編碼,那麼你隨便在BAT開口年薪一百萬都不算過分。我們先看題目:給定一個數組,它裡面除了一個元素外,其他元素都重複了三次,要求在空間複雜度為O(1),時間複雜度為O(n)的約束下,查詢到只重複了一次的元素。

在一個小時內設計出滿足條件的演算法並編寫正確的程式碼,難度相當大。我們先從簡單的角度思考,一種做法是先將陣列進行排序,然後從頭到尾遍歷一次,就可以找到重複一次的元素,但問題在於排序所需要時間為O(n*lg(n)),這就超出了題目對時間的限制,從題目的要求看,不能分配多餘空間,並且時間複雜度只能是O(n),這意味著演算法必須對陣列遍歷1次就要找出給定元素。

普通的查詢演算法在給定條件約束下都無法適用,此時我們必須考慮複雜抽象的位操作。根據題目描述,除了一個元素外,其餘元素都重複了三次,我們拿到一個重複3次的元素,將其轉換為二進位制,如果某個位元位的值是1,那麼如果我們遍歷一次陣列,該位置見到的1一定超過3次以上。看一個具體例子,假設一個重複三次的元素值是2,它的二進位制格式為011,那重複三次就是010,010,010,於是下標為0和1的位元位的1就出現了3次,假設我們有一種機制,能夠在某個位元位上檢測到該位出現的1有三次就清零,那麼所有重複三次的元素將會被清除,只剩下重複1次的元素。

我們看個具體例子,例如2,2,2,3,他們對應的二進位制位010,010,011,010,把他們累起來有:
010
010
010 =(下標為1的位元位上出現三次1,因此把該位清除為0)=>000
011 011 => 011 => 3
從上面例子看到,我們只要監控每一個位元位,一旦發現在該位元位上出現三次1就把它清0,由於除了一個元素外,其他元素都重複了三次,因此相應的位元位上肯定都相應出現三次1,而只重複1次的元素在相應位元位上的1只出現1次因此不會被清零,由此遍歷一次後,只有出現1次的元素的位元位上的1保留下來,這樣我們就把出現1次的元素給抽取出來。

問題在於我們如何實現監控每個位元位是否出現三次1的機制。我們設定兩個變數towOnes,oneOnes,當某個位元位第一次出現1時,我們把oneOnes對應的位置位元位設定為1,當某個位元位第二次出現為1時,把oneOnes對應的位元位設定為0,在twoOnes對應的位元位設定為1,當對應位元位第三次出現1時,將towOnes對應位元位設定為0,下面的程式碼可以實現位元位的監控機制:

//E是當前從陣列中讀入的元素
int T = towOnes;
int O = oneOnes;
twoOnes = T ^ (T & E);  //如果某個位元位上出現三次1的話將其清除
E = E ^ (T & E);  //把出現三次1的位元位上的1清除
twoOnes = twoOnes | (E & O) ; //如果某個位元位上出現兩次1,將其設定到twoOnes對應的位元位上
oneOnes = O ^ E ; //將出現1次的位元位設定到oneOnes上

我們用例項將上面步驟走一遍以便獲得跟深刻理解,假設當前陣列內容為:2,3,2,2,一開始towOnes =0, oneOnes = 0,一開始讀入的元素為2,二進位制位010,於是執行程式碼所示的四個步驟:
towOnes = 0 ^ (0 & 010) = 0;
E = 010 ^ (0 & 0) = 010
twoOnes = 0 | (0 & 0) = 0;
oneOnes = 0 ^ 010 = 010
於是,T = 0, O = 010
我們看下標為1的位元位第一次出現1時,oneOnes對應的位元位也設定為1.我們再看輸入第二個元素的處理,第二個元素是3,二進位制位011,於是有:
twoOnes = 0 ^ (0 & 011) = 0
E = 011 ^ (0 & 011) = 011
twoOnes = 0 | (011 & 010) = 010
oneOnes = 010 ^ 011 = 001
於是有T = 010, O = 001
從上面結果看出,下標為1的位元位連續出現兩個1,於是twoOnes對應的位元位也設定為1,oneOnes對應位元位設定為0,下標為0的位元位第一次出現1,所以oneOnes對應位元位設定為1.
再次讀入第三個元素2,其二進位制位010,於是有:
twoOnes = 010 ^ (010 & 010) = 0
E = 010 ^ (010 & 010) = 0
twoOnes = 0 | (0 & 001) = 0
oneOnes = 001 ^ 0 = 001
於是有T=0, O=001
從上面運算過程我們看到,再次讀入010時,twoOnes在給定下標的位置也是1,也就是下標為1的位元位上此時看到1第三次出現,於是把twoOnes在相應位置上的位元位清0,oneOnes位元位上的數字保持不變。
讀入最後一個元素是2,也是010,相應的運算過程有:
twoOnes = 0 ^ (0 & 010) = 0
E = 010 ^ (0 & 010) = 010
twoOnes = 0 | (010 & 001) = 0
oneOnes = 001 ^ 010 = 011

此時所有元素處理完畢,oneOnes位元位對應的1恰好就是隻重複一次的元素3對應位元位上的1,也就是oneOnes對應的值就是隻重複1次元素的值。我們遍歷陣列所有元素,執行上面演算法後就可以得到只重複1次的元素值,由於演算法只需遍歷一次陣列,同時沒有分配任何新記憶體,因此時間複雜度是O(n),空間複雜度是O(1)。我們看看完整實現程式碼:

import java.util.Arrays;
import java.util.HashSet;
import java.util.Random;

public class Searching {
	int extractOneTimeElement(int[] threeTimesArray) {
		//分別用於記錄只出現1次的位元位和2次的位元位
		int  oneOnes = 0;
		int  twoOnes = 0;
		
		for (int i = 0; i < threeTimesArray.length; i++) {
			int E = threeTimesArray[i];
			int T = twoOnes; 
			int O = oneOnes;
			twoOnes = T ^ (T & E); //去除那些出現過三次的位元位1
	        E = E ^ (T & E); //去除出現過3次的位元位1
	        twoOnes = twoOnes | (E & O); //設定出現兩次的位元位1
	        oneOnes = O ^ E; //設定只出現1次的位元位1
		}
		
		return oneOnes;
	}
	
	 public static void main(String[] args) {
		int[] A = new int[]{2, 2, 2, 4, 7, 11, 4, 4, 5,5,5, 7,7, 9,9,9};
		Searching s = new Searching();
		int oneTimeElement = s.extractOneTimeElement(A);
		System.out.println("The one time element is : " + oneTimeElement);
	 }
}

程式碼中初始化了一個數組,裡面除了11外所有元素都重複出現3次,程式碼執行後輸出的結果正是隻從復出現1次的數值11.

更詳細的講解和程式碼除錯演示過程,請點選連結

更多技術資訊,包括作業系統,編譯器,面試演算法,機器學習,人工智慧,請關照我的公眾號:
這裡寫圖片描述