STL—vector—模拟实现【深度理解vector】【模拟实现vector基本接口】

news2024/11/15 14:02:29

STL—vector—模拟实现

经过了前面对于vector的初步了解,我们已经具备了使用vector的能力了,现在我们就来深度学习一下vector,并做到能模拟实现vector的基础功能。

1.vector深度解析

要想深度了解vector,我们就要去看它的源代码。

image-20240730110131616

我们可以看到,它的成员变量给的是start finish storage而不是我们之前接触的,_a _size _capacity

image-20240730110344245

其实这只是换汤不换药而已,源代码中的iterator就是typedef之前的value_type*,而value_type*就是T的typedef。因此,唯一有变化的就是start finish storage都是T*指针,但是这也没什么不一样,我们来看一个图片。

image-20240730114948276

start指向第一个位置,finish指向最后一个位置的下一个位置。end_of_storage指向的位置就是start+capacity的位置并且start+size就是finish所指向的位置

因此后面对vector的模拟实现我们会尽量向源代码中的vector模拟实现

2.vector的模拟实现

跟string类的模拟实现相似,为了避免和库中的vector有冲突,我们要开个自己的命名空间,名字随意

namespace wzf
{
	template<class T>
	class vector
	{
	public: 
		typedef T* iterator;

	private:
        // 成员变量
		iterator _start;
		iterator _finish;
		iterator _end_of_storge;
	};
}

vector需要加模版template<class T>根据T的类型去开辟对应的T*空间

这部分的知识可以回顾初阶模版那块模版初阶【泛型编程】【函数模版】【类模版】-CSDN博客

2.1迭代器

在vector中,迭代器就是其原生指针

	iterator begin()
	{
		return _start;
	}

	iterator end()
	{
		return _finish;
	}
  • 带const的迭代器

需要在前面加上typedef const T* const_iterator;

	    const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _finish;
		}

2.2构造函数

  • 无参构造函数
		vector() // 无参构造函数
			:_start(nullptr) 
			,_finish(nullptr)
			,_end_of_storge(nullptr)
		{}
  • 带参构造函数
		vector(size_t n, const T& val = T()) // 开辟n个空间并都填充val
			:_start(nullptr)
			,_finish(nullptr)
			,_end_of_storge(nullptr)
		{
			reserve(n);
			while (n--)
			{
				push_back(val);
			}
		}
  • 迭代器做形参的构造函数

若使用iterator做迭代器,会导致初始化的迭代器区间[first,last)只能是vector的迭代器


		vector(iterator first, iterator last)
		{
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

测试代码:


void test9()
{
	// 测试两个特殊的构造函数
	wzf::vector<int> v(2, 10);

	print_vector(v);


	wzf::vector<int> v1(v.begin(), v.end());

	print_vector(v1);

	wzf::vector<string> s;
	s.push_back("111");
	s.push_back("222");

	wzf::vector<string> s1(s.begin(), s.end());

	for (auto e : s1)
	{
		cout << e << " ";
	}
	cout << endl;
} 

2.3拷贝构造

和string一样,vector的拷贝构造也涉及到深浅拷贝的问题。

在执行拷贝构造和赋值运算符重载等接口的时候,要用深拷贝。不用深拷贝会导致浅拷贝问题,程序会崩溃,因为会对同一个地址连续释放两次空间。具体解析可以复习之前的博客STL—string类—模拟实现-CSDN博客

深拷贝的流程就是,开辟新空间,数据拷贝到新空间

第一种写法:

		vector(const vector<T>& v) // 拷贝构造
		{
			size_t n = v.capacity();
			size_t sz = v.size();
			T* tmp = new T[n];
			if (v._start) // memcpy函数v._start不能为空
			{
				memcpy(tmp, v._start, sizeof(T) * sz);
			}
			_start = tmp;
			_finish = _start + sz;
			_end_of_storge = _start + n;
		}

第二种写法:

这个写法复用了我们后面实现的接口。

思路就是一开始就对自身初始化,然后把你的数据插入到我身上来。

		vector(const vector<T>& v)
			:_start(nullptr)
			,_finish(nullptr)
			,_end_of_storge(nullptr)
		{
			reserve(v.capacity()); // 避免在push_back中频繁的增容
			for (const auto& e : v)
			{
				push_back(e);
			}
		}

测试代码:

void print_vector(const wzf::vector<int>& v)
{
	wzf::vector<int>::const_iterator cit = v.begin();
	// 这里的v.begin()调用的是const的迭代器,因为v是带const的
	while (cit != v.end())
	{
		cout << *cit << " ";
		cit++;
	}
	cout << endl;
}

void test6()
{
	// 测试拷贝构造
	wzf::vector<int> v1;
	v1.push_back(1);
	v1.push_back(1);
	wzf::vector<int> v2(v1);

	print_vector(v2); // 1 1
}

2.4析构函数

		~vector()
		{
			delete[] _start;

			_start = nullptr;
			_finish = nullptr;
			_end_of_storge = nullptr;
		}

2.5size()

由之前vector的深度解析我们可以知道size(),即有效元素个数,就是_finish - _start

		size_t size() const
		{
			return _finish - _start;
		}

2.6capacity()

这个也是同理,用_end_of_storge - _start即可得到空间容量

		size_t capacity() const
		{
			return _end_of_storge - _start;
		}

2.7[]重载

_start本身就是一个T*,返回_start[i]即可

		T& operator[](size_t i) 
		{
			assert(i >= 0 && i < size());

			return _start[i]; // *(_start + i)也可以
		} 

还要提供一个带const[]的重载,这样才能给带const的vector对象提供[]接口。这里不能只有一个带const的[],因为[]要支持可读可写,需要有可以写的情况,但是size()和capacity()不一样,这个只需要一个带const的接口即可,因为不是带const的vector对象也可以调用它【涉及权限的放大和缩小问题】,并且这两个函数不需要再内部对vector对象进行修改。

	const T& operator[](size_t i) const
	{
		assert(i >= 0 && i < size());

		return _start[i]; // *(_start + i)也可以
	}

2.8赋值运算符重载

赋值运算符重载也涉及到深浅拷贝,要用深拷贝——开新空间,拷贝数据到新空间,释放旧空间

		// 赋值运算符重载
		vector<T>& operator=(const vector<T>& v)
		{
			if (this != &v) // 判断是否是自己给自己赋值
			{
				delete[] _start; // 把之前的空间释放掉

				size_t n = v.capacity();
				size_t sz = v.size();
				_start = new T[n]; // 开辟新空间
				// 拷贝数据
				memcpy(_start, v._start, sizeof(T) * sz);

				//更新对应的成员变量
				_finish = _start + sz;
				_end_of_storge = _start + n;
			}

			return *this;
		}

当然,如果觉得代码不够简洁,我们还可以用现代写法

		// 赋值运算符重载——现代写法
		vector<T>& operator=(vector<T> v)
		{
			//v由于拷贝构造,已经是我们要想的对象了。直接交换就行了
			swap(_start, v._start);
			swap(_finish, v._finish);
			swap(_end_of_storge, v._end_of_storge);
			
			return *this;
		}

要注意:实现现代写法的前提是拷贝构造没有问题,v是通过拷贝构造去复制一个实参的。

但这里还有一点问题,那就是我们这里是调用的库里的swap函数,这样代价会相对大一些,因为库里的swap是通过三个深拷贝实现的。因为他会构造vector<int>这种自定义对象,交换时还会创造临时对象完成深拷贝。

image-20240801192029963

因此我们最好自己实现一个swap接口,通过调用自己的swap接口去实现=的重载。

swap接口在后面增删查改的接口中有实现

		// 赋值运算符重载——现代写法
		vector<T>& operator=(vector<T> v)
		{
			//v由于拷贝构造,已经是我们要想的对象了。直接交换就行了
			swap(v);
			
			return *this;
		}

测试代码:

void test7()
{
	// 赋值运算符重载的测试
	wzf::vector<int> v1;
	v1.push_back(5);
	wzf::vector<int> v2;
	v2.push_back(1);
	v2.push_back(1);

	v1 = v2;
	print_vector(v1); // 1 1
}

增删查改类的接口

1.reserve()

n要大于当前容量才做处理,小于n不做处理。这里的做法是传统做法,即reserve这个函数自己去开空间,拷贝数据到新空间,释放旧空间。


		void reserve(size_t n)
		{
			if (n > capacity())
			{
				T* tmp = new T[n];
				size_t sz = size();// 原来的数据有多少必须提前算好
				if (_start) // 判断_start是否为空
				{
					memcpy(tmp, _start, sizeof(T) * sz); // start为空,memcpy会报错
					delete[] _start;
				}
				_start = tmp;
				_finish = _start + sz;
				_end_of_storge = _start + n;
			}
		}

这里要注意,上述这个reserve是有问题的,正确的代码如下:

至于代码为什么有问题,这个在后面的更深层次的深拷贝问题有讲解。上面这个reserve在因对T内置类型的时候是没有问题的。

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				T* tmp = new T[n];
				size_t sz = size();// 原来的数据有多少必须提前算好
				if (_start) // 判断_start是否为空
				{
					for (size_t i = 0; i < sz; i++)
					{
						tmp[i] = _start[i];// 不通过memcpy函数进行数据转移
					}
					delete[] _start;
				}

				_start = tmp;
				_finish = _start + sz;
				_end_of_storge = _start + n;
			}
		}
2.push_back()

在没有实现insert之前,我们用下面这个

	void push_back(const T& x) // 插入的数据类型取决于T是什么类型
	{
		// 判断是否需要增容
		if (_finish == _end_of_storge)
		{
			// 判断该vector的空间是否是0
			size_t newcapacity = capacity() == 0 ? 2 : capacity() * 2;
			reserve(newcapacity);
		}
		// 空间足够就插入
		*_finish = x;
		_finish++;
	}

实现了insert之后,我们知道insert的其中一个功能就是尾插。

那我们可以考虑直接函数复用了、

		void push_back(const T& x) // 插入的数据类型取决于T是什么类型
        {
			insert(_finish, x);
		}
3.pop_back()

这个就非常简单了,让_finish–即可

		void pop_back()
		{
			assert(_start < _finish);
			--_finish;
		}

如果实现了erase,可以考虑函数复用

		void pop_back()
		{
			assert(_start < _finish);
			erase(_finish - 1);
		}

4.insert()

insert就是在pos位置插入一个x元素。

  1. 首先就是判断pos是否合法
  2. 插入需要判断空间是否足够,不够就增容,增容要注意迭代器失效的问题
  3. 空间足够就插入,插入要注意数据是否会被覆盖。采用从后往前挪动数据
  4. 最后注意数据更新

		void insert(iterator pos, const T& x)
		{
			// 判断pos位置是否合法
			assert(pos <= _finish && pos >= _start);
			
			// 判断空间是否足够
			if (_finish == _end_of_storge)
			{
				size_t n = pos - _start; // 为了防止pos失效,我们要记载下pos到 _start的距离
				size_t newcapacity = capacity() == 0 ? 2 : capacity() * 2;
				reserve(newcapacity);
				pos = _start + n;  // 这里的_start是扩容后,新的位置的_start
			}

			// pos后的数据整体向后挪动一位
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				end--;
			}

			*pos = x;
			++_finish; 
		}
5.erase()

erase就是在pos位置下删除一个元素

  1. 要注意pos位置是否合法
  2. 删除元素就是让pos位置之后的数据整体向前移动就行
  3. 为了防止迭代器失效的问题,erase的返回值是被删除的元素的下一个有效元素的迭代器
iterator erase(iterator pos)
{
	assert(pos < _finish && pos >= _start);
	
	// erase不需要判断空间是否足够
	// 要把pos位置之后的位置往前面移动
	iterator it = pos;
	while (it != _finish)
	{
		*it = *(it + 1);
		it++;
	}

	_finish--;
	//此时的pos指向的是被删除元素的下一个元素
	return pos;
}
6.resize()

void resize(size_t n, const T& val = T())

resize有三种情况

  1. 若n大于capacity 那么会增容并且将size()[有效元素的个数]之后的空间全部填充val
  2. 若n小于capacity但是大于size,那么就会把size到n之间的空间填充val
  3. 若n小于size,那么会直接将size减少到n。相当于删减

要注意val的缺省值不能直接像之前模拟实现string的resize一样,去给一个\0,这里不能给这种精确的类型的缺省值,因为你并不知道vector<T>中的T到底是什么类型,因此这里给个T类型的对应的缺省值T()

这里的这个T(),其实就是c++中的构造函数,就是一个类型的构造函数,没给参数调用的就是默认构造函数,都会有个缺省值、int等内置类型为了兼容c++,也是有了构造函数之类的用法

比如:

int i = int();
double db = double();

我们来看代码:

这个代码对三种情况分的很明白,但是有点重复和冗余。

		void resize(size_t n, const T& val = T())
		{
			assert(n >= 0);
			//对n的值进行判断,要分类讨论
			if (n < size())
			{
				size_t time = size() - n;
				for (size_t i = 0; i < time; i++)
				{
					_finish--;
				}
			}
			else
			{
				if (n < capacity())
				{
					// 在size()~n之间要填充val
					size_t time = n - size();
					size_t begin = size();
					for (size_t i = 0; i < time; i++)
					{
						_start[begin + i] = val;
					}
					// 更新_finish指针指向的地址
					_finish += time;
				}
				else
				{
					// 先扩容
					reserve(n);

					// 在size()~capacity()之间填充val
					size_t time = capacity() - size();
					size_t begin = size();
					for (size_t i = 0; i < time; i++)
					{
						_start[begin + i] = val;
					}
					// 更新_finish指针指向的地址
					_finish += time;
				}

			}
		}

我们再来优化后的代码:

		void resize(size_t n, const T& val = T())
		{
			assert(n >= 0);
			//对n的值进行判断,要分类讨论
			if (n < size())
			{
				size_t time = size() - n;
				for (size_t i = 0; i < time; i++)
				{
					_finish--;
				}
			}
			else
			{
				if (n > capacity())
				{
					// 扩容
					reserve(n);
				}

				// 只要n大于size(), 那就是在_finish ~ n之间填充val
				while (_finish < _start + n)
				{
					*_finish = val;
					_finish++;
				}

			}
		}

注意:

这里一定要通过赋值_finish = val的形式去填充val,而不能通过memset等函数去处理。

因为memxxx之类的函数是按字节处理的,如果是char类型的数据,就没问题,因为char类型在c语言只占一个字节大小,但如果是int类型之类的,int类型一个对象是4个字节的大小,这就会导致出问题。

我们来看例子:

int arr2[5] = { 0 };
memset(arr2, 1, 20);// 20指的是20个字节  
	// 注意了memset函数在修改数据的时候是以字节为单位的 做不到以元素为单位 因此 这里让arr2里的五个元素都变成1是做不到的

image-20240801134612397

resize的测试代码:

void test5()
{
		// 测试resize
	wzf::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);

	v.resize(4);
	cout << v.size() << endl; // 4
	cout << v.capacity() << endl; // 4
	print_vector(v); // 1 2 3 0
	cout << endl << endl;

	v.resize(5, 1);
	cout << v.size() << endl; // 5
	cout << v.capacity() << endl; // 5
	print_vector(v); // 1 2 3 0 1
	cout << endl << endl;

	v.resize(8, 1); 
	cout << v.size() << endl; // 8
	cout << v.capacity() << endl; // 8
	print_vector(v); // 1 2 3 0 1 1 1 1
	cout << endl << endl;

	v.resize(10);
	cout << v.size() << endl; // 10
	cout << v.capacity() << endl; // 10
	print_vector(v); // 1 2 3 0 1 1 1 1 0 0
	cout << endl << endl;

	v.resize(2);
	cout << v.size() << endl; // 2
	cout << v.capacity() << endl; // 10
	print_vector(v); // 1 2
	cout << endl << endl;

}
7.swap()
		void swap(vector<T>& v)
		{
			::swap(_start, v._start);
			::swap(_finish, v._finish);
			::swap(_end_of_storge, v._end_of_storge);
		}

3.更深层次的深拷贝问题

在上面我们模拟实现的vector中的reserve接口实际上是有问题的,并且问题出在memcpy上。

前面在实现resize接口的时候,我们说过mem***之类的函数都是逐个字节处理的。因此这就会出问题,我们来看看怎么出问题的。

先来看一段代码:

void test8()
{
	wzf::vector<string> v;
	v.push_back("111");
	v.push_back("222");
	v.push_back("333");

	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

这个代码会崩溃,崩溃的现象根据编译器版本的不同会不太一样。

VS2022是在最后的析构函数崩溃

image-20240802002431171

之前的版本是直接在reserve的delete[]就出现了问题

image-20240802002220890

但是崩溃的原因都是同一个问题。

那为什么会崩溃呢?因为涉及到了增容操作,增容操作的memcpy是逐字节拷贝,造成了浅拷贝问题。

我们来详细分析一下。

首先这里的push_back是复用了insert函数的,insert函数中存在空间不够要扩容

image-20240802001007842

而扩容中的memcpy是将原来的_start上的数据逐个字节的拷贝到tmp上,这就导致了tmp上的string类的指针指向的是同一个空间的数据。而在析构函数的时候就会出问题。

image-20240802001100428

具体是这样的,tmp和每个元素都是一个string对象,string对象是从_start上的string对象通过memcpy逐字节拷贝过来的,就会导致str指针是一样的,这样两个指针都指向同一个数据,在析构的时候就会对同一个空间释放两次,因为string类的析构函数是需要释放空间的,而内置类型不用。

image-20240802001356238

因此我们不能再使用memcpy函数去完成数据的拷贝,尽管它应对内置类型不存在问题,但是面对需要对空间资源进行管理的自定义类型,就会出现深浅拷贝问题。

因此我们需要对其进行修改,要进行深拷贝操作**,即不是单纯的拷贝,而是有自己独立的空间,将数据赋值到新空间上去**

很简单,代码修改如下:

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				T* tmp = new T[n];
				size_t sz = size();// 原来的数据有多少必须提前算好
				if (_start) // 判断_start是否为空
				{
					for (size_t i = 0; i < sz; i++)
					{
						tmp[i] = _start[i];// 不通过memcpy函数进行数据转移
					}
					delete[] _start;
				}

				_start = tmp;
				_finish = _start + sz;
				_end_of_storge = _start + n;
			}
		}

4.关于vector<vector<T>的理解

其实就是vector<T>中的T是vector<T>类型的,类似二维数组中每个元素都是一个一维数组一样。

可以根据下图更好的理解:

这个图片是关于vector<vector<int>的分析

image-20240802004155802

5.vector的总结

下一个篇章要学习list。也就是之前数据结构学习的链表了,vector也就是顺序表,这两个数据结构的优缺点是面试会问的问题,我们需要能够总结上来

vector的缺点就是:

  1. 在中间和头部进行数据的插入或者删除的时候效率较低 O(N),因为是整体去挪动数据
  2. 在插入数据容量不够时,需要增容,增容要开辟空间,转移数据,释放旧空间。代价较大

vector的优点:

  1. 支持随机访问,vector中的任何一个元素都可以直接访问到,在查找数据的时候效率高,支持了二分查找/堆算法等算法

list(链表)的优点就是:

  1. 在插入数据的时候,不需要挪动数据,只需要新增一个节点就行,效率高,时间复杂度是O(1)
  2. list没有增容操作,是新增节点

list的缺点是:

  1. 不支持随机访问,list查询数据是遍历整个链表,直至找到,时间复杂度是O(N)

因此他两个相辅相成

image-20240802004912868

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

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

相关文章

“常温”前端网站框架(四)-- 音乐播放器【附源码】

开篇&#xff08;请大家看完&#xff09;&#xff1a;此网站写给挚爱&#xff0c;后续页面还会慢慢更新&#xff0c;大家敬请期待~ ~ ~ 此前端框架&#xff0c;主要侧重于前端页面的视觉效果和交互体验。通过运用各种前端技术和创意&#xff0c;精心打造了一系列引人入胜的页面…

高龙海洋增收不增利:毛利率有所下滑,产能利用率下降仍扩产?

《港湾商业观察》廖紫雯 日前&#xff0c;高龙海洋集团有限公司&#xff08;以下简称&#xff1a;高龙海洋&#xff09;递表港交所&#xff0c;保荐机构为越秀融资。高龙海洋国内运营主体为福建高龙海洋生物工程有限公司。 自2008年公司成立以来&#xff0c;高龙海洋一直从事…

vue3中 provide/inject用法详解

依赖注入&#xff1a;provide 和 inject 什么情况下推荐provide/inject使用&#xff1a;Prop 多层级数据透传 通常情况下&#xff0c;当我们需要从父组件向子组件传递数据时&#xff0c;会使用 props。想象一下这样的结构&#xff1a;有一些多层级嵌套的组件&#xff0c;形成了…

云HIS综合管理系统源码,云端SaaS服务,与监管系统有序对接,扩展性强

云HIS系统&#xff1a; 本套云HIS系统是一款适用于二级及以下医院、专科医院和社区卫生机构的综合性医院信息系统&#xff0c;它包含门诊预约挂号、收费结算、排班、医护协同、药房、药库、电子病历等10大功能模块&#xff0c;支持门诊、住院、医技、后勤各项核心业务。 采用…

每天五分钟玩转深度学习框架PyTorch:选择函数where和gather

本文重点 如图表所示,这几个方法可以理解为索引函数,有些函数在切片和索引一章进行了简单的介绍,本文将再次进行介绍,温故知新。 index_select 通过特殊的索引来获取数据index_select,这个这样来理解,第一个参数表示a的第几维度,第二个参数表示获取该维度的哪部分。 我…

strimzi operator 部署kafka集群

Strimzi介绍 官方文档:https://strimzi.io/docs/operators/0.42.0/overview#kafka-components_str Strimzi介绍 Strimzi 是一个用于 Apache Kafka 在 Kubernetes 上部署和管理的开源项目。它提供了一组 Kubernetes 自定义资源定义(Custom Resource Definitions,CRDs)、控制…

Oracle11.2.0 安装手册V1.0.doc

文档说明 编写目的 本手册是给系统技术人员人员提供Red Hat Enterprise Linux 5.4环境下得Oracle 11g的安装和配置指导&#xff0c;帮助实施人员或用户能够快速安装配置Oracle 11g。 准备介质 适用于Linux x86-64的Oracle Database 11g第2版 linux.x64_11gR2_database_1of2.…

校园课程助手【4】-使用Elasticsearch实现课程检索

本节将介绍本项目的查询模块&#xff0c;使用Elasticsearch又不是查询接口&#xff0c;具体流程如图所示&#xff08;如果不了解Elasticsearch可以使用sql语句进行查询&#xff09;&#xff1a; 这里是两种方法的异同点&#xff1a; Mysql&#xff1a;擅长事务类型操作&#…

​EtherCAT、CANopen、RS485在电机控制中的对比

​EtherCAT、CANopen、RS485在电机控制中的对比 EtherCAT 特点&#xff1a; 高速通信&#xff1a;EtherCAT是一种实时以太网技术&#xff0c;数据传输速度快&#xff0c;具有极低的通信延迟和抖动。 高同步性&#xff1a;可精确同步多台设备&#xff0c;适用于高要求的控制任…

Python爬虫技术 第23节 数据清洗和预处理

在使用Python进行网络爬虫项目时&#xff0c;数据清洗和预处理是非常重要的步骤。这些步骤有助于确保从网页上抓取的数据准确、一致&#xff0c;并且适合后续的分析或机器学习任务。下面我将详细介绍如何使用Python来进行数据清洗和预处理。 1. 数据获取 首先&#xff0c;你需…

Java 实现 AVL树

在二叉平衡树中&#xff0c;我们进行插入和删除操作时都需要遍历树&#xff0c;可见树的结构是很影响操作效率的。在最坏的情况下&#xff0c;树成了一个单支树&#xff0c;查找的时间复杂度成了O(N)&#xff0c;建树跟没建树一样。那么是不是有什么办法可以建一个树避免这种情…

基于 KubeSphere 的 Kubernetes 生产环境部署架构设计及成本分析

转载&#xff1a;基于 KubeSphere 的 Kubernetes 生产环境部署架构设计及成本分析 前言 导图 1. 简介 1.1 架构概要说明 今天分享一个实际小规模生产环境部署架构设计的案例&#xff0c;该架构设计概要说明如下&#xff1a; 本架构设计适用于中小规模(<50)的 Kubernetes …

本地生活服务商公司有哪些?一文教你搭建本地生活系统!

当前&#xff0c;本地生活领域群雄环伺&#xff0c;日益激烈的竞争推动各家互联网大厂调整布局模式的同时&#xff0c;也让本地生活市场持续迸发新的活力。在此背景下&#xff0c;想要通过本地生活服务商身份入局的创业者数量不断增多&#xff0c;以本地生活服务商公司有哪些等…

前端面试题整理-CSS

两栏布局 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>两栏布局</title><style>…

java计算机毕设课设—基于网络爬虫技术的网络新闻分析系统(附源码、文章、相关截图、部署视频)

这是什么系统&#xff1f; java计算机毕设课设—基于网络爬虫技术的网络新闻分析系统 基于网络爬虫技术的新闻分析系统&#xff0c;它能够实时抓取凤凰网、网易、新浪、搜狐等网站的新闻数据&#xff0c;提取正文和点击量&#xff0c;每日定时抓取。系统还能对抓取的新闻进行…

给echarts图表线条、数据点和区域设置颜色

let myChart echarts.init(document.getElementById("chartmainCop"));// 获取当前干部的各项评分const allIndicators Object.keys(this.dialogEacherTable[0]).filter(key > key ! "CadreID" && key ! "xm").map(key > ({name…

window电脑上使用python将pdf转换为word文档

1、电脑上安装Python运行环境 一、python官网下载链接 二、下载到电脑后&#xff0c;直接运行安装 三、安装完成后按&#xff1a;winR键进入window命令控制窗口&#xff0c;输入 python --version2、设置python依赖包国内镜像源 pip config set global.index-url https://mirr…

国家发改委区域司韩振海副司长一行莅临麒麟信安调研

7月31日&#xff0c;国家发改委区域司韩振海副司长一行莅临麒麟信安调研。湖南省发改委区域处处长孙健军&#xff0c;长沙市发改委党组成员、市长株潭一体化发展事务中心主任邹犇淼等相关领导陪同调研。麒麟信安总裁刘文清热情接待。 在麒麟信安展厅&#xff0c;韩振海副司长一…

在MANET中的TCP增强

本文内容节选自一篇系统性文献综述&#xff08;Systematic Literature Review, SLR&#xff09;&#xff0c;标题为“TCP Performance Enhancement in IoT and MANET”&#xff0c;由 Sultana Parween 和 Syed Zeeshan Hussain 撰写&#xff0c;发表在《International Journal …

Windows下Rust OpenCV环境配置

首发于Enaium的个人博客 安装Chocolatey 首先我们需要安装Chocolatey&#xff0c;Chocolatey是一个Windows的包管理器。 我们点击右上角的Install进入到Installing Chocolatey&#xff0c;选择Individual 复制命令 Set-ExecutionPolicy Bypass -Scope Process -Force; [Sys…