数据结构学习笔记
九、查找
-
顺序查找 O(N)
-
二分查找(静态查找) O(log2N)
-
二叉搜索树 O(h) h 为二叉查找树的高度
平衡二叉树 O(log2N) 平衡二叉树怎么构造呢?
-
散列查找 几乎是O(1),以空间换时间
9.5 散列查找
关键点:
计算位置:构造散列函数确定关键词存储位置;
解决冲突:应用某种策略解决多个关键词位置相同的问题。
9.5.1 散列表(哈希表)
装填因子(Loading Factor):设散列表空间大小为m,填入表中元素的个数是n,则称α = n/m 为散列表的装填因子。
散列函数: 以关键字key为自变量,计算出数据的存储地址。
冲突: 不同关键字通过散列函数计算得到同一个地址。
9.5.2 散列函数的构造
- 计算简单;
- 地址空间分布均匀。
9.5.2.1 数字关键词的散列函数构造
1、直接定址法
h(key) = a * key + b
2、除留余数法
h(key) = key mod p
p一般是表的大小,而且为了地址均匀,取素数。
3、数字分析法
分析数字关键字在各位上的变化情况,取比较随机的位作为散列地址。
例如:11位手机号码(前几位代表地区比较固定,取后4位为地址)
散列函数:h(key) = atoi(key+7)
4、折叠法
把关键词分割成位数相同的几个部分,然后叠加。
5、平方取中法
把数字平方,取结果的中间几位数。
9.5.2.2 字符关键词的散列函数构造
1、ASCII码加和法(冲突严重)
h(key) = (∑key[i]) mod TableSize
2、前3个字符移位法(仍然冲突,空间浪费≈30%)
h(key) = (key[0]*272 + key[1]*27 + key[2]) mod TableSize
27的原因是:26个字母加上空格。
3、移位法
考虑所有的字符。
h(key) = (∑key[n-i-1]*32i) mod TableSize
级数计算的处理方法
移位法的计算过程:(key[0]*32 + key[1])*32 + key[2]…
因为乘以32,相当于左移5位,(因为25=32)
Index Hash(const char* Key, int TableSize){
unsigned int h = 0; //当前结果
while(*Key != '\0')
h = (h << 5) + *Key++; //上一轮结果*32 + 当前字符值
return h % TableSize;
}
9.5.3 冲突处理方法
1、开发地址法:换个位置;
2、链地址法:同一位置的冲突对象组织在一起。
9.5.3.1 开放地址法
若发生了第 i 次冲突,试探的下一个地址将增加 di ,基本公式是:
hi(key) = (h(key) + di) mod TableSize
di的取值方法:线性探测、平方探测、双散列。
1、线性探测:di = i (第一次加1,第二次加2,第三次加3)
2、平方探测(二次探测):di = ± i2 (第一次+1,第二次-1,第三次+22,第四次-22……)
3、双散列:di = i * h2(key) (第二个散列函数用来计算偏移量)
线性探测法(Linear Probing)
线性探测的缺点:会产生聚集现象,冲突后,地址集中在一块。
平方探测法(Quadratic Probing)
二次探测优点:减少了聚集;缺点:空间还有的时候,可能找不到空位。
定理:散列表长度是某个 4k+3(k是正整数)形式的素数时,平方探测法就可以探查到整个散列表空间。
typedef struct{
ElementType Element;
int Info; //标志:是否为空,删除是逻辑删除
}Cell;
typedef struct HashTbl* HashTable;
struct HashTbl{
int TableSize;
Cell* TheCells;
};
HashTable InitializeTable(int TableSize){
HashTable H;
int i;
if(TableSize < MinTableSize){
Error("散列表太小");
return NULL;
}
H = (HashTable)malloc(sizeof(struct HashTbl));
if(H == NULL)
FatalError("空间溢出!!");
//if当前表大小不符合素数,就修改成素数。
H->TableSize = NextPrime(TableSize);
//分配Cells空间
H->TheCell = (Cell*)malloc(sizeof(Cell)*H->TableSize);
if(H->TheCells == NULL)
FatalError("空间溢出!!");
for(i=0; i<H->TableSize; i++)
H->TheCells[i].Info = Empty;
return H;
}
Position Find(ElementType Key, HashTable H){//平方探测
Position CurrentPos, NewPos;
int CNum; //记录冲突次数
NewPos = CurrentPos = Hash(Key, H->TableSize);
//产生冲突
while(H->TheCells[NewPos].Info != Empty &&
H->TheCells[NewPos].Element != Key){//字符串关键字用strcmp
if(++CNum % 2){ //判断冲突的奇偶
NewPos = CurrentPos + (CNum+1)/2*(CNum+1)/2;
while(NewPos >= H->TableSize)
NewPos -= H->TableSize;
} else{
NewPos = CurrentPos - CNum/2 * CNum/2;
while(NewPos < 0)
NewPos += H->TableSize;
}
}//while结束
return NewPos;
}
//插入
void Insert(ElementType Key, HashTable H){
Position Pos;
Pos = Find(Key, H);
if(H->TheCells[Pos].Info != Legitimate){
H->TheCells[Pos].Info = Legitimate;
H->TheCells[Pos].Element = Key;
//字符串关键词用strcpy函数。
}
}
双散列探测法(Double Hashing)
为了能实现探测到所有地址的效果,建议h2(key) = p - (key mod p)
其中,p < TableSize, p、TableSize都是素数
再散列(Rehashing)
当散列表元素太多(即装填因子过大)时,查找效率会下降。
最大装填因子一般取 0.5 <= α <= 0.85
扩大散列表;同时重新计算Hash值,塞入新表。
9.5.3.2 链地址法
分离链接法(Separate Chaining)
将相应位置上冲突的所有关键词存储在同一个单链表中。
#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;
}
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 ); /* 释放散列表结点 */
}