[数据结构]——哈希表

哈希表

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN ),搜索的效率取决于搜索过程中元素的比较次数。而实际上我们希望理想的搜索效率是O(1),那到底有没有一种数据结构可以实现O(1)的搜索效率呢?

今天我们要介绍的东西叫做哈希表,说到哈希表我们不得不先提一下哈希的概念。哈希也可以叫做散列,他是通过映射的思想将数据存放到一个表中,所以之后我们通过已经映射了的位置查到数据,这样就实现了O(1)的搜索时间复杂度。如果你还是不明白这种思想,别着急,接着往下看。

小练习

找出字符中第一个出现的唯一字符

给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。
在这里插入图片描述
题目很简单,读懂了题目看看我们是怎么利用高效的解法来解决这个问题的:

int firstUniqChar(string s) {
        int arr[26] = {0};//使用一个数组来统计每个字符出现的次数
        for(int i = 0;i<s.size();i++)
        {
            arr[s[i]-'a']++;
        }
        for(int j = 0; j < s.size(); j++)//遍历数组查找第一个出现次数为0的字符
        {
            if(arr[s[j] - 'a'] == 1)
            {
                return j;
            }
        }        
        return -1;
    }

对照着代码,这里实际上我们就应用了哈希映射的思想,每个下标对应一个字母,统计这个字母出现的次数,然后再次遍历原字符串,如果当前这个字符对应的统计次数为1就返回这个字母的下标。
在这里插入图片描述

哈希的构建方法

利用上面的例子我们已经初步了解了哈希基本的映射思想,但是上面的方法对于其它的问题可能就存在非常大的局限性,比如我将题目修改为:查找某个数据流中的某个数字是否存在,如果这个数据流只有俩个数字,一个数字为1,另一个为500万,那么在对这俩个数进行上面那种方式进行映射时就需要开辟500万大小的数组,这明显是不合理的方式。

我们第一种使用的映射思想叫做直接定制法,基于上面提出的问题,第一种方法明显不合理,所以这里我们引出了第二种方法:除留余数法,什么是处理余数法?我们来举个例子

  • hash表size为10,我们需要映射1,13,25三个数据,我们就将1映射到第2个位置,13映射到第4个位置,25映射到第6个位置。
    在这里插入图片描述
    我们这里举了俩个非常重要的映射方式,当然了还有其他方式有兴趣的同学可以下去自己了解一下。下面总计一下可能会遇到的几种哈希函数。

  • 直接定制法:取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先
    知道关键字的分布情况 使用场景:适合查找比较小且连续的情况(如小练习)

  • 除留余数法:设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函
    数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

  • 平方取中法:(了解)假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址 平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

  • 折叠法(了解):折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加
    求和,并按散列表表长,取后几位作为散列地址

  • 随机数法(了解):选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为
    随机数函数。

我们在真正设计哈希表时一般都使用第一二种办较多,后面的方法仅仅作为了解即可

哈希冲突

讲到这里,你可能会觉得哈希表设计起来也很简单么,事实上真的是这个样子么?不如我们再次改变场景来一同探讨。

问题:
现在我们哈希表的大小依旧还是10,我们现在所要映射的数据为5,15,25,这时你会发现,经过num % hash.size()后,这三个数字都要映射到哈希表的第六个位置,这时就发生了所谓的哈希冲突问题。
在这里插入图片描述
为了解决哈希冲突,我们这里引出了闭散列开散列两种方式,接着往下看。

闭散列

闭散列也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
在这里插入图片描述
接下来我们就来一起来探讨这种闭散列如何实现,而当前这种方法叫做闭散列的线性探测法,即key存放到冲突位置中的“下一个” 空位置中去。

闭散列的结构定义

  • 这里hash表直接使用vector来代替,vector中就存放的是HashNode
  • 我们这里直接创建一个K-V的HashTable,HashNode中的state(状态)我们后面再做讨论用处
  • HashTable中成员变量size_t _size是指表中现在的有效数据
template<class K,class V>
class HashNode
{
public:
	std::pair<K, V> _kv;
	State _state;
};

template<class K,class V>
class HashTable
{
private:
	vector<HashNode> _table;
	size_t _size;
};

我们现在来讨论一下为什么HashNode中为什么要有一个_state(状态)变量,问读者一个问题,如果我们现在需要从表中删除某个数据,我们应该怎么做,置空么?怎么将数组的这个位置置空,设置为0?如果我们删除的数据就是0呢。

enum State//使用三种状态分别代表: 1.空 2.删除 3.存在
{
	EMPTY,
	DELETE,
	EXITS,
};

所以我们这里引出了当前节点的状态,如果要删除某个数据时就将状态设置为DELETE,表示假删除。

闭散列的插入

现在到了闭散列的插入:

  • 如果要映射的位置没有数据那么就直接将数据放到相应的位置
  • 如果要映射的位置已经存在数据,那么就要找到这个位置之后第一个为空或者删除状态的位置将数据放入
  • 当查找到表位依旧没找到可插入的位置就需要从表头开始查找
  • 这个表不可能在放满数据后增容,因为表中数据越多那么发生冲突的可能就越大,效率就会越低
bool Insert(const std::pair<K, V>& kv)
	{
		CheckCapacity();//检查是否需要增容

		size_t index = kv.first % _table.size();//每个节点中first取模后的下标就是我们要映射的位置
		while (_table[index]._state == EXITS)//当前位置是删除或者空时表示当前的值就可以插入
		{
			if (_table[index]._kv.first == kv.first)//如果当前的first存在说明插入失败
			{
				return false;
			}

			++index;
			if (index == _table.size())//如果index已经到了表尾那么需要从表头开始搜索
			{
				index = 0;
			}
		}
		_table[index]._kv = kv;
		_table[index]._state = EXITS;
		++_size;		
		return true;
	}

闭散列的扩容

闭散列的扩容非常关键,因为闭散列的扩容时机决定了当前这张表查找效率是否高效,所以哈希表会引入一个叫做负载因子的概念,负载因子计算方式:有效数据个数/哈希表大小,负载因子最大为1表示表满。

当然了负载因子需要给一个合适的数字,因为太大会导致哈希冲突几率大大增加,太小又会导致空间浪费严重。这里我们负载因子给了7,是一个相对比较合适的数字。

void CheckCapacity()
	{
		if (_table.size() == 0 || (_size * 10) / _table.size() == 7)//如果表大小为0或者负载因子为7时增容
		{
			size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;

			HashTable<K, V> newht(newsize);//创建一个新表让新表帮助我们完成新的构建操作
			for (int i = 0; i < _table.size(); i++)
			{
				if (_table[i]._state == EXITS)
				{
					newht.Insert(_table[i]._kv);
				}
			}
			_table.swap(newht._table);
		}
	}

注意这里增容后创建一个新表帮助我们完成插入操作,最后一交换即可。

闭散列的查找

这里没什么好说的,注意假删除的数据实际上存在,但是他代表的已经不是有效数据

HashNode* Find(const K& key)//返回一个表节点的指针
	{
		size_t index = key % _table.size();
		while (_table[index]._state != EMPTY)
		{ 
			if (_table[index]._kv.first == key 
			&& _table[index]._state == EXITS)//这里注意被删除的数据也可能被找到
			{
				return &_table[index];
			}

			++index;
			if (index == _table.size())
			{
				index = 0;
			}
		}
		return nullptr;
	}

闭散列的删除

bool Erase(const K& key)
	{
		HashNode* node = Find(key);
		if (node == nullptr)
		{
			return false;
		}
		else
		{
			node->_state = DELETE;
			--_size;
		}
	}

以上就是闭散列中的线性探测法简单的增删查改,我们不再进一步完善,因为实际上我们的hash表不会使用这种方法(笔者第一次看到这个办法一脸懵逼,感觉这样的效率和数组顺序查找差不了多少啊),我们接着介绍效率更好的办法。

二次探测:

接下来有有人提出了一种叫二次探测的闭散列,这里的二次指的不是探测两次,而是在已经冲突的情况下将这个冲突的数据放到index+pow(n,2)这里的n为(0,1,2…),为什么要有这种办法?

这里已经插入了如图下的数据,但是如果要新插入8时我们发现本属于自己的位置却被其他数字占了,所以使用二次探测能够在一定程度上解决数据堆积的问题。
在这里插入图片描述

小结

其实从本质上闭散列的线性探测与二次探测都没有很有效的解决哈希冲突的问题,在哈希冲突较多的情况下效率肯定不会太理想,并且闭散列在空间上也没有将资源充分的利用,为了解决这些问题,我们接下来要说的就是开散列,那么开散列又是怎么样的呢?一起往下看。

开散列

开散列法又叫链地址法(拉链法,哈希桶),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中(如下图所示)
在这里插入图片描述
此时我们发现哈希冲突的问题得到了很好的解决,并且从空间的利用率上来说已经得到了大大的提高,此时负载可以在等于1时在增容,但是也不能超过1,因为在极端情况下所有的数据都存在一个单链中,此时查找的效率就会退化为单链表的查找效率。我们接下来实现的HashTable就可以供stl中的unordered_map/set使用。

开散列的基本框架

因为这里已经变为了链式结构,所以HashNode中存储的是下一个哈希节点的指针。

template<class V>
struct HashNode
{
	V _Value;
	HashNode<V>* _next;
	HashNode(const V& v)
		:_Value(v)
		, _next(nullptr)
	{}
};

template<class K, class V, class KeyOfValue, class HashFunc>
class HashTable
{
	typedef HashNode<V> Node;
private:
	vector<Node*> _table;
	size_t _size;
};

这里来详细讲一下为什么HashTable需要4个模板参数:

  • class K:很容易理解,K就是用来映射取模的key
  • class V:V有可能是K,也可能是pair<K,V>类型的键值对,这里不懂的小伙伴我把之前封装map和set的图拿过来这里原理相同
  • class KeyOfValue:这里是取K值的仿函数,如果是pair<K,V>类型就需要返回K
  • class HashFunc:这个模板参数非常重要,他也是一个仿函数,他的作用是如果遇到无法取模的类型(string…)帮助我们转化成数字
    在这里插入图片描述

获取哈希映射的下标

这里hf就是帮助我们将字符串转化为数字的仿函数,由上一层结构传递

size_t GetIndex(const K& key, size_t size)
	{
		HashFunc hf;
		return hf(key) % size;
	}

开散列的插入

  • 找到当前数据需要映射的位置,对当前的位置直接进行头插
    在这里插入图片描述
pair<iterator,bool> Insert(const V& v)//为了可以封装unordered_map/set返回pair<K,V>类型
	{
		CheckCapacity();
		KeyOfValue kov;
		size_t index = GetIndex(KeyOfValue()(v), _table.size());//kov获取不同类型的key值
		Node* cur = _table[index];
		while (cur)
		{
			if (kov(cur->_Value) == kov(v))//表示表中已经存在了此数据
				return make_pair(iterator(cur,this),false);
			cur = cur->_next;
		}

		Node* newnode = new Node(v);//此处进行头插,相当于对数组的赋值
		newnode->_next = _table[index];
		_table[index] = newnode;
		++_size;

		return make_pair(iterator(newnode, this), true);//注意这里构造的迭代器需要传this
	}

开散列的增容

  • 对于开散列来说,负载因子控制在1就是合适的,所以当_size == _table.size()时进行增容
  • 这里我们不在像闭散列一样创建新的HashTable,而是创建一个vector<Node*> _newht,因为我们旧节点可以拿过来直接进行插入,所以没必要再进行拷贝从而浪费时间
void CheckCapacity()
	{
		if (_size == _table.size())
		{
			size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;
			vector<Node*> _newht;
			_newht.resize(newsize);
			for (int i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				while (cur)
				{
					Node* next = cur->_next;//保存下一个位置
					size_t index = GetIndex(KeyOfValue()(cur->_Value) , _newht.size());
					cur->_next = _newht[index];
					_newht[index] = cur;
					cur = next;
				}
				_table[i] = nullptr;//完毕后将此位置置空
			}
			_table.swap(_newht);
		}
	}	

查和删这里就不在实现了,因为需要被map/set封装,所以我们需要实现哈希表的迭代器。

哈希表的迭代器

我们需要找到第一个不为nullptr的链表开始遍历
在这里插入图片描述
当走到25后我们如何跳到7的位置呢?因为我们的迭代器实际上就是一个个HashNode的指针,所以我们可以拿到当前节点的K值,这样也就是可以计算出我们所在表中的位置,从而找到下一个位置,但是要知道我们当前所在表中的位置我们就需要表的大小,所以我们就需要拿到哈希表的指针,综上所述,struct HTIterator如下

template<class K, class V, class KeyOfValue, class HashFunc>
struct HTIterator
{
	typedef HashNode<V> Node;
	typedef HTIterator<K, V, KeyOfValue, HashFunc> self;
	Node* _node;
	HashTable<K, V, KeyOfValue, HashFunc>* _ht;//需要哈希表指针来寻找迭代器下一个要到达的位置

	HTIterator(Node* node, HashTable<K, V, KeyOfValue, HashFunc>* ht)
		: _node(node)
		, _ht(ht)
	{}

	self operator++()
	{
		if (_node->_next)//直接指向下一给
		{
			_node = _node->_next;
		}
		else
		{
			//找到当前所在表的位置
			size_t index = _ht->GetIndex(KeyOfValue()(_node->_Value), _ht->_table.size());
			++index;

			for (; index < _ht->_table.size(); index++)
			{
				if (_ht->_table[index] != nullptr)
				{
					_node = _ht->_table[index];//寻找表的下一个位置
					break;
				}
			}
			if (index == _ht->_table.size())//如果已经走到结尾,那么直接将迭代器置空
			{
				_node = nullptr;
			}
		}

		return *this;
	}

	V& operator*()
	{
		return _node->_Value;
	}

	V* operator->()
	{
		return &_node->_Value;
	}

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

因为在struct HTIterator中访问了_table,所以我们不得不把struct HTIterator设置为HashTable的友元:

template<class K, class V, class KeyOfValue, class HashFunc>//模板类型的迭代器
	friend struct HTIterator;

在HashTable中实现begin()和end().

iterator begin()
	{
		for (int i = 0; i < _table.size(); i++)
		{
			if (_table[i] != nullptr)
			{
				return iterator(_table[i], this);//this实际上就是哈希表的指针
			}
		}
		return iterator(nullptr, this);//走到这里说明此时这个哈希表为空
	}

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

对哈希效率的进一步优化

我们知道,对素数进行取模能大幅度减少结果冲突的可能性:
15%7 == 1
25%7 == 4
5 %7 == 5

所以我们在对表的大小进行改变时,统一将表大小设置为素数:

static size_t GetNextPrime(size_t value)
{
	// 使用素数表对齐做哈希表的容量,降低哈希冲突
	const int _PrimeSize = 28;
	static const unsigned long _PrimeList[_PrimeSize] =
	{
		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
	};
 
	for (size_t i = 0; i < _PrimeSize; ++i)
	{
		if (_PrimeList[i] > value)
		{
			return _PrimeList[i];
		}
	} 
	return _PrimeList[_PrimeSize - 1];
}

Insert函数中将代码修改为:

size_t newSize = GetNextPrime(_tables.size());

总结

说到这里我们的HashTable就算是告一段落了,我们重在理解哈希表的创建原理和哈希冲突是怎么解决的,我们这里的开散列实际上已经是一种不错的优化方式了,但是解决哈希冲突的终极方法是:我们现在在表中挂载的是单链表,如果极端情况下数据都被累积在同一个链表中,这样搜索效率最糟情况下还是O(N),但是java8之后采用了在表下挂载红黑树的操作(一脸震惊),只要同一个位置挂载了超过8个节点后就会被转化成红黑树,这样即使在最早的情况下搜索时间复杂度也保持在O(logN ),这就大大提高了搜索的效率。这些大佬的操作真的不是一般人可以想到的。

关于哈希表的讲解就到这里,下篇文章我会用本节实现的Hash表去封装出unordered_map/set,本节中有什么错误或者不懂的地方还请读者提出。

附录(哈希表完整代码)

#pragma once
#include<iostream>
#include<vector>
#include<string>
using namespace std;

template<class V>
struct HashNode
{
	V _Value;
	HashNode<V>* _next;
	HashNode(const V& v)
		:_Value(v)
		, _next(nullptr)
	{}
};

template<class K, class V, class KeyOfValue,class HashFunc>//前置声名
class HashTable;

template<class K, class V, class KeyOfValue, class HashFunc>
struct HTIterator
{
	typedef HashNode<V> Node;
	typedef HTIterator<K, V, KeyOfValue, HashFunc> self;
	Node* _node;
	HashTable<K, V, KeyOfValue, HashFunc>* _ht;//需要哈希表指针来寻找迭代器下一个要到达的位置

	HTIterator(Node* node, HashTable<K, V, KeyOfValue, HashFunc>* ht)
		: _node(node)
		, _ht(ht)
	{}

	self operator++()
	{
		if (_node->_next)//直接指向下一给
		{
			_node = _node->_next;
		}
		else
		{
			//size_t index = KeyOfValue()(_node->_Value) % (_ht->_table.size());//找到当前所在表的位置
			size_t index = _ht->GetIndex(KeyOfValue()(_node->_Value), _ht->_table.size());
			++index;

			for (; index < _ht->_table.size(); index++)
			{
				if (_ht->_table[index] != nullptr)
				{
					_node = _ht->_table[index];//寻找表的下一个位置
					break;
				}
			}
			if (index == _ht->_table.size())//如果已经走到结尾,那么直接将迭代器置空
			{
				_node = nullptr;
			}
		}

		return *this;
	}

	V& operator*()
	{
		return _node->_Value;
	}

	V* operator->()
	{
		return &_node->_Value;
	}

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

template<class K, class V, class KeyOfValue, class HashFunc>
class HashTable
{
	typedef HashNode<V> Node;
	template<class K, class V, class KeyOfValue, class HashFunc>//模板类型的迭代器
	friend struct HTIterator;
public:
	typedef HTIterator<K, V, KeyOfValue, HashFunc> iterator;
	HashTable()
		:_size(0)
	{}

	iterator begin()
	{
		for (int i = 0; i < _table.size(); i++)
		{
			if (_table[i] != nullptr)
			{
				return iterator(_table[i], this);//this实际上就是哈希表的指针
			}
		}
		return iterator(nullptr, this);//走到这里说明此时这个哈希表为空
	}

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

	void CheckCapacity()
	{
		if (_size == _table.size())
		{
			size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;
			vector<Node*> _newht;
			_newht.resize(newsize);
			for (int i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				while (cur)
				{
					Node* next = cur->_next;
					//size_t index = KeyOfValue()(cur->_Value) % _newht.size();
					size_t index = GetIndex(KeyOfValue()(cur->_Value) , _newht.size());
					cur->_next = _newht[index];
					_newht[index] = cur;
					cur = next;
				}
				_table[i] = nullptr;
			}
			_table.swap(_newht);
		}
	}	

	pair<iterator,bool> Insert(const V& v)
	{
		CheckCapacity();
		KeyOfValue kov;
		//size_t index = kov(v)%_table.size();
		size_t index = GetIndex(KeyOfValue()(v), _table.size());
		Node* cur = _table[index];
		while (cur)
		{
			if (kov(cur->_Value) == kov(v))//表示表中已经存在了此数据
				return make_pair(iterator(cur,this),false);
			cur = cur->_next;
		}

		Node* newnode = new Node(v);//此处进行头插,相当于对数组的赋值
		newnode->_next = _table[index];
		_table[index] = newnode;
		++_size;

		return make_pair(iterator(newnode, this), true);//注意这里构造的迭代器需要传this
	}

	size_t GetIndex(const K& key, size_t size)
	{
		HashFunc hf;
		return hf(key) % size;
	}
private:
	vector<Node*> _table;
	size_t _size;
};
版权声明:本文为lucky52529原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/lucky52529/article/details/90113465

智能推荐

android问题记录

Error: Cannot fit requested classes in a single dex file (# methods: 80441 > 65536) 解决办法: gradle文件的defaultConfig默认配置里面增加...

ROS机器人Diego 1# 利用人工智能 风格迁移技术拍摄不同画风的视频

风格迁移,就是将一种图片的风格迁移到其他图片上,改变其他图片的风格,很好玩的一个人工自能模型,github上已经有很多实现的方法,本文参考https://github.com/hzy46/fast-neural-style-tensorflow 的算法,利用Diego1#的平台实现实时视频的风格转换,先上两张图看效果: 是不是很酷呢,其实实现方法和上篇博文中的原理是一样的,只是把人工智能的算法包装...

数据分析学习总结笔记17:文本分析入门案例实战

文章目录 1 数据准备 2 分词 3 统计词频 4 词云 5 提取特征 6 用sklearn进行训练 1 数据准备 数据样例如下, 数据总量为7.7万+: 本节通过一个实战的例子来展示文本分析的最简单流程。首先设定因变量为原始数据中的"评分"。自变量是"评价内容",这里根据评价内容提取TF-IDF特征。之后,通过评价内容的特征建模预测下整体评分。 以上只是最...

LeetCode 150. 逆波兰表达式求值

题目描述 根据逆波兰表示法,求表达式的值。 有效的运算符包括 +, -, *, / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。 整数除法只保留整数部分。 给定逆波兰表达式总是有效的。 换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。 示例 1: 输入:[“2”, “1”, “+”, “3&r...

猜你喜欢

并查集原理及应用

并查集 树形的数据结构,每个集合有其代表节点,代表节点相同的元素属于同一集合。 find:通过查找节点的代表节点,判断节点所属集合。 union:合并两集合,小集合合并到大集合,使用大集合的代表节点。 在find的递归过程中,让路过节点的父节点直接赋值为代表节点,节省下次查找时间,如图所示。 计算岛的个数 遍历二维数组,遇到1时就将所相连的1都改为2,看看遇到多少次1,就是岛的数量。改数时使用回溯...

linux nutch1.0安装配置

1,下载nutch1.0 下载地址:http://archive.apache.org/dist/nutch/,下载这个文件nutch-1.0.tar.gz   2,上传到服务器 上传位置:/home/www/,解压nutch-1.0.tar.gz #tar -xvf nutch-1.0.tar.gz 重命名 #mv nutch-1.0 nutch   3,修改配置文...

如何搭建自己的博客?附加美化

如何搭建自己的blog?附加美化 前言: 之前在腾讯云以学生优惠租了一年的服务器,还买了一年的域名,忽然觉得不能闲置着域名,所以搭建了个博客,过程也遇到了很多的问题,望在此阐述,予以他人帮助,祝好~ 准备工作:使用Xshell连接上Linux服务器,我的是centos系统,方便进行操作。使用Xftp连接上Linux服务器,方便传输文件。 安装apache服务器:yum install httpd ...

rabbitmq五种模式详解(含实现代码)

1.简单模式 当生产端发送消息到交换机,交换机根据消息属性发送到队列,消费者监听绑定队列实现消息的接收和消费逻辑编写.简单模式下,强调的一个队列queue只被一个消费者监听消费. 1.1 结构   生产者:生成消息,发送到交换机 交换机:根据消息属性,将消息发送给队列 消费者:监听这个队列,发现消息后,获取消息执行消费逻辑 1.2应用场景 常见的应用场景就是一发,一接的结构 例如: 手机...

AndroidStudio 常用配置

1. 设置主题&左侧导航栏字体 AndroidStudio->Preferences(下同) 2. 设置字体大小 3. 取消竖线 间距设置大些 4. 控制台字体大小 5. 修改LogCat颜色 LogCat 色值 Verbose BBBBBB Debug 48BB31 Info 0070BB Warn BBBB23 Error FF0006 Assert 8F0005 5. 修改变量...