【C++】哈希表封装unordered系列

news2024/11/17 7:50:36

 

文章目录

  • 前言
  • 一、哈希表的封装
  • 总结


前言

在看本篇文章前大家尽量拿出上一篇文章的代码跟着一步步实现,否则很容易引出大量模板错误而无法解决。


一、哈希表的封装

首先我们要解决映射的问题,我们目前的代码只能映射整形,那么如何支撑浮点数等的映射呢?只需要多加一个模板参数就可以了:

template <class K, class V>
	struct HashNode
	{
		HashNode<K, V>* _next;
		pair<K, V> _kv;
		HashNode(const pair<K, V>& kv)
			:_kv(kv)
			, _next(nullptr)
		{

		}
	};
	template<class K>
	struct HashFunc
	{
		size_t operator()(const K& key)
		{
			return key;
		}
	};
	template <class K, class V, class Hash>
	class HashTable
	{
		typedef HashNode<K, V> Node;

 这个仿函数可以将任何支持隐式类型转换的key转换为size_t类型,比如double类型会被隐式转换为size_t类型,那么字符串该如何解决呢?我们直接用模板特化来解决:(模板的特化就是有特化就走特化,没有特化就走原类型)

template <>
	struct HashFunc<string>
	{
		size_t operator()(const string& s)
		{
			size_t hashi = 0;
			for (auto& e : s)
			{
				hashi += e;
			}
			return hashi;
		}
	};

然后我们给Hash这个模板参数一个缺省参数,默认使用我们的仿函数:

 然后我们在每个函数体内将取模的key值用仿函数包一下:(这里我们只展示了修改后的代码,并不是函数体就只有这些代码)

bool insert(const pair<K, V>& kv)
		{
            //.....
			Hash hash;
			size_t hashi = hash(cur->_kv.first) % newtable.size();
			size_t hashi = hash(kv.first) % _tables.size();
            //......
			return true;
		}

Node* Find(const K& key)
		{
            //.....
			Hash hash;
			size_t hashi = hash(key) % _tables.size();
            //.......
			return nullptr;
		}


bool eraser(const K& key)
		{
            //.......
			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			//......
			return false;
		}

但是这个会有什么问题呢?比如abc这个字符串和cab这个字符串计算出来的hashi是一样的啊,这样不就增加了哈希冲突了吗,如下图所示:(HashStr就是我们的特化版本)

为了解决这个问题我们查阅相关资料后发现:

 只需要每次累乘因子31就能解决这样的问题,为什么是31呢?因为31是在大量测试中表现最好的一个值:

template <>
	struct HashFunc<string>
	{
		size_t operator()(const string& s)
		{
			size_t hashi = 0;
			for (auto& e : s)
			{
				hashi += e;
				hashi *= 31;
			}
			return hashi;
		}
	};

 所以特化版本变成上面这样就解决了问题:

 可以看到确实成功解决了这个问题,在这里我们补充一个上一篇遗留的问题:哈希表增删查改的时间复杂度都是O(1),并且因为有着载荷因子的控制每个桶的元素不会太长,下面我们验证一下:

 我们随机插入10000个随机数,然后将每个桶插入几个元素就打印出来:

 结果就是每个桶基本就是0-2之间,这就证明了我们刚刚说的由载荷因子控制每个桶的长度。那么如果面试官问出现了一个桶有很多的值,这个时候查找变成O(N)该怎么解决?其实很简单,当某个桶的长度超过规定的大小我们就将这个桶改为红黑树,红黑树会大幅度减少高度,这样就解决了这样的问题。除了刚刚挂红黑树可以优化哈希表,还有一个方法能优化,那就是每次开空间的大小都为素数,为什么是素数呢?因为素数只能被1和他本身整除,如果空间大小是素数每次映射的时候就能减少冲突。代码如下:

size_t GetNextPrime(size_t prime)
 {
 const int PRIMECOUNT = 28;
 static const size_t primeList[PRIMECOUNT] =
      {
 53ul, 97ul, 193ul, 389ul, 769ul,
 1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
 49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
 1572869ul, 3145739ul, 6291469ul, 12582917ul, 
25165843ul,
 50331653ul, 100663319ul, 201326611ul, 402653189ul, 
805306457ul,
 1610612741ul, 3221225473ul, 4294967291ul
      };
 size_t i = 0;
 for (; i < PRIMECOUNT; ++i)
 {
      if (primeList[i] > prime)
       return primeList[i];
 }

 return primeList[i];
 
 }

 只需要在扩容的时候把素数表中的值给newsize即可。下面我们就开始封装unordered系列了,这里很多地方都和红黑树的封装一样,所以我们就不讲的那么细了:

#include "HashTable6.h"

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

SetKeyOfT的作用是:如果HashNode中存储的是k模型那么就返回key,如果存储的是KV模型就返回pair中的first,而为什么调用哈希表要传两个相同的K是因为第一个参数K是用于Find,eraser接口使用的,而第二个K是HashNode中实际存储的类型,因为节点中有可能存储的是key,也有可能是pair。下面我们把map简单实现一下:

#include "HashTable6.h"

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

可以看到我们MapKeyOfT的仿函数是取pair中的first,下一步我们在哈希表中加入KeyOfT参数:

 然后我们修改一下节点中的kv键值对,用一个T类型来表示set中的key或者map中的pair:

template <class T>
	struct HashNode
	{
		HashNode<T>* _next;
		T _data;
		HashNode(const T& data)
			:_data(data)
			, _next(nullptr)
		{

		}
	};

然后相应的哈希表中用到节点的都需要修改:

bool insert(const T& data)
		{
			Hash hash;
			KeyOfT kot;
			if (Find(kot(data)))
			{
				return false;
			}
			if (_n == _tables.size())
			{
				//扩容
				size_t newsize = _tables.size() == 0 ? 10 : 2 * _tables.size();
				vector<Node*> newtable(newsize, nullptr);
				for (auto& cur : _tables)
				{
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = hash(kot(cur->_data)) % newtable.size();
						cur->_next = newtable[hashi];
						newtable[hashi] = cur;
						cur = next;
					}
				}
				_tables.swap(newtable);
			}
			size_t hashi = hash(kot(data)) % _tables.size();
			Node* newnode = new Node(data);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return true;
		}

用KeyOfT这个仿函数可以准确的拿到key值,然后我们用key值经过哈希仿函数转化为可以取模的无符号整形,这样就可以让unordered_set和map用哈希表做底层了,然后我们再把其他接口修改一下:

Node* Find(const K& key)
		{
			Hash hash;
			KeyOfT kot;
			if (_tables.size() == 0)
			{
				return nullptr;
			}
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}

修改到find和eraser接口我们就应该能体会到刚刚在set中多传一个参数的作用了,还记得我们怎么说的吗?第一个参数是给Find等接口使用的,因为这些接口用的参数一定是key。

bool eraser(const K& key)
		{
			Hash hash;
			KeyOfT kot;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					if (prev == nullptr)
					{
						Node* next = cur->_next;
						_tables[hashi] = next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}

下面我们给一组测试用例先跑一下,在这里大家一定要注意,因为模板参数很多一定要写一部分就编译一下,否则模板参数的报错很容易把人搞崩溃。

void _usettest()
	{
		unordered_set<int> ust;
		ust.insert(1);
		ust.insert(2);
		ust.insert(3);
	}

 set的运行没问题,下面我们再试试map:

void test_umap()
	{
		unordered_map<int, int> ump;
		ump.insert(make_pair(1, 1));
		ump.insert(make_pair(2, 2));
		ump.insert(make_pair(3, 3));
		
	}

 经过编译后我们发现没有问题,接下来就进行迭代器的实现了:

那么迭代器该如何实现呢?首先我们都知道要想遍历桶中的节点是一定需要节点的指针的,还需要什么呢?其实还需要一个哈希表的vector或者一整个哈希表,因为当我们要从一个桶++到另一个桶的时候是需要vector的,我们实现的迭代器里面就直接存指针和哈希表好了,大家也可以下去试试存vector,步骤都是一样的,STL源码中存的是哈希表,可以给大家看看:

 下面我们就实现起来:

	//前置声明
	template <class K, class T, class KeyOfT, class Hash>
	class HashTable;
	template <class K, class T, class KeyOfT, class Hash>
	struct HashIterator
	{
		typedef HashNode<T> Node;
		typedef HashTable<K, T, KeyOfT, Hash> HT;
		Node* _node;
		HT* _ht;
		HashIterator(Node* node, HT* ht)
			:_node(node)
			,_ht(ht)
		{

		}
	};

首先我们实现的迭代器有哈希表的指针和节点的指针,由于我们的迭代器实现在哈希表之前,所以我们需要加上前置声明才能正常使用哈希表,要注意的是:哈希表参数中的Hash的缺省参数我们只需要在哈希表的地方给出,在声明的时候不用带上缺省参数,如果有重定义的报错那么一定是你将Hash给上HashFunc的缺省参数了。

 如上图,只需要在哈希表的位置给出缺省参数就可以了,因为这里是定义。我们在初始化迭代器的时候不仅需要节点还需要哈希表的指针,下面我们实现迭代器的主要功能:

struct HashIterator
	{
		typedef HashNode<T> Node;
		typedef HashTable<K, T, KeyOfT, Hash> HT;
		typedef HashIterator<K, T, KeyOfT, Hash> Self;
		Node* _node;
		HT* _ht;
		HashIterator(Node* node, HT* ht)
			:_node(node)
			,_ht(ht)
		{

		}
		T& operator*()
		{
			return _node->_data;
		}
		T* operator->()
		{
			return &_node->_data;
		}
		bool operator!=(const Self& s)
		{
			return _node != s._node;
		}
	};

对于->的返回类型相信不用多说了吧,引用本身就是指针实现的所有返回引用是没问题的。下面实现++,由于unordered系列的迭代器是单向迭代器,所以我们实现个++就可以了:

Self& operator++()
		{
			if (_node->_next)
			{
				_node = _node->_next;
			}
			else
			{
				Hash hash;
				KeyOfT kot;
				size_t hashi = hash(kot(_node->_data)) % _ht->_tables.size();
				++hashi;
				while (hashi < _ht->_tables.size())
				{
					if (_ht->_tables[hashi])
					{
						_node = _ht->_tables[hashi];
						break;
					}
					++hashi;
				}
				if (hashi == _ht->_tables.size())
				{
					_node = nullptr;
				}
			}
			return *this;
		}

首先我们要判断迭代器当前节点的next是否为空,如果不为空那么我们++直接走到next的那个节点就可以了,如果next为空就说明我们++后要变成下一个不为空的桶的头结点,所以当next为空时,我们算出当前节点映射出来的哈希桶位置,因为当前的位置已经遍历完了(毕竟刚刚next都为空了),所以我们需要让当前位置++变成下一个位置,在这里要说明一下:迭代器的起始位置是哈希表中第一个不为空的桶的头结点 ,所以我们需要判断算出来的位置是否小于总的表长度,进入循环后判断当前位置的桶的头结点是否存在如果存在那么++后的位置就是这个头结点然后break即可。因为有可能在++的过程中后面有连续空的桶所以需要判断如果出循环后发现hashi和表长度一样大,那么就说明表中没有不为空的桶,所以我们让迭代器的节点为空即可。最后返回迭代器。

下面我们实现begin和end:

template <class K, class T, class KeyOfT,class Hash = HashFunc<K>>
	class HashTable
	{
	public:
		typedef HashNode<T> Node;
		typedef HashIterator<K, T, KeyOfT, Hash> iterator;
		iterator begin()
		{
			Node* cur = nullptr;
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i])
				{
					cur = _tables[i];
					break;
				}
			}
			return iterator(cur, this);
		}
		iterator end()
		{
			return iterator(nullptr, this);
		}

