此为自己学习课程的笔记,供自己复习用。
前言
链表不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用。
一、三种常见的链表结构
1、单链表
链表通过指针将一组零散的内存快串联在一起,这些内存块称为结点。
单链表中,每个结点除了存储数据外,还要存储下一个结点的地址。
一般第一个结点为头结点,最后一个结点为尾结点。头结点记录链表的基地址,尾结点的指针则指向一个空地址NULL。
链表插入和删除操作的时间复杂度为O(1),但是查找第k个元素时,要根据指针一个结点一个结点地依次遍历,因此时间复杂度为O(n)。
2、循环链表
循环链表是一种特殊的单链表。不同的是,循环链表的尾结点指针指向链表的头结点。
循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,就特别适合采用循环链表。比如著名的约瑟夫问题。
3、双向链表
双向链表,支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。
双向链表需要额外的两个空间来存储后继结点和前驱结点的地址,造成了空间的浪费,但是它支持双向遍历,比单链表的操作要更高效。(空间换取时间的设计思想)
(1)删除操作的高效体现
删除操作有两种情况:
- 删除结点中值等于某个值的结点
- 删除给定指针指向的结点
对于第一种情况,要找到具体给定值的结点需要进行遍历查找,时间复杂度为 O(n)。
对于第二种情况,双向链表中的结点已经保存了前驱结点的指针,不需要像单链表那样遍历。因此,单链表删除操作需要 O(n) 的时间复杂度,而双向链表只需要在O(1) 的时间复杂度。
插入操作同理。
对于有序链表,双向链表的按值查询的效率也要比单链表高一些:
- 我们可以记录上次查找的位置 p,每次查询时,根据要查找的值与 p 的大小关系,决定是往前还是往后查找,所以平均只需要查找一半的数据
4、双向循环链表
是循环链表和双向链表的结合体。
二、数组和链表的对比
在实际的使用过程中,不能仅仅利用复杂度分析就决定使用哪个数据结构来存储数据。
例如:
- 数组简单易用,在实现上使用的是连续的内存空间,可以借助 CPU 的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对 CPU 缓存不友好,没办法有效预读。
- 数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时(数据拷贝的操作是非常耗时的)。链表本身没有大小的限制,天然地支持动态扩容,这也是它与数组最大的区别。
- 如果代码对内存的使用非常苛刻,那数组就更合适。因为链表中的每个结点都需要消耗额外的存储空间去存储一份指向下一个结点的指针,所以内存消耗会翻倍。而且,对链表进行频繁的插入、删除操作,还会导致频繁的内存申请和释放,容易造成内存碎片。
总结
和数组相比,链表更适合插入、删除操作频繁的场景,查询的时间复杂度较高。不过,在具体软件开发中,要对数组和链表的各种性能进行对比,综合来选择使用两者中的哪一个。