【C++】哈希表

news2024/11/16 22:51:13

1. unordered系列关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 ,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。

最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,本文中只对unordered_map和unordered_set进行介绍。(unordered_multimap和unordered_multiset 使用的不多,用法与multimap和multiset使用类似)

1.1. unordered_map

1.1.1. 使用介绍

unordered_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。
在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。
在内部,unordered_map没有对<kye, value>按照任何特定的顺序排序, 为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。
unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
它的迭代器至少是前向迭代器。

unordered_map使用文档

1.1.2. 接口说明

  1. unordered_map的构造

函数声明

功能介绍

unordered_map

构造不同格式的unordered_map对象

  1. unordered_map的容量

函数声明

功能介绍

bool empty() const

检测unordered_map是否为空

size_t size() const

获取unordered_map的有效元素个数

  1. unordered_map的迭代器

函数声明

功能介绍

begin

返回unordered_map第一个元素的迭代器

end

返回unordered_map最后一个元素下一个位置的迭代器

cbegin

返回unordered_map第一个元素的const迭代器

cend

返回unordered_map最后一个元素下一个位置的const迭代器

  1. unordered_map的元素访问

函数声明

功能介绍

operator[]

返回与key对应的value,没有一个默认值

注意:该函数中实际调用哈希桶的插入操作,用参数key与V()构造一个默认值往底层哈希桶中插入,如果key不在哈希桶中,插入成功,返回V(),插入失败,说明key已经在哈希桶中,将key对应的value返回。

  1. unordered_map的查询

函数声明

功能介绍

iterator find(const K& key)

返回key在哈希桶中的位置

size_t count(const K& key)

返回哈希桶中关键码为key的键值对的个数

注意:unordered_map中key是不能重复的,因此count函数的返回值最大为1

  1. unordered_map的修改操作

函数声明

功能介绍

insert

向容器中插入键值对

erase

删除容器中的键值对

void clear()

清空容器中有效元素个数

void swap(unordered_map&)

交换两个容器中的元素

  1. unordered_map的桶操作

函数声明

功能介绍

size_t bucket_count()const

返回哈希桶中桶的总个数

size_t bucket_size(size_t n)const

返回n号桶中有效元素的总个数

size_t bucket(const K& key)

返回元素key所在的桶号

1.2. unordered_set

其功能与接口和unordered_map类似,这里就不再详细列举出其使用方法和接口。

详细请参考unordered_set使用手册

1.3. unordered系列容器与map和set的区别

unordered系列遍历不按key排序
unordered系列的迭代器全部是单向迭代器
unordered系列综合效率略胜map和set

map和set对key的要求:需要key能够支持比较大小,如果不能比较,必须要显式的传入比较仿函数。

unordered_map、unordered_set对key的要求:需要key能够支持取模或者转化成取模的无符号整数;需要key能够比较是否相等,或者传入仿函数。

2. 哈希表

unordered_map和unordered_set的增删查改的效率都为O(1),是非常快的,是因为它的底层结构使用了哈希表。

2.1. 哈希

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

插入元素

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

搜索元素对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

例如:数据集合{1,7,6,4,5,9};哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小

2.2. 哈希冲突

例如:我们使用哈希的方法向顺序表中插入整数,顺序表的容量为10,先插入15,那么插入到数组下标为5的位置,如果再插入25,怎么办?

类似上面这种情况,将不同的元素通过哈希函数映射到了哈希表的相同位置,就是哈希冲突。

2.3. 哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理哈希函数设计原则

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

常用哈希函数1. 直接定制法取关键字的某个线性函数为散列地址:**Hash(Key)= A*Key + B** 优点:简单、均匀 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况 面试题:字符串中第一个只出现一次字符

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

2.4. 哈希冲突解决

解决哈希冲突两种常见的方法是:闭散列开散列

2.4.1 闭散列

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

  1. 线性探测

比如上面举的例子,现在需要插入元素25,先通过哈希函数计算哈希地址,hashAddr为5,因此25理论上应该插在该位置,但是该位置已经放了值为15的元素,即发生哈希冲突。线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

  1. 二次探测

