扫码关注公众号,获取更多内容
目录
一、底层存储结构
我们观察下图
数组需要一块连续的内存空间来存储,对内存的要求比较高。如果我们申请一个 100MB 大小的数组,当内存中没有连续的、足够大的存储空间时,即便内存的剩余总可用空间大于 100MB,仍然会申请失败。
而链表恰恰相反,它并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用,所以如果我们申请的是 100MB 大小的链表,根本不会有问题。
我们把内存块称为链表的“结点”,为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址,我们把这个记录下个结点地址的指针叫作后继指针 next。
二、常见的链表
1、单链表
观察上图我们可以发现,有两个结点是比较特殊的,分别是第一个结点(头结点)和最后一个结点(尾节点)。其中头节点是用来记录链表的基地址,有了它,就可以遍历得到整条链表。而尾结点的指针不是指向下一个节点,而是指向一个空地址NULL,表示这是链表上最后一个结点。
与数组一样,链表也支持数据的查找、插入和删除。
针对链表进行插入和删除操作时,我们只需要考虑相邻结点指针的改变,所以时间复杂度是O(1)。
但当链表要随机访问第K个元素,就需要根据指针一个结点一个结点地依次遍历,直到找到对应的结点,时间复杂度是O(n)。
2、循环链表
循环链表是一种特殊的单链表,单链表的尾结点指针指向NULL,而循环链表的尾结点指针是指向链表的头结点。
3、双向链表
双链表支持两个方向,每个结点都有一个后继指针next指向后面的结点,还有一个前驱指针prev指向前面的结点。
双向链表需要额外的两个空间来存储后继结点和前驱结点地址。所以存储同样多的数据,双向链表占用更多的空间。虽然两个指针比较浪费存储空间,但可以支持双向遍历,使双向链表的操作更灵活。
4、双向循环链表
将双向链表与循环链表结合,我们可以得到双向循环链表。
三、链表与数组性能
在实际的软件开发中,不能仅仅利用复杂度分析就决定使用哪个数据结构来存储数据。
数组简单易用,在实现上使用的是连续的内存空间,可以借助 CPU 的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对 CPU 缓存不友好,没办法有效预读。
数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容,这也是它与数组最大的区别。