C++:哈希

目录

一、unordered系列关联容器

二、底层的结构

哈希结构

哈希冲突/哈希碰撞

①、闭散列 —> 开放定址法

闭散列的模拟实现

②、开散列 —> 拉链法/哈希桶

哈希桶的模拟实现

三、哈希应用

位图

位图的特点

位图的模拟实现

布隆过滤器

布隆过滤器的模拟实现


一、unordered系列关联容器

unordered_set

unordered_map

而关于unordered_set和unordered_map与map和set的用法区别不大,具体用法看map和set部分的详解

unordered_set和unordered_map与map和set主要有以下两个区别:

1、map和set遍历是有序的,而unordered_set和unordered_map遍历是无序的

2、map和set是双向迭代器,而unordered_set和unordered_map是单向迭代器

而引进unordered系列的原因是因为:在大量数据的情况下,增删查改效率更高,尤其是查效率最高

下面演示一下用法:

通过结果可以看出unordered_set插入后是无序的,unordered_set与set的用法基本相同,也是有去重的作用,剩下用法参照map和set


二、底层的结构

哈希结构

哈希也叫散列,表示值跟存储位置建立映射关联关系

在我们还没学习哈希时,做题时也用过类似哈希的思路去解决问题,比如说要查找一个数组中唯一只出现一次的数字

这时我们只需要遍历一遍数组,遍历到每一个数字时,在一个新数组的对应位置++,最后再遍历一遍新数组,看哪个位置的值为1,该位置所对应的数字即为只出现一次的数字

但是有时候会出现特殊情况,比如只有极少个数的数,但是它们之间的差距却很大,如果我们开辟与之对应位置的数组,就会导致非常浪费空间,这时就出现了哈希的除留余数法的思想

哈希中构造出来的结构:哈希表(散列表),就类比于上面使用的数组

除留余数法:散列表是大小为n的,用这个几个大小不同的数,分别去模p(%np),这里的p是最接近n或等于n的质数,之后再存入散列表中这就是除留余数法,Hash(i) = i % p(p <= n)这时无论是多大的数,都可以存在这个空间中,也不怕开辟太多的多余空间,造成空间浪费

例如:有,四个数,7,201,400,40000,这时我们用除留余数法的思想,假设有一个大小为5的散列表,接下来这,四个数分别%5,得到的结果是2,1,0,0,就可以很好地存储进散列表中了,不用像之前一样开辟40000个空间却只存4个数而造成空间浪费

这时又有问题了,四个数中的400与40000%5后都是0,那怎么解决呢?

这种情况就叫做哈希冲突/哈希碰撞

哈希冲突/哈希碰撞

有两种方法解决哈希碰撞/哈希冲突:

①、闭散列 —> 开放定址法

开放定址法即如果当前位置已经被占用了,那我们就接着往后面找,有没有没有被占用的的位置

而开放定址法也有两种方式:

第一种:线性探测

线性探测就是指一个位置被占用后,就依次往后找没被占用的位置

但是有可能会出现,位置依次占用后,如果删除一个位置的数据,接下来在找后面的数据时,会因为删除的位置为空而找不到了,所以我们可以用枚举设置一个状态,(EMPTY)空、(EXIST)存在、(DELETE)删除,这样删除完后,如果后面数据发现此位置为空, 但是状态是删除时,就不会终止查找了

哈希在扩容时引入了负载因子(载荷因子)的概念:

负载因子 = 填入表中元素个数 / 散列表的长度

负载因子越大,冲突概率越大;负载因子越小,冲突概率越小

一旦到了负载因子的基准值,就需要扩容了

基准值越大,冲突越多,效率越低,空间利用率越高

基准值越小,冲突越少,效率越高,空间利用率越低

一般基准值是控制在0.7~0.8

 但是线性探测在一些特殊情况下,就会显得非常不好:

比如某一个位置非常冲突,连续几个数都要这个位置,所以都会向后延伸,这时如果再遇到该位置后面位置的数,依然还得往后延伸,因此会造成某个位置冲突很多的情况下,互相占用,冲突一片,下面的二次探测能够稍微减轻冲突的情况