发生哈希冲突后寻找下一位空位置的方法为:Hi = (H0+i^2)%m (i=1,2,3...),H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。

线性探测优点:简单。

线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。

二次探测优点:缓解线性探测的数据堆积问题。

二次探测缺点:空间利用率较低。

还有一个有效解决哈希冲突的方法是扩容,这样能将部分数据的映射位置分开。

注意:

采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素直接删除元素会影响其他元素的搜索 。

实际的处理方法是,给每一个位置做一个标记位,通过改变标记位来控制删除。(例如:empty为空,exist为存在,delete为删除)

2.4.2 开散列

开散列概念

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

这样就能很好的解决哈希冲突的问题,实际哈希表在实现时也是使用的这种方法。

3. 位图

给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。【腾讯】

将所有数据存放在位图中,数据是否在给定的位图中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。节省了大量空间。

3.1. 概念

所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。

3.2. 位图的实现

#pragma once
#include <vector>

namespace bit
{
    template<size_t N>
    class bitset
    {
    public:
        bitset()
        {
            _bits.resize(N / 8 + 1, 0); //为了避免存入数据的个数不是8的整数倍,所以多开一开字节
        }

        void set(size_t x)
        {
            size_t i = x / 8; // 找到存入的值在哪个字节
            size_t j = x % 8; // 找到在该字节中的具体位置

            _bits[i] |= (1 << j); // 将该位置置为1
        }

        void reset(size_t x)
        {
            size_t i = x / 8;
            size_t j = x % 8;

            _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:
        std::vector<char> _bits; // 每个字段8个比特位
        //std::vector<int> _bits;
    };

3.3. 位图的应用

  1. 快速查找某个数据是否在一个集合中

  1. 排序

  1. 求两个集合的交集、并集等

  1. 操作系统中磁盘块标记

4. 布隆过滤器

如果遇到数据量非常大的问题,虽然有些可以使用位图解决,但是位图的使用非常局限,即位图中只能存整数,那么如果遇到字符串、自定义类型怎么办?

4.1. 布隆过滤器概念

布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。

4.2. 布隆过滤器的实现

由于布隆过滤器的底层也是由位图去实现的,那么就出现了一个问题,位图中的映射关系是直接映射,那么如果使用哈希函数将其他类型转化成整数就必然存在哈希冲突问题。

所以为了解决位图中的哈希冲突,布隆过滤器中使用了多个哈希函数,将转化的整数形成多个映射,存放在位图的不同位置。(当然这种方式不能完全解决哈希冲突)

#pragma once

#include <bitset>
#include <string>
#include <time.h>

struct BKDRHash
{
    size_t operator()(const string& s)
    {
        // BKDR
        size_t value = 0;
        for (auto ch : s)
        {
            value *= 31;
            value += ch;
        }
        return value;
    }
};

struct APHash
{
    size_t operator()(const string& s)
    {
        size_t hash = 0;
        for (long i = 0; i < s.size(); i++)
        {
            if ((i & 1) == 0)
            {
                hash ^= ((hash << 7) ^ s[i] ^ (hash >> 3));
            }
            else
            {
                hash ^= (~((hash << 11) ^ s[i] ^ (hash >> 5)));
            }
        }
        return hash;
    }
};

struct DJBHash
{
    size_t operator()(const string& s)
    {
        size_t hash = 5381;
        for (auto ch : s)
        {
            hash += (hash << 5) + ch;
        }
        return hash;
    }
};

template<size_t N,
size_t X = 8,
class K = string,
class HashFunc1 = BKDRHash,
class HashFunc2 = APHash,
class HashFunc3 = DJBHash>
class BloomFilter
{
public:
    void Set(const K& key)
    {
        size_t len = X*N;
        size_t index1 = HashFunc1()(key) % len;
        size_t index2 = HashFunc2()(key) % len;
        size_t index3 = HashFunc3()(key) % len;
    /*    cout << index1 << endl;
        cout << index2 << endl;
        cout << index3 << endl<<endl;*/


        _bs.set(index1);
        _bs.set(index2);
        _bs.set(index3);
    }

