C++学习记录——이십삼 哈希表

news2024/11/28 1:30:29

文章目录

  • 1、unordered_map unordered_set
  • 2、哈希表
    • 1、闭散列
    • 2、开散列(拉链法/哈希桶)
      • 继续优化
  • 3、封装unordered和迭代器


1、unordered_map unordered_set

C++11提供,功能和map、set完全类似,不过它们底层实现是红黑树,而这两个底层是哈希表。从名字上可以看出,它们的迭代是无序的。之前的是双向迭代器,这两个则是单向迭代器。

#include <iostream>
#include <unordered_set>
#include <unordered_map>
#include <string>
using namespace std;

void test_set1()
{
	unordered_set<int> s;
	s.insert(1);
	s.insert(4);
	s.insert(7);
	s.insert(10);
	s.insert(2);
	unordered_set<int>::iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;
}

void test_map1()
{
	string arr[] = { "西瓜", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉", "梨" };
	unordered_map<string, int> countMap;
	for (auto& e : arr)
	{
		countMap[e]++;
	}
	for (auto& kv : countMap)
	{
		cout << kv.first << ":" << kv.second << endl;
	}
}

int main()
{
	test_set1();
	test_map1();
	return 0;
}

在这里插入图片描述

它们也有multi版本。

相比之下,底层哈希表的map和set访问速度要更快。在release模式下测试一下

void Time()
{
	const size_t N = 1000000;//测试数据,因为随机数最多产生3万多个,所以有大量重复数据
	unordered_set<int> us;
	set<int> s;
	vector<int> v;
	v.reserve(N);
	srand(time(0));

	//插入
	for(size_t i = 0; i < N; ++i)
	{
		//v.push_back(rand());
		v.push_back(rand() + i);//减少重复数据
		//v.push_back(i)这个就是纯粹的N是多少,就有多少个
	}
	size_t begin1 = clock();
	for (auto e : v)
	{
		s.insert(e);
	}
	size_t end1 = clock();
	cout << "set insert:" << end1 - begin1 << endl;
	size_t begin2 = clock();
	for (auto e : v)
	{
		us.insert(e);
	}
	size_t end2 = clock();
	cout << "unordered_set insert:" << end2 - begin2 << endl;

	//查找
	size_t begin3 = clock();
	for (auto e : v)
	{
		s.find(e);
	}
	size_t end3 = clock();
	cout << "set find:" << end3 - begin3 << endl;
	size_t begin4 = clock();
	for (auto e : v)
	{
		us.find(e);
	}
	size_t end4 = clock();
	cout << "unordered_set find:" << end4 - begin4 << endl;
 
    cout << s.size() << endl;
	cout << us.size() << endl;

	//删除
	size_t begin5 = clock();
	for (auto e : v)
	{
		s.erase(e);
	}
	size_t end5 = clock();
	cout << "set erase:" << end5 - begin5 << endl;
	size_t begin6 = clock();
	for (auto e : v)
	{
		us.erase(e);
	}
	size_t end6 = clock();
	cout << "unordered_set erase:" << end6 - begin6 << endl;
}

在这里插入图片描述

差不多底层哈希表的都要更快一些。特别是哈希表的查找,比红黑树快得多。如果是有序数据,比如v.push_back(i),红黑树比哈希表快。

2、哈希表

哈希也叫散列。哈希/散列实际上是一种方法。Key和存储位置有关建立映射关系。

对于哈希这个方法,如果范围比较集中,就可以将每个数据分配一个唯一位置;如果范围不集中,分布分散,那么会采取取模的办法,但这也有可能造成不同的值映射到同一个位置,这会叫做哈希冲突/碰撞。为了解决这个,有以下这几个办法

1、闭散列

闭散列:它的思路就是找下一个位置。线性探测就是一个个看哪个为空,就占据哪个,一些相邻聚集位置连续冲突,可能会形成“踩踏”;缓解的办法是2次探测,key % len(长度) ,然后一直+i的平方,i >= 0。但事实上来讲2次探测也不是有用的,闭散列的情况就如同占位置一样,今天你坐在第4个位置,明天发现第4个位置被占领了,你只好去第5个位置,但这个位置曾经又何尝不是别人的?看到这里你也许会想到希尔伯特悖论…

但这不是数学,也不是无限,这是有限的,如果说现有空间不足了,已经全部被占领了,那么就需要拓宽空间,如果在原空间上增加,就会出现一个问题,原有的位置也跟着改变了,因为空间不一样了,除数也不一样了,那么找到的映射位置就不一样了。想要规则化它们很麻烦,所以就重新开辟空间,替换掉之前的空间,再重新映射。

删除不能直接删除,空出来这一块位置如何处理?查找的时候,从映射位置开始,直到找到空结束,如果删除一个位置,把它置为空,就会影响查找。为了解决问题,可以用一个状态标识来表示有没有值存在,这样删除就只改状态,不实际作用于数据。每个数据的位置除了数据还要有状态标识。

#include <vector>

enum State
{
	EMPTY,
	EXITS,
	DELETE
};

template<class K, class V>
struct HashDate
{
	pair<K, V> _kv;
	State _state;
};


template<class K, class V>
class HashTable
{
private:
	vector<HashDate<K, V>> _tables;
	size_t _n = 0;//存储的数据个数

};

插入(二次探测)

	bool Insert(const pair<K, V>& kv)
	{
		size_t hashi = kv.first % _tables.size();
		//二次线性探测
		size_t i = 0;
		size_t index = hashi;
		while (_tables[index]._state == EXIST)
		{
			index = hashi + i * i;//不是二次那就+i
			index %= _tables.size();//防止index走出去
			++i;
		}
		_tables[index]._kv = kv;
		_tables[index]._state = EXIST;
		_n++;
		return true;
	}

如果有100个位置,90个位置都有值了,那么再插入一个起冲突的概率就很大。这里有个载荷因子的概念,用来表示装满的程度。填入表中的元素个数 / 散列表的长度。C++把这个因子数控制在0.8之内。

扩容

		if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
		{
			size_t newsize = tables.size() == 0 ? 10 : _tables.size() * 2;
			vector<HashData> newtables(newsize);
			for (auto& data : _tables)
			{
				if (data._state == EXIST)
				{
					//重新算在新表的位置
				}
			}
			_tables.swap(newtable);
		}

这个改进一下,用复用。

		if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
		{
			size_t newsize = tables.size() == 0 ? 10 : _tables.size() * 2;
			//vector<HashData> newtables(newsize);
			HashTable<K, V> newht;
			newht._tables.resize(newsize);
			for (auto& data : _tables)
			{
				if (data._state == EXIST)
				{
					//重新算在新表的位置
					//复用方法
					newht.Insert(data._kv);
				}
			}
			_tables.swap(newht._tables);
		}

查找、删除

	HashDate<K, V>* Find(const K& key)
	{
		size_t hashi = kv.first % _tables.size();
		size_t i = 1;
		size_t index = hashi;
		while (_tables[index]._state != EMPTY)
		{
			if (_tables[index]._kv.first == key)
			{
				return &_tables[index];
			}
			index = hashi + i;
			index %= _tables.size();
			++i;
		}
		return nullptr;
	}

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

但是在查找函数,如果找到了与key相等的位置,就改变标识符,但实际上还是存在这个值的,映射时还会受影响。 所以if判断条件改为

_tables[index]._state == EXIST && _tables[index]._kv.first == key

所以可以在插入之前可以先find一下在不在,在就退出,但是会出现除0错误,所以find那里还需要一开头判断一下,如果大小为0,就return false。

如果全是删除状态呢?后者还有别的状态,比如插入一部分数据后,在扩容前,删除一部分数据,在插入数据,并且数据正好占据其他空位,导致表里除了存在就是删除,那么Find就无法正常运行,因为没有空状态了。

Find添加一个这个,index可能会走一圈又回到hashi的位置。

			if (index == hashi)
			{
				break;
			}

对于闭散列来讲,冲突越多越低效。i也可以随意变化,但实际上这样看下来,还是麻烦,考虑得多,并且效率也不是很好,所以要用开散列

2、开散列(拉链法/哈希桶)

对于每一个位置,都是一个子集合,都是一个桶,各个桶的元素通过一个单链表链接起来,链表头结点存储在哈希表中。桶中每个元素都是发生哈希冲突的元素。相当于这个数组是一个指针数组。

所以相当于数组中的元素是链表。载荷因子还是要有。载荷因子越大,冲突的概率越高,查找效率越低,空间利用率越高;载荷因子越小,冲突的概率越低,查找效率越高,空间利用率越低

扩容

			if (_n == _tables.size())
			{
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				HashTable<K, V> newht;
				newht.resize(newsize);
				for (auto cur : _tables)
				{
					while (cur)
					{
						newht.Insert(cur->_kv);
						cur = cur->_next;
					}
				}
				_tables.swap(newht._tables);
			}

这里还有优化空间。扩容有了新空间,需要释放原空间。

		~HashTable()
		{
			for (auto& cur : _tables)
			{
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				cur = nullptr;
			}
		}

这里的重点在于调用了insert,它会创建新结点,旧表要释放,完全要释放。所以不如之前插入的节点挪动到新的表中,而不是重新插入。这里的代码会复杂点,不过效率更高。

			if (_n == _tables.size())
			{
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				vector<Node*> newtables(newsize, nullptr);
				for (auto& cur : _tables)//Node*&
				{
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = cur->_kv.first % newtables.size();
						cur->_next = newtables[hashi];
						newtables[hashi] = cur;
						cur = next;
					}
				}
				_tables.swap(newtables);

现在的整体代码

	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 HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		~HashTable()
		{
			for (auto& cur : _tables)
			{
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				cur = nullptr;
			}
		}

		bool Insert(const pair<K, V>& kv)
		{
			//扩容
			if (_n == _tables.size())
			{
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				vector<Node*> newtables(newsize, nullptr);
				for (auto& cur : _tables)//Node*&
				{
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = cur->_kv.first % newtables.size();
						cur->_next = newtables[hashi];
						newtables[hashi] = cur;
						cur = next;
					}
				}
				_tables.swap(newtables);
				/*HashTable<K, V> newht;
				newht.resize(newsize);
				for (auto cur : _tables)
				{
					while (cur)
					{
						newht.Insert(cur->_kv);
						cur = cur->_next;
					}
				}
				_tables.swap(newht._tables);*/
			}
			size_t hashi = kv.first % _tables.size();
			//头插
			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return true;
		}
	private:
		vector<Node*> _tables;
		size_t _n = 0;//载荷因子
	};

