C++ | 哈希 | 开散列结构的模拟实现 | 迭代器和泛型的封装

开散列也称链地址法,与闭散列不同,开散列解决哈希冲突的方式是:将冲突的数据用链表维护起来,或者说开散列中不存在哈希冲突,没有了闭散列那样的“你占用我的空间,我占用他的空间”的情况,反而是用一个单链表维护发生冲突的数据,开散列还有一个形象的名字:哈希桶,单链表维护的一串数据是具有相同key值的键值对,也是桶的一块木板。当然哈希桶也有弊端,可能会发生哈希桶的某一块木板过长,或者所有的木板太长,我们知道单链表(木板)越长查找效率越低,所以哈希桶也需要维护负载因子,使负载因子处于一个均衡的状态,从而使哈希桶的查找效率处于一个稳定的状态。当然了在极端情况下,我们可以用红黑树代替单链表,使查找相同哈希值数据的时间复杂度为O(logN)

哈希桶的整体结构交代

首先哈希桶的本质是一个数组,一个存储节点地址的数组,可以认为数组的每个元素是一个单链表的头指针,其次同样重要的是需要维护的负载因子,所以至少要有一个变量记录哈希桶中的节点数量。接着的问题是哈希桶中的数组存储的是什么类型的变量?刚才说了数组的每个成员都是单链表的头指针,所以数组成员的类型是一个指针类型,指针指向的是什么类型?答案是一个节点类型,拥有指针域和数据域,数据域存储一个泛型,指针域存储下一个节点的地址。至此我们把梳理的结构声明出来

template <class T>
struct HashBucketData
{
	// 节点的数据域和指针域
	T _data;
	HashBucketData* _next;
};

template <class K, class Tclass KeyOfT>
class HashBucket
{
	typedef HashBucketData<T> Data;
private:
	vector<Data*> _bucket; // 存储指针的数组
	size_t _n;             // 用来维护负载因子的变量
};

解释一下它们的模板参数,节点的模板参数T,表示一个泛型,因为哈希桶作为unordered_set和unordered_map的底层结构,存储的可能是一个键值对还可能只是一个key值,为了模板的通用性,这里使用T接收上层结构的类型,并且需要上层提供仿函数以提取出泛型T中的key值。所以HashBucket有一个KeyOfT的模板参数,至于为什么还需要一个模板参数K接收key的类型,这是因为一些成员函数需要使用K作为形参的类型

实现了整体结构,接下来就是容器的操作接口,增删查改

插入函数需要接收泛型T类型的数据,将其插入然后返回bool值表示插入是否成功
删除函数只需要接收一个key值,根据key值查找对应的泛型是否存在,最后返回fool表示删除是否成功
查找函数也是只要接收一个key值,根据key值查找对应的泛型是否存在,最后返回的是一个指向该泛型的迭代器
修改函数可以不用特地的设置接口,用户通过接收查找函数返回的迭代器就能修改容器的值

template <class K, class T, class KeyOfT, class HashTrans>
class HashBucket
{
	typedef HashBucketData<T> Data;
public:
	bool Insert(const T& data);  // 增
	bool Erase(const K& key);	 // 删
	iterator Find(const K& key); // 查

private:
	vector<Data*> _bucket; // 存储指针的数组
	size_t _n = 0;         // 用来维护负载因子的变量      
};

当用户传入一个泛型值,我们需要用这个泛型值构造一个节点,将其存储到节点的数据域并将其指针域置空,构造节点就需要调用HashBucketData类的构造函数,注意需要调用有形参的构造函数,将数据传入节点。无参的构造函数是针对vector的resize写的,数组需要扩容时,会调用数组成员的默认构造函数,虽然不知道指针类型的默认构造是否会初始化为空指针,但我们还是显式的写一下,因为指针是否为空是该节点是否指向了下一个节点的表征,就算插入节点时我们会将指针置空,但还是严谨一些好

template <class T>
struct HashBucketData
{
	// 节点的数据域和指针域
	T _data;
	HashBucketData* _next;

	HashBucketData(T data)
		:_data(data)
		,_next(nullptr)
	{}

	HashBucketData()
		:_next(nullptr)
	{}
};