    bool Test(const K& key)
    {
        size_t len = X*N;
        size_t index1 = HashFunc1()(key) % len;
        if (_bs.test(index1) == false)
            return false;

        size_t index2 = HashFunc2()(key) % len;
        if (_bs.test(index2) == false)
            return false;

        size_t index3 = HashFunc3()(key) % len;

        if (_bs.test(index3) == false)
            return false;

        return true;  // 存在误判的
    }

    // 不支持删除,删除可能会影响其他值。
    void Reset(const K& key);
private:
    bitset<X*N> _bs;
};

注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判。

为了降低布隆过滤器的误判问题,可以适当增大位图的大小。

4.3. 布隆过滤器删除

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,如果该位置也可能映射有其他元素,可能就会影响其他元素。

一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作

缺陷:

  1. 无法确认元素是否真正在布隆过滤器中

  1. 存在计数回绕

4.3. 布隆过滤器优点

  1. 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关

  1. 哈希函数相互之间没有关系,方便硬件并行运算

  1. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势

  1. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势

  1. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能

  1. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算

4.4. 布隆过滤器缺陷

  1. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)

  1. 不能获取元素本身

  1. 一般情况下不能从布隆过滤器中删除元素

  1. 如果采用计数方式删除,可能会存在计数回绕问题

布隆过滤器使用场景:数据量大,节省空间,允许误判,这样的场景,就可以使用布隆过滤器。

如:注册时,输入昵称时,将数据库中的昵称放入布隆过滤器中检查是否存在。(昵称不存在时不会发生误判,如果存在可能会发生误判,但是不会影响数据库中的数据(即误判了本来不存在的昵称为存在))

5. 海量数据面试题

5.1. 哈希切割

给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?

哈希切分:

(1)依次读取IP,i = BKDRHash(ip)%200(n),i是多少ip就进入对应编号的小文件中,相同的ip就一定进入了同一个小文件,然后使用map统计一个小文件的ip的次数,就是这个ip准确的次数。

(2)使用priority_queue,将map中的每个ip插入小堆中,即可统计出来。

5.2. 位图应用

1.给定100亿个整数,设计算法找到只出现一次的整数?

使用两个位图,将两个位图相同的位置用作一个整数的计数(00:未出现,01:出现一次,10:出现两次及以上),这样只找01位置的数即可。

代码:

template<size_t N>
class TwoBitSet
{
public:
    void Set(size_t x)
    {
        if (!_bs1.test(x) && !_bs2.test(x)) // 00 -> 01
        {
            _bs2.set(x);
        }
        else if (!_bs1.test(x) && _bs2.test(x)) // 01 -> 10
        {
            _bs1.set(x);
            _bs2.reset(x);
        }
        // 10 表示已经出现2次或以上,不用处理
    }

    void PrintOnceNum()
    {
        for (size_t i = 0; i < N; ++i)
        {
            if (!_bs1.test(i) && _bs2.test(i)) // 01
            {
                cout << i << endl;
            }
        }
    }
private:
    bit::bitset<N> _bs1;
    bit::bitset<N> _bs2;
};

2.给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?

把一个文件中的整数set到位图bs1中,另一个文件set到位图bs2中。

  1. 遍历bs1中的值,然后看这个值在不在bs2中。

