11.vector的介绍及模拟实现

news2024/12/24 11:38:23

1.vector的介绍

记得之前我们用C语言实现过顺序表,vector本质上也是顺序表,一个能够动态增长的数组。

vector 的底层实现机制
 
- 动态数组:vector 的底层实现是动态数组。它在内存中连续存储元素,就像一个可以自动调整大小的数组。
- 内存分配策略:
- 当向 vector 中添加元素导致容量不足时,vector 会重新分配一块更大的内存空间,将原有元素复制到新空间中,然后释放旧空间。这个过程可能会比较耗时,尤其是当 vector 中存储的元素数量较大时。
- 初始时,vector 通常会分配一定大小的内存空间,随着元素的不断添加,逐步扩大容量。
- 迭代器失效:在进行插入、删除等操作时,可能会导致指向 vector 中元素的迭代器失效。这是因为这些操作可能会引起内存的重新分配和元素的移动。

迭代器失效后面模拟实现会详细讲解

其实vector的常用接口和string大部分相似,但是也有不同,那有什么不同呢?

一、存储内容不同
 
- vector:可以存储各种类型的数据,如整数、浮点数、结构体等。例如,可以存储一组整数 vector<int> v = {1, 2, 3} 。
- string:专门用于存储字符序列,即字符串。例如 string s = "hello" 。
 
二、操作不同
 
- vector:
- 支持随机访问,可以通过下标快速访问元素。例如 v[2] 可以快速访问 vector 中的第三个元素。
- 可以动态添加和删除元素,使用 push_back 添加元素, pop_back 删除最后一个元素。
- string:
- 提供了丰富的字符串操作函数,如查找、替换、拼接等。例如 s.find("ll") 可以查找字符串中“ll”的位置。
- 可以直接使用 + 进行字符串拼接。
 
三、性能特点不同
 
- vector:
- 在内存中连续存储元素,有利于提高访问速度,但在插入和删除元素时可能需要移动大量元素,效率较低。
- 可以预先分配一定的空间,避免频繁的内存分配和释放。
- string:
- 通常会进行一些优化,如小字符串优化等,以提高性能。
- 字符串的长度可以动态变化,但在进行大量修改操作时可能会有一定的性能开销。

2.vector的使用

2.1vector的初始化

无参构造:

vector<int> v1;

构造并初始化:用n个value初始化

vector<int> v2(10, 1);//10个1

迭代器区间初始化:

vector<int> v3(v2.begin(), v2.end());

拷贝构造:

vector<int> v4(v2);

2.2vector iterator 的使用

iterator 的使用接口说明
begin + end
获取第一个数据位置的iterator/const_iterator, 获取最后一个数据的下一个位置的iterator/const_iterator
rbegin + rend
获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的reverse_iterator

2.3 vector 空间

容量空间接口说明
size
获取数据个数
capacity获取容量大小
empty判断是否为空
resize改变vector的size
reserve改变vector的capacity
1.capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2
倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是
根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。
2.reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代
价缺陷问题。
3.resize在开空间的同时还会进行初始化,影响size
这些接口和string是类似的,就不一一调用介绍了

2.4 vector 增删查改

vrector增删查改接口说明
push_back尾插
pop_back尾删
find (#include <algorithm>)
查找。(注意这个是算法模块实现,不是vector的成员接口)
insert在position之前插入value
erase删除position位置的数据
swap交换两个vector的数据空间
operator[]像数组一样访问

在 C++中,vector 并非没有实现 find 接口,只是没有像一些容器(如关联容器)那样有专门的成员函数 find。
 
原因如下:
 
1. 效率考虑:对于顺序容器(如 vector),线性查找的效率相对较低,通常可以使用更高效的算法如二分查找等,而不是直接调用 find 成员函数。
2. 设计理念:C++标准库的设计尽量保持不同容器的特性和用途明确,vector 主要用于存储连续的元素,更强调随机访问和高效的插入/删除尾部元素等操作,而不是查找。
 
可以通过标准算法中的  find  函数来在 vector 中进行查找。例如: 

auto it = std::find(vector.begin(), vector.end(), target); 。

对于其他增删查改接口和string同样是类似的,就不详细介绍了,但是insert和erase这两个接口是不同的,这两个接口需要配合迭代器使用,但是配合迭代器使用这里就会有一个迭代器失效的问题

2.5 vector 迭代器失效问题。(重点)

迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对
指针进行了封装比如vector的迭代器就是原生态指针T* 。因此迭代器失效,实际就是迭代器
底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即
如果继续使用已经失效的迭代器,程序可能会崩溃)。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
    int a[] = { 1, 2, 3, 4 };
    vector<int> v(a, a + sizeof(a) / sizeof(int));
    // 使用find查找3所在位置的iterator
    vector<int>::iterator pos = find(v.begin(), v.end(), 3);
    // 删除pos位置的数据,导致pos迭代器失效。
    v.erase(pos);
    cout << *pos << endl; // 此处会导致非法访问
    return 0;
}
erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理
论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end
的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素
时,vs就认为该位置迭代器失效了。

迭代器失效解决办法:在使用前,对迭代器重新赋值即可

3.vector的模拟实现

3.1结构的定义

这里我们参考STL源码解析实现一个简易版本的vector

成员变量的定义

#include <iostream>
#include <assert.h>
using namespace std;
namespace Ro
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;

	private:
		iterator _start;
		iterator _finish;
		iterator _end_of_storage;

	};
}

这里大家可能会发现和模拟实现string的写法怎么不一样?

其实这就是参考STL的写法,虽然写法不同,但其实效果是大差不差的。

如图:

我们可以通过指针减指针的做法得到size和capacity。

这里同样和模拟实现string一样使用命名空间来和库中的vector区分开来,而且这里使用类模板是为了存储不同类型的数据,

这里顺带直接将size()和capacity()的接口实现出来,同时给成员变量加一个缺省值给初始化列表用

namespace Ro
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		size_t size() const
		{
			return _finish - _start;
		}
		size_t capacity() const
		{
			return _end_of_storage - _start;
		}
	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;

	};
}

3.2构造函数和析构函数

构造:

vector(){}

这里初始化列表不写也会走,通过给的缺省值来初始化

析构函数:

~vector()
{
	if (_start)
	{
		delete[] _start;
		_start = _finish = _end_of_storage = nullptr;
	}
}

3.3reverse()

由于后面大部分函数接口都需要扩容,所以为了后面方便,我们先实现reverse

void reverse(size_t n)
{
	if (n > capacity())
	{
		if (_finish == _end_of_storage)
		{
			size_t old_size = size();
			T* tmp = new[n];
			if (_start)
			{
				memcpy(tmp, _start, sizeof(T) * old_size);
				delete[] _start;
			}
			_start = tmp;
			_finish = tmp + old_size;
			_end_of_storage = tmp + n;
		}
	}
}

这里我们要先将size()存下来,不然在给_finish更新时,会出现野指针。如图:

一般情况下都不会进行缩容的,所以我们在实现的时候不考虑缩容。另外,这里还会有一个坑,后面出错时我们再解决并说明

3.4push_back()和operator[]

为了让我们的vector能够跑起来,先来实现一下push_back接口

void push_back(const T& val)
{
	if (_finish == _end_of_storage)
	{
		reverse(capacity() == 0 ? 4 : capacity() * 2);
	}
	*_finish = val;
	_finish++;
}

先检查容量有没有满,满了就扩容,这里我们扩容就扩2倍,然后再插入数据,_finish指针++,指向下一个位置。

为了接下来方便测试我们先来实现 下标+[] 来访问数据

operator[]:

T&: T是我们不知道数据的类型,加&是为了减少拷贝

T& operator[](size_t i)
{
	assert(i < size());
	return _start[i];
}
const T& operator[](size_t i) const
{
	assert(i < size());
	return _start[i];
}

测试一下:

void test_vector1()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	//v.push_back(5);
	for (int i = 0; i < v.size(); i++)
	{
		cout << v[i] << ' ';
	}
	cout << endl;
}

先测试一下扩容前的1 2 3 4,然后再测试一下扩容后的1 2 3 4 5

没有问题

3.5迭代器的实现

这里我们来实现一下普通迭代器和const迭代器

typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
	return _start;
}
iterator end()
{
	return _finish;
}
const_iterator begin() const
{
	return _start;
}
const_iterator end() const
{
	return _finish;
}

普通迭代器可读可写,const迭代器可读但是不可写。

测试一下迭代器遍历,同样范围for也可以使用了:

void test_vector2()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << ' ';
		it++;
	}
	cout << endl;
	for (int e : v)
	{
		cout << e << ' ';
	}
	cout << endl;
}

普通迭代器可写

void test_vector2()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
		*it *= 2;
        it++;
	}
	for (int e : v)
	{
		cout << e << ' ';
	}
	cout << endl;
}

const迭代器不可写

所以迭代器要正确匹配使用。

3.6深拷贝

前面我们提到过,扩容时还有一个坑没说,其实就是memcpy浅拷贝的问题。

如果我们vector存的是string这种自定义类型会发生什么?

void test_vector3()
{
	vector<string> v;
	v.push_back("111111111111111111111111");
    v.push_back("111111111111111111111111");
    v.push_back("111111111111111111111111");
    v.push_back("111111111111111111111111");
    //v.push_back("111111111111111111111111");
	for (string& s : v)
	{
		cout << s << ' ';
	}
	cout << endl;
}

我们来看看扩容前和扩容后:

扩容前:

扩容前没有问题。那扩容后呢?

扩容后:

扩容后出问题了,运行崩溃,且打印结果错了,这是为什么?

其实就是memcpy因为浅拷贝导致的,如图:

那么整个时候就应该要深拷贝,tmp创建自己的空间存放拷贝的数据

void reverse(size_t n)
{
	if (n > capacity())
	{
		if (_finish == _end_of_storage)
		{
			size_t old_size = size();
			T* tmp = new T[n];
			if (_start)
			{
				//memcpy(tmp, _start, sizeof(T) * old_size);
				for (int i = 0; i < old_size; i++)
				{
					tmp[i] = _start[i];//T会调用自己的深拷贝
				}
				delete[] _start;
			}
			_start = tmp;
			_finish = tmp + old_size;
			_end_of_storage = tmp + n;
		}
	}
}

这里我们可以直接赋值,如果T是内置类型,一个一个拷贝没有问题,如果是自定义类型,就让T自己去调用它自己的深拷贝,也没有问题。

测试一下:

现在就不会出错了。

3.7resize()

如果n小于size,有效数据就会变为n个,容量不变

大于size小于capacity就会将数据扩大到n个,且会把size到n之间的数据初始化为val

大于capacity的话就会先扩容,再初始化。

void resize(size_t n, const T& val = T())
{
	if (n < size())
	{
		_finish = _start + n;
	}
	else
	{
		reverse(n);
		while (_finish != _start + n)
		{
			*_finish = val;
			_finish++;
		}
	}
}

这里缺省值我们不能直接给0或者其他,因为存储的数据类型我们不知道,这里可以用T()匿名对象作为缺省值,让T调用自己的构造去初始化。

注意:匿名对象的生命周期只在那一行,所以要使用const引用匿名对象,目的就是延长匿名对象的生命周期。

测试一下:

void test_vector4()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	for (int e : v)
	{
		cout << e << ' ';
	}
	cout << endl;
	v.resize(10, 1);
	for (int e : v)
	{
		cout << e << ' ';
	}
	cout << endl;
}

3.8pop_back()和empty()

当没有数据时,即为空时不能尾删

所以先来实现判空:

bool empty() const
{
	return _start == _finish;
}

尾删:

void pop_back()
{
	assert(!empty());
	_finish--;
}

比较简单就不测试了。

3.9insert()

在pos前插入val,先将pos后元素全部向后移动一格,在将val插入pos位置

void insert(iterator pos, const T& val)
{
	assert(pos <= _finish && pos >= _start);
	if (_finish == _end_of_storage)
	{
		reverse(capacity() == 0 ? 4 : capacity() * 2);
	}
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		end--;
	}
	*pos = val;
	_finish++;
}

分别测试一下扩容前和扩容后:

void test_vector5()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	//v.push_back(4);
	for (int e : v)
	{
		cout << e << ' ';
	}
	cout << endl;
	vector<int>::iterator pos = find(v.begin(), v.end(), 2);
	v.insert(pos, 10);
	for (int e : v)
	{
		cout << e << ' ';
	}
	cout << endl;
}

扩容前:

扩容后:

这里结果出错了,为什么呢?

其实就是因为我们前面提到的迭代器失效问题。

由于扩容之后,pos还是指向旧空间的2,但是我们现在要在新空间的2前面插入10,所以我们应该在扩容后更新pos指针。

void insert(iterator pos, const T& val)
{
	assert(pos <= _finish && pos >= _start);
	if (_finish == _end_of_storage)
	{
        size_t len = pos - _start;
		reverse(capacity() == 0 ? 4 : capacity() * 2);
        pos = _start + len;
	}
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		end--;
	}
	*pos = val;
	_finish++;
}

现在再测试一下:

现在就没有问题了。

但是需要注意的是,在insert中我们虽然更新了形参pos,但是外面的实参pos并没有改变,形参是实参的临时拷贝,所以形参改变不会影响形参。

那怎么办?引用形参可以吗?这里我们给出解决办法,不推荐引用,可以通过返回值来返回更新之后的pos。

不采用引用形参的原因
- 避免意外修改:
- 如果 insert 函数通过引用形参返回插入位置,这可能会导致意外的修改。因为引用本身可以被重新赋值,函数调用者可能会不小心修改这个引用,从而改变了原本应该表示插入位置的信息。
- 语义不符:
- 从语义角度看, insert 操作主要是向容器中添加元素,重点在于插入操作本身和插入后的元素位置。返回一个表示插入位置的迭代器更符合这个操作的语义,而通过引用形参返回位置不太符合这种直观的理解。引用形参更多地用于在函数内部修改外部变量的值,而 insert 的主要目的不是修改外部传入的表示位置的变量,而是告知调用者新元素的位置。

iterator insert(iterator pos, const T& val)
{
	assert(pos <= _finish && pos >= _start);
	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;
		reverse(capacity() == 0 ? 4 : capacity() * 2);
		pos = _start + len;
	}
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		end--;
	}
	*pos = val;
	_finish++;
	return pos;
}

4.0erase()

同样erase也会有迭代器失效的问题,所以我们也可以和insert一样通过返回值来更新一下pos

iterator erase(iterator pos)
{
	assert(pos >= _start && pos < _finish);
    //挪动数据
	iterator end = pos + 1;
	while (end < _finish)
	{
		*(end - 1) = *end;
		end++;
	}
	_finish--;
	return pos;
}

测试一下:

void test_vector6()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	for (int e : v)
	{
		cout << e << ' ';
	}
	cout << endl;
	vector<int>::iterator pos = find(v.begin(), v.end(), 2);
	v.erase(pos);
	for (int e : v)
	{
		cout << e << ' ';
	}
	cout << endl;
}

如果我们要删除所有的偶数呢?

void test_vector6()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	for (int e : v)
	{
		cout << e << ' ';
	}
	cout << endl;
	vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0) it = v.erase(it);
		else it++;
	}
	/*vector<int>::iterator pos = find(v.begin(), v.end(), 2);
	v.erase(pos);*/
	for (int e : v)
	{
		cout << e << ' ';
	}
	cout << endl;
}

4.1拷贝构造

拷贝构造可以使用传统写法,也可以使用现代写法,这里我们直接干

vector(const vector<T>& v)
{
	reverse(v.size());
	for (auto& e : v)
	{
		push_back(e);
	}
}

测试一下:

void test_vector7()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	for (int e : v1)
	{
		cout << e << ' ';
	}
	cout << endl;
	vector<int> v2 = v1;
	for (int e : v1)
	{
		cout << e << ' ';
	}
	cout << endl;
}

4.2赋值重载

赋值重载我们用现代写法来写:

void Swap(vector<T>& v)
{
	swap(_start, v._start);
	swap(_finish, v._finish);
	swap(_end_of_storage, v._end_of_storage);
}
vector<T>& operator=(vector<T> v)
{
	Swap(v);
	return *this;
}

测试一下:

void test_vector8()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	cout << "v1:";
	for (int e : v1)
	{
		cout << e << ' ';
	}
	cout << endl;
	vector<int> v2;
	v2.push_back(10);
	v2.push_back(20);
	v2.push_back(30);
	cout << "v2赋值前:";
	for (int e : v2)
	{
		cout << e << ' ';
	}
	cout << endl;
	v2 = v1;
	cout << "v2赋值后:";
	for (int e : v2)
	{
		cout << e << ' ';
	}
	cout << endl;
}

4.3迭代器区间构造

template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

一个类模板的成员函数,还可以是一个函数模板

这里InputIterator就是函数模板,可以自动实例化出不同类型的迭代器。

来测试一下:

	void test_vector9()
	{
		vector<int> v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		cout << "v1:";
		for (int e : v1)
		{
			cout << e << ' ';
		}
		cout << endl;
		vector<int> v2(v1.begin(), v1.end());
		cout << "v2:";
		for (int e : v2)
		{
			cout << e << ' ';
		}
		cout << endl;
	}

OK,这次vector的认识就到这里了。

欢迎指正和补充。

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

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

相关文章

# 起步专用 - 哔哩哔哩全模块超还原设计!(内含接口文档、数据库设计)

↑ 上方下载文档 (大小374KB) 接口文档预览 (超过50个接口) 一、数据库25张表er-关系清晰构图&#xff01;(tip: 鼠标右键图片 > 放大图像) 二、难点/经验 详细说明 热门评论排序评论点赞列表|DTO封装经验分享|精华接口文档说明 组员都说喜欢分档对应枚举码 如果这篇文章…

android RecyclerView 垂直显示示例(java)

RecyclerView垂直列表显示示例&#xff0c;显示图片加文字。 1、RecyclerView.Adapter适配器 public class ListAdapter extends RecyclerView.Adapter<ListAdapter.ViewHolder> {private Context mContext;private List<TitleBean> titleBeans;public ListAdapt…

华为云语音交互SIS的使用案例(文字转语音-详细教程)

文章目录 题记一 、语音交互服务&#xff08;Speech Interaction Service&#xff0c;简称SIS&#xff09;二、功能介绍1、实时语音识别2、一句话识别3、录音文件识别4、语音合成 三、约束与限制四、使用1、API2、SDK 五、项目集成1、引入pom依赖2、初始化 Client1&#xff09;…

GitCode 光引计划投稿|智能制造一体化低代码平台 Skyeye云

随着智能制造行业的快速发展&#xff0c;企业对全面、高效的管理解决方案的需求日益迫切。然而&#xff0c;传统的开发模式往往依赖于特定的硬件平台&#xff0c;且开发过程繁琐、成本高。为了打破这一瓶颈&#xff0c;Skyeye云应运而生&#xff0c;它采用先进的低代码开发模式…

网络刷卡器的功能和使用场景

网络刷卡器是一种连接互联网的设备&#xff0c;能够通过网络将读取到的各种卡片信息传输至服务器进行处理。这类刷卡器通常支持多种类型的卡片&#xff0c;如银行卡、身份证、会员卡、公交卡等&#xff0c;并运用现代信息技术确保数据的安全性和高效性&#xff0c;功能十分强大…

从零开始C++游戏开发之第七篇:游戏状态机与回合管理

在游戏开发的道路上&#xff0c;状态管理是一个无法绕开的重要课题。尤其是在棋牌类游戏中&#xff0c;游戏的进行需要有条不紊地按照回合推进&#xff0c;同时管理多个游戏状态&#xff0c;如“等待玩家加入”、“游戏进行中”、“结算阶段”等。如何优雅且高效地实现这些逻辑…

有没有检测吸烟的软件 ai视频检测分析厂区抽烟报警#Python

在现代厂区管理中&#xff0c;安全与规范是重中之重&#xff0c;而吸烟行为的管控则是其中关键一环。传统的禁烟管理方式往往依赖人工巡逻&#xff0c;效率低且存在监管死角&#xff0c;难以满足当下复杂多变的厂区环境需求。此时&#xff0c;AI视频检测技术应运而生&#xff0…

CentOS7网络配置,解决不能联网、ping不通外网、主机的问题

1. 重置 关闭Centos系统 编辑->虚拟网络编辑器 还原默认设置 2. 记录基本信息 查看网关地址,并记录在小本本上 查看网段,记录下 3. 修改网卡配置 启动Centos系统 非root用户,切换root su root查看Mac地址 ifconfig 或 ip addr记录下来 修改配置文件 vim /et…

32岁前端干了8年,是继续做前端开发,还是转其它工作

前端发展有瓶颈&#xff0c;变来变去都是那一套&#xff0c;只是换了框架换了环境。换了框架后又得去学习&#xff0c;虽然很快上手&#xff0c;但是那些刚毕业的也很快上手了&#xff0c;入门门槛越来越低&#xff0c;想转行或继续卷&#xff0c;该如何破圈 这是一位网友的自述…

麒麟操作系统服务架构保姆级教程(三)ssh远程连接

如果你想拥有你从未拥有过的东西&#xff0c;那么你必须去做你从未做过的事情 作为一名成熟运维架构师&#xff0c;我们需要管理的服务器会达到几十台&#xff0c;上百台&#xff0c;上千台&#xff0c;甚至是上万台服务器&#xff0c;而且咱们的服务器还不一定都在一个机房&am…

Hmsc包开展群落数据联合物种分布模型分析通用流程(Pipelines)

HMSC&#xff08;Hierarchical Species Distribution Models&#xff09;是一种用于预测物种分布的统计模型。它在群落生态学中的应用广泛&#xff0c;可以帮助科学家研究物种在不同环境条件下的分布规律&#xff0c;以及预测物种在未来环境变化下的潜在分布范围。 举例来说&a…

十二月第22讲:巧用mask属性创建一个纯CSS图标库

&#xff08;Scalable Vector Graphics&#xff0c;可缩放矢量图形&#xff09;是一种基于 XML 的图像格式&#xff0c;用于定义二维图形。与传统的位图图像&#xff08;如 PNG 和 JPG&#xff09;不同&#xff0c;SVG 图像是矢量图形&#xff0c;可以在任何尺寸下保持清晰度&a…

单片机:实现驱动超声波(附带源码)

单片机实现驱动超声波模块 超声波模块&#xff08;如HC-SR04&#xff09;广泛应用于距离测量、避障系统、自动驾驶等嵌入式项目中。它能够通过发射超声波信号并接收反射波来计算物体的距离。本文将介绍如何使用单片机&#xff08;如51系列单片机&#xff09;驱动超声波模块&am…

封装(2)

大家好&#xff0c;今天我们来介绍一下包的概念&#xff0c;知道包的作用可以更好的面对今后的开发&#xff0c;那么我们就来看看包是什么东西吧。 6.3封装扩展之包 6.3.1包的概念 在面向对象体系中,提出了一个软件包的概念,即:为了更好的管理类,把多个类收集在一起成为一组…

重温设计模式--命令模式

文章目录 命令模式的详细介绍C 代码示例C代码示例2 命令模式的详细介绍 定义与概念 命令模式属于行为型设计模式&#xff0c;它旨在将一个请求封装成一个对象&#xff0c;从而让你可以用不同的请求对客户端进行参数化&#xff0c;将请求的发送者和接收者解耦&#xff0c;并且能…

基于STM32U575RIT6的智能除湿器

项目说明 除湿器原理 知识点 GPIO、定时器、中断、ADC、LCD屏幕、SHT20、SPI、IIC、UART 功能概述 模块功能LCD屏幕显示温湿度&#xff0c;风机开关情况&#xff0c;制冷 开关情况&#xff0c;加热片开关情况&#xff0c;温 湿度上下阈值&#xff0c;设备ID&#xff0c;电…

【电商搜索】CRM: 具有可控条件的检索模型

【电商搜索】CRM: 具有可控条件的检索模型 目录 文章目录 【电商搜索】CRM: 具有可控条件的检索模型目录文章信息摘要研究背景问题与挑战如何解决核心创新点算法模型实验效果&#xff08;包含重要数据与结论&#xff09;相关工作后续优化方向 后记 https://arxiv.org/pdf/2412.…

【python自动化六】UI自动化基础-selenium的使用

selenium是目前用得比较多的UI自动化测试框架&#xff0c;支持java&#xff0c;python等多种语言&#xff0c;目前我们就选用selenium来做UI自动化。 1.selenium安装 安装命令 pip install selenium2.selenium的简单使用 本文以chrome浏览器为例&#xff0c;配套selenium中c…

Sigrity Optimize PI CapGen仿真教程文件路径

为了方便读者能够快速上手和学会Sigrity Optimize PI和 Deacap Generate 的功能&#xff0c;将Sigrity Optimize PI CapGen仿真教程专栏所有文章对应的实例文件上传至以下路径 https://download.csdn.net/download/weixin_54787054/90171471?spm1001.2014.3001.5503

免费线上签字小程序,开启便捷电子签名

虽如今数字化飞速发展的时代&#xff0c;但线上签名小程序的开发制作却并非易事。需要攻克诸多技术难题&#xff0c;例如确保签名的真实性与唯一性&#xff0c;防止签名被伪造或篡改。 要精准地捕捉用户手写签名的笔迹特征&#xff0c;无论是笔画的粗细、轻重&#xff0c;还是…