【C++】模拟map和set以及改造红黑树

news2024/11/16 11:39:00

文章目录

    • 1、set和map的基础用法
      • 1.1 set的基本使用
      • 1.2 map的基本使用
    • 2、set和map的模拟实现
      • 2.1 建立map和set类
      • 2.1 红黑树的完善

1、set和map的基础用法

stl中,像vector、list、deque等这样的容器,称之为序列式容器,其底层是线性序列的数据结构,里面存储的元素本身。
关联式容器,关联式容器不同的是,其存储是一个<key, value>键值对。
STL中关于键值对的定义:
在这里插入图片描述
简单来说就是可以通过一个pair对象访问其两个成员变量first和second,达到键值对的目的。

树型结构中的关联式容器主要有set、multiset、map和multimap。这四个容器的底层都是用的红黑树。
下面先一个一个看。

1.1 set的基本使用

在这里插入图片描述
set存储的是一值,和大部分容器一样,有着基本的begin、end、insert、erase、find、empty、size,这也是主要的功能。
值得说的是set由于底层是一个红黑树,其存储的数据没有重复的,并且set不允许修改数据,所以相较于map缺少了[]访问符,因为会打乱其结构。

如果需要重复的数据,multiset就派上了用场,multiset除了存储的数据可以重复外,其它的用法和set基本一致。
在这里插入图片描述

#include <iostream>
#include <set>
using std::cout;
using std::endl;

template<class T>
void testCont();

void testSet()
{
	//set默认less->升序 greater->降序
	testCont< std::set<int, std::greater<int>> >();
}

void testMultiset()
{
	testCont< std::multiset<int, std::greater<int>> >();
}


template<class T>
void testCont()
{
	T s;
	int arr[] = { 1,4,6,7,9,2,5,6 };
	//set不仅可以排序,也有去重功能
	for (auto& e : arr)
	{
		s.insert(e);
	}

	typename T::iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	it = s.find(4);
	s.erase(it);
	for (auto& k : s)
	{
		cout << k << " ";
	}
	cout << endl;
}


int main()
{
	testSet();
	testMultiset();
	return 0;
}

在这里插入图片描述

1.2 map的基本使用

这里是引用
map存储的是一个键值对,有着基本的begin、end、insert、erase、find、empty、size、[],这也是主要的功能。


在这里插入图片描述
multimap和map基本上一样,除了可以存储重复数据以及没有[]操作符。

值得注意的是map的[]操作符:
在官方文档中,将[]操作符等价于
在这里插入图片描述
其中insert,返回一个键值对,如果插入新值,返回新结点的迭代器和true,如果已有结点返回指向已有结点的迭代器和false。
在这里插入图片描述
在这里插入图片描述

简化后如下

value& operator[](const K& key)
{
	pair<iterator, bool> ret = this->insert(make_pair(key, K()));
	return *(ret.first).second;
}

常规用法

#include <iostream>
#include <map>
using std::cout;
using std::endl;

template<class T>
void testCont();

void testMap()
{
	testCont< std::map<int, int, std::greater<int>> >();
}

void testMultimap()
{
	testCont< std::multimap<int, int, std::greater<int>> >();
}


template<class T>
void testCont()
{
	T s;
	int arr[] = { 1,4,6,7,9,2,5,6 };

	//map不仅可以排序,也有去重功能
	for (auto& e : arr)
	{
		s.insert(std::make_pair(e, e));
	}

	typename T::iterator it = s.begin();
	while (it != s.end())
	{
		cout << (*it).first << ":" << (*it).second << " ";
		++it;
	}
	cout << endl;

	it = s.find(4);
	s.erase(it);
	for (auto& kv : s)
	{
		cout << kv.first << ":" << kv.second << " ";
	}
	cout << endl;
	cout << endl;
}

void testinsert()
{
	std::map<int, int, std::greater<int>> m;
	std::multimap<int, int, std::greater<int>> ml;

	//multimap 不支持[]操作符,因为其存储重复数据
	int arr[] = { 1,4,6,7,9,2,5,6 };
	for (auto& e : arr)
	{
		m[e] = e;
		//ml[e] = e; error
		ml.insert(std::make_pair(e, e));
	}

	for (auto& kv : m)
	{
		cout << kv.first << ":" << kv.second << " ";
	}
	cout << endl;

	for (auto& kv : ml)
	{
		cout << kv.first << ":" << kv.second << " ";
	}

}

