各种数据结构及其优缺点和应用

一、顺序表

优点:本身为连续的内存块,访问元素效率高O(1)。
缺点:大小固定,扩展开销大,插入和删除开销大O(n)。
使用场景:需要大量访问元素而少量增加和删除的程序。

二、链表

优点:插入和删除节点十分高效O(1),不需要扩容。
缺点:链表不是随机存储结构,查找元素开销大O(n).
使用场景:适用于需要进行大量增加和删除元素操作而对访问元素无要求的程序。

三、二叉排序树

特点:左小右大,查找效率O(logn),极端情况下为O(n)。
插入:递归实现,新插入的节点一定是叶子节点。
删除:叶子节点:直接删除。删除节点只有左子树或右子树:用左子树或右子树代替自己。删除节点左子树右子树都有:与中序遍历的直接后继交换位置,并删除自己。

四、平衡二叉树

特点:左小右大,无重复值。为了解决二叉排序树退化成链表而诞生。
优点:平衡二叉树查询效率与高度成反比,h越小,查询越快O(logn)。
缺点:插入删除会破坏平衡,需要不定次左旋右旋恢复平衡,频繁的插入删除会影响效率。
调整不平衡:LL:右旋; RR:右旋; RL:右旋-左旋; LR:左旋-右旋。

五、红黑树

特点:根节点始终为黑色,没有相邻的红色节点,根节点到任意后代为NULL的节点的路径中,黑色节点数相同,每个叶子节点都为黑色。
优点:红黑树放弃了追求完全平衡,追求大致的平衡,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更简单O(logn)。
缺点:查找比AVL树慢(高度高),但删除效率大大提高。
应用:STL中 Set,MultiSet,Map,MultiMap。
插入规则:
1、若X为root,将X设置为黑色,结束。
叔叔为红:
1、将新插入的节点X标记为红色。2、发现X的Parent§同样为红色,违反了红黑树的第三条规则。3、发现X的Uncle(U)同样为红色。4、将P和U标记为黑色。5、将X和X的GrandParent(G)标记为相同的颜色,即红色。重复2,3。6、发现G是根节点,标记为黑色。7、结束。
叔叔为黑:
3、如果X的叔叔Uncle(U)是黑色的,我们要分四种情况处理。
3.1、LL,P是G的左孩子,并且X是P的左孩子。
3.1、LR,P是G的左孩子,并且X是P的右孩子。
3.1、RR,P是G的右孩子,并且X是P的右孩子。
3.1、RL,P是G的右孩子,并且X是P的左孩子。
3.2、LL:右旋; RR:右旋; RL:右旋-左旋; LR:左旋-右旋。

六、B树

特点:也叫平衡多路查找树,B树应用到数据库中的时候,数据库充分利用了磁盘的原理(磁盘数据存储采用块的形式存储,每个块大小为4K,每次IO读取时,用一个磁盘块可以一次性读取出来),把节点大小限制和充分使用在磁盘块大小范围。把树的节点关键字增多后层级比原来少了,减少数据查找的次数和复杂度。
应用:数据库索引技术,文件系统。

七、B+树

特点:B+树是B树的升级版,B+树更加充分的利用了节点空间,让查询速度更加稳定,速度完全接近二分查找。
应用:数据库,文件系统,区间访问。
优点:
1、每个非叶子节点存储的关键字数更多,树的层级更少,查询速度快。
2、查询速度稳定,所有关键字都在叶子节点上,每次查找次数相同,所以查询速度比B+树稳定。
3、B+树天然具备排序功能:B+树所有叶子节点数据构成了一个有序链表,在查询大小区间的数据时更方便,数据紧密性很高,缓存的命中率比B树高。
4、B+树全节点遍历更快,B+树便利整棵树只需遍历所有叶子节点即可,不需要像B树一样对每一层进行遍历,这有利于数据库做全表扫描。
5、B树相当于对B+树的优点是,如果经常访问数据离根节点很近,而B树的非叶子节点本身存有关键字机器数据的地址,所以这种检索比B+树快。
规则:
1、B+树的非叶子节点不保存关键字记录的指针,只进行数据索引,这样使得B+树每个非叶子节点所能保存的关键字大大增加。
2、B+树叶子节点保存了父节点的所有关键字记录的指针,所有数据地址必须要到叶子节点才能获取到,所以每次查询次数都一样。
3、B+树叶子结点的关键字从小到大有序排列,左边结尾数据都会保存右边节点开始数据的指针。
4、非叶子节点的子节点数=关键字数,MySQL采用B+树实现。

七、哈夫曼树

定义:当用N个节点(都做叶子节点且都有各自的权值)视图构建一棵树时,如果构建这棵树的带权路径长度最小,则称其为最优二叉树,也叫哈夫曼树。哈夫曼树中权重越大的节点离根节点越近。
应用:最小代价问题的决策。哈夫曼编码,数据压缩,加密解密,文件传输。

八、堆

性质1:堆是一颗完全二叉树,也成为二叉堆。堆中任意节点总是不大于/不小于其子节点的值。
性质2:由于堆是完全二叉树,因此根据二叉树的性质,完全二叉树能够完美的映射到数组结构中去。大顶堆:arr[i]>=arr[2i+1]&&arr[i]>=arr[2i+2];小顶堆:arr[i]<=arr[2i+1]&&arr[i]<=arr[2i+2];因此常常用数组来实现堆结构.
性质3:由于堆算作一个偏序,只有父节点和子节点的大小关系,没有两个子节点之间的大小关系,因此同一批元素采用不同算法构建成的堆在数组中实际存储顺序是不一定相同的,并且堆排序也是一种不稳定的排序算法。
应用:堆常被用于实现优先队列,优先队列可以自由添加数据,但取出数据时要从最小值开始按顺序取出。堆还用于实现堆排序。

九、栈

应用:数的任意进制转换,括号匹配检验,表达式求值,二叉树遍历等。

十、队列

应用:利用其先进先出的特性,解决具体业务场景如消息通知,订单处理,异步处理等。

十一、哈希表

一、HashMap
应用:适合查找性能要求高,数据元素之间无逻辑关系的情况。
定义:底层是一个数组结构,数组中每一项对应一个链表,这种结构称为链表散列,也叫散列表,哈希表。
存储过程:
1、对HashMap的Key调用HashCode方法,返回int值,即对应的HashCode。
2、把此HashCode作为哈希表的索引,查找哈希表的相应位置,若当前位置为空,则把HashMap的KeyValue包装成Entry数组,放入当前位置。
3、若当前位置不为空,则继续查找当前索引处存放的链表,利用Equals方法,找到Key相同的Entry数组,替换其Value值。
4、若未找到相同的Key,则把当前位置的链表后移,把新的Entry数组放到链表表头。
二、HashSet
定义:HashSet是通过HashMap来实现的,HashMap输入参数由KeyValue两个组成,再实现HashSet的时候,保持HashMap只对其Key对象进行处理。
存储过程:
1、HashSet会先调用元素的HashCode方法获得元素的哈希值。
2、然后通过哈希值经过移位等运算,算出在哈希表中的存储位置。
3、如果当前位置没有任何元素存储,那么该元素可以直接放到该位置上。
4、如果有其他元素了,那么会调用equals方法与该位置元素进行比较,如果返回true,则视为重复元素,不允许添加,如果返回fasle,那么允许添加。
三、哈希冲突的解决方法
原因:不同的key经过哈希函数计算后得到相同的值。
1、开散列法:哈希表所基于的数组中,每个位置是一个linkedlist的头节点,这样冲突的二元组都会放在同一个链表中。
2、闭散列法:在发生冲突时,后来的元素会往下一个位置去找空位。
四、哈希表的扩容
原因:随着数量增多,哈希表中的每个元素会越来越长,这样效率会大大降低。
扩容:将哈希表的存储空间增加为原来的两倍,此时需要将所有数据进行修改(重新调用哈希函数,来获取新的位置)。

十二、图

图的存储
一、邻接矩阵
定义:图的邻接矩阵存储方式是用两个数组来表示图,一个一维数组存储图中顶点信息,一个二维数组存储图中的边的信息。在这里插入图片描述
注意:
1、再简单应用中可以直接用二维数组作为图的邻接矩阵。
2、当邻接矩阵中的元素仅表示相应的边是否存在时,可定义值为0或1的枚举类型。
3、无向图的邻接矩阵是对称矩阵,对规模大的邻接矩阵可以采用压缩矩阵。
4、邻接矩阵表示法的空间复杂度事O(n2),其中n为图的顶点数|V|.
5、用邻接矩阵法存储图,很容易确定图中任意两个顶点之间是否有边相连。但是要确定途中有多少条边,则必须按行,列对每个元素进行检测,所花费的时间代价很大。
6、稠密图适合用邻接矩阵的存储表示。

二、邻接表

