哈希表哈希桶(C++实现)

哈希的概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素 时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即 O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:

  • 插入元素 根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
  • 搜索元素 对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置 取元素比较,若关键码相等,则搜索成功

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称 为哈希表(Hash Table)(或者称散列表)。

比如将数据{1,4,6,8,15}存储到哈希表中:
哈希函数设置为hashi = key % capacity(其中hashi就是元素存储的位置,key就是数据项,capacity为存储元素底层空间总的大小,比如vector),存储结构如下:

image.png
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。

哈希冲突

如果继续向上面哈希表中插入14,hashi(14) = 4。而4这个位置已经被占用,此时就会发生不同的值映射到同一个位置上。这种情况就叫做哈希冲突

哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
一个好的哈希函数设计应该遵循以下条件:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果哈希表允许有m个地址时,其值 域必须在0到m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

最常见的哈希函数有:
除留余数法(最常用)
对于哈希表长度为m的哈希函数公式为:
hashi(key) = key % p (p <= m)

解决哈希冲突的方法

处理哈希冲突有两种常用的方法:

  • 闭散列
  • 开散列

闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去
。寻找下一个位置的方式有:

  • 线性探测
  • 二次探测

线性探测

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

比如对下面数据项集合{12,67,56,16,25,37,22,29,15,47,48,34}。哈希表长为12。
哈希函数为hashi(key) = key % p (p <= m)。

计算前五个{12,67,56,16,25}时,都不会发生冲突。直接存入哈希表。如下:

image.png
当计算37时,hashi(37) = 1,此时就会与25发生冲突,此时就需要将37存放到25的下一个位置,即下标为2的位置处。

image.png
继续存放{22,29,15,47}都没有发生冲突,正常存放。

image.png
存放48时,hashi(48) = 0,与12所在的位置发生了冲突,此时下一个位置还是会发生冲突,一直线性探测到29的下一个,才有空位。将48存放到29的下一个。

image.png
继续存放34,hash(i) = 10。和22,47,12,37,15,16,29,48,67,56都会发生冲突。只能存放到56的下一个。

image.png

二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位 置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:
hashi = ( H 0 H_0 H0 + i 2 i^2 i2 )% m, 或者:hashi = ( H 0 H_0 H0 - i 2 i^2 i2 )% m。
其中:i = 1,2,3…,
H 0 H_0 H0是通过散列函数Hash(key)对元素的关键码 key 进行计算得到的位置,
m是表的大小。

image.png
比如上面哈希表存放最后一个34时, H 0 H_0 H0 = 10,i = 1,hashi = (10-1)%12 = 9。而下标9正好是空,只需一次二次探测即可存放34。

负载因子

以上就是闭散列寻址的两种方式。当然哈希表也是需要扩容的,如果继续对上面哈希表存放数据,哈希表已满,无法存放了。
关于哈希表扩容,引入了一个新的概念,负载因子。
哈希表的负载因子α = 表中的数据个数 / 哈希表长度。

比如下面哈希表的负载因子 = 6 / 12 = 0.5

image.png
负载因子是哈希表反馈装满程度的标志,由于表长是定值,负载因子和哈希表中的元素个数成正比,所以负载因子越大,表明哈希表中元素个数越多。产生哈希冲突的概率越大,反之,负载因子越小,哈希表中元素个数越少,发生哈希冲突的概率越低。
对于闭散列,哈希表的负载因子是非常重要的,一般严格控制在0.7~0.8以下(也就是说哈希表的负载因子超过0.8就要扩容,避免产生更多的哈希冲突)。

哈希表闭散列的实现

哈希表的存储结构

哈希表中每个位置除了存储数据元素之外,还要存储当前位置的状态(空,已存放,已删除)。因为采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索。因此线性探测采用标记的伪删除法(就是给每个存储位置添加一个存储状态)来删除一个元素。
哈希表存储位置的状态可以使用枚举常量来定义,如下:

enum Status
{
    EXIST,//已存储
    EMPTY,//空
    DELETE//已删除
};

整个哈希表闭散列的结构:

