数据结构全流程学习笔记(三):散列

数据结构全流程学习笔记(三):散列

基于《数据结构与算法分析:C语言描述》的笔记,可作为学习参考。
手边有这本书看起来更方便,没有也没关系。

5.1 一般想法

理想的散列表数据结构是一个含有关键字的具有固定大小的数组。
通过散列函数(hash function)将每个关键字映射到不同的单元。
需要解决的问题:选择一个函数,决定当两个关键字散列到同一个值的时候应该做什么 和 如何确定散列表的大小。

5.2 散列函数

//由散列函数返回的类型
typedef unsigned int Index;

//通常,关键字是字符串,我们将字符逐个相加来处理整个字符串
//这是一个简单的散列函数
Index Hash( const char *Key, int TableSize )
{
    unsigned int HashVal = 0;

    while( *Key != '\0' )
        HashVal += *Key++;
    
    return HashVal % TableSize;
}

但是,当散列表很大的时候,这个函数只能把值映射到一个很小的范围。
接下来看一个很好的散列函数。

//一个好的散列函数
Index Hash(const char* Key, int TableSize)
{
    unsigned int HashVal = 0;

    while(*Key != '\0')
        HashVal = (HashVal << 5) + *Key++;
        //位左移运算符,相当于乘32(2的5次方)

    return HashVal % TableSize;
}

不过,如果关键字特别长,计算会花费较多时间。一般会取关键字中的一部分进行计算。

当一个元素被插入处已经存在另一个元素,我们需要消除这个散列值相同的冲突,接下来讨论相关方法。

5.3 分离链接法

将散列到同一个值的所有元素保留在一个表中。
以下是分离链接法的代码实现。

ListNode声明与链表相同。散列表结构包括一个链表数组(以及数组中的链表的个数),它们在散列表结构初始化的时候动态分配空间。此处的HashTable类型就是指向该结构的指针类型。
TheList域实际上是一个指向 指向ListNode结构的指针 的指针。注意typedef的使用,它使代码更加清晰。

#include <stdio.h>
#include <stdlib.h>

#ifndef _HashSep_H

struct ListNode;
typedef struct ListNode* Position;
struct HashTbl;
typedef struct HashTbl* HashTable;
typedef int ElementType;
typedef Position List;
typedef int Index;

HashTable InitializeTable(int TableSize);
void DestroyTable(HashTable H);
Position Find(ElementType Key, HashTable H);
ElementType Retrieve(Position P);

#endif


struct ListNode
{
    ElementType Element;
    Position Next;
};


//*TheList是一个链表的数组
struct HashTbl
{
    int TableSize;
    List *TheList;
};

int IsPrime(int a)
{
    if(a == 1)
        return 0;
    if(a == 2)
        return 1;
    for (int i = 0; i * i < a; i++)
    {
        if(a % i == 0)
            return 0;
    }

    return 1;
    
}

int NextPrime(int a)
{
    int i;
    for (i = a; !IsPrime(i) ; i++)
    return i;
}

HashTable InitializeTable(int TableSize)
{
    HashTable H;
    int i;

    if(TableSize < 5 )
    {
        printf("Table is too small");
        return NULL;
    }

    H = malloc(sizeof(struct HashTbl));
    if(H == NULL)
        return NULL;
    
    H->TableSize = NextPrime(TableSize);

    H->TheList = malloc(sizeof(List) * H->TableSize);
    if(H->TheList == NULL)
        return NULL;
    
    for (int i = 0; i < H->TableSize; i++)
    {
        H->TheList[i] = malloc(sizeof(struct ListNode));
        if(H->TheList[i] == NULL)
            return NULL;
        else
            H->TheList[i]->Next = NULL;
    }

    return H;
    
}


//一个好的散列函数
Index Hash(const char* Key, int TableSize)
{
    unsigned int HashVal = 0;

    while(*Key != '\0')
        HashVal = (HashVal << 5) + *Key++;
        //位左移运算符,相当于乘32(2的5次方)

    return HashVal % TableSize;
}

Position Find(ElementType Key, HashTable H)
{
    Position P;
    List L;

    L = H->TheList[Hash(Key, H->TableSize)];
    P = L->Next;
    while(P != NULL && P->Element != Key)
        P = P->Next;
    
    return P;
}

void Insert(ElementType Key, HashTable H)
{
    Position Pos, NewCell;
    List L;

    Pos = Find(Key, H);
    if(Pos == NULL)
    {
        NewCell = malloc(sizeof(struct ListNode));
        if(NewCell == NULL)
            return NULL;
        else
        {
            L = H->TheList[ Hash(Key, H->TableSize) ];
            NewCell->Next = L->Next;
            NewCell->Element = Key;
            L->Next = NewCell;
        }
    }
}

这个算法的缺点是需要指针,给新单元分配地址需要时间。
我们定义装填因子 λ \lambda λ为散列表元素个数与散列表大小的比值。
λ = 1 \lambda = 1 λ=1
此时有最理想情况。

5.4 开放定址法

如果有冲突发生,就尝试选择另外的单元,直到找出空的单元为止。
单元h0(X), h1(X), h2(X),…相继试选。
h i ( X ) = ( H a s h ( X ) + F ( i ) )   m o d   T a b l e S i z e h_i(X) = (Hash(X)+F(i))\,mod\,TableSize hi(X)=(Hash(X)+F(i))modTableSize
其中F是冲突解决办法,F(0) = 0

5.4.1 线性探测法

F F F i i i的线性函数,逐个探测每个单元以查找出一个空单元。
花费的时间多,而且可能造成“聚集”的问题,即一些元素互相靠近形成一个区块。
可以对这种探测方法进行数学分析, λ = 0.5 \lambda = 0.5 λ=0.5时,这种方法的性能还是不错的。

5.4.2 平方探测法

F ( i ) = i 2 F(i) = i^2 F(i)=i2是最流行的选择。
定理:如果使用平方探测,且表的大小是素数,那么我们保证总能插入一个新的元素。
此处忽略数学证明,须知:哪怕表有比一半多一个的位置被填满,插入都有可能失败。同样的,表的大小必须是素数。

以下是这种方法的代码实现。

#include <stdio.h>
#include <stdlib.h>

#ifndef _HashQuad_H

typedef unsigned int Index;
typedef Index Position;
typedef int ElementType;

struct HashTbl;
typedef struct HashTbl *HashTable;

HashTable InitializeTable(int TableSize);
void DestroyTable(HashTable H);
Position Find(ElementType Key, HashTable H);
void Insert(ElementType Key, HashTable H);
ElementType Retrieve(Position P, HashTable H);
HashTable Rehash(HashTable H);

#endif

enum KindOfEntry{Legitimate, Empty, Deleted};

struct HashEntry
{
    ElementType Element;
    enum KindOfEntry Info;
};

typedef struct HashEntry Cell;

struct HashTbl
{
    int TableSize;
    Cell *TheCells;
};

int IsPrime(int a)
{
    if(a == 1)
        return 0;
    if(a == 2)
        return 1;
    for (int i = 0; i * i < a; i++)
    {
        if(a % i == 0)
            return 0;
    }

    return 1;
    
}

int NextPrime(int a)
{
    int i;
    for (i = a; !IsPrime(i) ; i++)
    return i;
}

HashTable InitializeTable(int TableSize)
{
    HashTable H;
    int i;

    H = malloc( sizeof( struct HashTbl ) );

    H->TableSize = NextPrime(TableSize);

    H->TheCells = malloc(sizeof(Cell) * H->TableSize);

    for ( i = 0; i < H->TableSize; i++)
        H->TheCells[i].Info = Empty;
    
    return H;
}

Position Find(ElementType Key, HashTable H)
{
    Position CurrentPos;
    int CollisionNum;

    CollisionNum = 0;
    CurrentPos = Hash(Key, H->TableSize);
    while( H->TheCells[CurrentPos].Info != Empty && H->TheCells[CurrentPos].Element != Key )
    {
        CurrentPos += 2 * ++CollisionNum - 1;
        if(CurrentPos >= H->TableSize)
            CurrentPos -= H->TableSize;
    }

    return CurrentPos;
}

5.4.3 双散列

一种流行的选择是 F i ( X ) = i ∗ h a s h 2 ( X ) F_i(X) = i * hash_2(X) Fi(X)=ihash2(X)。即为应用第二个散列函数,但这非常考验对散列函数的选择。
在实践中通常使用平方探测,双散列相较而言更加复杂、速度更慢。

5.5 再散列

对于使用平方探测的开放定址散列法,如果表的元素填得太满,那么操作的运行时间将开始消耗过长,而且Insert操作可能失败。
此时一种解决方法是建立另外一个大约两倍大的表(而且使用一个相关的新散列函数),扫描原始散列表,计算每个元素的新散列值并将其插入到新表中。
以下是再散列的实现:

HashTable Rehash(HashTable H)
{
    int i, OldSize;
    Cell *OldCells;

    OldCells = H->TheCells;
    OldSize = H->TableSize;

    H = InitializeTable(2 * OldSize);

    for(i = 0; i < OldSize; i++)
        if(OldCells[i].Info == Legitimate)
            Insert(OldCells[i].Element, H);
        
    free(OldCells);

    return H;
}

5.6 可扩散列

最后讨论一下数据量过大的情况,在此情况下,主存无法装进所有需要处理的数据,因此需要对磁盘进行操作。
一种选择叫可扩散列(extendible hashing),它的思想有些类似于B树。

  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值