1. 程式人生 > >經典大資料面試題

經典大資料面試題

什麼是大資料?

大資料(big data,mega data),或稱巨量資料,指的是需要新處理模式才能具有更強的決策力、洞察力和流程優化能力的海量、高增長率和多樣化的資訊資產。 在維克托·邁爾-舍恩伯格及肯尼斯·庫克耶編寫的《大資料時代》中大資料指不用隨機分析法(抽樣調查)這樣的捷徑,而採用所有資料進行分析處理。大資料的4V特點:Volume(大量)、Velocity(高速)、Variety(多樣)、Value(價值)。

1>給一個超過100G大小的log file,log中存著IP地址 ,設計演算法找到出現次數最多的IP地址?
答:首先看到100G的日誌檔案,我們的第一反應肯定 是太大了,根本載入不到記憶體,更別說設計演算法了, 那麼怎麼辦呢?既然裝不下,我們是不是可以將其切 分開來,一小部分一小部分輪流進入記憶體呢,答案當然是肯定的。

       在這裡要記住一點:但凡是大資料的問題,都可通過 切分來解決它。
吐舌頭粗略算一下:如果我們將其分成1000個小檔案,每個檔案大概就是500M左右的樣子,

現在計算機肯定輕輕 鬆鬆就能裝下。

 哭那麼,問題又來了,怎樣才能保證相 同的IP被分到同一個檔案中呢?
這裡我想到的是雜湊切分,使用相同的雜湊函式(如 BKDRHash)將所有IP地址轉換為一個整數key,

再利用 index=key%1000就可將相同IP分到同一個檔案。
吐舌頭依次將這1000個檔案讀入記憶體,出現次數最多的IP進行統計。
吐舌頭最後,在1000個出現次數最多的IP中找出最大的出現次數即為所求。

用到的雜湊函式:

template<class T>  
size_t BKDRHash(const T *str)  
{  
	register size_t hash = 0;  
	while (size_t ch = (size_t)*str++)  
	{         
		hash = hash * 131 + ch;   // 也可以乘以31、131、1313、13131、131313..          
	}  
	return hash;  
}  




2>與上題條件相同,如何找到TOP K的IP?
答:這倒題說白了就是找前K個出現次數最多的IP,即 降序排列IP出現的次數。

吐舌頭與上題類似,我們用雜湊切分對分割的第一個個小檔案中出現最多的前K個IP建小堆
吐舌頭然後讀入第二個檔案,將其出現次數最多的前K個IP與 堆中資料進行對比,
吐舌頭如果包含大於堆中的IP出現次數,則更新小堆,替換原堆中次數的出現少的資料
吐舌頭再讀入第三個檔案,以此類推……

吐舌頭直到1000個檔案全部讀完,堆中出現的K個IP即是出現 次數最多的前K個IP地址。




3>給定100億個整數,設計演算法找到只出現一次的整數 ?
答:看到此題目,我的第一反應就是估算其佔用記憶體 的大小:100億個int,一個int4個位元組,100億*4=400 億位元組
又因為42億位元組約等於4G,所以100億個整數大概佔用 的記憶體為40G,一次載入到記憶體顯然是不太現實的。
反過來想,所有整數所能表示的範圍為2^32,即16G, 故給出的資料有很多資料是重複的



吐舌頭解法1:雜湊切分
與第一題類似,用雜湊切分將這些資料分到100個檔案 中,每個檔案大約400M,

將每一個檔案依次載入到記憶體中,利用雜湊表統計出 現一次的整數,

將100個檔案中出現一次的整數彙總起來即為所求。
吐舌頭解法2:點陣圖變形
我們知道,點陣圖是利用每一位來表示一個整數是否存 在來節省空間,1表示存在,0表示不存在。
而上題對於所有整數而言,卻有三種狀態:不存在、 存在一次、存在多次。
故此,我們需要對傳統點陣圖進行擴充套件,用兩位來表示 一個整數的狀態:00表示不存在、

