数据结构与算法(3)链表(上)
链表
链表与数组之间的区别
- 数组需要一块连续的内存空间来存储,对内存要求比较高。而链表恰恰相反,它并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用。
单链表
- 链表通过指针将一组零散的内存块串联在一起。其中,我们把内存块称为链表的“结点”。为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址。如图所示,我们把这个记录下个结点地址的指针叫做后继指针next。
-
第一个结点叫头结点,最后一个结点叫尾结点。其中头结点用来记录链表的基地址。有了它就可以遍历得到整条链表。而尾结点的指针不是指向下一个结点,而是指向一个空地址NULL,这是链表的最后一个结点。
-
链表和数组一样也支持数据的查找、插入和删除操作。数组在插入或删除时需要保持数据的连续性,所以要做大量的移动,时间复杂度为O(n)。而链表在插入或删除一个数据,我们并不需要为了保护内存的连续性而搬移结点,因为链表本身就不是连续的,链表在对数据进行插入和删除操作时我们只需要考虑相邻结点的指针改变。所以,链表的时间复制度为O(1)。
循环链表
-
循环链表跟单链表唯一的区别就在尾结点,单链表的尾结点指针指向空地址,表示这就是最后的结点了。而循环链表的尾结点是指向链表的头结点。
-
和单链表相比,循环链表的优点是从链尾到链头比较方便。当要处理的数据具有换形结构特点是,就特别适合采用循环链表。
双向链表
- 单链表只有一个方向,结点只有一个后继指针next指向后面的结点。而双向链表支持两个方向,每个结点不止有一个后继指针next指向后面的结点,还有一个prev指向前面的结点。
- 如果存储同样多的数据,双向链表比单链表占用更多的内存空间,但双向链表因为两个指针的存在支持双向遍历,这样也为双向链表带来操作的灵活性。双向链表在插入、删除等操作都要比单链表简单、高效。
删除操作
在实际的软件开发中,从链表中删除一个数据无外乎这两种情况:
-
删除节点中“值等于某个给定值”的结点;
-
删除给定指针指向的结点。
对于第一种情况,不管是单链表还是双向链表,都需要从头结点一个个依次遍历对比,直到找到值等于给定值的结点,然后在通过指针操作将其删除。
单纯删除的时间为O(1),遍历查找的时间是主要耗时点,对应时间复杂度为O(n)。故删除操作的时间复杂度为O(n)。
对于第二种情况,我们已经找到了要删除的结点,但删除某个结点q需要知道其前驱结点,而单链表并不支持直接获取前驱结点,所以,为了找到前驱结点,我们还是要从头结点开始遍历链表,知道p->next=q,说明p是q的前驱结点。这一情况的时间复杂度为O(1)。
空间换时间的设计思想
当内存空间充足时,若我们追求代码执行速度,我们就可以选择空间复杂度相对较高、但时间复杂度相对较低的算法或数据结构。相反,如果内存比较紧缺,比如代码跑在手机或者单片机上,这时候,就要反过来用时间换空间的设计思路。
缓存实际上就是利用了空间换时间的设计思想。如果我们把数据储存在硬盘上,会比较节省内存,但每次查找数据都要询问一次硬盘,会比较慢。但如果我们通过缓存技术,事先将数据加载在内存中,虽然会比较耗费内存空间,但是每次数据查询的速度就大大提高了。
双向循环链表
将两种链表合在一起就是双向循环链表
数组VS链表
时间复杂度
时间复杂度 | 数组 | 链表 |
---|---|---|
插入删除 | O(n) | O(1) |
随机访问 | O(1) | O(n) |
-
数组在实现上使用的是连续的内存空间,可以接祖CPU的缓存机制,预读数组中的数据,所以访问效率高,而链表在内存中并不是连续储存,所以对CPU不友好,没办法有效预读。
-
数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容,我觉得这也是它与数组最大的区别。