C++面试:散列表

       

目录

1. 散列表的基本概念

散列表的定义

散列函数

哈希冲突

2. 处理冲突的方法

链地址法(Separate Chaining)

开放地址法

再散列

3. 散列表的性能分析

1. 平均查找长度(ASL)

2. 负载因子(Load Factor)

代码示例:计算负载因子和模拟查找

4. C++ 中的散列表实现

1. std::unordered_map

2. std::unordered_set

5. 散列表的应用场景

1. 快速数据访问

2. 数据去重

3. 实现映射和集合

6. 面试常见问题

1. 如何选择合适的散列函数?

2. 散列冲突的处理方式及其优缺点?

3. 如何根据场景选择合适的散列表实现(例如 std::unordered_map vs std::map)?

        散列表(Hash Table),也常称为哈希表,在 C++ 中是一种重要的数据结构,常用于快速的数据存储和查找。面试时,对散列表的理解和应用是衡量候选人技术能力的一个重要方面。

1. 散列表的基本概念

  • 散列表的定义

    散列表(哈希表)是一种使用散列函数组织数据,以支持快速插入和搜索的数据结构。它的核心思想是利用一个函数(称为散列函数或哈希函数),将输入(通常是一个键)映射到一个位置(即索引),然后在该位置存储值。这种方法提供了快速的数据查找速度,因为直接通过键计算出的索引访问数据,而不需要像在列表或树结构中那样逐个搜索。

  • 散列函数

    散列函数是散列表中的核心组成部分,其作用是将输入的键转换成散列表中的索引。一个好的散列函数应该满足以下条件:

  • 一致性:相同的输入总是产生相同的输出(索引)。
  • 高效计算:散列函数应该容易计算,不应该有复杂的运算。
  • 均匀分布:散列函数应该将键均匀分布在散列表中,以减少冲突的可能性。

  • 哈希冲突

    哈希冲突发生在两个或多个键被映射到散列表的同一位置时。这种情况是不可避免的,因为散列表的大小是有限的,而可能的键的数目通常是无限的。处理哈希冲突的方法很重要,因为它们会影响散列表的性能。

2. 处理冲突的方法

链地址法(Separate Chaining)

链地址法是处理散列冲突的一种常用方法。它的基本思想是:

  • 链表存储:每个散列表槽位(bucket)都关联一个链表。当多个键散列到同一个槽位时,它们的值将存储在同一个链表中。
  • 插入操作:当插入一个新的键值对时,首先通过散列函数确定其槽位,然后将键值对添加到对应槽位的链表中。
  • 查找操作:要查找一个键,先计算它的散列值定位到相应槽位,然后在链表中遍历查找。
  • 删除操作:类似于查找,先定位到槽位,然后在链表中遍历以找到并删除元素。

        在链地址法中,每个散列表的槽位关联一个链表。当不同的键映射到同一个槽位时,它们的值会被存储在同一个链表中。

#include <list>
#include <vector>
#include <iostream>

template<typename K, typename V>
class HashTable {
private:
    static const int HASH_GROUPS = 10;
    std::list<std::pair<K, V>> table[HASH_GROUPS];

public:
    bool isEmpty() const;
    int hashFunction(K key);
    void insertItem(K key, V value);
    void removeItem(K key);
    V searchByKey(K key);
    void printTable();
};

// 示例方法实现(如 hashFunction, insertItem, etc.)

开放地址法

开放地址法是另一种常用的冲突解决方法,它在散列表本身寻找空槽位:

  • 线性探测:当发生冲突时,顺序检查表中的下一个槽位,直到找到空槽位。
  • 二次探测:与线性探测类似,但搜索的间隔是二次方数列(1, 4, 9, 16, ...)。
  • 双重散列:使用一系列的散列函数。如果第一个散列函数导致冲突,就尝试第二个散列函数,依此类推。

        开放地址法的优点是不需要额外的存储空间来处理冲突,但是当散列表接近满时,性能会下降。

        在开放地址法中,所有的元素都直接存储在散列表数组中。当发生冲突时,算法会寻找另一个空槽位。