int main()
{
	testMap();
	testMultimap();

	testinsert();

	return 0;
}

2、set和map的模拟实现

为了更好的认识set和map,通过了解它们的stl源码,简单实现它们的功能。

set和map底层都是通过红黑树实现的。
在这里插入图片描述

2.1 建立map和set类

1、首先为了避免于std中重名,将创建的类放入test命名空间。
2、在stl源码中,map给红黑树传的模板参数是一个key和一个键值对
3、为了应对对红黑树结点取值不同的情况,通过传一个keyofValue类型,再通过仿函数的形式区分map和set访问的不同类型。
4、定义map迭代器的时候,typaname是为了表示
RBTree<K, pair<const K, V>, mapKeyOfValue>::iterator
是一个类型而不是一个其他的具体的值。(因为类::这样是可以访问类中定义的static类型和静态方法的)
5、注意键值对中的key不能修改。

//map.h
#pragma once
#include "RBTree.h"

namespace test
{
	template<class K, class V>
	class map
	{
		struct mapKeyOfValue
		{
			//专门放在mapKeyOfValue中,这样可读性好
			const K& operator()(const pair<const K, V>& kv)
			{
				return kv.first;
			}
		};
	public:
		typedef typename RBTree<K, pair<const K, V>, mapKeyOfValue>::iterator iterator;
		typedef typename RBTree<K, pair<const K, V>, mapKeyOfValue>::const_iterator const_iterator;
		pair<iterator, bool> insert(const pair<const K, V>& kv)
		{
			return _t.insert(kv);
		}

		iterator begin()
		{
			return _t.begin();
		}

		iterator end()
		{
			return _t.end();
		}

		const_iterator begin() const
		{
			return _t.begin();
		}

		const_iterator end() const
		{
			return _t.end();
		}

		V& operator[](const K& k)
		{
			pair<iterator, bool> ret = insert(make_pair(k, V()));
			return ret.first->second;
		}

		iterator find(const K& k)
		{
			return _t.find(k);
		}

		/*const_iterator find(const K& k) const
		{
			return _t.find(k);
		}*/

	private:
		RBTree<K, pair<const K, V>, mapKeyOfValue> _t;
	};

1、set要求的是迭代器返回指向的结点不能被修改,所以定义的iterator和const_iterator,都用的是红黑树的const_iterator进行定义。
2、set中的insert调用红黑树中的insert会返回一个键值对,这个键值对里面的iterator返回的是红黑树中的普通迭代器,这个普通迭代器再返回给set中的insert键值对时,会发生键值对的拷贝构造,对应红黑树的普通迭代器需要转换成const迭代器(因为set中iterator是用红黑树const迭代器实现的)。(我们在红黑树迭代器类中看具体是怎么处理的

#pragma once
#include "RBTree.h"

namespace test
{
	template<class K>
	class set
	{
		struct setKeyOfValue
		{
			const K& operator()(const K& k)
			{
				return k;
			}
		};
	public:
		typedef typename RBTree<K, K, setKeyOfValue>::const_iterator iterator;
		typedef typename RBTree<K, K, setKeyOfValue>::const_iterator const_iterator;

		pair<iterator, bool> insert(const K& k)
		{
			//这里返回的一个pair,然后调用pair的拷贝构造,pair拷贝构造又要调用iterator的拷贝构造(调用默认拷贝构造),
			//因为这里的iterator是RBTree中的const迭代器,返回的pair里是RBTree的普通迭代器
			//所以出现了如何将普通迭代器转换成const迭代器的问题
			//return _t.insert(k);
			

			//这样写就好理解一点
			pair<typename RBTree<K, K, setKeyOfValue>::iterator, bool> ret = _t.insert(k);
			return pair<iterator, bool>(ret.first, ret.second);
		}

		//这里只需要提供普通迭代器版本就行,因为const this指针会调用RBTree里的const迭代器
		iterator begin() const
		{
			return _t.begin();
		}

		iterator end() const
		{
			return _t.end();
		}

		iterator find(const K& k)
		{
			return _t.find(k);
		}
	private:
		RBTree<K, K, setKeyOfValue> _t;
	};

2.1 红黑树的完善

在红黑树的结点中,只存一个值,map中存键值对,set中存一个值。

//RBTree.h
#pragma once
#include <iostream>
#include <string>
#include <assert.h>
using namespace std;

enum Color { RED = 0, BLACK };

template<class T>
struct RBTreeNode
{
	struct RBTreeNode<T>* _left;
	struct RBTreeNode<T>* _right;
	struct RBTreeNode<T>* _parent;
	T _data;
	Color _col;


	RBTreeNode(const T& data)
		:_data(data)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _col(BLACK)
	{}
};

实现红黑树迭代器

迭代器中通过模板参数有这几个妙手:
1、比较常见的,对应普通迭代器需要返回T&、T*,对应const迭代器需要返回const T&、const T*,通过template<class T, class Ref, class Ptr>,就能实现普通迭代器和const迭代器的转换。
2、如果需要将普通迭代器转换成const迭代器,通过
typedef RBTreeIterator<T, T&, T*> iterator;表示普通迭代器。
在需要用普通迭代器构造成const迭代器的时候,通过调用RBTreeIterator(const iterator& s)这个构造就能实现。

//RBTree.h
// 对于红黑树类中定义迭代器
//	typedef RBTreeIterator<T, T&, T*> iterator;
//	typedef RBTreeIterator<T, const T&, const T*> const_iterator;
template<class T, class Ref, class Ptr>
struct RBTreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef RBTreeIterator<T, Ref, Ptr> Self;
	typedef RBTreeIterator<T, T&, T*> iterator;
	RBTreeIterator(Node* node)
		:_node(node)
	{}
	
	//当是普通迭代器的时候,Self和iterator都一样这时候下面的函数是一个拷贝构造
	//当是const迭代器的时候,Ref是const T&,Ptr是const T*
	//这个时候,Self代表const迭代器,iterator还是一个普通迭代器下面的函数就成了一个构造函数(用普通构造const的构造函数)。
	RBTreeIterator(const iterator& s)
		:_node(s._node)
	{}

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

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

	Self& operator++()
	{
		if (_node->_right)
		{
			Node* minRight = _node->_right;
			while (minRight->_left)
			{
				minRight = minRight->_left;
			}

			_node = minRight;
		}
		else
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && parent->_right == cur)
			{
				cur = parent;
				parent = parent->_parent;
			}

			_node = parent;
		}

		return *this;
	}

	Self& operator--()
	{
		if (_node->_left)
		{
			Node* maxLeft = _node->_left;
			while (maxLeft->_right)
			{
				maxLeft = maxLeft->_right;
			}

			_node = maxLeft;
		}
		else
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && parent->_left == cur)
			{
				cur = parent;
				parent = parent->_parent;
			}

			_node = parent;
		}

		return *this;
	}

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

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

	Node* _node;
};

改造红黑树插入

template<class K, class T, class KeyOfValue>
class RBTree
{
	typedef RBTreeNode<T> Node;
public:
	typedef RBTreeIterator<T, T&, T*> iterator;
	typedef RBTreeIterator<T, const T&, const T*> const_iterator;
	
	//树的最左结点
	iterator begin()
	{
		if (_root == nullptr)
		{
			return iterator(nullptr);
		}
		Node* pbegin = _root;
		while (pbegin->_left)
		{
			pbegin = pbegin->_left;
		}
		return iterator(pbegin);
	}

	iterator end()
	{
		return iterator(nullptr);
	}
	
	//迭代器指向的结点不能改变
	const_iterator begin() const
	{
		if (_root == nullptr)
		{
			return const_iterator(nullptr);
		}
		Node* pbegin = _root;
		while (pbegin->_left)
		{
			pbegin = pbegin->_left;
		}
		return const_iterator(pbegin);
	}

	const_iterator end() const
	{
		return const_iterator(nullptr);
	}
	
	
	pair<iterator, bool> insert(const T& data)
	{
		if (_root == nullptr)
		{
			_root = new Node(data);
			_root->_col = BLACK;
			return make_pair(iterator(_root), true);
		}
		
		//类实例化
		KeyOfValue kov;
		Node* cur = _root;
		Node* parent = _root;
		while (cur)
		{
			//仿函数
			if (kov(cur->_data) < kov(data))
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (kov(cur->_data) > kov(data))
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return make_pair(iterator(cur), false);
			}
		}

		cur = new Node(data);
		Node* ret = cur;
		cur->_col = RED;
		if (kov(parent->_data) < kov(cur->_data))
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		while (parent && parent->_col == RED)
		{
			Node* grandparent = parent->_parent;
			if (parent == grandparent->_left)
			{
				Node* uncle = grandparent->_right;
				if (uncle && uncle->_col == RED)
				{
					//第一种情况
					parent->_col = BLACK;
					uncle->_col = BLACK;
					grandparent->_col = RED;

					cur = grandparent;
					parent = cur->_parent;
				}
				else
				{
					//需要判断cur在parent的哪边
					if (parent->_right == cur)
					{
						//情况四
						RotateL(parent);
						RotateR(grandparent);
						cur->_col = BLACK;
						grandparent->_col = RED;
					}
					else
					{
						//情况二/三
						RotateR(grandparent);
						parent->_col = BLACK;
						grandparent->_col = RED;
					}
					break;
				}
			}
			else
			{
				Node* uncle = grandparent->_left;
				if (uncle && uncle->_col == RED)
				{
					//第一种情况
					parent->_col = BLACK;
					uncle->_col = BLACK;
					grandparent->_col = RED;

					cur = grandparent;
					parent = cur->_parent;
				}
				else
				{
					//需要判断cur在parent的哪边
					if (parent->_right == cur)
					{
						//情况二/三
						RotateL(grandparent);
						parent->_col = BLACK;
						grandparent->_col = RED;
					}
					else
					{
						//情况四
						RotateR(parent);
						RotateL(grandparent);
						cur->_col = BLACK;
						grandparent->_col = RED;
					}
					break;
				}

			}
		}

		_root->_col = BLACK;
		return make_pair(iterator(ret), true);
	}

	void RotateL(Node* parent)
	{
		Node* sub = parent->_right;
		Node* subL = sub->_left;

		parent->_right = subL;
		if (subL)
		{
			subL->_parent = parent;
		}
		sub->_left = parent;
		Node* ppnode = parent->_parent;
		parent->_parent = sub;

		if (ppnode == nullptr)
		{
			_root = sub;
			_root->_parent = nullptr;
		}
		else
		{
			if (ppnode->_right == parent)
			{
				ppnode->_right = sub;
			}
			else
			{
				ppnode->_left = sub;
			}
			sub->_parent = ppnode;
		}
	}

	void RotateR(Node* parent)
	{
		Node* sub = parent->_left;
		Node* subR = sub->_right;

		parent->_left = subR;
		if (subR)
		{
			subR->_parent = parent;
		}

		sub->_right = parent;
		Node* ppnode = parent->_parent;
		parent->_parent = sub;

		if (ppnode == nullptr)
		{
			_root = sub;
			_root->_parent = nullptr;
		}
		else
		{
			if (ppnode->_right == parent)
			{
				ppnode->_right = sub;
			}
			else
			{
				ppnode->_left = sub;
			}
			sub->_parent = ppnode;
		}
	}
	
	iterator find(const K& k)
	{
		if (_root == nullptr)
		{
			return iterator(nullptr);
		}

		KeyOfValue kov;
		Node* cur = _root;
		while (cur)
		{
			if (kov(cur->_data) < k)
			{
				cur = cur->_right;
			}
			else if (kov(cur->_data) > k)
			{
				cur = cur->_left;
			}
			else
			{
				return iterator(cur);
			}
		}

		return iterator(nullptr);
	}
private:
	Node* _root = nullptr;
};

结果测试

#include "map.h"
#include "set.h"
	void maptest()
	{
		test::map<int, int> m;
		int arr[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
		for (auto& e : arr)
		{
			m[e] = e;
		}

		test::map<int, int>::iterator it = m.begin();
		while (it != m.end())
		{
			cout << (*it).first << ":" << (*it).second << endl;
			++it;
		}

		test::map<string, int> countMap;
		string arr1[] = { "苹果", "西瓜", "香蕉", "草莓", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
		for (auto& e : arr1)
		{
			countMap[e]++;
		}

		for (auto& kv : countMap)
		{
			cout << kv.first << ":" << kv.second << endl;
		}

		test::map<string, int>::iterator it1 = countMap.find("西瓜");
		cout << "西瓜: " << (*it1).second << endl;
	}
	
	void settest()
	{
		test::set<int> s;
		int arr[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
		for (auto& e : arr)
		{
			s.insert(e);
		}

		test::set<int>::iterator it = s.begin();
		while (it != s.end())
		{
			//*it += 10; //set不能修改数据
			cout << *it << " ";
			++it;
		}

		cout << endl;
		it = s.find(4);
		cout << *it << endl;

	}
	
	int main()
	{
		settest();
		maptest();
		return 0;
	}

在这里插入图片描述

本节完~

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

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

相关文章

RabbitMQ学习(九):延迟队列

一、延迟队列概念延时队列中&#xff0c;队列内部是有序的&#xff0c;最重要的特性就体现在它的延时属性上&#xff0c;延时队列中的元素是希望 在指定时间到了以后或之前取出和处理。简单来说&#xff0c;延时队列就是用来存放需要在指定时间内被处理的 元素的队列。其实延迟…

005 利用fidder抓取app的api,获得股票数据

一、下载安装fidder 百度搜索fidder直接下载&#xff0c;按提示安装即可。 二、配置fidder 1. 打开fidder&#xff0c;选择tools——options。 2. 选择HTTPS选项卡&#xff0c;勾选前三项&#xff0c;然后点击右侧【actions】&#xff0c;选择【trust root certificate】&a…

七大排序经典排序算法

吾日三省吾身&#xff1a;高否&#xff1f;富否&#xff1f;帅否&#xff1f;答曰&#xff1a;否。滚去学习!!!(看完这篇文章先)目前只有C和C的功底&#xff0c;暂时还未开启新语言的学习&#xff0c;但是大同小异&#xff0c;语法都差不多。目录&#xff1a;一.排序定义二.排序…

fuzz测试之libfuzzer使用小结

fuzz测试之libfuzzer使用小结背景基本原理使用方法主调DEMO参考资料背景 项目中&#xff0c;为测试算法的鲁棒性&#xff0c;经常会用到fuzz测试进行压力测试。fuzz测试是一种模糊测试方法&#xff0c;本质是通过灌入各种变异的随机数据&#xff0c;去遍历不同函数分支&#xf…

蓝桥杯训练day1

前缀和差分1.前缀和(1)3956. 截断数组(2)795. 前缀和(3)796. 子矩阵的和(4)1230. K倍区间(5)99. 激光炸弹2.差分(1)797. 差分(2)差分矩阵(3)3729. 改变数组元素(4)100. 增减序列1.前缀和 (1)3956. 截断数组 方法1&#xff1a;暴力 先用两个数组分别保存前缀和&#xff0c;后缀…

如何快速、全面、深入地掌握一门编程语言

思考路线 如何快速&#xff1f; 什么样的Demo才能让人觉得你掌握了它&#xff1f; 空 判断&#xff1a;构造一个可以判断所有空的 is_empty 函数 for 循环&#xff1a;i 和 集合迭代两种 时间获取&#xff1a;年/月/日 时分秒 时间戳与时间格式互转 休眠时间函数 字符串处理…

对比学习MoCo损失函数infoNCE理解(附代码)

MoCo loss计算采用的损失函数是InfoNCE&#xff1a; ​​ 下面是MoCo的伪代码&#xff0c;MoCo这个loss的实现就是基于cross entropy loss。 将k作为q的正样本&#xff0c;因为k与q是来自同一张图像的不同视图&#xff1b;将queue作为q的负样本&#xff0c;因为queue中含有大量…

【Python学习笔记】44.Python3 MongoDB和urllib

前言 本章介绍Python的MongoDB和urllib。 Python MongoDB MongoDB 是目前最流行的 NoSQL 数据库之一&#xff0c;使用的数据类型 BSON&#xff08;类似 JSON&#xff09;。 PyMongo Python 要连接 MongoDB 需要 MongoDB 驱动&#xff0c;这里我们使用 PyMongo 驱动来连接。…

Ansys Zemax / SPEOS | 光源文件转换器

本文解释了如何在 SPEOS 与 Zemax 之间转换二进制光源文件。 下载 联系工作人员获取附件 简介 在本文中&#xff0c;为用户提供了一组Python代码&#xff0c;用于在Zemax和SPEOS之间转换源文件。 有些光源&#xff0c;如 .IES 文件&#xff0c;可在 SPEOS 和 Zemax 中进行…

计算机网络 | 谈谈TCP的流量控制与拥塞控制

文章目录一、TCP的流量控制1、利用滑动窗口实现流量控制【⭐⭐⭐】2、如何破解【死锁】局面❓二、TCP的拥塞控制1、拥塞控制的一般原理① 解决网络拥塞的误区② 拥塞控制与流量控制的关系【重点理解✔】2、TCP的拥塞控制方法① 接收窗口【rwnd】与拥塞窗口【cwnd】② 慢开始和拥…

BPE(Byte-Pair Encoding)简介

文章目录BPE简介Vocabulary构建Encoding and DecodingBPE简介 BPE是一种数据压缩算法的简单形式&#xff0c;数据中最常见的连续字节对被替换成该数据中不存在的字节。BPE的主要目标就是使用最少的token数目来表示一个corpus 在 A New Algorithm for Data Compression中首次提…

Spring IOC 容器 Bean 加载过程

Spring IOC 容器 Bean 加载过程 Spring 对于我们所有的类对象进行了统一抽象&#xff0c;抽象为 BeanDefinition &#xff0c;即 Bean 的定义&#xff0c;其中定义了类的全限定类名、加载机制、初始化方式、作用域等信息&#xff0c;用于对我们要自动装配的类进行生成。 Sprin…

新版本 | 异步复制、交易日历、自定义状态函数......请查收!

大家好~DolphinDB 最新版本近日已经发布&#xff0c;本次的 V2.00.9 与 V1.30.21 新版本推出了很多新功能&#xff0c;并对数据库做了全方位提升&#xff0c;是迄今为止新增功能最多的一次更新。新特性一览我们先来看一看新特性包含哪些方面&#xff1a;1、数据库针对数据安全和…

管理.模型.SWOT

1. SWOT 在企业战略规划中&#xff0c;通过辨析企业自身的竞争优势&#xff08;Strengths&#xff09;、劣势&#xff08;Weaknesses&#xff09;和外部环境为企业带来的机会&#xff08;Opportunities&#xff09;和威胁&#xff08; Threats&#xff09;&#xff0c;企业可制…

Interview系列 - 05 Java|Iterator迭代器|集合继承体系|Set List Map接口特性|List实现类区别

文章目录01. 迭代器 Iterator 是什么&#xff1f;02. 迭代器 Iterator 有什么特点&#xff1f;03. 迭代器 Iterator 怎么使用&#xff1f;04. 如何边遍历边移除 Collection 中的元素&#xff1f;05. Iterator 和 ListIterator 有什么区别&#xff1f;06. 数组和集合的区别&…

Alist ——本地网盘管理器

Alist ——本地网盘管理器 一、下载工具 Alist https://github.com/alist-org/alist二、启动登录 进入下载好的文件中&#xff0c;在地址栏输入cmd进入命令行启动 进入命令行输入 alist start启动 记住密码&#xff0c;和端口进入浏览器 输入 &#xff1a;127.0.0.1:5244用…

java final关键字 详解

概述&#xff1a;作用&#xff1a;细节&#xff1a;演示&#xff1a;总结&#xff1a;一、概述 : final [ˈ faɪnl]&#xff0c;最终的&#xff0c;最后的&#xff0c;决定性的&#xff0c;不可改变的。final作为Java中的一个关键字可以用来修饰类&#xff0c;方法&#xff0c…

【程序人生】从土木专员到网易测试工程师,薪资翻3倍,他经历了什么?

转行对于很多人来说&#xff0c;是一件艰难而又纠结的事情&#xff0c;或许缺乏勇气&#xff0c;或许缺乏魄力&#xff0c;或许内心深处不愿打破平衡。可对于我来说&#xff0c;转行是一件不可不为的事情&#xff0c;因为那意味着新的方向、新的希望。我是学工程管理的&#xf…

京东测试进阶之路:初入测试碎碎念篇

1、基本的测试用例设计方法 基本的测试用例设计方法&#xff08;边界值分析、等价类划分等&#xff09;。 业务和场景的积累&#xff0c;了解测试需求以及易出现的bug的地方。 多维角度设计测试用例&#xff08;用户、业务流程、异常场景、代码逻辑&#xff09;。 2、需求分析 …

idea自带maven位置、maven全局环境变量配置,安装jar到本地 mac

声明&#xff1a;本教程为mac版教程&#xff0c;Windows请路过 idea自带maven3配置全局环境变量 mac电脑maven3位置/Applications/IntelliJ\ IDEA.app/Contents/plugins/maven/lib/maven3配置全局变量,编~/.profile文件&#xff08;没有则新建&#xff09; export MAVEN/App…