STL_list

一、有关list的介绍

  1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代
  2. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
  3. Iist与forward_list非常相似,最主要的不同在于forward_list是单链表,只能朝前迭代,以让其更简单高效。
  4. 与其他的序列式容器相比(array,vector,deque),Iist通常在任意位置进行插入、移除元素的执行效率更好。
  5. 与其他序列式容器相比,Iist和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)
  6. list中的sort和算法库中sort的区别

在C++中,有两种不同的 sort 函数:一种是 list 容器的成员函数 sort,另一种是算法库中的 std::sort。它们在功能和应用上有一些区别。

  1. list 中的 sort:

    • list 是 C++ 标准库中的双向链表容器,它具有自己的成员函数 sort,可以用于对链表中的元素进行排序。
    • list::sort 使用的是链表特有的插入、删除操作,因此适用于链表这种数据结构。它的时间复杂度为 O(N*logN)。
    • 由于链表的特性,其插入、删除操作的时间复杂度为 O(1),因此 list::sort 可以高效地对链表进行排序。
  2. 算法库中的 std::sort:

    • std::sort 是 C++ 标准库中的算法,可以用于对各种容器(如数组、向量等)中的元素进行排序。
    • std::sort 使用的是比较和交换操作,适用于随机访问迭代器所表示的数据结构(比如数组、向量等)。它的时间复杂度也为 O(N*logN)。
    • 对于随机访问迭代器所表示的数据结构,std::sort 是一种通用的排序算法,可以适用于各种数据结构。

总的来说,list::sort 适用于对链表进行排序,而 std::sort 适用于对各种支持随机访问的数据结构进行排序,例如数组、向量等。选择哪种排序方式取决于你使用的数据结构以及具体的排序需求。

迭代器的类型:

虽然不管是算法库中的sort还是list中的sort,在传参时都使用了模板,理论上是可以传递任意类型的迭代器,但是它们在实现函数时,可能使用了  只有特定迭代器  才能使用的操作,导致如果传递的迭代器不支持这个功能,该函数就无法使用。

例如,在库sort实现时,使用了指针相减来确定元素距离,而对于链表来说,它的指针的指向不是顺序的而是随机的,用指针相减是毫无意义的。

那我们在使用时,如果确定传递怎样的迭代器呢?

实际上,在文档中命名迭代器时会有提示。

例如在算法库中sort迭代器命名为 RandomAccessIterator - 随机访问迭代器。所以在使用前可以查看一下文档。

迭代器从功能分类上可以分为五种主要类型:

  1. 输入迭代器(Input Iterator):

    只能用于读取数据,一般用于单向遍历容器中的元素,例如 std::vectorstd::list 等。
  2. 输出迭代器(Output Iterator):

    只能用于写入数据,也是单向的,只能逐个元素地写入数据。
  3. 单向迭代器(Forward Iterator):

    可以读取和写入数据,支持在序列中向前移动,例如单向链表。
  4. 双向迭代器(Bidirectional Iterator):

    除了具备前向迭代器的功能外,还支持在序列中向后移动,例如 std::list
  5. 随机访问迭代器(Random Access Iterator):

    提供了对迭代器进行算术操作的功能,可以在 O(1) 的时间复杂度内实现随机访问和跳跃式遍历,例如数组。

需要注意的是,我们传递的迭代器只要能够满足该函数所需的功能即可,不是一定要传特定的迭代器才行。例如,双向迭代器和随机访问迭代器含有单向迭代器的功能,所以这两个迭代器是可以传递给所使用的函数的。

二、list的模拟实现

list的数据结构:

// 带头双向循环链表的一个节点
template<class T>
struct list_node
{
	// 成员变量
	list_node<T>* _prev;
	list_node<T>* _next;
	T _data;
	// 构造函数
	list_node(const T& x = T())
		:_data(x), _prev(nullptr), _next(nullptr)
	{}
};

template<class T>
class list
{
	typedef list_node<T> node;

public:
    // 迭代器
	typedef __list_iterator<T, T&, T*> iterator;
	typedef __list_iterator<T, const T&, const T*> const_iterator;
private:
    // 头节点的指针 
	node* _head;
};

1、迭代器

这里迭代器的底层一定是一个Node*的指针,但是不能直接将Node*定义为iterator,因为Node*是一个指针,属于内置类型,当我们使用++iterator时,将相当于将Node*指针向后移动一个Node大小的空间,而list每一个节点存放的地址并不是连续的,所以这时候的++操作是不行的。

注:对于string和vector来说,他俩的底层是使用一个可变空间的数组,每一个元素存放的地址是连续的,不需要对++操作符进行重载,所以他俩可以直接使用原生指针作为迭代器,不用进行封装。

为了达到++iterator的功能是移动到下一个节点处,我们需要重载++操作符,因为对于内置类型才能进行重载操作符,所以我们要将Node*封装成为一个自定类型,进而对++操作符进行重载。

先大概看一下对于操作符 * 和 ++ 的重载,后面会详细讲解:

reference operator*() const { return node->data; }

self& operator++() 
{ 
    node = node->_next;
    return *this;
}

所以迭代器的框架可以定义为:

struct __list_iterator
{
	// 成员变量
	typedef list_node<T> node;
	typedef __list_iterator<T> self; // 因为__list_iterator名字较长,可以在内部重命名一下
	node* _node;
};

这个自定义类型也需要有一个构造函数:

__list_iterator(node* n)
	:_node(n)
{}
  • 迭代器中对操作符的重载

然后再__list_iterator自定义类型内部对各种操作符进行重载:

// 解引用操作符:返回的是node指向的值,要求可以被修改,所以返回值要使用引用
Ref& operator*()
{
	return _node->_data;
}

// 前置++
self& operator++()
{
	_node = _node->_next;
	return *this;
}

// 后置++
self operator++(int)
{
	self ret(*this);
	_node = _node->_next;
	return ret;
}

// 前置--
self& operator--()
{
	_node = _node->_prev;
	return *this;
}

// 后置--
self operator--(int)
{
	self ret(*this);
	_node = _node->_prev;
	return ret;
}

bool operator!=(const self& s)
{ 
	return _node != s._node;
}

bool operator==(const self& s)
{
	return _node == s._node;
}
  • const迭代器的设计

使用场景:

void func(const list<int>& lt)
{
	list<int>::iterator it = lt.begin();
	while (it != lt.end())
	{
		(*it) *= 2;
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

会出现以下错误:

原因是因为,lt是一个const的对象,则会用一个const的对象调用begin和end,而实现的begin()和end()都是普通对象才能够调用的,所以,我们还要重载一个const对象使用的begin和end函数。

iterator begin() const
{
	return iterator(_head->_next);
}

但是,如果只是使用const修饰begin()和end(),会出现这样的问题:我们使用const对象作为参数,主要是不想更改对象的内容的,但是现在,这段代码还是可以改变该对象的内容的。

这是因为,这个begin()返回的迭代器(_head->_next)是一个普通迭代器,并没有使用const进行修饰,所以可以修改迭代器指向的内容。

分析:

需要注意的是,虽然begin()使用const进行修饰,也能创建出一个普通的迭代器(非const):const修饰的是 list<int>* this 即 const *this,所以const修饰的是this指针指向的_head,所以_head的常性的,同时_head也是一个指针,用const修饰代表着_head的指向不能再改变了,但是_head指向的内容是可以改变的,即_head->_next是可以改变的,非const修饰的,所以由_head->_next创建出来的迭代器也是非const修饰的。

所以还要让这两个函数的返回值设置成一个const迭代器,要求不能通过这个const迭代器修改其指向的值。

错误结构:

不能直接在普通迭代器前面加上一个const成为一个const迭代器,这种写法是保证迭代器本身不能被修改,我们想要实现的效果是迭代器指向的内容不能被修改

注意:这里的typedef进行重命名时不是进行简单的替换,typedef const iterator const_iterator;并不等于 typedef const node* const_iterator,而应该是 typedef node* const const_iterator 

所以const修饰的是node的指针,并不是修饰node指针指向的内容。例如:

int main()
{
	int a = 2;
	int c = 1;
	typedef int* PA;
	const PA b = &a;
	*b += 2;
	printf("%d", a);
	return 0;
}

我们发现可以通过b修改 a 的值,从a = 2,变成a = 4。并且:

int main()
{
	int a = 2;
	int c = 1;
	typedef int* PA;
	const PA b = &a;
	b = &c;
	return 0;
}

b的指向不能被改变。

所以这样进行定义是不行的:const修饰的是b指针,而不是b指向的内容。

正确结构

我们可以对按照普通迭代器一样,再创建一个const类型的迭代器。

const迭代器和普通迭代器的之间的实现,对于其他操作符的实现不用改变(如++、==),需要改变的就是 * 操作符的重载,const迭代器要求迭代器指向的值不能被修改,因此只需要在实现 * 操作符的重载时,将返回值设置成为常量,这样就完成了。

template<class T>
struct __list_const_iterator
{
	// 成员变量
	typedef list_node<T> node;
	typedef __list_const_iterator<T> self;
	node* _node;

	// 函数、运算符
	__list_const_iterator(node* n)
		:_node(n)
	{}
	// 解引用操作符:返回的是node指向的值,要求不可以被修改,所以返回值要使用常量引用
	const T& operator*()
	{
		return _node->_data;
	}
	// 前置++
	self& operator++()
	{
		_node = _node->_next;
		return *this;
	}

	// 后置++
	self operator++(int)
	{
		self ret(*this);
		_node = _node->_next;
		return ret;
	}

	// 前置--
	self& operator--()
	{
		_node = _node->_prev;
		return *this;
	}

	// 后置--
	self operator--(int)
	{
		self ret(*this);
		_node = _node->_prev;
		return ret;
	}

	bool operator!=(const self& s)
	{
		return _node != s._node;
	}

	bool operator==(const self& s)
	{
		return _node == s._node;
	}
};

 但是,我们发现,这两个迭代器只有解引用操作符的返回值类型不同,其余的函数和变量都相同,代码比较冗余。

我们可以再使用一个模板参数进行改进,当我们在创建一个迭代器时可以将迭代器的类型作为一个参数进行创建。

template<class T, class Ref>
struct __list_iterator
{
	// 成员变量
	typedef list_node<T> node;
	typedef __list_iterator<T, Ref> self;
	node* _node;

	// 函数、运算符
	__list_iterator(node* n)
		:_node(n)
	{}
	// 解引用操作符:返回的是node指向的值,要求可以被修改,所以返回值要使用引用
	Ref operator*()
	{
		return _node->_data;
	}
	
};

typedef __list_iterator<T, T&> iterator;
typedef __list_iterator<T, const T&> const_iterator;

这样就可以使用一个模板完成两个类才能完成的工作。

const对象调用const类型的begin(),begin()函数返回一个const类型的迭代器(注意:const类型的迭代器不是const对象,其本身是可以修改的,是其指向的内容不能被修改),这样设计就能完成前面的使用场景了。

  • -> 操作符的重载

使用场景:

struct AA
{
	int _a1;
	int _a2;
	AA(int a1, int a2)
		:_a1(a1)
		, _a2(a2)
	{}
}

<<操作符右操作数为AA类型的一个对象(lt指向list第一个对象也就是AA,所以lt就是AA类型的一个指针),由于没有对<<进行重载,所以不能进行打印。

所以解决办法要么我们对<<操作符进行重载;

要么用这种方法进行使用,但用起来不方便,在C语言中,我们可以通过->操作符访问一个自定义类型指针的成员变量,所以我们也可以对->操作符进行重载。

T* operator->()
{
	return &_node->data;
}

可以这样使用:

使用的时候可以这样使用,但是通过分析好像有点不妥:

我们发现  it.operator->() 得到的只是AA*,并没有得到_a1,如果想要得到_a1,还要在此基础上再加一个->才能得到_a1,即 (it.operator->())->_a1,it->->_a1.

但事实上我们可以只用一个->就可以访问到_a1,其实是编译器进行了优化,省略了一个箭头。

同时为保证重载的operator->() 也有const的版本,也会像上面,那样再添加一个模板参数,用来实例化operator->() 不同的迭代器。

template<class T, class Ref, class Ptr>
struct __list_iterator
{
	// 成员变量
	typedef list_node<T> node;
	typedef __list_iterator<T, Ref, Ptr> self;
	node* _node;

    Ptr operator->()
    {
	    return &_node->data;
    }
};

typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;

2、list相关函数实现

1)Modifiers

Ⅰ. insert

这里的insert的操作就是在一个带头双向循环链表中插入一个节点:

void insert(iterator pos, const T& x)
{
	node* cur = pos._node;
	node* prev = cur->_prev;

	node* new_node = new node(x);

	prev->_next = new_node;
	new_node->_prev = prev;
	new_node->_next = cur;
	cur->_prev = new_node;
}

同时也要注意迭代器失效的问题,这里需要设置一个返回值,用来更新外部迭代器的位置。

iterator insert(iterator pos, const T& x)
{
	node* cur = pos._node;
	node* prev = cur->_prev;

	node* new_node = new node(x);

	prev->_next = new_node;
	new_node->_prev = prev;
	new_node->_next = cur;
	cur->_prev = new_node;

	return new_node;
}

与之逻辑相配的函数是push_back()和push_front(),因此这两个函数的实现可以对insert进行复用:

void push_back(const T& x)
{
	insert(end(), x);
}

void push_front(const T& x)
{
	insert(begin(), x);
}
Ⅱ. erase
iterator erase(iterator pos)
{
	assert(pos != end());

	node* prev = pos._node->_prev;
	node* next = pos._node->_next;

	prev->_next = next;
	next->_prev = prev;

	delete pos._node;

	return iterator(next);
}

注意这里的迭代器失效问题,会导致野指针的问题,所以同样设置一个返回值。

与之逻辑相配的函数是pop_back()和pop_front(),因此这两个函数的实现可以对erase进行复用:

void pop_back()
{
	erase(--end());
}

void pop_front()
{
	erase(begin());
}


2)默认成员函数

Ⅰ. destructor

在模拟实现析构函数前,先介绍一下clear()函数。

  • clear()

Removes all elements from the list container (which are destroyed), and leaving the container with a size of 0.--移除所有元素,不包括头节点。

这里为了防止迭代器失效的问题,可以使用erase的返回值(返回删除当前节点的下一个)。

// 不清除头节点
void clear()
{
	iterator it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}

也可以使用下面这种写法。

// 不清除头节点
void clear()
{
	iterator it = begin();
	while (it != end())
	{
		erase(it++);
	}
}

注意这里并不是通过 it 进行删除的,而是使用 it++ 返回的一个拷贝值进行删除。

  •  析构函数

可以对clear()函数进行复用,并删除头节点即可。

~list()
{
	clear();
	delete _head;
	_head = nullptr;
}
Ⅱ. constructor
  • 构造函数
// 构造函数
list()
{
	_head = new node;
	_head->_next = _head;
	_head->_prev = _head;
}

这里手动创建了一个头节点。

当然也可以使用迭代器进行构造一个对象,这里可以复用push_back()函数,但是在使用push_back之前,需要有一个头节点。因为后面也要使用到初始化list,让list有一个头节点,所以可以将这个步骤写成一个函数empty_init():

void empty_init()
{
	_head = new node;
	_head->_next = _head;
	_head->_prev = _head;
}
// 迭代器区间的初始化
template<class Iterator>
list(Iterator first, Iterator last)
{
	// 使用push_back之前需要有头节点
	empty_init();
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

  • 拷贝构造函数

第一种方法:根据右边的节点,一个一个创建出节点值相同的list

void empty_init()
{
	_head = new node;
	_head->_next = _head;
	_head->_prev = _head;
}

// 拷贝构造函数(传统写法)
list(const list<T>& lt)
{
	empty_init();
	for (auto e : lt)
	{
		push_back(e);
	}
}

第二种:使用迭代器区间的构造函数构造一个对象,再交换两者的指针

void empty_init()
{
	_head = new node;
	_head->_next = _head;
	_head->_prev = _head;
}

void swap(list<T>& temp)
{
	std::swap(_head, temp._head);
}
// 拷贝构造函数(使用swap交换头指针)
list(const list<T>& lt)
{
	empty_init();

	// 使用迭代器区间构建一个对象
	list<T> temp(lt.begin(), lt.end());
	// 交换指针
	swap(temp);
}

  • operator=

第一种,传统写法:

// 传统写法
list<T>& operator=(const list<T>& lt)
{
	if (this != &lt)
	{
		clear(); // 删除被赋值list中的值
		for (auto& e : lt) // 插入值
		{
			push_back(e);
		}
	}
	return *this;
}

第二种写法:

list<T>& operator=(list<T> lt)
{
	swap(lt);
	return *this;
}

注意:不能将对象的引用作为参数。要与被拷贝对象的一个拷贝进行交换(传值调用了拷贝构造函数),如果使用引用会导致右边被赋值的对象被改变。

3)iterator

typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
Ⅰ. begin()

这里的细节在前面已经讲过了。

iterator begin() 
{
	return iterator(_head->_next);
}

const_iterator begin() const
{
	return const_iterator(_head->_next);
}

Ⅱ. end()
iterator end()
{
	return iterator(_head);
}

const_iterator end() const
{
	return const_iterator(_head);
}

3、反向迭代器

1)反向迭代器的介绍

反向迭代器是一种迭代器,它可以在容器中从后向前遍历元素。与正向迭代器不同的是,反向迭代器的++操作符是让迭代器指向前一个元素,--操作符是让迭代器指向后一个元素。反向迭代器通常用于需要从后往前遍历容器的算法和场景中。

反向迭代器和正向迭代器在使用上有相似之处,它们都可以通过解引用运算符来访问当前迭代器指向的元素,也可以通过比较运算符来判断迭代器之间的大小关系。但是,它们之间也存在一些区别和联系。

区别:

  1. 步进方向不同:正向迭代器的步进方向是从前向后,而反向迭代器的步进方向是从后向前。

  2. 支持的操作不同:由于反向迭代器的步进方向不同,因此它们不支持正向迭代器的某些操作,例如递增和递减操作等。

  3. 消耗的空间不同:反向迭代器需要保存一个正向迭代器来实现其功能,因此它们通常会消耗更多的空间。

联系:

  1. 都是迭代器:反向迭代器和正向迭代器都是STL迭代器的一种,都具有迭代器的基本特性。

  2. 具有相同的接口:反向迭代器和正向迭代器都具有相同的解引用和比较运算符等接口。

  3. 可以互相转换:由于反向迭代器是基于正向迭代器实现的,因此它们可以通过std::reverse_iterator构造函数进行互相转换。

2)反向迭代器的模拟实现

反向迭代器的++就是正向迭代器的-,反向迭代器的一就是正向迭代器的++,因此反向迭
代器的实现可以借助正向迭代器。

如果直接在正向迭代器的基础上更改个别操作符的重载方法,虽然能够实现反向迭代器的功能,但是方法比较笨拙。

标准库中对反向迭代器是通过适配器适配正向迭代器来实现的。

所以反向迭代器的底层就是正向迭代器:

template<class Iterator> // 使用一个正向迭代器适配出一个反向迭代器
struct Reverse_Iterator
{
	Iterator cur; // 底层是一个正向迭代器

	// 构造函数
	Reverse_Iterator(Iterator it)
		:cur(it)
	{}
};
// 正向迭代器
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;

// 反向迭代器
typedef Reverse_Iterator<iterator, T&, T*> reverse_iterator;
typedef Reverse_Iterator<iterator, const T&, const T*> const_reverse_iterator;

有一些标准库中有以下规定(并不是所有的库都是这样实现的):

因此,在链表中rbegin()和rend()的模拟实现如下:

// 反向迭代器
reverse_iterator rbegin()
{
	return iterator(end());
}

reverse_iterator rend()
{
	return iterator(begin());
}

const_reverse_iterator rbegin() const
{
	return const_reverse_iterator(end());
}

const_reverse_iterator rend() const
{
	return const_reverse_iterator(begin());
}
  • 反向迭代器对操作符的重载

反向迭代器的++操作符是让迭代器指向前一个元素,所以反向迭代器对++操作符的重载可以复用正向迭代器的 --操作符,同时加上前面对正向迭代器关于const问题的处理,最终重载的结果为:

template<class Iterator, class Ref, class Ptr> // 使用一个正向迭代器适配出一个反向迭代器
struct Reverse_Iterator
{
	typedef Reverse_Iterator<Iterator, Ref, Ptr> Self;
	Iterator cur; // 底层是一个正向迭代器
	// 构造函数
	Reverse_Iterator(Iterator it)
		:cur(it)
	{}

	Self& operator++()
	{
		--cur; // 调用正向迭代器的--操作符
		return *this;
	}
	Self operator++(int)
	{
		Iterator tmp = *this;
		--cur; // 调用正向迭代器的--操作符
		return tmp;
	}
};

--操作符是让迭代器指向后一个元素,所以反向迭代器对--操作符的重载可以复用正向迭代器的 ++操作符:

Self& operator--()
{
	++cur; // 调用正向迭代器的++操作符
	return *this;
}

Self operator--(int)
{
	Iterator tmp = *this;
	++cur; // 调用正向迭代器的++操作符
	return tmp;
}

== 和 != 的重载

bool operator!=(const Self& it)
{
	return cur != it.cur; // 调用正向迭代器的!=操作符
}

bool operator==(const Self& it)
{
	return cur == it.cur; // 调用正向迭代器的==操作符
}

* 和 -> 的重载

因为我们选择让 实现的反向迭代器和正向迭代器的begin和end具有一定的对称性 ,所以这里对 * 操作符的重载并不能直接对正向迭代器的 * 操作符进行复用。

因为 rbegin 指向 end 的位置,直接使用正向迭代器的 * 操作符会出现错误,所以反向迭代器对 * 的重载应该返回 rbegin 前一个的元素:

Ref operator*()
{
	Iterator tmp = cur;
	--tmp;
	return *tmp; // 调用正向迭代器的解引用操作符
}

在前面正向迭代器部分,我们知道->操作符返回的是迭代器指向元素的指针,所以:

Ptr operator->()
{
	return &(operator*()); 
}

首先,让我们来解读 operator*() 函数的实现:

  1. 在函数内部创建一个临时的迭代器对象 temp,并将其初始化为当前迭代器对象 _it。
  2. 对 temp 进行递减操作符 -- 的操作,将其指向前一个元素。
  3. 返回 temp 所指向的元素对象 *temp。

这段代码的作用是返回当前迭代器所指向的元素。通过创建一个临时的迭代器对象并将其指向前一个元素,然后返回该元素,实现了迭代器的解引用操作。

接下来,我们来看 operator->() 函数的实现:

  1. 在函数内部调用 operator*() 函数,获取当前迭代器所指向的元素。
  2. 使用取地址符 & 取得该元素的指针,并返回该指针。

这段代码的作用是返回一个指向当前迭代器所指向元素的指针。通过调用 operator*() 函数获取元素的引用,然后使用取地址符 & 获取该元素的指针,实现了迭代器的箭头操作符。

注意:

因为这里的迭代器使用了一个模板,所以不仅可以实现list的反向迭代器,也可以通过其他容器的正向迭代器适配出它的反向迭代器。


今天的分享就到这里了,如果,你感觉这篇博客对你有帮助的话,就点个赞吧!感谢感谢……

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

elasticsearch8.x版本docker部署说明和集成springboot

前提&#xff0c;当前部署没有涉及证书和https访问 1、环境说明,我采用三个节点&#xff0c;每个节点启动两个es&#xff0c;用端口区分 主机角色ip和端口服务器Amaster192.168.2.223:9200服务器Adata192.168.2.223:9201服务器Bdata,master192.168.2.224:9200服务器Bdata192.1…

Nodejs基础5之网络基础概念的IP、端口,HTTP模块的创建HTTP服务、HTTP服务注意事项、浏览器查看HTTP报文

Nodejs基础 网络基础概念IPIP的介绍IP的作用IP的分类共享IP-公网家庭共享——局域网公网 本地回环IP地址总结 IP标准分类 端口端口举例端口介绍总结 HTTP模块创建HTTP服务HTTP服务注意事项浏览器查看HTTP报文查看请求报文查看请求体url当中的查询字符串查看响应报文 网络基础概…

Qt开源版 vs 商业版 详细比较!!!!

简单整理Qt开源版与商业版有哪些差别&#xff0c;仅供参考。 简单对比 开源版商业版许可证大部分采用对商业使用不友好的LGPLv3具备商业许可证保护代码专有许可证相关大部分模块使用LGPLv3和部分模块使用GPL组成仅第三方开源组件使用Qt的其他许可证Qt模块功能支持支持技术支持…

Java把列表数据导出为PDF文件,同时加上PDF水印

一、实现效果 二、遇到的问题 实现导出PDF主体代码参考&#xff1a;Java纯代码实现导出PDF功能&#xff0c;下图是原作者实现的效果 导出报错Font STSong-Light with UniGB-UCS2-H is not recognized.。参考&#xff1a;itext 生成 PDF(五) 使用外部字体 网上都是说jar包的版本…

翻译: GPT-4 Vision通过量身定制的推荐来增强应用的用户体验 升级Streamlit五

GPT-4 Vision 系列: 翻译: GPT-4 with Vision 升级 Streamlit 应用程序的 7 种方式一翻译: GPT-4 with Vision 升级 Streamlit 应用程序的 7 种方式二翻译: GPT-4 Vision静态图表转换为动态数据可视化 升级Streamlit 三翻译: GPT-4 Vision从图像转换为完全可编辑的表格 升级St…

SpringBoot使用OpenCV

SpringBoot使用OpenCV Spring boot 整合 OpenCV 4.5环境安装配置spring boot项目 OpenCV 训练自己的模型&#xff0c;实现特定物体的识别环境安装前期准备总结 Spring boot 整合 OpenCV 4.5 本文展示Windows下Spring Boot 整合Opencv 4.5 进行对图片中的人脸提取&#xff0c;开…

防火墙到防火墙的高可用知识汇总

目录​​​​​​​ 防火墙 防火墙的分类&#xff1a; 防火墙的发展史 传统防火墙&#xff08;包过滤防火墙&#xff09;—— 一个严格的规则表 传统防火墙&#xff08;应用代理防火墙&#xff09;—— 每个应用添加代理 传统防火墙&#xff08;状态检测防火墙&#xff09…

责任链模式在java中的实现

1 总览 2 概念 避免请求发送者与接收者耦合在一起&#xff0c;让多个对象都有可能接收请求&#xff0c;将这些对象连接成一条链&#xff0c;并且沿着这条链传递请求&#xff0c;直到有对象处理它为止。职责链模式是一种对象行为型模式。 3 实现 公共部分&#xff0c;一个系…

Windows、Linux、Mac数据库的安装(mysql、MongoDB、Redis)

数据库的安装 作为数据存储的重要部分&#xff0c;数据库同样是必不可少的&#xff0c;数据库可以分为关系型数据库和非关系型数据库。 关系型数据库如 SQLite、MySQL、Oracle、SQL Server、DB2 等&#xff0c;其数据库是以表的形式存储&#xff1b;非关系型数据库如 MongoDB…

wsl-ubuntu 安装 nginx

wsl-ubuntu 安装 nginx 1. 安装 nginx2. 确认 nginx 启动状态3. 重启 nginx4. 停止 nginx 1. 安装 nginx sudo apt install nginx2. 确认 nginx 启动状态 systemctl status nginx3. 重启 nginx systemctl restart nginx4. 停止 nginx systemctl stop nginx完成&#xff01;…

CRM系统多少钱一套?

CRM系统多少钱一套&#xff1f; 相信这是企业普遍关心的一个问题。 我们公司也使用过CRM客户关系管理系统&#xff0c;经验就是&#xff0c;建议在开发前先进行一次详细的需求分析&#xff0c;确定采用哪种开发方式&#xff0c;估量所需的工作量&#xff1b;然后在此基础上&a…

【软考问题】-- 5 - 知识精讲 - 项目进度管理

一、基本问题 项目进度管理&#xff08;按时完成&#xff09; 1&#xff1a;紧前关系绘图法&#xff1f; 定义&#xff1a;它是创建进度模型的一种技术。别称&#xff1a; PDM、前导图法、单代号网络图&#xff08;只有节点需要编号&#xff09;、活动节点图&#xff08;AON&a…

Habitat环境学习二:导航任务中的Habitat-sim基础Habitat-sim Basics for Navigation

导航任务在Habitat-sim任务中的实现 官方教程概述重要概念1、Hello World程序1.0.1 基础设置Basic settings1.0.2 模拟器设置Configurations for the simulator1.0.3 创建模拟器实例1.0.4 初始化Agent1.0.5 导航和探索 官方教程 Habitat是一个高效的真实的3D模拟器&#xff0c…

iTunes Connect 中修改后的内购(IPA)审核所需的时间

引言 在 iOS 开发过程中&#xff0c;将应用上传到 App Store 是一个重要的步骤。应用审核和 IAP 商品审核是分开的&#xff0c;审核一般需要等待一周左右。如果审核通过&#xff0c;我们会收到 Apple 发来的反馈邮件&#xff0c;根据邮件中的指示进行后续操作。如果已经上架的…

客户点赞,“信”任满满 | 竹云喜获近百封感谢信!

玉兔辞旧岁&#xff0c;威龙迎新春 在新春佳节来临之际 一封封感谢信、表扬信 纷至沓来 纸短情长 每一封感谢信的背后 都记载着一个动人的故事 字里行间情真意切 激励着竹云继续前行&#xff01; 国家电投 竹云项目组成员凭借丰富的业务、技术经验、专业的职业素养和较…

从换脸到克隆:IP Adapter FaceID的技术突破与应用

引言 换脸技术&#xff0c;一直以来都是数字图像处理领域的热门话题。从最早的传统方法到现在的AI驱动技术&#xff0c;换脸技术已经经历了多次重大的技术革新。近年来&#xff0c;随着深度学习和计算机视觉技术的发展&#xff0c;换脸技术开始向更加智能化、自动化的方向演进…

Springboot自定义线程池实现多线程任务

1. 在启动类添加EnableAsync注解 2.自定义线程池 package com.bt.springboot.config;import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTask…

移动端深度编辑产品技术解决方案

视频编辑已经成为企业宣传、教育、娱乐等多个领域的重要工具。美摄科技凭借其深厚的技术积累和对市场需求的敏锐洞察&#xff0c;开发出业界领先的移动端深度编辑产品&#xff0c;为企业提供高效、专业的视频编辑解决方案。 美摄科技移动端深度编辑产品方案&#xff0c;基于多…

思维导图:大神教你如何画思维导图

思维导图是什么&#xff1f; 思维导图是一种图形化的工具&#xff0c;用于组织和展示思维过程、概念关系和信息结构。它以一个中心主题为起点&#xff0c;通过分支和连接的方式&#xff0c;将相关的想法、概念、关键词等呈现在一个图形化的结构中。 思维导图通常以中心节点开始…

自动化测试——selenium工具(web自动化测试)

1、自动化测试 优点&#xff1a;通过自动化测试有效减少人力的投入&#xff0c;同时提高了测试的质量和效率。 也用于回归测试。随着版本越来越多&#xff0c;版本回归的压力越来越大&#xff0c;仅仅通过人工测试 来回归所以的版本肯定是不现实的&#xff0c;所以…