STL学习笔记(四)---树与关联式容器

关联式容器,观念上类似关联式数据库(实际上则简单许多) :每笔数据(每个元素)都有一个键值(key) 和一个实值(value) 。当元索被插入到关联式容器中时,容器内部结构(可能是RB-tree,也可能是hash-table)便依照其键值大小,以某种特定规则将这个元素放置于适当位置。关联式容器没有所谓头尾(只有最大元素和最小元素),所以不会有所谓push_back(). push_front().pop_back(). pop_front(). begin().end() 这样的操作。一般而言,关联式容器的内部结构是个balanced binary tree(平衡二叉树),以便获得良好的搜寻效率。balanced binary tree有许多种类型,包括AVL-tree,RB-tree, AA-tree,其中最被广泛运用于STL的是RB-tree (红黑树)。

一、二叉搜索树

二叉搜案树(binary search tree),可提供对数时间(logarithmic time) 的元素插入和访问。二叉搜索树的节点放置规则是:任何节点的键值一定大于其左子树中的每一个节点的键值,并小于其右子树中的每一个节点的键值。因此,从根节点一直往左走,直至无左路可走,即得最小元素;从根节点一直往右走,直至无右路可走,即得最大元素,图5-4所示的就是一棵二叉搜索树。
 

有数种特殊结构如AVL-tree,RB-tree,AA-tree,均可实现出平衡二叉搜索树,普通的二叉树在插入元素是正序或者逆序的时候,左右子树的高度会变得极其不平衡,导致丧失了查找复杂度O(logN)的优势,复杂度会增长到O(N),所以就引入了AVL树和红黑树。能够在元素插入删除时候自动按照给定的规则调整元素间的顺序关系从而保持平衡。它们比一般的(无法绝对维持平衡的)二叉搜索树复杂,因此插入节点和删除节点的平均时间也比较长,但是它们可以避免极难应付的最坏(高度不平衡)情况,而且由于总是保持某种程度的平衡,所以元素的访问查找时间平均也比较少。

1、AVL tree ( Adelson-Velskii-Landis tree ) 

AVLtree是一个“加上了额外平衡条件” 的二叉搜索树。其平衡条件的建立是为了确保整棵树的深度为OlogN)。 AVLtree要求任何节点的左右子树高度相差最多1。

2、 RB-tree (红黑树)

AVL-tree之外,另一个被广泛运用的平衡二叉搜索树是RB-tree(红黑树) 。RB-tree不仅是一个二叉搜索树,而且必须满足以下规则:

1. 每个节点不是红色就是黑色。
2.根节点为黑色。
3.如果节点为红, 其子节点必须为黑。
4. 任一节点至NULL (树尾端)的任何路径,所含之黑节点数必须相同。

根据规则4,新增节点必须为红;根据规则3,新增节点之父节点必须为黑。当新节点根据二叉搜索树的规则到达其插入点,却未能符合上述条件时,就必须调整颜色并旋转树形。

 二、map/multimap

map的所有元素都会根据元素的键值自动排序。所有元素都是pair,同时拥有实值(value) 和键值(key)。pair的第一元素被视为键值,第二元素被视为实值。map不允许两个元素拥有相同的键值。下面是<stl_pair.h> 中的pair定义:

map插入方式

用insert函数插入pair数据,

mapStudent.insert(pair<int, string>(1, "student_one"));  

用insert函数插入value_type数据

mapStudent.insert(map<int, string>::value_type (1, "student_one"));

在insert函数中使用make_pair()函数

mapStudent.insert(make_pair(1, "student_one"));  

用数组方式插入数据

mapStudent[1] = "student_one";  

vector和map下标访问运算符的区别?

通过下标访问vector中的元素时不会做边界检查,即便下标越界。也就是说,下标与first迭代器相加的结果超过了finish迭代器的位置,程序也不会报错,而是返回这个地址中存储的值。如果想在访问vector中的元素时首先进行边界检查,可以使用vector中的at函数。通过使用at函数不但可以通过下标访问vector中的元素,而且在at函数内部会对下标进行边界检查。

map的下标运算符[]的作用是:将key作为下标去执行查找,并返回相应的值;如果不存在这个key,就将一个具有该key和value的某值插入这个map。

map所有的元素根据pair里面的键值自动排序, 两个元素之间不允许有相同的键值。
和set一样,不能通过map的迭代器更改元素里的键值key,因为改变了key会破坏map的组织规则,但是可以通过迭代器更改实值value。
和set一样,当删除和插入map中一个元素时,对其他元素的迭代器不会有影响,其他迭代器不会失效。
map底层调用红黑树,即RB-Tree模板类。set要执行的所有操作,RB-tree差不多都提供了,所以几乎所有的map操作行为都是调用RB-tree的操作行为而已 。
multimap的特性以及用法和map完全相同,唯一的差别在于它允许键值重复,因此它采用的底层机制是RB-tree的insert_equal()而非insert_unique()

 

 用法

自动建立key - value的对应。key 和 value可以是任意需要的类型。使用map得包含map类所在的头文件#include <map>  //注意,STL头文件没有扩展名.h
map对象是模板类,需要关键字和存储对象两个模板参数:
std:map<int, string> personnel;
这样就定义了一个用int作为索引,并拥有相关联的指向string的指针.
 
map的构造函数
map共提供了6个构造函数,这块涉及到内存分配器这些东西。通常用如下方法构造一个map:
map<int, string> mapStudent;
 
插入元素
 
// 定义一个map对象
map<int, string> mapStudent;
// 第一种 用insert函數插入pair
mapStudent.insert(pair<int, string>(000, "student_zero"));
// 第二种 用insert函数插入value_type数据
mapStudent.insert(map<int, string>::value_type(001, "student_one"));
// 第三种 用"array"方式插入
mapStudent[123] = "student_first";
mapStudent[456] = "student_second";
以上三种用法都可以实现数据的插入,但它们是有区别的,第一种和第二种在效果上是一样的,用insert函数插入数据,在数据的插入上涉及到集合的唯一性这个概念,即当map中有这个关键字时,insert操作是不能在插入数据的,但是用数组方式就不同了,它可以覆盖以前该关键字对应的值,用程序说明如下:
mapStudent.insert(map<int, string>::value_type (001, "student_one"));
mapStudent.insert(map<int, string>::value_type (001, "student_two"));
上面这两条语句执行后,map中001这个关键字对应的值是“student_one”,第二条语句并没有生效,这就涉及到怎么知道insert语句是否插入成功的问题了,可以用pair来获得是否插入成功
// 构造定义,返回一个pair对象
pair<iterator,bool> insert (const value_type& val);
pair<map<int, string>::iterator, bool> Insert_Pair;
Insert_Pair = mapStudent.insert(map<int, string>::value_type (001, "student_one"));
if(!Insert_Pair.second)
    cout << ""Error insert new element" << endl;
通过pair的第二个变量来知道是否插入成功,它的第一个变量返回的是一个map的迭代器,如果插入成功的话Insert_Pair.second应该是true的,否则为false。

查找元素

当所查找的关键key出现时,返回数据所在对象的位置,如果沒有,返回iter与end函数的值相同。
// find 返回迭代器指向当前查找元素的位置否则返回map::end()位置
iter = mapStudent.find("123");
if(iter != mapStudent.end())
       cout<<"Find, the value is"<<iter->second<<endl;
else
   cout<<"Do not Find"<<endl;

刪除与清空元素

//迭代器刪除
iter = mapStudent.find("123");
mapStudent.erase(iter);
//用关键字刪除
int n = mapStudent.erase("123"); //如果刪除了會返回1,否則返回0
//用迭代器范围刪除 : 把整个map清空
mapStudent.erase(mapStudent.begin(), mapStudent.end());
//等同于mapStudent.clear()

map的大小

往map里面插入了数据,用size函数知道当前已经插入了多少数据呢:
int nSize = mapStudent.size();
 
map的基本操作函数:
     begin()         返回指向map头部的迭代器
     clear()        删除所有元素
     count()         返回指定元素出现的次数
     empty()         如果map为空则返回true
     end()           返回指向map末尾的迭代器
     equal_range()   返回特殊条目的迭代器对
     erase()         删除一个元素
     find()          查找一个元素
     get_allocator() 返回map的配置器
     insert()        插入元素
     key_comp()      返回比较元素key的函数
     lower_bound()   返回键值>=给定元素的第一个位置
     max_size()      返回可以容纳的最大元素个数
     rbegin()        返回一个指向map尾部的逆向迭代器
     rend()          返回一个指向map头部的逆向迭代器
     size()          返回map中元素的个数
     swap()           交换两个map
     upper_bound()    返回键值>给定元素的第一个位置
     value_comp()     返回比较元素value的函数

三、set/multiset

set即集合,主要特性有以下几个

set的底层是红黑树实现的,里面所有的元素键值会被自动排序,而且不允许两个元素有相同的键值,键值就是实值,实值就是键值。
通过set的迭代器不能改变set里面的元素键值。因为改变了键值红黑树的结点就得重新排列了,就会破坏set的组织。所以set的迭代器被定义为红黑树底层的const_iterator,杜绝写入操作。
当对某个元素插入或删除时,其余迭代器依然是保持有效的。
set底层调用红黑树,即RB-Tree模板类。set开放的操作接口,RB-tree也都提供了。所以几乎所有的set操作行为都是调用RB-tree的操作行为。
multiset的特性以及用法和set完全相同,唯一的差别在于它允许键值重复。因此它采用的底层机制是RB-tree的insert_equal()而非insert_unique()

用法

begin()        ,返回set容器的第一个迭代器
end()      ,返回set容器的最后一个迭代器的下一个位置
clear()          ,删除set容器中的所有的元素
empty()    ,判断set容器是否为空
max_size()   ,返回set容器可能包含的元素最大个数
size()      ,返回当前set容器中的元素个数
rbegin()     ,返回的值和begin()相同
rend()     ,返回的值和end()相同

四、总结

红黑树和AVL树的比较?

AVL树是带有平衡条件的二叉搜索树,一般是用平衡因子差值判断是否平衡并通过旋转来实现平衡,左右子树树高不超过1,和红黑树相比是严格的平衡二叉树,平衡条件必须满足(所有节点的左右子树高度差不超过1)。不管是执行插入还是删除操作,只要不满足上面的条件,就要通过旋转来保持平衡,而它的旋转非常耗时,所以AVL树适合用于插入与删除次数比较少但查找多的情况。
局限性:由于维护这种高度平衡所付出的代价比从中获得的效率收益还大,故而实际的应用不多,更多的地方是用追求局部而不是非常严格整体平衡的红黑树。当然如果应用场景中对插入删除不频繁,只是对查找要求较高,那么AVL还是较优于红黑树。
应用:在Windows NT内核中广泛存在;

红黑树也是一种二叉搜索树,但在每个节点增加一个存储位表示节点的颜色红或黑。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此,红黑树是一种弱平衡二叉树(由于是弱平衡,可以看到,在相同的节点情况下,AVL树的高度低于红黑树),相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下用红黑树。
广泛用于C ++的STL中,map和set都是用红黑树实现的;Linux的进程调度完全公平调度程序,用红黑树管理进程控制块,进程的虚拟内存区域都存储在一颗红黑树上,每个虚拟地址区域都对应红黑树的一个节点,左指针指向相邻的地址虚拟存储区域,右指针指向相邻的高地址虚拟地址空间;IO多路复用的epoll实现采用红黑树组织管理的sockfd,以支持快速的增删改查;Nginx中用红黑树管理定时器,因为红黑树是有序的,可以很快的得到距离当前最小的定时器。

红黑树和B+树的比较?

红黑树多用在内部排序,即全放在内存中的,STL的map和set的内部实现就是红黑树。B+树多用于外存上,成为一个磁盘友好的数据结构。

为什么B+树磁盘友好

磁盘读写代价更低
树的非叶子结点里面没有数据,这样索引比较小,可以放在一个blcok(或者尽可能少的blcok)里面。避免了树形结构不断的向下查找,然后磁盘不停的寻道,读数据。这样的设计可以降低IO的次数。

查询效率更加稳定
非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

遍历所有的数据更方便
B+树只要遍历叶子节点就可以实现整棵树的遍历,而其他的树形结构要中序遍历才可以访问所有的数据。

红黑树和哈希表的比较?

红黑树是有序的,Hash是无序的,根据需求来选择。红黑树占用的内存更小(仅需要为其存在的节点分配内存),而Hash事先就应该分配足够的内存存储散列表(即使有些槽可能不用)。红黑树查找和删除的时间复杂度都是O(logn),Hash查找和删除的时间复杂度都是O(1)。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值