深入理解PHP核心(六)雜湊表以及PHP的雜湊表實現,深入理解_PHP教程

來源:互聯網
上載者:User

深入理解PHP核心(六)雜湊表以及PHP的雜湊表實現,深入理解


原文連結:http://www.orlion.ga/241/

一、雜湊表(HashTable)

大部分動態語言的實現中都使用了雜湊表,雜湊表是一種通過雜湊函數,將特定的鍵映射到特定值得一種資料

結構,它維護鍵和值之間一一對應關係。

鍵(key):用於操作資料的標示,例如PHP數組中的索引或者字串鍵等等。

槽(slot/bucket):雜湊表中用於儲存資料的一個單元,也就是數組真正存放的容器。

雜湊函數(hash function):將key映射(map)到資料應該存放的slot所在位置的函數。

雜湊衝突(hash collision):雜湊函數將兩個不同的key映射到同一個索引的情況。

目前解決hash衝突的方法有兩種:連結法和開放定址法。

1、衝突解決

(1)連結法

連結法通過使用一個鏈表來儲存slot值的方式來解決衝突,也就是當不同的key映射到一個槽中的時候使用鏈表

來儲存這些值。(PHP中正是使用了這種方式);

(2)開放定址法

使用開放定址法是槽本身直接存放資料,在插入資料時如果key所映射到的索引已經有資料了,這說明有衝突,

這時會尋找下一個槽,如果該槽也被佔用了則繼續尋找下一個槽,直到找到沒有被佔用的槽,在尋找時也是這樣

2、雜湊表的實現

雜湊表的實現主要完成的工作只有三點:

* 實現雜湊函數

* 衝突的解決

* 操作介面的實現

(1)資料結構

首先需要一個容器來曹村我們的雜湊表,雜湊表需要儲存的內容主要是儲存進來的資料,同時為了方便的得知雜湊表中儲存的元素個數,需要儲存一個大小欄位,第二個需要的就是儲存資料的容器。下面將實現一個簡易的雜湊表,基本的資料結構主要有兩個,一個用於儲存雜湊表本身,另外一個就是用於實際儲存資料的單鏈表了,定義如下:

typedef struct _Bucket{    char *key;    void *value;    struct _Bucket *next; } Bucket; typedef struct _HashTable{    int size;    Bucket* buckets;} HashTable;

上邊的定義與PHP中的實現相似,為了簡化key的資料類型為字串,而儲存的結構可以為任意類型。

Bucket結構體是一個單鏈表,這是為瞭解決雜湊衝突。當多個key映射到同一個index的時候將衝突的元素連結起來

(2)雜湊函數實現

我們採用一種最簡單的雜湊演算法實現:將key字串的所有字元加起來,然後以結果對雜湊表的大小模數,這樣索引就能落在數組索引的範圍之內了。

static int hash_str(char *key){    int hash = 0;     char *cur = key;     while(*(cur++) != '\0') {        hash += *cur;    }     return hash;} // 使用這個宏來求得key在雜湊表中的索引#define HASH_INDEX(ht, key) (hash_str((key)) % (ht)->size)

PHP使用的雜湊演算法稱為DJBX33A。為了操作雜湊表定義了如下幾個操作函數:

int hash_init(HashTable *ht);                               // 初始化雜湊表int hash_lookup(HashTable *ht, char *key, void **result);   // 根據key尋找內容int hash_insert(HashTable *ht, char *key, void *value);     // 將內容插雜湊表中int hash_remove(HashTable *ht, char *key);                  // 刪除key所指向的內容int hash_destroy(HashTable *ht);

下面以插入和擷取操作函數為例:

