数组与链表的应用—链表基础原理
Posted gxkeven
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数组与链表的应用—链表基础原理相关的知识,希望对你有一定的参考价值。
链表(Lined List)
内存管理器
当我们使用高级语言创建一个数组的时候,实际上试讲这个指令传达给了操作系统里面的内存管理器(Memory Manager),内存管理器在收到指令后会在内存中分配一块相应大小的连续存储空间给这个数组。
例如:当想创建大小为5的数组时,内存管理器可能会从0x80000000这个地址开始分配一个连续的内存块给我们,我们便可以操作这个数组了,入下图所示:
而因为这个数组的大小一开始已经被确定好的,所以当我们想要增加一个元素的时候,内存管理器无法满足这个要求,只能创建一个新的大小为6的数组。然后将原来为5的数组的值一一复制到新的数组中去,再对新的元素进行赋值。假设内存管理器将新的数组起始地址设在了0x80000018这里,那这个时候的内存如下图所示:
所以我们看到,每次增加一个元素的时候,底层的内存管理器必须重新为我们分配一次内存,我们自己还必须将原来数组上的元素重新复制一遍到新的数组上。那有没有办法当每次增加元素的时候能减少这种内存上的消耗呢?其实可以想到的是当每次新增加一个元素的时候让内存管理器不要分配一段连续的内存空间就可以了。
假设我么这时候我们要保存3个元素,但是我们想告诉系统的内存管理器不需要一次分配3个连续的字节内存空间出来,而是当我们声明它们的时候才分配一个相应的内存空间出来。假设内存管理器所分配的内存空间如下图所示:
这三个元素别分配在了非相邻的内存空间里,但从上图中我们可以知道这三个元素各自的地址,所以想要遍历它们的话,我们需要保存一些额外的信息,比如,下一个能遍历到的元素地址。这样的话,每一个元素就保存了两部分内容,一部分是元素本身的值,另一部分是下一个元素的地址,而最后一个元素的下一个地址我们可以保存一个0x0的值来表示这个元素是最后一个了。这时候这个数据的内存就如下图所示:
从上图可以看到,尽管这些元素不是分配在连续的空间里,但是通过保存额外的信息让一个元素可以链接到下一个元素上,只要知道了这些数据中的第一个元素,便可以将全部的元素都遍历一遍。在计算机里,这种不保存在连续存储空间中。而每一个元素都保存了到下一个元素地址的数据结构,我们称之为链表(Linked List)。链表上的每一个元素又可以称它为节点(Node),而链表中的第一个元素,称它为头节点(Head Node),最后一个元素称它为尾结点(Tail Node)。
顺序访问
与数组不同的是,我们无法使用一个固定的公式来直接算出某一个特定元素的地址,从而得到那个元素的值。要找到链表中的第N个元素,我们必须从第一个元素开始,一个一个的遍历N次才能找到第N个元素,这种访问模式,我们就称之为顺序访问(Sequential Access)。
当我们需要在链表的结尾处需要增加一个元素的时候应该怎么办呢?很简单,只需要新创建一个链表的节点,然后将尾结点中保存的地址值更新成新的节点地址便可。
假设我们原有的链表内存如图下所示:
当我们要插入一个值为4的新节点进入这个链表的时候,系统的内存管理器会分配一个新的内存空间给我们,然后我们再将值为3的这个尾结点的地址更新为它的地址,如下图所示:
数组与链表的性能差异
空间的利用率
首先我们一起看看在空间利用率上的对比。因为数组在创建后大小是无法改变的,想要增加元素的话就必须重新创建一个新的数组。所以有的时候,为了能够动态增加元素,我们会在一开始创建数组的时候声明一个比我们本来需要的大小还多的空间出来,以便后面添加新的元素。这个时候就会造成空间上的浪费,所以,数组的空间利用率相当于本来需要的大小除于创建出来数组的大小。
而因为链表中的元素只有当被需要的时候才会被创建出来,所以不存在需要多预留空间的情况。对于我们工程师来说只有节点中的值是可以利用上的,而保存节点地址的内存其实对于我们来说是无法应用的。所以,链表的空间利用率相当于值的大小除于值的大小和节点地址大小的和。
在我们上面所举的例子中,因为链表中保存的值只是一个int值,和节点地址大小一样,所以在空间利用率上只有50%。但是在现实应用中,链表中保存的值远远不是一个基本类型这么简单,当我们所保存的值的大小越大的时候,空间利用率也会越高。
时间复杂度
在数组的章节中,我们知道了访问数组元素的时间复杂度是O(1)。而因为链表顺序访问这个特性,访问链表中第N个元素需要从第一个元素一直遍历到第N个元素,所以平均下来的时间复杂度是O(N)。
对于数组来说,插入操作无论发生在数组的结尾还是发生在数组的中间,因为都需要重新创建一个新的数组出来,并复制一遍之前的元素到新的数组中,所以平均的时间复杂度是O(N)。而对于链表来说,要是我们一直都能维护一个尾结点地址的话,那么插入一个新的元素,只需要O(1)的时间复杂度。而当插入一个元素到链表中间的时候,因为链表顺序访问的这个特性,我们需要先遍历一遍链表,从第一的节点开始直到第N个位置,然后再进行插入,所以平均下来的时间复杂度是O(N)。
链表的各种形式
单向链表
在上面所举的例子当中,你可能已经发现,所有的链表节点中都只保存了指向下一个地址节点的信息。这种在一个节点即保存我们需要的数据也保存了指向下一节点地址信息的链表,称之为单向链表(Singly LinKedList)。抽象的数据如下图所示:
双向链表
单向链表有着只能朝一个方向遍历的局限性,既然我们能保存指向下一个节点的地址信息,也可以保存指向上一个节点地址的信息。这种在一个节点中保存了我们需要的数据也保存了下一个和上一个节点地址信息的链表,称之为双向链表(Doubly LinkedList)。和链表中的尾结点下一个节点只保存空地址一样,链表中的头结点上一个节点也保存这空地址,抽象的数据就如下图所示:
循环链表
无论是单向链表或者是双向链表,当我们遍历至尾结点后都无法在遍历下去,如果将尾结点指向下一个节点地址的信息更新成指向头结点的话,这样整个链表就形成了一个环,这种链表称之为循环链表(Circular Linked List),抽象的数据结构图,如下图所示:
以上是关于数组与链表的应用—链表基础原理的主要内容,如果未能解决你的问题,请参考以下文章