C++数据结构:哈希 -- unordered系列容器、哈希表的结构以及如何通过闭散列的方法解决哈希冲突

news2024/11/28 6:50:08

目录

一. unordered系列关联式容器

1.1 unrodered_map和unordered_set 综述

1.2 常见的接口函数(以unordered_map为例)

1.3 unordered系列与map和set的效率对比

二. 哈希表的底层结构

2.1 什么是哈希

2.2 哈希函数

2.3 哈希冲突

三. 通过闭散列的方法解决哈希冲突问题

3.1 线性探测法

3.2 二次探测法

四. 线性探测法和二次探测法的实现

4.1 哈希表存储的数据类型

4.2 哈希表的扩容 

4.3 插入数据操作insert

4.4 数据查找操作find

4.5 数据删除操作erase

附录:线性探测法和二次探测法模拟实现哈希表完整版代码


一. unordered系列关联式容器

1.1 unrodered_map和unordered_set 综述

在C++98中,STL库提供了map和set两种容器,它们的底层都是通过红黑树来实现的,可以实现时间复杂度为O(log N)的查找。如果数据量N过多,就会造成红黑树的层数很大,查找效率依旧不会理想。因此,在C++11中,标准STL库又引入了unordered_map和unordered_set这两个容器。map和set与unordered系列容器的区别是:

  • map和set是有序的,unordered系列容器是无序的。
  • map和set的迭代器支持双向迭代,unordered系列容器仅支持单向迭代。
  • 在增删查改方面。unordered系列容器的效率高于map和set。

unordered系列容器的底层是通过哈希表来实现的。

stl库中对unordered_map和unordered_set类模板的声明见图1.1,这里尤其要注意Hash类,这是个仿函数,可以由用户自主实现。默认情况下,该仿函数仅仅是将输入的参数转换为size_t类型返回,但是对于string类等特殊数据作为Key时,则需要一些算法将这些特殊类型转换为size_t,这是仿函数Hash所实现的。

图1.1 unordered_set和unordered_map的声明

1.2 常见的接口函数(以unordered_map为例)

unordered_map和unordered_set接口函数的使用于map和set基本一致。

  • 构造相关函数
函数声明功能
unordered_map默认构造
unordered_map(InputIterator first, InputIterator last)用迭代器区间构造
unordered_map(const unordered_map& m)拷贝构造
  • 容量相关函数
函数声明功能
bool empty()检测是否为空
size_t size()检测数据个数
  • 迭代器相关函数
函数声明功能
begin()返回哈希表第一个元素位置的迭代器
end()返回哈希表最后一个元素后面位置的迭代器
  • 元素访问函数
函数声明功能
operator[]返回与Key对应的Value的引用,如果Key不存在就插入一个新节点
  • 修改相关函数
函数声明功能
pair<iterator, bool>  insert(pair)插入键值对
size_t erase(Key)删除特定Key值
iterator erase(iterator)删除某个迭代器位置的值
iterator erase(Iterator first, Iterator last)删除某段迭代器区间
void clear()清空数据
swap(const unordered_map& m)交换两个unordered_map的内容

注意:通过迭代器位置和迭代器区间删除数据的erase函数返回被删除的最后一个数据后面那个位置处的迭代器,指定Key值删除数据的erase函数返回删除数据的个数。

1.3 unordered系列与map和set的效率对比

运行代码1.1的测试程序,依次向map和unordered_map中插入200000个数据,比较两者的插入、查找、删除数据的效率,从图1.2的运行结果可以看出,unordered_map的增删查效率要高于map。因此,虽然unordered系列容器相比于map和set既不有序、也不支持双向迭代,但依旧有其优势:unordered系列容器的增删查效率高。

代码1.1:(unordered_map与map的效率对比)

#include<iostream>
#include<time.h>
#include<stdlib.h>
#include<map>
#include<set>
#include<unordered_map>
#include<unordered_set>

using namespace std;