至于HashBucket需不需要构造函数与析构?答案是不需要构造,但需要析构函数,解释一下为什么:HashBucket不写构造函数,编译器会生成一个默认构造函数,对于其vector成员,因为它是自定义类型,所以默认构造函数会调用它的默认构造函数,因为它又是STL中的容器,我们不用当心它的默认构造是否有问题,对于内置类型变量_n,我们给了它初始值0,所以编译器生成的默认构造会用0初始化这个变量。总结一下,因为vector的默认构造函数已经写好了,内置类型的默认值我们也给了,所以用编译器生成的默认构造函数不会有问题

那为什么要写析构函数呢?对于自定义类型成员_bucket,默认析构函数会调用_bucket的析构函数,对于内置类型_n默认构造函数不会管他,因为出了作用域其空间随着栈帧自动释放。但是_bucket的析构只会析构自己申请的资源,也就是存储指针的数组资源,但是对于vector的成员Data*,它可没有默认析构函数,因为它是内置类型,资源会自动释放,但是它是一个指向堆区资源的指针啊,存储了节点的数据,它的资源也需要释放啊,编译器可不知道一个指针指向的资源是不是动态开辟的。所以对于指针类型,我们需要根据其指向的资源决定是否要为它实现析构函数。由于Data节点的资源是new出来的,所以我们需要释放指针指向的资源。

但是我们不能在HashBucketData类中实现析构函数,释放自己(delete this),这会导致无限递归析构函数,最终栈溢出(或者二次释放使得程序崩溃)。我们应该要在封装了HashBucketData的HashBucket类中实现对HashBucketData类的对象的资源释放,这个析构函数主要是释放节点的资源,对于存储节点的数组资源就由vector自己释放(调用完自己的析构函数,由于其成员的生命周期也结束了,所以vector的析构函数也会被调用,存储节点的数组资源也会被释放


这些都是类和对象的基础知识点,属于是学起来简单,真正用起来却绕来绕去。这里实现一下HashBucket类的析构,遍历所有单链表的头指针,进而遍历所有单链表,将所有的节点释放即可

template <class K, class T, class KeyOfT, class HashTrans>
HashBucket<K, T, KeyOfT, HashTrans>::~HashBucket()
{
	for (size_t i = 0; i < _bucket.size(); ++i)
	{
		Data* cur = _bucket[i];
		// 当头指针不为空,进入单链表遍历
		while (cur)
		{
			Data* next = cur->_next;
			delete cur;
			cur = next;
		}
		// 释放后记得置空,虽然没什么必要,但是好习惯
		_bucket[i] = nullptr;
	}
}

迭代器的封装与实现

除了上面这些,我们还需要先定义一个迭代器,将节点的地址进行封装,使迭代器解引用后得到节点的数据域引用,并且支持比较,自增++等等功能。先交代迭代器的整体结构:首先迭代需要封装节点的地址,所以这里需要一个Data类型的指针变量,然后因为迭代器需要++,当遍历完一个单链表后,要怎么遍历下一个单链表?所以我们需要封装一下哈希桶的指针,当一个单链表遍历完,通过哈希桶的指针查找下一个单链表的头指针,也就是数组的下一个元素所保存的数据。当然如果单链表没有遍历完,将迭代器指向下一个节点就可以了

下面是迭代器的封装与接口定义,因为哈希桶的迭代器是一个单向迭代器,只支持++操作,并且++也是迭代器中最重要的操作,具体实现就不再阐述,注释已经很详细了

template <class K, class T, class KeyOfT, class HashTrans>
class __HashIterator
{
	typedef HashBucketData<T> Data;
	typedef HashBucket<K, T, KeyOfT, HashTrans> Bucket;
	typedef __HashIterator<K, T, KeyOfT, HashTrans> Self;
public:
	__HashIterator(Data* pnode, Bucket* pbucket) :_pnode(pnode), _pbucket(pbucket) {}
	Data& operator*() { return _pnode->_data; }
	Data* operator->() { return &(_pnode->_data); }
	Self& operator++(); // 先声明,在类外实现
	bool operator==(const Self& v) { return _pnode == v._pnode };
	bool operator!=(const Self& v) { return _pnode != v._pnode };

private:
	Data* _pnode;			 // 封装的节点的地址
	Bucket* _pbucket;		 // 封装的哈希桶的地址
};

// 迭代器的++迭代操作
template <class K, class T, class KeyOfT, class HashTrans>
typename __HashIterator<K, T, KeyOfT, HashTrans>::Self& __HashIterator<K, T, KeyOfT, HashTrans>::operator++()
{
	Data* cur = _pnode;
	if (cur == nullptr) // 当迭代器指向空时,直接返回
	{
		return *this;
	}

	// 仿函数对象的定义,key取出泛型的key值,trans将key值转换成哈希值
	KeyOfT key;
	HashTrans trans; 
	
	if (cur->_next == nullptr) // 当前迭代器指向链表的最后一个节点,需要使迭代器指向下一个链表的头指针
	{
		//  先计算当前迭代器在数组中的位置
		size_t hashi = trans(key(_pnode->_data)) % _pbucket->_bucket.size();
		// 找一个有节点的位置
		while (++hashi < _pbucket->_bucket.size() && _pbucket->_bucket[hashi] == nullptr){ }
		// 如果遍历完整个桶还没有找到节点,修改迭代器为空再返回
		if (hashi == _pbucket->_bucket.size())
		{
			_pnode = nullptr;
			return *this;
		}
		// 走到这说明找到了,修改迭代器并将其返回
		_pnode = _pbucket->_bucket[hashi];
		return *this;
	}

	// 如果迭代器没有指向链表的尾部,遍历到下一个节点就行
	_pnode = _pnode->_next;
	return *this;
}

查找接口的实现

插入接口需要调用查找接口以避免键值冗余,删除接口也需要调用查找接口先查找数据再删除数据,所以我们先实现查找接口。我们知道哈希桶的本质是一个指针数组,如果数组成员为空,说明该哈希值下没有节点,如果数组成员不为空,那么我们就遍历这个单链表,查找是否有相同key值的节点

template <class K, class T, class KeyOfT, class HashTrans>
typename HashBucket<K, T, KeyOfT, HashTrans>::iterator HashBucket<K, T, KeyOfT, HashTrans>::Find(const K& key)
{
	// 如果桶中没有数据直接返回
	if (_bucket.size() == 0)
	{
		return iterator(nullptr, this);
	}

	// 仿函数对象的定义,将key值转换成整数
	HashTrans trans;

	//  先计算当前迭代器在数组中的位置
	size_t hashi = trans(key) % _bucket.size();
	Data* cur = _bucket[hashi];
	while (cur) // 遍历这个单链表查找key值对应的节点
	{
		if (cur->_data == key)
		{
			// 用当前节点的地址构造一个迭代器
			return iterator(cur, this);
		}
		cur = cur->_next;
	}
	// 遍历完单链表还是没有找到,说明节点不存在,返回空迭代器
	return iterator(nullptr, this);
}

插入接口的实现

插入接口要额外考虑的就是负载因子的维护了,插入前先检查负载因子是否超过了指定的值,如果超过了就需要对数组进行扩容,扩容就涉及到重新映射新的哈希数组的问题了。容易实现的重新映射方式呢,就是再创建一个新的哈希桶,然后遍历旧哈希桶的所有值,将它们重新插入到新的哈希桶中,插入完成将新哈希桶的数组与旧哈希桶的数组进行交换,此时旧的哈希桶旧完成了数组的扩容。其中重新插入节点时,不应该根据data数据重新new一个节点插入,而应该移动原有的节点,能少调用该new就少调用,因为new会消耗程序的性能,直接移动可以提高程序的运行效率。

在检查扩容前还需要检查键值冗余,如果key值存在,函数直接返回不进行插入

template <class K, class T, class KeyOfT, class HashTrans>
bool HashBucket<K, T, KeyOfT, HashTrans>::Insert(const T& data)
{
	// 仿函数对象的定义
	KeyOfT key;
	HashTrans trans;

	iterator it = Find(key(data));
	if (it._pnode) // 出现了键值冗余
	{
		return false;
	}

	if (_bucket.size() == 0 || _n * 10 / _bucket.size() >= 7) // 负载因子的维护,扩容
	{
		// 计算新桶的大小
		size_t new_size = _bucket.size() == 0 ? 10 : 2 * _bucket.size();

		// 新桶的创建与空间开辟
		HashBucket new_bucket;
		new_bucket._bucket.resize(new_size);

		// 遍历旧表完成节点转移
		for (size_t i = 0; i < _bucket.size(); ++i)
		{
			Data* cur = _bucket[i];
			// 当头指针不为空,进入遍历单链表
			while (cur)
			{
				Data* next = cur->_next;
				// 获取新的哈希值
				size_t new_hashi = trans(key(cur->_data)) % new_size;
				// 头插操作
				cur->_next = new_bucket._bucket[new_hashi];
				new_bucket._bucket[new_hashi] = cur;

				// 更新遍历旧表的cur
				cur = next;
			}
			_bucket[i] = nullptr; // 好习惯,但这里不置空是真的会出问题
		}
		// 交换完成后将新旧表交换
		_bucket.swap(new_bucket._bucket);
	}

	// 获取哈希值
	size_t hashi = trans(key(data)) % _bucket.size();
	// 构造节点
	Data* new_node = new Data(data);
	// 将节点头插
	new_node->_next = _bucket[hashi];
	_bucket[hashi] = new_node;
	// 记得负载因子的维护
	++_n;
	return true;
}

删除接口的实现

因为节点的资源是通过new得到的,所以删除节点时我们需要用delete归还资源。那这里可以复用Find函数吗?可以是可以,就是Find函数已经写死,单链表的删除需要记录prev,cur,next三个节点,这样会比较简单,只有cur和next节点也可以,拷贝next的data值到cur中,将next删除就行,只是这样的话还需要考虑要删除的节点cur是最后一个节点的问题(可以拷贝链表的第一个节点的data值到cur上,然后做一个链表的头删,但这又需要计算当前节点的哈希值,因为Find函数只返回一个迭代器,我们只知道要删除节点的地址)。所以记录prev节点的删除较为简单,读者可以自行实现,我这里实现较复杂的拷贝删除


template <class K, class T, class KeyOfT, class HashTrans>
bool HashBucket<K, T, KeyOfT, HashTrans>::Erase(const K& key)
{
	HashTrans trans;

	iterator dele_it = Find(key);
	// 要删除的值不存在,函数直接返回
	if (dele_it._pnode == nullptr)
	{
		return false;
	}

	// 删除节点的保存
	Data* dele_node = dele_it._pnode;
	if (dele_node->_next) // 删除的节点不是链表的尾节点
	{
		// 要删除dele_node节点,转而删除其下一个节点,但在删除之前要维护好链表结构
		// 使当前节点指向下一个节点的下一个节点,并且拷贝下一个节点的值给自己
		// 最后删除下一个节点
		Data* next = dele_node->_next;  // 保存下一个节点
		Data* next_next = next->_next;  // 保存下下个节点
		dele_node->_data = next->_data; // 值的拷贝
		dele_node->_next = next_next;  
		delete next;
	}

	else
	{
		// 删除的节点是链表的尾节点
		// 计算该节点在哈希桶中的哈希值
		size_t hashi = trans(key) % _bucket.size();
		// 要删除的节点是链表的唯一成员,需要修改头指针
		if (_bucket[hashi] == dele_node)
		{
			delete dele_node;
			_bucket[hashi] = nullptr;
		}
		else
		{
			// 不是唯一成员,将第一个节点的值拷贝给自己,然后头删
			dele_node->_data = _bucket[hashi]->_data;
			// 头删
			Data* second = _bucket[hashi]->_next;
			delete _bucket[hashi];
			_bucket[hashi] = second;
		}
	}

	// 记得负载因子的维护
	--_n;
	return true;
}

主要的逻辑已经通过注释解释清楚,这里不再赘述。至于修改接口的实现,用户可以调用查找函数,通过其返回的迭代器修改数据,这里也不再单独实现,至此开散列的大致结构实现完成

hpp文件代码展示

这个模拟实现的开散列结构经过测试是可以使用的,没有严重的bug,这里把所有代码展示出来,需要注意的一点是将迭代器声明为HashBucket的友元类,使迭代器可以访问HashBucket的私有成员

至于HashBucket的后两个模板参数,分别是提取key值的KeyOfT和将其他类型转换成可以取模的整数的HashTrans,两个参数都有默认参数,如果存储的值不是int这样的整数,那么就需要自己写两个仿函数并传入模板参数了

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

template <class T>
struct HashBucketData
{
	// 节点的数据域和指针域
	T _data;
	HashBucketData* _next;

	HashBucketData(T data)
		:_data(data)
		,_next(nullptr)
	{}

	HashBucketData()
		:_next(nullptr)
	{}
};

// 迭代器的声明
template <class K, class T, class KeyOfT, class HashTrans>
struct __HashIterator;

template <class K, class T, class KeyOfT, class HashTrans>
class HashBucket
{
	template <class K, class T, class KeyOfT, class HashTrans>
	friend struct __HashIterator;

	typedef HashBucketData<T> Data;
	typedef __HashIterator<K, T, KeyOfT, HashTrans> iterator;
public:
	pair<iterator, bool> Insert(const T& data);  // 增
	bool Erase(const K& key);	 // 删
	iterator Find(const K& key); // 查
	~HashBucket();               // 析构的声明
	iterator begin();
	iterator end() { return iterator(nullptr, this); }
private:
	vector<Data*> _bucket; // 存储指针的数组
	size_t _n = 0;         // 用来维护负载因子的变量
};

template <class K, class T, class KeyOfT, class HashTrans>
struct __HashIterator
{
	typedef HashBucketData<T> Data;
	typedef HashBucket<K, T, KeyOfT, HashTrans> Bucket;
	typedef __HashIterator<K, T, KeyOfT, HashTrans> Self;

	__HashIterator(Data* pnode, Bucket* pbucket) :_pnode(pnode), _pbucket(pbucket) {}
	T& operator*() { return _pnode->_data; }
	T* operator->() { return &(_pnode->_data); }
	Self& operator++(); // 先声明,在类外实现
	bool operator==(const Self& v) { return _pnode == v._pnode; }
	bool operator!=(const Self& v) { return _pnode != v._pnode; }

	Data* _pnode;			 // 封装的节点的地址
	Bucket* _pbucket;		 // 封装的哈希桶的地址
};


// 迭代器的++迭代操作
template <class K, class T, class KeyOfT, class HashTrans>
typename __HashIterator<K, T, KeyOfT, HashTrans>::Self& __HashIterator<K, T, KeyOfT, HashTrans>::operator++()
{
	Data* cur = _pnode;
	if (cur == nullptr) // 当迭代器指向空时,直接返回
	{
		return *this;
	}

	// 仿函数对象的定义,key取出泛型的key值,trans将key值转换成哈希值
	KeyOfT key;
	HashTrans trans; 
	
	if (cur->_next == nullptr) // 当前迭代器指向链表的最后一个节点,需要使迭代器指向下一个链表的头指针
	{
		//  先计算当前迭代器在数组中的位置
		size_t hashi = trans(key(_pnode->_data)) % _pbucket->_bucket.size();
		// 找一个有节点的位置
		while (++hashi < _pbucket->_bucket.size() && _pbucket->_bucket[hashi] == nullptr){ }
		// 如果遍历完整个桶还没有找到节点,修改迭代器为空再返回
		if (hashi == _pbucket->_bucket.size())
		{
			_pnode = nullptr;
			return *this;
		}
		// 走到这说明找到了头指针,修改迭代器并将其返回
		_pnode = _pbucket->_bucket[hashi];
		return *this;
	}

	// 如果迭代器没有指向链表的尾部,遍历到下一个节点就行
	_pnode = _pnode->_next;
	return *this;
}

template <class K, class T, class KeyOfT, class HashTrans>
HashBucket<K, T, KeyOfT, HashTrans>::~HashBucket()
{
	for (size_t i = 0; i < _bucket.size(); ++i)
	{
		Data* cur = _bucket[i];
		// 当头指针不为空,进入单链表遍历
		while (cur)
		{
			Data* next = cur->_next;
			delete cur;
			cur = next;
		}
		// 释放后记得置空,虽然没什么必要,但是好习惯
		_bucket[i] = nullptr;
	}
}

template <class K, class T, class KeyOfT, class HashTrans>
typename HashBucket<K, T, KeyOfT, HashTrans>::iterator HashBucket<K, T, KeyOfT, HashTrans>::Find(const K& key)
{
	// 如果桶中没有数据直接返回
	if (_bucket.size() == 0)
	{
		return iterator(nullptr, this);
	}

	// 仿函数对象的定义
	KeyOfT get_key;
	HashTrans trans;

	//  先计算当前迭代器在数组中的位置
	size_t hashi = trans(key) % _bucket.size();
	Data* cur = _bucket[hashi];
	while (cur) // 遍历这个单链表查找key值对应的节点
	{
		if (get_key(cur->_data) == key)
		{
			// 用当前节点的地址构造一个迭代器
			return iterator(cur, this);
		}
		cur = cur->_next;
	}
	// 遍历完单链表还是没有找到,说明节点不存在,返回空迭代器
	return iterator(nullptr, this);
}


template <class K, class T, class KeyOfT, class HashTrans>
pair<typename HashBucket<K, T, KeyOfT, HashTrans>::iterator, bool> HashBucket<K, T, KeyOfT, HashTrans>::Insert(const T& data)
{
	// 仿函数对象的定义
	KeyOfT get_key;
	HashTrans trans;

	iterator it = Find(get_key(data));
	if (it._pnode) // 出现了键值冗余
	{
		return make_pair(it, false);
	}

	if (_bucket.size() == 0 || _n * 10 / _bucket.size() >= 7) // 负载因子的维护,扩容
	{
		// 计算新桶的大小
		size_t new_size = _bucket.size() == 0 ? 10 : 2 * _bucket.size();

		// 新桶的创建与空间开辟
		HashBucket new_bucket;
		new_bucket._bucket.resize(new_size);

		// 遍历旧表完成节点转移
		for (size_t i = 0; i < _bucket.size(); ++i)
		{
			Data* cur = _bucket[i];
			// 当头指针不为空,进入遍历单链表
			while (cur)
			{
				Data* next = cur->_next;
				// 获取新的哈希值
				size_t new_hashi = trans(get_key(cur->_data)) % new_size;
				// 头插操作
				cur->_next = new_bucket._bucket[new_hashi];
				new_bucket._bucket[new_hashi] = cur;

				// 更新遍历旧表的cur
				cur = next;
			}
			_bucket[i] = nullptr; // 好习惯,这里不置空是真的会出问题
		}
		// 交换完成后将新旧表交换
		_bucket.swap(new_bucket._bucket);
	}

	// 获取哈希值
	size_t hashi = trans(get_key(data)) % _bucket.size();
	// 构造节点
	Data* new_node = new Data(data);
	// 将节点头插
	new_node->_next = _bucket[hashi];
	_bucket[hashi] = new_node;
	// 记得负载因子的维护
	++_n;
	return make_pair(iterator(new_node, this), true);
}

template <class K, class T, class KeyOfT, class HashTrans>
bool HashBucket<K, T, KeyOfT, HashTrans>::Erase(const K& key)
{
	HashTrans trans;

	iterator dele_it = Find(key);
	// 要删除的值不存在,函数直接返回
	if (dele_it._pnode == nullptr)
	{
		return false;
	}

	// 删除节点的保存
	Data* dele_node = dele_it._pnode;
	if (dele_node->_next) // 删除的节点不是链表的尾节点
	{
		// 要删除dele_node节点,转而删除其下一个节点,但在删除之前要维护好链表结构
		// 使当前节点指向下一个节点的下一个节点,并且拷贝下一个节点的值给自己
		// 最后删除下一个节点
		Data* next = dele_node->_next;  // 保存下一个节点
		Data* next_next = next->_next;  // 保存下下个节点
		dele_node->_data = next->_data; // 值的拷贝
		dele_node->_next = next_next;  
		delete next;
	}

	else
	{
		// 删除的节点是链表的尾节点
		// 计算该节点在哈希桶中的哈希值
		size_t hashi = trans(key) % _bucket.size();
		// 要删除的节点是链表的唯一成员,需要修改头指针
		if (_bucket[hashi] == dele_node)
		{
			delete dele_node;
			_bucket[hashi] = nullptr;
		}
		else
		{
			// 不是唯一成员,将第一个节点的值拷贝给自己,然后头删
			dele_node->_data = _bucket[hashi]->_data;
			// 头删
			Data* second = _bucket[hashi]->_next;
			delete _bucket[hashi];
			_bucket[hashi] = second;
		}
	}

	// 记得负载因子的维护
	--_n;
	return true;

template <class K, class T, class KeyOfT, class HashTrans>
typename HashBucket<K, T, KeyOfT, HashTrans>::iterator HashBucket<K, T, KeyOfT, HashTrans>::begin()
{
	if (_n == 0) // 空桶直接返回
	{
		return iterator(nullptr, this);
	}

	for (size_t i = 0; i < _bucket.size(); ++i)
	{
		if (_bucket[i]) // 当头指针不为空,将其返回
		{
			return iterator(_bucket[i], this);
		}
	}
	return iterator(nullptr, this);
}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值