第二种:二次探测

线性探测是一个数所对应的位置如果有数了,就+i处理,即+1到下一个位置,如果下一个位置还有数+2,以此类推(i >= 0)

而二次探测则是+ i^2(i >= 0)

会比线性探测好一点,但是本质依然可能会互相占用概率大,这两种方法统一的弊端就是,例如:好几个数据都在一号位置存,那么就顺延到后面位置存,而后面数据存的时候发现自己的位置被占用,就又会占用其他数据的位置,恶性循环

下面有一种更好的方式就叫做拉链法/哈希桶,很好解决了上面的问题

闭散列的模拟实现
//设置状态,表示空、存在、删除
enum State
{EMPTY,EXIST,DELETE
};//有一个状态_state以及一个pair类型的数据_kv
template<class K, class V>
struct HashData
{pair<K, V> _kv;State _state = EMPTY;
};//这个是正常能取模的数据调用的
template<class K>
struct HashUsual
{size_t operator()(const K& key){return (size_t)key;}
};//有些数据不能直接取模,例如string,需要自己写仿函数
//也可以不主动调用,特化处理
template<>
struct HashUsual<string>
{//string类型的就返回所有字符的ascll码size_t operator()(const string& key){size_t val = 0;for (auto& e : key){val += e;}return val;}
};template<class K, class V, class Hash = HashUsual<K>>
class HashTable
{
public:bool Insert(const pair<K, V>& kv){//负载因子到了就扩容,一般是0.7~0.8//之所以>=7,不是>=0.7,是因为两个整数除完不能等于0.7这个小数//所以干脆放大十倍就不会有这个问题了if (_table.size() == 0 || 10 * _size / _table.size() >= 7){size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;HashTable<K, V> newHT;newHT._table.resize(newsize);//旧表的数据映射到新表中for (auto& e : _table){if (e._state == EXIST){newHT.Insert(e._kv);}}//旧表与新表交换,执行完毕旧表自动释放了_table.swap(newHT._table);}//线性探测Hash hs;//找到要存储的位置hashisize_t hashi = hs(kv.first) % _table.size();while (_table[hashi]._state == EXIST){hashi++;//超过散列表的范围后,需要返回到开头hashi %= _table.size();}_table[hashi]._kv = kv;_table[hashi]._state = EXIST;++_size;//	//二次探测//	Hash hs;//	//找到要存储的位置start//	size_t start = hs(kv.first) % _table.size();//	size_t i = 0;//	size_t hashi = start;//	while (_table[hashi]._state == EXIST)//	{//		//如果_table[hashi]有值,hashi就+i^2//		i++;//		hashi = start + i * i;//		hashi %= _table.size();//	}//	_table[hashi]._kv = kv;//	_table[hashi]._state = EXIST;//	++_size;return true;}HashData<K, V>* Find(const K& key){//如果表为空,就不查找了if (_table.size() == 0){return nullptr;}Hash hs;//除留余数法size_t hashi = hs(key) % _table.size();while (_table[hashi]._state != EMPTY){if (_table[hashi]._state != DELETE && _table[hashi]._kv.first == key){return &_table[hashi];}hashi++;//如果超过散列表,就回到开头hashi %= _table.size();//如果回到开头了,就breakif (hashi == key % _table.size()){break;}}return nullptr;}bool Erase(const K& key){//查找值为key的数据在不在//如果在,只需要改变状态再--_size就完成了删除HashData<K, V>* ret = Find(key);if (ret){ret->_state = DELETE;--_size;return true;}else{return false;}}void Print(){for (size_t i = 0; i < _table.size(); ++i){if (_table[i]._state == EXIST){printf("[%d:%d] ", i, _table[i]._kv.first);}else{printf("[%d:×] ",i);}}}
private://存储的每个数据是HashData<K, V>的vector<HashData<K, V>> _table;size_t _size = 0;//表示数组中存储了多少有效数据
};void testHT()
{HashTable<int, int> ht;int arr[] = { 3,6,15,13,27,48 };for (auto e : arr){ht.Insert(make_pair(e, e));}ht.Print();
}

②、开散列 —> 拉链法/哈希桶

哈希桶的方式,就是如果数据在一个位置,就先挂起,先不放入位置中

哈希桶的表就不是普通数组了,而是指针数组

每次插入新的数据时,只需要头插即可

例如下图所示方式: 

哈希桶的模拟实现
template<class K,class V>
struct HashNode
{pair<K, V> _kv;HashNode<K, V>* _next;HashNode(const pair<K,V>& kv):_kv(kv),_next(nullptr){}
};//这个是正常能取模的数据调用的
template<class K>
struct HashUsual
{size_t operator()(const K& key){return (size_t)key;}
};//有些数据不能直接取模,例如string,需要自己写仿函数
//也可以不主动调用,特化处理
template<>
struct HashUsual<string>
{//string类型的就返回所有字符的ascll码size_t operator()(const string& key){//BKDR法size_t val = 0;for (auto& e : key){val *= 131;val += e;}return val;}
};template<class K,class V,class Hash = HashUsual<K>>
class HashTable
{typedef HashNode<K, V> Node;
public://析构,释放哈希桶~HashTable(){for (size_t i = 0; i < _table.size(); ++i){Node* cur = _table[i];while (cur){//记录cur的nextNode* next = cur->_next;delete cur;cur = next;}//置空_table[i] = nullptr;}}bool Insert(const pair<K, V>& kv){//去重if (Find(kv.first)){return false;}Hash hs;//仿函数//负载因子到1就扩容if (_size == _table.size()){size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;vector<Node*> newtable;newtable.resize(newsize, nullptr);//旧表结点映射到新表for (size_t i = 0; i < newtable.size(); ++i){Node* cur = _table[i];while (cur){//提前保存cur的next,下面会改变next,不然会找不到Node* next = cur->_next;size_t hashi = hs(cur->_kv.first) % newtable.size();//头插:插入结点的next指向新表结点的第一个//插入结点作为新表结点的第一个cur->_next = newtable[hashi];newtable[hashi] = cur;}_table[i] = nullptr;}//交换_table与newtable,便于运行结束自动删除_table_table.swap(newtable);}size_t hashi = hs(kv.first) % _table.size();//头插Node* newnode = new Node(kv);newnode->_next = _table[hashi];_table[hashi] = newnode;++_size;return true;}Node* Find(const K& key){//如果表中无数据,直接returnif (_table.size() == 0){return nullptr;}Hash hs;//仿函数//找到要查找数据对应的位置size_t hashi = hs(key) % _table.size();Node* cur = _table[hashi];//在该位置挂起的数据中找while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}//如果代码走到这里,说明没找到return nullptr;}bool Erase(const K& key){//如果为空,直接returnif (_table.size() == 0){return nullptr;}Hash hs;//仿函数size_t hashi = hs(key)% _table.size();Node* cur = _table[hashi];Node* prev = nullptr;while (cur){//如果相等则删除if (cur->_kv.first == key){//删除的是_table[hashi]的头结点if (prev == nullptr){_table[hashi] = cur->_next;}//删除的是_table[hashi]的中间结点else{prev->_next = cur->_next;}delete cur;--_size;	return true;}prev = cur;cur = cur->_next;}//代码运行到这里,说明没有找到return false;}private:vector<Node*> _table;size_t _size = 0;//存储的有效数据个数	
};

三、哈希应用

位图

有一道题来引出位图的概念:

假设给40亿个不重复且未排序的无符号整数,然后给出一个无符号整数,如何快速判断这个数是否在这40亿个数中?

我们的之前学习过的方法例如:堆、搜索树、哈希这些方法确实搜索的速度很快,但是一个无符号整数4个字节,40亿个无符号整数存储空间得占用160亿个字节即大约14GB左右,这肯定是存不下的,只能存在磁盘中,而磁盘查找又很麻烦,效率非常低,所以之前学习的方法是没有办法解决的 

这里就引入位图的概念,在给的这40亿个无符号整数,给定的一个无符号整数只会有两种状态,在或者不在,那么就可以使用二进制的比特位0/1存储(1表示在,0表示不在)

因为1GB = 1024MB = 1024 * 1024KB = 1024 * 1024 * 1024 byte约等于10亿,所以40亿字节相当于4GB,而40亿大约是2^32,现在使用位图用的是40亿个比特位,一个byte = 8个比特位,所以位图占用的空间就是4GB / 8 约等于512MB,对比上面所占的空间,可以说是非常小了,效率也很快

而通过上面的例子,就可以得知,位图就是用每一位来存在某种状态,是用于非常大的数据,且数据无重复的情况,通常是判断某个数据存不存在

而这2^32比特位我们应该怎么开辟呢,就开辟每个元素是char类型的数组即可,一个char是8个比特位,所以就相当于每8个比特位用一个char存储,即0~7位存储在第一个char,8~15存储在第二个char,以此类推

这时我们通过/8和%8就分别可以得知在第几个char与在这个char的第几个比特位

例如前两个char表示的比特位分别是0~7和8~15,那么假设数字10,10 / 8 == 1,10 % 8 == 2,所以数字10就在第一个char的第二个比特位上(注意是从0开始的),如下所示:

而不论是多少数据,并不是说题目给10亿数据,我们就开10亿个比特位大小的空间,而是不论是10亿,50亿,90亿,我们都开整数的最大值,即42亿9千万多,因为给出的数据是无序的,不能保证数据的大小

所以开辟时可以有下面的方式:

bit_set<-1>:可以传-1,是因为位图的非类型模板参数是size_t类型的,即无符号整型,-1表示的就是整型的最大值

bit_set<0xffffffff>:0xffffffff是用8个16进制位表示的,1个16进制位等于4个2进制位,所以8个16进制位都是f,就表示二进制位都为1,也是整型的最大值


我们上面也计算了开辟整型的最大值也就是42亿多比特位,算下来就是512MB左右,下面可以看看实际的情况是不是我们所说的那样:

先用上面的方法创建整型最大值个比特位,然后打断点调试:

这时打开我们的Windows任务管理器,发现此时运行的这个进程所占内存是0.5MB:

接下来我们按F10,创建这个bs变量即开辟好整型的最大值个比特位的空间(箭头表示已经走到的位置,说明创建完成了):

这时再打开任务管理器:

可以清楚看到此时该进程所占用的内存变为了512MB


位图的特点

1、快,且节省空间(512MB左右)

2、比较局限,只能映射整型(因为对应比特位的下标都是整数)

位图是直接定址法,不存在冲突


位图的模拟实现

bitset是C++库中的容器,我们只实现了最核心的三个接口:set、reset、test

set:将一个数对应的比特位变为1

reset:将一个数对应的比特位变为0

test:测试一个数存在不存在

//非类型模板参数
template<size_t N>
class bitset
{
public:bitset(){//每次多开一个char,保证所有数据都能存进入//默认所有位初始化为0_bits.resize(N / 8 + 1, 0);}//下面的i都表示在第几个char中的比特位中//下面的j都表示在这个char的第几个比特位上void set(size_t x){size_t i = x / 8;size_t j = x % 8;//算出的比特位按位与1,该位置就为1了_bits[i] |= (1 << j);}void reset(size_t x){size_t i = x / 8;size_t j = x % 8;//~是按位取反,每一位都0变1,1变0_bits[i] &= ~(1 << j);}bool test(size_t x){size_t i = x / 8;size_t j = x % 8;return _bits[i] & (1 << j);}
private:vector<char> _bits;
};void test_bitset()
{bitset<-1> bs;
}

布隆过滤器

上面所说的位图只能处理整数,而如果是其他类型的例如字符串之类的就不能处理了,所以这里引入了布隆过滤器

布隆过滤器设计思路:就是将字符串使用字符串哈希算法转换成整型,然后去映射一个位置进行标记

但是这样实现,会造成误判的情况,比如说有两个字符串完全不同,但是使用字符串哈希算法转换成的整型数值却是一样的,从而造成误判

误判只会存在于在的情况,因为有一个字符串映射到a位置,而另一个字符串本身没有出现,但是经过算法算出来的值与上一个字符串相同,这时这个字符串存在的情况就会出现误判

字符串不存在的情况是不会有误判的情况出现的,因为如果该位置映射的值为0,那么就说明肯定没有对应该位置的字符串出现,所以也就不存在误判的事情了


对于上面误判的情况,我们可以加以改进,可以将一个字符串多映射几个位置,这样就可以有效降低误判率,因为一个字符串所映射的一个位置和另一个字符串映射的位置重复了,那再与该字符串映射的其他位置同样重复的情况的可能性就很小了,所以可以有效降低误判率

我们知道:一个值映射的位越多,误判的概率就越低,但是映射的位如果太多,那么空间的消耗也就会越多

我们一般选择映射3个位置

而布隆过滤器的使用场景如果允许误判的情况,例如游戏中给角色起名时,将已经存在的名称存在布隆过滤器中,这样新用户起名时,如果存在,告知用户存在,如果不存在,就起名成功,效率是非常高的

而如果有场景不允许存在误判,那么就多一个步骤,如果存在布隆过滤器中,就去数据库中查找,确认在或不在,而如果不在布隆过滤器中,那就肯定不在,就不需要再去数据库中查找,也可以有效的提高效率


布隆过滤器的模拟实现

//N表示映射N个值,Hash1/2/3表示仿函数,即使用三种不同方式映射到不同地址
template<size_t N, class K, class Hash1, class Hash2, class Hash3>
class BloomFilter
{
public:void Set(const K& key){size_t hash1 = hash1()(key) % _ratio * N;_bits.set(hash1);size_t hash2 = hash2()(key) % _ratio * N;_bits.set(hash2);size_t hash3 = hash3()(key) % _ratio * N;_bits.set(hash3);}void Test(const K& key){//验证该位置是否存在时,因为一个值映射三个位置//所以不能一个位置满足就当做存在//是需要判断这三个位置是否都不存在//直到三个位置都判断之后才结束,才return true//虽然有误判率,但是误判率也是很小的size_t hash1 = hash1()(key) % (_ratio * N);if (!_bits.set(hash1))return false; //准确判断size_t hash2 = hash2()(key) % (_ratio * N);if (!_bits.set(hash2))return false; //准确判断size_t hash3 = hash3()(key) % (_ratio * N);if (!_bits.set(hash3))return false; //准确判断return true;      //可能误判}
private://这里的_ratio是使用公式大致算出来,在三个哈希函数时//需要多给大约5个比特位存储//所以N个数据,就开辟_ratio*N个空间const static size_t _ratio = 5;bitset<_ratio* N> _bits;
};

而关于布隆过滤器的删除操作,也就是reset,是不太建议的,因为一个数据可以对应多个位置,如果有两个数据对应了同一个位置,想删除一个,另一个也就被删除了

想解决这种问题,只能给每个位置在设置一个值,表示有几个数据映射到这个位置,删除一个数据就--,这样就不会有上面的问题了,但是这样相当于原本每个位置用一个比特位存储,现在每个位置又多了一个字节存储映射该位置的次数,这与布隆过滤器的初衷相违背了,本来布隆过滤器的优势就是空间占用小,效率高,这样空间也变大了,效率也低了,所以一般我们是不考虑删除操作的


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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Lua与C++交互

文章目录 1、Lua和C交互2、基础练习2.1、加载Lua脚本并传递参数2.2、加载脚本到stable&#xff08;包&#xff09;2.3、Lua调用c语言接口2.4、Lua实现面向对象2.5、向脚本中注册c的类 1、Lua和C交互 1、lua和c交互机制是基于一个虚拟栈&#xff0c;C和lua之间的所有数据交互都通…

SYS/BIOS 开发教程: 创建自定义平台

目录 SYS/BIOS 开发教程: 创建自定义平台创建自定义平台新建工程并指定自定义平台修改现有工程使用自定义平台 参考: TI SYS/BIOS v6.35 Real-time Operating System User’s Guide 6.2节 本示例基于 EVMC6678L 开发板, 创建自定义平台, 并将代码段的位置指定到C6678器件内部的…

安卓主板,人脸识别主板考勤门禁智能门锁安卓主板开发方案

人脸识别主板是一种广泛应用于多个领域的技术&#xff0c;包括人脸支付系统、人脸识别监控系统、写字楼办公楼门禁闸机、校园、地铁、住宅门禁、考勤机、智能门锁、广告机、售卖机以及其他行业应用设计等。这些主板基于联发科MTK方案&#xff0c;由行业PCBA和MTK的核心板组成。…

搭建zlmediakit和wvp_pro

zlmediakit使用zlmediakit/zlmediakit:master镜像 wvp_pro使用648540858/wvp_pro&#xff0c;可参照https://github.com/648540858/wvp-GB28181-pro wvp_pro官方https://doc.wvp-pro.cn/#/ 刚开始我找了个docker镜像运行&#xff0c;后来播放页面一直加载&#xff0c;最后就用了…

Windows下Eclipse C/C++开发环境配置教程

1.下载安装Eclipse 官网下载eclipse-installer&#xff08;eclipse下载器&#xff09;&#xff0c;或者官方下载对应版本zip。 本文示例&#xff1a; Eclipse IDE for C/C Developers Eclipse Packages | The Eclipse Foundation - home to a global community, the Eclipse ID…

自动化测试07Selenium01

目录 什么是自动化测试 Selenium介绍 Selenium是什么 Selenium特点 工作原理 SeleniumJava环境搭建 Selenium常用的API使用 定位元素findElement CSS选择语法 id选择器&#xff1a;#id 类选择 .class 标签选择器 标签名 后代选择器 父级选择器 自己选择器 xpath …

TeeChart for .NET 2023.10.19 Crack

TeeChart.NET 的 TeeChart 图表控件提供了一个出色的通用组件套件&#xff0c;可满足无数的图表需求&#xff0c;也针对重要的垂直领域&#xff0c;例如金融、科学和统计领域。 数据可视化 数十种完全可定制的交互式图表类型、地图和仪表指示器&#xff0c;以及完整的功能集&am…

debian、ubuntu打包deb包工具,图形界面deb打包工具mkdeb

debian、ubuntu打包deb包工具&#xff0c;图形界面deb打包工具mkdeb&#xff0c;目前版本1.0 下载地址&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1QX6jXNMYRybI9Cx-1N_1xw?pwd8888 md5&#xff1a; b6c6658408226a8d1a92a7cf93834e66 mkdeb_1.0-1_all.deb

听GPT 讲Rust源代码--library/std(2)

File: rust/library/std/src/sys_common/wtf8.rs 在Rust源代码中&#xff0c;rust/library/std/src/sys_common/wtf8.rs这个文件的作用是实现了UTF-8编码和宽字符编码之间的转换&#xff0c;以及提供了一些处理和操作UTF-8编码的工具函数。 下面对这几个结构体进行一一介绍&…

【学习笔记】Git开发流程

Git开发大致流程图&#xff1a; 具体流程&#xff1a; 首先一个从仓库的main分支&#xff0c;然后从main分支中拉一个功能分支feature/xxx&#xff0c;在多人开发这个功能的时候拉去自己的个人分支比如&#xff1a;xxx/xxx 。然后每天开发完个人分支后压缩commit&#xff0c;…

vue2.x封装svg组件并使用

第一步&#xff1a;安装svg-sprite-loader插件 <!-- svg-sprite-loader svg雪碧图 转换工具 --> <!-- <symbol> 元素中的 path 就是绘制图标的路径&#xff0c;这种一大串的东西我们肯定没办法手动的去处理&#xff0c; 那么就需要用到插件 svg-sprite-loader …

护眼灯有效果吗?五款好用热门的护眼台灯推荐

可以肯定的是&#xff0c;护眼灯一般可以达到护眼的效果。看书和写字时&#xff0c;光线应适度&#xff0c;不宜过强或过暗&#xff0c;护眼灯光线较柔和&#xff0c;通常并不刺眼&#xff0c;眼球容易适应&#xff0c;可以防止光线过强或过暗导致的用眼疲劳。如果平时生活中需…

12、Python -- if 分支 的讲解和使用

目录 程序结构顺序结构分支结构分支结构注意点不要忘记冒号 if条件的类型if条件的逻辑错误if表达式pass语句 程序流程 分支结构 分支结构的注意点 if条件的类型 if语句的逻辑错误 if表达式 程序结构 Python同样提供了现代编程语言都支持的三种流程 顺序结构 分支结构 循环结构…

django建站过程(3)定义模型与管理页

定义模型与管理页 定义模型[models.py]迁移模型向管理注册模型[admin.py]注册模型使用Admin.site.register(模型名)修改Django后台管理的名称定义管理列表页面应用名称修改管理列表添加查询功能 django shell交互式shell会话 认证和授权 定义模型[models.py] 模仿博客形式&…

Mysql如何理解Sql语句?MySql分析器

1. 什么是 MySQL 分析器? MySQL 分析器是 MySQL 数据库系统中的一个关键组件&#xff0c;它负责解析 SQL 查询语句&#xff0c;确定如何执行这些查询&#xff0c;并生成查询执行计划。分析器将 SQL 语句转换为内部数据结构&#xff0c;以便 MySQL 可以理解和执行查询请求。 …

全是干货!2023年双十一买什么最划算、双十一值得买的好物推荐

在双十一前选购到好物&#xff0c;打败99.99%的人&#xff01;看了下日历马上就要到一年一度的购物节了&#xff0c;双十一都想好买什么了吗朋友们&#xff1f;双十一购物狂欢即将来临&#xff0c;你是否已经开始准备购买自己心仪的商品&#xff1f;在这个购物狂欢节中&#xf…

【算法小课堂】深入理解前缀和算法

前缀和是指某序列的前n项和&#xff0c;可以把它理解为数学上的数列的前n项和&#xff0c;而差分可以看成前缀和的逆运算。合理的使用前缀和与差分&#xff0c;可以将某些复杂的问题简单化。 我们通过一个例子来理解前缀和算法的优势&#xff1a; 一维前缀和&#xff1a; ww…

Unity Spine 指定导入新Spine动画的默认材质

指定导入新Spine动画的默认材质 找到Spine的Editor导入配置如何修改方法一: 你可以通过脚本 去修改Assets/Editor/SpineSettings.asset文件方法二&#xff1a;通过面板手动设置 找到Spine的Editor导入配置 通常在 Assets/Editor/SpineSettings.asset 配置文件对应着 Edit/Prefe…

2018年亚太杯APMCM数学建模大赛B题人才与城市发展求解全过程文档及程序

2018年亚太杯APMCM数学建模大赛 B题 人才与城市发展 原题再现 招贤纳士是过去几年来许多城市的亮点之一。北京、上海、武汉、成都、西安、深圳&#xff0c;实际上都在用各种吸引人的政策来争夺人才。人才代表着城市创新发展的动力&#xff0c;因为他们能够在更短的时间内学习…

Zip密码忘记了,如何破解密码?

Zip压缩包设置了密码&#xff0c;解压的时候就需要输入正确对密码才能顺利解压出文件&#xff0c;正常当我们解压文件或者删除密码的时候&#xff0c;虽然方法多&#xff0c;但是都需要输入正确的密码才能完成。忘记密码就无法进行操作。 那么&#xff0c;忘记了zip压缩包的密…