C++数据结构之哈希算法详解

目录
  • 1.哈希映射
    • 1.1哈希的概念
    • 1.2哈希冲突
    • 1.3哈希函数
  • 2.解决哈希冲突
    • 2.1闭散列法
  • 3代码实现
    • 3.1状态
    • 3.2创建哈希节点类
    • 3.3数据插入
    • 3.4查找与删除
    • 3.5仿函数
  • 4.开散列哈希桶
    • 4.1概念
    • 4.2仿函数
    • 4.3哈希桶结点构建
    • 4.4哈希桶的查找和删除
    • 4.5哈希桶的插入

1.哈希映射

1.1哈希的概念

在顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN),搜索的效率决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向该结构中:

  • 插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
  • 搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)。

unordered_map和unordered_set的底层就是哈希来实现的,它是无序的。

Hash(key)=key%capacity。

聪明的小伙伴已经找到矛盾了,如果说再添加一个数据5,那他存那?

1.2哈希冲突

对于两个数据元素的关键字Ki和Kj(i != j),有 Ki!=Kj,但有:Hash(Ki) == Hash(Kj),即:不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”

所以这种方式建立起来的映射就十分的不合理,所以我们要改进。

哈希设计的原则:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间。
  • 哈希函数计算出来的地址能均匀分布在整个空间中。
  • 哈希函数应比较简单。

1.3哈希函数

1.31直接定值法

取关键字的某个线性函数为散列地址:Hash(key)= A*key + B

优点:简单,速度快,节省空间,查找key O(1)的时间复杂度

缺点:当数据范围大时会浪费空间,不能处理浮点数,字符串数据

使用场景:适用于整数,数据范围比较集中

例如计数排序,统计字符串中出现的用26个英文字符统计,给数组分配26个空间,遍历到的字符是谁,就把相应的元素值++

1.32除留余数法 

把数据映射到有限的空间里面。设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将key转换成哈希地址。

哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突。

看1.1节的例子

解决哈希冲突最常用的方法是闭散列和开散列

2.解决哈希冲突

2.1闭散列法

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。

但是咋去找下一个空位置才是最关键的?

2.11线性探测

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

插入:通过哈希函数获取待插入元素在哈希表中的位置。如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。

删除:采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,否则会影响其他元素的搜索。比如删除元素1,如果直接删除掉,11查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。

给每个位置一个标记,用空、存在、删除3种状态来区分。

  • 负载因子 = 存储的有效数据个数/空间的大小 。
  • 负载因子越大,冲突的概率越高,增删查改效率越低。
  • 负载因子越小,冲突的概率越低,增删查改的效率越高,但是空间利用率低,浪费多。
  • 负载因子 <1,就能保证发生哈希冲突时一定能找到空位置。

线性探测的优缺点:

  • 线性探测优点:实现非常简单。
  • 线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。

2.12二次探测 

二次探测改进了一些线性探测,但是也就那样,这里我就不给太多画面了。

所谓的二次探测我们可以理解为飞跃式,线性是一个一个找空位置,二次就是跳着找。

方法:hash(key) + i^2

hash(11)=11%10+0*2=1,但是1的位置被占了,所以变成hash(11)+1*2,如果这个位置是空的就放进去,不是的话,i继续加。

3代码实现

3.1状态

状态:这里需要三种状态:空,已占用,已删除

如果只用有/没有来代表状态,那删除一个数据后,这个位置就是空的,那就不会再遍历了,但是它后面还有数据的话就存在问题了,所以我们用已删除这个状态来表示的话,还可以遍历后面的数据。

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

namespace CloseHash
{
	enum State
	{
		EMPTY,   //0 空
		EXIST,   //1  存在
		DELETE,   // 2 已删除
	};
}

3.2创建哈希节点类

template<class K,class V>
	struct HashData
	{
		pair<K, V> _kv;//数据
		State _state = State::EMPTY;//状态  --空
	};
	template<class K, class V>//添加仿函数便于把其他类型的数据转换为整型数据
	class HashTable
	{

	public:
		//相关功能的实现……

	private:
		vector<HashData<K, V>> _table;//哈希表
		size_t _n = 0;//存储哈希表中有效数据的个数
	};

查找:当数据是1时,直接映射到下标1处,此时该位置的状态是EXIST,数据是11时,映射到下标1处,但是已经有1了所以++往后找空位置找到后状态更新为EXIST。

删除:删除1,找11,当删除1后,状态更新为DELETE,查找11时下标发现状态是DELETE时会继续往后移动,然后找到11.

当插入的数据较多,而哈希表较短时,就要考虑到扩容,但是哈希的扩容不简单,因为一扩容,下标就变了,那很多数据的映射后的位置就变了。

现在的11在下标11处,就不在2处了。

所以我们在上面提到了负载因子: 填入表中的元素个数 / 散列表的长度

由于散列表的长度一定,所以负载因子和表中的元素个数成正比。元素个数越多,哈希冲突越大,所以我们一般将负载因子定在0.7~0.8,在代码中我们就定在0.7。

插入时我们再介绍。

3.3数据插入

插入的详细步骤:

去除重复:

  • 插入的值可能 已经存在,所以用用Find先进行查找
  • 找到就返回false,没找到在插入。

空间扩容:

  • 如果表是空的就给10个空间,否则扩大2倍。
  • 建立一个新哈希表,把旧表的值插入到新表

探测找位

  • 如果位置的状态是EXIST,继续往后移
  • 找到空位置后,插入,状态变为存在,_n++。
//查找
	bool Insert(const pair<K, V>& kv)
	{

		size_t hashi = kv.first % _tables.size();    //1.遇到空就停止了
		//线性探测
		while (_tables[hashi]._state == EXIST)
		{
			hashi++;
			hashi %= _tables.size();    //2. 可能出表
		}
		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_n;
		return true;
	}

在1.0版本中我注意两个细节:

hashi为啥要模size而不是capacity?

计算机开辟了20个空间,只存储10个数据,但是只能让计算机在前十个空间存,要不一旦用空的空间,遍历时遇到后就不在往后进行了。所以开的空间一般和数据个数一致。

while循环中为啥要加一步hashi%=_table.size()?

比如我们开了20个空间,下标16以后都存满了但是前面还有位置,但是我们存一个37,那他就一直找位置,直到找到19,然后就出这个哈希表了,所以要让他返回到表上再到下标靠前的位置去找。

接下来就要开辟空间了。但是这个可不简单。

当插入的数据较多,而哈希表较短时,就要考虑到扩容,但是哈希的扩容不简单,因为一扩容,下标就变了,那很多数据的映射后的位置就变了。

现在的11在下标11处,就不在2处了。

所以我们在上面提到了负载因子: 填入表中的元素个数 / 散列表的长度

由于散列表的长度一定,所以负载因子和表中的元素个数成正比。元素个数越多,哈希冲突越大,所以我们一般将负载因子定在0.7~0.8,在代码中我们就定在0.7。

在这里我们就要重新建一个哈希表。

//查找
	bool Insert(const pair<K, V>& kv)
	{
		if (Find(kv.first))     //插入的值已经存在
		{
			return false;
		}
		//扩容
		if (_tables.size() == 0 || 10 * _n / _tables.size() >= 7)  //负载因子
		{
			size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;   //扩容
			HashTable<K, V> newHT;
			newHT._tables.resize(newSize);  //扩容后用一个新表
			//遍历旧表,把旧表每个存在的元素插入newHT
			for (auto& e : _tables)
			{
				if (e._state == EXIST)
				{
					newHT.Insert(e._kv);
				}
			}
			newHT._tables.swap(_tables);//建立映射关系后交换

		}
		size_t hashi = kv.first % _tables.size();    //1.遇到空就停止了
		//线性探测
		while (_tables[hashi]._state == EXIST)
		{
			hashi++;
			hashi %= _tables.size();    //2. 可能出表
		}
		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_n;
		return true;
	}

如果哈希表中已经有插入的值时,我们就要去除冗杂,用find函数。

3.4查找与删除

查找的大致思路和插入的很接近,这里就不在重复了。

//查找
	HashData<K, V>* Find(const K& key)
	{
		if (_tables.size() == 0)
		{
			return nullptr;
		}
		size_t hashi = key % _tables.size();
		while (_tables[hashi]._state != EMPTY)
		{
			if (_tables[hashi]._kv.first == key)
			{
				return &_tables[hashi];   //返回的是地址
			}
			hashi++;
			hashi %= _tables.size();
		}
		return nullptr;
	}
	//删除
	bool Erase(const K& key)
	{
		HashData<K, V>* ret=Find(key);
		if (ret)
		{
			ret->_state = DELETE;
			--_n;
		}
		else
		{
			return false;
		}
	}

删除时,我们直接把要删除的数据状态改成DELETE就行了,甚至内部的数据都不用删除。

3.5仿函数

这里的取模用的都是整数,那如果数据是浮点型?更甚至是字符串怎么搞?所依我们就要用到仿函数来进行类型转换。

//利用仿函数将数据类型转换为整型
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
//模板的特化
template<>
struct HashFunc<string>  //如果是字符串,直接调用特化模板
{
	size_t operator()(const string& key)
	{

		size_t hash = 0;
		for (auto ch : key)
		{
			hash = hash * 131 + ch;//把所有字符的ascii码值累计加起来
		}
		return hash;
	}
};

完整cpp表

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

enum State
{
	EMPTY,   //0 空
	EXIST,   //1  存在
	DELETE,   // 2 已删除
};

template<class K, class V>
struct HashData
{
	pair<K, V> _kv;//数据
	State _state = State::EMPTY;//状态  --空
};
//利用仿函数将数据类型转换为整型
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
//模板的特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)  //字符串直接使用
	{
		size_t hash = 0;
		for (auto ch : key)
		{
			hash = hash * 131 + ch;//把所有字符的ascii码值累计加起来
		}
		return hash;
	}
};
template<class K, class V,class Hash = HashFunc<K>>
class HashTable
{

public:
	//插入
	bool Insert(const pair<K, V>& kv)
	{
		if (Find(kv.first))     //插入的值已经存在
		{
			return false;
		}
		//扩容
		if (_tables.size() == 0 || 10 * _n / _tables.size() >= 7)  //负载因子
		{
			size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;   //扩容
			HashTable<K, V> newHT;
			newHT._tables.resize(newSize);  //扩容后用一个新表
			//遍历旧表,把旧表每个存在的元素插入newHT
			for (auto& e : _tables)
			{
				if (e._state == EXIST)
				{
					newHT.Insert(e._kv);
				}
			}
			newHT._tables.swap(_tables);//建立映射关系后交换

		}
		Hash hf;
		size_t start = hf(kv.first);//取出键值对的key,并且避免了负数的情况,借用仿函数确保是整型数据
		start %= _tables.size();
		size_t hashi = start;
		size_t i = 1;
		//1.遇到空就停止了
		//线性探测
		while (_tables[hashi]._state == EXIST)
		{
			hashi++;
			hashi %= _tables.size();    //2. 可能出表
		}
		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_n;
		return true;
	}
	//查找
	HashData<K, V>* Find(const K& key)
	{
		if (_tables.size() == 0)
		{
			return nullptr;
		}
		Hash hf;
		size_t start = hf(key);//通过仿函数把其它类型数据转为整型数据
		start %= _tables.size();
		size_t hashi = start;
		size_t i = 1;
		while (_tables[hashi]._state != EMPTY)
		{
			if (_tables[hashi]._kv.first == key)
			{
				return &_tables[hashi];   //返回的是地址
			}
			hashi++;
			hashi %= _tables.size();
		}
		return nullptr;
	}
	//删除
	bool Erase(const K& key)
	{
		HashData<K, V>* ret=Find(key);
		if (ret)
		{
			ret->_state = DELETE;
			--_n;
		}
		else
		{
			return false;
		}
	}
private:
	vector<HashData<K, V>> _tables;//哈希表
	size_t _n = 0;//存储哈希表中有效数据的个数
};

void testHash1()
{
	int a[] = { 1,11,4,15,26,7 };
	HashTable<int, int> ht;
	for (auto e : a)
	{
		ht.Insert(make_pair(e, e));
	}
}

4.开散列哈希桶

4.1概念

开散列也叫拉链法:先对所有key用散列函数计算散列地址,把有相同地址的key每个key都作为一个桶,通过单链表链接在哈希表中。

此时的表里面存储一个链表指针,就是把冲突的数据通过链表的形式挂起来。

它的算法公式:hash(key)=key%capacity

这里的插入可以是头插也可以是尾插,插入时是无序的。

也就是说哈希桶的根本是一个指针数组,哈希桶的每一个位置存的都是一个链表指针。

这个指针数组里的每一个元素都是结点指针,并且头插的效率比较高。

4.2仿函数

这次我们先弄模板来将其他类型转换为size_t。

namespace OpenHash
{
	template<class K>
	struct Hash
	{
		size_t operator()(const K& key)
		{
			return key;
		}
	};
	// 特化
	template<>
	struct Hash < string >
	{
		size_t operator()(const string& s)
		{
			// BKDR Hash
			size_t value = 0;
			for (auto ch : s)
			{
				value += ch;
				value *= 131;
			}

			return value;
		}
	};
}

4.3哈希桶结点构建

因为是指针数组,所以结点中的成员变量多了一个指向下一个桶的指针。

//结点类
	template<class K, class V>
	struct HashNode
	{
		pair<K, V> _kv;
		HashNode<K, V>* _next;
		//构造函数
		HashNode(const pair<K, V>& kv)
			:_kv(kv)
			, _next(nullptr)
		{}
	};
	//哈希表的类
	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		//相关功能的实现……
	private:
		//指针数组
		vector<Node*> _tables;
		size_t _n = 0;//记录有效数据的个数
	};

4.4哈希桶的查找和删除

这里的查找\删除操作和上面的如出一辙,但是哈希桶的存储是链表的形式,所以会和链表的相关操作很接近。

//查找
		Node* Find(const K& key)
		{
			//防止后续除0错误
			if (_tables.size() == 0)
			{
				return nullptr;
			}
			//构建仿函数
			HashFunc hf;
			//找到对应的映射下标位置
			size_t hashi = hf(key);
			hashi %= _tables.size();
			Node* cur = _tables[hashi];
			//在此位置的链表中进行遍历查找
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					//找到了
					return cur;
				}
				cur = cur->_next;
			}
			//遍历结束,没有找到,返回nullptr
			return nullptr;
		}
		//删除
		bool Erase(const K& key)
		{
			if (_tables.size() == 0)
			{
				return nullptr;
			}
			size_t hashi = key % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					//1.头删
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else    //中间位置删除
					{
						prev->_next = cur->_next;
					}
					delete cur;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}

4.5哈希桶的插入

去除重复:

  • 插入的值可能 已经存在,所以用用Find先进行查找
  • 找到就返回false,没找到再进行插入。

空间扩容

  • 如果负载因子==1就进行扩容。
  • 建立一个新哈希表,把旧表的值插入到新表。
  • 再把新表交换到旧表那里。

但是在把旧表映射到新表时要释放掉旧表,vector类型会自动调用析构函数,然而存储的数据是Node*类型的,是内置类型,不会自动释放,结果就是哈希表释放了但是表中存的数据没释放,所以我们要手写一个析构函数。

头插操作

  • 根仿函数找到合适映射位置
  • 进行头插操作并更新桶内数据个数。

所以我们首先写一个析构函数:

//析构函数
		~HashTable()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_tables[i] = nullptr;//释放后置空
			}
		}

整体代码:

//插入
		bool Insert(const pair<K, V>& kv)
		{
			//1、去除重复
			if (Find(kv.first))
			{
				return false;
			}
			//2、负载因子 == 1就扩容
			if (_tables.size() == _n)
			{
				size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				vector<Node*> newTable;
				newTable.resize(newSize, nullptr);
				for (size_t i = 0; i < _tables.size(); i++)//遍历旧表
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = hf(cur->_kv.first) % newSize;//确认映射到新表的位置
						//头插
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;
						cur = next;
					}
					_tables[i] = nullptr;
				}
				newTable.swap(_tables);
			}
			//3、头插
			//构建仿函数,把数据类型转为整型,便于后续建立映射关系
			HashFunc hf;
			size_t hashi = hf(kv.first);
			hashi %= _tables.size();
			//头插到对应的桶即可
			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return true;
		}

到此这篇关于C++数据结构之哈希算法详解的文章就介绍到这了,更多相关C++哈希算法内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++实现哈希散列表的示例

    散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构.也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度.这个映射函数叫做散列函数,存放记录的数组叫做散列表. 给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数. 若关键字为k,则其值存放在f(k)的存储位置上.由此,不需比较便可直接取得所查记录

  • C++ 实现哈希表的实例

    C++ 实现哈希表的实例 该散列表的散列函数采用了除法散列函数.乘法散列函数.全域散列函数,每一个槽都是使用有序单向链表实现. 实现代码: LinkNode.h #include<iostream> using namespace std; class Link; class LinkNode { private: int key; LinkNode* next; friend Link; public: LinkNode():key(-1),next(NULL){} LinkNode(int

  • C++获取文件哈希值(hash)和获取torrent(bt种子)磁力链接哈希值

    复制代码 代码如下: // CHash.h : header file #pragma once#include "sha1.h" #define        SIZE_OF_BUFFER         16000 class CHash{// Constructionpublic:    CString SHA1Hash(CString strHashFile);}; 复制代码 代码如下: // CHash.cpp : implementation file//#include

  • C++数据结构哈希表详解

    目录 实现 散列函数 开散列方法 闭散列方法(开地址方法) 删除* 实现 哈希表,即散列表,可以快速地存储和查询记录.理想哈希表的存储和查询时间都是 O(1). 本<资料>中哈希表分以下几部分:散列函数.存储和查找时的元素定位.存储.查找.删除操作因为不常用,所以只给出思想,不给出代码. 根据实际情况,可选择不同的散列方法. 以下代码假设哈希表不会溢出. // N表示哈希表长度,是一个素数,M表示额外空间的大小,empty代表"没有元素". const int N=9997

  • C++ 哈希表的基本用法及说明

    目录 C++ 哈希表基本用法 为什么要用哈希表 遍历 查找 插入 删除 C++ 哈希表基础知识 常见的三种哈希结构 C++ 哈希表基本用法 哈希表是一种很常见的数据结构,我现在平时刷算法题一般使用C++刷(不要问我为什么,懂的都懂).C++关于哈希表有很多数据结构,平时使用的比较多的有unordered_set 跟 unordered_map.其中unordered_map 存储的是键值对. 其实我们在某些情况下可以使用数组构建哈希表(具体是哪些情况的呢,自行搜索).但是数组的大小是受限制的,而

  • C语言数据结构哈希表详解

    /* * 程序名:hash.c,此程序演示哈希表的实现,数据元素单链表带头结点. * */ #include <stdio.h> #include <stdlib.h> #include <string.h> // 哈希表中数据元素的结构体. typedef struct Element { unsigned int key; // 关键字. int value; // 数据元素其它数据项,可以是任意数据类型. // char value[1001]; // 数据元素其

  • Java数据结构之图的路径查找算法详解

    目录 前言 算法详解 实现 API设计 代码实现 前言 在实际生活中,地图是我们经常使用的一种工具,通常我们会用它进行导航,输入一个出发城市,输入一个目的地 城市,就可以把路线规划好,而在规划好的这个路线上,会路过很多中间的城市.这类问题翻译成专业问题就是: 从s顶点到v顶点是否存在一条路径?如果存在,请找出这条路径. 例如在上图上查找顶点0到顶点4的路径用红色标识出来,那么我们可以把该路径表示为 0-2-3-4. 如果对图的前置知识不了解,请查看系列文章: [数据结构与算法]图的基础概念和数据

  • Java数据结构之KMP算法详解以及代码实现

    目录 暴力匹配算法(Brute-Force,BF) 概念和原理 next数组 KMP匹配 KMP全匹配 总结 我们此前学了前缀树Trie的实现原理以及Java代码的实现.Trie树很好,但是它只能基于前缀匹配实现功能.但是如果我们的需求是:一个已知字符串中查找子串,并且子串并不一定符合前缀匹配,那么此时Trie树就无能为力了. 实际上这种字符串匹配的需求,在开发中非常常见,例如判断一个字符串是否包括某些子串,然后进行分别的处理. 暴力匹配算法(Brute-Force,BF) 这是最常见的算法字符

  • 数据结构 栈的操作实例详解

    数据结构 栈的操作实例详解 说明: 往前学习数据结构,想运行一个完整的顺序栈的程序都运行不了,因为书上给的都是一部分一部分的算法,并没有提供一个完整可运行的程序,听了实验课,自己折腾了一下,总算可以写一个比较完整的顺序栈操作的小程序,对于栈也慢慢开始有了感觉.下面我会把整个程序拆开来做说明,只要把这些代码放在一个文件中,用编译器就可以直接编译运行了. 一.实现 1.程序功能 关于栈操作的经典程序,首当要提及进制数转换的问题,利用栈的操作,就可以十分快速地完成数的进制转换. 2.预定义.头文件导入

  • 数据结构 双机调度问题的实例详解

    数据结构 双机调度问题的实例详解 1.问题描述 双机调度问题,又称独立任务最优调度:用两台处理机A和B处理n个作业.设第i个作业交给机器A处理时所需要的时间是a[i],若由机器B来处理,则所需要的时间是b[i].现在要求每个作业只能由一台机器处理,每台机器都不能同时处理两个作业.设计一个动态规划算法,使得这两台机器处理完这n个作业的时间最短(从任何一台机器开工到最后一台机器停工的总的时间). 研究一个实例:n=6, a = {2, 5, 7, 10, 5, 2}, b = {3, 8, 4, 1

  • python中实现k-means聚类算法详解

    算法优缺点: 优点:容易实现 缺点:可能收敛到局部最小值,在大规模数据集上收敛较慢 使用数据类型:数值型数据 算法思想 k-means算法实际上就是通过计算不同样本间的距离来判断他们的相近关系的,相近的就会放到同一个类别中去. 1.首先我们需要选择一个k值,也就是我们希望把数据分成多少类,这里k值的选择对结果的影响很大,Ng的课说的选择方法有两种一种是elbow method,简单的说就是根据聚类的结果和k的函数关系判断k为多少的时候效果最好.另一种则是根据具体的需求确定,比如说进行衬衫尺寸的聚

  • Java 数据结构之时间复杂度与空间复杂度详解

    目录 算法效率 时间复杂度 什么是时间复杂度 推导大 O 阶的方法 算法情况 计算冒泡排序的时间复杂度 计算二分查找的时间复杂度 计算阶乘递归的时间复杂度 计算斐波那契递归的时间复杂度 空间复杂度 计算冒泡排序的空间复杂度 计算斐波那契数列的空间复杂度(非递归) 计算阶乘递归Factorial的时间复杂度 算法效率 在使用当中,算法效率分为两种,一是时间效率(时间复杂度),二是空间效率(空间复杂度).时间复杂度是指程序运行的速度.空间复杂度是指一个算法所需要的额外的空间. 时间复杂度 什么是时间

  • Java数据结构之平衡二叉树的实现详解

    目录 定义 结点结构 查找算法 插入算法 LL 型 RR 型 LR 型 RL 型 插入方法 删除算法 概述 实例分析 代码 完整代码 定义 动机:二叉查找树的操作实践复杂度由树高度决定,所以希望控制树高,左右子树尽可能平衡. 平衡二叉树(AVL树):称一棵二叉查找树为高度平衡树,当且仅当或由单一外结点组成,或由两个子树形 Ta 和 Tb 组成,并且满足: |h(Ta) - h(Tb)| <= 1,其中 h(T) 表示树 T 的高度 Ta 和 Tb 都是高度平衡树 即:每个结点的左子树和右子树的高

  • Java求最小生成树的两种算法详解

    目录 1 最小生成树的概述 2 普里姆算法(Prim) 2.1 原理 2.2 案例分析 3 克鲁斯卡尔算法(Kruskal) 3.1 原理 3.2 案例分析 4 邻接矩阵加权图实现 5 邻接表加权图实现 6 总结 介绍了图的最小生成树的概念,然后介绍了求最小生成树的两种算法:Prim算法和Kruskal算法的原理,最后提供了基于邻接矩阵和邻接链表的图对两种算法的Java实现. 阅读本文需要一定的图的基础,如果对于图不是太明白的可以看看这篇文章:Java数据结构之图的原理与实现. 1 最小生成树的

随机推荐