STL源码剖析(五)——关联式容器

5 篇文章 2 订阅

1、概览

所谓关联式容器,观念上类似于关联式数据库,每笔数据都有一个键值(key)和实值(value),当元素被插入到关联式容器中,容器内部结构(可能是红黑树和哈希表)便依照键值大小,以某种特定规则将这个元素放置在适当位置,关联式容器没有所谓头尾(只有最大元素和最小元素)。

1.1、树

由节点(nodes)和边(edges)组成,最上方的是根节点(root),每个节点具有方向性的边(diercted edges),用来和其他节点相连。无子节点的称为叶节点,如果最多允许存在两个子节点,即所谓二叉树(binary tree)。不同节点如果拥有相同父节点,则彼此为兄弟节点。根节点至任一节点的路径长度,即节点的深度,根节点深度为0。某节点至其最深子节点的路径长度称为该节点的高度。整棵树的高度以根节点的高度代表。

1.2、二叉搜索树

二叉搜索树可以提供对数时间的元素插入和访问。二叉搜索树的节点放置规则是:任何节点的键值一定大于其左子树的每一个节点的键值,并小于其右子树的任何一个节点键值。因此,从根往左一直走为最小,往右一直走为最大。
插入和移除操作较为繁琐,从根节点开始,遇键值大者向左,遇键值小者向右一直到尾端为插入点。在这里插入图片描述
如图,删除节点A分两种情况,若A只有一个子节点,将A的子节点连接至A的父节点,并将A删除。如果A有两个子节点,就以右子树内的最小节点取代A。(右子树的最小点就是从右节点开始一直找左节点)。在这里插入图片描述
在这里插入图片描述

1.3平衡二叉搜索树

因为输入值的不够随机等原因,经过某些插入和删除操作后,二叉搜索树可能失去平衡,造成搜寻效率降低,如图。,在这里插入图片描述
“平衡”的大概意思是:没有任何一个节点过深,不同的平衡条件,造就出不同的效率表现,以及不同的实现复杂度。有数种特殊的结构如AVL-tree,RB-tree,AA-tree都可以实现平衡二叉搜索树,他们都比一般的二叉搜索树复杂,因此,插入和删除平均时间较长,但他们可以避免高度不平衡情况,搜寻时间一般可减少25%。

1.4 AVL-tree

AVL-tree是一个加上了额外平衡条件的二叉搜索树,其平衡条件的建立是为了确保树的深度为O(logn)。直观上最佳平衡条件是每个节点的左右子树有相同高度,但这样过于苛刻,因此AVL-tree要求任何节点的左右子树高度差最多为1。
如图为AVL-tree,插入节点11后,灰色节点违反ATL tree平衡条件,因此,调整最深的节点使树重新平衡。
在这里插入图片描述
调整“插入点至根节点”路径上,平衡状态被破坏之后各节点中最深的那个,使得整棵树重新获得平衡。假设最深的节点是X,由于节点最多有两个子节点,平衡被破坏意味着左右子树高度差为2,因此可分四种情况:
1、插入点位于X的左子节点的左子树——左左。
2、插入点位于X的左子节点的右子树——左右。
3、插入点位于X的右子节点的左子树——右左。
4、插入点位于X的右子节点的右子树——右右。
1、4彼此对称,2、3彼此对称。
1、4称为外侧插入,通过单旋转解决。2、3称为内侧插入,通过双旋转解决。
在这里插入图片描述

1.4.1单旋转

如图,我们希望A提高一成,C下降一成。把K1向上提起,使K2自然下滑,并将B子树挂到K2的左侧,二叉搜索树的规则表明k2>k1,所以k2必须成为新树形k1的右子节点,B子树的所有节点的键值必须在k1和k2之间,所以新树形B必须落在K2的左侧。
在这里插入图片描述

1.4.2双旋转

如图左侧为内侧插入造成的不平衡状态,单旋转无法解决。第一,我们不能以k3为根节点,其实,我们不能将k3和k1做一次单旋转,因为旋转之后还是不平衡。唯一的可能是以k2为新根节点,这使得k1必须称为k2的左子节点,k3必须称为k2的右子节点。新的树形满足AVL-tree的平衡条件,并且,像单旋转的情况,它恢复了节点插入前的高度。
在这里插入图片描述

1.5、RB-tree

红黑树需要满足以下规则:
1、每个节点不是红色就是黑色
2、根节点是黑色
3、节点为红,子节点必须为黑
4、任一节点至NULL的任何路径,所含黑色节点必须相同在这里插入图片描述

1.6、hashtable

二叉搜索树有对数平均时间的表现,但这样的表现构建在一个假设上,输入数据具有足够的随机性。hashtable在插入、删除、搜寻等操作上也具有“常数平均时间”表现,这种表现是以统计为基础,不需依赖输入元素的随机性。
hashtable可以对任何有名项存取和删除操作,由于操作对象是有名项,所以hash_table被视为一种古典结构(dictionary),这种结构的用意在于提供“常数平均时间”的基本操作,类似于stack和queue那样。
用数组查找可以实现时间复杂度为0(1)的查找,但存在两个问题:
(1)元素范围大时数组范围大,占用内存巨大
(2)不能以字符串得非数字类型作为索引
如何避免使用巨大的array:使用某种映射函数,将大数映射为小数,这样将一个元素映射为一个“大小可接受的索引”称为hash function(散列函数),例如x是任意整数,TableSize是array大小,则X%TableSize会得到一个整数,范围在0~TableSize-1之间。
使用hashfunction会带一个问题:可能有不同的元素映射到相同的位置,因为元素个数大于array的容量,这便是碰撞问题,解决碰撞问题的方法有线性探测、二次探测、开链等。
线性探测:负载系数是指元素个数除以表格大小,负载系数永远在0-1之间,除非采用开链。当hash function计算出某个元素的插入位置,而该位置上的空间不再可用时,最简单的办法就是循序往下一一寻找,知道找到可用空间为止,只要表格足够大,总能找到一个空间,搜索同理。但元素的删除必须采用惰性删除,只标记删除号不尽兴实际操作,实际操作等待表格重新整理时再进行,因为hash table中每一个元素不仅表述自己,也关系其他元素的排列。
线性探测最坏情况巡防整个表格,平均情况巡防一半表格,和常数时间相差甚远。实际情况,除非新的元素落在#4-#7,否则位置#4-#7永远不可能运用,新元素8、9、0、1、2、3中的哪一个都会落在3,3、5、6、7才会落各自位置,平均插入成本的增长幅度远高于负载系数的成长,称为
主集团

二次探测主要解决主集团问题。命名是因为解决碰撞问题的方程F(i) =i^2是个二次方程,如果hashfuction计算的位置是H,我们就依序尝试H+i的平凡,而不是线性探测。
使用这种方法,当表格大小为质数、永远保持负载系数在0.5以下时,可以确定每插入一个新元素需要的探测次数不多于2.
二次探测可以消除主集团,缺可能造成次集团,两个元素讲过hashfuction计算得到的位置相同,插入时的探测的位置也相同,造成某种浪费,复式散列可以解决次集团问题。
开链:每一个表格元素维护一个list,哈希函数分配一个list,在list上执行插入、删除和搜寻,list足够短,速度够快。 在这里插入图片描述

2、set

2.1常用接口

begin()
end()
rebgin()
rend()
lower_bound() //删除一个区间的上界
upper_bound() //删除一个区间的下界
find() //查找
//下面搭配迭代器使用
clear() //删除set容器中的所有元素
empty() //判断set容器是否为空
max_size() set可能包含的元素最大个数
size() //返回set容器的元素个数

2.2实现

#include<iostream>
#include<set>
#include<iterator>
using namspace std;
void print(set<int> s){
	cout<<"正向迭代器" << endl;
	set<int>::iterator it1 = s.begin();
	while(it != s.end()){
		cout << * it << endl;
		++it;
	}
	cout << endl;
}
void testSet()
{
	set<int> s1;
	for(int i = 0; i < 10; i++){
		s1.insert(i * 10);
	}
	//输出pos
	set<int>::iterator pos = s1.find(20);
	if(pos != s1.end())
		s1.erase(pos);
	s1.erase(30);
	//删除15-50之间的数
	set<int>::iterator low = s1.lower_bound(15);
	set<int>::iterator up = s1.uppder_bound(50);
	s1.erase(low, up);
	print(s1);
	cout <<endl;
	if(s1.count(60)) cout << "60存在" << endl;
	s1.clear(); print(s1);
}

3、map

3.1 常用接口

map里面存的是key-value对,其中key起到索引的作用
value表示与索引相关的数据,比如字典就是一个实用map的例子,单词当做key,解释当做value
begin()
end()
count()
empty()
end()
equal_range()
erase()
find()
get_allocator()
insert()
key_comp()
lower_bound()
max_size()
rbegin()
rend()
size()
swap() //交换两个map
upper_bound() //返回键值>给定元素的第一个位置
value_comp() //返回比较元素的value函数

#include<map>

map<int, string> map1;
map1[3] = "Saniya";
map1.insert(map<int,string>::value_type(2,"Diyabi"));
map1.insert(pair<int, string>(1, "sijgj"));
map1.inset(make_pair<int, string>(4, "v4"));
string str = map1[4]; //当key不存在返回空字符串
auto iter_map = map1.begin(); //根据迭代取首地址
int key = iter_map->first;
string value = iter_map->second;
map1.erase(3);
map1.clear();
map1.size();
map1.clear();
//遍历
for (map<int,string>::iterator iter=map1.begin(); iter !=map1.end(); iter++)
{int keys = iter->first; string value = iter->second;}

4、multi_map multi_set 可重复

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值