查找

		Node* Find(const K& key)
		{
			if (_tables.size() == 0) return nullptr;
			size_t hashi = key % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}

 //Insert就可以在一开始添加上
 if(Find(kv.first))
 {
     return false;
 }

删除不能用Find来帮助Erase。

		bool Erase(const K& key)
		{
			size_t hashi = key % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
		}

继续优化

如果用HashTable<string, string>的数据来插入的话会报错,原因出在插入函数的这:size_t hashi = cur->_kv.first % newtables.size(),因为字符串不能取模,为了整体的泛型编程,我们在HashTable那里加一个模板,写一个仿函数用来类型转换。

	template<class K>
	struct HashFunc
	{
		size_t operator()(const K& key)
		{
			return key;
		}
	};

	template<class K, class V, class Hash = HashFunc<K>>

那么每个取模的地方都得改,

			Hash hash;
			size_t hashi = hash(key) % _tables.size();

这样就行了。但是字符串不支持显示整形,在调用测试函数前这样写,访问第一个字母

	struct HashStr
	{
		size_t operator()(const string& s)
		{
			return s[0];
		}
	};

	void TestHashTable3()
	{
		HashTable<string, string, HashStr> ht;
		ht.Insert(make_pair("sort", "排序"));
		ht.Insert(make_pair("left", "左边"));
		ht.Insert(make_pair("right", "右边"));
	}

