数据结构(五):顺序循环队列与哈希表
一、顺序循环队列
1. 基本概念
顺序循环队列本质上是一个使用数组实现的队列结构,其头尾指针分别用于标记出队(删除)与入队(添加)的位置。为了实现循环效果,队列头尾位置通过取模操作循环利用数组空间。
我们通常定义一个结构体表示循环队列:
head
:队头下标tail
:队尾下标pbase
:指向连续内存空间的指针(模拟数组)其中
pbase[head]
表示队头元素,pbase[tail]
表示即将插入的位置
数据部分可以使用结构体封装,如:
struct Data {
char name[20];
int score;
};
循环队列就是一个装载结构体数据的“数组”。
2. 空队与满队判断方式
为了简化逻辑、避免队头和队尾重合带来的混淆,循环队列通常会浪费一个空间来区分“队满”和“队空”。因此,如果申请空间为 N
,最多只能存储 N - 1
个元素。
判空条件:
head == tail
判满条件:
(tail + 1) % len == head
其中 len
是申请的总空间大小,%
表示取模,确保不会越界。当尾指针追上头指针,意味着队列已满。
小技巧:申请长度为 10 的数组,最大只能存储 9 个元素。
3. 顺序循环队列的基本操作
这段代码实现了一个顺序循环队列的基本操作流程。
一开始通过 creatSequeue()
函数动态申请了一个队列结构体,它里面包含了:
head
:指向队头的位置(用于出队)tail
:指向队尾的位置(用于入队)pbase
:指向一块连续内存空间,用来存储具体的数据,相当于一个数组名
这个队列采用循环的方式进行操作,为了区分队满和队空,默认会浪费一个存储空间。
之后实现了几个常用功能函数:
isFullSequeue
:判断队列是否满isEmptySequeue
:判断队列是否空pushSequeue
:入队(尾部插入)popSequeue
:出队(从头部取出)getSequeueHead
:查看队头元素但不删除printSequeue
:遍历并打印所有元素destorySequeue
:释放队列所占用的内存资源
#include <stdio.h>
#include "seq_queue.h"
#include <stdlib.h>Sequeue *creatSequeue()
{Sequeue *psq = malloc(sizeof(Sequeue));if(NULL == psq){fprintf(stderr, "malloc error\n");return NULL;}psq->head = 0;psq->tail = 0;psq->pbase = malloc(sizeof(DataType) * SEQQUE_MAX_LEN);if(NULL == psq->pbase){fprintf(stderr, "malloc errer");return NULL;}return psq;
}int isFullSequeue(Sequeue *psq)
{if((psq->tail + 1) % SEQQUE_MAX_LEN == psq->head)\{return 1;}return 0;
}int isEmptySequeue(Sequeue *psq)
{if(psq->head == psq->tail){return 1;}return 0;
}int pushSequeue(Sequeue *psq, DataType data)
{if(isFullSequeue(psq)){fprintf(stderr, "sequeue is full\n");return -1;}psq->pbase[psq->tail] = data;psq->tail = (psq->tail + 1) % SEQQUE_MAX_LEN;return 0;
}int popSequeue(Sequeue *psq, DataType *data)
{if(isEmptySequeue(psq)){return -1;}*data = psq->pbase[psq->head];psq->head = (psq->head + 1) % SEQQUE_MAX_LEN;return 0;
}int getSequeueHead(Sequeue *psq, DataType *data)
{if(isEmptySequeue(psq) || NULL == data){return -1;}*data = psq->pbase[psq->head];return 0;
}void destorySequeue(Sequeue **psq)
{free((*psq)->pbase);free((*psq));*psq = NULL;return ;
}void printSequeue(Sequeue *psq)
{int i;for(i = psq->head; i != psq->tail; i = (i + 1) % SEQQUE_MAX_LEN){printf("%d \n", psq->pbase[i]);}puts("");
}
二、哈希表(Hash Table)
1. 基本定义
哈希表(又称散列表)是一种以查找效率高为特点的数据结构。它通过一个称为“哈希函数”的映射,将关键字(key)转换为一个地址,从而将数据直接存储在对应位置。
这个映射函数 f(key)
就是 哈希函数。
散列技术既是一种存储方式,也是一种查找策略。
2. 哈希表查找过程
插入时:通过
f(key)
计算地址并将数据存入对应位置查找时:再次用
f(key)
找到目标位置,直接访问数据
如果函数设计得好,理想情况下 时间复杂度为 O(1)。
3. 设计哈希函数时考虑的因素
为保证性能,应根据实际情况选择合适的哈希函数,以下是几个考虑维度:
关键字的长度是否过长
关键字在数据集中的分布是否均匀
哈希表的大小(容量)
查找频率是否集中于某些 key
哈希函数的计算复杂度是否合理
4. 构造哈希函数的常用方法
除留余数法(最常用)
形式为:f(key) = key % p // 其中 p <= 哈希表长度
适用于数值型关键字,常搭配质数 p 来均匀分布。
平方取中法、折叠法
先进行一些处理(如平方、分段求和),再取模。
5. 哈希冲突与解决方案
由于多个关键字可能映射到同一个地址(哈希冲突),我们需要有策略来处理这种冲突。
常见方法:
链地址法(拉链法):
每个槽位存储一个链表,所有哈希值相同的元素都放在该链表中。开放地址法:
冲突发生时,尝试寻找下一个可用槽位。具体策略包括:线性探测(+1 往后找)
二次探测
再哈希等
冲突处理方法的选型会影响哈希表的插入与查询效率。
6. 哈希表的基本操作
这段代码实现了一个基于链地址法的哈希表,使用一个指针数组作为哈希表,每个数组元素指向一个链表的头节点,链表中存储哈希值相同的数据;例如多个名字首字母相同的人(如“王五”“王二”)会被存入同一个链表中,每插入一个人就动态申请一个节点,支持插入、查找、打印和销毁等基本操作,实现了按姓名快速分类和查找的功能。
#include <endian.h>
#include <stdio.h>
#include <stdlib.h>
#include "hash.h"
#include <string.h>int hashFunction(char key)
{if(key >= 'a' && key <= 'z'){return key - 'a';}else if(key >= 'A' && key <= 'Z'){return key - 'A';}else{return HASH_TABLE_MAX_SIZE - 1;}
}int insertHashTable(HSNode **hashtable, DataType data)
{HSNode *node = malloc(sizeof(HSNode));if(NULL == node){fprintf(stderr, "malloc error\n");return -1;}node->data = data;node->pnext = NULL;int addr = hashFunction(data.name[0]);if(*(hashtable + addr) == NULL){*(hashtable + addr) = node;}else{if(strcmp(hashtable[addr]->data.name, node->data.name) >= 0){node->pnext = hashtable[addr];hashtable[addr] = node;}else{HSNode *p = hashtable[addr];while(p->pnext != NULL && strcmp(node->data.name, p->pnext->data.name) > 0){p = p->pnext;}node->pnext = p->pnext;p->pnext = node;}}return 0;
}void printhash(HSNode **hashtable)
{HSNode *temp = NULL;for(int i = 0;i < HASH_TABLE_MAX_SIZE;++i){temp = *(hashtable + i);while(temp){printf("%s : %s\n", temp->data.name, temp->data.phone);temp = temp->pnext;}printf("\n");} return ;
}HSNode *findHashNodeByName(HSNode **hashtable, char *s)
{HSNode *temp = hashtable[0];for(int i = 0;i < HASH_TABLE_MAX_SIZE;++i){temp = hashtable[i];while(temp){if(0 == strncmp(temp->data.name, s, strlen(s))){return temp;}temp = temp->pnext;}}return NULL;
}void destroyHash(HSNode **hashtable)
{for(int i = 0;i < HASH_TABLE_MAX_SIZE;++i){HSNode *temp = *(hashtable + i);while(hashtable[i]){hashtable[i] = temp->pnext;free(temp);temp = hashtable[i];}}
}
总结
顺序循环队列:掌握其基本结构、判空/判满逻辑及基本操作
哈希表:理解散列函数设计思路、冲突处理机制以及应用场景