C++ 位图与布隆过滤器

在这里插入图片描述

目录

  • 前言
  • 位图
    • 场景演示
    • 应用场景
    • 模拟实现
    • 问题
    • 例题
  • 布隆过滤器
    • 例子理解
    • 应用
  • 例题

前言

位图与布隆过滤器是用来在海量数据中判断一个数据在不在的问题的数据结构,这种数据结构在存储空间上大大的优于红黑树、哈希等数据结构

位图

我们为了处理一个数据在海量数据中在不在,那么我们的结果只有在与不在,我们令在为1,不在为0,也就是说对于每一个数据,我们只需要存储两个值,那么一个数据只需要一个bit位就够了

场景演示

在这里插入图片描述
比如这里我就有一个大小为21的位图,默认存储0,也就是没有数据,如果我们来了三个数据:2 7 19,那么位图就变成了下图
在这里插入图片描述
也就是把对应位映射成了1,表示对应位存在了数据,如果我们想要让数据消失,就只需要把对应位的内容映射为0即可

这里还需要注意一点:这里上面图的画是逻辑上的位图,不是物理内存存储的位图,因为物理内存会涉及到大小端的问题,也就是小端:低地址存低字节序,高地址存高字节序;大端:低地址存高字节序,高地址存低字节序。但是我们在使用位图是,只需要通过逻辑位图来理解即可,编译器会帮我们做好物理空间上的存储问题
在这里插入图片描述

