数组和链表

数组

数组是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。

  • 线性表
    表上的数据最多之后前后两个方向。常见的线性表结构有:数组、链表、队列、栈等

  • 连续的内存空间和相同类型的数据
    因为这两个限制,使得数组的“随机访问”相对高效。
    带来的弊端是增删操作变得非常低效。原因是,为了保证连续性,需要做大量的数据搬移工作。

(1)数组适合查找操作,但是要说查找的时间复杂度是 O(1),是不严谨的。比如排好序的数组,使用二分查找的时间复杂度为 O(logn)。严谨的说法是,根据下标随机访问的时间复杂度为 O(1)。

根据下标随机访问高效的原因:
因为数组的内存空间是连续的,并且每个元素占用的内存大小相同(数据类型相同),所以通过公式很容易计算出指定元素的地址。

a[i]_address = base_address + i * data_type_size

a[i]_address : i 号元素的内存地址
base_address : 内存块的首地址
data_type_size : 数组中每个元素的大小。例:int 类型,就是4字节

(2)增删操作效率低的改进办法:

  • 增:假如需要向第 k 个位置插入数据。
    如果数据没有排序或者其他规律,直接将第 k 个位置上的数据拿出来,放到末尾,然后将新的数据放在这个位置上。

  • 删:假如要删除第 k 个位置的数据
    将多次删除集中在一起执行。
    具体操作是,每次对需要删除的数据做标记,当数组空间不足时再真正触发删除操作,从而大大减少数据搬移。这和 JVM 标记清除垃圾回收算法的核心思想一致。

(3)容器能否完全代替数组?

  • 容器的优点:
    1.将数组操作的细节封装了起来
    2.支持动态扩展
    因为数据扩展涉及内存申请和数据搬移,比较耗时,所以如果实现确定需要存储的数据大小,最好在创建的时候事先指定数据大小。

  • 容器的缺点:无法存储基本类型,装箱和拆箱有一定的性能消耗,如果特别关注性能,可以使用数组。


链表

链表与数组的区别是,它不像数组那样使用连续的内存空间,而是通过“指针”将一组零散的内存块串联起来。

常见的链表有:单链表、双向链表和循环链表等。

一、

(1)单链表
单链表.png

链表中,每个蓝色快被叫做节点,Next 叫做指针。

(2)循环链表

它是一种特殊的单链表。只是将结尾节点的指针指向头节点。
image.png
当要处理的数据具有环形特征的时候,就采用循环链表。比如著名的约瑟夫问题。

(3)双向链表
image.png
双向链表额外的两个空间用来存储后继节点和前驱节点的地址。因此它不像单链表那样只支持单项遍历,它可以支持双向遍历。

二、链表与数组相比的优缺点:

  • 优点:数组在插入、删除操作的时候,由于需要做大量的数据搬移,所以时间复杂度为O(n)。而链表的存储空间本身就不是连续的,在做同样操作的时候不需要搬移数据,理论上讲时间复杂度是 O(1) (单链表和双向链表的复杂度不一样)。

  • 缺点是:链表想要随机访问第 k 个元素,需要根据指针一个个节点进行遍历,时间复杂度为 O(n)。

(1)单链表和双向链表在增删操作的时候时间复杂度不一样在什么地方?
以删除操作为例。
实际开发中,从链表中删除一个数据无外乎有两种情况:

1.删除节点中“值等于某个给定值”的节点
2.删除某一指针指向的节点

  • 第一种情况:无论是单链表还是双向链表,为了找到给定值的节点,都需要从头到尾依次遍历对比,直到找到这个给定值的节点,再将其删除。尽管单纯的删除操作时间复杂度是 O(1),但是查找的过程确实主要的消耗时间,对应的时间复杂度是 O(n)。根据加法法则,得出总的操作时间复杂度为 O(n)。

  • 第二种情况:我们已经知道要删除的节点了,但是要删除某个节点 p 需要知道其前驱节点(删完之后还要讲旁边的节点连接气起来),而单链表不支持直接获取前驱节点,所以需要遍历整个链表,时间复杂度为 O(n)。
    而双向链表已经保存了前驱节点的指针,不需要遍历整个表,只需要在 O(1) 的时间复杂度内就可以搞定。

这也是实际开发中,双向链表尽管很费内存,但却应用更加广泛的原因。java 中的 LinkedHashMap 就用到了双链表数据结构。这就是用空间换时间的设计思想。

(2)链表 VS 数组性能大比拼


image.png

不过数组与链表的对比,并不能局限于时间复杂度。

数据使用的是连续的内存空间,可以借助 CPU 的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储的,所以对 CPU 的缓存不友好,无法预读。

数组的缺点是大小固定,一经生命就要占用整块连续内存空间。如果生命的数组过大,可能会导致内存不足“out of memory”,而如果申请的数组过小,内可能出现不够用的情况,这时再去申请内存空间,又牵扯到数据搬移的问题,比较费时。链表则天然的支持动态扩容。

此外,如果代码对内存的要求比较苛刻,那么数组更加合适。因为链表的每个节点都需要额外的空间去存储下个节点的指针,所以内存消耗会翻倍。而且,对链表进行频繁的插入、删除操作,可能会导致频繁的内存申请和释放,Java 语言中可能会导致频繁的 GC。

你可能感兴趣的:(数组和链表)