01表示存在一次, 10表示存在多次,11表示無效狀態。
按照上述表示,兩位表示一個整數狀態,所有整數只 需要1G即可表示其存在與否。
吐舌頭解法3:
眾所周知,一個整數佔32位,那麼我們可對每一位按 照0和1將其分為兩個檔案,

直到劃分到最低位,如果 被分的檔案中哪個檔案只包含一個數據,那麼,此資料即為只出現一次的整數。

如下圖:


4>給兩個檔案,分別有100億個整數,我們只有1G記憶體 ,如何找到兩個檔案交集?
答:100億*4位元組 = 400億位元組 = 40G
吐舌頭解法1:普通查詢
將其中的一個檔案分為100個小檔案,每一份佔400M, 將每一小份輪流加到記憶體中,
與第二個檔案中的資料進行對比,找到交集。此種算 法時間複雜度為O(N*N)
吐舌頭解法2:雜湊切分
對兩個檔案分別進行雜湊切分,將其分為100個小檔案 ,index=key%100(index為檔案下標)
將兩個檔案中下標相同的小檔案進行對比,找出其交 集。
將100個檔案的交集彙總起來即為所給檔案的檔案交集 。此種演算法時間複雜度為O(N)
吐舌頭解法3:點陣圖
我們知道,點陣圖中的每一位就可代表一個整數的存在 與否,而16G的整數用點陣圖512M即可表示,

將第一個檔案中的整數對映到點陣圖中去,拿第二個檔案中的數字到第一個檔案對映的點陣圖中去 對比,

相同數字存在即為交集。此種演算法時間複雜度 為O(N)
注意:重複出現的數字交集中只會出現一次

點陣圖的簡單模擬:

//點陣圖:專門用來判斷資料是否存在,不能統計資料出現的次數
class BitMap
{
public:
	BitMap(size_t N = 1024)//N代表需要判斷的資料個數
	{
		_array.resize((N>>5) + 1);//相當於(N/32)+1,結果為需要開闢的位元組個數
	}

	void Set(size_t value)//將狀態由無置為有,即0變為1
	{
		size_t index = value >> 5;//代表整數的下標,即第幾個整數
		size_t num = value % 32;//代表第幾位
		
		_array[index] |= 1<<num;//將特定位置1
	}

	void ReSet(size_t value)//將狀態由有置為無,即1變為0
	{
		size_t index = value >> 5;//代表整數的下標,即第幾個整數
		size_t num = value % 32;//代表第幾位

		_array[index] &= (~(1<<num));//將特定位置0
	}

	bool Test(size_t value)
	{
		size_t index = value >> 5;//代表整數的下標,即第幾個整數
		size_t num = value % 32;//代表第幾位

		return _array[index] & (1<<num);
	}
protected:
	std::vector<size_t> _array;//每個size_t可判斷32個數是否存在
};

void TestBitMap()
{
	BitMap bm((size_t)-1);

	bm.Set(2);
	bm.Set(20);
	bm.Set(200);
	bm.Set(2000);
	bm.Set(20000);
	bm.Set(200000);
	bm.Set(2000000);
	bm.Set(20000000);

	bm.ReSet(2);
	bm.ReSet(2000);
	bm.ReSet(2000000);
	bm.ReSet(20000000);
	
	cout<<bm.Test(2)<<endl;
	cout<<bm.Test(20)<<endl;
	cout<<bm.Test(200)<<endl;
	cout<<bm.Test(2000)<<endl;
	cout<<bm.Test(20000)<<endl;
	cout<<bm.Test(200000)<<endl;
	cout<<bm.Test(2000000)<<endl;
	cout<<bm.Test(20000000)<<endl;
}
執行結果:







