常见的5种数据结构,常见的数据结构有哪些

数据结构有哪些(教你弄懂9大常见数据结构)完全二叉树:除了最

完全二叉树:除了最后一层的节点,其他层的节点数都达到了最大值;同时,最后一层的节点从左到右排列。

全二叉树:除了最后一层,其他层的所有节点都有两个子节点。

平衡二叉树

平衡二叉树又称AVL树,是一种二叉排序树,具有以下性质:它是一棵空树或其左右子树高度差的绝对值小于1,左右子树都是平衡二叉树。

二叉排序树:它是一棵空树,或者如果它的左子树不为空,那么左子树上所有节点的值都小于它的根节点的值;如果其右子树不为空,则右子树上所有节点的值都大于其根节点的值;它的左右子树分别是二叉排序树。

树的高度:节点层次的最大值。

平衡系数:左侧子树高度-右侧子树高度

二叉排序树是指二叉树中的数据是有序的,顺序是左节点、根节点、右节点,表示二叉排序树的遍历结果是有序的。(不了解二叉树四种遍历方式【前序遍历、中序遍历、后序遍历、顺序遍历】的同学抓紧时间!)

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

创建平衡二叉树是为了解决二叉排序树插入时线性排列的问题。由于二叉排序树本身是有序的,当插入一个有序度很高的序列时,生成的二叉排序树会继续插入某一方向字数上的数据,导致最终的二叉排序树退化为链表,从而恶化了二叉树的查询和插入效率。

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

平衡二叉树的出现可以解决上述问题,但在构造平衡二叉树时,需要采用不同的调整方法来保持插入数据后二叉树的平衡。四种主要的调节模式是LL(左手)、RR(右手)、LR(左手然后右手)和RL(右手然后左手)。这里先介绍一下简单的单旋转操作,左手和右手。LR和RL本质上只是LL和RR的组合。

插入节点后,路径上的节点平衡因子应该沿着搜索路径进行修改。当平衡系数大于1时,需要平衡。从不平衡的节点开始,沿着刚才追溯的路径直接取下两层的节点。如果这三个节点在一条直线上,用单转进行平衡;如果这三个节点在一条折线上,使用双旋转来平衡。

左撇子:s是当前需要左撇子的节点,e是当前节点的父节点。

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

左手操作可以简单地用一句话来表达:把当前节点S的左子变成当前节点的父节点E的右子,同时把父节点E变成当前节点S的左子.可供动画:

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

右旋:s是当前需要左旋的节点,e是当前节点的父节点。右旋转是左旋转的镜像旋转。

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

左手操作也可以简单地用一句话来表达:将当前节点S的左子E的右子旋转到当前节点S的左子,同时将当前节点S旋转到左子E的右子.可用于动画:

/>

红黑树

平衡二叉树(AVL)为了追求高度平衡,需要通过平衡处理使得左右子树的高度差必须小于等于1。高度平衡带来的好处是能够提供更高的搜索效率,其最坏的查找时间复杂度都是O(logN)。但是由于需要维持这份高度平衡,所付出的代价就是当对树种结点进行插入和删除时,需要经过多次旋转实现复衡。这导致AVL的插入和删除效率并不高。

为了解决这样的问题,能不能找一种结构能够兼顾搜索和插入删除的效率呢?这时候红黑树便申请出战了。

红黑树具有五个特性:

  1. 每个结点要么是红的要么是黑的。
  2. 根结点是黑的。
  3. 每个叶结点(叶结点即指树尾端NIL指针或结点)都是黑的。
  4. 如果一个结点是红的,那么它的两个儿子都是黑的。
  5. 对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。
图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

红黑树通过将结点进行红黑着色,使得原本高度平衡的树结构被稍微打乱,平衡程度降低。红黑树不追求完全平衡,只要求达到部分平衡。这是一种折中的方案,大大提高了结点删除和插入的效率。C++中的STL就常用到红黑树作为底层的数据结构。

红黑树VS平衡二叉树

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

除了上面所提及的树结构,还有许多广泛应用在数据库、磁盘存储等场景下的树结构。比如B树、B+树等。这里就先不介绍了诶,下次在讲述相关存储原理的时候将会着重介绍。(其实是因为懒)

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

了解完二叉树,再来理解堆就不是什么难事了。堆通常是一个可以被看做一棵树的数组对象。堆的具体实现一般不通过指针域,而是通过构建一个一维数组与二叉树的父子结点进行对应,因此堆总是一颗完全二叉树。

对于任意一个父节点的序号n来说(这里n从0算),它的子节点的序号一定是2n+1,2n+2,因此可以直接用数组来表示一个堆。

不仅如此,堆还有一个性质:堆中某个节点的值总是不大于或不小于其父节点的值。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

