C++ [STL之list模拟实现]

news2024/10/6 12:18:57

list模拟实现

本文已收录至《C++语言和高级数据结构》专栏!
作者:ARMCSKGT

CSDN


STL之list模拟实现

  • 前言
  • 正文
    • 基本框架
      • 节点类
      • 迭代器类
      • list类
    • 迭代器类功能实现
      • list迭代器
      • 迭代器设计思想
      • 迭代器操作设计
    • list类功能实现
      • 默认成员函数
      • 容量查询
      • 数据访问
      • 节点插删相关
        • 头尾插删
        • 任意位置插删
      • 其他函数
  • 最后


前言

list的底层与vector和string不同,实现也有所差别,特别是在迭代器的设计上,本节将为大家介绍list简单实现,并揭开list迭代器的底层!
list-出自STL源码剖析


正文

本文介绍list部分简单接口,以list迭代器的介绍为主!

基本框架


list底层是一个带头双向循环链表,在节点上变化不大,主要是操作!

list整体由三个类组成:

  • 节点类(封装一个节点)
  • 迭代器类
  • list类

节点类

list节点类是一个模板类以适合于任何类型的list对象,封装了一个节点需要的基本成员:

  • 成员变量
    – 前驱指针 _prev
    – 后继指针 _next
    – 数据存储 _data
  • 成员函数:只有一个构造函数,用于初始化节点data数据和置空指针,构造函数的value参数是缺省参数,适应不同场景
    注意:节点类不需要拷贝构造和析构函数,对于节点的拷贝构造在list中只需要浅拷贝即可,节点的释放也不在节点类中进行!
//节点类
template<class T>
struct list_node
{
	list_node<T>* _prev, * _next;
	T _data;

	list_node(const T& value = T())
		:_prev(nullptr) //指针置空
		, _next(nullptr)
		, _data(value) //初始化data
	{}
};

迭代器类

迭代器类也是封装节点,但是迭代器类不会创造节点,只会利用现有节点构造一个迭代器,在迭代器内部通过各种运算符重载对节点的状态做修改!

其次,我们在使用迭代器时,会涉及返回节点指针或数据data的引用,所以需要在构造对象时将list数据类型的指针和引用类型传给迭代器对象!

迭代器在不访问data的情况下返回的都是迭代器,为了书写简洁,我们事先声明本迭代器类型self(即实例的类型),在函数操作完成时返回self(迭代器)即可!

template<class T, class Ref, class Ptr>
struct __list_iterator
{
	typedef list_node<T> node; //节点指针
	typedef __list_iterator<T, Ref, Ptr> self; //迭代器对象(方便返回迭代器)

	node* _node; //节点

	__list_iterator(node* n) //默认构造函数
		:_node(n)
	{}
};

注意,迭代器对象不会凭空创造节点,只会利用现有的节点(迭代器节点类型与list节点类型相同),所以不需要拷贝构造和析构函数,浅拷贝足以!


list类

list类中包含一个头节点和迭代器以及各种操作函数!

template<class T>
class list
{
	typedef list_node<T> node; //node节点类型
public:
	typedef T& reference; //T&引用类型
	typedef const T& const_reference; //const T&引用类型

private:
	node* _head; //头节点
};

这里有些类型为了见名知意,进行了 typedef !


迭代器类功能实现


在介绍list之前,我们先介绍list迭代器部分!

list迭代器

list迭代器是一个单独的struct类,使用struct是为了公开成员供用户使用!
虽然list不支持下标随机访问,但是我们希望list仍然像vector一样使用++和 - - 访问每一个节点,对于list对象,我们是无法实现的,因为对list定义的对象重载运算符++和 - - 操作会丢失节点,因为list要保存完整的节点信息,所以只能把这个功能外包给迭代器类!
为什么使用迭代器
list的是双向链表,就可以往后走也可以往前走,所以list迭代器是双向迭代器,但是list迭代器不支持随机访问,也就是只支持begin++和begin - -,不支持加整数(begin+3等)随机访问 (不支持随机访问) ,毕竟不是连续的空间!

迭代器分类:

  • 单向迭代器:只支持 ++ 或 - - 一个方向上的遍历(即正向或反向)
  • 双向迭代器:支持++和 - - 前后遍历
  • 随机迭代器:支持通过加上一个整数的方式跳跃式随机访问(例如begin+3,只有随机迭代器才能使用库函数sort)

迭代器设计思想

我们规定list迭代器begin指向头节点所指向的下一个节点(链表中的第一个有效节点),end指向头节点,返回的都是迭代器对象,遍历时从第一个节点开始遍历一直到头节点结束!
迭代器设计
首先,我们的迭代器需要list实例化的数据类型,list精华在于迭代器类的设计,而迭代器类中的精华在于多参数模板,这种传多模板参数的方法,巧妙的解决了 普通对象const对象 的冗余设计问题!

迭代器分为 iteratorconst_iterator,不同的对象调用不同的迭代器类型,如果不用多参数模板,就需要写两份相差不大的迭代器类,造成代码冗余!

多参数模板:

  • T:节点值数据类型
  • Ref:节点值数据类型引用类型
  • Ptr:节点值数据类型指针类型

普通迭代器和const迭代器
使用不同的参数组合可以实例化出不同的迭代器,这正是 泛型编程 思想之一!

因为返回类型都为迭代器对象,所以可以使用匿名对象进行构造!

list迭代器定义:

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;

	//普通迭代器
	iterator begin() { return iterator(_head->_next); } 
	iterator end() { return iterator(_head); }

	//普通迭代器const重载版本	
	const_iterator begin() const { return const_iterator(_head->_next); }
	const_iterator end() const { return const_iterator(_head); }

	//const迭代器
	const_iterator cbegin() const { return const_iterator(_head->_next); } 
	const_iterator cend() const { return const_iterator(_head); }

private:
	node* _head;
};

在某些场景下,使用const引用list对象传参,在内部定义迭代器或使用范围for,编译器默认范围for使用begin和end迭代器,而我们加上const而编译器无法匹配迭代器,所以普通迭代器我们提供const版本去适应不同场景!

// 例如该场景,如果没用const普通迭代器则报错
void Print(const list<int>& ls)
{
	for (const auto& x : ls) //内部范围for调用的是const普通迭代器begin和end
		cout << x << " ";

	cout << endl;
}

在后续涉及list对象打印所有节点参数的时候会调用此函数!


迭代器操作设计

迭代器遍历:
对于迭代器的遍历,无非是前置++和 - - 以及后置++和 - - !
对于vector,是一片连续的空间,++相当于走到下一个元素,- - 相当于回到上一个元素!
但是list是一个一个的节点,并非连续的空间,对于list迭代器的++和 - - 只能手动走向下一个节点,即_node = _node->_next !
list与vector的迭代器
注意: 我们直接对list迭代器中的_node进行++和 - - ,因为不是连续空间,直接进行++和 - - 会造成野指针问题;而且我们迭代器在切换节点后需要返回一个迭代器,所以返回self类型(当前迭代器类型)即可!

//返回__list_iterator对象引用
self& operator++()//前置++
{
	_node = _node->_next;
	return *this;
}

//返回__list_iterator对象
self operator++(int)//后置++
{
	self tmp(*this);
	_node = _node->_next;
	return tmp;
}

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

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

对于前置操作: 我们只需要将当前迭代器节点走到下一个节点即可,然后返回迭代器!
对于后置操作: 我们需要先构造一个临时对象保存当前的节点,然后让迭代器走到下一个节点上,返回临时对象的节点即可完成后置操作!

注意: 虽然我们可以通过某些手段实现随机访问,但是效率极低,为了跟库保持一致不进行实现!

其他操作:
对于迭代器,如果我们list实例化的是指针,则需要 * 运算符解引用取值,以及因为我们嵌套了一层迭代器,在使用 -> 运算符时需要返回data的地址!

因为我们在使用迭代器遍历时需要有遍历条件作为终止条件,不妨会涉及迭代器的比较,所以还需要 != 和 == 的比较操作!

//迭代器比较
bool operator!=(const self& n) const { return _node != n._node; }
bool operator==(const self& n) const { return _node == n._node; }

//返回T&——解引用访问数据
Ref operator*() { return _node->_data; }

//返回T*——自定义类型需要->访问成员(其_data可能是一个自定义类型)
Ptr operator->() { return &_node->_data; }
void test()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	my_list::list<int> ls(arr, arr + 10); //迭代器区间构造
	auto it = ls.begin();
	while (it != ls.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	//当然,支持迭代器就支持范围for
	for (const auto& x : ls)
	{
		cout << x << " ";
	}
	cout << endl;
}

演示
迭代器中每时每刻只有一个节点指针,当我们进行遍历操作时内部改变即可,外部迭代器还是同一个迭代器对象!


list类功能实现


默认成员函数

对于list默认采成员函数无非就是我们常见的四种类默认采用函数:

  • 构造函数(默认构造和迭代器区间构造)
  • 拷贝构造函数
  • 赋值重载函数
  • 析构函数

构造函数
因为我们是带头的链表,所以在实例化时必须事先申请一个头节点,所以所有的构造函数都会在操作前申请一个头节点,为了避免代码的冗余,我们将头节点的创建封装为一个private函数,在构造函数初始化时调用创建头节点!

private:
	node* _head;

	void Empty_Init() //申请一个头节点初始化链表
	{
		_head = new node;
		_head->_next = _head->_prev = _head;
		_head->_data = T();
	}

默认构造函数需要支持构造一个空链表,也可以构造有n个value值节点的链表!
当我们需要构造空链表时,直接调用 Empty_Init 函数即可!

list() { Empty_Init(); } //默认构造

list(int n, const_reference value = T()) //构造有n个value值节点的链表!
{
	Empty_Init();
	while (n--) { push_back(value); }
}

但我们发现,构造有n个T类型值的链表的构造函数可以包含默认构造的功能,只需要一个缺省参数0即可;如果需要插入数据,调用push_back即可(库中没有这样实现,需要给定n指定的节点数)!

list(int n = 0, const_reference value = T())
{
	Empty_Init();
	while (n--) { push_back(value); }
}

list(size_t n = 0, const_reference value = T());  
//避免因为list(size_t,size_t)匹配上迭代器区间构造
//实现与以上函数一样,但因为迭代器区间构造冲突问题不进行实现

这里的 const_reference 相当于 const T& !

迭代器区间构造也是调用push_back进行尾插!

template <class InputIterator>//迭代器区间构造
list(InputIterator first, InputIterator last)
{
	Empty_Init();
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

实例演示:

void test()
{
	list<int> l1; //构造空的list对象
	list<int> l2(3,1); //构造有3个节点为1的list对象
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	list<int> l3(arr, arr + 10); //迭代器区间构造
	cout << "l1:"; Print(l1);
	cout << "l2:"; Print(l2);
	cout << "l3:"; Print(l3);
}

实例演示
Print函数是打印list对象节点的函数,在上面迭代器部分介绍过!

拷贝构造
拷贝构造需要注意深拷贝问题,当然对于自定义类型会调用其自带的拷贝构造,,其次就是对象参数需要引用,否则会引起无穷递归问题!

拷贝构造采用现代写法,构造临时对象使用swap交换,在此之前,因为是构造函数所以需要创造头节点,调用 Empty_Init() 函数,再进行拷贝!

list(const list<T>& l) //拷贝构造
{
	Empty_Init();
	list<T> tmp(l.begin(), l.end());
	swap(tmp);
}

实例演示:

void test()
{
	list<int> l1(3, 1); //构造有3个节点为1的list对象
	list<int> l2(l1); 
	Print(l2);
}

实例演示

赋值重载
对于赋值重载,与拷贝构造相似,但是赋值重载在传递参数时使用传值传参,这样就自动帮我们构造了一个临时对象,我们只需要swap取下临时对象的头节点即可,将我们现有的链表交给临时对象销毁,这样就完成了赋值;因为我们可能会连等(l1=l2=l3等等),所以需要返回对象的引用!

list<T>& operator=(list<T> l) //赋值重载
{
	swap(l);
	return *this;
}

实例演示:

void test()
{
	list<int> l1(5, 668); 
	list<int> l2(3,2); 

	cout << "赋值前:"; Print(l2);
	l2 = l1;
	cout << "赋值后:"; Print(l2);
}

实例演示

析构函数
析构函数需要释放所有节点以及头节点,在释放前需要判断当前链表是否为空,如果为空直接是否头节点即可!

~list()
{
	if(!empty()) //判断是否为空
		clear(); //clear函数会清空所有节点(除了头节点)

	delete _head; //释放头节点
	_head = nullptr; //头节点指针置空
}

容量查询

关于容量的查询,一共是查询节点数,另一个是判断链表是否为空!

  • 节点数size:只需要全部遍历一次计数即可
  • 判空empty:只需要判断头节点_head的_next指针是否指向自己即可
size_t size()const //获取节点数
{
	size_t sz = 0;
	const_iterator it = begin();
	while (it != end())
	{
		++sz;
		++it;
	}
	return sz;
}

bool empty()const { return _head->_next == _head; } //判空,为空返回true

当然,这里的实现是与库中统一,我们也可以单独定义一个 _n 整型变量进行计数!

实例演示:

void test()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	list<int> l1(arr, arr+10); 
	cout << "size:" << l1.size() << endl;
	cout << "empty:" << l1.empty() << endl;
	l1.clear(); //清空节点
	cout << "empty:" << l1.empty() << endl;
}

实例演示


数据访问

list常用迭代器进行数据访问,也提供了头尾节点的访问!
这里为了减少拷贝,返回的是data值的引用;当然,在访问前需要判断链表是否为空!

reference front() //访问头节点返回其data值的引用
{
	assert(!empty());
	return _head->_next->_data;
}

const_reference front() const //访问头节点返回其data值的const引用
{
	assert(!empty());
	return _head->_next->_data;
}

reference back() //访问尾节点返回其data值的引用
{
	assert(!empty());
	return _head->_prev->_data;
}

const_reference back() const //访问尾节点返回其data值的const引用
{
	assert(!empty());
	return _head->_prev->_data;
}

实例演示:

void test()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	list<int> l1(arr, arr+10); 
	cout << "l1:"; Print(l1);
	cout << "front node data:" << l1.front() << endl;
	cout << "back node data:" << l1.back() << endl;
}

实例演示


节点插删相关

插入删除是list的优点,只要我们拿到对应的节点,就可以立即对其进行插入和删除操作!
list插入和删除

头尾插删

头尾插删的操作主要是对头节点_head的_next节点和_prev节点进行操作!
头尾插删

头插
头插主要是修改原来的第一个有效节点与_head节点的链接关系,在其中链入一个新的节点!

  • 先根据参数构建一个新节点 newnode
  • 找到原第一个有效节点 frontnode
  • 修改 _headfrontnode 链接关系,将 newnode 链入到两节点中
void push_front(const_reference val) //头插
{
	node* newnode = new node(val); //新节点
	node* frontnode = _head->_next; //当前第一个有效节点

	//修改 _head 和 frontnode 链接关系,将 newnode 链入到两节点中
	_head->_next = newnode; //头节点指向新节点
	newnode->_prev = _head; //新节点的前驱指向头节点
	newnode->_next = frontnode; //新节点指向原第一个有效节点
	frontnode->_prev = newnode; //向原第一个有效节点的前驱更新为新节点
}

头删
头删也是修改原来的第一个有效节点与_head节点的链接关系,删除第一个有效节点转而让_head指向第二个有效节点!

  • 在删除前检查是否存在有效节点
  • 记录当前第一个有效节点frontnode
  • 记录第二个有效节点frontnext
  • 修改 _head 与第二个有效节点链接关系,将 frontnode 节点剥离链表
  • 删除 frontnode 节点
void pop_front() //头删
{
	assert(!empty()); //检查是否为空链表
	node* frontnode = _head->_next; //当前第一个有效节点
	node* frontnext = frontnode->_next; //记录第二个有效节点

	//让_head与第二个有效节点链接 - 剥离第一个有效节点
	_head->_next = frontnext;
	frontnext->_prev = _head;

	//删除原第一个有效节点
	delete frontnode;
	frontnode = nullptr;
}

尾插
尾插节点与头插一样,修改 _headbacknode 的链接关系,链入新节点!

  • 根据参数创建一个新节点 newnode
  • 记录原尾节点 backnode
  • 关系 _headbacknode 之间的链接关系,在其中链入 newnode
void push_back(const_reference val) //尾插
{
	node* newnode = new node(val); //新节点
	node* backnode = _head->_prev; //尾节点

	//更新链接关系
	_head->_prev = newnode;
	newnode->_next = _head;
	newnode->_prev = backnode;
	backnode->_next = newnode;
}

尾删
尾删与头删一样,剥离尾节点,修复链接关系即可!

  • 在删除前检查是否存在有效节点
  • 记录原尾节点 backnode
  • 记录尾节点的前驱节点 backprev
  • 更新 _headbackprev 节点的链接更新,剥离尾节点 backnode
  • 删除尾节点 backnode
void pop_back() //尾删
{
	assert(!empty()); //检查是否为空链表
	node* backnode = _head->_prev; //尾节点
	node* backprev = backnode->_prev; //尾节点前驱

	//更新链接更新-剥离尾节点
	_head->_prev = backprev;
	backprev->_next = _head;

	//删除尾节点
	delete backnode;
	backnode = nullptr;
}

实例演示:

void test()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	list<int> l1;
	for (const auto& x : arr)
	{
		l1.push_front(x);
		l1.push_back(x);
	}
	while (!l1.empty())
	{
		Print(l1);
		l1.pop_front();
		l1.pop_back();
	}
}

实例演示

任意位置插删

任意位置插删,我们约定使用迭代器指定插入位置,在当前迭代器节点前插入一个节点!

注意: 无论是任意位置删除还是任意位置插入,在操作后都需要返回一个迭代器!

  • 对于任意位置插入,返回的是新插入节点的迭代器
  • 对于任意位置删除,返回的是删除节点的下一个节点的迭代器

任意位置插入
任意位置插入是在pos迭代器位置前插入一个节点,并返回这个节点的迭代器!

  • 检查pos迭代器中节点是否正常
  • 根据参数申请一个新节点 newnode
  • 获取pos位置节点的前驱节点 posprev
  • 将新节点 newnode 链入 pos节点pos前驱节点 之间
  • 链入成功后返回新插入节点的迭代器
iterator insert(iterator pos, const_reference val)
{
	assert(pos._node); //确保节点非空
	node* newnode = new node(val); //创建新节点
	node* posprev = pos._node->_prev; //获取pos节点的前驱

	//更新链接关系 - 将新节点链入pos节点与pos前驱节点之间
	newnode->_next = pos._node;
	newnode->_prev = posprev;
	posprev->_next = newnode;
	pos._node->_prev = newnode;
	
	//返回新节点迭代器	
	return iterator(newnode);
}

任意位置删除
任意位置删除是删除pos迭代器位置的节点,并返回删除节点的下一个节点的迭代器!

  • 检查链表是否为空pos迭代器中节点是否正常pos节点不为_head头节点
  • 获取 pos节点前驱节点posprev
  • 获取 pos节点后继节点posnext
  • 链接 posprevposnext 节点,剥离pos节点
  • 删除pos节点
  • 返回 posnext节点迭代器(即pos的下一个节点的迭代器)
iterator erase(iterator pos)
{
	//链表非空&确保节点非空&不为头节点_head
	assert(!empty() && pos._node && pos._node != _head); 

	node* posprev = pos._node->_prev; //pos的下一个节点
	node* posnext = pos._node->_next; //pos的上一个节点
	node* freenode = pos._node;

	//链接posprev和posnext 剥离 pos 节点
	posprev->_next = posnext;
	posnext->_prev = posprev;

	delete freenode; //释放pos节点
	return iterator(posnext); //返回pos节点的下一个节点的迭代器
}

当我们实现了任意位置插入和删除,头尾插删就可以简化代码复用任意位置插删函数了!

//头插
void push_front(const_reference val) { insert(begin(), val); }

//尾插
void push_back(const_reference val) { insert(end(), val); }

//头删
void pop_front() { erase(begin()); }

//尾删
void pop_back() { erase(--end()); }

实例演示:

void test()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	list<int> l1(arr,arr+10);
	Print(l1);
	l1.insert(++l1.begin(), 668); //在2前插入668
	Print(l1);
	l1.erase(--(--l1.end())); //删除9
	Print(l1);
}

实例演示


其他函数

剩余一些常用的函数有:

  • 交换swap
    – 使用库函数交换头节点即可
  • 清空函数clear
    – 遍历节点,逐一删除即可,但是保留头节点_head,如果链表为空则不执行
  • 调整函数resize
    – 调整节点个数,只能在尾部增加节点个数,增加的节点data可以指定值
//交换
void swap(list<T>& l) { std::swap(_head, l._head); }

//清空
void clear()
{
	if(empty()) return; //如果为空则不执行
	iterator it = begin(); //获取迭代器

	//迭代器遍历逐一删除(后置++返回上一个迭代器)
	while (it != end()) { erase(it++); }
}

//调整
void resize(size_t n, const_reference val = T())
{
	if (n > size())
		while (size() < n) { push_back(val); }
}

最后

list模拟实现到这里就介绍了,本篇我们简单介绍了一下list的增删功能实现(与链表差别不大),重点介绍了list的迭代器思想,深入理解list的迭代器可以让我们对类和对象又有进一步的认识,如果我们可以理解list迭代器思想,那么list的其他函数对于我们来说都不成问题!

本次 <C++ STL之list模拟实现> 就先介绍到这里啦,希望能够尽可能帮助到大家。

如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!

本文整体代码:list模拟实现代码

C-PLUS-PLUS

🌟其他文章阅读推荐🌟
C++ <STL之list使用> -CSDN博客
C++ <STL之vector模拟实现> -CSDN博客
C++ <STL之vector的使用> -CSDN博客
C++ <STL之string的使用> -CSDN博客
C++ <STL之string模拟实现> -CSDN博客
🌹欢迎读者多多浏览多多支持!🌹

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

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

相关文章

MySQL数据库索引的种类、创建、删除

目录 一&#xff1a;MySQL 索引 1、MySQL 索引介绍 2、 索引的作用 3、索引的副作用 4、 创建索引的原则依据 二、索引的分类和创建 1、 普通索引 &#xff08;1&#xff09; 直接创建索引 &#xff08;2&#xff09; 修改表方式创建 &#xff08;3&#xff09; 创建表的…

【Turfjs】几何计算,计算地理空间上点坐标的经纬度,距离,围成的闭合空间面积等工作,都可以通过Turfkjs来实现

​​​​​​Turf.js中文网 几何计算&#xff1a; 1. 前端js就用这个 Turfjs的类库。参考网站&#xff1a; 计算两线段相交点 | Turf.js中文网 2. 后端java语言就可以用 JTS这个类库&#xff0c;参考网站&#xff1a;https://locationtech.github.io/jts/ https://github.com…

MVCC和undo log

MVCC多版本并发控制 MVCC是多版本并发控制&#xff08;Multi-Version Concurrency Control&#xff0c;简称MVCC&#xff09;&#xff0c;是MySQL中基于乐观锁理论实现隔离级别的方式&#xff0c;用于实现已提交读和可重复读隔离级别的实现&#xff0c;也经常称为多版本数据库…

面向对象接口

生活中大家每天都在用 USB 接口&#xff0c;那么 USB 接口与我们今天要学习的接口有什 么相同点呢&#xff1f; 在Java程序设计中的接口 接口就是规范&#xff0c;定义的是一组规则&#xff0c;体现了现实世界中“如果你是/要…则必须 能…”的思想。继承是一个"是不是&…

几款GB28181流媒体平台的详细介绍和使用整理

随着监控行业国标GB28181的应用范围越来越广泛&#xff0c;成熟的GB28181接入平台越来越多&#xff0c;本文梳理一下目前各大成熟的流媒体服务器平台及实际应用效果供各位参考。 1)NTV GBS NTV GBS是一款成熟、功能完善、产品化程度很高的GB28181服务平台&#xff0c;从2022年…

原点安全助力金融机构消费者个人信息保护合规

数字经济的发展进一步加速了金融业务与生活场景之间的融合&#xff0c;数亿民众在享受金融数字化便利服务的同时&#xff0c;也更容易遭受个人信息泄露、权益侵害等事件。在实际业务开展过程中&#xff0c;部分金融机构仍存在各种侵害消费者个人信息权益的乱象。 我国对数据安…

工作经验--产品季节性分析

产品季节性分析 1.了解季节性的重要性2.如何发现季节性产品统计方法&#xff1a;季节性指数法&#xff1a;傅里叶分析法&#xff1a;其他&#xff1a; 1.了解季节性的重要性 产品是否存在季节性变化&#xff0c;对于卖家来说相当重要&#xff0c;旺季提前备货、淡季防止库存冗余…

工业无监督缺陷检测,提升缺陷检测能力,解决缺陷样品少、不平衡等问题(二)

1. 工业缺陷检测简介 在工业生产中,质量保证是一个很重要的话题, 因此在生产中细小的缺陷需要被可靠的检出。工业异常检出旨在从正常的样本中检测异常的、有缺陷的情况。工业异常检测主要面临的挑战: 难以获取大量异常样本正常样本和异常样本差异较小异常的类型不能预先得知…

UNITY3D回合制游戏开发教程案例

UNITY3D实现回合制游戏 &#xff0c;类似梦幻西游&#xff0c;口袋妖怪&#xff0c;阴阳师。 先上效果 UNITY3D 回合制游戏案例源码开发教程 普通攻击 AOE技能 游戏概述 回合制游戏是一种策略游戏&#xff0c;玩家需要在自己的回合内进行决策&#xff0c;然后等待对手的回合…

redis学习 -- 常用指令

应用场景 String &#xff1a;缓存&#xff0c;限流&#xff0c;计数器&#xff0c;分布式锁&#xff0c;分布式session Hash&#xff1a;存储y用户信息&#xff0c;用户主页访问量&#xff0c;组合查询 List&#xff1a;关注人时间轴列 Set&#xff1a;点赞&#xff0c;标签&…

【在人间】关于网吧的记忆

高考完的暑假当了两个月夜班的网管&#xff0c;挣得一台小米6&#xff0c;也见识了不少社会人。 乡镇网吧&#xff0c;店里有老虎机&#xff0c;挣的钱比网吧一晚上收入多得多&#xff0c;最狠的一次有人一下输了3000个币(一个币一块钱)&#xff0c;半夜喊老板下楼哭爹喊娘的要…

Docker:启动,停止,删除

1.启动一个容器: docker run 可选参数 镜像名 [COMMAND] [ARG...] docker run -it ubuntu /bin/bash ,启动一个使用ubuntu的docker,并使用/bin/bash做为dcoker中执行的命令。 其中818d5a1c32ac为容器ID 在宿主机上,可以通过docker ps查看容器的状态: 启动容器时常用的可选…

程序员疯抢的 Java 面试宝典(PDF 版)限时开源

Java 面试 2023 届高校毕业生规模预计 1076 万人&#xff0c;同比增加 367 万人&#xff0c;对于 23 届的同学们来说&#xff0c;今年下半年大规模进行的秋招是获得全职 Offer 的最重要的途径&#xff01;对于程序员来说&#xff0c;大家都知道校招难度相对于社招来说会有所降…

解析 HashMap 源码:深入探究核心方法的实现与原理

前言数据结构类属性构造方法核心方法阈值&#xff1a;tableSizeFor插入元素&#xff1a;put树化&#xff1a;treeifyBin扩容&#xff1a;resize获取元素&#xff1a;get删除元素&#xff1a;remove遍历元素&#xff1a;keySet、entrySet 方法 总结 前言 一切的源头从类注释开始…

【Java se】集合——迭代器(Iterator接口)的实现原理

目录 一、迭代器的应用——遍历集合 步骤1&#xff1a;通过集合获取迭代器 步骤2&#xff1a;使用while循环 案例展示&#xff1a; 二、跟踪源代码 #1. 通过集合获取迭代器 #2. 通过成员方法next( ) 获取每一个集合元素对象 #3. 通过成员方法hasNext( )判断是否进行下一次…

计算机组成原理 | 理解二进制编码

二进制的转换 二进制——> 十进制&#xff1a; 从右到左的第 N 位&#xff0c;乘上一个 2 的 N 次方&#xff0c;然后加起来&#xff0c;就变成了一个十进制数例如二进制数&#xff1a;0011&#xff0c;对应的十进制表示&#xff0c;就是 0 2 3 0 2 2 1 2 1 1 2 0…

阿里云斩获 4 项年度云原生技术服务优秀案例

日前&#xff0c;在 ICT 中国2023 高层论坛-云原生产业发展论坛上&#xff0c;由阿里云容器服务提供技术支持的 “数禾科技”和“智联招聘” 两大案例以及阿里云云原生 AI 套件、云原生 FinOps 成本分析套件两大产品技术方案&#xff0c;共同获得 2023 年度云原生应用实践先锋—…

oai核心网启动多切片自动生成方法

简介 启动一个切片需要&#xff1a; 核心网侧&#xff1a; 启动核心网yaml文件及相关配置文件&#xff08;datebase conf healthscripts&#xff09; 对应业务的sever &#xff08;如&#xff09;基站侧&#xff1a; 虚拟机 启动ueransim的yaml文件及相关配置 代理程序&#…

拿捏指针(一)---对指针的基本认识(初级)

文章目录 指针是什么&#xff1f;指针的定义指针的大小 指针类型指针有哪些类型&#xff1f;指针不同类型有什么意义&#xff1f; 野指针野指针的成因如何避免野指针&#xff1f; 指针运算指针 - 整数指针 - 指针指针的关系运算 二级指针 指针是什么&#xff1f; 指针的定义 …

DNDC模型建模方法及在土壤碳储量、温室气体排放、农田减排、土地变化、气候变化

由于全球变暖、大气中温室气体浓度逐年增加等问题的出现&#xff0c;“双碳”行动特别是碳中和已经在世界范围形成广泛影响。国家领导人在多次重要会议上讲到&#xff0c;要把“双碳”纳入经济社会发展和生态文明建设整体布局。同时&#xff0c;提到要把减污降碳协同增效作为促…