哈希表详解及模拟实现(unordered_map)

news2024/9/21 2:50:17

目录

认识哈希表:

哈希冲突:

除留余数法--(常用)

 平方取中法--(了解)

折叠法--(了解)

随机数法--(了解)

泛型编程:

闭散列:

线性探测:

二次探测:

扩容:

查找:

插入:

 删除:

开散列:

扩容:

查找:

插入:

删除:

迭代器:

 全部代码:


认识哈希表:

        哈希表是一种数据结构,也称散列表,主要用于查找,且使用很频繁,可见它的效率相比其他用于查找的数据结构,肯定有优势。之前学习的顺序表和平衡二叉搜索树,查找的时间复杂度为O(n)和O(logn),它们两都需要通过key值一一比较不断缩小查找范围,进而查找到所需数据。而哈希表的优势在于无需比较,只需通过某种函数(哈希函数)计算关键码,通过映射关系可直接找到数据,近似O(1)的时间复杂度。

当向该结构中:

插入元素

根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放

搜索元素

对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
取元素比较,若关键码相等,则搜索成功

我们将它们放入哈希表key值如何取呢?一般数据是size_t或者能强转成size_t类型的key值直接取该数据,对于无法强转的例如string,则通过哈希函数转换,key值也不是直接当作下标存入哈希表内,不然如果一个数据过大,就要开很大的数组, 一般我们需要将 数据%数组大小来得到对应的下标,这种转换也是哈希函数, 举个例子:数据集合{1,7,6,4,5,9};

哈希冲突:

        还是上面的例子,这时我们有一个数据75要进入哈希表,通过哈希函数计算key值,75 % 10 = 5,但是发现下标为5的位置已经有数据,这就出现了不同的数据对应的相同的下标,这就是所谓的哈希冲突,又称哈希碰撞。

        注意哈希冲突是不能完全解决的,只能缓解,鸽巢定理可证,所有的字符串,肯定比size_t的最大值要大,因为不限长度字符串几乎是无限的,所以鸽子比巢多,肯定会有巢有两只以上的鸽子。

 缓解哈希冲突主要从两个方面:

1.,第一个方面就是对哈希函数入手,上面这个例子的哈希函数,是用key对数组大小取余,这是直接定址法,这种哈希函数出现哈希冲突的概率还是不小的,先来看看哈希函数的设计原则:

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

再来看看几种常见的哈希函数:

除留余数法--(常用)

假设哈希表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

 平方取中法--(了解)

假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

折叠法--(了解)

折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这
几部分叠加求和,并按哈希表表长,取后几位作为散列地址。

随机数法--(了解)

选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中
random为随机数函数。

 2.第二个方面就是对哈希表的存储结构入手,想必大家见过最多的哈希表结构就是顺序表+链表,其实哈希表也可以单纯用顺序表实现,两种不同的底层结构在于它们如何应对哈希冲突,C++的STL库中使用的是顺序表+链表的方式,没错这种方式的效率是更优的,但是单纯用顺序表的结构也是值得学习的,接下来的内容我会分别介绍并模拟实现这两种哈希表的底层结构。

泛型编程:

        在模拟实现中,我的my_unordered_set和my_unordered_map封装了一个哈希表HashTable,但set里面存的是一个数据K,而set里面存的是pair<K,T>,HashTable里面只接受一个data,这就导致了如果是set,data就是K类型,如果是map,data就是pair<K,V>,但我们只有一个哈希表,该怎么解决这种矛盾呢?

 仿函数:我们可以分别在set和map里创建一个类,在类里重载运算符(),然后在set中的()重载中直接返回K,在map中的()重载中返回pair中的K,也就是pair中的first,然后将这个类传给HashTable,在HashTable中使用data前就调用这个类的括号来取里面的数据:

set:

map: 

在HashTable中的使用:(哈希地址的计算中就用到了)

 HashFunc和上面讲的一样,主要作用是如果key为其他不是size_t的类型将它们强转成size_t和若为string则通过哈希函数转化成size_t,这种方法就是泛型编程的一种。