但是这样不行,一个是传nullptr问题,一个是如果首字母都一样,那就分辨不出来了。

	struct HashStr
	{
		size_t operator()(const string& s)
		{
			size_t hash = 0;
			for (auto ch : s)
			{
				hash += ch;
			}
			return hash;
		}
	};

实际上这还有问题,就是打印四个字母,不同的字符串但是加起来相同,打印出来还是一样的数字

		HashStr hashstr;
		cout << hashstr("abcd") << endl;
		cout << hashstr("aadd") << endl;

关于字符串的哈希算法可以看这篇:字符串哈希算法

改动时可以在 += ch后写上hash *= 31,也有其他值都可以,30什么的。

更好的写法是把这个针对字符串做的改动做成类的特化。外面的实例化对象也不需要传HashStr了。

	template<class K>
	struct HashFunc
	{
		size_t operator()(const K& key)
		{
			return key;
		}
	};

	template<>
	struct HashFunc<string>
	{
		size_t operator()(const string& s)
		{
			size_t hash = 0;
			for (auto ch : s)
			{
				hash += ch;
				hash *= 31;
			}
			return hash;
		}
	};

增删查改的时间复杂度最坏是O(N),但因为扩容的原因,一些冲突元素大概率不冲突,并且还有载荷因子的控制,所以最坏的情况很少出现,所以看平均复杂度O(1)即可。

