当前位置: 首页 > news >正文

奶茶店里面的数据结构

今天在咖啡店里买咖啡,窗口有0-9号的位置用于放咖啡,店员会将945号放到5的位置,946号放到6的位置。
这个是个典型的mod10的哈希表,通过链地址法进行拓展,即945和955都放到5窗口。hashfunction 为index = key % table_size

哈希表的基本原理:

哈希表是通过“键值对”存储数据的。它使用一个哈希函数将键映射到一个数组中的索引位置。
哈希表的查找、插入和删除操作的平均时间复杂度是 O(1)。
哈希表的结构:

哈希表通过一个数组存储数据,其中每个数组位置称为“桶”(bucket)。
如果多个键通过哈希函数映射到同一个桶,则需要用某种方式存储这些冲突的数据(例如链表或开放地址法)。
哈希函数:

哈希函数将键映射为一个整数索引,比如 index = key % table_size。
一个好的哈希函数应该尽量将数据均匀分布到哈希表中,减少冲突。
时间复杂度:

查找、插入和删除操作的平均时间复杂度是 O(1),但最坏情况下(例如所有数据都映射到同一个桶),时间复杂度会退化为 O(n)。

哈希冲突是指不同的键经过哈希函数计算后得到相同的索引位置。解决冲突的常见方法有以下几种:

链地址法(Chaining):

每个桶存储一个链表或其他动态结构。
当发生冲突时,将新值插入链表中。
缺点:当链表过长时,查询速度会下降为 O(n)。
开放地址法(Open Addressing):

所有元素存储在哈希表数组中,无需额外的链表。
如果发生冲突,就按照某种探测规则(线性探测、二次探测或双重散列)寻找下一个空闲位置。
缺点:当哈希表装载因子(负载因子)过高时,性能会显著下降。
再哈希法(Rehashing):

当哈希冲突过多或装载因子超过一定阈值时,重新扩展哈希表大小,并重新计算所有元素的哈希值。
例子:假设我们有哈希表大小为 10,哈希函数是 key % 10:

插入键值对 15 和 25 时:15 % 10 = 5 和 25 % 10 = 5,它们都会映射到索引 5 位置,此时发生冲突。
使用链地址法时,索引 5 存储链表 [15, 25];使用开放地址法时,25 会存储到下一个空闲位置(如索引 6)。

这个数据结构用cpp表示就是

#include <iostream>
#include <vector>
#include <list>class MilkTeaHashTable {
private:int tableSize;int numElements;std::vector<std::list<int>> table;int hashFunction(int key) {return key % tableSize;// return (key * 7 + 3) % tableSize; // 更复杂的哈希函数}void rehash() {int oldSize = tableSize;tableSize *= 2; // 扩容std::vector<std::list<int>> newTable(tableSize);for (int i = 0; i < oldSize; ++i) {for (int key : table[i]) {int newIndex = hashFunction(key);newTable[newIndex].push_back(key);}}table = std::move(newTable);}public:MilkTeaHashTable(int size) : tableSize(size), numElements(0), table(size) {}void insert(int key) {int index = hashFunction(key);table[index].push_back(key);numElements++;}void print() {for (int i = 0; i < tableSize; ++i) {std::cout << "Bucket " << i << ": ";for (int key : table[i]) {std::cout << key << " ";}std::cout << std::endl;}}
};int main() {MilkTeaHashTable mt(10);mt.insert(965);mt.insert(975);mt.insert(1055);mt.insert(1155);mt.print();return 0;
}

改进后的代码大概如下:


#include <iostream>
#include <vector>
#include <list>class MilkTeaHashTable {
private:int tableSize; // 桶的数量int numElements; // 当前订单数量std::vector<std::list<int>> table; // 哈希表,每个桶是一个链表// 更复杂的哈希函数int hashFunction(int key) {return ((key * 31 + 7) ^ (key >> 3)) % tableSize;}// 动态扩容并重新哈希void rehash() {int oldSize = tableSize;tableSize *= 2; // 扩容为原来的两倍std::vector<std::list<int>> newTable(tableSize);for (int i = 0; i < oldSize; ++i) {for (int key : table[i]) {int newIndex = hashFunction(key);newTable[newIndex].push_back(key);}}table = std::move(newTable); // 更新哈希表}public:MilkTeaHashTable(int size) : tableSize(size), numElements(0), table(size) {}// 插入订单号void insert(int key) {if ((double)numElements / tableSize > 0.7) { // 如果装载因子超过 0.7,扩容rehash();}int index = hashFunction(key);table[index].push_back(key);numElements++;}// 查找订单号是否存在bool search(int key) {int index = hashFunction(key);for (int order : table[index]) {if (order == key) {return true;}}return false;}// 删除订单号void remove(int key) {int index = hashFunction(key);table[index].remove(key);}// 打印哈希表void print() {for (int i = 0; i < tableSize; ++i) {std::cout << "Bucket " << i << ": ";for (int key : table[i]) {std::cout << key << " ";}std::cout << std::endl;}}
};int main() {MilkTeaHashTable mt(10);// 插入订单号mt.insert(965);mt.insert(975);mt.insert(1055);mt.insert(1155);// 打印哈希表mt.print();// 查找订单是否存在std::cout << "Search 975: " << (mt.search(975) ? "Found" : "Not Found") << std::endl;// 删除订单号mt.remove(975);std::cout << "After removing 975:" << std::endl;mt.print();return 0;
}

哈希函数不局限于简单的取模运算,它可以是任意映射函数,只要满足以下特性:

确定性(Deterministic):

同一个输入值总是映射到相同的输出值。
均匀性(Uniformity):

哈希函数应尽量将输入值均匀分布到所有桶中,减少冲突。
高效率(Efficiency):

哈希函数的计算应快速,尤其是在处理大量数据时。
例如:

简单取模:index = key % table_size
数字哈希:index = (key * 31 + 7) % table_size
字符串哈希:将字符串转为数字后取模或使用更复杂的算法(如 FNV、MurmurHash 等)。
在奶茶店的例子中,可以设计一个更复杂的哈希函数,比如对订单号进行某种加权运算,避免简单的取模导致冲突过于集中。
增大哈希表大小:使用更大的数组,减少冲突概率。
更好的哈希函数:使用更复杂的哈希函数,避免简单的数字取模导致冲突。
动态扩容:当装载因子(装载因子 = 元素个数 / 哈希表大小)超过一定值时,动态扩展哈希表。
原理设计:
动态扩容:

如果某些桶(取餐号)过于拥挤(链表过长),则可以动态增加桶的数量,重新分配订单号。
比如将原来的 10 个桶扩充为 20 个桶,重新计算每个订单号的桶位置。
改进哈希函数:

使用更复杂的哈希函数,比如对订单号进行加权或混合运算:
index = ((key * 31 + 7) ^ (key >> 3)) % table_size;
这样可以减少冲突概率,使订单号更加分散。
限制链表长度:

如果某个桶的链表长度超过一定阈值(例如 5 个订单),可以将链表改为更高效的数据结构(如平衡树)。
实时清理过期订单:

定期清理已经取走的订单,避免链表长度不断增加,影响查询效率。

相关文章:

  • ProxySQL实现mysql8主从同步读写分离
  • Vue3祖先后代组件数据双向同步实现方法
  • TypeScript-知识点梳理
  • 阿里云 AI 搜索开放平台:RAG智能化工作流助力 AI 搜索
  • 【数据结构和算法】6. 哈希表
  • Hive中Map和Reduce阶段的分工
  • C++笔记-stack_queue(含deque,priority_queue,仿函数的讲解)
  • NHANES指标推荐:CTI
  • NOIP2012提高组.同余方程
  • Java基础复习(JavaSE进阶)第九章 网络编程
  • 考研单词笔记 2025.04.23
  • 脂质体挤出器有哪些知名品牌?
  • 2025深圳中兴通讯安卓开发社招面经
  • 【金仓数据库征文】从Oracle到KingbaseES的语法兼容与迁移
  • Spring Boot 项目:如何在 JAR 运行时读取外部配置文件
  • 【每日八股】复习计算机网络 Day4:TCP 协议的其他相关问题
  • 【Java学习笔记】random的使用
  • 并行RANSAC平面拟合(C++)
  • [特殊字符]‍[特殊字符]Linux驱动开发入门 | 并发与互斥机制详解
  • ActiveMQ 核心概念与消息模型详解(二)
  • 建投读书会·东西汇流|东西方戏剧在上海的相逢、交锋与融合
  • 大卫·第艾维瑞谈历史学与社会理论③丨尼古拉斯·卢曼与历史研究
  • 停止水资源共享、驱逐武官,印度对巴基斯坦宣布多项反制措施
  • 特朗普激发加拿大爱国热情之下:大选提前投票人数创纪录,魁北克分离情绪被冲淡
  • 翁东华卸任文和友小龙虾公司董事,此前抢镜“甲亢哥”惹争议
  • 著名水声学家陆佶人逝世,曾参加我国第一代核潜艇主动声纳研制