//存储位置状态
enum Status
{
    EXIST,
    EMPTY,
    DELETE
};
//存储位置结构
template<class K, class V>
struct HashData
{
    pair<K, V> _kv;//哈希表存储数据元素。
    Status _status = EMPTY;//存储位置状态默认为空
};
//哈希表
template<class K, class V>
class HashTable
{
    typedef HashData<K, V> HashTableNode;
public:
    //哈希表提供的三个方法(插入、删除、查找)
    bool Insert();
    bool Erase();
    HashTableNode* find();
private:
    vector<HashTableNode> _tables;//使用vector做为哈希表底层存储结构
    size_t n = 0;//哈希表元素个数(方便计算哈希表的负载因子)
};

插入

向哈希表中插入数据的步骤如下:

  1. 查看哈希表中是否存在该键值的键值对,若已存在则插入失败。
  2. 判断是否需要调整哈希表的大小,若哈希表的大小为0,或负载因子过大都需要对哈希表的大小进行调整。
  3. 将键值对插入哈希表。
  4. 哈希表中的有效元素个数加一。
bool Insert(const pair<K, V> kv)
{
    //不允许键值冗余
    if (find(kv.first) != nullptr)
    {
        return false;
    }
    //空哈希表初始化10个存储位置
    if (_tables.size() == 0 )//扩容
    {
        _tables.resize(10);
    }
    else if((double)_tables_size / (double)_tables.size() > 0.8)//负载因子大于0.8扩容
    {
        //创建新哈希表对象
        HashTable<K,V> newtables;
        newtables._tables.resize(_tables.size() * 2);
        //遍历旧表,重新映射到新表中
        for (auto& data : _tables)
        {
            if (data._status == EXIST)
            {
                newtables.Insert(data._kv);
            }
        }
        //交换这两个哈希表
        _tables.swap(newtables._tables);
    }

    //线性探测确认哈希地址
    size_t hashi = kv.first % _tables.size();
    size_t i = 1;
    size_t index = hashi;
    while (_tables[index]._status == EXIST)
    {
        index = hashi + i;
        ++i;
        index %= _tables.size();//从0继续寻找位置。
    }

    //确认位置进行存储
    _tables[index]._kv = kv;
    _tables[index]._status = EXIST;
    _tables_size++;
    return true;
}

注意细节:

  • 扩容时创建新哈希表对象,让新哈希表对象调用insert不构成函数递归,来完成扩容后重新建立映射关系。
  • index %= _tables.size();//从0继续寻找位置。

查找

HashTableNode* find(const K& key)
{
    //空表
    if (_tables.size() == 0)
    {
        return nullptr;
    }
    //线性探测
    size_t hashi = key % _tables.size();
    size_t start = hashi;
    size_t i = 1;
    while (_tables[start]._status != EMPTY)
    {
        if (_tables[start]._kv.first == key 
         && _tables[start]._status == EXIST)
        {
            return &_tables[start];
        }
        start = hashi + i;
        i++;
        start %= _tables.size();
        //已经找了一圈了
        if (start == hashi)
        {
            break;
        }
    }
    return nullptr;
}

删除

删除哈希表中的元素采用伪删除法,只需将被删除元素找到,将其状态修改为DELETE即可。

bool Erase(const K& key)
{
    HashTableNode* ret = find(key);
    if (ret == nullptr)
    {
        return false;
    }
    else
    {
        ret->_status = DELETE;
        --_tables_size;
    }
    return true;
}

开散列

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地 址的关键码归于同一子集合,每一个子集合称为一个哈希桶,各个哈希桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

哈希桶和vector< list >本质是一样的(本质就是指针数组)。逻辑图如下:

image.png

比如对下面数据项集合{1,33,3,5,55,7,9},存放到表长为10的哈希表中。同样哈希函数采用除留余数法,存放到哈希表中如下:

