数据结构相关知识总结(C++)

二、数据结构

1、二叉树

1.1、平衡二叉树

1、定义
  • 左右子树都是平衡二叉树(左子叶<根节点<右子叶),且左右子树的深度差的绝对值不大于1。

  • 树的深度为logn,查找、插入、删除的时间均为logn。

  • 平衡因子BF为该节点左子树深度减去右子树深度,将距离结点最近的,且平衡因子的绝对值大于1的结点为根的子树,称为最小不平衡子树

2、插入新结点、平衡操作
  • 最小不平衡子树的BF与它的子树的BF符号相同时:最小不平衡子树根节点的平衡因子大于1时(小于1时),右旋(左旋)
  • 最小不平衡子树的BF与它的子树的BF符号相反时:先对子树结点进行一次旋转使得符号相同,然后再反向旋转一次完成平衡操作
3、删除结点:

​ 删除结点后,从根节点到该节点的路径上的平衡因子都有可能改变,所以删除操作最多需要进行logn次旋转。

1.2、红黑树

​ 红黑树是一个二叉排序树,继承二叉排序树的显性特性:

  • 若左子树不空,则左子树上所有结点的值均小于或等于它的根结点的值。
  • 若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值。
  • 左、右子树也分别为二叉排序树
1、红黑树需要满足的条件:
  • 树的节点非红即黑;
  • 根节点必须是黑色;
  • 红节点的子节点必为黑(黑节点子节点可为黑);
  • 如果一个结点是红的,那么两个儿子都是黑的;
  • 对于每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑节点(从根到nullptr的任何路径上的黑节点数目相同)。
2、插入和删除操作

​ 插入和删除操作都需要根据红黑树的性质,进行对应的旋转和调整

插入:根据红黑树的性质 ,说明插入的结点初始色要为红色 不是很理解

  • 要是父节点(存在)是黑色的话,则插入成功,不需要改变;
  • 要是父节点(存在)是红色的话,并且是祖父的左子结点,会违反性质

​ 如果叔结点为红色,将祖父结点调整为红,父结点和叔结点调整为黑色,然后从祖父结点递归调整一下,直到根结点。如果碰巧将根节点染成了红色, 可以在最后强制 root->黑。

​ 叔结点为黑色,且当前节点是右孩子,需要右旋

​ 叔结点为黑色,且当前节点是左孩子,需要左右旋

  • 如果父结点(存在)是红色的话,且是祖父的右子结点,此时说明性质4会违反

​ 叔结点为红色,将祖父结点调整为红,父结点和叔结点调整为黑色,然后从祖父结点递归调整一下,直到根结点。如果碰巧将根节点染成了红色, 可以在最后强制 root->黑。

​ 叔结点为黑色,且当前节点是右孩子,需要左旋

​ 叔结点为黑色,且当前节点是左孩子,需要右左旋

总结红黑树比AVL的优势

第一种回答:

​ 首先红黑树是不符合AVL树的平衡条件的,即每个节点的左子树和右子树的高度最多差1的二叉查找树。但是提出了为节点增加颜色,红黑是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。所以红黑树的插入效率更高!!!

第二种回答:

​ 红黑树的 查询性能略微逊色于AVL树,因为他比AVL树会稍微不平衡最多一层,也就是说红黑树的查询性能只比相同内容的AVL树最多多一次比较,但是,红黑树在插入和删除上完爆AVL树, AVL树每次插入删除会进行大量的平衡度计算,而红黑树为了维持红黑性质所做的红黑变换和旋转的开销,相较于AVL树为了维持平衡的开销要小得多。

