1.Hash表的結構
首先,允許我們花一點時間來簡單介紹hash表。
1.什么是hash表
hash表是一種二維結構,管理着一對對<Key,Value>這樣的鍵值對,Hash表的結構如下圖所示:
如上圖所示,左側部分是一個一維順序存儲的數組,數組單元格里的內容是指向另一個鏈式數組的指針。圖中綠色部分是<Key,Value>,綠色部分右側的白色部分是指向下一對鍵值對的指針。
hash表的工作原理:
(1).第一步 先根據給定的key和散列算法得到具體的散列值,也就是對應的數組下標。
(2).第二步,根據數組下標得到此下標里存儲的指針,若指針為空,則不存在這樣的鍵值對,否則根據此指針得到此鏈式數組。(此鏈式數組里存放的均為一對對<Key,Value>)。
(3).遍歷此鏈式數組,分別取出Key與給定的Key比較,若找到與給定key相等的Key,即在此hash表中存在此要查找的<Key,Value>鍵值對,此后便可以對此鍵值對進行相關操作;若找不到,即為不存在此 鍵值對。
所以hash表其實就是管理一對對<Key,Value>這樣的結構。
2.不可避免的hash沖突
總所周知,hash表是管理着一組組的<key,Value>的數據結構,訪問時對Key采取散列算法求值,根據此值得到鏈式數組,根據鏈式數組取得Value.那對一個給定的Key是怎么的呢?
散列技術基於散列算法,理想情況下是將相同的key散列為相同的值,不同的key散列為不同的值。但是實際情況下,因為存儲空間有限,使得這種算法是不可能被實現的,所以當不同的key被散列為相同的值時,便產生了沖突。這就是我們所說的hash沖突,下面我們來談一談為什么這種算法是不可能實現的?
理想情況下一維的hash表 :如下圖:
(2)理想情況下的一維hash表
理想情況下的一維hash表存放的是一對對<key,Value>鍵值,
1.對hash散列算法的要求:不同的Key必須散列為不同的值,相同的Key必須散列為相同的值。
2.對數組的要求,為了保持訪問的高效,必須保持為順序存儲的數組。(鏈式數組的訪問時間為logn,而且還必須時刻維持平衡樹的結構代價較大,不符合要求)。
OK....那么問題來了,理想情況下的hash表能完美解決以下問題嗎:
(1).為了高效,你是順序存儲的數組,那么你知道你每次需要開辟多大的存儲給此數組嗎?如果太大,勢必會造成空間的浪費,而且此空間還必須是連續的,如果過小,需要不斷調整。這樣你還能維持高效嗎?
(2).針對每個<Key,Value>你又准備多大的空間去存儲此鍵值對呢?很遺憾的告訴你...你不知道。過大過小都會面對剛才我們提出的問題?
問題出現后,總會有那么幾個天才冒出來?
3.hash表的完美實現(允許沖突的存在)
hash查找是一種高效的查找,在存儲空間和查找時間的相互妥協下,有人提出來了一種新的想法,即允許將不同的key散列為相同的值。
具體實現如下:
1.先在空間中開辟一個數組。我們首先根據key和散列算法取得數組的下標,也就是上圖部分的左側數組。
2.數組中的每個單元格維持着一個鏈式數組,鏈式數組里存放<Key,Value>.
3.訪問的時候,根據key一一比較,若找到,取得到value,若找不到,不同的編程語言返回着不同的值.
4.存儲的時候,先查找此key是否存在,若存在則修改此Value值,若不存在,則在此鏈式數組的尾部加上一個一對<Key,Value>;
有以上我們可以看出,利用hash表存儲對象,查詢的時候速度是非常快的。左側數組可以隨機訪問,右側鏈式數組雖然需要遍歷,但是如果散列算法夠出色的話,每個鏈式數組存儲的鍵值對數量就會很小。查找的速度也足夠快。理想情況下查找速度幾乎可以達到o(1)。
2.JavaScript中對象的存儲形式?
1.兩種創建對象的區別
(1).用構造函數創建對象
1 var object=new Object(); 2 object.x=1; 3 object.2=1; //出錯,因為變量不能以數字開頭..
以上的錯誤相信大家都知道,那么用字面量創建對象呢?
(2).用字面量創建對象
var object = { x: 1, 2: 2 };//不會報錯
(3)兩種方式比較
兩則相比,大家不覺得奇怪嗎?------為什么第一種方式報錯,第二種反而不會?
當然你也可以理解為JS引擎對語法錯誤的屏蔽.那么JS引擎干嘛要費那么大的勁去屏蔽違法的變量呢?答案就是:---事出必有因
假設JS允許通過下面這種方式賦值或者訪問
object.2=2; //假設不會出錯
2==2;
console.log(2);//那么出現這種方式的時候,你讓JS引擎怎么辦?是把2當做全局變量理解還是數字2
當出現這樣的情況,解釋器也只能干瞪眼了.所以為了語法的統一,才對以變量的形式的訪問對象的變量定義了各種要求,
也就是說,js引擎對於違法變量的屏蔽是合理的。
那么問題又來了----
//既然,對變量的形式規定了要求,那么又干嘛允許這種玩意存在呢?-----答案就是:因為它也是合理的/
var object = {
x:1, 2: 2 };
為什么上面這種方式合理呢?
因為在hash表的<Key,Value>鍵值對中並沒有對key提出這樣或者那樣的要求。所以,如果JS對象是基於Hash表存儲變量的,既然在存儲時這種形式合法,那么你又為什么不允許我訪問我存儲的變量呢? 這不是自相矛盾嗎!
當然,這里有一個邏輯性的問題,我為什么認同JS對象的存儲是基於Hash的呢?(這點在第三部分我會給出解釋)
以上列舉了兩種定義對象變量方法以及區別,進而解釋了為什么兩種定義形式明明相互沖突卻又同時存在!
下面開始我們的討論: JS對象在hash表中Key到底是什么?------答案:字符串
首先來看一下他的輸出結果
var object = { x: 1, 2: 2 } for (var property in object) { console.log('(' + typeof property + ')' + property + ':' + object[property]); }
//結果 (string)2:2
// (string)x:1
以上的代碼證明了兩點: object[2]是真的存在的..並且<Key,Value>中的key是以字符串的形式存在。也就是說,對象的變量和值作為<Key,Value>鍵值對存儲在hash表中,key是以字符串的形式存在,那么value是以什么形式存在呢? 這點我們以后再討論。也很值得討論。
總結:
用字面量初始化的對象的時候,變量名可以使數字,也可以是字符串,但是不可以是對象(下面會給出解釋), 但是以后用[]訪問的時候,[]中的內容則可以是對象,只要[]中內容可以轉換為字符串就可以。
初始化對象的兩種形式都是合法的,但是在以.號訪問變量時只能訪問我們平時所說的合法變量,以[]訪問時則比較自由,也沒有那么多的要求,這是JS設計時候存在的缺點,但同時也是JS語言的有點。
為什么對象初始化的時候不能用對象作為變量,如下所示,:
var object={
{x:1}:2 //這種形式是錯誤的,因為解釋器不能正確識別這種語法,會報錯。-----理論上是可以做到的,可能是設計的時候存在的缺陷。 }
//以下的方式則不會報錯
var object={};
object[{x:1}]=1;
console.log(object[{x:1}]) ;//1 解釋器會盡力量把[]中的內容解釋成字符串。
知道看Key是字符串,那么我們現在可以解釋這個問題了:object[2]和object["2"]有區別嗎?-----(沒有,當然,關於這點也得看你怎么理解)!為了證明這兩種形式沒有區別我們看下面兩個例子
請思考:用[]訪問對象變量的時候,解釋器幫我們都干了什么?
var object = { x: 1, 2: 2 } console.log(object[2]);//2 console.log(object["2"]);//2 object[2]=3; console.log(object["2"]);//3
之所以object[2]和object["2"]等效,那是因為解釋器幫我們干了一點活,那么解釋器幫我們干了什么呢?
解釋器在訪問object[2]的時候,先將方括號里面的2轉換成字符串。然后再訪問,為了證明這點,我寫了一點代碼證明這點。
var object = { x: 1, 2: 2 } Object.prototype.toString = function () { return '2'; } console.log(object[{x: 1}]); //2
console.log(object["2"]); //2
console.log(object[2]); //2
下面花一點時間來分析上述代碼的執行過程:
1.首先定義並初始化了一個object對象,對象中存在兩個變量。
2.重寫了Object原型中的toString()方法。
3.第7行輸出時對於[]中我們用了一個臨時的對象{x:1}。此對象被初始化后,在object[]執行時,先分析方括號中{x:1},此時解釋器為了將此對象轉換為字符串,如果是引用類型會調用原型對象中的toString()函數,如果是基本數據類型是也會將基本數據類型轉化為對應字符串,結果即是訪問object["2"].輸出的結果也就是2了。
所以我們也間接證明了JS對象中,所有的key都是字符串,即使你訪問的時候不是字符串的形式,解釋器也會盡力先將其轉化為字符串。
所以下面兩種方式初始化對象是完全等效的:
1 var object1 = { 2 x: 1, 3 1: 2 4 } //第一種 5 var object2 = { 6 'x': 1, 7 '1': 2 8 } //第二種
以上我們討論的是 JS對象的存儲形式以及數據是怎么存放的。
下面我們討論,為什么說JS中對象存儲的變量是基於hash的。
3.JavaScript中的對象基於Hash表存儲變量。
(1).證明:
我們可以隨時給一個對象增加或刪除變量(如果此變量允許刪除的話)
1 var object={}; 2 object.x=1;//增加一個變量 3 delete object.x;//刪除一個變量 success 4 console.log(Object.keys(object).length); //0
(1) 既然變量的對象類型和個數是可變的,我們也就不能像java,c++那樣,先將一個對象分配固定的空間。JS的引用指向的對象所占用的空間必須支持隨時調整。基於此,順序存儲的數組已經被淘汰。
(2)鏈式數組查詢較慢的弊端已經先天決定其不可能作為對象中變量的存儲結構。
(3)當然你可以說,我可以用樹的存儲結構,效率較高的可能就是平衡樹了。平衡樹在查詢的時候時間復雜度為log(n),也不算太高,但是當刪除屬性的時候,平衡樹在調整的時候代價相比 於hash表也是很大。或許,你還有其他的選擇,但是我敢說,肯定沒有任何一種有hash表存儲數據那么方便和高效。
(4).只有JavaScript的對象是基於hash表存儲的,那么所有的在c++,java,c#中存在的不合理才會在JavaScript語言中變得合理。
其實說白一點,物理存儲並非非hash表不可,只是沒有比hash表更好的了,所以在JS語言設計的時候就是當做hash表結構進行設計的。
為了證明對象是基於hash表以鍵值對存儲的,我們來簡單看一下數組類型和函數:
函數:
var person=function y(){}; person.x=1; person.y=2; for(var property in person){ console.log(property+" : "+ person[property]) }//x:1
// y:2
數組:
var array=[1,2,4] for(var property in array){ console.log(property+" : "+ array[property]) }// 0: 1
// 1: 2
// 2:4
(2).理解了這些有什么用:
1.你還會對數組數可以擁有屬性,函數也可以擁有屬性奇怪嗎? 因為他們本身管理着屬於自己的hash表,所以他們隨時都可以給自己添加或則刪除一些屬性。
2.我們知道數組中的下表是可以隨意添加的,無論你設置為多大,你也可以越界訪問(嚴格來說,根本就沒有所謂的越界),只是返回的結果是undefined...因為沒有找到對應的key。
你將其理解為下標,倒不如理解為Key,如此,JS數組還有那么神秘嗎? 說白了,也就是一種hash結構。如果你想,你也可以把object當成數組,然后自定義一整套函數,只是可能沒有那么方便。
注:當然,函數作為一種對象肯定有其的特殊性。在這里我們就不過多的討論了。
4.JS對象是基於Hash表的典型應用
數組去重
1 var array=['true',true,false,'1',1,'','sss'," ",1,34,,,{x:1},{x:2}] 2 3 Array.prototype.unique=function(){ 4 5 //利用對象的hash存儲特性去重 6 var object={},result=[]; 7 8 for(var i=0,length=this.length;i<length;i++){ 9 10 var temp=this[i],key; 11 12 if((typeof temp)=='object'){ 13 key=JSON.stringify(temp); //若為對象類型,將對象序列化為字符串 14 }else{ 15 key=typeof temp+temp; 16 } 17 18 if(!object[key]){ 19 object[key]=true; //若object中已經存在此鍵值,則證明此元素在數組中已經存在 20 result.push(temp); 21 22 } 23 24 } 25 26 return result; 27 28 } 29 30 console.log(array.unique());
//此算法的缺點,因為另外建立一個object對象和result數組,所以比較占用空間,但是速度非常快,至少比用樹形結構快。這是對網上一些算法的改進,網絡上有好多針對對象hash的算法並不能完美的去重。
比如數組[1,"1",{x:1},{x:2}]。
文章中存在的疑點: Number,Boolean 等基本類型在轉化為字符串的時候到底調用的是什么方法?(不是原型鏈中的toString()方法,關於這點未能敘述,歡迎補充);