概念:邻接表指图中每个顶点vi建立一个单链表,低i个单链表中的节点表示依附于顶点vi的边,这个单链表就成为顶点vi的边表。边表的头指针和顶点的数据信息采用顺序存储,所以在邻接表中存在两种节点:顶点表节点和边表节点。顶点表节点由顶点域和只想第一条邻接边的指针构成,边表节点由邻接点域和指向下一条邻接边的指针域构成。
在这里插入图片描述
在这里插入图片描述

特点:
1、若图G为无向图,则所需的存储空间为O(|V|+2|E|);若G为有向图则存储空间为O(|V|+|E|),前者的倍数2是由于无向图中每条边会在邻接表中出现两次。
2、对于系数图,采用邻接表表示将极大的节省存储空间。
3、在邻接表中,给定一顶点,能很容易地找出他所有临边,因为只需要读取他的邻接表。在邻接矩阵中,相同的操作则需要扫描每一行,花费的时间为O(N),但是若要确定给定两个顶点间是否存在边,则在邻接矩阵中可以立即查到,而在邻接表中则需要在相应节点对应的边中查找另一节点,效率较低。
4、在有向图的邻接表表示中,求一个给定顶点的出度只需计算其邻接表中的节点个数,但求其顶点的入度则需要遍历全部的邻接表,因此 可以采用逆邻接表法存储。
5、图的邻接表表示并不唯一,因为在每个顶点对应的邻接表中,各边节点的连接次序可以是任意的,它取决于建立邻接表的算法及边的输入次序。

三、十字链表
定义:十字链表是有向图的一种链式存储结构,对于有向图而言,邻接表是有缺陷的。关心了出度问题,入读就必须遍历整个图才能知道。十字链表很好的解决了这个问题。定点结构如下图所示,其中firstin表示入边表头指针,指向该顶点的入边表中的第一个顶点。firstout表示出边表头指针,指向该顶点出边表中的第一个节点。边表节点如下如是,其中tailvex是指弧起点在顶点表的下标,headvex是指弧终点在顶点表的下标,headlink是指入边表指针域,指向终点相同的下一条边,taillink是指出边表指针域,只想起点相同的下一条边。在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
十字链表的好处是因为把邻接表和逆邻接表整合在了一起,这样既容易找到以v1结尾的弧,也容易找到以v1为头的弧,因而容易求得顶点的出度和入度。而且他除了结构复杂矮一点,其实创建图算法的时间复杂度和邻接表也是相同的。因此在有向图的应用中,十字链表是非常好的数据结构模型。
四、边集数组
定义:边集数组是由两个一维数组组成,一个存储顶点的信息,另一个是存储边的信息,这个便数组每个数据元素由一条边的起点下标,终点下标和权重组成。如下图所示,在边集数组中查找一个顶点需要扫描整个边数组,效率并不高,因此它更适合对边一次操作进行处理的操作,而不适合对顶点相关的操作。在这里插入图片描述

五、图的遍历
1、DFS算法性能:DFS需要一个 递归的工作站,故空间复杂度为O(v),对于n个顶点e条边的图来说,邻接矩阵由于是二维数组,要查找每个顶点的邻接点需要访问数组中的所有元素,因此都需要O(V2)的时间。而邻接表做存储结构时,找邻接点所需的时间取决于顶点和边的数量了所以是O(v+E),显然对于点多边少的稀疏图来说,邻接表结构使得算法在时间效率上大大提高,对于有向图而言,由于它只是对通道存在可行或不可行,算法上没有变化,是完全可以通用的。
2、深度优先的生成树和生成森林
深度优先搜索会产生一颗优先生成树,对连通图调用DFS才会产生优先生成树,否则将会产生优先生成森林。
3、BFS算法性能分析
无论是邻接表还是邻接矩阵的存储方式,BFS算法都需要借助一个辅助队列Q,N个顶点均需入队一次,在最坏的情况下,空间复杂度为O(v).采用邻接表存储方式时,每个顶点均需搜索一次,在搜索任意顶点的邻接点时,每条边至少访问一次,算法的总时间复杂度为O(E+V),采用邻接矩阵存储方式时,查找每个顶点的邻接点所需的时间为O(V),故总的为O(V2).
注意:图的邻接矩阵表示是唯一的,但对于邻接表来说,若边的输入次序不同,生成的邻接表也不同,因此对于同样一个图,基于邻接矩阵的遍历所得到的DFS和BFS序列是唯一的,基于邻接表遍历的DFS和BFS序列是不唯一的。
4、通过图的遍历判断图的连通性
对于无向图而言,若无向图是联通的,则从任意节点出发,仅需一次遍历就能访问所有节点。若无相图示非联通的,则从任意节点出发,仅需一次遍历就能访问所有节点。若无相图示非联通的,则从一个顶点出发,依次遍历只能访问该顶点所在连通分量的所有顶点,而对于途中其他连通分量的顶点,无法通过这次遍历访问。对于有向图来说,若从初始点到图中每个顶点都有路径,则能够轻易访问到图中所有顶点,否则不能。所以在BFSTraverse()和DFSTraverse()中添加了第二个for循环,再选取初始点,继续进行遍历,以防止无法遍历图的所有顶点。对于无向图,上述两个函数调用次数等于连通分量数;对于有向图不是这样的,因为一个联通的有向图分为强连通和非强连通,他的连通子图也分为强连通分量和非强连通分量,非强连通分量依次调用DFS和BFS无法访问到该连通分量的所有顶点。