#include <iostream>
#include <vector>

template<typename K, typename V>
class HashTable {
private:
    static const int HASH_TABLE_SIZE = 10;
    std::pair<K, V> table[HASH_TABLE_SIZE];
    K EMPTY_KEY;

public:
    HashTable(K emptyKey);
    int hashFunction(K key);
    void insertItem(K key, V value);
    void removeItem(K key);
    V searchByKey(K key);
};

// 示例方法实现(如 hashFunction, insertItem, etc.)

再散列

再散列是通过使用第二个(或多个)散列函数来解决冲突的方法。它的基本思想是:

  • 当插入操作导致冲突时,使用第二个散列函数计算新的槽位。
  • 如果第二个散列函数还是导致冲突,可以尝试第三个散列函数,依此类推。
  • 这种方法通常与其他方法(如链地址法或开放地址法)结合使用。

        再散列的优点在于它可以在不同的散列函数之间分散冲突,减少了单个散列函数可能导致的冲突集中。然而,选择合适的散列函数组合对于性能至关重要。

#include <iostream>
#include <vector>

template<typename K, typename V>
class HashTable {
private:
    static const int HASH_TABLE_SIZE = 10;
    std::pair<K, V> table[HASH_TABLE_SIZE];
    K EMPTY_KEY;

    int hashFunction1(K key);
    int hashFunction2(K key);

public:
    HashTable(K emptyKey);
    void insertItem(K key, V value);
    void removeItem(K key);
    V searchByKey(K key);
};

3. 散列表的性能分析

        散列表的性能分析主要集中在两个方面:平均查找长度(Average Search Length, ASL)和负载因子(Load Factor)。理解这两个概念对于设计高效的散列表至关重要。

1. 平均查找长度(ASL)

平均查找长度是衡量散列表效率的重要指标,它分为两部分:

  • 成功平均查找长度(ASL成功):在表中找到元素所需的平均比较次数。
  • 不成功平均查找长度(ASL不成功):在表中未找到元素所需的平均比较次数。

ASL取决于多种因素,包括散列函数的质量、处理冲突的方法和散列表的负载因子。

2. 负载因子(Load Factor)

负载因子是散列表已填充的程度的量度,定义为表中已有元素数量与散列表总空间的比例。其公式为:

  • 负载因子越高,意味着散列表中的冲突可能性越大,这会增加查找时间。
  • 一般而言,当负载因子增长到一定阈值(如0.7或0.75)时,会执行散列表的扩容操作(rehashing),以保持操作的效率。

代码示例:计算负载因子和模拟查找

        下面是一个简单的 C++ 示例,展示了如何在散列表实现中计算负载因子,并模拟查找过程以估计平均查找长度。

#include <iostream>
#include <vector>
#include <list>

template<typename K, typename V>
class HashTable {
private:
    static const int HASH_TABLE_SIZE = 10;
    std::list<std::pair<K, V>> table[HASH_TABLE_SIZE];
    int numElements = 0; // 跟踪插入的元素数量

    int hashFunction(K key) {
        return key % HASH_TABLE_SIZE;
    }

public:
    void insertItem(K key, V value) {
        int index = hashFunction(key);
        table[index].push_back(std::make_pair(key, value));
        numElements++;
    }

    double loadFactor() const {
        return (double)numElements / HASH_TABLE_SIZE;
    }

    int searchByKey(K key) {
        int index = hashFunction(key);
        int searchCount = 0;

        for (auto it = table[index].begin(); it != table[index].end(); it++) {
            searchCount++;
            if (it->first == key) {
                return searchCount; // 返回查找次数
            }
        }
        return searchCount; // 如果未找到,返回查找次数
    }
};

int main() {
    HashTable<int, std::string> hashTable;

    // 插入元素
    hashTable.insertItem(1, "value1");
    hashTable.insertItem(2, "value2");
    hashTable.insertItem(3, "value3");

    // 计算负载因子
    std::cout << "Load Factor: " << hashTable.loadFactor() << std::endl;

    // 模拟查找
    int searchCount = hashTable.searchByKey(2);
    std::cout << "Search Count: " << searchCount << std::endl;

    return 0;
}

        这个示例中,HashTable 类实现了基本的插入和查找操作,并计算了负载因子。searchByKey 方法返回查找一个元素所需的步骤数,从而可以用来估计 ASL。在实际应用中,ASL 的计算会更加复杂,通常涉及对大量操作的统计分析。

        在面试中,展示你如何通过代码实现和分析这些性能指标,可以证明你对散列表及其性能优化有深入的理解。这对于腾讯等公司的 C++ 后台开发职位是非常重要的。

4. C++ 中的散列表实现

        在 C++ 中,标准库提供了两种基于散列表的容器:std::unordered_mapstd::unordered_set。这些容器提供了高效的插入、查找和删除操作。

1. std::unordered_map

std::unordered_map 是一种关联容器,存储键值对,其中键是唯一的。它提供快速的查找、插入和删除操作,平均时间复杂度为 O(1)。

特点

  • 基于散列表实现。
  • 键值对的存储不按特定顺序排列。
  • 提供单个元素的插入、访问和删除操作。
  • 允许自定义散列函数和相等判断凭据。
#include <iostream>
#include <unordered_map>

int main() {
    std::unordered_map<int, std::string> map;

    // 插入元素
    map[1] = "apple";
    map[2] = "banana";
    map[3] = "cherry";

    // 访问元素
    std::cout << "map[2] = " << map[2] << std::endl;

    // 查找元素
    if (map.find(3) != map.end()) {
        std::cout << "Found cherry!" << std::endl;
    }

    // 删除元素
    map.erase(2);

    return 0;
}

2. std::unordered_set

std::unordered_set 是一个集合容器,它存储唯一的元素,不允许重复。它的内部实现也是基于散列表。

特点

  • 每个元素的值即是其键。
  • 不允许重复的元素。
  • 提供快速的查找、插入和删除操作,平均时间复杂度为 O(1)。
  • 元素的存储顺序是不确定的。
#include <iostream>
#include <unordered_set>

int main() {
    std::unordered_set<int> set;

    // 插入元素
    set.insert(1);
    set.insert(2);
    set.insert(3);

    // 查找元素
    if (set.find(2) != set.end()) {
        std::cout << "Found 2" << std::endl;
    }

    // 删除元素
    set.erase(2);

    // 遍历集合
    for (int number : set) {
        std::cout << number << std::endl;
    }

    return 0;
}

        在这两个例子中,std::unordered_mapstd::unordered_set 提供了高效的散列功能,非常适合需要快速查找和存储唯一元素的场景。在面试中,你可能会被要求展示对这些容器的理解和应用,特别是在谈到性能优化和数据结构选择时。了解它们的内部工作原理和适用场景可以帮助你在腾讯等公司的 C++ 后台开发面试中脱颖而出。

5. 散列表的应用场景

1. 快速数据访问

        散列表因其平均时间复杂度为 O(1) 的查找性能而被广泛用于需要快速数据访问的场景。

  • 缓存系统:散列表是构建缓存系统(如 Memcached、Redis)的理想数据结构。在这些系统中,可以通过键快速检索存储的数据项,从而提高数据访问速度,并减少对主存储(如硬盘)的访问。
  • 数据库索引:数据库中的索引通常使用散列表来实现,以加快数据的检索速度。通过将键(如数据库行的主键)映射到数据的位置,散列表使得数据库能够快速找到存储的记录。

        这个示例演示了如何使用 std::unordered_map 实现一个基础的内存缓存系统。

#include <iostream>
#include <unordered_map>
#include <string>

class MemoryCache {
private:
    std::unordered_map<std::string, std::string> cache;

public:
    void set(const std::string& key, const std::string& value) {
        cache[key] = value;
    }

    std::string get(const std::string& key) {
        auto it = cache.find(key);
        if (it != cache.end()) {
            return it->second;
        }
        return "Not found";
    }
};

