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