image.png
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
实际中哈希表的开散列比闭散列更实用。
开散列同样也要考虑扩容问题,开散列一般负载因子为1时在发生扩容。
这样的话,如果哈希表中所有的元素都发生冲突,都存放在一个哈希桶中,有负载因子的控制扩容,几乎不会出现这种极端情况。
如果真的出现极端场景,可以考虑将这个桶由单链表改为红黑树。
由此可见,哈希表的开散列效率是极高的。增删查改的平均时间复杂度都是O(1)级别的存在,非常的恐怖。

哈希表开散列的模拟实现

在哈希表的开散列中,每个位置存储的是单链表的结点,这里需要实现一个哈希表的结点类。结点类和单链表一样,除了存储数据元素之外,还需要存储一个结点的指针用来指向下一个结点。

template<class K, class V>
struct HashNode
{
	HashNode<K, V>* _next;
	pair<K, V> _kv;
	HashNode(const pair<K,V> kv)
		:_next(nullptr)
		,_kv(kv)
	{}
};

哈希桶框架

template<class K, class V>
class HashBucket
{
	typedef HashNode<K, V> Node;
public:
	//哈希桶提供的方法...
	~HashBucket();
	bool Insert(const pair<K, V> kv);
	Node* Find(const K& key);
	bool Erase(const K& key);
	size_t size();
private:
	vector<Node*> _tables;
	size_t _size = 0;
};

下面就一一实现哈希桶提供的方法

insert函数

  1. 检查所插入元素是否存在
  2. 检查容量以及负载因为为1时进行扩容
  3. 确认插入元素所在桶的位置申请结点进行头插
bool Insert(const pair<K,V> kv)
{
        //1.检查所插入元素是否存在
        if (Find(kv.first) != nullptr)
        {
                return false;
        }
        //2.检查容量以及负载因为为1时进行扩容
        if (_tables.size() == 0)
        {
                _tables.resize(10);
        }
        else if (_size == _tables.size())//扩容
        {
                vector<Node*> new_bucket(_tables.size() * 2, nullptr);
                for (size_t i = 0; i < _tables.size(); ++i)
                {
                        Node* cur = _tables[i];
                        while (cur != nullptr)
                        {
                                Node* next = cur->_next;
                                size_t hashi = cur->_kv.first % new_bucket.size();
                                cur->_next = new_bucket[hashi];
                                new_bucket[hashi] = cur;
                                cur = next;
                        }
                }
                _tables.swap(new_bucket);
        }
        //3.确认插入元素所在桶的位置申请结点进行头插
        size_t hashi = kv.first % _tables.size();
        Node* newnode = new Node(kv);
        newnode->_next = _tables[hashi];
        _tables[hashi] = newnode;
        ++_size;
        return true;
}

注意: 这里的扩容不像闭散列一样,创建新的哈希表对象调用insert完成插入,会造成同样的数据再次申请结点,还要释放结点。虽然可以解决问题,但是效率不好。采用上面的方式更优,直接与新表重新建立映射关系即可。

// 创建新的哈希表对象调用insert完成插入。会造成同样的数据再次申请和释放。
HashBucket<K, V> new_bucket;
new_bucket._tables.resize(_tables.size() * 2);
for (size_t i = 0; i < _tables.size(); ++i)
{
        Node* cur = _tables[i];
        while (cur != nullptr)
        {
                new_bucket.Insert(cur->_kv);
                cur = cur->_next;
        }
}
_tables.swap(new_bucket._tables);

这里哈希表中的结点需要申请,那么也需要写析构函数来完成资源的释放,否则就会造成内存泄漏。

~HashBucket()
{
        for (size_t i = 0; i < _tables.size(); ++i)
        {
                Node* cur = _tables[i];
                while (cur != nullptr)
                {
                        Node* next = cur->_next;
                        delete cur;
                        cur = next;
                }
                cur = nullptr;
        }
        cout << "~HashBucket" << endl;
}

Find

  1. 找到查找元素在桶的位置
  2. 遍历单链表进行查找
Node* Find(const K& key)
{
    if (_tables.size() == 0)
    {
            return nullptr;
    }
    //1. 找到查找元素所在桶的位置
    size_t hashi = key % _tables.size();//注意/0错误
    Node* cur = _tables[hashi];
    //2. 遍历单链表查找
    while (cur != nullptr)
    {
            if (cur->_kv.first == key)
            {
                    return cur;
            }
            cur = cur->_next;
    }
    return nullptr;
}

Erase

  1. 找到删除元素所在桶的位置
  2. 单链表的删除
bool Erase(const K& key)
{
    if (_tables.size() == 0)
    {
            return false;
    }
    //1.找到删除元素所在桶的位置
    size_t hashi = key % _tables.size();//防止除0错误
    Node* cur = _tables[hashi];
    Node* prev = nullptr;
    //2.单链表的删除
    while (cur != nullptr)
    {
            if (cur->_kv.first == key)
            {
                    if (prev == nullptr)
                    {
                            _tables[hashi] = cur->_next;
                    }
                    else
                    {
                            prev->_next = cur->_next;
                    }
                    delete cur;
                    --_size;
                    return  true;
            }
            else
            {
                    prev = cur;
                    cur = cur->_next;
            }
    }
    return false;
}

开散列的其他问题

当存储的元素不是整形,而是其他类型,比如string等,如何让解决?

哈希函数采用除留余数法,所以被摸的key必须要为整形才可以。解决方法是提供一个仿函数,将key转化为整形,更准确的来说是无符号整形,如果是负数,转化为正整数进行存储。

struct HashFunc
{
	size_t operator()(const K& key)
	{
		return key;
	}
};

此时,哈希桶需要第三个模板参数将key转化为整形做除余运算(这里第三个模板参数给了缺省值,可以根据自己的需要来实现)。
这里以Find为例,凡是需要%运算的都需要转化。

template<class K, class V,class Hash = HashFunc<K>>
class HashBucket
{
    Node* Find(const K& key)
    {
            if (_tables.size() == 0)
            {
                    return nullptr;
            }
            //1. 找到查找元素所在桶的位置
            size_t hashi = _hash(key) % _tables.size();//注意/0错误
            //....
    }                 
private:
	Hash _hash;
}

如果数据类型是string,可以使用模板的特殊来完成,也可以自己实现做为模板参数来实例化。
这里以模板的特化为例

template<>
struct HashFunc<string>
{
	size_t operator()(const string& str)
	{
		return str[0];//取字符串第一个字符作为key进行 求%
	}
};

这样写的话,如果首字母相同的话,全部在一个桶里面,这样就会造成哈希桶的极端场景。使哈希桶的效率降低,有一种字符串哈希算法BKDRHash(此算法由于在Brian Kernighan与Dennis Ritchie的《The C Programming Language》一书被展示而得 名,是一种简单快捷的hash算法,也是Java目前采用的字符串的Hash算法(累乘因子为31))。这个算法就是每次累加然后乘31即可(原理目前不知道)。

//BKDRHash算法
template<>
struct HashFunc<string>
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto ch : s)
		{
			hash += ch;
			hash *= 31;// 也可以乘以31、131、1313、13131、131313..
		}
		return hash;
	}
};

哈希桶的效率

产生一百万个随机数,存放到哈希桶中,取出最大桶下面的个数。求最大桶个数成员函数如下:

size_t MaxBucketSize()
{
    size_t max = 0;
    for (size_t i = 0; i < _tables.size(); ++i)
    {
            Node* cur = _tables[i];
            size_t count = 0;
            while (cur != nullptr)
            {
                    count++;
                    cur = cur->_next;
            }

            printf("[%d]->%d\n", i, count);//打印每个桶下面的元素个数

            if (count > max)
            {
                    max = count;
            }
    }
    return max;//最大桶的元素个数
}

测试代码如下:
这里rand()函数产生的不重复随机数最大只用32768个,让rand()+i后 能产生636105个不重复的随机数。

int main()
{
	HashBucket<int, int> hb1;
	srand((unsigned int)time(0));
	for (int i = 0; i < 1000000; ++i)
	{

		hb1.Insert(make_pair(rand() + i, rand() + i));
	}
	cout << hb1.MaxBucketSize() << endl;
	cout << hb1.size() << endl;
	return 0;
}

运行结果:

image.png
对于这636105个不重复的随机数,最大的哈希桶数据个数才是2。哈希表的性能非常恐怖。

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

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

相关文章

HTML-多媒体嵌入-MDN文档学习笔记

HTML-多媒体与嵌入 查看更多学习笔记&#xff1a;GitHub&#xff1a;LoveEmiliaForever MDN中文官网 HTML-中的图片 将图片放入网页 可以使用<img/>来将图片嵌入网页&#xff0c;它是一个空元素&#xff0c;最少只需src属性即可工作 <img src"图片链接"…

知识图谱:py2neo将csv文件导入neo4j

文章目录 安装py2neo创建节点-连线关系图导入csv文件删除重复节点并连接边 安装py2neo 安装python中的neo4j操作库&#xff1a;pip install py2neo 安装py2neo后我们可以使用其中的函数对neo4j进行操作。 图数据库Neo4j中最重要的就是结点和边&#xff08;关系&#xff09;&a…

【论文精读】BERT

摘要 以往的预训练语言表示应用于下游任务时的策略有基于特征和微调两种。其中基于特征的方法如ELMo使用基于上下文的预训练词嵌入拼接特定于任务的架构&#xff1b;基于微调的方法如GPT使用未标记的文本进行预训练&#xff0c;并针对有监督的下游任务进行微调。 但上述两种策略…

机器学习——聚类问题

&#x1f4d5;参考&#xff1a;西瓜书ysu老师课件博客&#xff08;3&#xff09;聚类算法之DBSCAN算法 - 知乎 (zhihu.com) 目录 1.聚类任务 2.聚类算法的实现 2.1 划分式聚类方法 2.1.1 k均值算法 k均值算法基本原理&#xff1a; k均值算法算法流程&#xff1a; 2.2 基于…

代码随想录算法训练营第31天|● 理论基础 ● 455.分发饼干 ● 376. 摆动序列 ● 53. 最大子序和

文章目录 理论基础分发饼干思路&#xff1a;代码&#xff1a; 摆动序列思路一 贪心算法&#xff1a;代码&#xff1a; 思路二&#xff1a;动态规划&#xff08;想不清楚&#xff09;代码&#xff1a; 最大子序和思路&#xff1a;代码&#xff1a; 理论基础 贪心算法其实就是没…

Android Jetpack Compose 沉浸式状态栏的实现

目录 概述效果展示代码实现总结 概述 说到沉浸式状态栏&#xff0c;很多小伙伴可能不太熟悉&#xff0c;其实让Android的状态栏的颜色和APP的主题颜色相同&#xff0c;给人感觉状态栏和APP就是一体的。沉浸式的状态栏让页面看起来更舒服&#xff0c;实现沉浸式状态栏也很简单&…

精品jsp+ssm基于Java的疫苗接种管理系统

《[含文档PPT源码等]精品jspssm基于Java的疫苗接种管理系统[包运行成功]》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程、包运行成功&#xff01; 使用技术&#xff1a; 开发语言&#xff1a;Java 框架&#xff1a;ssm 技术&#xff1a;JSP JDK版…

2024年!PyCharm快捷键大全

收藏&#xff01;PyCharm快捷键大全 工欲善其事必先利其器&#xff0c;PyCharm 是最popular的Python开发工具&#xff0c;它提供的功能非常强大&#xff0c;是构建大型项目的理想工具之一&#xff0c;如果能挖掘出里面实用技巧&#xff0c;能带来事半功倍的效果。 本文主要向大…

PLC_博图系列☞FBD

PLC_博图系列☞FBD 文章目录 PLC_博图系列☞FBD背景介绍FBD优势局限性 FBD 元素 关键字&#xff1a; PLC、 西门子、 博图、 Siemens 、 FBD 背景介绍 这是一篇关于PLC编程的文章&#xff0c;特别是关于西门子的博图软件。我并不是专业的PLC编程人员&#xff0c;也不懂电路…

Linux程序地址空间

引言 以下为示意草图 下面以代码验证一下&#xff1a; 1 #include<stdio.h> 2 #include<stdlib.h>3 …

