【C++】STL——list的模拟实现

目录

  • 前言
  • list介绍
  • list的模拟实现
    • 总体结构
      • 节点类
      • 迭代器类
      • 链表类
    • 默认成员函数
      • 构造函数
      • 拷贝构造
      • 赋值重载
      • 析构函数
    • 迭代器实现
      • 双向迭代器
      • 迭代器的其他功能
      • 用多参数模板完成最终的迭代器类
    • list的容量相关和数据访问
      • empty()和size()
      • front()和back()
    • list的修改操作
      • 任意位置插入和删除
      • 头尾插删
      • 清理
  • 完整代码
    • list.h

请添加图片描述

前言

  我们前面重点介绍了string的使用和模拟实现以及vector的使用及模拟实现。下面我们来说说list的模拟实现。
  list是一个带头双向循环链表,除了不支持任意位置的随机访问,其余的操作它的效率都是非常高的,下面就来学习一下list容器。

list介绍

简单的带头双向循环链表如下:
在这里插入图片描述
list的介绍:

  1. list是可以在常数范围内任意位置进行插入删除的序列式容器,且该容器可以前后双向迭代
  2. list的底层是带头双向链表结构且每个元素存储在互不相关的独立节点中,在节点中通过指针指向前一个元素和后一个元素。
  3. list和forward_list非常相似,forward_list是单链表。
  4. 其他序列式容器(array,vector,deque)相比,list在任意位置进行插入,移除元素的效率更高。
  5. 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问

对于list的相关接口函数的学习可以看:cplusplus—list。
list相关函数接口的使用与vector和string类型,遍不再细讲,直接进入list的模拟实现。

list的模拟实现

总体结构

  list不像前面的vector和string只需一个类即可完成所有操作,而是分成了三个类:节点类、``迭代器类以及链表类

节点类

  由于在需要插入链表节点时都需要创造节点,因此我们将节点封装为一个类,在有需要时直接new一个即可,方便快捷。
  每个节点都由三部分组成:指向前一个节点的指针(_prve),指向后一个节点的指针(_next),以及数据本身(_data)。

template<class T>
struct ListNode//struct在c++中是默认公有
{
	ListNode* _prev;
	ListNode* _next;
	T _data;

	ListNode(const T& data = T())//使用匿名对象作为缺省参数传参
		:_prev(nullptr)
		,_next(nullptr)
		,_data(data)
	{}
};

由于我们后面在进行链表的各种操作时会频繁用到这三个变量,因此我们直接使用struct默认公有即可。
使用匿名对象作为缺省参数传参

  • 是内置类型,则C++为了兼容模版,同样也可以调用内置类型的默认构造
  • T是自定义类型,则直接调用它的默认构造

迭代器类

节点类的类型过长,我们typedef一下,缩短名字。

typedef ListNode<T> Node;

  在前面vector和string的迭代器使用中,我们都是直接用的原生指针T作为他们的迭代器,那list是否可以直接用Node来作为迭代器呢。答案是否定的。
  因为链表是由一个个节点构成,每个节点都是独立new出来的,它们的空间是不连续的,Node*是内置类型只能直接++或- -,这对于不连续的空间是行不通的。

因此我们封装一个迭代器类,再通过运算符重载来控制它的行为。

//封装Node*成为一个类控制它的行为
template<class T>//每次使用模板时都需要加上
struct ListIterator
{
	typedef ListNode<T> Node;
	typedef ListIterator<T> Self;

	Node* _node;
	//运算符重载控制行为
};

链表类

  链表类来实现链表的各个增删查改的功能,是最主要的类,其成员变量包括了一个哨兵位节点。

template<class T>
class List
{
	typedef ListNode<T> Node;//节点
	typedef ListIterator<T> iterator;//迭代器
public:
	//....
private:
	Node* _head;//哨兵位节点
};

默认成员函数

  一个自定义类型有空间申请释放的类,最重要的默认成员函数都是必须要写的,包括构造拷贝构造赋值重载析构

构造函数

  但凡我们要创建一个链表,无论是构造还是拷贝构造,都需要创造一个头结点,因此我们直接把头结点写成一个函数,后面直接调用即可

//创建头结点(哨兵位),后续各种构造都会使用上,因此将其写成函数
void empty_init()
{
	_head = new Node;//申请一个Node类型的空间
	_head->_next = _head;
	_head->_prev = _head;
}

在这里插入图片描述

对于默认构造函数,我们只需要创造头结点即可:

List()//默认构造,创建一个头结点即可
{
	empty_init();
}

迭代器区间构造初始化

template<class InputIterator>
List(InputIterator first, InputIterator end)//函数模板——支持任意迭代器的迭代区间进行初始化
{
	empty_init();
	while (first != end)
	{
		push_back(*first);
		first++;
	}
}

带参构造,n是有n个数据,x是数据值。

List(size_t n, const T& x = T())
{
	empty_init();
	for (size_t i = 0; i < n; i++)
	{
		push_back(x);
	}
}
List(int n, const T& x = T())
{
	empty_init();
	for (int i = 0; i < n; i++)
	{
		push_back(x);
	}
}

  我们多写一个list(int n, const T& val = T())是因为如果不写时,我们写list< int > v(3,5)时会优先匹配上迭代器构造,此时InputIterator会被替代成为int,而解引用就会报错:

error C2100: 非法的间接寻址

list中还有一个initializer_list构造函数,这使得其可以多参数初始化构造,即:

List<int> ls1 = { 1,2,3,4 };

在这里插入图片描述
模拟实现也很简单:

List(initializer_list<T> il)
{
	empty_init();//首先创建头结点
	for (auto& e : il)
	{
		push_back(e);
	}
}

拷贝构造

同样在拷贝构造前要先初始化创建头结点:

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

赋值重载

  赋值重载用的传值传参,会调用拷贝构造,则lt就是lt3的一个拷贝,把lt1的节点和lt的节点进行了交换,lt1就拿到了lt的节点,lt拿到了lt1的节点,但是lt出了作用域直接销毁,没有影响。

//lt1 = lt3
List<T>& operator=(List<T> lt)
{
	swap(_head, lt._head);
	return *this;
}

析构函数

析构函数只需要把每个节点给销毁即可。

~List()
{
	clear();//清理各个节点
	deplete _head;
	_head = nullptr;
}

clear()涉及到迭代器以及删除函数,后面再讲解。

迭代器实现

上面我们只是写了迭代器类的一个框架,下面来把这个类完整实现。

双向迭代器

vector和string都是使用的双向迭代器,包括前置++,后置++,前置- -以及后置- - 。
上面我们也说过为何要重载这些运算符:

因为链表是由一个个节点构成,每个节点都是独立new出来的,它们的空间是不连续的,Node*是内置类型只能直接++或- -,这对于不连续的空间是行不通的。

因此我们封装一个迭代器类,再通过运算符重载来控制它的行为。

typedef ListIterator<T> Self;
Self& operator++()//前置++
{
	_node = _node->_next;
	return *this;//返回++之后
}
Self operator++(int)//后置++
{
	Self tmp(_node);
	_node = _node->_next;
	return tmp;
}

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

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

当然还有随机迭代器,即随机移动迭代器的位置,即+n或-n,当然对于链表是不支持的,我们也不用去实现。

迭代器的其他功能

  对于迭代器,不只是要实现++和- -那么简单,在调用迭代器进行遍历时,我们会使用到解引用,以及判断相等的操作。而对于如果链表节点中带的是自定义类型就还会有取节点指针地址访问其成员。
因此迭代器的其他功能还有以下几个:

  • 解引用operator*
  • 判断是否相等operator==或operator!=
  • 取节点指针地址operator->
T& operator*()//解引用
{
	return _node->_data;
}

bool operator==(const Self& it)//判断相等
{
	return _node = it._node;
}

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

T* operator->()//取当前节点指针的地址
{
	return &_node->_data;
}

对于前面两个解引用和判断相等都比较好理解,下面我们来讲讲取当前节点指针的地址这点,先看代码:

struct Date
{
	Date(int year = 1970, int month = 01, int day = 01)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	int _year;
	int _month;
	int _day;
};
void test2()
{
	List<Date> ls;
	ls.push_back(Date(2024, 10, 01));
	ls.push_back(Date(2025, 11, 02));
	ls.push_back(Date(2026, 12, 03));
	auto it1 = ls.begin();
	while (it1 != ls.end())
	{
		cout << it1->_year << "年" << it1->_month << "月" << it1->_day << "日" << endl;
		it1++;
	}
}

结果如下:
在这里插入图片描述

  由代码可以发现,我们可以直接通过迭代器it1->的方式来访问自定义类型中的成员,当然这也更方便我们操作,就会有人好奇它是如何得来的,下面来解释一下:

  • list迭代器本身也是一个Node*的一个指针,想要访问其中自定义类型的成员,就需要:(*迭代器).成员,这样的方式写起来比较难受,因此就有了operator->重载
  • 重载operator->实际上返回的是指向当前节点指针的地址,而需要访问其中的成员,则需再加上->操作符,因此整个流程就变成了:
it1.operator->()->_year;

根据运算符重载的规则又可以写成:

it1->->_year;

而编译器又对其进行了优化,所以就变成了:

it1->_year;

用多参数模板完成最终的迭代器类

迭代器不只有iterator,还有const_iterator
我们在实现iterator时,用的一个模版:

typedef ListIterator<T> iterator;//迭代器

当我们还想要const_iterator时,就要再写一个ListConstIterator类:

typedef ListConstIterator<T> const_iterator;//const迭代器

  而写出的ListConstIterator这个类,本质上和ListIterator这个类没有太大差别,此时就会造成代码冗余,于是我们就想到了使用模板来解决。单一个参数模版可能还不太够,我们使用到多参数模板
参数如下:

  • T节点中值的普通类型
  • Ref节点中值的引用
  • Ptr节点中值的指针

由此,完整的迭代器类如下:

//封装Node*成为一个类控制它的行为
template<class T, class Ref, class Ptr>
struct ListIterator
{
	typedef ListNode<T> Node;
	typedef ListIterator<T, Ref, Ptr> Self;

	Node* _node;

	//构造
	ListIterator(Node* node)
		:_node(node)
	{}

	Ptr operator->()//取当前节点的指针
	{
		return &_node->_data;
	}

	Ref operator*()//解引用
	{
		return _node->_data;
	}
	
	Self& operator++()//前置++
	{
		_node = _node->_next;
		return *this;//返回++之后
	}
	Self operator++(int)//后置++
	{
		Self tmp(_node);
		_node = _node->_next;
		return tmp;
	}

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

	Self operator--(int)//后置++
	{
		Self tmp(_node);
		_node = _node->_prev;
		return tmp;
	}

	bool operator==(const Self& it)//判断相等
	{
		return _node = it._node;
	}

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

在这里插入图片描述
在list链表类中就可以对iterator和const_iterator这两个的begin()和end()函数进行定义:

template<class T>
class List
{
	typedef ListNode<T> Node;
	typedef ListIterator<T, T&, T*> iterator;//声明两种不同类型的迭代器
	typedef ListIterator<T, const T&, const T*> const_iterator;
public:
	iterator begin()
	{
		return iterator(_head->_next);//直接对头结点的下一个节点进行迭代器构造
	}
	const_iterator begin() const
	{
		return const_iterator(_head->_next);
	}
	iterator end()
	{
		return iterator(_head);
	}
	const_iterator end() const
	{
		return const_iterator(_head);
	}
};

这样,对于普通对象还是const对象都可以从容应对了。

list的容量相关和数据访问

empty()和size()

在这里插入图片描述

  • 对于判空,只需判断begin()和end()是否相等,相等则为空。
  • 对于统计大小,只需要迭代器访问一遍计数即可。
bool empty() const
{
	return begin() == end();
}

size_t size() const
{
	int count = 0;
	auto it = begin();
	while (it != end())
	{
		count++;
		it++;
	}
	return count;
}

front()和back()

  对于数据访问,库里面给出的接口只有访问第一个数据和最后一个数据,代码也比较简单。
在这里插入图片描述

T front()
{
	return *begin();
}
const T front() const
{
	return *begin();
}
T back()
{
	return *(--end());
}
const T back() const
{
	return *(--end());
}

list的修改操作

任意位置插入和删除

  任意位置插入和删除就是给了个迭代器位置pos在pos位置前插入或删除pos位置的节点。
对于任意位置插入步骤如下:

  1. 找到pos位置对应的节点cur
  2. 新建一个节点newnode
  3. 找到cur的前一个节点prev
  4. 此时将prev,newnode,cur三个节点链接在一起即可。

代码如下:

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

	//prev newnode cur
	cur->_prev = newnode;
	newnode->_next = cur;
	newnode->_prev = prev;
	prev->_next = newnode;
	return iterator(newnode);
}

注意:insert不存在迭代器失效的问题。


对于任意位置删除步骤如下:

  1. 首先判断链表不能为空
  2. 找到pos位置的节点cur,以及前一个节点prev和后一个节点next
  3. prev和next链接在一起
  4. 删除cur节点
  5. 此时pos迭代器会失效pos指向的节点被释放了,因此要返回next节点

代码如下:

iterator erase(iterator pos)
{
	assert(pos != end());
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;

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

	delete cur;
	return iterator(next);
}

头尾插删

对于头尾插删在数据结构的链表中已经有详细讲述,这里便不再多说。
学了前面那么多,我们已经学会了代码复用,直接复用insert和erase即可

void push_back(const T& x = T())
{
	push_back(end(), x);
}
void pop_back()
{
	erase(--end());
}
void push_front(const T& x)
{
	insert(begin(), x);
}
void pop_front()
{
	erase(begin());
}

清理

只需要把除头结点外的所有节点全部删除即可。

void clear()
{
	auto it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}

完整代码

list.h

#pragma once

#include<iostream>
#include<assert.h>
#include<algorithm>
using namespace std;

//list是带头双向循环链表
namespace bit
{
	//定义节点类
	template<class T>
	struct ListNode//struct在c++中是默认公有
	{
		ListNode* _prev;
		ListNode* _next;
		T _data;

		ListNode(const T& data = T())//使用匿名对象作为缺省参数传参
			:_prev(nullptr)
			,_next(nullptr)
			,_data(data)
		{}
	};
	//封装Node*成为一个类控制它的行为
	template<class T, class Ref, class Ptr>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr> Self;

		Node* _node;

		//构造
		ListIterator(Node* node)
			:_node(node)
		{}

		Ptr operator->()//取当前节点的指针
		{
			return &_node->_data;
		}

		Ref operator*()//解引用
		{
			return _node->_data;
		}

		Self& operator++()//前置++
		{
			_node = _node->_next;
			return *this;//返回++之后
		}
		Self operator++(int)//后置++
		{
			Self tmp(_node);
			_node = _node->_next;
			return tmp;
		}

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

		Self operator--(int)//后置++
		{
			Self tmp(_node);
			_node = _node->_prev;
			return tmp;
		}

		bool operator==(const Self& it)//判断相等
		{
			return _node = it._node;
		}

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

	template<class T>
	class List
	{
		typedef ListNode<T> Node;
		//我们想像vector一样,直接用指针作为迭代器,但是对Node是行不通的,因为它不是连续的空间,不能直接++或--,迭代器的操作它进行不了
		//Node* 作为内置类型,我们不能修改它的行为,所以我们可以通过用类来封装Node*,再通过运算符重载来控制它的行为
		//typedef Node* iterator;
		typedef ListIterator<T, T&, T*> iterator;//声明两种不同类型的迭代器
		typedef ListIterator<T, const T&, const T*> const_iterator;
	public:
		iterator begin()
		{
			return iterator(_head->_next);//直接对头结点的下一个节点进行迭代器构造
		}
		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}
		iterator end()
		{
			return iterator(_head);
		}
		const_iterator end() const
		{
			return const_iterator(_head);
		}
		//创建头结点(哨兵位),后续各种构造都会使用上,因此将其写成函数
		void empty_init()
		{
			_head = new Node;//申请一个Node类型的空间
			_head->_next = _head;
			_head->_prev = _head;
		}
		List()//默认构造,创建一个头结点即可
		{
			empty_init();
		}
		template<class InputIterator>
		List(InputIterator first, InputIterator end)//函数模板——支持任意迭代器的迭代区间进行初始化
		{
			empty_init();
			while (first != end)
			{
				push_back(*first);
				first++;
			}
		}
		List(size_t n, const T& x = T())//带参构造
		{
			empty_init();
			for (size_t i = 0; i < n; i++)
			{
				push_back(x);
			}
		}
		List(int n, const T& x = T())
		{
			empty_init();
			for (int i = 0; i < n; i++)
			{
				push_back(x);
			}
		}
		List(initializer_list<T> il)
		{
			empty_init();
			for (auto& e : il)
			{
				push_back(e);
			}
		}

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

		List<T>& operator=(List<T> lt)//赋值重载
		{
			swap(_head, lt._head);
			return *this;
		}

		~List()
		{
			clear();//清理各个节点
			delete _head;
			_head = nullptr;
		}

		bool empty() const
		{
			return begin() == end();
		}

		size_t size() const
		{
			int count = 0;
			auto it = begin();
			while (it != end())
			{
				count++;
				it++;
			}
			return count;
		}
		T front()
		{
			return *begin();
		}
		const T front() const
		{
			return *begin();
		}
		T back()
		{
			return *(--end());
		}
		const T back() const
		{
			return *(--end());
		}

		void push_back(const T& x )
		{
			/*Node* newnode = new Node(x);
			Node* tail = _head->_prev;
			tail->_next = newnode;
			_head->_prev = newnode;
			newnode->_next = _head;
			newnode->_prev = tail;*/
			insert(end(), x);
		}
		void pop_back()
		{
			erase(--end());
		}
		void push_front(const T& x)
		{
			insert(begin(), x);
		}
		void pop_front()
		{
			erase(begin());
		}
		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);

			//prev newnode cur
			cur->_prev = newnode;
			newnode->_next = cur;
			newnode->_prev = prev;
			prev->_next = newnode;
			return iterator(newnode);
		}
		iterator erase(iterator pos)
		{
			assert(pos != end());
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;

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

			delete cur;
			return iterator(next);
		}
		void clear()
		{
			auto it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

	private:
		Node* _head;//哨兵位节点
	};

	struct Date
	{
		Date(int year = 1970, int month = 01, int day = 01)
			:_year(year)
			,_month(month)
			,_day(day)
		{}
		int _year;
		int _month;
		int _day;
	};
}

感谢大家观看,如果大家喜欢,希望大家一键三连支持一下,如有表述不正确,也欢迎大家批评指正。
请添加图片描述

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

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

相关文章

数据结构 ——— C语言实现无哨兵位单向不循环链表

目录 前言 动态顺序表的缺陷 单链表的概念 单链表中节点的结构 单链表逻辑结构示意图​编辑 实现单链表前的准备工作 实现单链表 1. 定义节点的指针 2. 创建节点 3. 打印单链表中的所有数据 4. 在单链表头部插入数据 5. 在单链表尾部插入数据 6. 在单链表头部删除数…

脏读、不可重复读、幻读的解决方法

上一篇博客提到了脏读、不可重复读、幻读的含义&#xff0c;也知道了是因为什么情况导致出现的这些问题&#xff0c;这篇博客就带大家一起来了解一下他们的解决办法~ 脏读&#xff1a;脏读出现的原因主要是因为一个事务读取了另外一个事务未提交的数据&#xff0c;就可能出现脏…

掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系

在日常开发中&#xff0c;我们常常需要对复杂的 SQL 进行数据血缘分析。 本文重点讨论在具有 * 列的嵌套子查询中建立表和列之间正确关系的挑战。使用 Teradata SQL 代码示例来说明该过程。 本文聚焦于一个别名为 SUBSCRIBER_ 的子查询及其派生的列&#xff0c;这些列在外层查…

无需VPN!大厂力作:免费AI对口型神器登场,让你的视频制作更简单!

大家好&#xff0c;我是Shelly&#xff0c;一个专注于输出AI工具和科技前沿内容的AI应用教练&#xff0c;体验过300款以上的AI应用工具。关注科技及大模型领域对社会的影响10年。关注我一起驾驭AI工具&#xff0c;拥抱AI时代的到来。 &#xff08;偶尔会因为推荐工具&#xff…

《深度学习》OpenCV 图像拼接 原理、参数解析、案例实现

目录 一、图像拼接 1、直接看案例 图1与图2展示&#xff1a; 合并完结果&#xff1a; 2、什么是图像拼接 3、图像拼接步骤 1&#xff09;加载图像 2&#xff09;特征点检测与描述 3&#xff09;特征点匹配 4&#xff09;图像配准 5&#xff09;图像变换和拼接 6&am…

实验3 选择结构

1、计算分段函数的值 #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <math.h> int main() {double x,y0;scanf("%lf",&x);if(x<0){printf("error!\n");return 0;}if(0<x&&x<1){ylog10(x);}else if(1<…

缓存数据减轻服务器压力

问题:不是所有的数据都需要请求后端的 不是所有的数据都需要请求后端的,有些数据是重复的、可以复用的解决方案:缓存 实现思路:每一个分类为一个key,一个可以下面可以有很多菜品 前端是按照分类查询的,所以我们需要通过分类来缓存缓存代码 /*** 根据分类id查询菜品** @pa…

Java | Leetcode Java题解之第459题重复的子字符串

题目&#xff1a; 题解&#xff1a; class Solution {public boolean repeatedSubstringPattern(String s) {return kmp(s s, s);}public boolean kmp(String query, String pattern) {int n query.length();int m pattern.length();int[] fail new int[m];Arrays.fill(fa…

54.二叉树的最大深度

迭代 class Solution {public int maxDepth(TreeNode root) {if(rootnull){return 0;}int de0;Queue<TreeNode> qunew LinkedList<>();TreeNode tn;int le;qu.offer(root);while(!qu.isEmpty()){lequ.size();while(le>0){tnqu.poll();if(tn.left!null){qu.offe…

学会这几个简单的bat代码,轻松在朋友面前装一波13[通俗易懂]

大家好&#xff0c;又见面了&#xff0c;我是你们的朋友全栈君。 这个标题是干什么用的? 最近看晚上某些人耍cmd耍的十分开心&#xff0c;还自称为“黑客”&#xff0c;着实比较搞笑.他们那些花里胡哨的东西在外行看来十分nb,但只要略懂一些&#xff0c;就会发现他们的那些十…

论文阅读笔记-A Comparative Study on Transformer vs RNN in Speech Applications

前言 介绍 序列到序列模型已广泛用于端到端语音处理中,例如自动语音识别(ASR),语音翻译(ST)和文本到语音(TTS)。本文着重介绍把Transformer应用在语音领域上并与RNN进行对比。与传统的基于RNN的模型相比,将Transformer应用于语音的主要困难之一是,它需要更复杂的配…

JavaScript 数组方法

数组(array)是按次序排列的一组值。每个值的位置都有编号(从0开始)&#xff0c;整个数组用方括号表示。两端的方括号是数组的标志。 var a["a","b","c"]; 除了在定义时赋值&#xff0c;数组也可以先定义后赋值。 var arr[];arr[1]"a"…

Qt_绘图

目录 1、绘图核心类 2、QPainter类的使用 2.1 绘制线段 2.2 绘制矩形 2.3 绘制圆形 2.4 绘制文本 3、QPen类的使用 3.1 使用画笔 4、QBrush类的使用 4.1 使用画刷 5、绘制图片 5.1 测试QPixmap 5.1.1 图片移动 5.1.2 图标缩小 5.1.3 旋转图片 5.1.4 将…

windows10或11家庭版实现远程桌面连接控制

远程协助是一种Windows工具&#xff0c;允许控制者使用鼠标和键盘远程控制接受者的计算机&#xff0c;从某种程度上讲&#xff0c;这也是Win10家庭版无法远程桌面的一个有效替代方案。 步骤1. 在使用Windows远程协助之前&#xff0c;您需要先更改某些设置&#xff0c;右键单击…

封装el-upload组件,用于上传图片和视频

使用环境 vue3element-ui plus 需要根据后端返回结构修改的函数&#xff1a;onPreview onRemove onSuccess 组件使用 基本使用 源代码&#xff1a; <script setup> import AutoUploadFile from /components/auto-upload-file/index.vue function change(urls){console.…

金智维KRPA之Excel自动化

Excel自动化操作概述 Excel自动化主要用于帮助各种类型的企业用户实现Excel数据处理自动化&#xff0c;Excel自动化是可以从单元格、列、行或范围中读取数据&#xff0c;向其他电子表格或工作簿写入数据等活动。 通过相关命令&#xff0c;还可以对数据进行排序、进行格式…

javaScript数组(16个案例+代码+效果图)

目录 1.数组的概念 2.创建数组 1.通过数组字面量创建数组 1.代码 2.效果 2.通过new Array()创建数组 1.代码 2.效果 3.数组的基本操作 1.获取数组的长度 案例:获取数组的长度 1.代码 2.效果 2.修改数组的长度 1.代码 2.效果 4.访问数组 案例:访问数组 1.代码 2.效果 5.遍历数组…

【EXCEL数据处理】000013 案例 EXCEL筛选与高级筛选。

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 【EXCEL数据处理】000013 案例 EXCEL筛选与高级筛选。使用的软件&#…

一个真实可用的登录界面!

需要工具&#xff1a; MySQL数据库、vscode上的php插件PHP Server等 项目结构&#xff1a; login | --backend | --database.sql |--login.php |--welcome.php |--index.html |--script.js |--style.css 项目开展 index.html&#xff1a; 首先需要一个静态网页&#x…

【HTML5】html5开篇基础(4)

1.❤️❤️前言~&#x1f973;&#x1f389;&#x1f389;&#x1f389; Hello, Hello~ 亲爱的朋友们&#x1f44b;&#x1f44b;&#xff0c;这里是E绵绵呀✍️✍️。 如果你喜欢这篇文章&#xff0c;请别吝啬你的点赞❤️❤️和收藏&#x1f4d6;&#x1f4d6;。如果你对我的…