int hash_insert(HashTable *ht, char *key, void *value){    // check if we need to resize the hashtable    resize_hash_table_if_needed(ht);    // 雜湊表不固定大小,當插入的內容快佔滿雜湊表的儲存空間                                        // 將對雜湊表進行擴容,以便容納所有的元素    int index = HASH_INDEX(ht, key);    // 找到key所映射到的索引     Bucket *org_bucket = ht->buckets[index];    Bucket *bucket = (Bucket *)malloc(sizeof(Bucket)); // 為新元素申請空間     bucket->key   = strdup(key);    // 將值內容儲存起來,這裡只是簡單的將指標指向要儲存的內容,而沒有將內容複寫    bucket->value = value;       LOG_MSG("Insert data p: %p\n", value);     ht->elem_num += 1; // 記錄一下現在雜湊表中的元素個數     if(org_bucket != NULL) { // 發生了碰撞,將新元素放置在鏈表的頭部        LOG_MSG("Index collision found with org hashtable: %p\n", org_bucket);        bucket->next = org_bucket;    }     ht->buckets[index]= bucket;     LOG_MSG("Element inserted at index %i, now we have: %i elements\n",        index, ht->elem_num);     return SUCCESS;}

在尋找時首先找到元素所在的位置,如果存在元素,則將鏈表中的所有元素的key和要尋找的key依次對比,直到找到一致的元素,否則說明該值沒有匹配的內容。

int hash_lookup(HashTable *ht, char *key, void **result){    int index = HASH_INDEX(ht, key);    Bucket *bucket = ht->buckets[index];     if(bucket == NULL) return FAILED;     // 尋找這個鏈表以便找到正確的元素,通常這個鏈表應該是只有一個元素的,也就不同多次迴圈    // 要保證這一點需要有一個合適的雜湊演算法。    while(bucket)    {        if(strcmp(bucket->key, key) == 0)        {            LOG_MSG("HashTable found key in index: %i with  key: %s value: %p\n",                index, key, bucket->value);            *result = bucket->value;                return SUCCESS;        }         bucket = bucket->next;    }     LOG_MSG("HashTable lookup missed the key: %s\n", key);    return FAILED;}

PHP中的數組是基於雜湊表實現的,依次給數組添加元素時,元素之間是有順序的,而這裡的雜湊表在物理上顯然是接近平均分布的,這樣是無法根據插入的先後順序擷取到這些元素的,在PHP的實現中Bucket結構體還維護了另一個指標欄位來維護元素之間的關係。

二、PHP的雜湊表實現

1、PHP的雜湊實現

PHP中的雜湊表是十分重要的一個資料介面,基本上大部分的語言特徵都是基於雜湊表的,例如:變數的範圍和變數的儲存,類的實現以及Zend引擎內部的資料有很多都是儲存在雜湊表中的。

(1)資料結構及說明

Zend為了儲存資料之間的關係使用了雙向鏈表來儲存資料

(2)雜湊表結構

PHP中的雜湊表實現在Zend/zend_hash.c中,PHP使用如下兩個資料結構來實現雜湊表,HashTable結構體用於儲存整個雜湊表需要的基本資料,而Bucket結構體用於儲存具體的資料內容,如下:

typedef struct _hashtable {     uint nTableSize;        // hash Bucket的大小,最小為8,以2x增長    uint nTableMask;        // nTableSize-1,索引取值的最佳化    uint nNumOfElements;    // hash Bucket中當前存在的元素個數,count()函數會直接返回此值    ulong nNextFreeElement; // 下一個數字索引的位置    Bucket *pInternalPointer;   // 當前遍曆的指標(foreach 比for快的原因之一)    Bucket *pListHead;          // 儲存數頭元素指標    Bucket *pListTail;          // 儲存數組尾元素指標    Bucket **arBuckets;         // 儲存hash數組    dtor_func_t pDestructor;    zend_bool persistent;    unsigned char nApplyCount; // 標記當前hash Bucket被遞迴訪問的次數(防止多次遞迴)    zend_bool bApplyProtection;// 標記當前hash桶允許不允許多次訪問,不允許時,最多隻能遞迴3此#if ZEND_DEBUG    int inconsistent;#endif} HashTable;

nTableSize欄位用於標示雜湊表的容量,雜湊表的初始化容量最小為8.首先看看雜湊表的初始化函數:

ZEND_API int _zend_hash_init(HashTable *ht, uint nSize, hash_func_t pHashFunction,                    dtor_func_t pDestructor, zend_bool persistent ZEND_FILE_LINE_DC){    uint i = 3;    //...    if (nSize >= 0x80000000) {        /* prevent overflow */        ht->nTableSize = 0x80000000;    } else {        while ((1U << i) < nSize) {            i++;        }        ht->nTableSize = 1 << i;    }    // ...    ht->nTableMask = ht->nTableSize - 1;     /* Uses ecalloc() so that Bucket* == NULL */    if (persistent) {        tmp = (Bucket **) calloc(ht->nTableSize, sizeof(Bucket *));        if (!tmp) {            return FAILURE;        }        ht->arBuckets = tmp;    } else {        tmp = (Bucket **) ecalloc_rel(ht->nTableSize, sizeof(Bucket *));        if (tmp) {            ht->arBuckets = tmp;        }    }     return SUCCESS;}

例如如果設定初始大小為10,則上面的演算法將會將大小調整為16.也就是始終將大小調整為接近初始大小的2的整數次方

為什麼這麼調整呢?先看看HashTable將雜湊值對應到槽位的方法:

h = zend_inline_hash_func(arKey, nKeyLength);nIndex = h & ht->nTableMask;

從上邊的_zend_hash_init()函數中可知,ht->nTableMask的大小為ht->nTableSize – 1。這裡使用&操作而不是使用模數,這是因為相對來說模數的操作的消耗和按位與的操作大很多。

設定好了雜湊表的大小後就需要為雜湊表申請儲存空間了,如上邊初始化的代碼,根據是否需要持久儲存而調用了不同的記憶體申請方法,是需要持久體現的是在前面PHP生命週期裡介紹的:持久內容能在多個請求之間可訪問,而如果是非持久儲存則會在在請求結束時釋放佔用的空間。具體內容將在記憶體管理中詳解

HashTable中的nNumOfElements欄位很好理解,每插入一個元素或者unset刪掉元素時會更新這個欄位,這樣在進行count()函數統計數組元素個數時就能快速的返回。

nNextFreeElement欄位非常有用,先看一段PHP代碼:

 'Hello');$a[] = 'TIPI';var_dump($a); // ouputarray(2) {  [10]=>  string(5) "Hello"  [11]=>  string(5) "TIPI"}

PHP中可以不指定索引值向數組中添加元素,這時將預設使用數字作為索引,和C語言中的枚舉類似,而這個元素的索引到底是多個就由nNextFreeElement欄位決定了。如果數組中存在了數字key,則會預設使用最新使用的key+1,如上例中已經存在了10作為key的元素,這樣新插入的預設索引就為11了。

下面看看儲存雜湊表資料的槽位元據結構體:

typedef struct bucket {    ulong h;            // 對char *key進行hash後的值,或者是使用者指定的數字索引值    uint nKeyLength;    // hash關鍵字的長度,如果數組索引為數字,此值為0    void *pData;        // 指向value,一般是使用者資料的副本,如果是指標資料,則指向pDataPtr    void *pDataPtr;     // 如果是指標數組,此值會指向真正的value,同時上面pData會指向此值    struct bucket *pListNext;   // 整個hash表的下一個元素    struct bucket *pListLast;   // 整個hash表的上一個元素    struct bucket *pNext;       // 存放在同一個hash Bucket內的下一個元素    struct bucket *pLast;       // 存放在同一個hash Bucket內的上一個元素    char arKey[1];      /*    儲存字元索引,此項必須放在最末尾,因為此處只定義了1個位元組,儲存的實際上是指向char *key的值,    這就意味著可以省去再賦值一次的消耗,而且,有時此值並不需要,所以同時還節省了空間。    */} Bucket;

如上面各欄位的注釋。h欄位儲存雜湊表key雜湊後的值。在PHP中可以使用字串或者數字作為數組的索引。因為數位索引是唯一的。如果再進行一次雜湊將會極大的浪費。h欄位後面的nKeyLength欄位是作為key長度的標示,如果索引是數位話,則nKeyLength為0.在PHP中定義數組時如果字串可以被轉換成數字也會進行轉換。所以在PHP中例如'10','11'這類的字元索引和數字索引10,11沒有區別

  • Bucket結構體維護了兩個雙向鏈表,pNext和pLast指標分別指向本槽位所在的鏈表的關係

  • 而pListNext和pListLast指標指向的則是整個雜湊表所有的資料之間的連結關係。HashTable結構體中的pListHead和pListTail則維護整個雜湊表的頭元素指標和最後一個元素的指標

雜湊表的操作介面:

PHP提供了如下幾類操作介面:

  • 初始化操作,例如zend_hash_init()函數,用於初始化雜湊表介面,分配空間等。

  • 尋找,插入,刪除和更新操作介面,這是比較常規的操作。

  • 迭代和迴圈,這類的介面用於迴圈對雜湊表進行操作。

  • 複製,排序,倒置和銷毀等操作。

http://www.bkjia.com/PHPjc/1115246.htmlwww.bkjia.comtruehttp://www.bkjia.com/PHPjc/1115246.htmlTechArticle深入理解PHP核心(六)雜湊表以及PHP的雜湊表實現,深入理解 原文連結:http://www.orlion.ga/241/ 一、雜湊表(HashTable) 大部分動態語言的實現中都使...

  • 聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.