C++数据结构与算法——双指针法

C第二阶段——数据结构和算法&#xff0c;之前学过一点点数据结构&#xff0c;当时是基于Python来学习的&#xff0c;现在基于C查漏补缺&#xff0c;尤其是树的部分。这一部分计划一个月&#xff0c;主要利用代码随想录来学习&#xff0c;刷题使用力扣网站&#xff0c;不定时更…

Flutter 动画(显式动画、隐式动画、Hero动画、页面转场动画、交错动画)

前言 当前案例 Flutter SDK版本&#xff1a;3.13.2 显式动画 Tween({this.begin,this.end}) 两个构造参数&#xff0c;分别是 开始值 和 结束值&#xff0c;根据这两个值&#xff0c;提供了控制动画的方法&#xff0c;以下是常用的&#xff1b; controller.forward() : 向前…

【开源】在线办公系统 JAVA+Vue.js+SpringBoot+MySQL

目录 1 功能模块1.1 员工管理模块1.2 邮件管理模块1.3 人事档案模块1.4 公告管理模块 2 系统展示3 核心代码3.1 查询用户3.2 导入用户3.3 新增公告 4 免责声明 本文项目编号&#xff1a; T 001 。 \color{red}{本文项目编号&#xff1a;T001。} 本文项目编号&#xff1a;T001。…

前端vue3实现本地及在线文件预览(含pdf/txt/mp3/mp4/docx/xlsx/pptx)

一、仅需实现在线预览&#xff0c;且文件地址公网可访问 &#xff08;一&#xff09;微软office免费预览&#xff08;推荐&#xff09; 支持doc/docx/xls/xlsx/ppt/pptx等多种office文件格式的免费预览 //示例代码//​在https://view.officeapps.live.com/op/view.aspx?src…

骨传导耳机是什么?如何选择骨传导耳机不会踩雷?

骨传导耳机是利用骨传导技术研发而成一种新型蓝牙耳机&#xff0c;其传声方式很独特&#xff0c;不通过空气传导&#xff0c;而是通过人体骨骼来传递声音。 详细传声原理请看下图&#xff1a; 随着骨传导耳机逐渐热门&#xff0c;如何选购耳机成为了问题&#xff0c;下面跟大家…

计算机设计大赛 深度学习YOLO图像视频足球和人体检测 - python opencv

文章目录 0 前言1 课题背景2 实现效果3 卷积神经网络4 Yolov5算法5 数据集6 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 深度学习YOLO图像视频足球和人体检测 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非…

1_opencv3环境搭建与测试

之前2020年5月写过一次&#xff0c;时隔3年多&#xff0c;有机会再重新写一次。相比之前&#xff0c;应该是有一点儿进步的。之前是使用默认安装路径&#xff0c;所以无需配置共享库的搜索路径。这次是自定义安装路径&#xff0c;略有区别。随着写程序的时间增长&#xff0c;编…

Dockerfile 常用指令

1、FROM 指定base镜像。 2、Docker history 显示镜像的构建历史&#xff0c;也就是Dockerfile的执行过程。 Missing 表示无法获取IMAGE ID&#xff0c;通常从Docker Hub下载的镜像会有这个问题。 3、调试Dockerfile&#xff0c;使用sudo docker run -it XXXX&#xff0c;XXXX…

036-安全开发-JavaEE应用第三方组件Log4j日志FastJson序列化JNDI注入

036-安全开发-JavaEE应用&第三方组件&Log4j日志&FastJson序列化&JNDI注入 #知识点&#xff1a; 1、JavaEE-组件安全-Log4j 2、JavaEE-组件安全-Fastjson 3、JavaEE-基本了解-JNDI-API 演示案例&#xff1a; ➢Java-三方组件-Log4J&JNDI ➢Java-三方组件-Fa…

【开源】SpringBoot框架开发大学生相亲网站

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、系统展示四、核心代码4.1 查询会员4.2 查询相亲大会4.3 新增留言4.4 查询新闻4.5 新增新闻 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的大学生相亲网站&#xff0c;包含了会员管理模块、新闻管…