数据结构(自用)

数组与链表

数组

数组一般存储相同类型的数据,由连续的内存进行存储

数组非尾部的插入和删除比较复杂,因为还涉及其他所有项的移位

数组无法动态扩容(大小无法改变)

我们把数组称为顺序表

链表

链表查询的特点是从头开始依次(顺着指针域)查询,导致了效率低;链表的插入很快,但搜寻就很慢

单链表的删除需要知道:要删除的结点和上一个结点(前驱),需要对前驱的指针进行变更

头结点相当于一个固定的入口,不存储有意义的值

小总结

从下面可以得知,当进行查询操作用数组,插入和删除操作用链表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xITLiVIK-1642997213072)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220121154133309.png)]

跳表

跳表(SkipList,全称跳跃表)是用于有序元素序列快速搜索查找的一个数据结构

关键词:链表(有序链表)、索引、二分查找

很多算法和数据结构以空间换时间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V0Nd5wFR-1642997213073)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220121160404654.png)]

跳表的指针不再是next了,一般我们用指针right和down进行区分

与链表相比较,让查询的时间复杂度从O(n)提升至O(logn)。

stack的第一种含义是一组数据的存放方式,特点为LIFO,即后进先出(Last in, first out)。

stack的第三种含义是存放数据的一种内存区域。程序运行的时候,需要内存空间存放数据。一般来说,系统会划分出两种不同的内存空间:一种叫做stack(栈),另一种叫做heap(堆);

​ 它们的主要区别是:stack是有结构的,每个区块按照一定次序存放,可以明确知道每个区块的大小;heap是没有结构的,数据可以任意存放。因此,stack的寻址速度要快于heap。

​ 其他的区别还有,一般来说,每个线程分配一个stack,每个进程分配一个heap,也就是说,stack是线程独占的,heap是线程共用的。此外,stack创建的时候,大小是确定的,数据超过这个大小,就发生stack overflow错误,而heap的大小是不确定的,需要的话可以不断增加。

​ 根据上面这些区别,数据存放的规则是:只要是局部的、占用空间确定的数据,一般都存放在stack里面,否则就放在heap里面。

​ 一般来说,内存泄漏都发生在heap,即某些内存空间不再被使用了,却因为种种原因,没有被系统回收。

队列

先进先出,就跟现实生活中的“排队”一样

树主要是与图进行比较的,树更具有逻辑性,关系更加一目了然

树好像很高级,其实可看作是链表的高配版;树的实现就是对链表的指针域进行了扩充,增加了多个地址指向子结点。

若将树的指针域设置为双指针,那么即可形成最常见的二叉树;二叉树根据结点的排列和数量还可进一度划分为完全二叉树、满二叉树、平衡二叉树、红黑树等。

完全二叉树

完全二叉树:除了最后一层结点,其它层的结点数都达到了最大值;同时最后一层的结点都是按照从左到右依次排布。(最下面一行可以不满)

满二叉树

满二叉树:除了最后一层,其它层的结点都有两个子结点。(最下面一行也必须满)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mDpT4sKq-1642997213075)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124110519916.png)]

二叉排序树

二叉排序树:(Binary Sort Tree)又称二叉查找树、二叉搜索树。它具有的性质是一棵空树或者:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉排序树。

二叉排序树意味着二叉树中的数据是排好序的,顺序为左结点<根结点<右结点,这表明二叉排序树的中序遍历结果是有序的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0JfnQ6PD-1642997213075)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124110733139.png)]

平衡二叉树

平衡二叉树:又被称为AVL树,它是一棵二叉排序树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

平衡二叉排序树是一类特殊的二叉排序树

★发明平衡二叉树是为了避免出现二叉排序树中线性的情况;为了保证相对平衡,每次插入元素都会做相应的旋转

树的高度:结点层次的最大值

平衡因子:左子树高度 - 右子树高度

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bor9mjq2-1642997213076)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124110656088.png)]

1 平衡二叉树与非平衡二叉树

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-entFcUb3-1642997213077)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124112652890.png)]

2 平衡调整

在构造平衡二叉树时,却需要采用不同的调整方式,使得二叉树在插入数据后保持平衡

主要的四种调整方式有LL(左旋)、RR(右旋)、LR(先左旋再右旋)、RL(先右旋再左旋)

是否平衡的判据:在插入一个结点后应该沿搜索路径将路径上的结点平衡因子进行修改,当平衡因子大于1时,就需要进行平衡化处理

从发生不平衡的结点起,沿刚才回溯的路径取直接下两层的结点,如果这三个结点在一条直线上,则采用单旋转进行平衡化,如果这三个结点位于一条折线上,则采用双旋转进行平衡化。

LL(左旋)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K9epJuGm-1642997213078)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124113655421.png)]

RR(右旋)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g7w1WDPw-1642997213079)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124113724012.png)]

先介绍下简单的单旋转操作,左旋和右旋。LR和RL本质上只是LL和RR的组合。

LR:先左旋后右旋 RL:先右旋后左旋

这些旋转的关键是找到父结点和子结点,对应的孩子进行转换

哈夫曼树

哈夫曼树:也叫做最优二叉树,一种带权路径长度最短的二叉树。树的带权路径长度是树中所有的叶子结点的权值乘上其根结点的路径长度。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XZBqxtY6-1642997213079)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124112136569.png)]