因为begin是哈希表中第一个不为空的桶的头结点,所以我们挨个遍历哈希表,找到不为空的桶我们就将头结点给cur,然后返回由cur这个节点构造的迭代器,因为我们的迭代器还有哈希表,而我们就是在哈希表中实现的begin,所以直接返回this指针,this指针就是我们当前的哈希表(注意不是*this,因为我们的迭代器要的就是哈希表的指针,所以不需要解引用),end的实现非常简单,只需要返回空节点构造的迭代器即可,因为哈希表的结束位置的节点就是为空,下面我们将迭代器封装进set和map中:

 还记得typename的作用吗?当我们使用模板的时候编译器会找不到iterator,这个时候需要typename让编译器知道iterator以后会由模板实例化生成,所以不要报错。然后我们运行起来:

void _usettest()
	{
		unordered_set<int> ust;
		ust.insert(1);
		ust.insert(2);
		ust.insert(3);
		unordered_set<int>::iterator it = ust.begin();
		while (it != ust.end())
		{
			cout << *it << " ";
			++it;
		}
	}

运行起来后发现报错了:

 这是因为我们的迭代器无法访问哈希表中的私有成员,所以我们直接用友元函数搞一下:

 上图这样的方法不用声明模板参数,下面我们再用一下传统方法:

 这里报错的原因是没加迭代器的模板参数,我们加一下:

 同样解决问题,下面我们在用一下map的迭代器:

 map的迭代器测试也没问题,下面我们实现一下map的特有功能:[]运算符

要实现[]运算符需要先修改insert插入的返回值,让其返回值变成pair<iterator,bool>想必我们实现过红黑树的封装的同学并不陌生,下面我们就修改一下:

 我们在修改哈希表中的insert前需要先修改一下find接口,因为有了迭代器后find应该返回迭代器才对:

iterator Find(const K& key)
		{
			Hash hash;
			KeyOfT kot;
			if (_tables.size() == 0)
			{
				return end();
			}
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					return iterator(cur, this);
				}
				cur = cur->_next;
			}
			return end();
		}

 当我们找不到的时候返回end()即可,end就是nullptr构造的迭代器,找到了就返回由找到的节点构造的那个迭代器。

pair<iterator,bool> insert(const T& data)
		{
			Hash hash;
			KeyOfT kot;
			auto it = Find(kot(data));
			if (it!=end())
			{
				return make_pair(it, false);
			}
			if (_n == _tables.size())
			{
				//扩容
				size_t newsize = _tables.size() == 0 ? 10 : 2 * _tables.size();
				vector<Node*> newtable(newsize, nullptr);
				for (auto& cur : _tables)
				{
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = hash(kot(cur->_data)) % newtable.size();
						cur->_next = newtable[hashi];
						newtable[hashi] = cur;
						cur = next;
					}
				}
				_tables.swap(newtable);
			}
			size_t hashi = hash(kot(data)) % _tables.size();
			Node* newnode = new Node(data);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return make_pair(iterator(newnode, this), true);
		}

插入的时候因为find接口返回的是迭代器所以我们也用迭代器来接收find的返回值,如果it不等于end()说明就是要插入的值在哈希表找到了,这个时候不在插入了因为我们不插入重复的值,返回it这个迭代器和false即可。最后插入成功返回新节点构造的迭代器和哈希表即可。

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

【】不管怎么样都返回pair的second,在插入的时候由于不知道V给的是什么所以用匿名对象就可以了。下面测试一下:

void test_umap2()
	{
		string arr[] = { "苹果","西瓜","西瓜","香蕉","苹果" };
		unordered_map<string, int> mp;
		for (auto& e : arr)
		{
			mp[e]++;
		}
		for (auto& m : mp)
		{
			cout << m.first << ":" << m.second << endl;
		}
	}

 经过测试没有问题,下面我们要引出另一个问题,如果是自定义类型该如何存入哈希表呢?我们以日期类为例:

	class Date
	{
	
	public:
		Date(int year = 1900, int month = 1, int day = 1)
			: _year(year)
			, _month(month)
			, _day(day)
		{}
		bool operator<(const Date& d)const
		{
			return (_year < d._year) ||
				(_year == d._year && _month < d._month) ||
				(_year == d._year && _month == d._month && _day < d._day);
		}
		bool operator>(const Date& d)const
		{
			return (_year > d._year) ||
				(_year == d._year && _month > d._month) ||
				(_year == d._year && _month == d._month && _day > d._day);
		}
		
		friend ostream& operator<<(ostream& _cout, const Date& d)
		{
			_cout << d._year << "-" << d._month << "-" << d._day;
			return _cout;
		}
	private:
		int _year;
		int _month;
		int _day;
	};

下面是测试代码:

void test_umap3()
	{
		Date d1(2023, 1, 11);
		Date d2(2013, 4, 16);
		Date d3(2022, 6, 20);
		Date d4(2014, 7, 19);
		Date d5(2030, 8, 24);
		Date d6(2020, 1, 17);
		Date a[] = { d1,d2,d3,d4,d5,d6 };
		unordered_map<Date, int> ump;
		for (auto& data : a)
		{
			ump[data]++;
		}
		for (auto& data : ump)
		{
			cout << data.first << ":" << data.second << endl;
		}
	}

 运行时报错了因为日期类无法转化为size_t类型,这也在我们意料之中,我们用库中的哈希表要解决这样的问题实际上是自己提供一个将key转化为可以取模的整形就可以搞定了,所以我们之前实现的Hash是有问题的,因为这个参数应该是给map和set,当有人调用map和set的时候自己传入Hash,所以我们修改一下:

 调用哈希表的时候必须显式的传Hash这个参数,然后修改一下map和set:

 下面我们再修改一下map的:

 修改完成后我们写一个针对日期类的取模仿函数:

struct HashDate
	{
		size_t operator()(const Date& d)
		{
			size_t hash = 0;
			hash += d._year;
			hash *= 31;
			hash += d._month;
			hash *= 31;
			hash += d._day;
			hash *= 31;
			return hash;
		}
	};

写完后我们发现这个仿函数用不了日期类的私有成员,下面将仿函数设为友元:

然后显示传参运行一下:

 

 运行后发生报错,原因是日期类没有支持等于操作符,下面我们实现一下:

bool operator==(const Date& d) const
		{
			return (_year == d._year
				&& _month == d._month
				&& _day == d._day);
		}

 然后我们将<<运算符重载放到外面,因为放在类里面会少一个参数无法直接打印日期类。

 然后这次我们运行一下:

 ok运行成功没问题,下面我们总结一下:

1.红黑树实现的map/set要用自定义类型只需要支持<符号。

2.哈希表实现的unordered_map/set要用自定义类型需要一个支持取模转化为无符号整形的仿函数和支持==运算符。

接下来我们再实现一下const迭代器,还是和红黑树一样,set不管是普通迭代器还是const迭代器都是用哈希表中的const迭代器实现的,map中普通迭代器还是普通迭代器,const迭代器还是const迭代器,下面我们实现一下:

 我们还是和以前一样,加入ref和ptr这两个模板参数:

 以上修改完成后我们实现一下const迭代器:

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

然后我们在修改一下set中的迭代器:

public:
		typedef typename HashBucket::HashTable<K, K, SetKeyOfT,
			Hash>::const_iterator iterator;
		typedef typename HashBucket::HashTable<K, K, SetKeyOfT,
			Hash>::const_iterator const_iterator;
		iterator begin()
		{
			return _ht.begin();
		}
		iterator end()
		{
			return _ht.end();
		}
		const_iterator begin() const
		{
			return _ht.begin();
		}
		const_iterator end() const
		{
			return _ht.end();
		}

然后我们运行一下set:

 这里报的错与红黑树那部分一样,因为我们已经将普通迭代器用const迭代器实现了,所以begin()中返回的普通迭代器和begin的返回类型无法转换了,因为begin的返回类型是经过typedef的const迭代器,而返回值是调用哈希表中的普通迭代器,无法从普通迭代器转化为const迭代器,所以我们像之前一样直接增加一个支持将普通迭代器转化为const迭代器的构造函数:

 首先我们typedef一个普通迭代器,这一定是普通迭代器。因为只有传Ref和Ptr的时候如果传参是const类型就会转化为const迭代器,而我们用的T&,T*是不会转化的。

HashIterator(const iterator& it)
			:_node(it._node)
			,_ht(it._ht)
		{

		}

 这次就解决刚刚的问题了,我们再实现一下map的const迭代器:

public:
		typedef typename HashBucket::HashTable<K, pair<const K, V>,
			MapKeyOfT,Hash>::iterator iterator;
		typedef typename HashBucket::HashTable<K, pair<const K, V>,
			MapKeyOfT, Hash>::const_iterator const_iterator;
		iterator begin()
		{
			return _ht.begin();
		}
		iterator end()
		{
			return _ht.end();
		}
		const_iterator begin() const
		{
			return _ht.begin();
		}
		const_iterator end() const
		{
			return _ht.end();
		}

 运行起来并没有问题,以上就是我们哈希表的封装。


总结

哈希表的封装是比红黑树复杂的相信大家看出来了,不过只要大家在红黑树的封装那边没有问题,那么哈希表的封装也不会很困难的,如果有不懂的完全可以查一下源码或者看上一篇红黑树封装的文章,那篇文章将源码的实现都列出来了。

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

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

相关文章

Java使用zxing.jar生成二维码

由于时代科学的进步&#xff0c;二维码已经和我们的生活密不可分&#xff0c;在开发过程中往往会涉及到和二维码相关的开发&#xff0c;今天这篇文章就教会大家如何使用zxing.jar包生成二维码 下面这个就是百度上面自带的一个生成二维码的功能&#xff0c;那他是怎么实现这个功…

计算机组成原理与体系结构概述

目录 一、计算机的发展 二、计算机的硬件系统 三、硬件的工作原理 四、计算机系统的层次结构 五、计算机的性能指标 一、计算机的发展 第一代计算机&#xff1a;电子管计算机 第一台电子计算机&#xff1a;ENIAC&#xff08;1946&#xff09; 设计目的&#xff1a;计算导弹…

平板触控笔哪种好?主动式电容笔推荐

现在市面上的电容笔分为主动式和被动式电容笔&#xff0c;很多小伙伴都分不清主动式和被动式电容笔的区别。今天给大家介绍一下这两款电容笔的区别。给大家分享几款好用的平替电容笔。 一、主动式电容笔和被动式电容笔的区别&#xff1a; 1.主动式电容笔&#xff1a; 主动式电…

数据结构与算法(九)

红黑树复习 图 图&#xff0c;是一种数据结构 集合只有同属于一个集合&#xff1b;线性结构存在一对一的关系&#xff0c;树形结构一对多的关系&#xff0c;图形结构&#xff0c;多对多的关系。 微信中&#xff1a;许多的用户组成了一个多对多的朋友关系网&#xff0c;这个关…

【C语言】变量

&#x1f6a9; WRITE IN FRONT &#x1f6a9; &#x1f50e; 介绍&#xff1a;"謓泽"正在路上朝着"攻城狮"方向"前进四" &#x1f50e;&#x1f3c5; 荣誉&#xff1a;2021|2022年度博客之星物联网与嵌入式开发TOP5|TOP4、2021|2022博客之星T…

【机器学习】分类问题和逻辑(Logistic)回归算法详解

在阅读本文前&#xff0c;请确保你已经掌握代价函数、假设函数等常用机器学习术语&#xff0c;最好已经学习线性回归算法&#xff0c;前情提要可参考https://blog.csdn.net/weixin_45434953/article/details/130593910 分类问题是十分广泛的一个问题&#xff0c;其代表问题是&…

Android studio 环境安装

1. Java JDK安装 https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe 下载jdk-17 并安装 安装完成后设置环境变量 #新增环境变量JAVA_HOME C:\Program Files\Java\jdk-17#Path 环境变量添加 %JAVA_HOME%\bin %JAVA_HOME%\jdk\bin#新增环境变量CLASSPAT…

HEVC量化编码介绍

介绍 ● 视频编码中&#xff0c;残差信号经过DCT&#xff0c;变换系数具有较大动态范围&#xff0c;因此对变换系数量化可以有效减小信号取值空间&#xff0c;获得更好的压缩效果&#xff1b; ● 多对一映射机制&#xff0c;所以不可避免的引入失真&#xff0c;这是视频编码中…

Spring(三)对bean的详解

一、引入外部属性文件 首先我们将依赖进行导入&#xff1a; <!--MySQL驱动--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.22</version></dependency><!--数据…

idea连接Linux服务器

一、 介绍 配置idea的ssh会话和sftp可以实现对linux远程服务器的访问和文件上传下载&#xff0c;是替代Xshell的理想方式。这样我们就能在idea里面编写文件并轻松的将文件上传到linux服务器中。而且还能远程编辑linux服务器上的文件。掌握并熟练使用&#xff0c;能够大大提高我…

烂怂if-else代码优化方案 | 京东云技术团队

0.问题概述 代码可读性是衡量代码质量的重要标准&#xff0c;可读性也是可维护性、可扩展性的保证&#xff0c;因为代码是连接程序员和机器的中间桥梁&#xff0c;要对双边友好。Quora 上有一个帖子&#xff1a; “What are some of the most basic things every programmer s…

日本医疗保健和健康管理公司【Zerospo】申请纳斯达克IPO上市

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 猛兽财经获悉&#xff0c;来自日本的医疗保健和健康管理公司【Zerospo】&#xff0c;近期已向美国证券交易委员会&#xff08;SEC&#xff09;提交招股书&#xff0c;申请在纳斯达克IPO上市&#xff0c;股票代码为&#xff…

感谢海洋一所陈老师用Pospac MMS解算pospac数据及GNSS验潮

非常感谢海洋一所陈老师 帮忙用Pospac MMS解算博主的pospa从数据。解算的结果txt文件大小有2个G&#xff0c;令人非常吃惊&#xff0c;因为原始数据的时长不到1天&#xff0c;打开文件才知道每行位置数据的间隔时间是5ms&#xff0c;5ms正是惯导数据的采样频率。 用抽稀软件按…

短视频矩阵系统源码-开源开发php语言搭建

短视频矩阵系统源码---------- php源码是什么&#xff1f; PHP源码指的就是PHP源代码&#xff0c;源代码是用特定编程语言编写的人类可读文本&#xff0c;源代码的目标是为可以转换为机器语言的计算机设置准确的规则和规范。因此&#xff0c;源代码是程序和网站的基础。 PHP…

【数据结构】插入排序详细图解(一看就懂)

&#x1f4af; 博客内容&#xff1a;【数据结构】插入排序详细图解&#xff08;一看就懂&#xff09; &#x1f600; 作  者&#xff1a;陈大大陈 &#x1f989;所属专栏&#xff1a;数据结构笔记 &#x1f680; 个人简介&#xff1a;一个正在努力学技术的准前端&#xff0c;…

ARM-FS6818-点亮LED灯

点亮LED灯 1.开发板介绍 2.cpu控制硬件原理 六大指令里边&#xff0c;只有内存访问指令能访问cpu之外的内容。那cpu如何控制硬件&#xff1f; *load/store指令-->操作4G内存 任何一个芯片都有一个地址映射表。告诉地址空间是如何映射的&#xff0c;便于我们找到对应的硬件地…

ChatGPT3.5-4资源汇总,直连无梯子

什么是ChatGPT? ChatGPT&#xff0c;全称&#xff1a;聊天生成预训练转换器&#xff08;英语&#xff1a;Chat Generative Pre-trained Transformer&#xff09;&#xff0c;是OpenAI开发的人工智能聊天机器人程序&#xff0c;于2022年11月推出。该程序使用基于GPT-3.5、GPT-4…

limou的C语言学习路径

0.前言 你好这里是limou3434的一篇个人博文&#xff0c;感兴趣的话您可以到我的CSDN博客上看看&#xff0c;下面我将以前学习了大概7个月的C语言学习总结给您做一个集合&#xff0c;希望能够帮助到您。 1.C语言大略 学习C前的一些基础知识 这篇文章简单过一下C语言的基础&a…

U盘移动硬盘变本地硬盘怎么办 ,移动硬盘变本地硬盘的恢复方法

这是分区逻辑损坏后最常见的表现。U盘移动硬盘变本地硬盘怎么办 &#xff0c;移动硬盘变本地硬盘的恢复方法有些用人到这种情况后首先会尝试使用Windows系统自带的硬盘修复工具chk命令进行修复&#xff0c;不过&#xff0c;这样操作并不能解决问题&#xff0c;往往会造成更严重…

UFS 2 -UFS架构简介2

UFS 2 -UFS架构简介2 1 UFS架构简介1.1 System Boot and Enumeration1.2 UFS Interconnect (UIC) Layer1.2.1 UFS Physical Layer Signals1.2.2 MIPI UniPro1.2.3 MIPI UniPro Related Attributes 1.3 UFS Transport Protocol (UTP) Layer1.3.1 Architectural Model1.3.1.1 Cli…