【数据结构】哈希表--链地址法

1,668 阅读4分钟

小知识,大挑战!本文正在参与“  程序员必备小知识  ”创作活动

大家好,我是melo,一名大二上软件工程在读生,经历了一年的摸滚,现在已经在工作室里边准备开发后台项目啦。
不过这篇文章呢,还是想跟大家聊一聊数据结构与算法,学校也是大二上才开设了数据结构这门课,希望可以一边学习数据结构一边积累后台项目开发经验。
不知不觉,我们已经接触了两三个数据结构了:链表、栈和队列,但有没有发现,前边的几个数据结构在查找性能方面其实并不是很好?为了进一步改进查找的性能,我们又要学习另一个全新的数据结构--哈希表

思路

  • 数组+链表来实现

Java8,当哈希冲突达到一定程度后,每一个位置从链表转成红黑树

数组

  • 一个指针数组,里边存放着元素的地址

链表

  • 我们设计出来的哈希函数,难免会出现冲突(即A和B都找到了H.rcd[i]这个位置),链地址法解决的思路是,让他们存在于这个位置上的一条同义词链表上(所谓同义词就是指他们的哈希地址一样)

开放地址法比起来,当发生哈希冲突时,他不是去改变哈希地址的值,而是"将错就错",让他们共存在同一个哈希地址上的链表上的不同位置。

插入新元素

先调用哈希函数找到val要所在的key

  • 若key有值,则遍历数组[key]上的同义词链表,验证是否已存在该val(哈希表要求不能重复)
    • 若存在,则return UNSUCCESS; //插入失败
    • 若不存在,则让该val指向原来的表头,同时成为新表头
  • 若key没值,则直接让数组[key]=val就好了

当然,上边都是伪代码,具体的实现还有很多细节需要注意

  • 而且上边的查找我们还可以单独封装成一个函数

头文件HashTable.h

typedef long KeyType;

//Node装载的结构体定义,里边可能有多种数据类型的元素.这里假设只有keyType类型,即long类型
typedef struct{
	KeyType key;
} RcdType;

头文件CHashTable.h

#include "Common.h"
#include "HashTable.h"

typedef struct Node {
     RcdType r;
     struct Node *next;
 } Node;

typedef struct {
      //指针数组
      Node **rcd;
      // 哈希表容量
	  int size;
      // 当前表中含有的记录个数
      int count; 
      // 函数指针变量,用于选取的哈希函数
      int (*hash)(KeyType key, int);  
} CHashTable; 

// 初始化哈希表
Status InitHash(CHashTable &H, int size, int (*hash)(KeyType,int));
// 销毁哈希表
Status DestroyHash(CHashTable &H); 
// 查找
Node* SearchHash(CHashTable H, KeyType key); 
// 插入
Status InsertHash(CHashTable &H, RcdType e); 
// 删除
Status deleteHash(CHashTable &H, KeyType key, RcdType &e); 

源文件CHashTable.cpp

#include "CHashTable.h"

//初始化哈希表
Status InitHash(CHashTable &H, int size, int (*hash)(KeyType,int))  {    
  int i;
  H.rcd = (Node**)malloc(sizeof(Node *)*size);
  if(NULL==H.rcd) return OVERFLOW;
  for (i = 0; i < size; i++) {
	  H.rcd[i] = NULL;
  }
  H.size = size;  
  H.hash = hash;  
  H.count = 0;
  return OK;
}

// 销毁哈希表
Status DestroyHash(CHashTable& H) {
	H.size = 0;
	H.count = 0;
	free(H.rcd);
	free(H.hash);
	//若销毁失败
	if (H.rcd != NULL || H.hash != NULL) {
		return UNSUCCESS;
	}
	return SUCCESS;
}

//查找
Node* SearchHash(CHashTable H, KeyType key) { 
	//找到在数组中的位置
   int p = H.hash(key, H.size);
   Node* np;
   //在该位置上的同义词链表上搜索
   for (np = H.rcd[p]; np != NULL; np = np->next) {
	   if (np->r.key == key) {
		   return np;
	   }
   }
   return NULL;
}

//插入记录e
Status InsertHash(CHashTable &H, RcdType e){
   int p;   
   Node *np;
   // 查找不成功时插入到表头
   if((np = SearchHash(H, e.key))==NULL) { 
      p = H.hash(e.key, H.size);
      np = (Node*)malloc(sizeof(Node));      
	  if(np==NULL)return OVERFLOW;
      np->r = e;  
	  //先指向表头
	  np->next = H.rcd[p];   
	  //再成为表头
	  H.rcd[p] = np;     
	  H.count++; 
	  return OK;
   }   
   else   
	   return ERROR;
}

//删除指定元素,并返回到e
Status deleteHash(CHashTable &H,KeyType key,RcdType &e) {
	//找到所在位置
	Node* np = SearchHash(H, key);
	//若查到不到
	if (NULL == np) {
		return UNSUCCESS;
	}
	//若找到了
	else
	{
		//找到在数组中的位置
		int p = H.hash(key, H.size);
		//当前节点
		Node* pNow;
		//返回给e
		e = np->r;
		//先特判是否是第一个
		if (H.rcd[p] == np) {
			//改变新表头
			H.rcd[p] = np->next;
		}
		//若不是第一个
		else
		{
			//在该位置上的同义词链表上搜索
			for (pNow = H.rcd[p]; pNow != NULL && pNow->next != NULL; pNow = pNow->next) {
				//若找到了Search返回结果的前驱
				if (pNow->next == np) {
					//让当前节点指向删除节点的下一个
					pNow->next = np->next;
				}
			}
		}
		/*
		  注意释放
		  释放后np如果没有置空或者再赋值,就会变成野指针了
		  防止再被用到,手动置空一下
		*/
		free(np);
		np = NULL;
        //注意count要相应减少
        H.count--;
		return SUCCESS;
	}
}

关于此处野指针的问题,具体可以参考 迷途指针

Test文件

#include "ChashTable.h"

int hash(KeyType key, int size) { 
    return (3*key)%size;
}

void collision(int &hashValue, int hashSize) {//线性探测法
     hashValue = (hashValue +1)% hashSize;
}

void TraverseCHashTable(CHashTable H){
   Node* np;
   int i;
   for(i = 0; i<H.size; i++) {
	   printf("\n%d :", i);
	   for(np = H.rcd[i]; np!=NULL; np = np->next)
		   printf(" %d ", np->r.key);
   }
}

void main(){
   /********链地址哈希表*********/
   CHashTable H;
   int (*h)(KeyType, int);
   h = hash;
   InitHash(H, 11, h);
   
   RcdType data[] = {22, 41, 53, 46, 30, 13, 12, 67};
   int i;
   for(i = 0; i<8; i++)
     InsertHash(H, data[i]);  

   printf("原始链表\n");
   TraverseCHashTable(H);
   printf("\n");
   RcdType e;
   KeyType k[] = { 41,22,40 };
   for (int i = 0; i < 3; i++) {
       if (deleteHash(H, k[i], e) == SUCCESS) {
           TraverseCHashTable(H);
           printf("\n删除%d成功\n",k[i]);
       }
       else
       {
           TraverseCHashTable(H);
           printf("\n删除%d失败\n",k[i]);
       }
       
   }
      
   system("pause");
}

删除运行效果

构建哈希表题目

其实他本身帮我们初始化好了,H.size也给出来了

\

#include "allinclude.h"  //DO NOT edit this line

//在哈希表中查找是否有key这个元素
HNode* SearchHash(ChainHashTab H,HKeyType key){
  //找到在数组中的位置
   int p = Hash(H,key);
   HNode* np;
   //在该位置上的同义词链表上搜索
   for (np = H.rcd[p]; np != NULL; np = np->next) {
     //找到则返回该指针
	   if (np->data == key) {
		   return np;
	   }
   }
   //找不到返回NULL
   return NULL;
}

//向哈希表中插入key这个元素
Status InsertHash(ChainHashTab &H,HKeyType key){
   int p;   
   HNode *np = SearchHash(H,key);
   // 查找不成功时插入到表头
   if(np == NULL) { 
      p = Hash(H,key);
      np = (HNode*)malloc(sizeof(HNode));  
      //若分配失败,返回NULLKEY
  	  if(np==NULL){
  	    return NULLKEY;
  	  }
      np->data = key;  
  	  //先指向表头
  	  np->next = H.rcd[p];   
  	  //再成为表头
  	  H.rcd[p] = np;     
  	  H.count++; 
  	  return OK;
   }
   //若是重复插入,返回UNSUCCESS
   else{   
	   return UNSUCCESS;
   }
}

int BuildHashTab(ChainHashTab &H, int n, HKeyType es[])
{  // Add your code here
  
  for(int i=0;i<n;i++){
    Status IfSucceed = InsertHash(H,es[i]);
    //重复插入,并不意味着整个哈希表的构建失败了
    //只有是插入的时候分配失败了,才意味着.....
    if(NULLKEY == IfSucceed){
      return ERROR;
    }
  }
  return OK;
}

写在最后

  • 其实关于哈希表的实现还有很多种方式,而且不同语言的底层也不一样,等到以后深入Java集合源码的时候,我们还可以再来聊一聊!

作者:『Melo_』
出处:juejin.cn/user/299988…
本文版权归作者和掘金共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。