第三种回答:

  • 如果插入一个node引起了树的不平衡,AVL和RB-Tree都是最多只需要2次旋转操作,即两者都是O(1);但是在删除node引起树的不平衡时,最坏情况下,AVL需要维护从被删node到root这条路径上所有node的平衡性,因此需要旋转的量级O(logN),而RB-Tree最多只需3次旋转,只需要O(1)的复杂度。
  • 其次,AVL的结构相较RB-Tree来说更为平衡,在插入和删除node更容易引起Tree的unbalance,因此在大量数据需要插入或者删除时,AVL需要rebalance的频率会更高。因此,RB-Tree在需要大量插入和删除node的场景下,效率更高。自然,由于AVL高度平衡,因此AVL的search效率更高。
  • map的实现只是折衷了两者在search、insert以及delete下的效率。总体来说,RB-tree的统计性能是高于AVL的。

原文链接:https://blog.csdn.net/mmshixing/article/details/51692892

重点第四种回答:

  • 红黑树不追求"完全平衡",即不像AVL那样要求节点的 |balFact| <= 1,它只要求部分达到平衡,但是提出了为节点增加颜色,红黑是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。

  • 就插入节点导致树失衡的情况,AVL和RB-Tree都是最多两次树旋转来实现复衡rebalance,旋转的量级是O(1);
    删除节点导致失衡,AVL需要维护从被删除节点到根节点root这条路径上所有节点的平衡,旋转的量级为O(logN),而RB-Tree最多只需要旋转3次实现复衡,只需O(1),所以说RB-Tree删除节点的rebalance的效率更高,开销更小!

  • AVL的结构相较于RB-Tree更为平衡,插入和删除引起失衡,如2所述,RB-Tree复衡效率更高;当然,由于AVL高度平衡,因此AVL的Search效率更高啦。

  • 针对插入和删除节点导致失衡后的rebalance操作,红黑树能够提供一个比较"便宜"的解决方案,降低开销,是对search,insert ,以及delete效率的折衷,总体来说,RB-Tree的统计性能高于AVL.

  • 故引入RB-Tree是功能、性能、空间开销的折中结果

    • AVL更平衡,结构上更加直观,时间效能针对读取而言更高;维护稍慢,空间开销较大。
    • 红黑树,读取略逊于AVL,维护强于AVL,空间开销与AVL类似,内容极多时略优于AVL,维护优于AVL。

    基本上主要的几种平衡树看来,红黑树有着良好的稳定性和完整的功能,性能表现也很不错,综合实力强,在诸如STL的场景中需要稳定表现。

1.3、B树

	B树也是B-树,是多路平衡查找树,它每个节点最多有m-1个关键字,根节点最少可以只有1个关键字,非根节点至少有m/2个关键字,每个节点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,右子树中的所有关键字都大于它。根节点到每个叶子节点的长度都相同,每个节点都存有索引和数据(key和value)。

1.4、B+树

​ B和B+树其实非常相似

相同点:根节点都是至少一个元素,非根节点元素范围:m/2 <= k <= m-1(K 节点数,m是阶数)。

​ 但是B+树右两种类型的节点:内部节点和叶子结点,内部结点就是非叶子结点,内部结点不存储数据,只存储索引,数据都存储在叶子结点。内部结点中的key都按照从小到大的顺序排列,对于内部结点的Key,左树中的所有key都小于它,右树中的key都大于等于它,叶子结点中的记录也按照Key的大小排序。

​ 在B+Tree中,所有数据节点都是按照键值大小存放在同一层的叶子结点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。我们知道IO次数取决于B+数的高度h,假设当前数据表的数据为N,每个磁盘块的数据项的数量是m,则有h=㏒(m+1)N,当数据量N一定的情况下,m越大,h越小;而m = 磁盘块的大小 / 数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数据项的数量越多,树的高度越低

​ 所以综上,B+树其实是改进了B树,让内节点只做为索引使用,去掉了其中指向data record的指针,使得每个结点中都能够存放更多的key,因此可以右更大的出度。这样就意味着,存放同样多的key,树的层高可以被进一步的压缩,让检索的时间更短。

B树和B+树的不同:

  • 非叶子结点只存储键值信息;
  • 所有叶子结点之间都有一个链指针;
  • 数据记录都存放在叶子节点中。

