33

数据结构之链表

 5 years ago
source link: https://blog.csdn.net/mingyunxiaohai/article/details/85945547?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

链表和数组一样,都是编程语言中经常用到的数据结构。

相比数组,链表是一种稍微复杂一点的数据结构,我们可以两者对比着来看

数组需要一块 连续的内存空间 来存储数据,对内存的要求比较高,如果我们申请一个100MB大小的数组,如果内存中没有一个连续的100MB的空间,即使内存中剩余的空间大小大于100MB,也会申请失败

链表就不一样了,它不需要一块连续的内存空间,它是 通过‘指针’把一组零散的内存块串联起来 所以如果剩余内存如果大于100MB,即使不是连续的也没问题。

链表有很多种,比较常见的是:单链表、双向链表、循环链表。

单链表:

qQ3i22n.png!web

链表是通过指针把一组零散的内存块串联起来,我们把内存块称为链表的‘结点’为了把所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址。我们把这个记录下一个结点的地址的指针叫 做‘后继指针 next’

从上面的图中可以看到,有两个结点是特殊的,那就是第一个结点和最后一个结点,即 头结点尾结点 ,头结点用来记录地阿里表的基地址,有了它我们就可以遍历得到整条链表,而为节点最后指向的不是下一个结点,而是 一个空的地址null

和数组一样,链表也支持数据的查找、删除、插入操作。不过执行的效率他们之间是不同的,对于插入和删除操作,链表的效率要高,对于根据下标查询操作,数组的效率就高了。

从上一篇数组我们知道,数组在插入和删除数据的时候,为了保证内存数据的连续性,需要做大量的数据搬移,而在链表中插入或者删除一个数据,我们并不需要保证内存的连续性,也就不用进行大量的数据搬移,因为链表的存储空间本来就不是连续的。我们只需要考虑相邻的结点的指针改变就好了,所以删除和插入操作在链表中是非常快速的。

但是链表想要随机访问第k个元素,就没有数组那么高效了,因为链表中的数据并不是连续的,所以不能像数组那样,根据首地址和下标,通过寻址公式就能直接计算出对应的内存地址,而是需要根据指针一个结点一个结点的依次遍历,直到找到相应的结点。

我们可以把链表想象成一个队伍,每个人都只直到自己的后面是谁,所以,当我们想要直到排在第N个位置的人是谁的时候,只能从第一个开始一个一个往下数。

循环链表:

zyI7Nrr.png!web

循环链表是一种特殊的单链表,实际上,循环链表也很简单,它跟单链表唯一的区别就是它的尾结点,单链表中是指向一个空地址,表示这是最后一个结点了,而循环链表的尾结点指向链表的头结点。

和单链表相比,它的优点就是从链尾到链头比较方便,当要处理的数据具有环形结构特点的时候,适合采用循环链表,比如 约瑟夫问题

双向链表:

uUjEruy.png!web

跟单链表只有一个方向不同,双向链表支持两个方向,每个节点上除了有一个‘后继指针next’指向后面的结点外,还有一个‘前驱指针prev’指向前面的结点。

双向链表需要额外的空间来来存储后继结点和前驱结点的地址。所以如果存储同样多的数据,双向链表要不单链表占用更多的内存空间。虽然两个指针比较浪费空间,但是可以支持双向遍历,这样也带来了操作上的灵活性。

从表结构上看,双向链表既知道前驱结点也知道后继结点,插入和删除结点等操作在某些情况下比单链表更加简单高效。

那吗些地方会更高效呢

比如删除操作,一般我们删除一个数据无外乎两种情况:

一个是删除节点中值等于某个给定值的结点,

一个是删除给定指针指向的结点

第一种情况,不管是单向还是双向链表,其实都是一样的,都得从头结点开始一个一个的遍历比对,直到找到相对应的值,然后杉树。

第二种情况就不一样了,我们已经找到了要删除的结点,但是删除某个结点,我们需要直到其前驱结点,让其前驱结点的next指向其后继结点的prve,但是单链表不能直接获取其前驱结点,为了找到其前驱结点,还得从头开始遍历链表。但是双向链表就不同了,双向链表中的结点已经保存了其前驱接单的指针,不需要再遍历查找了。所以这种情况下双向链表效率比单向的高。

同理当我们想要插入一个元素的时候,跟上面的删除操作一样,第二种情况下,双向链表更有优势。

对于一个有序的链表来说,双向链表的按值查询的效率也比单链表的要高,因为我们可以记录上次查找的位置p,下次查询的时候,根据查找的值于p的值比较来决定往前查找还是往后查找,而单链表就只能往后查找。

从上面的分析我们知道,双向链表在很多时候都比单链表效率高,这也是为什么实际的软件开发中,双向链表尽管比较费内存,但是比单链表应用更加广泛的原因。java中LinkedHashMap这个容器,其内部原理就是一个双向链表。

这里有一个用时间换空间的设计思想,当内存空间充足的时候,如果我们追求更快的代码执行速度,就可以选择空间复杂度交高时间复杂度较低的算法结构,反之,如果内存紧缺,就可以选择用时间换空间的设计思路了。

实际上缓存就是一个利用空间换时间的设计思想,如果我们把数据存储在硬盘上会比较节省内存,但是每次查找数据访问硬盘会比较慢,如果我们通过缓存技术,事先把数据加载到内存中,虽然会耗费一定的内存空间,但是每次查询数据的时间就快了。

双向循环链表:

双向链表结构如下图

VJriYvZ.png!web

使用链表实现一个LRU缓存淘汰算法:

维护一个有序的单链表,最早访问的放在尾部,当有一个新的数据被访问的时候,从链表的头部开始遍历链表:

(1)遍历此链表,如果此数据之前已经被缓存在链表中了,就删除它,然后把新的放在链表头部。

(2)遍历链表,如果数据没有在缓存链表中

<2.1>如果缓存未满,将此节点直接插入到链表的头部

<2.2>如果缓存满了,删除尾部结点,把新的数据插入到链表的头部。

OK这样就使用链表实现了一个LRU缓存。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK