GLIB 常用數據結構介紹 2

哈希表

概念

到目前爲止,本教程只介紹了有序容器,在其中插入的條目會保持特定次序不變。哈希表 是另一類容器,也稱爲“映射”、“聯合數組(associative array)” 或者“目錄(dictionary)”。

正如語文辭典使用一個定義來關聯一個詞,哈希表使用一個 鍵(key) 來唯一標識一個 值(value)。哈希表可以根據鍵非常快速地執行插入、查找和刪除操作;實際上,如果使用得當,這些可以都是常數時間 —— 也就是 O(1) —— 操作。這比從一個有序列表中查找或刪除條目快得多,那是 O(n) 操作。

哈希表之所以能快速執行操作,是因爲它們使用 散列函數 來定位鍵。散列函數獲得一個鍵併爲其計算一個唯一的值,稱爲 散列值(hash)。例如,一個散列函數可以接受一個詞並將那個詞中的字母數作爲散列值返回。那是個不好的散列函數,因爲 “fiddle”和“faddle”將會散列爲相同的值。

當散列函數爲不同的鍵返回相同的散列值時,取決於哈希表的實現會發生各種不同的事情。哈希表可以使用第二個值覆蓋第一個值,也可以將值放入一個列表,或者或以簡單地拋出一個錯誤。

注意,哈希表不是必然比列表更快。如果擁有的條目較少 —— 少於一打左右 —— 那麼使用有序的集合會獲得更好的性能。那是因爲,儘管在哈希表中存儲和獲取數據需要常數時間,那個常數時間值可能會很大,因爲計算條目的散列值相對於反引用一兩個指針會是一個較慢的過程。對於較小的值,簡單地遍歷有序列表會比進行散列計算更快。

無論何時,重要的是在選擇容器時要考慮自己應用程序的具體數據存儲需要。如果應用程序很明顯需要某種容器,那麼沒有理由不去使用它。




一些簡單的哈希表操作

這裏是一些示例,可以生動地展示以上的理論:


//ex-ghashtable-1.c
#include <glib.h>
int main(int argc, char** argv) {
 GHashTable* hash = g_hash_table_new(g_str_hash, g_str_equal);
 g_hash_table_insert(hash, "Virginia", "Richmond");
 g_hash_table_insert(hash, "Texas", "Austin");
 g_hash_table_insert(hash, "Ohio", "Columbus");
 g_printf("There are %d keys in the hash/n", g_hash_table_size(hash));
 g_printf("The capital of Texas is %s/n", g_hash_table_lookup(hash, "Texas"));
 gboolean found = g_hash_table_remove(hash, "Virginia");
 g_printf("The value 'Virginia' was %sfound and removed/n", found ? "" : "not ");
 g_hash_table_destroy(hash);
 return 0;
}

***** Output *****

There are 3 keys in the hash
The capital of Texas is Austin
The value 'Virginia' was found and removed


有很多新東西,所以給出一些註解:

    * 對 g_hash_table_new 的調用指定了這個哈希表將使用字符串作爲鍵。函數 g_str_hash 和 g_str_equal 是 GLib 的內置函數,因爲這很常用。其他內置 散列/等同(equality) 函數包括 g_int_hash /g_int_equal(使用整數作爲鍵)以及 g_direct_hash/g_direct_equal(使用指針作爲鍵)。
    * GLists 和 GSLists 擁有一個 g_[container]_free 函數來清除它們;可以使用 g_hash_table_destroy 來清空 GHashTable。
    * 當嘗試使用 g_hash_table_remove 刪除 鍵/值 對時,會獲得一個 gboolean 返回值,表明鍵是否找到並刪除。gboolean 是 真/假 值的一個簡單的跨平臺 GLib 實現。
    * g_hash_table_size 返回哈希表中鍵的數目。





插入和替換值

當使用 g_hash_table_insert 插入鍵時,GHashTable 首先檢查那個鍵是否已經存在。如果已經存在,那麼那個值會被替換,而鍵不會被替換。如果希望同時替換鍵和值,那麼需要使用 g_hash_table_replace。它稍有不同,因此在下面同時展示了二者:


//ex-ghashtable-2.c
#include <glib.h>
static char* texas_1, *texas_2;
void key_destroyed(gpointer data) 
{
 g_printf("Got a key destroy call for %s/n", data == texas_1 ? "texas_1" : "texas_2");
}
int main(int argc, char** argv) 
{

 GHashTable* hash = g_hash_table_new_full(g_str_hash, g_str_equal,  (GDestroyNotify)key_destroyed, NULL);
 
 texas_1 = g_strdup("Texas");
 texas_2 = g_strdup("Texas");
 g_hash_table_insert(hash, texas_1, "Austin");
 g_printf("Calling insert with the texas_2 key/n");
 g_hash_table_insert(hash, texas_2, "Houston");
 g_printf("Calling replace with the texas_2 key/n");
 g_hash_table_replace(hash, texas_2, "Houston");
 g_printf("Destroying hash, so goodbye texas_2/n");
 g_hash_table_destroy(hash);
 g_free(texas_1);
 g_free(texas_2);
 return 0;
}

***** Output *****

Calling insert with the texas_2 key
Got a key destroy call for texas_2
Calling replace with the texas_2 key
Got a key destroy call for texas_1
Destroying hash, so goodbye texas_2
Got a key destroy call for texas_2


從輸出可以看到,當 g_hash_table_insert 嘗試插入與現有鍵相同的字符串(Texas)時, GHashTable 只是簡單的釋放傳遞進來的鍵(texas_2),並令當前鍵(texas_1)保持不變。但是當 g_hash_table_replace 做同樣的事情時,texas_1 鍵被銷燬,並在使用它的地方使用 texas_2 鍵。更多註解:

    * 當創建新的 GHashTable 時,可以使用 g_hash_table_full 來提供一個 GDestroyNotify 實現,在鍵被銷燬時調用它。這讓您能夠爲那個鍵進行完全的資源清除,或者(在本例中)去查看在鍵變化時實際發生的事情。
    * 在前面的 GSList 部分已經出現過 g_strdup;在這裏使用它來分配字符串 Texas 的兩個拷貝。可以發現,GHashTable 函數 g_str_hash 和 g_str_equal 正確地檢測到,儘管指針指向不同的內存位置,但實際上字符串是相同的。爲了避免內存泄漏,在函數的末尾必須釋放 texas_1 和 texas_2 當然,在本例中這並不重要,因爲程序會退出,但是無論如何能夠清除是最好的。





遍歷 鍵/值 對

有時需要遍歷所有的 鍵/值 對。這裏是如何使用 g_hash_table_foreach 來完成那項任務:


//ex-ghashtable-3.c
#include <glib.h>
void iterator(gpointer key, gpointer value ,gpointer user_data) 
{
 g_printf(user_data, *(gint*)key, value);
}



int main(int argc, char** argv) 
{
 GHashTable* hash = g_hash_table_new(g_int_hash, g_int_equal);
 
gint* k_one = g_new(gint, 1), *k_two = g_new(gint, 1), *k_three = g_new(gint, 1);
*  k_one = 1, *k_two=2, *k_three = 3;

 g_hash_table_insert(hash, k_one, "one");
 g_hash_table_insert(hash, k_two, "four");
 g_hash_table_insert(hash, k_three, "nine");
 
 g_hash_table_foreach(hash, (GHFunc)iterator, "The square of %d is %s  /n");
 g_hash_table_destroy(hash);
 return 0;
}


***** Output *****

The square of 1 is one
The square of 2 is four
The square of 3 is nine


在這個示例中有一些細微的不同之處:

    * 可以發現,使用 GLib 提供的散列函數 g_int_hash 和 g_int_equal 讓您能夠使用指向整數的指針作爲鍵。本示例使用的是整數的 GLib 跨平臺抽象: gint。
    * g_hash_table_foreach 與您已經瞭解的 g_slist_foreach 和 g_list_foreach 函數非常類似。唯一的區別是,傳遞到 g_hash_table_foreach 的 GHFunc 要接受三個參數,而不是兩個。在本例中,傳遞進入一個用來格式化鍵和字符串的打印的字符串作爲第三個參數。另外,儘管在本示例時鍵恰巧是以它們插入的順序打印出來,但絕對不保證那個鍵插入的順序會被保留。





查找條目      //(//(((((((((((((((((((未完全理解))))))))))))))))))))))))))))))))

使用 g_hash_table_find 函數來查找某個特定的值。這個函數支持查看每一個 鍵/值 對,直到定位到期望的值。這裏是一個示例:


//ex-ghashtable-4.c
#include <glib.h>
void value_destroyed(gpointer data) 
{
 g_printf("Got a value destroy call for %d/n", GPOINTER_TO_INT(data));
}

gboolean finder(gpointer key, gpointer value, gpointer user_data) 
{
 return (GPOINTER_TO_INT(key) + GPOINTER_TO_INT(value)) == 42;
}


int main(int argc, char** argv) 
{
 GHashTable* hash = g_hash_table_new_full(g_direct_hash, g_direct_equal,  NULL,  (GDestroyNotify)value_destroyed);
 
 g_hash_table_insert(hash, GINT_TO_POINTER(6), GINT_TO_POINTER(36));
 g_hash_table_insert(hash, GINT_TO_POINTER(10), GINT_TO_POINTER(12));
 g_hash_table_insert(hash, GINT_TO_POINTER(20), GINT_TO_POINTER(22));
 
 gpointer item_ptr = g_hash_table_find(hash, (GHRFunc)finder, NULL);
 gint item = GPOINTER_TO_INT(item_ptr);
 g_printf("%d + %d == 42/n", item, 42-item);
 
 g_hash_table_destroy(hash);
 return 0;
}

***** Output *****

36 + 6 == 42
Got a value destroy call for 36
Got a value destroy call for 22
Got a value destroy call for 12


照例,本示例介紹了 g_hash_table_find 以及其他一些內容:

    * GHRFunc 返回 TRUE 時,g_hash_table_find 返回第一個值。如果 GHRFunc 作用於任意條目都都不返回 TRUE(這表明沒有找到合適的條目),則它返回 NULL。
    * 本示例介紹了另一組內置的 GLib 散列函數:g_direct_hash 和 g_direct_equal。這組函數支持使用指針作爲鍵,但卻沒有嘗試去解釋指針背後的數據。由於要將指針放入 GHashTable,所以需要使用一些便利的 GLib 宏(GINT_TO_POINTER 和 GPOINTER_TO_INT)來在整數與指針之間進行轉換。
    * 最後,本示例創建了 GHashTable,並給予它一個 GDestroyNotify 回調函數,以使得您可以查看條目是何時被銷燬的。大部分情況下您會希望在一個與此類似的函數中釋放某些內存,不過出於示例的目的,這個實現只是打印出一條消息。





難處理的情形:從表中刪除

偶爾可能需要從一個 GHashTable 中刪除某個條目,但卻沒有獲得 GHashTable 所提供的任意 GDestroyNotify 函數的回調。要完成此任務,或者可以根據具體的鍵使用 g_hash_table_steal,或者根據所有匹配某個條件的鍵使用 g_hash_table_foreach_steal。


//ex-ghashtable-5.c
#include <glib.h>
gboolean wide_open(gpointer key, gpointer value, gpointer user_data) 
{
 return TRUE;
}

void key_destroyed(gpointer data) 
{
 g_printf("Got a GDestroyNotify callback/n");
}

int main(int argc, char** argv) 
{
 GHashTable* hash = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)key_destroyed,(GDestroyNotify)key_destroyed);
 
 g_hash_table_insert(hash, "Texas", "Austin");
 g_hash_table_insert(hash, "Virginia", "Richmond");
 g_hash_table_insert(hash, "Ohio", "Columbus");
 g_hash_table_insert(hash, "Oregon", "Salem");
 g_hash_table_insert(hash, "New York", "Albany");
 
 g_printf("Removing New York, you should see two callbacks/n");
 g_hash_table_remove(hash, "New York");
 if (g_hash_table_steal(hash, "Texas")) 
 {
  g_printf("Texas has been stolen, %d items remaining/n", g_hash_table_size(hash));
 }
 g_printf("Stealing remaining items/n");
 g_hash_table_foreach_steal(hash, (GHRFunc)wide_open, NULL);
 g_printf("Destroying the GHashTable, but it's empty, so no callbacks/n");
 g_hash_table_destroy(hash);
 return 0;
}

***** Output *****

Removing New York, you should see two callbacks
Got a GDestroyNotify callback
Got a GDestroyNotify callback
Texas has been stolen, 3 items remaining
Stealing remaining items
Destroying the GHashTable, but it's empty, so no callbacks






高級查找:找到鍵和值

針對需要從表中同時獲得鍵和值的情況,GHashTable 提供了一個 g_hash_table_lookup_extended 函數。它與 g_hash_table_lookup 非常類似,但要接受更多兩個參數。這些都是“out”參數;也就是說,它們是雙重間接指針,當數據被定位時將指向它。這裏是它的工作方式:


//ex-ghashtable-6.c
#include <glib.h>
int main(int argc, char** argv) 
{
 GHashTable* hash = g_hash_table_new(g_str_hash, g_str_equal);
 
 g_hash_table_insert(hash, "Texas", "Austin");
 g_hash_table_insert(hash, "Virginia", "Richmond");
 g_hash_table_insert(hash, "Ohio", "Columbus");
 
 char* state = NULL;
 char* capital = NULL;
 char** key_ptr = &state;
 char** value_ptr = &capital;
 
 gboolean result = g_hash_table_lookup_extended(hash, "Ohio", (gpointer*)key_ptr, (gpointer*)value_ptr);
 if (result)
  {
  g_printf("Found that the capital of %s is %s/n", capital, state);
 }
 if (!g_hash_table_lookup_extended(hash, "Vermont", (gpointer*)key_ptr, (gpointer*)value_ptr)) 
 {
  g_printf("Couldn't find Vermont in the hash table/n");
 }
 
 g_hash_table_destroy(hash);
 return 0;
}

***** Output *****

Found that the capital of Columbus is Ohio
Couldn't find Vermont in the hash table


初始化能夠接收 鍵/值 數據的變量有些複雜,但考慮到它是從函數返回多於一個值的途徑,這可以理解。注意,如果您爲後兩個參數之一傳遞了 NULL,則 g_hash_table_lookup_extended 仍會工作,只是不是填充 NULL 參數。




每個鍵多個值

到目前爲止已經介紹了每個鍵只擁有一個值的散列。不過有時您需要讓一個鍵持有多個值。當出現這種需求時,使用 GSList 作爲值並及 GSList 添加新的值通常是一個好的解決方案。不過,這需要稍多一些工作,如本例中所示:


//ex-ghashtable-7.c
#include <glib.h>
void print(gpointer key, gpointer value, gpointer data) 
{
 g_printf("Here are some cities in %s: ", key);
 g_slist_foreach((GSList*)value, (GFunc)g_printf, NULL);
 g_printf("/n");
}

void destroy(gpointer key, gpointer value, gpointer data)
 {
 g_printf("Freeing a GSList, first item is %s/n", ((GSList*)value)->data);
 g_slist_free(value);
}

int main(int argc, char** argv)
 {
 GHashTable* hash = g_hash_table_new(g_str_hash, g_str_equal);
 
 g_hash_table_insert(hash, "Texas",  g_slist_append(g_hash_table_lookup(hash, "Texas"), "Austin "));
 
 g_hash_table_insert(hash, "Texas",  g_slist_append(g_hash_table_lookup(hash, "Texas"), "Houston "));
 
 g_hash_table_insert(hash, "Virginia",  g_slist_append(g_hash_table_lookup(hash, "Virginia"), "Richmond "));
 
 g_hash_table_insert(hash, "Virginia",  g_slist_append(g_hash_table_lookup(hash, "Virginia"), "Keysville "));
 
 g_hash_table_foreach(hash, print, NULL);
 g_hash_table_foreach(hash, destroy, NULL);
 g_hash_table_destroy(hash);
 return 0;
}
***** Output *****

Here are some cities in Texas: Austin Houston
Here are some cities in Virginia: Richmond Keysville
Freeing a GSList, first item is Austin
Freeing a GSList, first item is Richmond


g_slist_append 接受 NULL 作爲 GSList 的合法參數,示例中的“insert a new city”代碼利用了這一事實;它不需要檢查這是不是添加到給定州的列表的第一個城市。

當銷燬 GHashTable 時,必須記住在釋放哈希表本身之前先釋放那些 GSList。注意,如果沒有在那些列表中使用靜態字符串,這會更爲複雜;在那種情況下需要在釋放列表本身之前先釋放每個 GSList 之中的每個條目。這個示例所展示的內容之一是各種 foreach 函數多麼實用 —— 它們可以節省很多輸入。




現實應用

這裏是如何使用 GHashTables 的樣例。

在 Gaim 中:

    * gaim-1.2.1/src/buddyicon.c 使用 GHashTable 來保持對“好友圖標(buddy icons)”的追蹤。鍵是好友的用戶名,值是指向 GaimBuddyIcon 結構體的指針。
    * gaim-1.2.1/src/protocols/yahoo/yahoo.c 是這三個應用程序中唯一使用 g_hash_table_steal 的地方。它使用 g_hash_table_steal 作爲構建帳號名到好友列表的映射的代碼片斷的組成部分。

在 Evolution 中:

    * evolution-2.0.2/smime/gui/certificate-manager.c 使用 GHashTable 來追蹤 S/MIME 證書的根源;鍵是組織名,值是指向 GtkTreeIter 的指針。
    * evolution-data-server-1.0.2/calendar/libecal/e-cal.c 使用 GHashTable 來追蹤時區;鍵是時區 ID 字符串,值是某個 icaltimezone 結構體的字符串描述。

在 GIMP 中:

    * gimp-2.2.4/libgimp/gimp.c 使用 GHashTable 追蹤臨時的過程。在整個代碼基(codebase)中唯一使用 g_hash_table_lookup_extended 的地方,它使用 g_hash_table_lookup_extended 調用來找到某個過程,以使得在刪除那個過程之前能首先釋放散列鍵的內存。
    * gimp-2.2.4/app/core/gimp.c 使用 GHashTable 來保存圖像;鍵是圖像的 ID(一個整數),值是指向 GimpImage 結構體的指針。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章