堆常用来实现优先队列,在面试中经常考的问题都是与排序有关,比如堆排序、topK问题等。由于堆的根节点是序列中最大或者最小值,因而可以在建堆以及重建堆的过程中,筛选出数据序列中的极值,从而达到排序或者挑选topK值的目的。

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

散列表

散列表也叫哈希表,是一种通过键值对直接访问数据的机构。在初中,我们就学过一种能够将一个x值通过一个函数获得对应的一个y值的操作,叫做映射。散列表的实现原理正是映射的原理,通过设定的一个关键字和一个映射函数,就可以直接获得访问数据的地址,实现O(1)的数据访问效率。在映射的过程中,事先设定的函数就是一个映射表,也可以称作散列函数或者哈希函数。

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

散列表的实现最关键的就是散列函数的定义和选择。一般常用的有以下几种散列函数:

直接寻址法:取关键字或关键字的某个线性函数值为散列地址。

数字分析法:通过对数据的分析,发现数据中冲突较少的部分,并构造散列地址。例如同学们的学号,通常同一届学生的学号,其中前面的部分差别不太大,所以用后面的部分来构造散列地址。

平方取中法:当无法确定关键字里哪几位的分布相对比较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为散列地址。这是因为:计算平方之后的中间几位和关键字中的每一位都相关,所以不同的关键字会以较高的概率产生不同的散列地址。

取随机数法:使用一个随机函数,取关键字的随机值作为散列地址,这种方式通常用于关键字长度不同的场合。

除留取余法:取关键字被某个不大于散列表的表长 n 的数 m 除后所得的余数 p 为散列地址。这种方式也可以在用过其他方法后再使用。该函数对 m 的选择很重要,一般取素数或者直接用 n。

确定好散列函数之后,通过某个key值的确会得到一个唯一的value地址。但是却会出现一些特殊情况。即通过不同的key值可能会访问到同一个地址,这个现象称之为冲突。

冲突在发生之后,当在对不同的key值进行操作时会使得造成相同地址的数据发生覆盖或者丢失,是非常危险的。所以在设计散列表往往还需要采用冲突解决的办法。

常用的冲突处理方式有很多,常用的包括以下几种:

开放地址法(也叫开放寻址法):实际上就是当需要存储值时,对Key哈希之后,发现这个地址已经有值了,这时该怎么办?不能放在这个地址,不然之前的映射会被覆盖。这时对计算出来的地址进行一个探测再哈希,比如往后移动一个地址,如果没人占用,就用这个地址。如果超过最大长度,则可以对总长度取余。这里移动的地址是产生冲突时的增列序量。

再哈希法:在产生冲突之后,使用关键字的其他部分继续计算地址,如果还是有冲突,则继续使用其他部分再计算地址。这种方式的缺点是时间增加了。

链地址法:链地址法其实就是对Key通过哈希之后落在同一个地址上的值,做一个链表。其实在很多高级语言的实现当中,也是使用这种方式处理冲突的。

公共溢出区:这种方式是建立一个公共溢出区,当地址存在冲突时,把新的地址放在公共溢出区里。

目前比较常用的冲突解决方法是链地址法,一般可以通过数组和链表的结合达到冲突数据缓存的目的。

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

左侧数组的每个成员包括一个指针,指向一个链表的头。每发生一个冲突的数据,就将该数据作为链表的节点链接到链表尾部。这样一来,就可以保证冲突的数据能够区分并顺利访问。

考虑到链表过长造成的问题,还可以使用红黑树替换链表进行冲突数据的处理操作,来提高散列表的查询稳定性。

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

图相较于上文的几个结构可能接触的不多,但是在实际的应用场景中却经常出现。比方说交通中的线路图,常见的思维导图都可以看作是图的具体表现形式。

图结构一般包括顶点和边,顶点通常用圆圈来表示,边就是这些圆圈之间的连线。边还可以根据顶点之间的关系设置不同的权重,默认权重相同皆为1。此外根据边的方向性,还可将图分为有向图和无向图。

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

图结构用抽象的图线来表示十分简单,顶点和边之间的关系非常清晰明了。但是在具体的代码实现中,为了将各个顶点和边的关系存储下来,却不是一件易事。

邻接矩阵

目前常用的图存储方式为邻接矩阵,通过所有顶点的二维矩阵来存储两个顶点之间是否相连,或者存储两顶点间的边权重。

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

无向图的邻接矩阵是一个对称矩阵,是因为边不具有方向性,若能从此顶点能够到达彼顶点,那么彼顶点自然也能够达到此顶点。此外,由于顶点本身与本身相连没有意义,所以在邻接矩阵中对角线上皆为0。

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

有向图由于边具有方向性,因此彼此顶点之间并不能相互达到,所以其邻接矩阵的对称性不再。

用邻接矩阵可以直接从二维关系中获得任意两个顶点的关系,可直接判断是否相连。但是在对矩阵进行存储时,却需要完整的一个二维数组。若图中顶点数过多,会导致二维数组的大小剧增,从而占用大量的内存空间。

