数组与链表区别
从底层结构上来看,数组是一块连续的内存,对内存要求较高.而链表则是通过”指针”将一组零散的内存块串联起来.
常见的链表结构:单链表,双向链表和循环链表
单链表
链表通过指针将一组零散的内存块串联在一起,其中把内存块成为链表的”结点”,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址.我们把这个记录结点的指针叫做后继指针 next.
其中有两个结点比较特殊.习惯性的把第一个结点叫头结点,最后一个结点叫尾结点.其中头结点用来记录链表的基地址.尾结点指针指向一个空地址NULL,表示这是链接上的最后一个结点.
插入,删除操作
链表插入和删除数据是非常快速的.因为链表的存储空间并不是连续的,插入或删除只是改变前后指针的位置,如图:
//todo
查询操作
因为链表数据并非连续存储的,所以随机访问k个元素,无法像数组那样根据地址和下标通过寻址公式,直接计算出对应的内存地址,而是需要根据指针一个结点一个结点依次遍历,直到找到相应的结点.
循环链表
循环列表是一种特殊的单链表.循环链表的尾结点指针是指向链表的头结点.
和单链表相比,循环链表的优点是从链尾到链头比较方便.当要处理的数据具有环形特点时,就特别适用.比如约瑟夫问题.
双向链表
单链表只有一个方向,结点只有一个后继指针 next指向后一个结点.而双向链表,支持前后两个方向,还有一个前驱指针 prev指向前面的结点
双链表需要额外空间来存储后继结点和前驱结点,所以存储同样多的数据,双向链表比单链表占用更多的内存空间.
从结构上来看,双向链表可以支持O(1)时间复杂度的情况下找到前驱结点,正是这样的特点,双向链表在某些情况下的插入和删除操作比单链表更简单高效.
删除操作
删除结点中”值等于某个给定值”的结点
不管是单链表还是双向链表,为了完成上述操作,都需要从头结点开始依次遍历对比.尽管单纯的删除操作时间复杂度为O(1),但遍历的时间复杂度为O(n).根据加法法则,删除给点值的结点的总时间复杂度为O(n).
删除给点指针指向的结点
假设删除某个结点q,我们得知道其前驱结点.单链表为了找到前驱结点,就需要从头遍历.但是,对于双向链表来说,结点已经保存了前驱结点的指针.所以在此情况下单链表操作需要O(n)时间复杂度,双向链表为O(1)时间复杂度.
同理,插入操作双向链表可以在O(1)时间复杂度搞定,而单链表需要O(n)时间复杂度.
此外,对于一个有序的链表,双向链表也比单链表效率高一些.因为我们可以记录上次查询的位置p,每次查询时,根据查找的值与p的大小关系,决定往前还是往后查找,所以平均只需要查找一般的数据
这就是空间换时间的设计思想
双向循环链表
循环链表和双向链表结合一起就是双向循环列表
链表数组性能比较
如何轻松写出链表代码
理解指针或引用的含义
将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量
警惕指针丢失和内存泄漏
指针是如何丢失的呢?我们以单链表插入操作为例分析.
如图,我们希望在a和b之间插入x,假设当前指针p指向a结点,
1 | p->next = x; //将p的next指针指向x |
插入结点时一定要注意顺序.正确操作如下:
1 | x->next = p->next; |
利用哨兵简化实现难度
针对链表的插入,删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理.利用哨兵结点来解决这个问题
如果我们引入一个哨兵结点,不管链表是不是空,head指针都会指向这个哨兵结点,我们把这种有哨兵结点的叫做带头链表,相反叫做不带头链表.
重点留意边界条件处理
检查链表代码是否正确的边界条件
- 如果链表为空时,代码能否正常工作.
- 如果链表只包含一个结点时,代码是否能正常工作
- 如果代码只包含两个结点时,代码是否能正常工作
- 代码再处理头结点和尾结点的时候,是否能正常工作
举例画图,辅助思考
多写多练,没有捷径
常见链表操作
- 单链表翻转
- 链表中环检测
- 两个有序的链表合并
- 删除链表倒数第n个结点
- 求链表的中间结点