闭散列:

        闭散列,又称开放定址法,也就是上面提到的单纯使用顺序表的方法来实现哈希表,它应对哈希冲突的方法是如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去,那么如何找到“下一个”空位置呢?

线性探测:

        回到最开始的例子,我们需要插入75,通过哈希函数计算下标为5,但下标为5的位置已经被占用。

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

所以最后我们能找到8下标位置为空位置,然后进行插入数据。
注意往后探测过程中不能超过数组长度,所以我们每向后走一次就需要%数组长度,以保证当超过数组长度时,下标能回到数组开头。

如何判断这个位置是否为空,只需将每个位置里面存入一个状态值(枚举类型),总共有三个状态:EMPTY,EXIST,DELETE,分别表示空,存在数据,之前有数据但被删除

这部分代码只需理解线性探测思路就行,其他不理解的地方,需结合全部代码。

二次探测:

        虽然线性探测能解决哈希冲突,但可以发现这样冲突的数据大部分都聚在了一起,不离散,如图:

为了避免这种情况,线性探测时hashi每次向后走1步,我们采用二次探测,也就是每次向后走i的平方步,每次i++,也就是依次走1,4,9,16,25......步,实现很简单,这里不演示。

扩容:

        闭散列的扩容不是满了才扩容,我们先引入一个概念:负载因子,负载因子 = 存在的元素/数组的容量,简单来说负载因子就是占用率,当负载因子>=0.7的时候我们才进行扩容。

扩容思路:

我们可以直接开一个新的hash表,将新表的大小设为旧表的2倍,再将旧表的元素一个个插入到新表,最后用swap函数交换新旧表。

这样写的好处:不必销毁新表,因为新表是局部对象,函数结束后自动销毁了。

查找:

         通过key查找某个节点:

先通过key用哈希函数算出对应哈希地址,再从哈希地址开始往后线性探测,找到后返回节点:

插入:

        分析一下插入,当插入一个数时该如何做呢?

1.先用查找函数判断能否找到,若找到了,代表原哈希表里有,直接返回false。

2.用负载因子判断是否需要扩容,需要就进行扩容。

3.通过key和哈希函数,算出哈希地址。

4.哈希地址上有值就往后线性探测。

 删除:

        因为之前,我们在每个节点上都设置了三种状态:EMPTY,EXIST,DELETE,所以现在删除一个数就非常简单了:

只需先通过哈希函数和线性探测找到该节点,再将该节点的状态改为DELETE即可。

开散列:

        开散列也就是C++STL库哈希表实现方法,说明它相比闭散列还是有一定的优越性的,开散列应对哈希冲突的方法就是在冲突数据下面用链表进行连接。

扩容:

        开散列的扩容条件就是_n == 数组大小的时候: 

相比闭散列的扩容方法,开散列只要扩容条件不同,其他差不多,只有旧表中每个桶的数据要依次头插到新表对应的哈希地址。

查找:

  如果对应的哈希地址里面有数据,就沿着该地址的链表遍历,找到即可。 

插入:

1.先用查找函数判断能否找到,若找到了,代表原哈希表里有,直接返回false。

2.判断是否需要扩容,需要就进行扩容。

3.通过key和哈希函数,算出哈希地址。

4.在该哈希地址的链表处进行头插。

删除:

1.先通过查找函数找到key对应的哈希地址

2.遍历该哈希地址的链表。

3.找到后连接该节点的上一个和下一个节点。

4.注意如果是头删要特殊处理,因为上一个节点为空。 

迭代器:

        迭代器功能都比较简单,这里我只讲++的思路,其他功能可以到文章最后看全部代码。

++:

1.到一个哈希地址时要先判断存不存在冲突数据,也就是链表。

2.若有冲突数据,直接走向链表的next即可。

3.链表走到尾部,就需要从这个链表的哈希地址开始往后线性探测,直到找到下一个有有效数据的哈希地址或者哈希表走完。

 全部代码:

  my_ordered_set.h

#include"HashTable.h"

template<class K,class Hash = HashFunc<K>>
class my_unordered_map
{
	struct SetKeyOfT
	{
		const K& operator()(const K& key)
		{
			return key;
		}
	};
	typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::iterator iterator;
	typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::const_iterator const_iterator;

public:
	iterator begin()
	{
		return _ht.begin();
	}

	iterator end()
	{
		return _ht.end();
	}

	const_iterator begin()const
	{
		return _ht.begin();
	}

	const_iterator end()const
	{
		return _ht.end();
	}

	pair<iterator,bool> insert(const K& key)
	{
		return _ht.Insert(key);
	}

	iterator find(const K& key)
	{
		return _ht.Find(key);
	}

	bool erase(const K& key)
	{
		return _ht.Erase(key);
	}


private:
	hash_bucket::HashTable<K,const K,SetKeyOfT,Hash> _ht;
};

my_unordered_map.h

#include"HashTable.h"
template<class K, class T,class Hash = HashFunc<K>>
class my_unordered_set
{
	struct MapKeyOfT
	{
		const K& operator()(const pair<K,T>& kv)
		{
			return kv.first;
		}
	};
	typedef typename hash_bucket::HashTable<K, pair<K,T>, MapKeyOfT, Hash>::iterator iterator;
	typedef typename hash_bucket::HashTable<K, pair<K,T>, MapKeyOfT, Hash>::const_iterator const_iterator;

public:
	iterator begin()
	{
		return _ht.begin();
	}

	iterator end()
	{
		return _ht.end();
	}

	const_iterator begin()const
	{
		return _ht.begin();
	}

	const_iterator end()const
	{
		return _ht.end();
	}

	pair<iterator, bool> insert(const K& key)
	{
		return _ht.Insert(key);
	}

	iterator find(const K& key)
	{
		return _ht.Find(key);
	}

	bool erase(const K& key)
	{
		return _ht.Erase(key);
	}

	T& operator[](const K& key)
	{
		pair<iterator, bool> ret = _ht.Insert(make_pair(key, T()));
		return ret->iterator->second;
	}
private:
	hash_bucket::HashTable<K, pair<K,T>, MapKeyOfT, Hash> _ht;
};

 HashTable.h

#pragma once
#include<vector>
#include<string>
using namespace std;
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

// 特化
template<>
struct HashFunc<string>
{

	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto ch : key)
		{
			hash *= 131;
			hash += ch;
		}

		return hash;
	}
};


namespace open_address
{
	enum State
	{
		EMPTY,
		EXIST,
		DELETE
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;
	};

	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
	public:
		HashTable()
		{
			_tables.resize(10);
		}

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;
			  //_n / _table.size() >= 0.7//不采用这种判断方法是因为左右类型不同,所以将左右都*10
			if (_n * 10 / _tables.size() >= 7)
			{
				HashTable<K, V, Hash> newHT;
				newHT._tables.resize(_tables.size() * 2);

				// 旧表重新计算负载到新表
				for (size_t i = 0; i < _tables.size(); i++)
				{
					if (_tables[i]._state == EXIST)
					{
						newHT.Insert(_tables[i]._kv);
					}
				}

				_tables.swap(newHT._tables);//交换新旧表
			}

			Hash hs;
			size_t hashi = hs(kv.first) % _tables.size();//hs为仿函数,将key转为size_t,因为kv.first 可能为string 
			                                             
			// 线性探测
			while (_tables[hashi]._state == EXIST)//_tables为顺序表
			{
				++hashi;
				hashi %= _tables.size();
			}

			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			++_n;

			return true;
		}

		HashData<K, V>* Find(const K& key)
		{
			Hash hs;
			size_t hashi = hs(key) % _tables.size();

			// 线性探测
			while (_tables[hashi]._state != EMPTY)
			{
				if (_tables[hashi]._state == EXIST &&
					_tables[hashi]._kv.first == key)
				{
					return &_tables[hashi];
				}

				++hashi;
				hashi %= _tables.size();//防止越界
			}

			return nullptr;
		}

		bool Erase(const K& key)
		{
			HashData<K, V>* ret = Find(key);
			if (ret == nullptr)
			{
				return false;
			}
			else
			{
				ret->_state = DELETE;
				--_n;

				return true;
			}
		}

	private:
		vector<HashData<K, V>> _tables;
		size_t _n = 0;  // 有效数据个数
	};
}





namespace hash_bucket
{
	template<class T>
	struct HashNode
	{
		T _data;
		HashNode<T>* next;
		HashNode(const T& data)
			:_data(data)
			,next(nullptr)
		{}
	};
	template<class K,class T,class KeyOfT,class Hash = HashFunc<T>>
	class HashTable
	{
		typedef HashNode<T> Node;
	public:
		template<class Ptr,class Ref>
		struct _HTIterator//迭代器,设置为内部类
		{
			typedef _HTIterator Self;
			typedef HashNode<T> Node;

			Node* _node;
			const HashTable* _pht;

			_HTIterator(Node* node,const HashTable* pht)
				:_node(node)
				,_pht(pht)
			{}

			Ref operator*()
			{
				return _node->_data;
			}

			Ptr operator->()
			{
				return &_node->_data;
			}

			bool operator!=(const Self& s)
			{
				return _node != s._node;
			}



			Self& operator++()
			{
				if (_node->next)//当前桶没走完,直接找下一个节点
				{
					return _node->next;
				}
				else
				{
					Hash ha;
					KeyOfT kot;
					size_t i = ha(kot(_node->_data)) % _pht->_table.size();
					i++;
					for (;i < _pht->_table.size();i++)
					{
						if (_pht->_table[i])
							break;
					}//循环结束后要么找到一个有数据的桶,要么整个表走完了

					if (i == _table.size())//表走完了
					{
						_node = nullptr;
					}
					else//找到了有数据的桶
					{
						_node = _pht->_table[i];
					}

				}
				return this;
			}
		};
		typedef _HTIterator<T*, T&> iterator;
		typedef _HTIterator<const T*, const T&> const_iterator;

		iterator end()
		{
			return iterator(nullptr, this);
		}

		iterator begin()
		{
			size_t i = 0;
			for (i = 0;i < _table.size();i++)
			{
				Node* cur = _table[i];
				if (cur)
				{
					return iterator(cur, this);
				}
			}
		}

		const_iterator end() const
		{
			return const_terator(nullptr, this);
		}

		const_iterator begin() const
		{
			size_t i = 0;
			for (i = 0;i < _table.size();i++)
			{
				Node* cur = _table[i];
				if (cur)
				{
					return const_iterator(cur, this);
				}
			}
		}

		pair<iterator,bool> Insert(const T& data)
		{
			HashFunc hs;
			KeyOfT kot;
			iterator it = Find(kot(data));
			if (it!=end())//如果表中原来有
			{
				return make_pair(it, false);
			}
			if (_n == _table.size())//扩容
			{
				vector<Node*> newtable(2 * _table.size(), nullptr);
				for (int i = 0;i < _table.size();i++)
				{
					Node* cur = _table[i];
					while (cur)
					{
						Node* next = cur->next;
						size_t hashi = hs(kot(data)) % newtable._table.size();
						//头插
						cur->next = newtable[hashi];
						newtable[hashi] = cur;
						cur = next;
					}
					_table[i] = nullptr;
				}
				//新旧表交换
				_table.swap(newtable._table);
			}

			size_t hashi = hs(kot(data)) % _table.size();
			Node* newnode = new HashNode(data);
			//头插
			newnode->next = _table[hashi];
			_table[hashi] = newnode;
			_n++;
			return make_pair(interator(newnode,this),true);
		}

		iterator Find(const K& key)
		{
			Hash hs;
			KeyOfT kot;
			size_t hashi = hs(key) % _table.size();
			Node* cur = _table[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					return iterator(cur,this);
				}
				cur = cur->next;
			}
			return end();
		}
		bool Erase(const K& key)
		{
			Hash hs;
			KeyOfT kot;
			size_t hashi = hs(key) % _table.size();
			Node* prev = nullptr;
			Node* cur = _table[hashi];
			while (cur)
			{
				if (kot(cur->data) == key)
				{
					if (prev == nullptr)//如果删除的是第一个
					{
						_table[hashi] = cur->next;
					}
					else//如果删除的是中间的
					{
						prev->next = cur->next;
					}
					delete cur;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->next;
				}
			}
			return false;
		}
	private:
		vector<Node*> _table;
		size_t _n;
	};
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

哆啦AAA萌也能用HTML画出来?看这里!【完整代码可运行】

关注微信公众号「ClassmateJie」有完整代码以及更多惊喜等待你的发现。 简介/效果展示 你是否曾经想过&#xff0c;那些可爱的哆啦A梦角色是如何被创造出来的呢&#xff1f;今天&#xff0c;我要带你一起探索一个神奇的秘密——用HTML画出哆啦A梦&#xff01; 代码 <!DOCT…

Nature Communications 南京大学开发智能隐形眼镜用于人机交互

近日&#xff0c;南京大学的研究人员研制了一种微型、难以察觉且生物相容的智能隐形眼镜&#xff08;smart contact lenses &#xff0c;SCL&#xff09;&#xff0c;可用于原位眼球追踪和无线眼机交互。采用频率编码策略&#xff0c;无芯片、无电池的镜头成功地检测眼球运动和…

消费增值:国家支持的消费新零售模型

在当下的消费时代&#xff0c;一个全新的概念——消费增值&#xff0c;正逐渐走进大众视野。它不仅仅是一种消费模式&#xff0c;更是一种全新的财富增长途径。那么&#xff0c;消费增值究竟是什么&#xff1f; 首先&#xff0c;消费增值的本质在于将消费行为与投资行为相结合…

Oracle数据库Day01-SELECT语句

一、SQL语句 1. 环境配置与准备 linux端oracle用户打开监听//查看监听状态与开始监听 lsnrctl status lsnrctl start开启数据库sqlplus / as sysdba startup;解锁hr用户样例数据库&#xff0c;给hr用户设置密码并且连接alter user hr account unlock; alter user hr identifie…

香橙派 AIpro开发体验:使用YOLOV8对USB摄像头画面进行目标检测

香橙派 AIpro开发体验&#xff1a;使用YOLOV8对USB摄像头画面进行目标检测 前言一、香橙派AIpro硬件准备二、连接香橙派AIpro1. 通过网线连接路由器和香橙派AIpro2. 通过wifi连接香橙派AIpro3. 使用vscode 通过ssh连接香橙派AIpro 三、USB摄像头测试1. 配置ipynb远程开发环境1.…

视觉SLAM十四讲:从理论到实践(Chapter5:相机与图像)

前言 学习笔记&#xff0c;仅供学习&#xff0c;不做商用&#xff0c;如有侵权&#xff0c;联系我删除即可 目标 理解针孔相机的模型、内参与径向畸变参数。理解一个空间点是如何投影到相机成像平面的。掌握OpenCV的图像存储与表达方式。学会基本的摄像头标定方法。 一、相…

【面试必看】Java并发

并发 1. 线程 1. 线程vs进程 进程是程序的一次执行过程&#xff0c;是系统运行程序的基本单位&#xff0c;因此进程是动态的。 系统运行一个程序即是一个进程从创建&#xff0c;运行到消亡的过程。在 Java 中&#xff0c;当我们启动 main 函数时其实就是启动了一个 JVM 的进…

CLIP源码详解:clip.py 文件

前言 这是关于 CLIP 源码中的 clip.py 文件中的代码带注释版本。 clip.py 文件的作用&#xff1a;封装了 clip 项目的相关 API&#xff0c;通过这些 API &#xff0c;我们可以轻松使用 CLIP 项目预训练好的模型进行自己项目的应用。 另外不太容易懂的地方都使用了二级标题强…

echart扩展插件词云echarts-wordcloud

echart扩展插件词云echarts-wordcloud 一、效果图二、主要代码 一、效果图 二、主要代码 // 安装插件 npm i echarts-wordcloud -Simport * as echarts from echarts; import echarts-wordcloud; //下载插件echarts-wordcloud import wordcloudBg from /components/wordcloudB…

【Linux】升级make(版本4.4.1)、升级gdb(版本14.1)、升级autoconf(版本2.71)

