【list】list库介绍 + 简化模拟实现

news2024/10/7 8:23:31

本节博客先对list进行用法介绍,再在库的基础上简化其内容和形式,简单进行模拟实现,有需要借鉴即可。

目录

  • 1.list介绍
    • 1.1 list概述
    • 1.2相关接口的介绍
  • 2.简化模拟实现
  • 3.各部分的细节详述
    • 3.1结点
    • 3.2迭代器
      • 细节1:迭代器用原生指针还是专门设计为类的问题
      • 细节2:迭代器++、--行为重载的返回类型问题
      • 细节3:迭代器解引用返回类型
      • 细节4:迭代器operator->重载
    • 3.3链表
      • 细节1:list中提供begin和end函数的理由和返回类型?
      • 细节2:插入元素代码
      • 细节3:删除元素代码
      • 细节4:clear()函数和析构函数
      • 细节5:拷贝构造函数与赋值运算符重载
      • 细节6:insert返回为void?
  • 4.总结

1.list介绍

1.1 list概述

1.概述:list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
2. 底层实现:list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。

3. list与forward_list区别:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。

4. list的优势:与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。
5. list的缺陷:与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)

我们用代码来体会一下list的缺点:

void test_op1()
{
	srand(time(0));
	const int N = 1000000;//一百万数据

	//两个链表
	list<int> lt1;
	list<int> lt2;
	
	//一个顺序表
	vector<int> v;
	
	//生成随机数据,尾插到链表1和顺序表v中去
	for (int i = 0; i < N; ++i)
	{
		auto e = rand()+i;//加上这个i主要是为了减少重复数字概率
		lt1.push_back(e);
		v.push_back(e);
	}

	//vector排序
	int begin1 = clock();
	sort(v.begin(), v.end());
	int end1 = clock();
	
	//list排序
	int begin2 = clock();
	lt1.sort();
	int end2 = clock();
	
	//打印比较两者用时
	printf("vector sort:%d\n", end1 - begin1);
	printf("list sort:%d\n", end2 - begin2);
}
void test_op2()
{
	srand(time(0));
	const int N = 1000000;

	list<int> lt1;
	list<int> lt2;

	for (int i = 0; i < N; ++i)
	{
		auto e = rand();
		lt1.push_back(e);
		lt2.push_back(e);
	}

	// 拷贝vector
	int begin1 = clock();
	vector<int> v(lt2.begin(), lt2.end());
	
	// 排序
	sort(v.begin(), v.end());

	// 拷贝回lt2
	lt2.assign(v.begin(), v.end());
	int end1 = clock();
	
	//lt1排序
	int begin2 = clock();
	lt1.sort();
	int end2 = clock();
	
	//打印
	printf("list copy vector sort copy list sort:%d\n", end1 - begin1);
	printf("list sort:%d\n", end2 - begin2);
}

1.2相关接口的介绍

在这里插入图片描述

2.简化模拟实现

通过去查看stl中list.h的源码我们可以知道,list是通过一个_head的Node*指针进行维护的,而其中广泛使用迭代器进行传值和访问数据。下面对其先直接摆代码,然后对其中细节进行详细介绍。

#pragma once

#include<assert.h>
#include<iostream>

namespace zzg
{
	template<typename T>
	struct ListNode
	{
		ListNode<T>* _next;//这个地方为什么类型不是T*???答:因为我们指针是需要指向一个ListNode<T>*类型的,而非T类型。
		ListNode<T>* _prev;
		T _data;

		//ListNode有参构造
		ListNode(const T& x = T())
			:_next(nullptr)
			,_prev(nullptr)
			, _data(x)
			{} 
	};

	template<class T, class Ref, class Ptr>
	class list_iterator
	{
		typedef struct ListNode<T> Node;
		typedef list_iterator<T, Ref, Ptr> Self;
	public:
		Node* _node;
	public:
		//带参构造
		list_iterator(Node* node)//这个地方用值拷贝,用引用会有bug
			:_node(node)
		{}

		//++
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		
		Self operator++(int)//后置++
		{
			Self temp = *this;//拷贝构造一份,这时候会调用编译器自动生成的拷贝构造,为浅拷贝,但是需求满足了。
			_node = _node->_next;
			return temp;//这个地方得返回值了,因为现在的Self已经变了
		}
		//--
		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		Self operator--(int)//后置--
		{
			Self temp = *this;//拷贝构造一份,这时候会调用编译器自动生成的拷贝构造,为浅拷贝,但是需求满足了。
			_node = _node->_prev;
			return temp;//这个地方得返回值了,因为现在的Self已经变了
		}

		//*it
		Ref operator*()
		{
			return _node->_data;
		}

		//!=
		bool operator!=(const Self& it) 
		{
			return _node != it._node;
		}
		//==
		bool operator==(const Self& it)
		{
			return _node == it._node;
		}

		//->重载
		Ptr operator->()
		{
			return &_node->_data;
		}
	};

	//template<typename T>
	//class list_const_iterator
	//{
	//	typedef struct ListNode<T> Node;
	//	typedef list_const_iterator<T> Self;
	//public:
	//	Node* _node;
	//public:
	//	//带参构造
	//	list_const_iterator(Node* node)//这个地方用值拷贝,用引用会有bug
	//		:_node(node)
	//	{}

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

	//	Self operator++(int)//后置++
	//	{
	//		Self temp = *this;//拷贝构造一份,这时候会调用编译器自动生成的拷贝构造,为浅拷贝,但是需求满足了。
	//		_node = _node->_next;
	//		return temp;//这个地方得返回值了,因为现在的Self已经变了
	//	}
	//	//--
	//	Self& operator--()
	//	{
	//		_node = _node->_prev;
	//		return *this;
	//	}

	//	Self operator--(int)//后置--
	//	{
	//		Self temp = *this;//拷贝构造一份,这时候会调用编译器自动生成的拷贝构造,为浅拷贝,但是需求满足了。
	//		_node = _node->_prev;
	//		return temp;//这个地方得返回值了,因为现在的Self已经变了
	//	}

	//	//*it
	//	const T& operator*()
	//	{
	//		return _node->_data;
	//	}

	//	//!=
	//	bool operator!=(const Self& it)
	//	{
	//		return _node != it._node;
	//	}
	//	//==
	//	bool operator==(const Self& it)
	//	{
	//		return _node == it._node;
	//	}

	//	//->重载
	//	const T* operator->()
	//	{
	//		return &_node->_data;
	//	}

	//};

	template<typename T>
	class list
	{
	public:
		typedef list_iterator<T, T&, T*> iterator;
		typedef list_iterator<T, const T&, const T*> const_iterator;
		typedef struct ListNode<T> Node;

	private:
		Node* _head;//头节点

	public:
		//无参构造函数
		list()
		{
			empty_init();
		}

		//拷贝构造
		list(const list<T>& lt)
		{
			empty_init();
			for (auto& e : lt)
			{
				push_back(e);
			}
		}

		// lt1 = lt3
		list<T>& operator=(list<T> lt)//先调用拷贝构造,构造出一个lt来
		{
			swap(lt);//然后交换这个局部变量与this,原this中是其他的东西
			return *this;//返回this本身
		}

		void empty_init()
		{
			_head = new Node;

			_head->_next = _head;
			_head->_prev = _head;
		}

		//清理函数
		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

		//析构函数
		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

		iterator begin()
		{
			return _head->_next;//隐式类型转换
			//return iterator(_head->_next);
		}

		iterator end()
		{
			return _head;//隐式类型转换
			//return iterator(_head);
		}
		
		//const + 迭代器 --> 迭代器本身不可修改
		//我们需要的:迭代器指向的内容不可修改 const T*类型 而不是 T* const类型
		//如果我们直接在一般迭代器前面+const,即const iterator --> 该迭代器不可修改,因为这是一个自定义类
		//解决,直接再单独一个自定义const迭代器类出来
		const_iterator begin() const
		{
			return _head->_next;//return iterator(_head->_next);
		}

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

		//尾插
		void push_back(const T& x)
		{
			insert(end(), x);
		}
		//头插
		void push_front(const T& x)
		{
			insert(begin(), x);
		}
		
		//void push_back(const T& x)
		//{
		//	Node* newnode = new Node(x);

		//	//找到尾巴
		//	Node* tail = _head->_prev;
		//	tail->_next = newnode;//链接临近两点
		//	newnode->_prev = tail;

		//	newnode->_next = _head;
		//	_head->_prev = newnode;
		//}

		//任意插入
		void insert(iterator pos, const T& val)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(val);

			prev->_next = newnode;
			newnode->_prev = prev;

			newnode->_next = cur;
			cur->_prev = newnode;
		}

		//任意删除
		iterator erase(iterator pos)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;

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

			delete cur;

			return iterator(next);
		}
		//尾删
		void pop_back()
		{
			erase(--end());
		}
		//头删
		void pop_front()
		{
			erase(begin());
		}
 
	};

	struct A
	{
	public:
		int _a;
		int _b;

		A(int a = 0, int b = 0)
		{
			_a = a;
			_b = b;
		}
	};

	//测试函数
	void test_list1()
	{
		/*list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);
		lt.push_back(6);

		lt.pop_back();
		lt.pop_back();
		lt.pop_back();

		list<int>::iterator it = lt.begin();
		while (it != lt.end())
		{
			std::cout << *it << " ";
			it++;
		}
		std::cout << std::endl;*/

		list<A> Al;
		Al.push_back({ 1,1 });
		Al.push_back({ 2,2 });
		Al.push_back({ 3,3 });
		Al.push_back({ 4,4 });
		Al.push_back({ 5,5 });
		Al.push_back({ 6,6 });

		list<A>::iterator it = Al.begin();
		while (it != Al.end())
		{
			//std::cout << (*it)._a << '/' << (*it)._b << std::endl;
			std::cout << it->_a << '/' << it->_b << std::endl;//it->_a  --->  it->->_a == it.operator->()->_a;
			it++;
		}
		std::cout << std::endl;
	}
}

3.各部分的细节详述

主要包含三个部分,一是整体的链表类,二是链表中的每个元素结点类,还有就是用来访问修改结点的迭代器类

下面分开进行细节介绍。

3.1结点

每个结点我们很熟悉,无非是两个指针和一个数据,一个指针指向它前面的结点,另一个指向其后面的结点,数据中放具体元素的值。

下面是基本结构框架:

template<typename T>
struct ListNode
{
	ListNode<T>* _next;//这个地方为什么类型不是T*???答:因为我们指针是需要指向一个ListNode<T>*类型的,而非T类型。
	ListNode<T>* _prev;
	T _data;

	//ListNode有参构造
	ListNode(const T& x = T())
		:_next(nullptr)
		,_prev(nullptr)
		, _data(x)
		{} 
};

细节:

  • 使用struct,标注为公有属性,方便外部调用
  • list是带头双向循环链表,因而每个结点要有两个指针
  • 提供全缺省的默认构造函数

思考:每个结点的两个指针为什么是ListNode*类型而不是T*类型呢?
答:因为我们每个结点的指针指向的是一个结点,T仅仅是一个结点中的数据而已。

3.2迭代器

template<class T, class Ref, class Ptr>
class list_iterator
{
	typedef struct ListNode<T> Node;
	typedef list_iterator<T, Ref, Ptr> Self;
public:
	Node* _node;
public:
	//带参构造
	list_iterator(Node* node)//这个地方用值拷贝,用引用会有bug
		:_node(node)
	{}
}

细节1:迭代器用原生指针还是专门设计为类的问题

思考:list迭代器为什么要专门设置一个类???
答:
这是由于list的每个节点物理空间不连续,导致迭代器不能像之前string\vector那样简单的设计为原生指针,而是设计为一个类,以此来扩大我们对迭代器行为控制权限,重新设计*,->,++等操作。

vector,string原生指针充当迭代器:
像之前string,vector这种容器,其原生指针T*就是天然的迭代器,因为++就会自动指向到下一个数据,*引用也是拿到的我们想要的数据。
但是在list中,我们++T*,很明显由于地址不连续的缘故,压根不知道会指向的是什么(大概率会是随机值)。
在这里插入图片描述

但是需要重点注意的是,我们所设计的迭代器类是模拟的string/vector中T*的动作。

细节2:迭代器++、–行为重载的返回类型问题

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

Self operator++(int)//后置++
{
	Self temp = *this;//拷贝构造一份,这时候会调用编译器自动生成的拷贝构造,为浅拷贝,但是需求满足了。
	_node = _node->_next;
	return temp;//这个地方得返回值了,因为现在的Self已经变了
}
//--
Self& operator--()
{
	_node = _node->_prev;
	return *this;
}

Self operator--(int)//后置--
{
	Self temp = *this;//拷贝构造一份,这时候会调用编译器自动生成的拷贝构造,为浅拷贝,但是需求满足了。
	_node = _node->_prev;
	return temp;//这个地方得返回值了,因为现在的Self已经变了
}

思考:为什么前置++返回的是类对象引用,而后置++返回类型是一般类型?
答:这要结合函数设计来看,在前置++中,我们返回的是类本身;而后置++,我们返回的是一个局部的类对象,局部类对象在出函数后会自动销毁

细节3:迭代器解引用返回类型

//*it
Ref operator*()
{
	return _node->_data;
}

注:

//iterator:
typedef list_iterator<T, Ref, Ptr> Self;
//list:
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;

返回Ref,用来区分const_iterator 和 iterator。
在这里插入图片描述

思考:为什么不重载const iterator?
答:const + 类 --> 表示该指针不可修改,并非我们所期望的指针指向内容不可修改。

思考:iterator 与 const_iterator 是同一个类吗?
答:不是。是利用同一份类模板生成的完全不同两份类。

细节4:迭代器operator->重载

//->重载
Ptr operator->()
{
	return &_node->_data;
}

注:

//iterator:
typedef list_iterator<T, Ref, Ptr> Self;
//list:
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
struct A
{
public:
	int _a;
	int _b;

	A(int a = 0, int b = 0)
	{
		_a = a;
		_b = b;
	}
};

main:
list<A> Al;
Al.push_back({ 1,1 });
Al.push_back({ 2,2 });
Al.push_back({ 3,3 });
Al.push_back({ 4,4 });
Al.push_back({ 5,5 });
Al.push_back({ 6,6 });

list<A>::iterator it = Al.begin();
while (it != Al.end())
{
	//std::cout << (*it)._a << '/' << (*it)._b << std::endl;
	std::cout << it->_a << '/' << it->_b << std::endl;//it->_a  --->  it->->_a == it.operator->()->_a;
	it++;
}
std::cout << std::endl;

为了可读性,编译器把it->->_a优化为了it->_a

思考:上述代码的it->->_a代表了什么?
答:it->_a —> it->->_a == it.operator->()->_a;,这里在编译器写法上理论上应该写两个箭头的,一个用于运算符重载函数的调用,另一个是为了进入到指针里面访问数据,这里编译器为了可读性将其优化为了一个箭头。

3.3链表

链表类主要结构如下:

template<typename T>
class list
{
public:
	typedef list_iterator<T, T&, T*> iterator;
	typedef list_iterator<T, const T&, const T*> const_iterator;
	typedef struct ListNode<T> Node;
	
	//无参构造函数
	list()
	{
		empty_init();
	}
	void empty_init()
	{
		_head = new Node;

		_head->_next = _head;
		_head->_prev = _head;
	}
	
private:
	Node* _head;//头节点
}

细节1:list中提供begin和end函数的理由和返回类型?

iterator begin()
{
	return _head->_next;//隐式类型转换
	//return iterator(_head->_next);
}

iterator end()
{
	return _head;//隐式类型转换
	//return iterator(_head);
}

//const + 迭代器 --> 迭代器本身不可修改
//我们需要的:迭代器指向的内容不可修改 const T*类型 而不是 T* const类型
//如果我们直接在一般迭代器前面+const,即const iterator --> 该迭代器不可修改,因为这是一个自定义类
//解决,直接再单独一个自定义const迭代器类出来
const_iterator begin() const
{
	return _head->_next;//return iterator(_head->_next);
}

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

思考:list提供begin和end的原因:
答:
1._head是私有的。
2.更加方便的使用迭代器,在上面代码我们可以发现,返回的都是迭代器类型。

返回迭代器类型的原因:
与后面使用迭代器相兼容。

细节2:插入元素代码

//尾插
void push_back(const T& x)
{
	insert(end(), x);
}
//头插
void push_front(const T& x)
{
	insert(begin(), x);
}

//void push_back(const T& x)
//{
//	Node* newnode = new Node(x);

//	//找到尾巴
//	Node* tail = _head->_prev;
//	tail->_next = newnode;//链接临近两点
//	newnode->_prev = tail;

//	newnode->_next = _head;
//	_head->_prev = newnode;
//}

//任意插入
void insert(iterator pos, const T& val)
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* newnode = new Node(val);

	prev->_next = newnode;
	newnode->_prev = prev;

	newnode->_next = cur;
	cur->_prev = newnode;
}

细节3:删除元素代码

//任意删除
iterator erase(iterator pos)
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;

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

	delete cur;

	return iterator(next);
}
//尾删
void pop_back()
{
	erase(--end());
}
//头删
void pop_front()
{
	erase(begin());
}

思考:为什么erase()要返回迭代器类型???
答:因为要及时对外更新迭代器指针,防止迭代器失效

思考:为什么pop_back()没有返回迭代器类型?
答:因为pop_back()不会对外接收迭代器,不存在对外更新迭代器问题。但是erase是接收迭代器的,因而要及时更新。
在这里插入图片描述
思考:–end()是否会影响到_head,为什么?
答:会影响到,但是影响到的是_head的值拷贝,没有影响到“母体”。
在这里插入图片描述

细节4:clear()函数和析构函数

void empty_init()
{
	_head = new Node;

	_head->_next = _head;
	_head->_prev = _head;
}

//清理函数
void clear()
{
	iterator it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}

//析构函数
~list()
{
	clear();
	delete _head;
	_head = nullptr;
}

细节5:拷贝构造函数与赋值运算符重载

//拷贝构造
list(const list<T>& lt)
{
	empty_init();
	for (auto& e : lt)
	{
		push_back(e);
	}
}
// lt1 = lt3
list<T>& operator=(list<T> lt)//先调用拷贝构造,构造出一个lt来
{
	swap(lt);//然后交换这个局部变量与this,原this中是其他的东西
	return *this;//返回this本身
}

思考:为什么赋值运算符重载函数参数用list lt而不是引用呢?
答:复用拷贝构造函数,是为现代写法。

细节6:insert返回为void?

//任意插入
void insert(iterator pos, const T& val)
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* newnode = new Node(val);

	prev->_next = newnode;
	newnode->_prev = prev;

	newnode->_next = cur;
	cur->_prev = newnode;
}

在string中insert我们返回的是迭代器,但是这里为什么返回值是void呢?
答:因为string是连续空间,插入数据会挪动数据,造成迭代器失效。但是链表是由结点链接而成,插入数据不会挪动数据,不会造成迭代器失效问题。

在vector中 insert/erase因为增删都会牵扯到数据挪动问题,两个函数肯定都要去返回迭代器来更新外部迭代器。
但是对于list,insert不会挪动数据因而不会失效,但是erase时候,原结点被删除,会造成迭代器失效。
在这里插入图片描述

4.总结

list模拟实现核心就是一个类迭代器的实现,相比之前string、vector,list迭代器更值得细细思考与总结。
list模拟实现还一个难点在于使用类模板,应注意类模板问题。


EOF

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

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

相关文章

【动态规划】斐波那契数列模型(C++)

目录 1137.第N个泰波那契数 解法&#xff08;动态规划&#xff09; 算法流程 1. 状态表⽰&#xff1a; 2. 状态转移⽅程&#xff1a; 3. 初始化&#xff1a; 4. 填表顺序&#xff1a; 5. 返回值&#xff1a; C算法代码 优化&#xff1a; 滚动数组 测试&#xff1a; …

电脑提示请重新安装软件MSVCP140.dll的几种解决方法分享

在日常使用电脑的过程中&#xff0c;我们常常会遇到一些错误提示&#xff0c;其中之一就是找不到msvcp140.dll文件&#xff0c;导致软件无法正常启动运行。这个问题可能是由于缺少相应的依赖库或者版本不匹配引起的。下面我将介绍5种解决方法&#xff0c;帮助大家解决这个问题。…

0524_网络编程8

思维导图&#xff1a;

Java基础的语法---StringBuilder

StringBuilder 构造方法 StringBuilder()&#xff1a;创建一个空的StringBuilder实例。 StringBuilder(String str)&#xff1a;创建一个StringBuilder实例&#xff0c;并将其初始化为指定的字符串内容。 StringBuilder(int a): 创建一个StringBuilder实例…

数据结构--《二叉树》

二叉树 1、什么是二叉树 二叉树(Binar Tree)是n(n>0)个结点的优先集合&#xff0c;该集合或者为空集(称为空二叉树)&#xff0c;或者由一个根结点和两颗互不相交的、分别称为根结点的左子树和右子树的二叉树构成。 这里给张图&#xff0c;能更直观的感受二叉树&#xff1…

AJAX初级

AJAX的概念&#xff1a; 使用浏览器的 XMLHttpRequest 对象 与服务器通信 浏览器网页中&#xff0c;使用 AJAX技术&#xff08;XHR对象&#xff09;发起获取省份列表数据的请求&#xff0c;服务器代码响应准备好的省份列表数据给前端&#xff0c;前端拿到数据数组以后&#xf…

手把手教学,一站式教你实现服务器(Ubuntu)Anaconda多用户共享

背景&#xff1a;书接上回&#xff0c;一站式安装Ubuntu及配置服务器手把手教学&#xff0c;一站式安装ubuntu及配置服务器-CSDN博客 在安装及配置好服务器后&#xff0c;因为课题组可能涉及多个用户共用一台服务器&#xff0c;为了防止服务器上代码误删和Anaconda环境管理混乱…

js之图表使用

今天为了给大家演示图表的使用,今天展示下切换图形的修改属性快速修改 <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title><script src"./js/jquery-3.7.1.js"></script><script src…

Android 使用 adb 列出设备上所有危险权限

步骤1&#xff1a;确定 Android SDK 位置 打开 Android Studio 的设置&#xff0c;并来到 Languages & Frameworks › Android SDK 处&#xff1a; 这里可以看到 Android SDK 目录的位置&#xff1a; 例如&#xff1a;/Users/admin/Library/Android/sdk。 复制这个路径&am…

RAG概述(一):RAG架构的演进

目录 概述 RAG核心步骤 Indexing索引 Retrieval检索 Generation生成​​​​​​​ Native RAG Advanced RAG Modular RAG 参考 概述 RAG&#xff1a;Retrieval-Augmented Generation 检索增强生成。 RAG通过结合LLMs的内在知识和外部数据库的非参数化数据&#xff…

区间合并-leetcode合并石头的最低成本-XMUOJ元素共鸣:深层次的唤醒

题目 思路 话不多说&#xff0c;直接上代码 附上INT_MAX和INT_MIN 【C】详解 INT_MAX 和 INT_MIN&#xff08;INT_MAX 和 INT_MIN是什么&#xff1f;它们的用途是什么&#xff1f;如何防止溢出&#xff1f;&#xff09;_c int max-CSDN博客 代码 /* leetcode合并石头的最低…

未授权访问:Hadoop 未授权访问漏洞

目录 1、漏洞原理 2、环境搭建 3、未授权访问 4、通过REST API命令执行 防御手段 今天继续学习各种未授权访问的知识和相关的实操实验&#xff0c;一共有好多篇&#xff0c;内容主要是参考先知社区的一位大佬的关于未授权访问的好文章&#xff0c;还有其他大佬总结好的文章…

【机器学习数据可视化-07】波士顿房价预测数据分析

波士顿房价预测&#xff1a;基于数据可视化的深入探索 一、引言   在当今社会&#xff0c;房地产市场作为经济的重要支柱之一&#xff0c;其走势与波动直接影响着国家经济的稳定和人民生活的品质。波士顿&#xff0c;这座历史悠久且充满活力的城市&#xff0c;其房地产市场一…

ElasticSearch学习篇12_《检索技术核心20讲》基础篇

背景 学习极客实践课程《检索技术核心20讲》https://time.geekbang.org/column/article/215243 课程分为基础篇、进阶篇、系统案例篇 主要记录企业课程学习过程课程大纲关键点&#xff0c;以文档形式记录笔记。 内容 检索技术&#xff1a;它是更底层的通用技术&#xff0c…

如何用bet快速创建文件夹多个同级文件夹,多层子文件夹

第一种用txt编辑&#xff0c;保存格式改为bat 运行即可 md用来创建文件夹 md空格文件夹名字 或者 md空格文件夹名字\子文件夹名字 第一个创建一个文件夹&#xff0c;或者多个同级文件夹用空格隔开或者用,英文逗号隔开 md 00 md 00 md 11 md 22 md 33 或者 md 00 1…

Python 中别再用 ‘+‘ 拼接字符串了!

当我开始学习 Python 时&#xff0c;使用加号来连接字符串非常直观和容易&#xff0c;就像许多其他编程语言&#xff08;比如Java&#xff09;一样。 然而&#xff0c;很快我意识到许多开发者似乎更喜欢使用.join()方法而不是。 在本文中&#xff0c;我将介绍这两种方法之间的…

Charles抓包App_https_夜神模拟器

Openssl安装 下载安装 下载地址&#xff1a; http://slproweb.com/products/Win32OpenSSL.html 我已经下载好了64位的&#xff0c;也放出来&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1Nkur475YK48_Ayq_vEm99w?pwdf4d7 提取码&#xff1a;f4d7 --来自百度网…

Golang实现文件复制

方法&#xff1a;三种 package zdpgo_fileimport ("errors""io""os" )// CopyFile 使用io.Copy进行文件的复制&#xff0c;同时也会复制文件的所有权限 // param src 复制文件 // param des 目标文件 // return error 错误信息 func CopyFile(s…

MacOS安装Docker-Compose

方法一 按照百度的方法进行安装&#xff0c;我大致复制一下百度的方法 1.确保您已经安装了Docker Desktop。 2.下载Docker Compose二进制文件。 3.将二进制文件移动到/usr/local/bin目录。 4.更改二进制文件的权限&#xff0c;使其可执行。 以下是具体的命令&#xff1a;…

LiveGBS流媒体平台GB/T28181用户手册-用户管理:添加用户、编辑、关联通道、搜索、重置密码

LiveGBS流媒体平台GB/T28181用户手册-用户管理:添加用户、编辑、关联通道、搜索、重置密码 1、用户管理1.1、添加用户1.2、编辑用户1.3、关联通道1.4、重置密码1.5、搜索1.6、删除 2、搭建GB28181视频直播平台 1、用户管理 1.1、添加用户 添加用户&#xff0c;可以配置登陆用户…