链表
相比数组,链表不需要一块连续的内存空间,而是通过指针将一组零散的内存块串联起来使用,而这里的内存块就叫做节点,一般节点除了保存data还会保存下一个节点的地址也就是指针。
单链表
- 头节点 链表基地址
- 尾节点next指向空
- 插入 ,删除 O(1) ,改变相邻的节点指针即可
- 不支持随机访问,需要循着指针一个一个找O(n)
循环链表
- 在单链表基础上,尾节点next指向头节点
- 循环链表的优点是从链尾到链头比较方便
双向链表
- 链表节点较一般节点多了一个prev指针,用来指向该节点前面一个节点,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间
- 双向链表支持双向遍历,操作灵活,用空间换时间
双向循环链表
- 顾名思义,双向链表的尾节点next指向头节点,头节点prev指向尾节点
特点
- 链表不支持随机访问,需要循着指针逐个查找时间复杂度为O(n)
- 删除和插入简单,只要改变相邻节点指针即可复杂度低为O(1)
- 删除和插入实际都需要查找到节点位置才可以进行删除或插入,而查找是主要的耗时点O(n)
单链表和双向链表:
1. 删除“值等于某个给定值”的节点
无论是单链表还是双向链表,为了删除值等于给定值的节点,都需要从头节点开始逐个遍历,直至查找到该节点。虽然删除操作为O(1),但是查找耗时为O(n),则该操作时间复杂度为O(n)
2. 删除给定指针指向的节点或者第N个节点
同上,需要找到指定节点后,如果是单链表由于删除操作还需要知道该节点的前置节点,所以还需要再遍历链表,直至p.next==q
,如果是双向链表就不需要再遍历一遍
3.相较之下,双向链表在查找和删除方面更有优势
4. 对于一个有序链表,双向链表的按值查询的效率也要比单链表高一些
因为,我们可以记录上次查找的位置 p,每次查询时,根据要查找的值与 p 的大小关系,决定是往前还是往后查找,所以平均只需要查找一半的数据
5.空间换时间
实际的软件开发中,双向链表尽管比较费内存,但还是比单链表的应用更加广泛。Java的LinkedHashMap容器的实现原理就用到了双向链表数据结构
技巧
1、理解指针或者引用
指针是C的概念,Java是没有指针的概念的,取而代之的是引用,不过都一样是用来存储所指对象的内存地址。
p->next=q
class Node{
Object data;
Node next;
}
node1.next = node2;
无论指针还是引用,next变量不是下一个节点,next只是保存了下一个节点的内存地址即指向下一个节点
2、警惕指针丢失和内存泄漏
指针丢失
主要会发生在链表的插入和删除操作中,删除和操作需要改变相邻节点指针的指向,一定要主要指针指向变更操作的顺序,一不小心就会导致指针丢失,可以看一下下面操作:
p.next = x
x.next = p.next
这里操作目的是在p节点后插入x节点,但实际操作后这里链表到x节点就断了,后续节点指针丢失。
插入操作需要先将后续节点指针赋值给新节点x,然后再将p节点指向新节点x,如下:
x.next = p.next
p.next = x
内存泄漏
内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
删除链表结点时,要记得手动释放被删节点的内存空间,不过Java的虚拟机会自动管理内存,所以不需要手动释放。
p.next = p.next.next
3、利用哨兵简化代码
如上所示代码,如果插入操作是向空链表插入第一个节点或者删除的是链表的最后一个节点 (不是尾节点,是链表只剩下一个节点,再删除这个节点),上面的代码就不适用了,需要加入if判断来应对特殊情况,如下:
if (head == null) {
head = new_node;
}
if (head.next == null) {
head = null;
}
这里的哨兵
不参与业务逻辑,只是为了简化代码而存在。
一般表示一个空链表:
Node head = null;
引入哨兵节点作为头节点,无论链表是否为空链表,head一直指向这个哨兵;
Node head = new Node(data,next);
如上图所示,哨兵结点是不存储数据的。因为哨兵结点一直存在,所以插入第一个结点和插入其他结点,删除最后一个结点和删除其他结点,都可以统一用相同的代码实现逻辑了。
实际上,这种利用哨兵简化编程难度的技巧,在很多代码实现中都有用到,比如插入排序、归并排序、动态规划等。
4、注意临界条件处理
- 如果链表为空,代码处理是否正常?
- 如果链表只有一个节点,代码处理是否正常?
- 如果链表只有两个节点,代码处理是否正常?
- 代码逻辑在处理头节点和尾节点时是否正常?
5、画图举例,辅助思考
关于算法,可以先思考解法,如果有一会想不出也不要死磕,去直接看解法。
其中对于比较复杂的链表操作,可以用画图的方式来演示每一步操作,可以帮助理解算法。
6、常见链表操作
- 单链表反转 (p\q指针借助head调换位置)
- 链表中环的检测 (快慢指针)
- 两个有序的链表合并 (递归)
- 删除链表倒数第 n 个节点 (快慢指针)
- 求链表的中间结点 (快慢指针)
熄灯
写链表代码是最考验逻辑思维能力的;
对于算法问题的解法,首相能想到解法的不用考虑解法是否优秀直接尝试去解,想不到的不要死磕,直接看解法,毕竟优秀的算法很多都是图灵奖的得主。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)