散列查找(哈希表)



layout: post
title: 散列查找(哈希表)
date: 2017-05-20
tag: 數據結構和算法

目錄

  • TOC
    {:toc}

散列表

  • 現有的查找算法,對數據量特別大的時候不適用
  • 填裝因子(Loading Factor):設散列表空間大小為m,填入表中元素個數為n,則a=n/m為散列表的填裝因子。

  • 散列(Hashing) 的基本思想是:

①以關鍵字key為自變量,通過一個確定的函數 h(散列函數),計算出對應的函數值h(key),作為數據對象的存儲地址。

②可能不同的關鍵字會映射到同一個散列地址上,即h(keyi) = h(keyj)(當keyi ≠keyj),稱為“沖突(Collision)”----需要某種沖突解決策略

散列函數的構造方法

  • 散列函數兩個關鍵:

①計算簡單,以便提高轉換速度;

②關鍵詞對應的地址空間分布均勻,以盡量減少沖突。

數字關鍵字的散列函數構造

  • ①直接定址法:取關鍵詞的某個線性函數值為散列地址,即
      h(key) = a * key + b (a、b為常數)

  • ②除留余數法:散列函數為:h(key) = key mod p  (一般p取素數)

  • ③數字分析法:分析數字關鍵字在各位上的變化情況,取比較隨機的位作為散列地址。Eg:取11位手機號碼key的后4位作為地址:散列函數為:h(key) = atoi(key+7) (char *key)

  • ④折疊法:把關鍵詞分割成位數相同的幾個部分,然后疊加

    Eg: 56793542
         542
         793
        + 056
       ———
        1391
            h(56793542) = 391
  • ⑤平方取中法
    Eg: 56793542
      56793542
     x 56793542
    —————————
 3225506412905764
            h(56793542) = 641

字符關鍵詞的散列函數構造

  • 舉例:

  Eg:h(“abcde”)=‘a’*324+’b’*323+’c’*322+’d’*32+’e’

  Index Hash ( const char *Key, int TableSize )
  {
    unsigned int h = 0; /* 散列函數值,初始化為0 */
    while ( *Key != ‘\0’) /* 位移映射 */
      h = ( h << 5 ) + *Key++;
    return h % TableSize;
  }

處理沖突的方法

  • 換個位置:開放地址法
  • 同一位置的沖突對象組織在一起:鏈地址法

開放地址法

  • 在開放地址散列表中,刪除操作要小心。通常只能“懶惰刪除”,即需要增加一個“刪除標記(Deleted)”,而並不是真正刪除它,以便查找事不會“斷鏈”,其空間可以再下次插入時重用。

線性探測法

  • 以增量序列 1,2,……,(TableSize -1)循環試探下一個存儲地址。

  • 性能分析

平方探測法 (Quadratic Probing)--- 二次探測

  • 平方探測法:以增量序列1^2,-1^2,2^2,-2^2,……,q^2,-q^2且q ≤ TableSize/2 循環試探下一個存儲地址。
  • 定理:如果散列表長度TableSize是某個4k+3(k是正整數)形式的素數時,平方探測法就可以探查到整個散列表空間
//哈希表平方探測法
#include <stdio.h> 
#include <stdlib.h>
#include <math.h>

#define MAXTABLESIZE 100000 /* 允許開辟的最大散列表長度 */
typedef int ElementType;    /* 關鍵詞類型用整型 */
typedef int Index;          /* 散列地址類型 */
typedef Index Position;     /* 數據所在位置與散列地址是同一類型 */
/* 散列單元狀態類型,分別對應:有合法元素、空單元、有已刪除元素 */
typedef enum { Legitimate, Empty, Deleted } EntryType;
 
typedef struct HashEntry Cell; /* 散列表單元類型 */
struct HashEntry{
    ElementType Data; /* 存放元素 */
    EntryType Info;   /* 單元狀態 */
};
 
typedef struct TblNode *HashTable; /* 散列表類型 */
struct TblNode {   /* 散列表結點定義 */
    int TableSize; /* 表的最大長度 */
    Cell *Cells;   /* 存放散列單元數據的數組 */
};

/* 返回大於N且不超過MAXTABLESIZE的最小素數 */
int NextPrime( int N )
{ 
    int i, p = (N%2)? N+2 : N+1; /*從大於N的下一個奇數開始 */
 
    while( p <= MAXTABLESIZE ) {
        for( i=(int)sqrt(p); i>2; i-- )
            if ( !(p%i) ) break; /* p不是素數 */
        if ( i==2 ) break; /* for正常結束,說明p是素數 */
        else  p += 2; /* 否則試探下一個奇數 */
    }
    return p;
}
 
HashTable CreateTable( int TableSize )
{
    HashTable H;
    int i;
 
    H = (HashTable)malloc(sizeof(struct TblNode));
    H->TableSize = NextPrime(TableSize);/* 保證散列表最大長度是素數 */
    H->Cells = (Cell *)malloc(H->TableSize*sizeof(Cell));/* 聲明單元數組 */
    /* 初始化單元狀態為 空單元 */
    for( i=0; i<H->TableSize; i++ )
        H->Cells[i].Info = Empty;
 
    return H;
}

Position Hash(ElementType Key, int TableSize )
{
    return Key % TableSize;
}

/*平方探測法1^2,-1^2,2^2,-2^2 …*/ 
Position Find( HashTable H, ElementType Key )
{
    Position CurrentPos, NewPos;
    int CNum = 0; /* 記錄沖突次數 */
 
    NewPos = CurrentPos = Hash( Key, H->TableSize ); /* 初始散列位置 */
    /* 當該位置的單元非空,並且不是要找的元素時,發生沖突 */
    while( H->Cells[NewPos].Info!=Empty && H->Cells[NewPos].Data!=Key ) {
                                           /* 字符串類型的關鍵詞需要 strcmp 函數!! */
        /* 統計1次沖突,並判斷奇偶次 */
        if( ++CNum%2 ){ /* 奇數次沖突 */
            NewPos = CurrentPos + (CNum+1)*(CNum+1)/4; /* 增量為+[(CNum+1)/2]^2 */
            if ( NewPos >= H->TableSize )
                NewPos = NewPos % H->TableSize; /* 調整為合法地址 */
        }
        else { /* 偶數次沖突 */
            NewPos = CurrentPos - CNum*CNum/4; /* 增量為-(CNum/2)^2 */
            while( NewPos < 0 )
                NewPos += H->TableSize; /* 調整為合法地址 */
        }
    }
    return NewPos; /* 此時NewPos或者是Key的位置,或者是一個空單元的位置(表示找不到)*/
}

bool Insert( HashTable H, ElementType Key )
{
    Position Pos = Find( H, Key ); /* 先檢查Key是否已經存在 */
    if( H->Cells[Pos].Info != Legitimate ) { /* 如果這個單元沒有被占,說明Key可以插入在此 */
        H->Cells[Pos].Info = Legitimate;
        H->Cells[Pos].Data = Key;
        /*字符串類型的關鍵詞需要 strcpy 函數!! */
        return true;
    }
    else {
        printf("鍵值已存在");
        return false;
    }
}

int main()
{
    HashTable hash;
    hash = CreateTable(5);    //real size 0 1 2 3 4 5 6
    printf("size = %d\n",hash->TableSize);
    Insert(hash,1);
    Insert(hash,5);
    Insert(hash,6);
    Insert(hash,7);
    Insert(hash,8);
    Insert(hash,9);
    Insert(hash,10);
    return 0;
}

哈希表平方探測法

雙散列探測法 (Double Hashing)

  • 雙散列探測法: di 為i*h2(key),h2(key)是另一個散列函數探測序列成:h2(key),2h2(key),3h2(key),……(對任意的key,h2(key) ≠ 0 )
  • 探測序列還應該保證所有的散列存儲單元都應該能夠被探測到。選擇以下形式有良好的效果:h2(key) = p - (key mod p) (p < TableSize,p、TableSize都是素數)

再散列 (Rehashing)

  • 當散列表元素太多(即裝填因子 α太大)時,查找效率會下降;
  • 實用最大裝填因子一般取 0.5 <= α<= 0.85;當裝填因子過大時,解決的方法是加倍擴大散列表,這個過程叫做“再散列(Rehashing)”

分離鏈接法

  • 同一位置的沖突對象組織在一起
  • 分離鏈接法:將相應位置上沖突的所有關鍵詞存儲在同一個單鏈表中
  • 所有地址鏈表的平均長度定義成裝填因子α,α有可能超過1。

//分離鏈接法
#include <iostream>
#include <cstdio> 
#include <cstdlib>
#include <cstring>
#include <math.h>
using namespace std;

#define MAXTABLESIZE 100000 /* 允許開辟的最大散列表長度 */
#define KEYLENGTH 15                   /* 關鍵詞字符串的最大長度 */
typedef char ElementType[KEYLENGTH+1]; /* 關鍵詞類型用字符串 */
typedef int Index;                     /* 散列地址類型 */
 
/******** 以下是單鏈表的定義 ********/
typedef struct LNode *PtrToLNode;
struct LNode {
    ElementType Data;
    PtrToLNode Next;
};
typedef PtrToLNode Position;
typedef PtrToLNode List;
/******** 以上是單鏈表的定義 ********/
 
typedef struct TblNode *HashTable; /* 散列表類型 */
struct TblNode {   /* 散列表結點定義 */
    int TableSize; /* 表的最大長度 */
    List Heads;    /* 指向鏈表頭結點的數組 */
};
 
int NextPrime( int N )
{ /* 返回大於N且不超過MAXTABLESIZE的最小素數 */
    int i, p = (N%2)? N+2 : N+1; /*從大於N的下一個奇數開始 */
 
    while( p <= MAXTABLESIZE ) {
        for( i=(int)sqrt(p); i>2; i-- )
            if ( !(p%i) ) break; /* p不是素數 */
        if ( i==2 ) break; /* for正常結束,說明p是素數 */
        else  p += 2; /* 否則試探下一個奇數 */
    }
    return p;
}

HashTable CreateTable( int TableSize )
{
    HashTable H;
    int i;
 
    H = (HashTable)malloc(sizeof(struct TblNode));
    H->TableSize = NextPrime(TableSize);/* 保證散列表最大長度是素數 */
    H->Heads = (List)malloc(H->TableSize*sizeof(struct LNode));/* 以下分配鏈表頭結點數組 */
    
    /* 初始化表頭結點 */
    for( i=0; i<H->TableSize; i++ ) {
         H->Heads[i].Data[0] = '\0';
         H->Heads[i].Next = NULL;
    }
 
    return H;
}

Index Hash(ElementType Key, int TableSize )
{
    
    return (*Key - 'a') % TableSize;
}


Position Find( HashTable H, ElementType Key )
{
    Position P;
    Index Pos;
     
    Pos = Hash( Key, H->TableSize ); /* 初始散列位置 */
    P = H->Heads[Pos].Next; /* 從該鏈表的第1個結點開始 */
    /* 當未到表尾,並且Key未找到時 */ 
    while( P && strcmp(P->Data, Key) )
        P = P->Next;
 
    return P; /* 此時P或者指向找到的結點,或者為NULL */
}

bool Insert( HashTable H, ElementType Key )
{
    Position P, NewCell;
    Index Pos;
     
    P = Find( H, Key );
    if ( !P ) { /* 關鍵詞未找到,可以插入 */
        NewCell = (Position)malloc(sizeof(struct LNode));
        strcpy(NewCell->Data, Key);
        Pos = Hash( Key, H->TableSize ); /* 初始散列位置 */ 
        /* 將NewCell插入為H->Heads[Pos]鏈表的第1個結點 */
        NewCell->Next = H->Heads[Pos].Next;
        H->Heads[Pos].Next = NewCell; 
        return true;
    }
    else { /* 關鍵詞已存在 */
        printf("鍵值已存在");
        return false;
    }
}

void DestroyTable( HashTable H )
{
    int i;
    Position P, Tmp;
     
    /* 釋放每個鏈表的結點 */
    for( i=0; i<H->TableSize; i++ ) {
        P = H->Heads[i].Next;
        while( P ) {
            Tmp = P->Next;
            free( P );
            P = Tmp;
        }
    }
    free( H->Heads ); /* 釋放頭結點數組 */
    free( H );        /* 釋放散列表結點 */
}

int main()
{
    HashTable hash;
    hash = CreateTable(5); //real size 7: 0 1 2 3 4 5 6
    Insert( hash, "a" );
    Insert( hash, "b" );
    Insert( hash, "c" );
    Insert( hash, "d" );
    Insert( hash, "e" );
    Insert( hash, "h" );
    Insert( hash, "g" );
    return 0;
}

分離鏈接法

Reference


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM