1,哈希地址
哈希地址是一个逻辑地址,也就是说,哈希地址并不是真正意义上数据的地址,而是我们为了方便储存数据而自己进行设定的一种“编号”,而哈希地址是由哈希函数来得到的,一般这个函数是由自己决定的。
这里假设用数组来描述哈希的哈希内存,也就是用数组来存储哈希数据,而在数组中,每个数据的下标就是它们的哈希地址,例如在下面这个数组中,40的下标就为0,而0,也就是A的哈希地址。一般情况下,我们使用哈希构造函数,把数据和数据的下标建立联系。
40 | 51 | 47 | 33 | 44 |
0 | 1 | 2 | 3 | 4 |
在求数据的哈希地址时,我们通常用取余法来作为哈希函数,公式如下:H(key)=key%p,这里的p一般是最大容量,而上表的最大容量就是5。上图就是用取余法得到的每个数的哈希地址。例如,H(40)=40%5=0,H(51)=51%5=1,H(47)=47%5=2。
2,哈希冲突
但是,如果采用这种取余的方法来求每个数据的哈希地址,就会产生一个问题,就是有可能会有多个数据对应一个地址,而这种现象被称为哈希冲突。而我们解决哈希冲突的方法一般有两种,一种是“开放地址法”,另一种是
首先,是开放地址法,所谓开放地址法就是如果当前地址产生了哈希冲突,就向该地址后面寻找空的地址,并将该数据放入,例如下表
36 | 43 | 2 | 35 | ||
0 | 1 | 2 | 3 | 4 | 5 |
在上图的数组中,如果我们要放入一个数13进去,而通过H(13)=13%6=1求得13本来的哈希地址为1,但是,在1上已经有数据了,这时,我们就向下寻找,先看下标为2的数据,有数据了,再往下看下标为3的位置,发现没有数据,这时,我们就可以得出13的哈希地址为3。
而第二种方式,就是邻接表法。所谓邻接表法,就是以当前位置去创建一个链表出来,把具有冲突的元素来存放在链表中。
3,数组实现哈希结构
首先,构建一个键,来充当哈希地址的求解
typedef struct pair
{
int key;//构建一个键出来 充当哈希地址的求解
char element[20];//数据类型
}DATA,*LPDATA;
接下来,是哈希表的构建,这里采用的方式是取余法
typedef struct hashTable
{
LPDATA* table;//便于初始化,以及判断当前hash地址是否存在冲突
int divisor;//H(key)=key%p-->就是限定hash地址的数目
//[0,p-1]
int curSize;
}HASH,*LPHASH;
创建一个哈希结构
LPHASH createHashTable(int P)//哈希表的创建
{
LPHASH hash = (LPHASH)malloc(sizeof(HASH));//内存申请
assert(hash);//断言处理
hash->curSize = 0;
hash->divisor = P;//p为取余数
hash->table = (LPDATA*)malloc(sizeof(LPDATA) * hash->divisor);//容量是由取余数决定
assert(hash->table);//断言处理
for (int i = 0; i < hash->divisor; i++)
{
hash->table[i] = NULL;对每个一级指针赋值为空
}
return hash;
}
在插入元素前,还需要寻找储存当前元素的地址
int search(LPHASH hash, int key)
{
int pos = key % hash->divisor;//不存在冲突的hash
//开放地址法
int curPos = pos;
do
{
//key相同,采用覆盖的数据方式
if (hash->table[curPos] == NULL||hash->table[curPos]->key==key)
return curPos;
curPos = (curPos + 1) % hash->divisor;
} while (curPos != pos);
return curPos;
}
在找到可以存放该元素的地址后,便可以开始插入元素了
void insertData(LPHASH hash, DATA data)
{
//求hash地址
int pos = search(hash, data.key);
if (hash->table[pos] == NULL)
{
hash->table[pos] = (LPDATA)malloc(sizeof(DATA));
memcpy(hash->table[pos], &data, sizeof(DATA));
hash->curSize++;
}
else
{
if (hash->table[pos]->key == data.key)
{
strcpy(hash->table[pos]->element, data.element);
}
else
{
printf("hash表满了,无法插入\n");
return;
}
}
}
接下来,就是将这些元素打印下来
void printHash(LPHASH hash)
{
for (int i = 0; i < hash->divisor; i++)
{
if (hash->table[i] == NULL)
{
printf("NULL\n");
}
else
{
printf("%d:%s\n",hash->table[i]->key, hash->table[i]->element);
}
}
}
最后就是将前面的函数引入主函数中去
int main()
{
LPHASH hash = createHashTable(10);
DATA array[5] = { 1,"A",21,"B",13,"C",44,"D",56,"E" };
for (int i = 0; i < 5; i++)
{
insertData(hash, array[i]);
}
printHash(hash);
return 0;
}
4,散列表
散列表是由一个数据域和两个指针域组成的,两个指针域分别为纵向和横向。其中,纵向是一个有序链表,也就是每个元素的地址,而横向则是用来处理哈希冲突的。
10 | 25 | 35 |
21 | 26 | |
37 | ||
43 | 53 | |
14 |
例如上表中,该哈希函数的p为5,所以10,25,35对5取余都是0,则它们产生了哈希冲突,所以就放入一个横向的链表中,而纵向的第一列也是一个链表,而这个链表则是按照哈希地址来进行有序排序的。故而散列表是由一个数据域和两个指针域构成,对于元素“10”来说,它的数据域中则放入“10”所对应的数据,一个指针域指向的是“25”对应的结点,另一个则指向“21”对应的结点。这种结构,就称为散列结构。
5,散列描述哈希结构
通过散列结构描述哈希结构,首先是定义数据
struct dataType
{
int key;//被取余的数
char element[20];//数据
};
接下来是创建横向链表的结点
struct Node
{
struct dataType data;
struct Node* next;
};
struct Node* createNode(struct dataType data)//创建结点的过程
{
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
assert(newNode);
newNode->data = data;
newNode->next = NULL;
return newNode;
}
然后就是散列结点描述,在该结点中,包括一个用来处理哈希冲突的横向链表,该节点的数据和指向下一个哈希地址的指针,并与下一个哈希地址组成纵向链表,而这个纵向链表就是每个哈希地址的第一个数据,拿上面的数据举例,10,21,37,43,14中的数据组成的链表就是纵向链表。
//第一列结点
struct skipListNode* createSkipList(struct dataType data)//散列结点的创建
{
struct skipListNode* newNode = (struct skipListNode*)malloc(sizeof(struct skipListNode));
assert(newNode);
newNode->data = data;
newNode->firstNode = NULL;
newNode->next = NULL;
return newNode;
}
既然创建了散列结点了,就要开始开始对哈希结构体进行描述,并创建哈希结构
struct listHash
{
struct skipListNode* headNode;
int curSize;
int divisor;//divisor为取余数,作用是求hash地址
};
struct listHash* createHash(int divisor)
{
struct listHash* hash = (struct listHash*)malloc(sizeof(struct listHash));
assert(hash);//断言处理
hash->curSize = 0;
hash->divisor = divisor;
hash->headNode = NULL;//这里没有创建表头
return hash;
}
接下来,就要进行最难也就是最核心的步骤了,也就是插入数据。首先,要通过取余来得到哈希地址,如果不存在这个元素,并不存在哈希冲突,就不要考虑横向链表,就只用创建一个纵向的有序链表这时,我们就要从纵向链表中的第一个结点开始寻找,找到第一次大于该数据余数的结点,并在该结点前插入一个新的结点,如果这个纵向链表本来就不存在或该数据的哈希地址时最小的,就需要将这个新结点当作纵向链表的表头。而在寻找大于该数据的结点时,会有三种情况,一种是找到表尾也没找到,则直接将其插入表尾,一种是找到大于其的结点,则插入该结点前,最后一种就是找到一个与其哈希地址相同的结点,如果该节点的数据与要插入的数据相同,则采用覆盖的方式将其覆盖,如果不同,则在该结点插入横向链表,当然,在插入横向链表是也要判断在横向链表中判断有无冲突,若冲突,则也采用覆盖的方式,若不冲突,则采用表头法插入。代码如下
void insertData(struct listHash* hash, struct dataType data)
{
int dataHashPos = data.key % hash->divisor;
struct skipListNode* newSkipNode = createSkipList(data);
if (hash->headNode == NULL)//第一种情况,表头为空
{
hash->headNode = newSkipNode;
hash->curSize++;
}
else//如果表头不为空
{
//找相邻的两个结点
struct skipListNode* pmove = hash->headNode;//移动结点
struct skipListNode* premove = NULL;//移动的前驱结点
//1,考虑新结点的hash地址是最小,成为表头
if (pmove->data.key % hash->divisor > dataHashPos)//如果插入元素的hash地址小于表头的hash地址,则插入的结点为第一结点
{
newSkipNode->next = hash->headNode;
hash->headNode = newSkipNode;
hash->curSize++;
}
else
{
//纵向链表寻找相邻的两个结点(找到第一次大于插入元素hash地址位置)
while (pmove != NULL && (pmove->data.key % hash->divisor < dataHashPos))
{
premove = pmove;
pmove = pmove->next;
}
//分析结果
if (pmove != NULL && (pmove->data.key % hash->divisor) == dataHashPos)//如果等于,则存在冲突
{
if(pmove->data.key==data.key)//相等的key,则采用覆盖的方式
{
strcpy(pmove->data.element, data.element);
}
else//不相同的key,则采用横向插入
{
//横向插入
struct Node* newNode = createNode(data);
struct Node* ppmove = pmove->firstNode;
if (ppmove == NULL)
{
newNode->next = pmove->firstNode;
pmove->firstNode = newNode;
hash->curSize++;
}
else//横向链表是否存在相同键的结点
{
while (ppmove != NULL && ppmove->data.key != data.key)
{
ppmove = ppmove->next;
}
if (ppmove == NULL)//没有找到相同的结点,则采用表头法插入
{
newNode->next = pmove->firstNode;
pmove->firstNode = newNode;
hash->curSize++;
}
else//若key相同
{
strcpy(ppmove->data.element, data.element);
}
}
}
}
else
{
premove->next = newSkipNode;
newSkipNode->next = pmove;
hash->curSize++;
}
}
}
}
最后就是散列表的打印了
void printHash(struct listHash* hash)
{
struct skipListNode* pmove = hash->headNode;
while (pmove != NULL)
{
printf("%d:%s\t\t", pmove->data.key, pmove->data.element);
struct Node* ppmove = pmove->firstNode;
while (ppmove != NULL)
{
printf("%d:%s\t\t", ppmove->data.key, ppmove->data.element);
ppmove = ppmove->next;
}
pmove = pmove->next;
printf("\n");
}
}
把其带入主函数:
int main()
{
struct listHash* hash = createHash(10);//表示有10个元素,取余得到的最大地址为9
struct dataType array[7]= { 1,"A",11,"B",23,"张三",44,"老王",56,"OK",54,"666",73,"李四"};
for (int i = 0; i < 7; i++)
{
insertData(hash, array[i]);
}
printHash(hash);
return 0;
}