总结AVL、RB-tree、B、B+

  • AVL:平衡二叉树,一般是用平衡因子差值决定并通过旋转来实现,左右子树树高差不超过1,那么和红黑树比较它是严格的平衡二叉树,平衡条件非常严格(树高差只有1),只要插入或删除不满足上面的条件就要通过旋转来保持平衡。由于旋转是非常耗费时间的。我们可以推出AVL树适合用于插入删除次数比较少,但查找多的情况

  • RB-tree:通过对任何一条从根到叶子的简单路径上各个节点的颜色进行约束,确保没有一条路径会比其他路径长2倍,因而是近似平衡的。所以相对于严格要求平衡的AVL树来说,它的旋转保持平衡次数较少。用于搜索时,插入删除次数多的情况下我们就用红黑树来取代AVL。

  • B:它们特点是一样的,是多路查找树,一般用于数据库系统中,为什么,因为它们分支多层数少呗,都知道磁盘IO是非常耗时的,而像大量数据存储在磁盘中所以我们要有效的减少磁盘IO次数避免磁盘频繁的查找。

  • B+:B+树是B树的变种树,有n棵子树的节点中含有n个关键字,每个关键字不保存数据,只用来索引,数据都保存在叶子节点。是为文件系统而生的。

1.5、为什么MySQL索引要使用B+树,而不是B树或者红黑树?

​ 我们在MySQL中的数据一般是放在磁盘中的,读取数据的时候肯定会有访问磁盘的操作,磁盘中有两个机械运动的部分,分别是盘片旋转和磁臂移动。盘片旋转就是我们市面上所提到的多少转每分钟,而磁盘移动则是在盘片旋转到指定位置以后,移动磁臂后开始进行数据的读写。那么这就存在一个定位到磁盘中的块的过程,而定位是磁盘的存取中花费时间比较大的一块,毕竟机械运动花费的时候要远远大于电子运动的时间。当大规模数据存储到磁盘中的时候,显然定位是一个非常花费时间的过程,但是我们可以通过B树进行优化,提高磁盘读取时定位的效率。

1.6、为什么B类树可以进行优化呢?

​ 我们可以根据B类树的特点,构造一个多阶的B类树,然后在尽量多的在结点上存储相关的信息,保证层数(树的高度)尽量的少,以便后面我们可以更快的找到信息,磁盘的I/O操作也少一些,而且B类树是平衡树,每个结点到叶子结点的高度都是相同,这也保证了每个查询是稳定的。

​ **特别地:只有B-树和B+树,这里的B-树是叫B树,不是B减树,没有B减树的说法

1.7、为什么MySQL索引要使用B+树,而不是B树或者hash表?

  • 利用Hash需要把数据全部加载到内存中,如果数据量大,是一件很消耗内存的事,而采用B+树,是基于按照节点分段加载,由此减少内存消耗

  • 和业务场景有段,对于唯一查找(查找一个值),Hash确实更快,但数据库中经常查询多条数据,这时候由于B+数据的有序性,与叶子节点又有链表相连,他的查询效率会比Hash快的多。

  • B+树的非叶子节点不保存数据只保存子树的临界值(最大或者最小),所以同样大小的节点,B+树相对于b树能够有更多的分支,使得这棵树更加矮胖,查询时做的IO操作次数也更少

1.8、既然Hash比B+树更快,为什么MySQL用B+树来存储索引呢?

​ MySQL中存储索引用到的数据结构是B+树,B+树的查询时间跟树的高度有关,是log(n),如果用hash存储,那么查询时间是O(1)。

​ 采用Hash来存储确实要更快,但是采用B+树来存储索引的原因主要有以下两点:

  • 从内存角度上说,数据库中的索引一般是在磁盘上,数据量大的情况可能无法一次性装入内存,B+树的设计可以允许数据分批加载。

  • 从业务场景上说,如果只选择一个数据那确实是hash更快,但是数据库中经常会选中多条,这时候由于B+树索引有序,并且又有链表相连,它的查询效率比hash就快很多了。

1.9、树的存储结构

1、双亲表示法

​ 以双亲作为索引的关键词的一种存储方式。假设以一组连续空间存储数的结点,同时在每个结点中,附设一个指示器指示其双亲结点到链表中的位置

​ 双亲表示的结点结构

data(数据域) parent(指针域)
存储结点的数据信息 存储该结点的双亲所在数组中的下标

​ 双亲表示法的特点

  • 由于根结点是没有双亲的,约定根结点的位置位置域为-1.
  • 根据结点的parent指针很容易找到它的双亲结点。所用时间复杂度为O(1),直到parent为-1时,表示找到了树结点的根。
  • 缺点:如果要找到孩子结点,需要遍历整个结构才行
2、孩子表示法

​ 由于每个结点可有多个子树(无法确定子树个数),可以考虑使用多重链表来实现。把每个结点的孩子结点排列起来,以单链表作为存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中。

​ 孩子表示法有两种结点结构:孩子链表的孩子结点表头数组的表头结点

  • 孩子链表的孩子结点
child(数据域) next(指针域)
存储某个结点在表头数组中的下标 存储指向某结点的下一个孩子结点的指针
  • 表头数组的表头结点
data(数据域) firstchild(头指针域)
存储某个结点的数据信息 存储该结点的孩子链表的头指针
3、双亲孩子表示法定义

​ 对于孩子表示法,查找某个结点的某个孩子,或者找某个结点的兄弟,只需要查找这个结点的孩子单链表即可。但是当要寻找某个结点的双亲时,就不是那么方便了。所以可以将双亲表示法和孩子表示法结合,形成双亲孩子表示法

4、孩子兄弟表示法

​ 任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟存在也是唯一的。因此,设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。

​ 孩子兄弟表示法的结点结构

data(数据域) firstchild(指针域) rightsib(指针域)
存储结点的数据信息 存储该结点的第一个孩子的存储地址 存储该结点的右兄弟结点的存储地址

2、map底层实现

2.1、map底层为什么用红黑树

​ map,set底层都提供了排序功能,且查找速度快。红黑树实际上是AVL的一种变形(低配版,尽量维持了树的平衡,平衡二叉树是严格平衡的,需要频繁的rebalance,导致效率低下),红黑树通过对任意一条从根到叶子的路径上各个结点着色方式的限制,确保没有一条路径会比其他路径长两倍,是弱平衡的,插入最多旋转2次,删除最多旋转3次,所以它可以在O(log n)时间内做查找,插入和删除,有较高的效率。

2.2、map和set

​ map是key_value键值对,set是单值,他们都是按照顺序排序,但是set不允许修改key,并且值不重复。

2.3、为什么用红黑树?

​ 因为map和set要求是自动排序的,红黑树能够实现这一功能,而且时间复杂度比较低。

3、unordered_map 和 map

​ 都是存储的key-value的值,可以通过key快速索引到value。unordered_map不会根据key的大小进行排序,存储时根据key的hash判断元素是否相同,unordered_map内部元素是无序的底层由哈希表实现,map中的元素是按照红黑树进行的存储。

​ unordered_map 用链表散列的存储方式,频繁在大量数据中查询,速度几乎是常数级别,所以比起map搜索速度快(红黑树实现,查找速度O(logn))。

​ unordered_map和map,红黑树(每个节点都额外保存父节点、孩子节点和红黑性质,其实占空间也蛮大的)和哈希表(链表散列空间会存在部分未被使用的位置,所以内存效率不是100%),还是unorderd_map内存占用高。同时还有一点就是,因为unorded_map这样的容器,其遍历顺序与创建该容器时输入的顺序不一定相同,因为遍历是按照哈希表从前往后依次遍历的。

​ 综上,对有顺序要求的问题且需要元素有序,用map。但是查找问题,还是unorded_map高效。但是查找性能的稳定性,还是map高,因为map的查找是类似于平衡二叉树的查找,查找次数与存储数据的分布和大小无关。unordered_map的查找次数与存储数据的分布与大小有密切关系,所以unordered_map还是适用于要求查找速率快,且对单次查询性能要求不敏感的场景。

4、unordered_set和set

​ set基于红黑树实现,红黑树具有自动排序功能,内部数据在任何时候都是有序的。

​ unordered_set内部基于哈希表,数据插入和查找时间复杂度很低,几乎为常数级别,但是会消耗较多的内存,无自动排序功能。底层上,使用一个下标范围较大的数组来存储元素,就相当于形成很多的桶,利用哈希函数对key进行映射存储。unordered_set这样的容器,其遍历顺序与创建该容器时输入的顺序不一定相同,因为遍历是按照哈希表从前往后依次遍历的。

5、二分查找与二叉树查找

​ 时间复杂度O(logN)

5.1、二分查找

​ 二分查找:即折半查找,优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难

5.2、二叉树查找

​ 二叉树查找:它或者是一棵空树,或者若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
​ 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。

5.3、二分查找与二叉树查找的区别

​ 两者明显的区别是二分查找速度快删除和插入困难,二对于建立的二叉树索引来说,他的插入和删除是相对较快的。为什么会出现这两者的差别其实底层更多的考虑的是数据的存储结构:

1、顺序存储和链式存储的概念:
  • 从空间性能,顺序存储会对空间资源做到百分之百的利用,而链式存储对对空间的利用不是百分之百,因为存储了指针,不是真正的数据
  • 从时间性能上来讲读取速度的话顺序存储更优,插入和删除操作链式存储更优,链式存储只需要移动指针,不需要移动元素。
2、什么时候采用二分?什么时候采用二叉索引?
  • 如果我们的数据是不进行频繁变化且是有序,而且查询相对较多的情况下采用二分查找
  • 我们的数据是频繁变化的考虑到后面的数据扩容的情况下,我们考虑采用二叉索引的方式,但是这种会有一点空间资源的牺牲。

6、哈希表

​ 哈希函数:根据查找的关键字key值,可以直接确定查找值所在的位置

6.1、哈希函数的构造方法:

1、直接定地址法:取关键字或关键字的某个线性函数值为哈希地址;

2、数字分析法:假设关键字集合中的每个关键字key都是由s位数字组成,分析key中的数据,从中提取某位某均匀分布的数字做为key。此种方法通常用于数字位数较长的情况,且数字需要存在一定的规律。比如:一个班同学的ID,就可以用身份证后五位;

3、平法取中法:如果关键字的每一位都有某些数字重复出现频率很高的现象,可以先求关键字的平方值,通过平方扩大差异,而后取中间数位作为最终存储地址比如key=1234 1234^2=1522756 取227作hash地址,比如key=4321 4321^2=18671041 取671作hash地址。 这种方法适合事先不知道数据并且数据长度较小的情况;

4、折叠法:数字的位数很多,将数字分割成几部分,取他们的叠加和做为hash地址;

5、除留余数法:H = key MOD p ,p应该为不大于m的质数,可以有效减少地址的重复;

6、随机数法:H = random key。当关键字长度不等时用这种方法;

6.2、哈希函数设计的考虑因素:

  • 计算哈希函数所需时间;
  • 关键字长度;
  • 哈希表的大小;
  • 关键字的分布情况;
  • 记录的查找频率。

6.3、哈希冲突

​ 例如:6 3 9,模取3的话,都会发生地址冲突。

6.4、哈希冲突的解决方式

1、开放地址法

​ 线性探测再散列(直接往后面挨个挨个排)、二次探测再散列(左右两边探测)、随机探测再散列

2、链地址法

​ 在产生hash冲突后在存储数据后面加个指针,指向后面冲突的数据

3、公共溢出区法

​ 建立一个特殊存储空间,专门存放冲突数据。此种方法适用于数据和冲突较少的情况。

4、哈希表的查找效率

​ 首先与选用的哈希函数、处理哈希冲突的方法、哈希表的饱和度

7、一致性哈希

你可能感兴趣的:(hash,数据结构,c++,b树)