centos7升级make&#xff08;版本4.4.1&#xff09;&#xff1a; make&#xff1a;编译和构建工具。Linux中很多软件包需要make编译构建。官网&#xff1a;Make - GNU Project - Free Software Foundation 本次升级前的make版本是3.82&#xff0c;准备安装的版本是4.4.1。make…

很耐看的Go快速开发后台系统框架

序言 秉承Go语言设计思路&#xff0c;我们集成框架简单易用、扩展性好、性能优异、兼顾安全稳定&#xff0c;适合企业及初学者用来开发项目、学习。我们框架和市面上其他家设计的不同&#xff0c;简单一步做到的我们不会两步&#xff0c;框架能自动处理&#xff0c;绝不手动处…

MySQL8.0新特性join lateral 派生子查询关联

在 MySQL 8.0 及更高版本中&#xff0c;LATERAL 是一个用于派生表&#xff08;derived tables&#xff09;的关键字&#xff0c;它允许派生表中的查询引用包含该派生表的 FROM 子句中的表。这在执行某些复杂的查询时特别有用&#xff0c;尤其是当需要在子查询中引用外部查询的列…

服了这群人!已举报!

文章首发于公众号&#xff1a;X小鹿AI副业 大家好&#xff0c;我是程序员X小鹿&#xff0c;前互联网大厂程序员&#xff0c;自由职业2年&#xff0c;也一名 AIGC 爱好者&#xff0c;持续分享更多前沿的「AI 工具」和「AI副业玩法」&#xff0c;欢迎一起交流~ 服了这群人&#x…

生产订单工序新增BAPI:CO_SE_PRODORD_OPR_CREATE增强

背景&#xff1a; 创建生产订单工序时需要通过BAPI来维护圈起来的字段&#xff0c;但是BAPI不包含这些字段&#xff0c;所以对BAPI进行一些增强处理。 实现过程&#xff1a; 1.拷贝标准BAPI:CO_SE_PRODORD_OPR_CREATE至ZCO_SE_PRODORD_OPR_CREATE&#xff08;最好放在新的自定…

结合Django和Vue.js构建现代Web应用

文章目录 1. 创建Django项目2. 配置Django后端3. 创建Vue.js前端4. 连接Django和Vue.js5. 构建和部署 在现代Web开发中&#xff0c;结合后端框架和前端框架是非常常见的&#xff0c;其中Django作为一种流行的Python后端框架&#xff0c;而Vue.js则是一种灵活强大的前端框架。本…

使用DoraCloud搭建研发办公云桌面,保障信息安全

一、背景 在信息化全面推进的今天&#xff0c;小型公司的数据安全和员工远程办公已成为亟待解决的重要问题。为了提高工作效率和数据安全性&#xff0c;公司决定引入云桌面技术&#xff0c;实现员工远程办公和数据安全保障。 云桌面&#xff08;VDI&#xff09;&#xff0c;也…

如何自学制作电子画册,这个秘籍收藏好

随着数字技术的飞速发展&#xff0c;电子画册作为一种新兴的媒体展示形式&#xff0c;以其独特的魅力和丰富的表现手法&#xff0c;受到了越来越多人的喜爱。那么&#xff0c;如何自学制作电子画册呢&#xff1f; 1. 学习基础知识 首先&#xff0c;你需要了解电子画册的基本构…

python探索转义字符的奥秘

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、转义字符的定义与功能 案例解析&#xff1a;换行符与双引号 二、转义字符的应用场景 …

jQuery效果2

jQuery 一、属性操作1.内容2.列子&#xff0c;购物车模块-全选 二、内容文本值1.内容2.列子&#xff0c;增减商品和小记 三、元素操作(遍历&#xff0c;创建&#xff0c;删除&#xff0c;添加&#xff09;1.遍历2.例子&#xff0c;购物车模块&#xff0c;计算总件数和总额3.创建…

什么是访问越界(C语言数组、指针、结构体成员访问越界)

在C语言中&#xff0c;访问越界&#xff08;Access Violation 或 Out-of-Bounds Access&#xff09;是指程序试图访问的内存位置超出了其合法或已分配的范围。这通常发生在数组、指针或其他内存结构的使用中。 案例&#xff1a; #include <stdio.h>//数组 //Visiting b…