为了检测,在类里写一个找出最深桶的函数,并且打印所有的桶大小。

		size_t MaxBucketSize()
		{
			size_t max = 0;
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				auto cur = _tables[i];
				size_t size = 0;
				while (cur)
				{
					++size;
					cur = cur->_next;
				}
				printf("[%d]->%d\n", i, size);
				if (size > max)
				{
					max = size;
				}
			}
			return max;
		}

        void TestHashTable4()
	    {
		    size_t N = 100000;
		    HashTable<int, int>ht;
		    srand(time(0));
		    for (size_t i = 0; i < N; ++i)
		    {
			    size_t x = rand() + i;
			    ht.Insert(make_pair(x, x));
		    }
		    cout << ht.MaxBucketSize() << endl;
	    }

对于极端情况,比如链表长度比较大,那么一个解决办法就是挂红黑树而不是链表,java有这样做。

还有一个优化就是让除模那里的除数是素数。SGI版本对此的做法是给了一个素数表,数量是28个。

在这里插入图片描述

size_t GetNextPrime(size_t prime)
{
	const int PRIMECOUNT = 28;
	static const size_t primeList[PRIMECOUNT] =
	{
	53ul, 97ul, 193ul, 389ul, 769ul,
	1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
	49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
	1572869ul, 3145739ul, 6291469ul, 12582917ul,
   25165843ul,
	50331653ul, 100663319ul, 201326611ul, 402653189ul,
   805306457ul,
	1610612741ul, 3221225473ul, 4294967291ul
	};
	size_t i = 0;
	for (; i < PRIMECOUNT; ++i)
	{
		if (primeList[i] > prime)
			return primeList[i];
	}
	return primeList[i];
}

把代码里的数字换成图片中就好。每次扩容时这样写就行

size_t newsize = GetNextPrime(_tables.size());

3、封装unordered和迭代器

迭代器最关键的就是++应该怎么办?这里就按照库的做法,再遍历一个桶之前,就先找下一个不为空的桶。这样一个桶结束后就跳去下一个桶;当然还会考虑一个问题,如果走到了尾还没有发现不为空的桶,这就在代码中展现。

为了优化迭代器,代码里还放上了简洁的日期类,日期类不支持转成整型,哈希表里的hashtable那里不能传 class Hash = HashFunc< K >,这个要放在map和set的文件里写。

迭代器里不可修改,所以要加上这个。

哈希表

结束。

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

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

相关文章

JDBC~

