详解PHP中Array构造HashTable

Jun 13, 2016 am 10:54 AM
gt hash hashtable

详解PHP中Array结构HashTable

我们知道PHP中的Array在内部是以Hash的结构进行存储的。本文主要重点也是对PHP中Array的静态结构和动态结构进行分析和记录。

这里的静态结构,是指存储PHP中Array数据时使用的数据结构,即所谓的HashTable。

动态结构,是指程序在运行过程中,Array数据的存储状态。

?

首先PHP中的hashTable的结构如下:

typedef struct bucket {    ulong h;                        /* Used for numeric indexing */    uint nKeyLength;    void *pData;    void *pDataPtr;    struct bucket *pListNext;    struct bucket *pListLast;    struct bucket *pNext;    struct bucket *pLast;    char *arKey;} Bucket;typedef struct _hashtable {    uint nTableSize;    uint nTableMask;    uint nNumOfElements;    ulong nNextFreeElement;    Bucket *pInternalPointer;   /* Used for element traversal */    Bucket *pListHead;    Bucket *pListTail;    Bucket **arBuckets; ? ? ? ? ?    dtor_func_t pDestructor;    zend_bool persistent;    unsigned char nApplyCount;    zend_bool bApplyProtection;#if ZEND_DEBUG    int inconsistent;#endif} HashTable;
ログイン後にコピー

?

一个PHP中的Array在内部对应一个HashTable,HashTable内部的四个Bucket类型的指针数据记录着数组实际存储的元素内容的地址。具体的内容,各字段名都可以自解释,不做多说明了。

?

?

如果只看这几行代码,可能无法理解PHP数组实际的工作原理,接下来,我们可以手工模拟一下PHP数组中的一些最简单的操作。

?

1. 从无到有

HashTable的初始化,首先需要给一个HashTable构造一个内存空间,具体代码如下:

?

//hash_func_t在函数内用不到,hash函数在PHP范围内都是固定的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;    SET_INCONSISTENT(HT_OK);    if (nSize >= 0x80000000) {        /* prevent overflow */        ht->nTableSize = 0x80000000;    } else {        while ((1U nTableSize = 1 nTableMask = 0; /* 0 means that ht->arBuckets is uninitialized */    ht->pDestructor = pDestructor;    ht->arBuckets = (Bucket**)&uninitialized_bucket; ? //实际的数据存储空间还未创建    ht->pListHead = NULL;    ht->pListTail = NULL;    ht->nNumOfElements = 0; ? ? ? ? ? ? ? ? ? //表示数组内还没有一个元素,    ht->nNextFreeElement = 0;    ht->pInternalPointer = NULL;    ht->persistent = persistent;    ht->nApplyCount = 0;    ht->bApplyProtection = 1;    return SUCCESS;}
ログイン後にコピー
?

上述代码可以理解为,为数组构造了一个总的大门,数据都可以经由这个门进入到自己对应的内存块中。当然现在门里还没有“座位”呢。

?

2. 数据插入

对于一个一无所有的空间,怎么给它加点东西呢?这就是数据的插入,即数据是如何保存到这个HashTable中的。

PHP的数组索引可以是数值或字符串,我们首先看字符串的索引如何存储,代码如下:

int _zend_hash_add_or_update(HashTable *ht, const char *arKey, uint nKeyLength, void *pData, uint nDataSize, void **pDest, int flag ZEND_FILE_LINE_DC){	ulong h;	uint nIndex;	Bucket *p;	IS_CONSISTENT(ht);	if (nKeyLength nTableMask;	p = ht->arBuckets[nIndex];	while (p != NULL) {		if (p->arKey == arKey ||			((p->h == h) && (p->nKeyLength == nKeyLength) && !memcmp(p->arKey, arKey, nKeyLength))) {				if (flag & HASH_ADD) {					return FAILURE;				}				HANDLE_BLOCK_INTERRUPTIONS();#if ZEND_DEBUG				if (p->pData == pData) {					ZEND_PUTS("Fatal error in zend_hash_update: p->pData == pData\n");					HANDLE_UNBLOCK_INTERRUPTIONS();					return FAILURE;				}#endif				if (ht->pDestructor) {					ht->pDestructor(p->pData);				}				UPDATE_DATA(ht, p, pData, nDataSize);				if (pDest) {					*pDest = p->pData;				}				HANDLE_UNBLOCK_INTERRUPTIONS();				return SUCCESS; ?//更新之后直接退出		}		p = p->pNext;	}		if (IS_INTERNED(arKey)) {		p = (Bucket *) pemalloc(sizeof(Bucket), ht->persistent);		if (!p) {			return FAILURE;		}		p->arKey = (char*)arKey;	} else {		p = (Bucket *) pemalloc(sizeof(Bucket) + nKeyLength, ht->persistent);		if (!p) {			return FAILURE;		}		p->arKey = (char*)(p + 1);		memcpy(p->arKey, arKey, nKeyLength);	}	p->nKeyLength = nKeyLength;	INIT_DATA(ht, p, pData, nDataSize);	p->h = h;	CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex]);	if (pDest) {		*pDest = p->pData;	}	HANDLE_BLOCK_INTERRUPTIONS();	CONNECT_TO_GLOBAL_DLLIST(p, ht);	ht->arBuckets[nIndex] = p;	HANDLE_UNBLOCK_INTERRUPTIONS();	ht->nNumOfElements++;	ZEND_HASH_IF_FULL_DO_RESIZE(ht);		/* If the Hash table is full, resize it */	return SUCCESS;}
ログイン後にコピー

首先,检查数组空间是否初始化,代码如下:

?

#define CHECK_INIT(ht) do {                                             \    if (UNEXPECTED((ht)->nTableMask == 0)) {                                \        (ht)->arBuckets = (Bucket **) pecalloc((ht)->nTableSize, sizeof(Bucket *), (ht)->persistent);   \        (ht)->nTableMask = (ht)->nTableSize - 1;                        \    }                                                                   \} while (0)
ログイン後にコピー
?

?

然后计算要插入的字符串索引的hash值,并与nTableMask做按位与,得到nindex,这个nIndex就是对应的bucket*在二维数组arBucket**中的偏移量。根据代码逻辑,如果nIndex位置不为空,则说明当前计算得到的hash值之前存在。如果连key也相同并且flag为HASH_ADD则失败,否则就是更新操作。如果是更新操作则不会对现有数组结构有任何影响,更新了对应的值之后直接退出即可。

?

在需要有新元素插入到HashTable时,构造好的新元素会经过两步来链入该HashTable

?

第一步代码如下:

?

#define CONNECT_TO_BUCKET_DLLIST(element, list_head)        \    (element)->pNext = (list_head);                         \    (element)->pLast = NULL;                                \    if ((element)->pNext) {                                 \        (element)->pNext->pLast = (element);                \    }
ログイン後にコピー

?

在这一步中如果新元素的key的hash值之前存在过,则list_head为HashTable.arBucket[nIndex],nIndex怎么来的前面已经说过了。在这一步过后会将HashTable.arBucket[nIndex]赋值为当前的新元素,你懂得。

?

如果新元素的key对应的hash之前没有存在过,则list_head就为NULL,因为HashTable.arBucket[nIndex]为NULL。你也懂得。

?

第二步代码如下:

?

#define CONNECT_TO_GLOBAL_DLLIST(element, ht)               \    (element)->pListLast = (ht)->pListTail;                 \    (ht)->pListTail = (element);                            \    (element)->pListNext = NULL;                            \    if ((element)->pListLast != NULL) {                     \        (element)->pListLast->pListNext = (element);        \    }                                                       \    if (!(ht)->pListHead) {                                 \        (ht)->pListHead = (element);                        \    }                                                       \    if ((ht)->pInternalPointer == NULL) {                   \        (ht)->pInternalPointer = (element);                 \    }
ログイン後にコピー
?

关于这一步会对HashTable的内容有什么样的影响,请参看下面的动态示例。相信你也懂得。

?

?

?

动态示例:

现在我们假设数组中没有任何元素,则进行插入操作。现在我们按照代码的逻辑,手动模拟一下数据插入的过程:

?

1.

插入第一个元素A,假设其key对应的hash值为1

则插入之后,内存中的状态如下:

?

HashTable.arBucket[1]=A;

HashTable.pListHead = A

HashTable.pListTail = A

HashTable.pInternalPointer = A

A.pNext = null

A.pLast = null

A.pListLast = null

A.pListNext = null

?

2.

插入第二个元素B,假设其key对应的hash值为2

则插入之后内存的状态如下:

HashTable.arBucket[2] = B;

HashTable.pListHead = A

HashTable.pListTail = B

HashTable.pInternalPointer = A?????? //这个只在第一次的时候设置

A.pNext=null

A.pLast = null

A.pListNext = B

A.pListLast = null

B.pListLast = A

B.pListNext = null

B.pNext = null

B.pLast = null

?

3.

插入第三个元素C,假设其key的hash值为1,和A相同

则插入之后内存状态如下:

HashTable.arBucket[1] = C;

HashTable.pListHead = A

HashTable.pListTail =C

HashTable.pInternalPointer = A?????? //这个只在第一次的时候设置

A.pNext=null

A.pLast = C

A.pListNext = B

A.pListLast = null

?

B.pNext = null

B.pLast = null

B.pListLast = A

B.pListNext = C

C.pNext = A

C.pLast = null

C.pListNext = null

C.pListLast = B

?

插入A,B,C三个值之后的内存中状态即为:

HashTable.arBucket[1] = C;

HashTable.pListHead = A

HashTable.pListTail =C

HashTable.pInternalPointer = A

A.pNext=null

A.pLast = C

A.pListNext = B

A.pListLast = null

?

B.pNext = null

B.pLast = null

B.pListLast = A

B.pListNext = C

C.pNext = A

C.pLast = null

C.pListNext = null

C.pListLast = B

?

OK,A、B、C三个元素都已插入了,现在我们要实现两个任务:

?

1.

查找某key的元素值(value):

如果我们要访问A元素,则提供A的key:key_a,得到对应的hash值为1

然后找HastTable.arBucket[1]。这时HastTable.arBucket[1]其实为C不是A,但由于C的key不等于A的key,因此,要沿着pNext的指针找下去,直到NULL,而此时C.pNext就是A,即找到了key_a对应的值A。

总之由key查找一个元素时,首先要hash,然后顺着hash后的索引位置的pNext指针一直找下去,直到NULL,如果遇到了和要查找的key相同的值,则找到,否则找不到。

?

2.

遍历数组:

由于我们的例子中的key是字符串类型的,全部循环遍历不能用for。只能用foreach,那foreach的遍历是如何实现的呢?

?

简单,根据最后的HashTable的状态,我们从HastTable.pListHead开始沿着pListNext指针顺序找下去即可了。以本文例子为例,则结果为:

?

?

HashTable.pListHead====>A

A.pListNext?????????????????? ====>B

B.pListNext?????????????????? ====>C

?

则最后的遍历顺序就是A,B,C,发现foreach的遍历顺序是和元素插入到数组的顺序相关的。

?

?

如果插入的元素的key不是字符串,而是数值。则可以省去做计算hash值这一步,直接拿数值的key做为hash值使用。

这样就不存在hash冲突的问题,这样也就不会用到每个元素的pNext、pLast两个指针了,这两个指针都只会是NULL。

?

这样我们可以通过使用for循环来遍历数组了,因为不存在hash冲突。

?

同样,如果我们使用foreach来遍历数组的话,遍历顺序还是元素的插入顺序,这个你当然懂得。

?

?

ps:

本文并未对zend中的hash结够做全面的记录,只是对本文主题涉及到的逻辑的重点代码进行了分析和演示。同时也为了能抓住重点。有些代码并未列出,如:再hash的逻辑,和索引为数值类型数据的代码等。这些可在代码文件Zend/zend_hash.c中找到详细内容。

?

このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

AI Hentai Generator

AI Hentai Generator

AIヘンタイを無料で生成します。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

Huawei GT3 ProとGT4の違いは何ですか? Huawei GT3 ProとGT4の違いは何ですか? Dec 29, 2023 pm 02:27 PM

多くのユーザーはスマートウォッチを選ぶときにファーウェイブランドを選択しますが、その中でもファーウェイ GT3pro と GT4 は非常に人気のある選択肢であり、多くのユーザーはファーウェイ GT3pro と GT4 の違いに興味を持っています。 Huawei GT3pro と GT4 の違いは何ですか? 1. 外観 GT4: 46mm と 41mm、材質はガラスミラー + ステンレススチールボディ + 高解像度ファイバーバックシェルです。 GT3pro: 46.6mm および 42.9mm、材質はサファイアガラス + チタンボディ/セラミックボディ + セラミックバックシェルです。 2. 健全な GT4: 最新の Huawei Truseen5.5+ アルゴリズムを使用すると、結果はより正確になります。 GT3pro: ECG 心電図と血管と安全性を追加

修正: Windows 11 で Snipping ツールが機能しない 修正: Windows 11 で Snipping ツールが機能しない Aug 24, 2023 am 09:48 AM

Windows 11 で Snipping Tool が機能しない理由 問題の根本原因を理解すると、適切な解決策を見つけるのに役立ちます。 Snipping Tool が正しく動作しない主な理由は次のとおりです。 フォーカス アシスタントがオンになっている: これにより、Snipping Tool が開かなくなります。破損したアプリケーション: 起動時にスニッピング ツールがクラッシュする場合は、破損している可能性があります。古いグラフィック ドライバー: 互換性のないドライバーは、スニッピング ツールに干渉する可能性があります。他のアプリケーションからの干渉: 実行中の他のアプリケーションが Snipping Tool と競合する可能性があります。証明書の有効期限が切れています: アップグレード プロセス中のエラーにより、この問題が発生する可能性があります。これらの簡単な解決策は、ほとんどのユーザーに適しており、特別な技術知識は必要ありません。 1. Windows および Microsoft Store アプリを更新する

PHPでRedisハッシュ操作を実装する方法 PHPでRedisハッシュ操作を実装する方法 May 30, 2023 am 08:58 AM

ハッシュ演算 //ハッシュテーブルのフィールドに値を代入します。成功した場合は 1 を返し、失敗した場合は 0 を返します。ハッシュ テーブルが存在しない場合は、まずテーブルが作成されてから値が割り当てられ、フィールドが既に存在する場合は古い値が上書きされます。 $ret=$redis->hSet('user','realname','jetwu');//ハッシュ テーブル内の指定されたフィールドの値を取得します。ハッシュ テーブルが存在しない場合は false を返します。 $ret=$redis->hGet('ユーザー','rea

iPhoneでApp Storeに接続できないエラーを修正する方法 iPhoneでApp Storeに接続できないエラーを修正する方法 Jul 29, 2023 am 08:22 AM

パート 1: 最初のトラブルシューティング手順 Apple のシステムステータスを確認する: 複雑な解決策を掘り下げる前に、基本から始めましょう。問題はデバイスにあるのではなく、Apple のサーバーがダウンしている可能性があります。 Apple のシステム ステータス ページにアクセスして、AppStore が適切に動作しているかどうかを確認してください。問題があれば、Apple が修正してくれるのを待つしかありません。インターネット接続を確認します。「AppStore に接続できません」問題は接続不良が原因である場合があるため、安定したインターネット接続があることを確認してください。 Wi-Fi とモバイル データを切り替えるか、ネットワーク設定をリセットしてみてください ([一般] > [リセット] > [ネットワーク設定のリセット] > [設定])。 iOS バージョンを更新します。

Laravel 開発: Laravel ハッシュを使用してパスワード ハッシュを生成するにはどうすればよいですか? Laravel 開発: Laravel ハッシュを使用してパスワード ハッシュを生成するにはどうすればよいですか? Jun 17, 2023 am 10:59 AM

Laravel は現在最も人気のある PHP Web フレームワークの 1 つであり、開発者に多くの強力な機能とコンポーネントを提供しており、LaravelHash もその 1 つです。 LaravelHash は、パスワードを安全に保ち、アプリケーションのユーザー データをより安全にするために使用できるパスワード ハッシュ用の PHP ライブラリです。この記事では、LaravelHash の仕組みと、LaravelHash を使用してパスワードをハッシュし検証する方法を学びます。 Lara を学習するための前提知識

Java の Hashtable クラスの isEmpty() メソッドを使用してハッシュ テーブルが空かどうかを判断する Java の Hashtable クラスの isEmpty() メソッドを使用してハッシュ テーブルが空かどうかを判断する Jul 24, 2023 pm 02:21 PM

Java では、Hashtable クラスの isEmpty() メソッドを使用して、ハッシュ テーブルが空かどうかを判断します。ハッシュ テーブルは、Java コレクション フレームワークで一般的に使用されるデータ構造の 1 つです。キーと値の格納と取得を実装します。ペア。 Hashtable クラスでは、isEmpty() メソッドを使用して、ハッシュ テーブルが空かどうかを判断します。この記事では、Hashtable クラスの isEmpty() メソッドの使用方法と、対応するコード例を紹介します。まず、Hashtable クラスを理解する必要があります。ハッシュ

php提交表单通过后,弹出的对话框怎样在当前页弹出,该如何解决 php提交表单通过后,弹出的对话框怎样在当前页弹出,该如何解决 Jun 13, 2016 am 10:23 AM

php提交表单通过后,弹出的对话框怎样在当前页弹出php提交表单通过后,弹出的对话框怎样在当前页弹出而不是在空白页弹出?想实现这样的效果:而不是空白页弹出:------解决方案--------------------如果你的验证用PHP在后端,那么就用Ajax;仅供参考:HTML code

watch4proとGTのどちらが優れていますか? watch4proとGTのどちらが優れていますか? Sep 26, 2023 pm 02:45 PM

Watch4proとgtはそれぞれ特徴や適用シーンが異なりますが、総合的な機能、高性能、スタイリッシュな外観を重視し、価格は高くてもいいという方にはWatch 4 Proの方が適しているかもしれません。高度な機能要件はなく、バッテリー寿命と手頃な価格を重視する場合は、GT シリーズの方が適しているかもしれません。最終的な選択は、個人のニーズ、予算、好みに基づいて決定する必要がありますが、購入する前に自分のニーズを慎重に検討し、さまざまな製品のレビューや比較を参照して、より情報に基づいた選択を行うことをお勧めします。

See all articles