一、顺序循环队列
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
:释放队列所占用的内存资源
cs
#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. 哈希表的基本操作
这段代码实现了一个基于链地址法的哈希表,使用一个指针数组作为哈希表,每个数组元素指向一个链表的头节点,链表中存储哈希值相同的数据;例如多个名字首字母相同的人(如"王五""王二")会被存入同一个链表中,每插入一个人就动态申请一个节点,支持插入、查找、打印和销毁等基本操作,实现了按姓名快速分类和查找的功能。
cs
#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];
}
}
}
总结
-
顺序循环队列:掌握其基本结构、判空/判满逻辑及基本操作
-
哈希表:理解散列函数设计思路、冲突处理机制以及应用场景