六、最小生成树
一个连通图的生成树是一个极小的连通子图,它含有图中的全部顶点,但只足以构成一棵树的n-1条边,若砍去他的一条边,则会使生成树变成非连通图,若给他增加一条鞭,则会形成图中的一条回路。对于一个带全连通无向图G=(V,E),生成树不同,其中的边的权值之和最小的那个书,称为G的最小生成树。
构造最小生成树有多种算法,但大多数算法都利用了最小生成树的下列性质:假设G=(V,E)是一个带权联通无向图,U是顶点集V的一个非空子集,若(u,v)是一条最小权值的边,其中u∈U,v∈V-U,则必存在一棵树包含(u,v)的最小生成树。基于该性质的最小生成树算法主要有prim算法和kruskal算法,他们都是基于贪心算法的策略。
1、普利姆算法(Prim)
通俗点说就是:从一个顶点出发,在保证不形成回路的前提下,每找到并添加一条最短的边,就把当前形成的连通分量当作一个整体或者一个点看待,然后重复找最短的边并添加的操作。
在这里插入图片描述
2、克鲁斯卡尔算法(Kruskal)
Kruskal算法构造最小生成树的过程如下图所示。初始时为只有n个顶点的无边的非连通图T=V,每个顶点自成一格连通分量,然后按照边的权值由小到大排序,不断选取当前未被选取过的权值最小的边,若该边依附的顶点落在T中不同的连通分量上,则将此边加入T,否则舍弃此边而选择下一条权值最小的边。以此类推,直至T中所有顶点都在一个连通分量上。在这里插入图片描述
算法思路:我们直接就以边目标去构建,因为权值在边上,直接去找最小权值的边来构建生成树也是自然的想法,只不过构建时要考虑是否会形成环路而已。此时我们就用到了图的存储结构中的边集数组结构。
七、最短路径
在网图和非网图中,最短路径的含义是不同的,由于非网图他没有边上的权值,所谓的最短路径,其实就是指两顶点之间经过的边数最少的路径,而对于网图来说,最短路径是指两顶点之间经过的边上权值之和最少的路径,并且我们称路径上的第一个顶点是原点,最后一个顶点是终点。
1、迪杰斯特拉算法(Dijkstra)
Dijkstra算法用于构建单源点的最短路径,即图中某个点到达任意其他点的距离都是最短的,例如构建地图应用时查找自己的坐标离某个地标的最短距离。可用于有向图,但不能存在负权值。一下图为例,通途点说,这个算法他并不是一下子求出了v0到v8的最短路径,而是一步步求出他们之间顶点的最短路径,过程中都是基于已经求出的最短路径的基础上,求得更远顶点的最短路径,最终得到结果。
DIjkstra算法也是基于贪心策略的,使用邻接矩阵或带权的邻接表表示时,时间复杂度为O(V2).人么可能只希望找到从源点到某个特定顶点的最短路径,但这个问题和求解原点到其他所有顶点的最短路径一样复杂,时间复杂度也为O(V2).
2、弗洛伊德算法(Floyd)
弗洛伊德算法的思路是:首先初始化距离矩阵,然后从第一个点开始逐渐更新矩阵点值。d[i][j]表示从i点到j点的距离。第k次更新时,判断d[i][k]+d[k][j]与d[i][j]的大小,如果前者小,则更新这个值,否则不变。
状态转移方程:map[i,j]:=min{map[i,k]+map[k,j],map[i,j]};

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_44802051/article/details/121824062