文章目录 JDBCJDBC API详解DriverManagerConnectionStatementResultSetPreparedStatementPreparedStatement原理 JDBC JDBC就是使用JAVA语言操作关系型数据库的一套API JDBC是java处理数据库的接口 JDBC API详解 DriverManager 驱动管理类: 注册驱动&#xff0c;获取数据库连接…

Cplex的数据类型结构及基本语法功能

本序列将会重开一门新的序列----数学求解器cplex,文章不做简单介绍&#xff0c;不灌水&#xff0c;直接给大家进行讲述如何上手实操&#xff0c;并有针对性的给出相应案例分析。 OPL编程 OPL是ILOG团队为运筹学专家量身定制的一种优化建模语言&#xff0c;语法相对简单&#x…

C++系列之入门基础知识

&#x1f497; &#x1f497; 博客:小怡同学 &#x1f497; &#x1f497; 个人简介:编程小萌新 &#x1f497; &#x1f497; 如果博客对大家有用的话&#xff0c;请点赞关注再收藏 &#x1f31e; 命名空间 常见域的种类 常见的域的种类有&#xff1a;全局域&#xff0c;局部…

快速构建springboot项目

需求&#xff1a;浏览器发送 /hello 请求&#xff0c;服务器接受请求并处理&#xff0c;响应 Hello World 字符串分析 构建 Spring Boot 项目&#xff0c;事实上建立的就是一个 Maven 项目 1.创建maven 工程 在 IDEA上新建一个空的jar类型 的 maven 工程 2.修改pom.xml 1.在…

【Tkinter.Floodgauge】当程序需要长时间运行,可以用这个组件显示进度【文末附源码地址】

文章目录 效果展示源码解析导包Floodgauge组件界面初始化创建窗口修改数值运行 源码地址 效果展示 我在使用tkinter进行界面化操作的时候&#xff0c;会遇到运行很慢的程序&#xff0c;比如&#xff1a;爬虫下载视频、压缩解压文件&#xff0c;这些操作会很耗时间。 Floodgau…

力扣 ~ JavaScript ~ 35. 搜索插入位置

《跃动青春》好可爱的女孩子的友谊哇 35. 搜索插入位置 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 示例 1: …

redis(12)

1)添加地理位置坐标:已经添加的数据&#xff0c;无法再添加 geoadd key 经度 纬度 变量名字 geoadd china:city 121.47 31.23 shanghai 2)获取指定地区的坐标值: geopos key member(变量名字) geopos china:city shnaghai 3)获取到两个坐标之间的距离&#xff0c;后面是可选参…

95后的自述,文科女吃上IT饭了,历经5个月学习成功上岸软件测试......

想不到我一个女文科生也能吃上IT饭&#xff0c;真的是太香了&#xff01;&#xff01;&#xff01; 程序员的待遇怎么这么好&#xff01;&#xff08;请大家原谅我没有见过市面的感叹&#xff0c;对于我这种刚上岗的新手而言&#xff0c;能够在厦门这个全国工资房价比最低的地方…

景区手绘图的配准、切图及上传

目录 1 增加一个项目 2 基础配置 3 获取自动上传需要的3个参数 3.1 生成切片上传验证码 3.2 按F5刷新页面 3.3 查看验证码以及其他2个参数 4 配准 5 切图 6 到迅达平台看一下切图效果 7 软件免费下载 这个免费客户端专用于迅达平台&#xff0c;对手绘图背景进行配准&…

java 调用 python 方法

目录 1.JPython 1.1 JPython介绍 1.2 JPython实践 2.使用Runtime.getRuntime()执行python脚本文件&#xff0c;推荐使用 一个项目可能需要用到人工智能算法&#xff0c;在java后端中需要调用python算法&#xff0c;这篇文章就简单介绍一下其中的两种java调用python方法。 …

一个简单的基于QT的图像浏览器

以前学习前端的时候&#xff0c;对于多张图片的布局一般使用瀑布流布局&#xff08;CSS总结——瀑布流布局_css 瀑布流_黑白程序员的博客-CSDN博客&#xff09;&#xff0c;然后再通过懒加载&#xff08;如何实现图片懒加载&#xff0c;预加载&#xff01;&#xff01; - 简书&…