void TestHash()
{
	srand((unsigned int)time(NULL));
	std::unordered_map<int, int> hashMap;
	std::map<int, int> mp;

	size_t N = 200000;
	std::vector<int> v;
	for (size_t i = 0; i < N; ++i)
	{
		int x = rand() + i;
		v.push_back(x);
	}

	size_t sz = v.size();

	//map插入数据时间
	size_t begin1 = clock();
	for (auto& e : v)
	{
		mp.insert(std::make_pair(e, e));
	}
	size_t end1 = clock();

	//unordered_map插入数据时间
	size_t begin2 = clock();
	for (auto& e : v)
	{
		hashMap.insert(std::make_pair(e, e));
	}
	size_t end2 = clock();

	std::cout << "map insert: " << end1 - begin1 << std::endl;
	std::cout << "unordered_map insert: " << end2 - begin2 << std::endl;

	//map查找数据时间
	size_t begin3 = clock();
	for (auto& e : v)
	{
		mp.find(2);
	}
	size_t end3 = clock();

	//unordered_map查找数据时间
	size_t begin4 = clock();
	for (auto& e : v)
	{
		hashMap.find(e);
	}
	size_t end4 = clock();

	std::cout << "map find: " << end3 - begin3 << std::endl;
	std::cout << "unordered_map find: " << end4 - begin4 << std::endl;

	//map删除数据时间
	size_t begin5 = clock();
	for (auto& e : v)
	{
		mp.erase(e);
	}
	size_t end5 = clock();

	//unordered_map删除数据时间
	size_t begin6 = clock();
	for (auto& e : v)
	{
		hashMap.erase(e);
	}
	size_t end6 = clock();

	std::cout << "map erase: " << end5 - begin5 << std::endl;
	std::cout << "unordered_map erase: " << end6 - begin6 << std::endl;
}

int main()
{
	TestHash();
	return 0;
}
图1.2 代码1.1的运行结果

二. 哈希表的底层结构

2.1 什么是哈希

对于理想的查找,我们希望拿到给定的Key之后,只要一次查找就可以找到与Key配对的Value值,或者说是以O(1)的时间复杂度查找。

如果我们可以将Key值与存储位置建立一定的映射关系,那么就可以在拿到Key值的同时就通过相应的规则在与之映射的位置存储pair<Key, Value>,这样就可以实现时间复杂度为O(1)的插入和查找。这就是哈希表结构,Key与存储位置之间的换算公式,称为哈希函数。

在我之前的博客[数据结构基础]排序算法第四弹 -- 归并排序和计数排序_【Shine】光芒的博客-CSDN博客中所讲到的计数排序,就是用哈希表数据结构实现的。

图2.1 哈希表线性映射示意图

2.2 哈希函数

哈希函数,就是关键码Key与存储位置的映射函数,常用的哈希函数实现方法有两种:

  1. 直接定值法 -- Hash(key) = A*Key + B
  2. 除留余数法 -- Hash(key) = key % p

直接定值法

直接定值法通过简单地线性映射,获得Key对应的存储位置。

  • 优点:映射关系简单,分布均匀。
  • 缺点:需要实现知道关键字的分布情况,适用于范围较小且较为连续的数据。

如图2.1所示的映射,以及计数排序,都是使用直接定值法。

除留余数法

设哈希散列中运行存储数据的地址数为m,取不大于m的数据p作为被除数,按照哈希函数:Hash(Key) = Key % p,来获取与关键码Key匹配的存储地址。

图2.2 使用除留余数法映射插入数据

2.3 哈希冲突

哈希冲突,就是两个不同的Key值,经过一定的哈希函数转换过后,其计算所得的存储位置相同,除留余数法,就很容易造成哈希冲突,如:

  • 当p=10的情况下,使用Hash(key) = Key%p 作为哈希函数,向哈希散列中插入数据。当Key=1和Key=11是会有哈希冲突的问题,因为 1%10 = 11%10 = 1。

解决哈希冲突有两大类方法:

  1. 闭散列 -- 开放定值法(线性探测、二次探测)
  2. 开散列 -- 哈希桶

三. 通过闭散列的方法解决哈希冲突问题

3.1 线性探测法

线性探测,就是在直接通过哈希函数计算获得映射位置后,如果发生冲突,就在这个位置的前后,通过++/--操作,找到没有被占用的位置。但是,新找到的位置有可能会与之后插入的元素产生哈希冲突。

图2.1 通过线性探测法解决哈希冲突的实现流程

3.2 二次探测法

二次探测,并不是指探测两次,而是2的平方。如果某个Key对于的Hash位置pos发生冲突,那么先去pos+1^2的位置查找看是否冲突,如果还是冲突就去pos+2^2的位置处查找,然后就是pos+3^2,依次类推,直到找到不冲突的位置。

图2.2 通过二次探测法解决哈希冲突问题的流程

四. 线性探测法和二次探测法的实现

这里对insert插入键值对、find查找key以及erase删除数据进行实现

4.1 哈希表存储的数据类型