红黑树

平衡二叉树(AVL)为了追求高度平衡,需要通过平衡处理使得左右子树的高度差必须小于等于1。高度平衡带来的好处是能够提供更高的搜索效率,其最坏的查找时间复杂度都是O(logN)。

但是由于需要维持这份高度平衡,所付出的代价就是当对树种结点进行插入和删除时,需要经过多次旋转实现复衡。这导致AVL的插入和删除效率并不高。

为了解决这样的问题,找了一种结构能够兼顾搜索和插入删除的效率,它就是红黑树。

红黑树属性:

  1. 每个结点要么是红的要么是黑的。

  2. 根结点是黑的。

  3. 每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。(NIL结点意为空结点)

  4. 如果一个结点是红的,那么它的两个儿子都是黑的。

  5. 对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2jUvHKxT-1642997213080)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124114119279.png)]

黑树通过将结点进行红黑着色,使得原本高度平衡的树结构被稍微打乱,平衡程度降低。

红黑树不追求完全平衡,只要求达到部分平衡。

这是一种折中的方案,大大提高了结点删除和插入的效率。

红黑树VS平衡二叉树

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kF1del9V-1642997213081)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124114247786.png)]

堆通常是一个可以被看做一棵树的数组对象。

堆的具体实现一般不通过指针域,而是通过构建一个一维数组与二叉树的父子结点进行对应,因此堆总是一颗完全二叉树。

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RI1NmuWF-1642997213082)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124114824990.png)]

堆常用来实现优先队列

散列表(哈希表)

是一种通过键值对直接访问数据的机构。

散列表的实现原理就是映射的原理,通过设定的一个关键字和一个映射函数,就可以直接获得访问数据的地址,实现O(1)的数据访问效率。

在映射的过程中,事先设定的函数就是一个映射表,也可以称作散列函数或者哈希函数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lyYLhscN-1642997213082)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124115051759.png)]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nauPjE4P-1642997213083)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124115232545.png)]

图一般包括顶点和边,顶点通常用圆圈来表示,边就是这些圆圈之间的连线。

边还可以根据顶点之间的关系设置不同的权重,默认权重相同皆为1。

此外根据边的方向性,还可将图分为有向图和无向图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0U1apRDl-1642997213084)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124115338912.png)]

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

邻接矩阵

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-paurkFSG-1642997213085)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124115431811.png)]

无向图的邻接矩阵是一个对称矩阵,是因为边不具有方向性,若能从此顶点能够到达彼顶点,那么彼顶点自然也能够达到此顶点。

此外,由于顶点本身与本身相连没有意义,所以在邻接矩阵中对角线上皆为0。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LpNGJz9L-1642997213086)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124115512157.png)]

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

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

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

邻接表

为了解决邻近矩阵很多0,太稀疏的问题从而引出邻接表

在邻接表中,图的每一个顶点都是一个链表的头节点,其后连接着该顶点能够直接达到的相邻顶点。

相较于无向图,有向图的情况更为复杂,因此这里采用有向图进行实例分析。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v3acOsTg-1642997213087)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124115730794.png)]

· 邻接表中,每一个顶点都对应着一条链表,链表中存储的是顶点能够达到的相邻顶点。

​ 存储的顺序可以按照顶点的编号顺序进行。

​ 比如上图中对于顶点B来说,其通过有向边可以到达顶点A和顶点E,那么其对应的邻接表中的顺序即B->A->E,其它顶点亦如此。

· 通过邻接表可以获得从某个顶点出发能够到达的顶点,从而省去了对不相连顶点的存储空间。

​ 然而,这还不够。对于有向图而言,图中有效信息除了从顶点“指出去”的信息,还包括从别的顶点“指进来”的信息。

​ 这里的“指出去”和“指进来”可以用出度和入度来表示。

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

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

由此看出,在对有向图进行表示时,邻接表只能求出图的出度,而无法求出入度。

这个问题很好解决,那就是增加一个表用来存储能够到达某个顶点的相邻顶点。这个表称作逆邻接表。

逆邻接表

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BFKXLvSX-1642997213087)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124120138640.png)]

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

十字链表

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vv73tf0y-1642997213088)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124120203310.png)]

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

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

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

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

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

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

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

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ywvx7xgZ-1642997213089)(C:\Users\92992\AppData\Roaming\Typora\typora-user-images\image-20220124120321110.png)]

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

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

总结

图的内容主要是从网上找的,因为自己在学习那一部分没太学懂,前面的数组、链表、队列、树是有一些自己的想法,这些主要是理论知识,没有太涉及代码的实现,如果要代码可以去程序羊的个人博客的算法部分找。

Road 2 Coding (r2coding.com) (搜索数据结构)

参考资料

24张图,九大数据结构安排得明明白白! (qq.com)

数组 vs 链表,到底哪个性能更强? (qq.com)

https://mp.weixin.qq.com/s/czkZcQL8mEqG2xeX8huqsA (跳表)

Stack的三种含义 - 阮一峰的网络日志 (ruanyifeng.com)

数据结构里各种难啃的“树”,一文搞懂它 (qq.com)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值