AutoDL使用百度网盘来进行数据的交互

文章目录 1. 简介 2. 操作2.1. 设置密码2.1.1. 登录网盘 2.2. 访问网盘中的数据2.3. 将数据放入到网盘中 1. 简介 AutoDL上面其实是可以直接使用百度网盘、阿里云盘等公共网盘的&#xff0c;这样传资料的时候就不用通过Xshell慢悠悠的传输了&#xff0c;如果有百度网盘会员的话…

[MTK7621] dhcp.script 脚本分析

该脚本只要完成IP地址、DNS和路由设置。由udhcpc程序调用。调用的函数在下面两个文件中定义&#xff1a; . /lib/functions.sh&#xff1a;定义了一些基础操作&#xff0c;例如配置文件获取等 . /lib/netifd/netifd-proto.sh&#xff1a;定义了IP、DNS和路由的处理函数 在dhcp.…

关于Photoshop中的【Stable-Diffusion WEBUI】插件:Auto.Photoshop.SD.plugin

文章目录 &#xff08;零&#xff09;前言&#xff08;一&#xff09;PS的插件&#xff08;1.1&#xff09;安装&#xff08;1.2&#xff09;简评 &#xff08;零&#xff09;前言 本篇主要提到Photoshop中的Stable-Diffusion WEBUI插件&#xff0c;相对WEBUI并不算好用&#x…

MySQL 数据库面试题

TOC 1. MySQL 的内连接、左连接、右连接有有什么区别&#xff1f; inner join 内连接&#xff0c;在两张表进行连接查询时&#xff0c;只保留两张表中完全匹配的结果集。 left join 在两张表进行连接查询时&#xff0c;会返回左表所有的行&#xff0c;即使在右表中没有匹配的记…

STM32F401RET6 LQFP64 (Nucleo-F401RE) uart 打印输出

STM32F401RET6 LQFP64 (Nucleo-F401RE) uart 打印输出 1.STM32F401RET6 芯片情况 2.板子硬件 电源部分&#xff0c;开发板可利用JP5跳帽座子来选择使用USB供电或者使用管脚座子的外部Vin进行供电&#xff0c;无论是哪种供电&#xff0c;都需要经过U4的5V转成3.3V的LDO的再给S…

Simulink中如何获取所需变量对应的时间,并实时传输给其他模块

假设需要将时间信息传输给一个显示器模块,可以按照以下步骤进行操作: 在模型中添加“Clock”模块,将其输出与MATLAB函数“simulinktime”模块进行连接。 在模型中添加一个Scope模块,将其输入与Clock模块的输出进行连接。 运行模型,Scope模块将显示当前的仿真时间。 将Scop…

2023自动化测试选择什么工具或者框架好呢?

自动化测试的工具或者框架在市场上种类是比较繁多的。那么作为软件测试开发者应该怎么去选择呢&#xff1f;笔者觉得可以从测试需求、测试用例复杂度、技能水平和预算等方面去考虑。 Selenium 这是最常用的自动化测试工具之一&#xff0c;它可以模拟用户在不同的浏览器中进行操…

工厂人员定位系统源码,支持智能考勤、工时统计、行为检测、历史轨迹回放、人员管理、电子围栏功能

系统概述&#xff1a; 工厂人员定位系统&#xff0c;采用UWB定位技术&#xff0c;通过在厂区内部署一定数量的定位基站&#xff0c;以及为人员、车辆、物资佩戴标签卡的形式&#xff0c;实时获取人员精确位置&#xff0c;精度高达10cm。 工厂人员定位系统可实现物资/车辆实时定…

【Linux】】Linux权限的理解

一.Linux中的用户 Linux中分为两种用户&#xff0c;分别为&#xff1a; 1.root 用户&#xff0c;也叫超级用户&#xff0c;它的权限非常高&#xff0c;不受其他权限的约束&#xff0c;也就是可以为所欲为&#xff1b; 2.普通用户&#xff1a;除了root用户外&#xff0c;都是普通…