在哈希表的每个位置,要存储一键值对,来记录关键字Key和用于配对的Value。同时,还应定义状态枚举类型常量State,用于记录每个位置是存在数据EXIST、还没有数据EMPTY还是之前有数据但是被删除了DELETE。

代码4.1:(hashDate -- 哈希表数据类型的定义)

enum State
{
	EMPTY,
	EXIST,
	DELETE
};

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

4.2 哈希表的扩容 

如果哈希表没有剩余空间后再进行扩容,则容易产生大量的哈希冲突,造成查找效率的下降。因此,引入散列表载荷因子a,当a大于一定值时,就进行扩容。

a是表示哈希散列表装满程度的因子,定义为:

  • a = 装入表中的元素个数 / 表的长度(可容纳元素个数)

a越大,散列表中数据越满,越容易产生哈希冲突,一般而言,一般设置扩容时a的值为0.7~0.8,当大于这个临界值时,将会扩容。

为了保证散列表中capacity()内每个位置都可以使用下标[]进行访问,以插入数据,应当使用resize进行而不是reserve扩容,resize扩容可以给新增的空间赋初值,将新增的空间的_state设为EMPTY,表明其没有被占用。

哈希表扩容不能仅仅是增容,由于映射关系发生改变,还应当改变表中数据的存放位置。扩容一般只发生在插入操作insert时,直接创建临时哈希散列复用插入操作,要比编码计算映射位置方便。

图4.1 哈希表扩容前后的数据存储位置变化情况

4.3 插入数据操作insert

哈希散列插入数据流程:

  1. 查找原来哈希表中是否存在要插入的数据,如果存在,不再插入,函数终止执行。
  2. 判断是否需要扩容,需要就执行扩容操作。
  3. 根据哈希函数计算插入数据所应存放的位置,并通过线性探测或二次探测的方法,解决哈希冲突的问题。(由于哈希表始终不满,一定能找到不发生哈希冲突的位置)
  4. 在最终找到的不发生哈希冲突的位置处插入数据。

代码4.2:(哈希表数据插入insert函数)

	bool insert(const std::pair<K, V>& date)
	{
		//key已经存在,不进行插入操作
		if (find(date.first))
		{
			return false;
		}

		//检查扩容
		if (_table.size() == 0 || 10 * _size / _table.size() >= 7)
		{
			//如果哈希表中没有存储数据(容量为0),或者插入新数据后存储的数据多于哈希表的容量的70%,就进行扩容
			//这里的哈希表容量是_table.size()而不是_table.capacity(),因为下标大于等于_table.size()是无法进行随机访问
			size_t newCapacity = _size == 0 ? 10 : 2 * _table.size();
			//_table.resize(newCapacity);

			//扩容之后每个Key对应的哈希映射的位置发生改变
			//这里创建一个新的临时Hash表,将原来哈希表中的有效数据依次插入到临时的哈希表中
			//然后使用swap函数,交换原本的哈希表和新的哈希表
			HashTable tmpHash;
			tmpHash._table.resize(newCapacity);

			for (size_t i = 0; i < _table.size(); ++i)
			{
				if (_table[i]._state == EXIST)
				{
					tmpHash.insert(_table[i]._kv);
				}
			}

			_table.swap(tmpHash._table);
		}

		//插入数据
		//1.线性探测解决哈希冲突问题
		Hash hash;    //创建Hash类对象,用于调用将Key转换为size_t类型的仿函数
		size_t mapPos = hash(date.first) % _table.size();   //通过哈希函数获取映射位置
		size_t start = mapPos;

		while (_table[mapPos]._state == EXIST)
		{
			//发生了哈希冲突,通过线性探测来解决哈希冲突问题
			//线性探测:在冲突位置附近寻找没有被占用的位置
			++mapPos;
			mapPos %= _table.size();
		}

		2.二次探测解决哈希冲突问题
		//Hash hash;
		//size_t i = 0;
		//size_t start = hash(date.first) % _table.size();
		//size_t mapPos = start + i;

		//while (_table[mapPos]._state == EXIST)
		//{
		//	++i;
		//	mapPos = (start + i * i) % _table.size();
		//}

		_table[mapPos]._kv = date;
		_table[mapPos]._state = EXIST;
		++_size;

		return true;
	}

4.4 数据查找操作find

通过哈希函数,找到Key对应的理论存储位置,如果发生冲突就按照线性探测或二次探测的规律到理论位置的周围去查找,直到找到Key值或者发现EMPTY。

发现EMPTY表明哈希散列中没有Key,返回nullptr。

代码4.3:(查找函数Find)

	//Key查找函数
	hashDate<K, V>* find(const K& key)
	{
		if (_size == 0)
		{
			return nullptr;
		}

		Hash hash;
		size_t mapPos = hash(key) % _table.size();   //哈希映射位置
		size_t start = mapPos;

		//处理哈希冲突问题
		while (_table[mapPos]._state != EMPTY)
		{
			if (_table[mapPos]._state != DELETE && _table[mapPos]._kv.first == key)
			{
				return &_table[mapPos];
			}

			++mapPos;
			mapPos %= _table.size();

			//如果所有位置都遍历一遍(全为DELETE),那么break掉while循环
			//此时哈希表为空,所有位置的数据都被删除,不进行break处理会死循环
			if (mapPos == start)
			{
				break;
			}
		}

		return nullptr;
	}

4.5 数据删除操作erase

通过Find找到Key的位置,如果Key存在于哈希表中,将那个位置的_state改为DELETE即可。

代码4.4:(哈希表数据删除erase函数)

	bool erase(const K& key)
	{
		hashDate<K, V>* pos = find(key);
		if (pos)
		{
			//找到要删除数据的位置,直接将状态置为DELETE即可
			pos->_state = DELETE;
			return true;
		}
		else
		{
			//要删除的key不存在,直接返回,不进行任何操作
			return false;
		}
	}

附录:线性探测法和二次探测法模拟实现哈希表完整版代码

#include<iostream>
#include<vector>

enum State
{
	EMPTY,
	EXIST,
	DELETE
};

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

//用于获取哈希表键值Key的仿函数
//Key用于与存储位置建立映射关系
template<class K>
struct HashKey
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

template<class K, class V, class Hash = HashKey<K>>
class HashTable
{
public:
	//数据插入函数
	bool insert(const std::pair<K, V>& date)
	{
		//key已经存在,不进行插入操作
		if (find(date.first))
		{
			return false;
		}

		//检查扩容
		if (_table.size() == 0 || 10 * _size / _table.size() >= 7)
		{
			//如果哈希表中没有存储数据(容量为0),或者插入新数据后存储的数据多于哈希表的容量的70%,就进行扩容
			//这里的哈希表容量是_table.size()而不是_table.capacity(),因为下标大于等于_table.size()是无法进行随机访问
			size_t newCapacity = _size == 0 ? 10 : 2 * _table.size();
			//_table.resize(newCapacity);

			//扩容之后每个Key对应的哈希映射的位置发生改变
			//这里创建一个新的临时Hash表,将原来哈希表中的有效数据依次插入到临时的哈希表中
			//然后使用swap函数,交换原本的哈希表和新的哈希表
			HashTable tmpHash;
			tmpHash._table.resize(newCapacity);

			for (size_t i = 0; i < _table.size(); ++i)
			{
				if (_table[i]._state == EXIST)
				{
					tmpHash.insert(_table[i]._kv);
				}
			}

			_table.swap(tmpHash._table);
		}

		//插入数据
		//1.线性探测解决哈希冲突问题
		Hash hash;    //创建Hash类对象,用于调用将Key转换为size_t类型的仿函数
		size_t mapPos = hash(date.first) % _table.size();   //通过哈希函数获取映射位置
		size_t start = mapPos;

		while (_table[mapPos]._state == EXIST)
		{
			//发生了哈希冲突,通过线性探测来解决哈希冲突问题
			//线性探测:在冲突位置附近寻找没有被占用的位置
			++mapPos;
			mapPos %= _table.size();
		}

		2.二次探测解决哈希冲突问题
		//Hash hash;
		//size_t i = 0;
		//size_t start = hash(date.first) % _table.size();
		//size_t mapPos = start + i;

		//while (_table[mapPos]._state == EXIST)
		//{
		//	++i;
		//	mapPos = (start + i * i) % _table.size();
		//}

		_table[mapPos]._kv = date;
		_table[mapPos]._state = EXIST;
		++_size;

		return true;
	}

	//Key查找函数
	hashDate<K, V>* find(const K& key)
	{
		if (_size == 0)
		{
			return nullptr;
		}

		Hash hash;
		size_t mapPos = hash(key) % _table.size();   //哈希映射位置
		size_t start = mapPos;

		//处理哈希冲突问题
		while (_table[mapPos]._state != EMPTY)
		{
			if (_table[mapPos]._state != DELETE && _table[mapPos]._kv.first == key)
			{
				return &_table[mapPos];
			}

			++mapPos;
			mapPos %= _table.size();

			//如果所有位置都遍历一遍(全为DELETE),那么break掉while循环
			//此时哈希表为空,所有位置的数据都被删除,不进行break处理会死循环
			if (mapPos == start)
			{
				break;
			}
		}

		return nullptr;
	}

	//数据删除函数
	bool erase(const K& key)
	{
		hashDate<K, V>* pos = find(key);
		if (pos)
		{
			//找到要删除数据的位置,直接将状态置为DELETE即可
			pos->_state = DELETE;
			return true;
		}
		else
		{
			//要删除的key不存在,直接返回,不进行任何操作
			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);
			}
		}
		std::cout << std::endl;
	}

private:
	std::vector<hashDate<K, V>> _table;   //哈希表容器
	size_t _size = 0;   //哈希表目前所含的数据量
};

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

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

相关文章

JavaEE(系列2) -- 多线程(创建多线程)

讲述下面的内容之前,先来回顾一下一个重要的知识点 进程和线程之间的区别 1.进程包括线程。 2.进程有自己独立的内存空间和文件描述符表。同一个进程中的多个线程之间&#xff0c;共享同一份地址空间和文件描述符表。 3.进程是操作系统资源分配的基本单位&#xff0c;线程是操作…

5.14学习周报

文章目录 前言文献阅读摘要介绍方法模型框架评价指标结果结论 时间序列预测总结 前言 本周阅读文献《A Hybrid Model for Water Quality Prediction Based on an Artificial Neural Network, Wavelet Transform, and Long Short-Term Memory》&#xff0c;文献主要提出了基于人…

iconfont-extract: 一个将iconfont图标转化为React组件的工具

iconfont 提供了海量的图标&#xff0c;同时也方便了前端开发者使用这些图标&#xff0c;只需要添加对应的js、css或者字体文件即可。在我们的项目中使用添加js文件的方式&#xff0c;js文件中都包含了所有的图标&#xff0c;一个项目中通常只会使用其中的一部分&#xff0c;所…

【架构设计】DDD 到底解决了什么问题

文章目录 前言一、架构设计是为了解决系统复杂度1.1 架构设计的误区1.1.1 每个系统都要做架构设计/公司流程要求有架构设计1.1.2 架构设计是为了追求高性能、高可用、可扩展性等单一目标 1.2 架构设计的真正目的1.3 系统复杂度的六个来源及通用解法1.3.1 高性能1.3.1.1 单机复杂…

JavaWeb-一篇文章带你入门CSS(笔记+案列)

目录 CSS是什么基本语法 CSS的引入方式内部样式表行内样式表外部样式表 选择器基础选择器标签选择器类选择器id选择器通配符选择器 复合选择器后代选择器子选择器 常用元素属性字体属性文本属性背景属性圆角矩形 元素的显示模式块级元素行内元素 我们可以使用display属性来修改…

对称加密/非对称加密

古典密码学 起源于古代战争:在战争中&#xff0c;为了防止书信被截获后重要信息泄露&#xff0c;人们开始对书信进行加密。 移位式加密 如密码棒&#xff0c;使用布条缠绕在木棒上的方式来对书信进行加密。 加密算法&#xff1a;缠绕后书写 密钥&#xff1a; 木棒的尺寸 替…

[笔记]深入解析Windows操作系统《四》管理机制

文章目录 前言4.1注册表查看和修改注册表注册表用法注册表数据类型注册表逻辑结构HKEY_CURRENT_USERHKEY_USERS 实验&#xff1a;观察轮廓加载和卸载HKEY_CLASSES_ROOTHKEY_LOCAL_MACHINE 实验:离线方式或远程编辑BCDHKEY_CURRENT_CONFIGHKEY_PERFORMANCE_DATA 前言 本章讲述了…

day3_垃圾回收器

文章目录 Serial回收器ParNew回收器Parallel Scavenge回收器Serial Old回收器Parallel Old回收器CMS&#xff08;Concurrent Mark Sweeping)回收器G1 主要有7种垃圾回收器&#xff0c;如下所示&#xff1a; 其中有直线关联的表示&#xff0c;这2种垃圾回收器可以配合使用的。 S…

大模型之PaLM2简介

1 缘起 大模型时代。 时刻关注大模型相关的研究与进展&#xff0c; 以及科技巨头的商业化大模型产品。 作为产品&技术普及类文章&#xff0c;本文将围绕PaLM2是什么、特点、如何使用展开。 想要了解更多信息的可以移步官方网站提供的参考文档&#xff0c;后文会给出相关链…

Oracle11g服务说明

一、服务说明 1.OracleDBConsoleorcl&#xff1a;非必须启动 Oracle数据库控制台服务&#xff0c;orcl是Oracle的实例标识&#xff0c;默认的实例为orcl。在运行Enterprise Manager&#xff08;企业管理器OEM&#xff09;的时候&#xff0c;需要启动这个服务。 2.OracleJobS…

一文了解异步编程

promise 什么是promise promise是异步编程的一种解决方案&#xff0c;从语法上来说&#xff0c;Promise是一个对象&#xff0c;从它可以获取异步操作的消息 ES6规定&#xff0c;Promise对象是一个构造函数&#xff0c;接受一个函数作为参数&#xff0c;这个函数会立即执行&a…

Long类型返回前端精度丢失

【1】给前端返回Long会出现精度丢失问题 在《阿里巴巴Java开发手册》中&#xff0c;有一条关于前后端超大整数返回的规约&#xff0c;具体内容如下&#xff1a; 【2】问题复现 后端直接用postman测试接口&#xff0c;返回数据没有问题。但是前端访问接口的时候&#xff0c;发…

系统化了解Micrometer

本文从官方文档整理出一篇系统化全面了解的文章, 后续可能会慢慢补上源码层面的解析: https://micrometer.io/docs 学习本文的目的在于深入了解中间件的监控模块的设计, 先看看主流的做法于核心思想 本文的引用来的笔者的理解于备注 需要做的是: 先理解功能存在的理由设计模式…

kibana 代码执行 (CVE-2019-7609)

漏洞原理 “原型污染”是一种针对JS语言开发的攻击方法。JS语言中&#xff0c;每一个对象都是有其原型&#xff08;proto&#xff09;的&#xff0c;而该原型又有其自己的原型对象&#xff0c;直到某个对象的原型为null。而如果JS对其中定义的对象原型的属性读写缺乏控制&…

进程程序替换+简易版shell实现

索引 进程程序替换如何进行程序替换不同程序替换函数之间的区别系统接口调用其他语言的函数替换函数execle 简易版shell实现 进程程序替换 什么是进程程序替换&#xff1f; 指在一个正在运行的进程中&#xff0c;将原来的程序替换成新的程序的过程。 eg&#xff1a;如果我们想…

I IntelliJ IDEA 2023.1 最新解锁方式,支持java20,让Coding飞一会儿

IntelliJ IDEA 2023.1 最新变化 在 IntelliJ IDEA 2023.1 中&#xff0c;我们根据用户的宝贵反馈对新 UI 做出了大量改进。 我们还实现了性能增强&#xff0c;从而更快导入 Maven&#xff0c;以及在打开项目时更早提供 IDE 功能。 新版本通过后台提交检查提供了简化的提交流程…

Android不基于第三发依赖包解析shp文件(2)

接着上篇文章继续 2)Point (点)   一个 Point 由一对双精度坐标组成,存储顺序为 X,Y。    /*** PointGeometry记录读取* */static Geometry renderPointGeometry(byte[] recordContent,GeometryFactory geometryFactory) {int shapetype2

N轴机械臂的MDH正向建模,及python算法

目录 一、前言二、三维空间的坐标系变换三、MDH建模要点四、MDH的变换矩阵推导五、机械臂MDH的python模型六、python源码 一、前言 如果机器人工程师缺乏机器人学理论的支撑和足够的认识&#xff0c;那么随着机器人项目的深入推进&#xff0c;可能会越走越艰难&#xff0c;所谓…

【Lambda】集合的Lambda表达式

【Lambda】集合的Lambda表达式 【一】Stream的方法介绍【1】Stream里常用的方法【2】collect(toList()) & filter【3】map【4】flatMap【5】max&min【6】reduce 【二】常用案例总结【1】准备方法查询数据库获取List结果【2】取值【3】分组【4】去重【5】排序【6】list的…

uniapp制作水印相机给图片添加水印并且保存图片至本地

uniapp保存文件的三种方式 文件主要分为两大类&#xff1a; 1.代码包文件&#xff1a;代码包文件指的是在项目目录中添加的文件。 2.本地文件&#xff1a;通过调用接口本地产生&#xff0c;或通过网络下载下来&#xff0c;存储到本地的文件。 其中本地文件又分为三种&#…