应用场景

  1. 快速查找某个数据是否在一个集合中
  2. 去重并排序(位图大多数情况是用来处理在不在,如果已经在了,体现不出多个相同数据
//打印这些排序后的数据,实际的数据就是位图的下标,比特位表示的是在不在
for(int i = 0; i < N; i++) 
{if(bs.test(i)){cout << i << ' '; }
}
  1. 求两个集合的交集、并集等
//bs1是集合一的位图,bs2是集合二的位图
for(int i = 0; i < N; i++)
{//交集if(bs1.test(i) && bs2.test(i)){cout << i << " ";}
}for(int i = 0; i < N; i++)
{//并集if(bs1.test(i) || bs2.test(i)){cout << i << " ";}
}

模拟实现

我们的内置类型有int,char,float,double没有专门针对于位做操作的类型,当然这里会有人说bool类型,bool经过测试,每个bool变量,还是占了1个字节,也就是8个比特位
在这里插入图片描述
所以我们通常是通过int或者char用模运算和除运算来确定数据对应的比特位,我们下面使用int来模拟实现位图

位图通常的需求有三个:set() reset() test() 这三个函数的意义分别为:对应比特位映射为1,表示数据存在、对应比特位映射为0,表述数据不存在,查找对应比特位是否存在,所有的int数据都用vector来存储,注释即为代码解释,请自行阅读代码

//N为非类型模板参数,N表示的需要使用的位的个数,也就是我们这张位图能够存储的数据的范围 —— [0, N)
template<size_t N>
class bitset
{
public:bitset(){//计算出需要开辟的vector空间大小,也可以N >> 5 + 1来计算 - 右移相当于 /2//当然对于负数的右移,就不是简单的是/2的问题了,有些编译器是逻辑右移,有些编译器是算术右移,没有完全统一size_t size = N / 32 + 1;//比如N = 35,也就是需要35个比特位,N/32==1,空间不够,需要+1, 其实更节省空间的说法是 - N/32向上取整_a.resize(size, 0);}// 数据num映射的那个标记成1void set(size_t num){size_t index = num / 32;//num >> 5;size_t right_count = num % 32;_a[index] |= (1 << right_count);}// num映射的那个标记成0void reset(size_t num){size_t index = num / 32;size_t right_count = num % 32;_a[index] &= (~(1 << right_count));//右移然后取反,使得x映射的那个位一定在&后变成0,其他位的0、1结果完全与原始的0、1一致}// 测试num映射位置是0还是1bool test(size_t num){size_t index = num / 32;size_t right_count = num % 32;return _a[index] & (1 << right_count);//_a[index]中的第right_count位是0是1,是1就是true,是0就是false}private:vector<int> _a;//用一个vector来存储位图
};

完成了基础版的位图的模拟实现,此时我们就能够用一个int大小的数据去存储[0, 32)这32个数据,位图的N的大小越大,存储的数据范围越广

问题

可能有些读者会有疑惑,这种映射关系是否只能针对正整数?是否能够处理字符串?
对于负数,我们有两种处理方法
方法一:先遍历数据,找到最小的负数MIN,然后整体的数据加上这个MIN的绝对值,然后放到位图中,在后序查询时只需要把查询的数加上MIN进行查询即可,不过这种方法有致命缺陷,如果又插入了一个更小的负数,那么位图中的表示全部都乱了

方法二:用两张位图存储,第一张位图存储正数,第二张位图存储负数,具体操作就是遍历所有数据,如果是正数和零,放入第一张位图,如果是负数,取绝对值放入第二张位图,查询时判断正负,然后分别去对应的位图中寻找即可

方法三:遍历数据,找到两个最值MAX和MIN,我们开辟位图时开辟的大小变成MAX + MIN,每一个负数都取绝对值 + MAX,然后把结果放到位图中,在查询时,如果查询数小于0,就查询 查询数的绝对值 + MAX的结果对应的位图上是0还是1,如果查询数大于等于0,小于等于MAX,就正常查询,如果大于MAX,就直接return false;

方法三中,比如下图中,-1,绝对值 + MAX = 9,所以位图中的9位置为1,|-4| + MAX = 12,所以位图中的12位置为1
在这里插入图片描述
方法二、三是比较推荐的

对于字符串的处理,所有的字符,不管是中文英文,都是有对应编码的,比如ASCII码,Unicode的UTF-8、UTF-16,也就是说,这些字符串都可以转换成整形,不过这里就会有个问题,不同的字符串,转换成整形是有可能相同的,也就是发生冲突,所以为了降低这种相同的可能性,前人设计了很多种字符串哈希算法,这里给了一个字符串哈希算法的网址:https://www.cnblogs.com/-clq/archieve/2012/05/31/2528153.html,可以自行了解阅读,简单来说,我们的字符串,通过字符串哈希算法的处理,都能够变成一个整形,然后放到位图中

不过这种方法在实际中还是有不少冲突的问题,所以就有了布隆过滤器,来降低冲突问题

例题

1、给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中?
40亿不重复的无符号整数,如果是在32位机上,无符号整数是4个字节,也就是总共160亿字节的数据,如果用set处理,其底层是红黑树,除了这些数据key,还需要存储,left、right、parent、colour,这些也都是4字节的,那么总共下来就是160 * 5 = 800亿字节的数据

1G = 2^10MB = 2^20KB = 2^30Byte,2^30字节就是10亿字节,也就是说:1G等于10亿字节,那么上面这道题如果用set存储,那么就需要80个G,想想就可怕是吧,内存就只有8个G,16个G,真要set处理,那只能分成小文件处理,后面会有其他题具体讲解此方法。

所以这里最好就用位图来处理,这40亿的不重复数据,也就是至少需要40亿的位图大小,但是我们并不知道最大的数据是多少,所以直接给一个顶配的位图,直接把无符号数覆盖满,也就是给一个2^32个比特空间大小的位图,也就是2^29个字节大小的位图,1G = 2^30字节,也就是说这个位图的大小为500MB,这个大小我们就能够承受,这个数据量大多数电脑就能够轻松拿捏了,对于template< size_t N >这个N的设置有多种方式,bitset< 0xFFFFFFFF > bitset< -1 >都可,只不过需要注意的是,库中提供的bitset给了很多限制,怕我们乱开空间,我们可以用自己写的位图来开辟这么大的空间处理问题

2、给定100亿个整数,设计算法找到只出现一次的整数
有两个方法:
方法一:我们的位图每一个位只能存储在不在,我们修改位图的存储结构 —— 每两个比特位,存储一个数据的信息,此时我们对于一个数据,就能够存储四种信息,比如我们可以规定:00 - 没有数据, 01 - 有一个数据,10 - 有两个以上的数据
在这里插入图片描述
比如上面这个修改后的位图,存储的信息如下:1个0,多个1,多个2,0个3,多个4,1个5,1个6,0个7
这张位图的实现上需要修改的就是,查询、插入整数num时对应操作的比特位需要×2,此时对应的比特位,才是存储信息的第一位,下一位就是第二位,我们在操控信息时,也就是处理:_a[num * 2 / 32]里的1 << (num * 2 % 32) 1 << ((num * 2 % 32) + 1),因为int的bit位是偶数,所以(num % 32)并不会因为 + 1跑到下一个int中

方法二:用两张位图处理,两张位图的相同位置一起组成两个位,同样也是00 - 该没有数据, 01 - 有一个数据,10 - 有两个以上的数据,这样的存储模式,这就可以不用修改位图了,只需要封装一个针对这个题的新位图

template<size_t N>
class twicebitset
{
public:void set(size_t num){//_bt1 _bt2//  0   0    ->该bit位没有值//  0   1    ->该bit位有一个值//  1   0    ->该bit位有两个及以上个值if (!_bt1.test(num) && !_bt2.test(num))//如果bt1和bt2的num的位为0,那么这里会进去{_bt2.set(num);//设置num位}else if (!_bt1.test(num) && _bt2.test(num))// 01 会进去{_bt1.set(num);_bt2.reset(num);}else//10不会处理,不会有11的情况{;}}void reset(size_t num){_bt1.reset(num);_bt2.reset(num);}bool is_once(size_t num){return !_bt1.test(num) && _bt2.test(num);//01 -> 唯一值}private:bitset<N> _bt1;//高位bitbitset<N> _bt2;//低位bit
};

3、给两个文件,分别有100亿个整数,我们只有1G内存,如何找到这两个文件的交集?
我们这里需要明确,这种海量数据的情况下位图所开大小和数据量无关,只与数据类型有关,这100亿数据中必定会有重复数据,这些都是整数,直接开bitset< -1 >大小的位图空间就可以了,也就是大概500MB左右,读入第一个文件的数据到位图中,另一个文件的数据在位图中寻找,寻找的结果放到unordered_set< int >容器中,因为可能存在重复值,用set容器去重,并且用unordered效率更高一些,除此之外,这100亿个在文件中的整数,并不是一次性放到内存中处理,而且分批次放到内存中处理,比如一次处理1亿个整数,也就是250MB 这样就能够在1G内存来处理这些数据

布隆过滤器

上面我们讲到了一个例子,是通过哈希函数把字符串转成整形,这种方式是有可能出现两个不同的字符串映射到同一个整形,导致误判的,所以就使用到了布隆过滤器,来降低这种误判的风险

布隆过滤器是一种概率性的数据结构,是对一个字符串使用多个哈希函数,在位图中映射到多个位置,当在检测在不在的时候,只有被检测的字符串在多个哈希函数的处理下,都完全映射到位图中被映射过的位置,此时才能判断出,被检测字符串在位图中。

例子理解

下图即是:百度、腾讯、阿里,通过三个哈希函数,映射到了位图中
在这里插入图片描述
然后来了一个网易
1、如果网易通过相同的三个哈希函数,映射到了下图位置中
在这里插入图片描述
也就是通过布隆过滤器,发现网易是在位图中的,也就发生了误判,这是一种情况,也是我们不想看到的情况

2、如果映射成了:
在这里插入图片描述
也就是网易通过三个哈希,映射到了位图中为0的位置,此时就判断,网易一定不在位图中,这个判断是一定正确的

有人会说,这样有没有可能比单个哈希函数的误判率更高?
我认为,如果位图开的过小,是有可能的,因为存储大量的值后,可能位图都快满了,怎么检测,都是1,但是,在实际中,位图开的空间是很大的,始终让位图保持在一个稀疏状态,在存储阶段,像百度和腾讯通过哈希函数映射到相同的位图位置的概率会很小,检测阶段,被检测字符串能够通过多个哈希函数映射到都为1的位置概率也会变的很小

//N表示的需要使用的位的个数,也就是我们这张位图能够存储的整形范围 —— [0, N)
template<size_t N>
class bitset
{
public:bitset(){//计算出需要开辟的vector空间大小,也可以N >> 5 + 1来计算 - 右移相当于 /2//当然对于负数的右移,就不是简单的是/2的问题了,有些编译器是逻辑右移,有些编译器是算术右移,没有完全统一size_t size = N / 32 + 1;//比如N = 35,也就是需要35个比特位,N/32==1,空间不够,需要+1, 其实更节省空间的说法是 - N/32向上取整_a.resize(size, 0);}// 数据num映射的那个标记成1void set(size_t num){size_t index = num / 32;//num >> 5;size_t right_count = num % 32;_a[index] |= (1 << right_count);}// num映射的那个标记成0void reset(size_t num){size_t index = num / 32;size_t right_count = num % 32;_a[index] &= (~(1 << right_count));//右移然后取反,使得x映射的那个位一定在&后变成0,其他位的0、1结果完全与原始的0、1一致}// 测试num映射位置是0还是1bool test(size_t num){size_t index = num / 32;size_t right_count = num % 32;return _a[index] & (1 << right_count);//_a[index]中的第right_count位是0是1,是1就是true,是0就是false}
private:vector<int> _a;//用一个vector来存储位图
};template<size_t N>
class BloomFilter
{
private:size_t BKDRHash(const string& str){size_t hash = 0;size_t i = 0;int ch = 0;while (ch = str[++i]){hash = hash * 131 + ch;   }return hash;}size_t SDBMHash(const string& str){size_t hash = 0;size_t i = 0;int ch = 0;while (ch = str[++i]){hash = 65599 * hash + ch;//hash = (size_t)ch + (hash << 6) + (hash << 16) - hash;  }return hash;}size_t RSHash(const string& str){size_t hash = 0;size_t magic = 63689;size_t i = 0;int ch = 0;while (ch = str[++i]){hash = hash * magic + ch;magic *= 378551;}return hash;}public:void set(const string& str){_bt.set(BKDRHash(str) % N);_bt.set(SDBMHash(str) % N);_bt.set(RSHash(str) % N);}//void reset();//不能有reset,因为一个位图位置,不一定属于那一个人bool test(const string& str){return _bt.test(BKDRHash(str) % N) && _bt.test(SDBMHash(str) % N) && _bt.test(RSHash(str) % N);}private:bitset<N> _bt;
};

应用

通过前面的例子,需要知晓的,布隆过滤器对于判断不在是完全准确的,对于判断在是不准确的
所以应用场景应该是针对不要求完全准确的场景,比如快速判断昵称是否被注册过?如果没有被注册过,就能进行后序操作,如果注册过,就让用户更换昵称,这里的逻辑就比较符合布隆过滤器,昵称没有被注册过,对应的就是通过布隆过滤器检测为不在,这是准确的,而对于昵称存在,让用户更换昵称,这个不是准确的(新昵称指向了位图中的三个标注为1的位图位置,但是这个昵称并没有被用过),但是没有关系,反正用户不知道,所以这里就要求,布隆过滤器的误判率需要小于5%?3%?这是根据需求来说的。
在这里插入图片描述

当然这种场景也能够做到精准,只不过会牺牲一定的效率,我们的所有数据还是首先通过布隆过滤器进行判断,如果是没有被注册过,这是准确的,不用管别的了,如果是被注册过,就去数据库中寻找,如果找到了,提示:该昵称被注册过了,如果没找到,那么也就是没有被注册过,提示:改昵称没有被注册过

简单来说:降低数据库的查询负载压力,提高效率

除了上面的应用还有利用布隆过滤器减少磁盘IO或者网络请求,一旦一个值必定不存在,我们就可以不用进行后续消耗较大的查询请求

例题

1、给定两个文件,分别有100亿个query(查询),只有1G内存,如何找到两个文件的交集?精确算法和近似算法
query是查询,一般是针对数据库中的信息进行查询,比如查询字符串,假设查询的字符串平均长度是30byte,那么这里就是3000Byte,也就是300G的数据量(10亿Byte = 1G)
因为内存一般只有8G、16G,和位图的例题部分的第三个问题相同,也需要把这100亿个查询,分成多个小文件,比如分成1000个小文件,那么最简单的思路就是,第一个文件中的一个小文件分别与另一个文件中的1000个小文件进行比较,找出这一个小文件与第二个文件的交集,这么做1000次,然后进行去重,结果就是这两个文件的交集,总共会有1000 * 1000次小文件之间的比较

但是,这种方式的效率非常低,有很多重复动作,所以,我们需要进行改进,这1000个小文件,我们可以进行预处理 —— 哈希切分
我们使用一个哈希函数,去处理query字符串,然后 % 1000,( i = hash_fun(query) % 1000; // i是小文件编号 )其结果就被映射到了i ∈ [0, 1000),然后我们就把这个query放到对应的小文件中,这样我们把两个文件处理成了2000个小文件,比如:第一个文件中的i ==0的小文件和第二个文件中i == 0的文件找交集,第一个文件的i == 4的和第二个文件i == 4找交集,这样得到的结果再进行去重,去重后的就是我们需要的结果(两个文件中相同的query,一定会经过哈希函数 和 % 1000之后,分别进入两个对应的小文件,我们只需要对应的小文件进行查找,有一点点哈希桶的味道,相同特点的数据进入一个桶中,只需要在这个桶里查询值),总共会有1000次小文件之间的比较,大幅度的提高了效率
在这里插入图片描述

这300G的文件的读取是一部分一部分读取,把一些query数据放到缓冲区中(把磁盘一个扇区的数据读到缓冲区),然后把缓冲区中的数据读取到出来进行处理,处理完后马上就写入到对应的小文件中去,所以预处理这300G的文件是不需要消耗太多的内存的,主要的消耗是IO消耗,即读写的消耗,以及会有一些CPU的消耗。CPU密集型也就是计算密集性,IO密集性也就是读写密集性,这里是IO密集性

这里需要注意,缓冲区是内存与外设进行交换时的一块内存区域,而缓存命中这个概念的缓存是CPU高速缓存,不是内存中的缓存

但是,这里还有问题,如果大量的query字符串预处理阶段,都进入了同一个小文件(i = hash_fun(query) % 1000; 处理后某个 i 下的query过多,“桶太长”),就会导致小文件过大,超过800MB,1G …
小文件过大的来源有二:
一:有大量的重复query
二:大量的不同字符串通过hash %1000,得到相同的 i (发生冲突)

对于情况二,比较好处理,可以更换hash函数,再切分
对于情况一:换hash函数没有作用,都还是会进入同一个小文件

我们是不知道是情况一还是情况二的
所以真正的解决方案:还是先把小文件的query读到内存中的一个数据容器中(set、unordered_set),如果insert时,报错抛bad_alloc异常,那么说明是情况一,此时就换一个哈希函数,再进行哈希切分;如果能够正常全部读到数据容器中,说明是情况一或者就是普通的小文件,这个小文件就处理完成了,只需要接着对另一个文件中的对应小文件进行读入数据容器

对于上面两个小文件的精确算法,就使用set数据容器,而对于近似算法,就还是使用布隆过滤器

思考:布隆过滤器是否支持删除操作?
不支持,因为一个布隆过滤器中的一个位,可能同时是"阿里巴巴"和"百度"通过哈希函数映射过去的,所以,我们针对阿里巴巴的删除操作,就会影响到百度,所以不支持

2、给一个超过100G大小的log file,log中存IP地址,设计算法,找到出现次数最多的IP?那出现次数最多的K个IP地址呢?
第一步同样,先用哈希切分,把这100多个G的log file切分成500个,1000个小文件,大概平均一份就是100MB,200MB,当然这里还是会出现问题一中的问题,还是相同的解决办法,这里就不赘述了

这里每个小文件中都有一些重复的IP,我们就是要从这几百个小文件中找到最大的那一个,那么我们就用map分别统计每个小文件中每个IP出现的次数,并用一个pair<string, int>变量不断更新记录各个每个小文件中的出现次数最多的IP

对于出现次数最多的K个IP地址,就使用priority_queue数据结构,建一个大小为K的小堆,先随便放入K个数据,然后让每个小文件中的数据和堆顶数据进行比较,如果堆顶数据小于外部数据,那么堆顶数据pop掉,外部数据入堆,这么扫过所有小文件,最后堆中的数据就是最后的结果

3、如何扩展布隆过滤器使得它支持删除操作呢?
要完成删除操作,就需要进行计数,计数将不可避免的会消耗一定的空间
具体的操作方式是,原来我们对于每一个bit位进行判断是否存在扩展为每四个bit位,每六个bit位,也就是将每一个位扩展为了一个小计数器,四位就允许冲突15次,六位就允许冲突63次,如果超出冲突次数,还是无法完成删除操作,所以为了保证完成删除操作,只能尽可能扩大位图大小,减少冲突可能性,所以删除操作是不稳定的,一般也不会要求布隆过滤器做删除操作。

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

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

相关文章

SSM整合RabbitMQ,Spring4.x整合RabbitMQ

SSM整合RabbitMQ目录 前言版本实现目录参考pom.xml依赖rabbitmq.properties配置文件spring-rabbitmq.xmlspring-mvc.xml或applicationContext.xmlrabbitmq目录下MessageConsumer.javaMessageConsumer2.javaMessageProducer.javaMessageConstant.java 测试调用 扩展消息重发方式…

PG14归档失败解决办法archiver failed on wal_lsn

问题描述 昨晚RepmgrPG14主备主库因wal日志撑爆磁盘&#xff0c;删除主库过期wal文件重做备库后上午进行主备状态巡查&#xff0c;主库向备库发送wal文件正常&#xff0c;但是查主库状态时发现显示有1条归档失败的记录。 postgres: archiver failed on 000000010000006F000000…

甘特图组件DHTMLX Gantt示例 - 如何有效管理团队工作时间?(一)

如果没有有效的时间管理工具&#xff0c;如工作时间日历&#xff0c;很难想象一个项目如何成功运转。这就是为什么我们的开发团队非常重视项目管理&#xff0c;并提供了多种选择来安排DHTMLX Gantt的工作时间。使用DHTMLX Gantt这个JavaScript库&#xff0c;您可以创建一个强大…

Java架构师缓存架构设计解决方案

目录 1 缓存常见的三大问题1.1 缓存雪崩1.2 缓存穿透1.3 缓存击穿2 缓存key的生成策略3 热点数据集中失效的问题4 如何提高缓存的命中率5 缓存和数据库双写不一致的问题6 如何对缓存数据进行分片1 缓存常见的三大问题 缓存雪崩、缓存穿透和缓存击穿是缓存系统中常见的三大问题,…

日常学习记录随笔-大数据之日志(hadoop)收集实战

数据收集(nginx)--->数据分析---> 数据清洗--->数据聚合计算---数据展示 可能涉及到zabix 做任务调度我们的项目 电商日志分析 比如说我们现在有一个系统,我们的数仓建立也要有一个主题 我这个项目是什么我要干什么定义方向 对用户进行分析,用户信息 要懂整个数据的流…

理解LoadRunner,基于此工具进行后端性能测试的详细过程(上)

1、LoadRunner 的基本原理 后端性能测试工具通过虚拟用户脚本生成器生成基于协议的虚拟用户脚本&#xff0c;然后根据性能测试场景设计的要求&#xff0c;通过压力控制器控制协调各个压力产生器以并发的方式执行虚拟用户脚本&#xff0c;并且在测试执行过程中&#xff0c;通过…

【手写数字识别】数据挖掘实验二

文章目录 Ⅰ、项目任务要求任务描述&#xff1a;主要任务要求(必须完成以下内容但不限于这些内容)&#xff1a; II、实现过程数据集描述实验运行环境描述KNN模型决策树模型朴素贝叶斯模型SVM模型不同方法对MNIST数据集分类识别结果分析(不同方法识别对比率表及结果分析) 完整代…

2023最新pytorch安装教程,简单易懂,面向初学者(Anaconda+GPU)

一、前言 目前是2023.1.27,鉴于本人安装过程中踩得坑&#xff0c;安装之前我先给即将安装pytorch的各位提个醒&#xff0c;有以下几点需要注意 1.判断自己电脑是否有GPU 注意这点很重要&#xff0c;本教程面向有NVIDA显卡的电脑&#xff0c;如果你的电脑没有GPU或者使用AMD显…

2023-10-12 LeetCode每日一题(找出数组的串联值)

2023-10-12每日一题 一、题目编号 2562. 找出数组的串联值二、题目链接 点击跳转到题目位置 三、题目描述 给你一个下标从 0 开始的整数数组 nums 。 现定义两个数字的 串联 是由这两个数值串联起来形成的新数字。 例如&#xff0c;15 和 49 的串联是 1549 。 nums 的 串…

QT运行界面与画布大小不一致问题(一步到位)

QT运行界面与画布大小不一致问题 出现的问题直接设置环境变量main函数中输入以下代码更改系统缩放比好了&#xff0c;看一下运行结果吧 出现的问题 当我们运行程序时&#xff0c;发现运行出来的大小和设计的几面大小有很大的差别&#xff0c;这使我们开发起来就特别的困难&…

iOS 面试题以及自我理解答案

1、简述push原理&#xff0c;push的证书和其他的有什么不一样&#xff1f; 第 一阶段&#xff1a;BeejiveIM服务器把要发送的消息、目的iPhone的标识打包&#xff0c;发给APNS。 第二阶段&#xff1a;APNS在自身的已注册Push服务 的iPhone列表中&#xff0c;查找有相应标识的iP…

数字孪生技术:新零售的未来之路

随着科技的不断进步&#xff0c;新零售产业正经历着巨大的变革。数字孪生作为一种新兴技术正在加速这一变革的进程。它不仅为新零售企业带来了更高效的运营方式&#xff0c;还为消费者提供了更个性化、便捷的购物体验。那么&#xff0c;数字孪生技术究竟如何在新零售产业中发挥…