int main() {
    MemoryCache myCache;

    // 设置缓存值
    myCache.set("key1", "value1");
    myCache.set("key2", "value2");

    // 获取缓存值
    std::cout << "key1: " << myCache.get("key1") << std::endl;
    std::cout << "key3: " << myCache.get("key3") << std::endl;  // 未设置的键

    return 0;
}

2. 数据去重

        在需要快速检查和删除重复元素的场合,散列表提供了一个高效的解决方案。

  • 去重操作:散列表可以用来快速检测和处理重复的数据。例如,在处理大量数据时,可以使用散列表来检查某个元素是否已经存在。如果散列表中已存在该元素,则可以确定该元素是重复的,从而进行相应的去重处理。
#include <iostream>
#include <unordered_set>
#include <vector>

int main() {
    std::vector<int> numbers = {1, 2, 2, 3, 4, 4, 4, 5};
    std::unordered_set<int> uniqueNumbers;

    for (int number : numbers) {
        uniqueNumbers.insert(number);
    }

    std::cout << "Unique numbers: ";
    for (int number : uniqueNumbers) {
        std::cout << number << " ";
    }
    std::cout << std::endl;

    return 0;
}

3. 实现映射和集合

散列表也用于实现映射和集合这类抽象数据类型。

  • 映射(字典):在很多编程语言中,映射或字典结构是用散列表实现的。映射存储键值对,每个键映射到一个值。例如,C++ 的 std::unordered_map 就是使用散列表实现的。
  • 集合:集合是一种只存储唯一元素的数据结构。散列表的唯一性特性使其成为实现集合的理想选择。C++ 的 std::unordered_set 是一个基于散列表实现的集合。
#include <iostream>
#include <unordered_map>
#include <unordered_set>
#include <string>

int main() {
    // 使用 unordered_map 实现字典
    std::unordered_map<std::string, int> wordCount;
    wordCount["apple"] = 1;
    wordCount["banana"] = 2;

    for (const auto& pair : wordCount) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }

    // 使用 unordered_set 实现集合
    std::unordered_set<std::string> fruitSet;
    fruitSet.insert("apple");
    fruitSet.insert("banana");
    fruitSet.insert("cherry");

    std::cout << "Fruits in set: ";
    for (const std::string& fruit : fruitSet) {
        std::cout << fruit << " ";
    }
    std::cout << std::endl;

    return 0;
}

6. 面试常见问题

1. 如何选择合适的散列函数?

选择合适的散列函数是关键,因为它直接影响到散列表的性能。合适的散列函数应该满足以下条件:

  • 均匀性:散列函数应该将键均匀地分布在散列表中,以减少冲突的概率。
  • 计算效率:散列函数的计算应该快速,避免复杂的运算,因为它会在每次插入和查找操作中被调用。
  • 适应数据特性:散列函数应根据数据的特性来选择。例如,对于字符串数据,可以使用多项式滚动哈希;对于整数,可能使用模运算或位运算等。

2. 散列冲突的处理方式及其优缺点?

散列冲突的处理方式主要有两种:链地址法和开放地址法。

  • 链地址法(Separate Chaining):
    • 优点:简单,容易实现;链表可以无限增长,不受散列表大小限制。
    • 缺点:需要额外的内存空间存储指针;缓存性能不如开放地址法好。
  • 开放地址法(Open Addressing,如线性探测、二次探测):
    • 优点:所有数据都存储在数组中,可以更好地利用缓存;数据存储紧凑,节省空间。
    • 缺点:当散列表接近满时,插入和查找的性能会下降;删除操作较复杂。

3. 如何根据场景选择合适的散列表实现(例如 std::unordered_map vs std::map)?

选择合适的散列表实现取决于具体的应用场景:

  • std::unordered_map
    • 适用场景:当需要快速的查找、插入和删除操作,且不关心元素的顺序时。
    • 特点:基于哈希表实现,提供平均常数时间的性能;元素无序。
  • std::map
    • 适用场景:当需要有序的元素集合,或者经常进行范围查询时。
    • 特点:通常基于红黑树实现,元素按键排序;查找、插入和删除操作的时间复杂度为 O(log n)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/351460.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