5>1個檔案有100億個int,1G記憶體,設計演算法找到出現 次數不超過兩次的所有整數?
答:類似題目3
吐舌頭解法1:雜湊切分
與第一題類似,用雜湊切分將這些資料分到100個檔案 中,每個檔案大約400M,
將每一個檔案依次載入到記憶體中,利用雜湊表統計出 現不超過兩次的整數
將100個檔案中出現不超過兩次的整數彙總起來即為所求。
吐舌頭解法2:點陣圖變形
我們知道,點陣圖是利用每一位來表示一個整數是否存  在來節省空間,1表示存在,0表示不存在。
而上題對於所有整數而言,卻有三種狀態:不存在、  存在一次、存在多次。
故此,我們需要對傳統點陣圖進行擴充套件,用兩位來表示  一個整數的狀態:00表示不存在、

01表示存在一次,  10表示存在兩次,11表示出現超過兩次。
按照上述表示,兩位表示一個整數狀態,所有整數只需要1G即可表示其存在次數。





6>給兩個檔案,分別有100億個query,我們只有1G內 存,如何找到兩個檔案交集?分別給出精確演算法和近 似演算法。
答:類似於第四題,
100億*4位元組 = 400億位元組 = 40G
吐舌頭精確演算法:雜湊切分
對兩個檔案分別進行雜湊切分,使用相同的雜湊函式 (如 BKDRHash雜湊函式)
將所有query

轉換為一個整數key ,再利用 index=key%1000就可將相同query分到同一 個檔案。(index為檔案下標)

將兩個檔案中下標相同的小檔案進行對比,找出其交 集。
將100個檔案的交集彙總起來即為所給檔案的檔案交集 。此種演算法時間複雜度為O(N)
吐舌頭近似演算法:布隆過濾器
首先使用相同的雜湊函式(如 BKDRHash雜湊函式)將所有 query轉換為一個整數key,

又因為布隆過濾器中的每 一位就可代表一個整數的存在 與否,而16G的整數用 點陣圖512M即可表示,
將第一個檔案中的整數對映到點陣圖中去,
拿第二個檔案中的數字到第一個檔案對映的點陣圖中去對比,相同數字存在即為交集。
此種演算法時間複雜度為O(N)
注意:布隆過濾器判斷不存在是確定的,而存存在在可能導致誤判,所以稱近似演算法。

布隆過濾器的簡單模擬:

各種不同的雜湊函式:



template<class T>  
size_t BKDRHash(const T *str)  
{  
	register size_t hash = 0;  
	while (size_t ch = (size_t)*str++)  
	{         
		hash = hash * 131 + ch;   // 也可以乘以31、131、1313、13131、131313..          
	}  
	return hash;  
}  
 
template<class T>  
size_t SDBMHash(const T *str)  
{  
	register size_t hash = 0;  
	while (size_t ch = (size_t)*str++)  
	{  
		hash = 65599 * hash + ch;         
		//hash = (size_t)ch + (hash << 6) + (hash << 16) - hash;  
	}  
	return hash;  
}  
  
template<class T>  
size_t RSHash(const T *str)  
{  
	register size_t hash = 0;  
	size_t magic = 63689;     
	while (size_t ch = (size_t)*str++)  
	{  
		hash = hash * magic + ch;  
		magic *= 378551;  
	}  
	return hash;  
}  

template<class T>  
size_t APHash(const T *str)  
{  
	register size_t hash = 0;  
	size_t ch;  
	for (long i = 0; ch = (size_t)*str++; i++)  
	{  
		if ((i & 1) == 0)  
		{  
			hash ^= ((hash << 7) ^ ch ^ (hash >> 3));  
		}  
		else  
		{  
			hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));  
		}  
	}  
	return hash;  
}  
  
template<class T>  
size_t JSHash(const T *str)  
{  
	if(!*str)        // 這是由本人新增,以保證空字串返回雜湊值0  
		return 0;  
	register size_t hash = 1315423911;  
	while (size_t ch = (size_t)*str++)  
	{  
		hash ^= ((hash << 5) + ch + (hash >> 2));  
	}  
	return hash;  
}  
template<class T>  
size_t DEKHash(const T* str)  
{  
	if(!*str)        // 這是由本人新增,以保證空字串返回雜湊值0  
		return 0;  
	register size_t hash = 1315423911;  
	while (size_t ch = (size_t)*str++)  
	{  
		hash = ((hash << 5) ^ (hash >> 27)) ^ ch;  
	}  
	return hash;  
}  
  