而根据实际情况可以分析得,图中的顶点并不是任意两个顶点间都会相连,不是都需要对其边上权重进行存储。那么存储的邻接矩阵实际上会存在大量的0。虽然可以通过稀疏表示等方式对稀疏性高的矩阵进行关键信息的存储,但是却增加了图存储的复杂性。

因此,为了解决上述问题,一种可以只存储相连顶点关系的邻接表应运而生。

邻接表

在邻接表中,图的每一个顶点都是一个链表的头节点,其后连接着该顶点能够直接达到的相邻顶点。相较于无向图,有向图的情况更为复杂,因此这里采用有向图进行实例分析。

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

在邻接表中,每一个顶点都对应着一条链表,链表中存储的是顶点能够达到的相邻顶点。存储的顺序可以按照顶点的编号顺序进行。比如上图中对于顶点B来说,其通过有向边可以到达顶点A和顶点E,那么其对应的邻接表中的顺序即B->A->E,其它顶点亦如此。

通过邻接表可以获得从某个顶点出发能够到达的顶点,从而省去了对不相连顶点的存储空间。然而,这还不够。对于有向图而言,图中有效信息除了从顶点“指出去”的信息,还包括从别的顶点“指进来”的信息。这里的“指出去”和“指进来”可以用出度和入度来表示。

入度:有向图的某个顶点作为终点的次数和。

出度:有向图的某个顶点作为起点的次数和。

由此看出,在对有向图进行表示时,邻接表只能求出图的出度,而无法求出入度。这个问题很好解决,那就是增加一个表用来存储能够到达某个顶点的相邻顶点。这个表称作逆邻接表。

逆邻接表

逆邻接表与邻接表结构类似,只不过图的顶点链接着能够到达该顶点的相邻顶点。也就是说,邻接表时顺着图中的箭头寻找相邻顶点,而逆邻接表时逆着图中的箭头寻找相邻顶点。

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

邻接表和逆邻接表的共同使用下,就能够把一个完整的有向图结构进行表示。可以发现,邻接表和逆邻接表实际上有一部分数据时重合的,因此可以将两个表合二为一,从而得到了所谓的十字链表。

十字链表

十字链表似乎很简单,只需要通过相同的顶点分别链向以该顶点为终点和起点的相邻顶点即可。

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

但这并不是最优的表示方式。虽然这样的方式共用了中间的顶点存储空间,但是邻接表和逆邻接表的链表节点中重复出现的顶点并没有得到重复利用,反而是进行了再次存储。因此,上图的表示方式还可以进行进一步优化。

十字链表优化后,可通过扩展的顶点结构和边结构来进行正逆邻接表的存储:(下面的弧头可看作是边的箭头那端,弧尾可看作是边的圆点那端)

data:用于存储该顶点中的数据;

firstin指针:用于连接以当前顶点为弧头的其他顶点构成的链表,即从别的顶点指进来的顶点;

firstout指针:用于连接以当前顶点为弧尾的其他顶点构成的链表,即从该顶点指出去的顶点;

边结构通过存储两个顶点来确定一条边,同时通过分别代表这两个顶点的指针来与相邻顶点进行链接:

tailvex:用于存储作为弧尾的顶点的编号;

headvex:用于存储作为弧头的顶点的编号;

headlink 指针:用于链接下一个存储作为弧头的顶点的节点;

taillink 指针:用于链接下一个存储作为弧尾的顶点的节点;

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

以上图为例子,对于顶点A而言,其作为起点能够到达顶点E。因此在邻接表中顶点A要通过边AE(即边04)指向顶点E,顶点A的firstout指针需要指向边04的tailvex。同时,从B出发能够到达A,所以在逆邻接表中顶点A要通过边AB(即边10)指向B,顶点A的firstin指针需要指向边10的弧头,即headlink指针。以此类推。

十字链表采用了一种看起来比较繁乱的方式对边的方向性进行了表示,能够在尽可能降低存储空间的情况下增加指针保留顶点之间的方向性。具体的操作可能一时间不好弄懂,建议多看几次上图,弄清指针指向的意义,明白正向和逆向邻接表的表示。

图解!24 张图彻底弄懂九大常见数据结构
图解!24 张图彻底弄懂九大常见数据结构

总结

数据结构博大精深,没有高等数学的讳莫如深,也没有量子力学的玄乎其神,但是其在计算机科学的各个领域都具有强大的力量。本文试图采用图解的方式对九种数据结构进行理论上的介绍,但是其实这都是不够的。

即便是简单的数组、栈、队列等结构,在实际使用以及底层实现上都会有许多优化设计以及使用技巧,这意味着还需要真正把它们灵活的用起来,才能够算是真正意义上的熟悉和精通。但是本文可以作为常见数据结构的一个总结,当你对某些结构有些淡忘的时候,不妨重新回来看看。

内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/173462.html

(0)

相关推荐