CAD-autolisp(二)——选择集、命令行设置对话框、符号表

目录 一、选择集1.1 选择集的创建1.2 选择集的编辑1.3 操作选择集 二、命令行设置对话框2.1 设置图层2.2 加载线型2.3 设置字体样式2.4 设置标注样式&#xff08;了解即可&#xff09; 三、符号表3.1 简介3.2 符号表查找3.2 符号表删改增 一、选择集 定义&#xff1a;批量选择…

go语言(十八)---- goroutine

一、goroutine package mainimport ("fmt""time" )func main() {//用go创建承载一个形参为空&#xff0c;返回值为空的一个函数go func() {defer fmt.Println("A.defer")func() {defer fmt.Println("B.defer")//退出当前goroutinefmt…

《WebKit 技术内幕》学习之十五(6):Web前端的未来

6 Chromium OS和Chrome的Web应用 6.1 基本原理 HTML5技术已经不仅仅用来编写网页了&#xff0c;也可以用来实现Web应用。传统的操作系统支持本地应用&#xff0c;那么是否可以有专门的操作系统来支持Web应用呢&#xff1f;当然&#xff0c;现在已经有众多基于Web的操作系统&…

跟无神学AI之Prompt

在大模型时代会写prompt变得很重要。 Prompt翻译为中文为提示词&#xff0c;在大模型的特定领域指的是大模型使用者给大模型提交的一种有一定格式的交互命令&#xff0c;让我们看看科大讯飞的大模型给出的答案—— Prompt是一种向人工智能模型提供的输入文本或指令&#xff0…

uv胶UV大灯修复液修复汽车车灯大灯尾灯?

使用UV胶进行汽车大灯修复是一种常见的方法&#xff0c;特别是用于修复裂纹、划痕或氧化的透明塑料表面。以下是使用UV胶修复汽车大灯的一般步骤&#xff1a; 1.准备工作&#xff1a; 确保汽车大灯表面是干净的&#xff0c;没有灰尘、油脂或其他污垢。可以使用清洁剂和软布进行…

Microsoft Remote Desktop for Mac(远程桌面连接)激活版

Microsoft Remote Desktop是一款由微软开发的远程桌面连接工具&#xff0c;它允许用户从另一台计算机或移动设备远程连接到Windows桌面或服务器。 以下是该软件的一些主要特点和功能&#xff1a; 跨平台支持&#xff1a;Microsoft Remote Desktop支持Windows、macOS、iOS和Andr…

leetcode:二叉树的中序遍历(外加先序,后序遍历)

题外&#xff1a;另外三种遍历可以看这&#xff1a; 层序遍历&#xff1a; Leetcode:二分搜索树层次遍历-CSDN博客 先序遍历&#xff1a; 二叉树的先序&#xff0c;中序&#xff0c;后序遍历-CSDN博客 后序遍历&#xff1a; 二叉树的先序&#xff0c;中序&#xff0c;后序…

网络安全全栈培训笔记(58-服务攻防-应用协议设备KibanaZabbix远控向日葵VNCTV)

第58天 服务攻防-应用协议&设备Kibana&Zabbix&远控向日葵&VNC&TV 知识点&#xff1a; 1、远程控制第三方应用安全 2、三方应用-向日葵&VNC&TV 3、设备平台-Zabbix&Kibanai漏洞 章节内容&#xff1a; 常见版务应用的安全测试&#xff1a; 1…

【Web】CTFSHOW SQL注入刷题记录(上)

目录 无过滤注入 web171 web172 web173 web174 web175 时间盲注 写马 过滤注入 web176 web177 web178 web179 web180 web181-182 web183 web184 web185-186 web187 web188 web189 web190 布尔盲注 web191 web192 web193 web194 堆叠注入 web195 …

[C++]使用纯opencv部署yolov8旋转框目标检测

【官方框架地址】 https://github.com/ultralytics/ultralytics 【算法介绍】 YOLOv8是一种先进的对象检测算法&#xff0c;它通过单个神经网络实现了快速的物体检测。其中&#xff0c;旋转框检测是YOLOv8的一项重要特性&#xff0c;它可以有效地检测出不同方向和角度的物体。…

格子表单GRID-FORM | 嵌套子表单与自定义脚本交互

格子表单/GRID-FORM已在Github 开源&#xff0c;如能帮到您麻烦给个星&#x1f91d; GRID-FORM 系列文章 基于 VUE3 可视化低代码表单设计器嵌套表单与自定义脚本交互 新版本功能 &#x1f389; 不觉间&#xff0c;GRID-FORM 已经开源一年&#xff08;2023年1月29日首次提交…

【JaveWeb教程】(28)SpringBootWeb案例之《智能学习辅助系统》的详细实现步骤与代码示例(1)

目录 SpringBootWeb案例011. 准备工作1.1 需求&环境搭建1.1.1 需求说明1.1.2 环境搭建 1.2 开发规范 2. 部门管理 SpringBootWeb案例01 前面我们已经讲解了Web前端开发的基础知识&#xff0c;也讲解了Web后端开发的基础(HTTP协议、请求响应)&#xff0c;并且也讲解了数据库…

QT+VS实现Kmeans聚类算法

1、Kmeans的定义 聚类是一个将数据集中在某些方面相似的数据成员进行分类组织的过程&#xff0c;聚类就是一种发现这种内在结构的技术&#xff0c;聚类技术经常被称为无监督学习。k均值聚类是最著名的划分聚类算法&#xff0c;由于简洁和效率使得他成为所有聚类算法中最广泛使…

Java 基础知识-IO流

大家好我是苏麟 , 今天聊聊IO流 . 资料来源黑马程序员 . IO概述 生活中&#xff0c;你肯定经历过这样的场景。当你编辑一个文本文件&#xff0c;忘记了ctrls &#xff0c;可能文件就白白编辑了。当你电脑上插入一个U盘&#xff0c;可以把一个视频&#xff0c;拷贝到你的电脑硬…

ajax点击搜索返回所需数据

html 中body设置&#xff08;css设置跟进自身需求&#xff09; <p idsearch_head>学生信息查询表</p> <div id"div_1"> <div class"search_div"> <div class"search_div_item"> …

day22 事件委托

目录 事件委托 事件委托 事件委托是利用事件流的特征解决一些开发需求的知识技巧 优点&#xff1a;减少注册次数&#xff0c;提高程序性能原理&#xff1a;事件委托其实是利用事件冒泡的特点 给父元素注册事件&#xff0c;当触发子元素时&#xff0c;会冒泡到父元素上&#x…

automa插件使用的一些实战经验3

1 子流程的变量怎么传回父流程 主流程向子流程传参很容易 在子流程可以看到&#xff0c;父流程定义的表格&#xff0c;在子流程中是看不到的&#xff0c;那么子流程定义的变量如何传回父流程呢&#xff1f;另外在子流程再添加执行工作流&#xff0c;是无法选择父流程本身&…

C++:vector容器(memcpy浅拷贝问题、迭代器失效问题)

文章目录 一. vector 的介绍二. vector 的使用1. string 和 vector<char> 的区别2. 为什么 vector 没有 find() 接口 三. vector 的模拟实现1. vector 的基本框架2. memcpy 和 memmove 的浅拷贝问题3. vector 迭代器失效问题4. 模拟代码 一. vector 的介绍 vector 的文档…

二次元动漫卡通手机APP应用下载页HTML源码

HTML源码&#xff0c;记事本修改里面的内容即可&#xff0c;本地双击index.html即可运行 蓝奏云&#xff1a;https://wfr.lanzout.com/itZRg1mf3b9c

华为HCIP Datacom H12-831 卷18

判断题 1、对于同一个MAC地址,手工配置的MAC表项优先级高于动态的表项,某二层报文的源MAC地址已经绑定在了交换机的GEO/0/1接口,当交换机从GEO/0/2收到该报文时,会丢弃该报文 A 对 B 错 正确答案 A 解析:为了提高接口安全性,网络管理员可手工在MAC地址表中加入特定M…