template<class T>  
size_t FNVHash(const T* str)  
{  
	if(!*str)   // 這是由本人新增,以保證空字串返回雜湊值0  
		return 0;  
	register size_t hash = 2166136261;  
	while (size_t ch = (size_t)*str++)  
	{  
		hash *= 16777619;  
		hash ^= ch;  
	}  
	return hash;  
}  
  
template<class T>  
size_t DJBHash(const T *str)  
{  
	if(!*str)   // 這是由本人新增,以保證空字串返回雜湊值0  
		return 0;  
	register size_t hash = 5381;  
	while (size_t ch = (size_t)*str++)  
	{  
		hash += (hash << 5) + ch;  
	}  
	return hash;  
}  

template<class T>  
size_t DJB2Hash(const T *str)  
{  
	if(!*str)   // 這是由本人新增,以保證空字串返回雜湊值0  
		return 0;  
	register size_t hash = 5381;  
	while (size_t ch = (size_t)*str++)  
	{  
		hash = hash * 33 ^ ch;  
	}  
	return hash;  
}  
 
template<class T>  
size_t PJWHash(const T *str)  
{  
	static const size_t TotalBits       = sizeof(size_t) * 8;  
	static const size_t ThreeQuarters   = (TotalBits  * 3) / 4;  
	static const size_t OneEighth       = TotalBits / 8;  
	static const size_t HighBits        = ((size_t)-1) << (TotalBits - OneEighth);      

	register size_t hash = 0;  
	size_t magic = 0;     
	while (size_t ch = (size_t)*str++)  
	{  
		hash = (hash << OneEighth) + ch;  
		if ((magic = hash & HighBits) != 0)  
		{  
			hash = ((hash ^ (magic >> ThreeQuarters)) & (~HighBits));  
		}  
	}  
	return hash;  
}  
  
template<class T>  
size_t ELFHash(const T *str)  
{  
	static const size_t TotalBits       = sizeof(size_t) * 8;  
	static const size_t ThreeQuarters   = (TotalBits  * 3) / 4;  
	static const size_t OneEighth       = TotalBits / 8;  
	static const size_t HighBits        = ((size_t)-1) << (TotalBits - OneEighth);      
	register size_t hash = 0;  
	size_t magic = 0;  
	while (size_t ch = (size_t)*str++)  
	{  
		hash = (hash << OneEighth) + ch;  
		if ((magic = hash & HighBits) != 0)  
		{  
			hash ^= (magic >> ThreeQuarters);  
			hash &= ~magic;  
		}         
	}  
	return hash;  
}  

布隆過濾器:
//布隆過濾器
struct __HashFunc1
{ 
	size_t operator()(const std::string& s)
	{
		return BKDRHash(s.c_str());
	}
};
struct __HashFunc2
{
	size_t operator()(const std::string& s)
	{
		return SDBMHash(s.c_str());
	}
};
struct __HashFunc3
{
	size_t operator()(const std::string& s)
	{
		return RSHash(s.c_str());
	}
};
struct __HashFunc4
{
	size_t operator()(const std::string& s)
	{
		return JSHash(s.c_str());
	}
};
struct __HashFunc5
{
	size_t operator()(const std::string& s)
	{
		return APHash(s.c_str());
	}
};


template<class K = string,
class HashFunc1 = __HashFunc1,
class HashFunc2 = __HashFunc2,
class HashFunc3 = __HashFunc3,
class HashFunc4 = __HashFunc4,
class HashFunc5 = __HashFunc5
>
class BloomFilter
{
public:
	BloomFilter(size_t N = 1024)
		:_bm(N * 10)
		,_size(N * 10)
	{}
	void Set(const K& key)
	{
		size_t hash1 = HashFunc1()(key) % _size;
		size_t hash2 = HashFunc2()(key) % _size;
		size_t hash3 = HashFunc3()(key) % _size;
		size_t hash4 = HashFunc4()(key) % _size;
		size_t hash5 = HashFunc5()(key) % _size;

		_bm.Set(hash1);
		_bm.Set(hash2);
		_bm.Set(hash3);
		_bm.Set(hash4);
		_bm.Set(hash5);

		cout<<hash1<<":"<<hash2<<":"<<hash3<<":"<<hash4<<":"<<hash5<<endl;
	}
	//void ReSet(const K& key);//不支援
	bool Test(const K& key)
	{
		size_t hash1 = HashFunc1()(key) % _size;
		if (_bm.Test(hash1) == false)
			return false;
		
		size_t hash2 = HashFunc2()(key) % _size;
		if (_bm.Test(hash2) == false)
			return false;

		size_t hash3 = HashFunc3()(key) % _size;
		if (_bm.Test(hash3) == false)
			return false;

		size_t hash4 = HashFunc4()(key) % _size;
		if (_bm.Test(hash4) == false)
			return false;

		size_t hash5 = HashFunc5()(key) % _size;
		if (_bm.Test(hash5) == false)
			return false;

		return true;
	}
protected:
	BitMap _bm;
	size_t _size;
};

測試函式:
void TestBloomBitMap()
{
	BloomFilter<> bm(1024);
	bm.Set("http://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html");
	bm.Set("http://www.cnblogs.com/-clq/archive/2012/05/31/2528154.html");
	bm.Set("http://www.cnblogs.com/-clq/archive/2012/05/31/2528155.html");
	bm.Set("http://www.cnblogs.com/-clq/archive/2012/05/31/2528156.html");
	bm.Set("http://www.cnblogs.com/-clq/archive/2012/05/31/2528157.html");

	cout<<bm.Test("http://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html")<<endl;
	cout<<bm.Test("http://www.cnblogs.com/-clq/archive/2012/05/31/2528158.html")<<endl;
	cout<<bm.Test("http://www.cnblogs.com/-clq/archive/2012/05/31/2528154.html")<<endl;
}
執行結果:




7>如何擴充套件BloomFilter使得它支援刪除元素的操作?
答:因為一個布隆過濾器的key對應多個為位,衝突的 概率比較大,所以不支援刪除,因為刪除有可能影響 到其他元素。如果要對其元素進行刪除,就不得不對 每一個位進行引用計數,同下題。




8>如何擴充套件BloomFilter使得它支援計數的操作?
答:我們都知道,點陣圖非常的節省空間,但由於每一 位都要引入一個int,所以空間浪費還是比較嚴重的, 因此不得不放棄點陣圖了,程式碼如下:

//帶刪除功能的布隆過濾器(引用計數)
template<class K = string,
class HashFunc1 = __HashFunc1,
class HashFunc2 = __HashFunc2,
class HashFunc3 = __HashFunc3,
class HashFunc4 = __HashFunc4,
class HashFunc5 = __HashFunc5
>
class RefBloomFilter
{
public:
	RefBloomFilter(size_t N = 1024)
		:_size(N * 10)
	{
		_refbm.resize(_size);
	}
	void Set(const K& key)
	{
		size_t hash1 = HashFunc1()(key) % _size;
		size_t hash2 = HashFunc2()(key) % _size;
		size_t hash3 = HashFunc3()(key) % _size;
		size_t hash4 = HashFunc4()(key) % _size;
		size_t hash5 = HashFunc5()(key) % _size;

		_refbm[hash1]++;
		_refbm[hash2]++;
		_refbm[hash3]++;
		_refbm[hash4]++;
		_refbm[hash5]++;

		cout<<hash1<<":"<<hash2<<":"<<hash3<<":"<<hash4<<":"<<hash5<<endl;
	}
	void ReSet(const K& key)
	{
		size_t hash1 = HashFunc1()(key) % _size;
		size_t hash2 = HashFunc2()(key) % _size;
		size_t hash3 = HashFunc3()(key) % _size;
		size_t hash4 = HashFunc4()(key) % _size;
		size_t hash5 = HashFunc5()(key) % _size;

		_refbm[hash1]--;
		_refbm[hash2]--;
		_refbm[hash3]--;
		_refbm[hash4]--;
		_refbm[hash5]--;
	}
	bool Test(const K& key)
	{
		size_t hash1 = HashFunc1()(key) % _size;
		if (_refbm[hash1] <= 0)
			return false;

		size_t hash2 = HashFunc2()(key) % _size;
		if (_refbm[hash2] <= 0)
			return false;

		size_t hash3 = HashFunc3()(key) % _size;
		if (_refbm[hash3] <= 0)
			return false;

		size_t hash4 = HashFunc4()(key) % _size;
		if (_refbm[hash4] <= 0)
			return false;

		size_t hash5 = HashFunc5()(key) % _size;
		if (_refbm[hash5] <= 0)
			return false;

		return true;
	}
protected:
	vector<size_t> _refbm;
	size_t _size;
};






9>給上千個檔案,每一個檔案大小為1K-100M,給n個單 詞,設計演算法對每個詞找到所有包含它的檔案,你只 有100K記憶體。
答:對上千個檔案生成1000個布隆過濾器,並將1000 個布隆過濾器存入一個檔案中,將記憶體分為兩份,一 分用來讀取布隆過濾器中的詞,一塊用來讀取檔案, 直到每個布隆過濾器讀完為止。


吐舌頭用一個檔案info 準備用來儲存n個詞和包含其的檔案資訊。
吐舌頭 首先把n個詞分成x份。對每一份用生成一個布 隆過濾器(因為對n個詞只生成一個布隆過濾器,記憶體可能不夠用)。把生成的所有布隆過濾器存入外存 的一個檔案Filter中。
吐舌頭將記憶體分為兩塊緩衝區,一塊用於每次讀入一個 布隆過濾器,一個用於讀檔案(讀檔案這個緩衝區使用 相當於有界生產者消費者問題模型來實現同步),大文 件可以分為更小的檔案,但需要儲存大檔案的標示信 息(如這個小檔案是哪個大檔案的)。
吐舌頭對讀入的每一個單詞用記憶體中的布隆過濾器來判 斷是否包含這個值,如果不包含,從Filter檔案中讀 取下一個布隆過濾器到記憶體,直到包含或遍歷完所有 布隆過濾器。如果包含,更新info 檔案。直到處理完 所有資料。刪除Filter檔案。



10>有一個詞典,包含N個英文單詞,現在任意給一個 字串,設計演算法找出包含這個字串的所有英文單 詞。
答:對於這道題目,我們要用到一種特殊的資料結 構----字典樹來解決它,所謂字典樹,又稱單詞查詢樹(或Trie樹),是一種雜湊樹的變種。
典型應用:用於統計、排序和儲存大量的字串,經 常被搜尋引擎系統用於文字詞頻統計。
優點:利用字串的公共字首來減少查詢時間,最大 限度地減少無謂的字串比較,查詢效率高於雜湊表 。
基本性質:吐舌頭根節點不包含字元,除根節點外每個節點 都只包含一個字元;
                   吐舌頭從根節點到某一節點,路徑上所有經過的字 符連線起來,為該節點對應的字串;
                   吐舌頭  每個節點的所有子節點包含的字元都不相同 。
應用:串的快速檢索、串排序、最長公共字首

解:
用給出的N個單詞建立一棵與上述字典樹不同的字典樹 ,用任意字串與字典樹中的每個節點中的單詞進行 比較,在每層中查詢與任意字串首字母一樣的,找到則遍歷其下面的子樹,找第二個字母,以此類推, 如果與任意字串的字元全部相同,則算找到。

如下圖: