unordered_map 与 unordered_set 的模拟实现

unordered_map 与 unordred_set 的模拟实现与 map 与 set 的模拟实现差不多。map 与 set 的模拟实现中,底层的数据结构是红黑树。unordered_map 与 unordered_set 的底层数据结构是哈希表。因此,在模拟实现 unordered_map 与 unordred_set 之前你必须确保你已经熟练的掌握哈希表。如果你还对哈希表感到陌生,你可以参考我之前写过的这两篇文章:
哈希表开散列的实现
我们这里实现的 unordered_mapunordered_set 是以开散列实现的哈希表作为底层数据结构的哦!

修改存储的数据

在 unordered_map 与 unordred_set 的使用部分,我们知道:他俩存储的数据类型是不一样的,为了让哈希表能同时适配出 unordered_mapunordered_set 我们就需要对 HashNode 进行一定程度的修改。原理比较简单哈,因为他们俩的数据类型不一样,我们只需要将他俩存储的数据类型模板化即可。通过传入不同的数据类型,实例化出 HashNode 存储不同元素的哈希表。

  • 当这个模板参数传入 pair 那么是 unordered_map
  • 当这个模板参传入的不是一个 pair 那么就是一个 unordered_set
template<class T>
struct HashNode
{
   T _data;
   HashNode<T>* _next;

   HashNode(const T& data)
   	:_data(data)
   	, _next(nullptr)
   {}
};

我们将原本两个模板参数改为了一个,传入什么类型,那么 HashNode 就存储这个类型。

key 的获取

对于 unnordered_set 来说,他的 key 值就是模板参数 T。但是对于 unordered_map 来说,他的 key 值却是:T.first。因为 unordered_map 存储的是一个 pair 嘛。
怎么解决呢?处理思路和模拟实现 mapset 是一样的。通过仿函数来实现。
因此在 HashTable 中又要增加一个模板参数,不妨叫做:KeyOfT。在这个仿函数中,我们会根据传入的数据类型获取他的 key 值。

  • 对于 unordered_map 来说仿函数会返回传入参数 first。因为它存储的是 pair 嘛。key 值就是他的 first。
  • 对于 unordered_set 来说,仿函数直接将传入参数本身返回。因为他存储的数据就是 key 值本身嘛!
//这是 unordered_set.h 的代码
#pragma once
#include"Hash.h"

template<class K>
class unordered_set
{
public:
    struct SetKeyOfT
    {
        const K& operator()(const K& key)
        {
            return key;
        }
    };
private:
    HashTable<K, K, SetKeyOfT> _ht;
};

在上面的代码中,我们定义了一个结构体 SetOfT 在这个结构体中,我们重载了圆括号运算符。然后将这个类型传入 HashTable 用来获取 unorered_set 的 key 值。你可能会问:为什么还要传入一个 K 值来实例化哈希表呢?不着急等会儿马上为你解答。

//这是 unordered_map.h 中的代码
#pragma once
#include "Hash.h"

template<class K, class V>
class unordered_map
{
public:
    struct MapKeyOfT
    {
        const K& operator()(const pair<K, V>& kv)
        {
            return kv.first;
        }
    };
private:
    HashTable<K, pair<K, V>, MapKeyOfT> _ht;
};

同样的,我们定义了一个结构体:MapOfT 在这个结构体结构体中,重载了圆括号运算符,然后将这个结构体类型传入 HashTable 中用来获取 unordered_map 的 key 值。


我们修改了 HashNode 的存储数据, HashTable 的模板参数。那么 HashTable 中的代码也需要做相应的修改。
我们先来看 HashTable 的模板参数:
在这里插入图片描述

  • K:有了这个模板参数,HashTable 的函数参数的类型能更加好写:
    在这里插入图片描述
    在这里插入图片描述
    上面的两个成员函数中,我们都需要用到 key 的类型来定义函数的形参。如果 HashTable 的模板参数不加上 K,我们就很难获取 unordered_mapunoredred_set 的 key 值的类型。这便是为啥要在 unordered_map.hunordered_set 中实例化哈希表的时候传入 key 值的类型。是不是非常精妙。

  • T:存储的数据类型,unordered_map 就是 pair<K, V>unordered_set 就是:K。

  • KeyOfT:用该类型实例化出来对象,调用 operator(),仿函数嘛!获取 key 值。

  • HashFunc:这个模板参数可以处理字符串作为 key 值的情况。详细的逻辑在之前的文章:哈希表闭散列实现中讲过。


我们再来看 HashTable 中代码的修改:

  • insert 函数的参数不再是 const pair<K, V>& kv ,而是 const T& data
  • 所有的获取 key 值的地方,都要使用仿函数来获取。

普通迭代器

不用说,哈希表的迭代器肯定不是原生指针。因为每一个哈希表节点数据并不是连续的物理空间。
我们就要思考如何封装哈希表的迭代器:

  • 首先,类中肯定包含 HashNode 的指针。
  • 其次,在对迭代器进行加加或者减减运算时,可能会跨越不同的哈西桶,这个时候就比较难办了!不妨先想想我们应该如何解决。
    在这里插入图片描述
    • 假设我们当前迭代器指向的 HashNode 的值为 44,那么当这个迭代器加加之后,我们就要跨越当前的哈希桶,找到下一个有效的 HashNode
    • 再假设我们当前迭代器指向的 HashNode 的值为 7,那么当这个迭代器减减之后,我们也要跨越当前哈希桶,向前找到一个有效的 HashNode
  • 刚才谈到,我们迭代器加加或者减减之后可能跨越哈希桶。那么是不是要在迭代器中封装当前的哈希桶在数组中的下标呢?仔细一想其实是不需要的,因为我们可以通过当前迭代器,拿到存储数据的 key 值,经过除留余数法获得其在数组中的下标。
    但是另外的一个问题就暴露出来了:跨越哈希桶的时候,迭代器中拿不到哈希表的 _table 哇,我们就无法向前或者向后寻找一个有效的 HashNode。因此,在迭代器中我们可以封装一个 HashTable 的指针,在跨越哈希桶的时候就能顺利地向前或者向后查找啦!(当然实现的方法有很多,这取决于你的想象力啦!比如:你可以将 _table 传过来)
template <class K, class T, class KeyOfT, class HashFunc = DefaultHashFunc<K>>
struct __HashIterator
{
	typedef HashNode<T> Node;
	typedef __HashIterator<K, T, KeyOfT, HashFunc> self;
	
	//构造函数
	__HashIterator(Node* node, HashTable<K, T, KeyOfT, HashFunc>* pht)
	{
		_node = node;
		_pht = pht;
	}

	HashTable<K, T, KeyOfT, HashFunc>* _pht;
	Node* _node;
};

operator++()

  • 如果当前迭代器的 _node->_next 不为 nullptr,说明当前节点的 _next 是一个有效的节点。我们直接修改 _node_node->_next 即可。
  • 如果当前迭代器的 _node->_next 为空,那么,我们就需要寻找下一个位置。
    • 首先我们需要计算当前迭代器的 _nodeHashTable 的哪个下标。
    • 然后从计算出来的下标的下一个位置开始,查找下一个不为空的哈希桶。
    • 如果找不到不为空的哈希桶,说明当前迭代器的节点已经是哈希表的最后一个有效元素了。我们可以令 _nodenullptr 充当我们 end 迭代器。
self operator++()
{
	if(_node->_next) //当前的哈希桶还有节点
	{
		_node = _node->_next;
		return *this;
	}
	else
	{
		KeyOfT kot;
		HashFunc hf;
		size_t hashi = hf(kot(_node->_data)) % _pht->_table.size();
		++hashi;
		while(hashi < _pht->_table.size())
		{
			if(_pht->_table[hashi])
			{
				_node = _pht->_table[hashi];
				return *this;
			}
			else
			{
				++hashi;
			}
		}

		_node = nullptr;
		return *this;
	}
}

operator!=()

这个函数简单,直接用节点的指针比较就可以啦!

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

operator*() 与 operator->()

这两个函数我们也写了很多遍了。分别返回数据域和数据域的指针就行。

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

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

begin() 和 end()

这两个函数是在 HashTable 的类中写,千万不敢写迷糊了。

  • begin():我们只需要遍历 _table 找到第一个不为空的哈希桶。返回这个哈希桶的第一个节点就行。如果找不到,就用 nullptr 作为迭代器构造函数的第一个参数返回。
  • end():直接用 nullptr 作为迭代器的第一个参数即可。
    __HashIterator 中,构造函数的第二个参数是哈希表的指针,begin 与 end 返回时第二个参数应该怎么传递呢?是不是就是 this 指针哇!这就是为什么我们将哈希表的指针作为迭代器成员的原因,因为简单嘛!如果是 _table 也行,总归要麻烦点。
iterator begin()
{
	for(size_t i = 0; i < _table.size(); i++)
	{
		Node* cur = _table[i];
		if(cur) return iterator(cur, this);
	}
	return iterator(nullptr, this);
}

iterator end()
{
	return iterator(nullptr, this);
}

你编译一下代码,发现编译不通过:原因就是在迭代器中你要用哈希表,在哈希表中你又要用迭代器。无论将哪一个类放在前面都行不通。因此需要加前置声明。

template <class K, class T, class KeyOfT, class HashFunc>
class HashTable;

unorered_mapunordered_set 添加 begin() 与 end()

这两个函数都比较简单哈!我们已经在 HashTable 中实现了 begin 与 end 函数。因此只需要在这两个容器中分别调用 begin 与 end 函数即可。

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

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

上面的代码在 unordred_mapunordered_set 中都搞一份就行啦!

代码写到这里我们就可以使用范围 for 遍历我们自己实现的 unordred_mapunordered_set 了。但是我们运行代码之后又报了一个错误。说是:_table 不可访问。在迭代器中我们封转了哈希表的指针。但是因为 _table 是哈希表中的私有成员,外部当然不能访问啦!
在这里插入图片描述
这里就有两种比较靠谱的解决方式:

  • 写一个函数,返回 HashTable 的 _table 成员。
  • 用友元,谁是谁的友元呢?当然是 __HashIteratorHashTable 的友元啦!
template <class K, class T, class KeyOfT, class HashFunc>
friend struct __HashIterator;

上面的代码在 visual studio 中运行是没有问题的,但是在 vscode 中就不能运行。报错的提示是:嵌套作用域不能使用相同的模板参数。如果你遇到了这样的报错提示,那么随便改一下模板参数的名称即可!

const 迭代器

首先我们要明确普通迭代器与 const 迭代器的区别,在之前封转 list,map,set 我们就已经知道,其实就是 begin 与 end 的返回值类型不一样嘛!处理方式都是一样的:将不一样的地方参数化即可!因此我们又要向 __HashIterator 中加入模板参数啦!

template <class K, class T, class Ref, class Ptr, class KeyOfT, class HashFunc>
struct __HashIterator
  • Ref:表示返回引用,如果是普通迭代器传入的 Ref 就是 T&,返回的也就是 T& 啦!如果是 const 迭代器,传入的 Ref 就是 const T&,返回的自然也是 const T& 啦!
  • Ptr:表示返回指针。如果是普通迭代器传入的 Ptr就是 T*,返回的也就是 T* 啦!如果是 const 迭代器,传入的 Ptr就是 const T*,返回的自然也是 const T* 啦!

因此:在 HashTable 中就要传入不同的参数类型,来定义出普通迭代器和 const 迭代器。

typedef __HashIterator<K, T, T&, T*, KeyOfT, HashFunc> iterator;
typedef __HashIterator<K, T, const T&, const T*, KeyOfT, HashFunc> const_iterator;

实现了 cosnt_iterator 之后呢?我们就需要为 HashTable 添加 const 迭代器的版本,也是非常简单呢!添加完成了之后,我们再为 unordred_mapunordered_set 添加 cosnt 迭代器。其中 unordered_set 的 key 值不允许修改,因此 unordered_set 的 iterator 与 const_iterator都是 const_iterator。
map 解决 key 不能被修改的方法就是:在 pair 中为 K 加上 const。
但是这么修改之后报错:
在这里插入图片描述

invalid conversion from ‘const HashTable<int, int, unordered_set::SetKeyOfT, DefaultHashFunc >’ to ‘HashTable<int, int, unordered_set::SetKeyOfT, DefaultHashFunc >’ [-fpermissive]

这是什么原因呢?begin() const 中的 const 修饰的是 this 指向的内容。因此,这里的 this 的完整类型为:const HashTable<K, T, KeyOfT, HashFunc> * 但是呢?迭代器的构造函数的第二个参数是:HashTable<K, T, KeyOfT, HashFunc>* pht
非 const 是无法转化为 const 类型的,因此会报错,解决办法就是在构造函数的参数加上 cosnt 就行。并且将 __HashIterator 的哈希表指针的成员改为 const。因为迭代器中不会通过哈希表的指针修改哈希表的内容。因此这么写完全没有问题呢!

修改 Find 返回值

在库函数中 find 的返回值是 iterator。因此我们也需要修改。修改的方法很简单,只需在返回的地方处理一下就可以啦!
在返回的地方调用构造函数就可以啦!

iterator Find(const K &key)
{
	HashFunc hf;
	KeyOfT kot;
	size_t hashi = hf(key) % _table.size();
	Node *cur = _table[hashi];
	while (cur)
	{
		if (kot(cur->_data) == key)
		{
			return iterator(cur, this);
		}

		cur = cur->_next;
	}

	return iterator(nullptr, this);
}

修改 insert 函数的返回值

首先我们得知道库函数中 Insert 的返回值:pair<iterator, bool>

  • 在判断哈希表中是否已经存在这个元素的时候,需要用 iterator 接收 Find 函数的返回值。通过这个返回值与 end 做比较,如果不等于 end 说明哈希表中已经存在这个元素了,我们返回这个迭代器,和 false 构造出来的 pair 即可。
  • 如果是新插入,就用新插入的节点构造一个迭代器对象和 true 一起打包成 pair 返回即可。
    在这里插入图片描述
    在这里插入图片描述
    好的,我们现在已经修改完成了 HashTable 中的 Insert 函数。之后呢,我们顺利的修改了 unordered_mapunordered_set 中的 insert 函数,但是修改之后编译,发现又出问题了!
    在这里插入图片描述
    如上图,在 unordered_set 中,insert 的返回值虽然看上去是:pair<iterator, bool> 但是因为 unordered_set 的 iterator 与 const_iterator 都是 const_iterator 。因此,unordered_set 的 insert 函数返回值的实际类型是:pair<const_iterator, bool> 自然会出现类型不兼容,无法转换的问题。
    解决办法在 mapset 的模拟实现部分已经讲解过了。
    我们可以为 __HashIterator 加上一个非常像构造函数的函数:
typedef __HashIterator<K, T, T*, T&, KeyOfT, HashFunc> Iterator;
__HashIterator(const Iterator& it)
	:_node(it._node)
	,_pht(it._pht)
{}

Iterator 是用 T*,T&定义的,而不是 Ref 和 Ptr,因此当这个迭代器是普通迭代器的时候,就是拷贝构造函数。当这个迭代器是 const 迭代器的时候,就是一个类型转换。非常巧妙。

unordered_map 的 opertor[]

原理比较简单,就是调用 insert 函数。无论插入成功还是失败,都将 insert 返回值对应的 iterator 的数据中的 second 返回即可。

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

完整代码

Hash.h

#pragma once
#include <vector>

template <class K>
struct DefaultHashFunc
{
	size_t operator()(const K &key)
	{
		return (size_t)key;
	}
};

// 12:00
template <>
struct DefaultHashFunc<string>
{
	size_t operator()(const string &str)
	{
		// BKDR
		size_t hash = 0;
		for (auto ch : str)
		{
			hash *= 131;
			hash += ch;
		}

		return hash;
	}
};

template <class T>
struct HashNode
{
	T _data;
	HashNode<T> *_next;

	HashNode(const T &data)
		: _data(data), _next(nullptr)
	{
	}
};

template <class K, class T, class KeyOfT, class HashFunc>
class HashTable;

template <class K, class T, class Ref, class Ptr, class KeyOfT, class HashFunc>
struct __HashIterator
{
	typedef HashNode<T> Node;
	typedef __HashIterator<K, T, Ref, Ptr, KeyOfT, HashFunc> self;
	typedef __HashIterator<K, T, T&, T*, KeyOfT, HashFunc> Iterator;
	
	//构造函数
	__HashIterator(Node* node, const HashTable<K, T, KeyOfT, HashFunc>* pht)
	{
		_node = node;
		_pht = pht;
	}

	__HashIterator(const Iterator& it)
		:_node(it._node)
		,_pht(it._pht)
	{}

	self operator++()
	{
		if(_node->_next) //当前的哈希桶还有节点
		{
			_node = _node->_next;
			return *this;
		}
		else
		{
			KeyOfT kot;
			HashFunc hf;
			size_t hashi = hf(kot(_node->_data)) % _pht->_table.size();
			++hashi;
			while(hashi < _pht->_table.size())
			{
				if(_pht->_table[hashi])
				{
					_node = _pht->_table[hashi];
					return *this;
				}
				else
				{
					++hashi;
				}
			}

			_node = nullptr;
			return *this;
		}
	}

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

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

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

	const HashTable<K, T, KeyOfT, HashFunc>* _pht;
	Node* _node;
};

template <class K, class T, class KeyOfT, class HashFunc = DefaultHashFunc<K>>
class HashTable
{
public:
	typedef HashNode<T> Node;
	typedef __HashIterator<K, T, T&, T*, KeyOfT, HashFunc> iterator;
	typedef __HashIterator<K, T, const T&, const T*, KeyOfT, HashFunc> const_iterator;

private:
	template <class U, class Q, class W, class E, class Y, class I>
	friend struct __HashIterator;

public:


	iterator begin()
	{
		for(size_t i = 0; i < _table.size(); i++)
		{
			Node* cur = _table[i];
			if(cur) return iterator(cur, this);
		}
		return iterator(nullptr, this);
	}

	iterator end()
	{
		return iterator(nullptr, this);
	}


	const_iterator begin() const
	{
		for(size_t i = 0; i < _table.size(); i++)
		{
			Node* cur = _table[i];
			if(cur) return const_iterator(cur, this);
		}
		return const_iterator(nullptr, this);
	}

	const_iterator end() const
	{
		return const_iterator(nullptr, this);
	}

	HashTable()
	{
		_table.resize(10, nullptr);
	}

	~HashTable()
	{
		for (size_t i = 0; i < _table.size(); i++)
		{
			Node *cur = _table[i];
			while (cur)
			{
				Node *next = cur->_next;
				delete cur;
				cur = next;
			}

			_table[i] = nullptr;
		}
	}

	pair<iterator, bool> Insert(const T &data)
	{
		KeyOfT kot;

		iterator it = Find(kot(data));
		if(it != end())
		{
			return make_pair(it, false);
		}

		HashFunc hf;

		// 负载因子到1就扩容
		if (_n == _table.size())
		{
			// 16:03继续
			size_t newSize = _table.size() * 2;
			vector<Node *> newTable;
			newTable.resize(newSize, nullptr);

			// 遍历旧表,顺手牵羊,把节点牵下来挂到新表
			for (size_t i = 0; i < _table.size(); i++)
			{
				Node *cur = _table[i];
				while (cur)
				{
					Node *next = cur->_next;

					// 头插到新表
					size_t hashi = hf(kot(cur->_data)) % newSize;
					cur->_next = newTable[hashi];
					newTable[hashi] = cur;

					cur = next;
				}

				_table[i] = nullptr;
			}

			_table.swap(newTable);
		}

		size_t hashi = hf(kot(data)) % _table.size();
		// 头插
		Node *newnode = new Node(data);
		newnode->_next = _table[hashi];
		_table[hashi] = newnode;
		++_n;
		return make_pair(iterator(newnode, this), true);
	}

	iterator Find(const K &key)
	{
		HashFunc hf;
		KeyOfT kot;
		size_t hashi = hf(key) % _table.size();
		Node *cur = _table[hashi];
		while (cur)
		{
			if (kot(cur->_data) == key)
			{
				return iterator(cur, this);
			}

			cur = cur->_next;
		}

		return iterator(nullptr, this);
	}

	bool Erase(const K &key)
	{
		HashFunc hf;
		KeyOfT kot;
		size_t hashi = hf(key) % _table.size();
		Node *prev = nullptr;
		Node *cur = _table[hashi];
		while (cur)
		{
			if (kot(cur->_data) == key)
			{
				if (prev == nullptr)
				{
					_table[hashi] = cur->_next;
				}
				else
				{
					prev->_next = cur->_next;
				}

				delete cur;
				return true;
			}

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

		return false;
	}

	void Print()
	{
		for (size_t i = 0; i < _table.size(); i++)
		{
			printf("[%d]->", i);
			Node *cur = _table[i];
			while (cur)
			{
				cout << cur->_kv.first << ":" << cur->_kv.second << "->";
				cur = cur->_next;
			}
			printf("NULL\n");
		}
		cout << endl;
	}

private:
	vector<Node *> _table; // 指针数组
	size_t _n = 0;		   // 存储了多少个有效数据
};

unodered_map.h

#pragma once

#include "Hash.h"

template<class K, class V>
class unordered_map
{
public:
    struct MapKeyOfT
    {
        const K& operator()(const pair<K, V>& kv)
        {
            return kv.first;
        }
    };
    typedef typename HashTable<K, pair<const K, V>, MapKeyOfT>::iterator iterator;
    typedef typename HashTable<K, pair<const K, V>, MapKeyOfT>::const_iterator const_iterator;

    pair<iterator, bool> insert(const pair<K, V>& kv)
    {
        return _ht.Insert(kv);
    }

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

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


private:
    HashTable<K, pair<const K, V>, MapKeyOfT> _ht;
};

unordered_set.h

#pragma once
#include"Hash.h"

template<class K>
class unordered_set
{
public:
    struct SetKeyOfT
    {
        const K& operator()(const K& key)
        {
            return key;
        }
    };
    typedef typename HashTable<K, K, SetKeyOfT>::const_iterator iterator;
    typedef typename HashTable<K, K, SetKeyOfT>::const_iterator const_iterator;

    pair<iterator, bool> insert(const K& key)
    {
        pair<typename HashTable<K, K, SetKeyOfT>::iterator, bool> ret = _ht.Insert(key);
        return pair<iterator, bool>(ret.first, ret.second);
    }

    iterator begin() const
    {
        return _ht.begin();
    }

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

private:
    HashTable<K, K, SetKeyOfT> _ht;
};


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

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

相关文章

nodejs微信小程序+python+PHP-青云商场管理系统的设计与实现-安卓-计算机毕业设计

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性&#xff1a;…

org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder

密码&#xff0c;加密&#xff0c;解密 spring-security-crypto-5.7.3.jar /** Copyright 2002-2011 the original author or authors.** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with t…

HTML新特性【缩放图像、图像切片、平移、旋转、缩放、变形、裁切路径、时钟、运动的小球】(二)-全面详解(学习总结---从入门到深化)

目录 绘制图像_缩放图像 绘制图像_图像切片 Canvas状态的保存和恢复 图形变形_平移 图形变形_旋转 图形变形_缩放 图形变形_变形 裁切路径 动画_时钟 动画_运动的小球 引入外部SVG 绘制图像_缩放图像 ctx.drawImage(img, x, y, width, height) img &#xf…

开源与闭源

我的观点&#xff1a; 开源与闭源软件都有各自的优势和劣势&#xff0c;没有绝对的对错之分。.. 一、开源和闭源的优劣势比较 开源的好处与劣处 优势&#xff1a; 创新与合作&#xff1a;开源软件能够吸引更多的开发者参与到项目中来&#xff0c;促进创新和合作。开放的源代码…

【网易云商】构建高效 SaaS 系统的技术要点与最佳实践

SaaS 是什么 定义 相信大家都对云服务中的 IaaS、PaaS、SaaS 早就有所耳闻&#xff0c;现在更是衍生出了 aPaaS、iPaaS、DaaS 等等的类似概念。对于 SaaS 也有各种各样的定义&#xff0c;本文给出的定义是&#xff1a; SaaS 是一种基于互联网提供服务和软件的交付模式&#xf…

一文彻底看懂Python切片,Python切片理解与操作

1.什么是切片 切片是Python中一种用于操作序列类型(如列表、字符串和元组)的方法。它通过指定起始索引和结束索引来截取出序列的一部分,形成一个新的序列。切片是访问特定范围内的元素,就是一个Area。 说个笑话:切片不是切片,而是切片,但是又是切片。大家理解下呢(末…

80C51单片机----数据传送类指令

目录 一.一般传送指令&#xff0c;即mov指令 1.16位传送&#xff08;仅1条&#xff09; 2.8位传送 &#xff08;1&#xff09;目的字节为A&#xff08;累加器&#xff09; &#xff08;2&#xff09;目的字节为Rn(工作寄存器) &#xff08;3&#xff09;目的字节为direct…

java中的String.format()方法详解

介绍 String.format() 是 Java 中的一个字符串格式化方法&#xff0c;它用于生成指定格式的字符串。这个方法可以接受一个或多个参数&#xff0c;并将它们按照指定的格式插入到字符串中。它使用了类似于 C 语言中的 printf 函数的语法。 String.format() 方法的使用格式如下&…

Tars框架 Tars-Go 学习

Tars 框架安装 网上安装教程比较多&#xff0c;官方可以参数这个 TARS官方文档 (tarsyun.com) 本文主要介绍部署应用。 安装完成后Tars 界面 增加应用amc 部署申请 amc.GoTestServer.GoTestObj 名称不知道的可以参考自己创建的app config 点击刷新可以看到自己部署的应用 服…

微机原理_3

一、单项选择题(本大题共15小题,每小题3分,共45分。在每小题给出的四个备选项中,选出一个正确的答案,请将选定的答案填涂在答题纸的相应位置上。) 在 8086 微机系统中&#xff0c;完成对指令译码操作功能的部件是&#xff08;)。 A. EU B. BIU C. SRAM D. DRAM 使计算机执行某…

【Rust日报】2023-11-22 Floneum -- 基于 Rust 的一款用于 AI 工作流程的图形编辑器

Floneum -- 基于 Rust 的一款用于 AI 工作流程的图形编辑器 Floneum 是一款用于 AI 工作流程的图形编辑器&#xff0c;专注于社区制作的插件、本地 AI 和安全性。 Floneum 有哪些特性&#xff1a; 可视化界面&#xff1a;您无需任何编程知识即可使用Floneum。可视化图形编辑器可…

2023年金融信创行业研究报告

第一章 行业概况 1.1 定义 金融信创是指在金融行业中应用的信息技术&#xff0c;特别是那些涉及到金融IT基础设施、基础软件、应用软件和信息安全等方面的技术和产品。这一概念源于更广泛的“信创 (信息技术应用创新)”&#xff0c;即通过中国国产信息技术替换海外信息技术&a…

某60区块链安全之未初始化的存储指针实战二学习记录

系列文章目录 文章目录 系列文章目录未初始化的存储指针实战二实验目的实验环境实验工具实验原理实验内容实验过程EXP利用 未初始化的存储指针实战二 实验目的 学会使用python3的web3模块 学会分析以太坊智能合约未初始化的存储指针漏洞 找到合约漏洞进行分析并形成利用 实验…

【Vue】图片切换

上一篇&#xff1a; vue的指令 https://blog.csdn.net/m0_67930426/article/details/134599378?spm1001.2014.3001.5502 本篇所需要的指令有&#xff1a; v-on v-bind v-show <!DOCTYPE html> <html lang"en"> <head><meta charset"…

【微服务专题】SpringBoot自动配置源码解析

目录 前言阅读对象阅读导航前置知识笔记正文0、什么是自动配置0.1 基本概念0.2 SpringBoot中的【约定大于配置】0.3 从SpringMVC看【约定大于配置】0.4 从Redis看【约定大于配置】 一、EnableAutoConfiguration源码解析二、SpringBoot常用条件注解源码解析2.1 自定义条件注解2.…

基于 STM32Cube.AI 的嵌入式人脸识别算法实现

本文介绍了如何使用 STM32Cube.AI 工具开发嵌入式人脸识别算法。首先&#xff0c;我们将简要介绍 STM32Cube.AI 工具和 STM32F系列单片机的特点。接下来&#xff0c;我们将详细讨论如何使用 STM32Cube.AI 工具链和相关库来进行人脸识别算法的开发和优化。最后&#xff0c;我们提…

仿 美图 / 饿了么,店铺详情页功能

前言 UI有所不同&#xff0c;但功能差不多&#xff0c;商品添加购物车功能 正在写&#xff0c;写完会提交仓库。 效果图一&#xff1a;左右RecyclerView 联动 效果图二&#xff1a;通过点击 向上偏移至最大值 效果图三&#xff1a;通过点击 或 拖动 展开收缩公告 效果图四&…

2021年12月 Scratch(三级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 执行下列程序,屏幕上可以看到几只小猫? A:1 B:3 C:4 D:0 答案:B 第2题 下列程序哪个可以实现:按下空格键,播放完音乐后说“你好!”2秒? A: B: C:

知虾官网:探索跨境电商数据的新平台

随着电子商务的快速发展&#xff0c;跨境电商已成为全球贸易的重要组成部分。为了帮助企业更好地了解市场、选品、分析竞争对手和科学运营&#xff0c;知虾官网应运而生。本文将为您介绍知虾官网的功能和优势&#xff0c;以及广州萌啦信息科技有限公司的背景。 一、知虾官网的功…

Python中classmethod的妙用

更多Python学习内容&#xff1a;ipengtao.com 在Python中&#xff0c;classmethod装饰器为开发者提供了一种强大的工具&#xff0c;使得类方法的定义和使用更加灵活。本文将深入探讨classmethod的妙用&#xff0c;通过丰富的示例代码展示其在不同场景下的实际应用。 类方法与实…