  1. 将两个位图相与,得到的结果位图中,为1的位置就是交集。

3.位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数

同第一题一样,使用两个位图,00:未出现,01:出现1次,10:出现2次,11:出现3次及以上。

5.3. 布隆过滤器

1.给两个文件,分别有100亿个query(查询),我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法。

近似算法:将一个文件放入布隆过滤器,遍历另一个文件,看它在布隆过滤器中有没有,如果存在就是交集。(存在误判,会导致不是交集的query也被找出来)

精确算法:哈希切分:分别读取两个的query,使用哈希算法(如:i = BKDRHash(query)%200),得到的结果分别放入Ai、Bi号小文件,这样相同的文件就会被放进相同编号(i)的文件中,然后相同的编号文件进行比较即可。(如果单个小文件太大,超过内存,可以考虑换个哈希算法,再切分一次)

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

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

相关文章

TCP/IP网络协议介绍及原理分析

一.应用层协议对于应用层而言&#xff0c;协议是开发者自己进行定义的&#xff0c;开发者根据自定义的格式规范对数据进行编码和解析。但是从原理上进行分析&#xff0c;其核心主要包括两点内容&#xff1a;①确定客户端和服务端交互的内容&#xff08;协议的内容&#xff09;②…

记一次docker虚拟机横向移动渗透测试

本次渗透在几个docker虚拟机间多次横向移动&#xff0c;最终找到了一个可以进行docker逃逸的出口&#xff0c;拿下服务器。渗透过程曲折但充满了乐趣&#xff0c;入口是172.17.0.6的docker虚拟机&#xff0c;然后一路横向移动&#xff0c;最终在172.17.0.2出实现了docker逃逸&a…

【免费教程】地下水环境监测技术规范HJ/T164-2020解读使用教程

地下水环境监测技术规范依据《中华人民共和国环境保护法》第十一条“国务院环境保护行政主管部门建立监测制度、制订监测规范”和《中华人民共和国水污染防治法》的要求&#xff0c;积极开展地下水环境监测&#xff0c;掌握地下水环境质量&#xff0c;保护地下水水质&#xff0…

常青科技冲刺A股上市:研发费用率较低,关联方曾拆出资金达1亿元

近日&#xff0c;江苏常青树新材料科技股份有限公司&#xff08;下称“常青科技”或“常青树科技”&#xff09;递交招股书&#xff0c;准备在上海证券交易所主板上市。本次冲刺上市&#xff0c;常青科技计划募资8.50亿元&#xff0c;光大证券为其保荐机构。 据招股书介绍&…

我的 System Verilog 学习记录(4)

引言 本文简单介绍 System Verilog 语言的 数据类型。 前文链接&#xff1a; 我的 System Verilog 学习记录&#xff08;1&#xff09; 我的 System Verilog 学习记录&#xff08;2&#xff09; 我的 System Verilog 学习记录&#xff08;3&#xff09; 数据类型简介 Sys…

Linux:共享内存api使用

代码&#xff1a; #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h> #include <arpa/inet.h> #include <sys/un.h> #include <sys/ipc.h…

Codeforces Round #849 (Div. 4)(E~G)

A~D比较简单就不写了&#xff0c;哎嘿E. Negatives and Positives给出一个数组a&#xff0c;可以对数组进行若干次操作&#xff0c;每次操作可以将相邻的两个数换为它们的相反数&#xff0c;求进行若干次操作之后能得到数组和的最大值是多少。思路&#xff1a;最大的肯定是把负…

VSCode+Qt+MinGW开发环境搭建

VSCodeQtMinGW开发环境搭建 概述 VSCode扩展性很强&#xff0c;插件机制让其具备不断演进的潜力&#xff0c;适合作为稳定的开发工具。 VSCodeQt开发环境的搭建需要依赖于以下工具&#xff1a; VSCode、Qt&#xff0c;其中Qt需要安装MinGW编译工具&#xff1b;VSCode插件&a…

常年霸榜TK彩妆类目,看Focallure菲鹿儿如何玩转出海市场

据市场调研机构弗若斯特沙利文数据报告&#xff0c;2017年至2021年&#xff0c;中国化妆品市场规模由6305亿元增长至9468亿元&#xff0c;年均复合增长率为10.7%&#xff0c;报告预计2023年中国化妆品市场规模将达11414亿元&#xff0c;今后几年的增速将逐渐放缓。随着国内市场…

LeetCode刷题系列 -- 112. 路径总和

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径&#xff0c;这条路径上所有节点值相加等于目标和 targetSum 。如果存在&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。叶子节点 是指没有子节点的…

外置的媒体查询,对性能又一次的优化提升

通常情况下我们写媒体查询都是写在一个样式文件中&#xff0c;对于浏览器加载的时候&#xff0c;会解析到最后一行样式时才会渲染页面&#xff0c;这样就会造成页面的白屏时间过长。 但是通常情况下大量的媒体查询样式都是无用的&#xff0c;现在浏览器允许我们在引用样式文件…

SpringBoot优雅地处理全局异常,返回前端

笔者这边提供了两种处理全局异常的方式。这两种方式各有千秋&#xff0c;都很优雅。至于伙伴们想用哪种方式&#xff0c;那就仁者见仁&#xff0c;智者见智了。0、公共部分在介绍异常处理方式前&#xff0c;先定义一些公共的类。这些类在两种处理方式中都会用到。【自定义业务异…

jupyter的安装步骤

1.安装python文件 首先去官网python去下载python的安装包&#xff0c;点击donwload,选择合适的系统。这里我是windown系统&#xff0c;点击进去&#xff0c;如图找到有installer的去下载。不建议下载最新版本的&#xff0c;会有兼容问题。 2.安装python 点击第二个选项是自己配…

深度学习如何训练出好的模型

深度学习在近年来得到了广泛的应用&#xff0c;从图像识别、语音识别到自然语言处理等领域都有了卓越的表现。但是&#xff0c;要训练出一个高效准确的深度学习模型并不容易。不仅需要有高质量的数据、合适的模型和足够的计算资源&#xff0c;还需要根据任务和数据的特点进行合…

【2023/图对比/增强】MA-GCL: Model Augmentation Tricks for Graph Contrastive Learning

如果觉得我的分享有一定帮助&#xff0c;欢迎关注我的微信公众号 “码农的科研笔记”&#xff0c;了解更多我的算法和代码学习总结记录。或者点击链接扫码关注【2023/图对比/增强】MA-GCL: Model Augmentation Tricks for Graph Contrastive Learning 【2023/图对比/增强】MA-…

备份策略从“3-2-1”到“4-3-2-1”

在数据存储备份领域&#xff0c;说起“3-2-1”备份策略真是无人不知、如雷贯耳&#xff01;笔者也经常把“3-2-1”备份策略挂在嘴边&#xff0c;那简直就是确保数据安全的圭臬&#xff01;但是&#xff0c;最近有一位读者问我&#xff1a;“3-2-1”备份策略的出处在哪里&#x…

MySQL - 多表查询

目录1. 多表查询示例2. 多表查询分类2.1 等/非等值连接2.1.1 等值连接2.1.2非等值连接2.2 自然/非自然连接2.3 内/外连接2.3.1 内连接2.3.2 外连接3.UNION的使用3.1 合并查询结果3.1.1 UNION操作符3.1.2 UNION ALL操作符4. 7种JOIN操作多表查询&#xff0c;也称为关联查询&…

LocalDateTime使用

开发中常常需要用到时间&#xff0c;随着jdk的发展&#xff0c;对于时间的操作已经摒弃了之前的Date等方法&#xff0c;而是采用了LocalDateTime方法&#xff0c;因为LocalDateTime是线程安全的。 下面我们来介绍一下LocalDateTime的使用。 时间转换 将字符串转换为时间格式…

五分钟搞懂POM设计模式

今天&#xff0c;我们来聊聊Web UI自动化测试中的POM设计模式。 为什么要用POM设计模式 前期&#xff0c;我们学会了使用PythonSelenium编写Web UI自动化测试线性脚本 线性脚本&#xff08;以快递100网站登录举栗&#xff09;&#xff1a; import timefrom selenium import …

R统计绘图 | 物种组成堆叠柱形图(绝对/相对丰度)

一、数据准备 数据使用的不同处理土壤样品的微生物组成数据&#xff0c;包含物种丰度&#xff0c;分类单元和样本分组数据。此数据为虚构&#xff0c;可用于练习&#xff0c;请不要作他用。 # 1.1 设置工作路径 #knitr::opts_knit$set(root.dir"D:\\EnvStat\\PCA")#…