程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> 深入理解PHP內核(六)哈希表以及PHP的哈希表實現,深入理解

深入理解PHP內核(六)哈希表以及PHP的哈希表實現,深入理解

編輯:關於PHP編程

深入理解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代碼:

<?php
$a = array(10 => 'Hello');
$a[] = 'TIPI';
var_dump($a);
 
// ouput
array(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()函數,用於初始化哈希表接口,分配空間等。

  • 查找,插入,刪除和更新操作接口,這是比較常規的操作。

  • 迭代和循環,這類的接口用於循環對哈希表進行操作。

  • 復制,排序,倒置和銷毀等操作。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved