哈希表构建和冲突处理

查找本质:已经对象找位置
1、有序安排对象:全序、半序
2、直接“算出”对象位置:散列(哈希)
散列查找的两项基本工作:
1、计算位置:构造散列函数确定关键词存储的位置
2、解决冲突:应用某种策略解决多个关键词位置相同的问题
时间复杂度几乎是常量:O(1),查找时间和问题的规模无关
散列表(哈希表)
类型名称:符号表(SymbolTable)
数据对象集:符号表是“名字(name)-属性(Attribute)”对的集合
操作集:

//创建一个长度为TableSize的符号表
SymbolTable Initialize(int TableSize);
//查找特定的名字Name是否在符号表Table中
Boolean IsIn(SymbolTable Table, NameType Name);
//获取Table中指定名字Name对应的属性
AttributeType Find(SymbolTable Table, NameType Name);
//将Table中指定名字Name的属性修改为Attr
SymbolTable Modefy(SymbolTable Table, NameType Name, AttributeType Attr);
//向Table中插入一个新的名字Name及其属性Attr
SymbolTable Insert(SymbolTable Table, NameType Name, AttributeType Attr);
//从Table中删除一个名字Name以及其属性
SymbolTable Delete(SymbolTable Table, NameType Name);

例:有n = 11个数据对象的集合{18,23,11,20,2,7,27,30,42,15,34}。符号表的大小为TableSize = 17,选取散列函数h如下:h(Key) = Key mod TableSize(求余)

地址 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
关键字 34 18 2 20 23 7 42 27 11 30 15

如果新插入元素35,h(35) = 1,该位置已有对象,冲突
查找:
key = 22, h(22) = 5, 该地址空,不在表中
key = 30, h(30) = 13, 该地址存放30,找到
装填因子:设散列表空间大小为m,填入表中的元素个数是n,则称a = n / m为散列表的装填因子
a = n / m = 11 / 17 = 0.65
对于散列表的冲突问题,设立一个二维数组进行存放,如果插入元素时冲突,就将元素放入第二个位置。
例:将acos、define、float、exp、char、atan、ceil、floor、clock、ctime,顺次存入一张散列表中。
散列表设计为一个二维数组Table[26][2],2列为2个槽
散列函数为h(Key) = key[0] - ‘a’,所得到整数值为0-25,进而可以存入散列表
如果没有溢出
T(查询) = T(插入) = T(删除) = O(1)
散列函数的构造方法
原则:
1、计算简单,以便提高转化速度
2、关键词对应的地址空间分布均匀,以尽量减少冲突。
数字关键词的散列函数构造
1、直接定址法
取关键词的某个线性函数值为散列地址,即
h(key) = a * key + b
2、除留余数法
散列函数为:h(key) = key mod p, p一般取素数
3、数字分析法
分析数字关键字在各位上的变化情况,取比较随机的位作为散列地址
比如:取11位手机号码Key的后4位作为地址:
散列函数为:h(Key) = atoi(Key+7)(char * key),key指向11位号码字符串的第一个地址,加7向后移动7位,用atoi()将字符串转化成整数
字符关键词的散列函数构造
1、一个简单的散列函数——ASCll码加和法
把字符串中字符的ASCII码值加起来
代码如下

typedef unsigned int Index
Index Hash(const char *Key, int TableSize)
{
	unsigned int HashVal = 0;
	while(*Key != '\0')
		HashVal += *Key++;
	return HashVal % TableSize;
}

但当表很大时就不能很好的分配关键字,设TableSize = 10007(10007为素数),并设所有的关键字至多8个字符长,由于char类型的值最多是127,因此散列函数只能取值在1016之间,其中1016 = 127 * 8,这显然不是一种均匀分配

2、一个较好的散列方法——移位法
该散列的方法未必是最好的,但却是具有极其简单的优点。如果关键字特别长,那么散列函数计算起来将会花费过多的时间。在这种情况下通常的做法是不使用所有的字符,选取关键字中一部分有特定规律或者特点的字符进行散列函数的计算。

Index Hash(const char * key, int TableSize)
{
	unsigned int HashVal = 0;  //散列函数值,初始化为0
	while(*key != '\0')  //位移映射
		HashVal = (HashVal << 5) + *Key++;    //位操作
	return HashVal % TableSize
}

冲突处理方法
如果当一个元素被插入时另一个元素已经存在,就会产生一个冲突,这个冲突需要消除,解决这种冲突的方法有几种,我们将讨论其中最简单的两种:分离链接法和开放地址法。
换个位置:开放地址法
同一个位置的冲突对象组织在一起:分离链接法
开放地址法:一旦发生了冲突就按某种规则去寻找另一空地址
若发生第i次冲突,一个试探的下一个地址将增加d(i),基本公式是:
hi(key) = (h(key) + d(i)) mod TableSize (1 <= i <= TableSize)
d(i)决定了不同的解决冲突方案:线性探测、平方探测、双散列
线性探测d(i) = i, 平方探测d(i) = ±i²(超出散列表地址之外通过求余解决),双散列d(i) = i * h2(key),再设计一个散列函数,计算原散列函数的偏移量
线性探测容易产生聚集现象
散列查找性能分析
成功平均查找长度(ASLs)、不成功平均查找长度(ASLu)
对于平方探测:如果散列表长度TableSize是某个4K+3形式的素数时,平方探测法就可以探测到整个散列表空间
创建开放地址法的散列表,Cell表示一个数组,TableSize为当前表的大小,我们希望TableSize为素数,用NextPrime函数解决,用真正计算出来的TableSize分配一个空间,将它分配给指针,数组中的每个分量设置为结构数组,Info表示位置状态,
代码如下:

#define MAX_TABLE_SIZE 10000	 /*允许开辟的最大散列表的长度*/
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;               /*存放散列单元数据的数组*/
}
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;
    if(TableSize < MinTableSize)
    {
    	Error("散列表太小")return NULL;
    }

    H = (HashTable)malloc(sizeof(struct TblNode));
    /* 保证散列表最大长度是素数 */
    H->TableSize = NextPrime(TableSize);
    /* 声明单元数组 */
    H->Cells = (Cell *)malloc(H->TableSize*sizeof(Cell));
    /* 初始化单元状态为“空单元” */
    if(H->cells == NULL)
    	FataError("空间溢出")
    for( i=0; i<H->TableSize; i++ )
          H->Cells[i].Info = Empty;

    return H;
}

平方探测的查找与插入,代码如下:

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;
    }
}

分离链接法其做法是将散列到同一个值的所有元素保留到一个表中,为了方便期间,这些表都设有表头
执行一次查找所需要的时间为计算散列函数值所需要的常数时间加上遍历表所用的时间
分离链接法的散列实现,代码如下:

#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; /* 散列表类型,HashTable类型就是指向该结构的指针类型 */
struct TblNode {   /* 散列表结点定义 */
    int TableSize; /* 表的最大长度 */
    List Heads;    /* 指向链表头结点的数组 */
};
/******** 以上是散列表的定义 ********/
//散列表的初始化函数
HashTable Initialize_Table( int TableSize )
{
    HashTable H;
    int i;
    
    if(TableSize < MinTableSize)
    {
    	Error("Table size too small")
    	return NULL;
	}
	/* Allocate table*/
    H = (HashTable)malloc(sizeof(struct TblNode));
    if(H == NULL)
    	FatalError("Out of space");
    /* 保证散列表最大长度是素数,具体见文中代码 */
    H->TableSize = NextPrime(TableSize);
    /* 以下分配链表头结点数组 */
    H->Heads = malloc(sizeof(List) * H->TableSize);
    if(H->Heads == NULL)
    	FateError("Out of space");
    /* 初始化表头结点 */
    for( i = 0; i  < H->TableSize; i++ ) 
    {
    	 H->Heads[i] = (List)malloc(sizeof(struct LNode));
    	 if(H->Heads[i] == NULL)
    	 	FateError("Out of space");
    	 else
         	H->Heads[i]->Next = NULL;
    }
    return H;
}
//对于Find(key,H)函数调用将返回一个指针,该指针指向包含Key那个单元
Position Find(ElementType Key, HashTable H )
{
    Position P;
    Index Pos;  //散列数组的下标
    
    Pos = Hash( Key, H->TableSize ); /* 初始散列位置 */
    P = H->Heads[Pos].Next; /* 从该链表的第1个结点开始 */
    /* 当未到表尾,并且Key未找到时 */ 
    while( P != NULL && 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));
        NewCell->Data = Key; /* C语言中用函数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 != NULL ) {
            Tmp = P->Next;
            free( P );
            P = Tmp;
        }
    }
    free( H->Heads ); /* 释放头结点数组 */
    free( H );        /* 释放散列表结点 */
}

开放地址法:基于一个数组,存储效率高,随机查找。但容易产生“聚集”现象
分离链接法:是顺序存储和链式存储的结合,链表部分的存储效率和查找效率都比较低。关键字删除不需要“懒惰删除”法,从而没有存储“垃圾”。太小的a可能导致空间浪费,太大的a又将付出更多的时间代价。不均匀的链表长度导致时间效率的严重下降

你可能感兴趣的:(数据结构与算法学习笔记)