链表
是以节点(node)存储的链式存储结构
,一个node包含一个data域(存放数据)和一个next域(存放下一个node的指针),链表的各个节点不必定是连续的,它能够分为带头结点
和不带头结点
。头结点仅包含next域。java
若是不熟悉链表的同窗,建议先看看个人一篇文章。在这篇文章中,主要讲解使用链表的小技巧,如何使用这些技巧来解题,深刻解析了LeetCode
中具备表明性
的链表题目,相信我,看了这篇文章,你不再用担忧关于链表的题目了。node
事实上,链表的结构比较简单,阻碍咱们理解链表的经常是由于链表的指针
、边界问题
等,这有时会让咱们很烦躁,不要慌,咱们下面一一对这下概念解析,相信你看了会有收获。面试
咱们学习C语言时,学过指针,它描述的是指向一个内存地址
,在Java语言中,是不存在指针的,可是咱们能够把它理解为引用
。算法
当咱们将某个变量(对象)赋值给指针(引用),实际上就是将这个变量(对象)的地址赋值给指针(引用)。数组
p—>next = q; //表示p节点的后继指针存储了q节点的内存地址。
p—>next = p—>next—>next; //表示p节点的后继指针存储了p节点的下下个节点的内存地址。
复制代码
咱们写链表代码时,使用的指针的指来指去
,很快就把咱们搞糊涂了,在这种状况下很容易发生指针丢失
和内存泄漏
。咱们先普及下这两个概念:markdown
指针丢失:本身定义的指针不知道指到哪里了,没有明确的指向。数据结构
内存泄漏:链表中的节点没有确切的指针判断,运行时会抛出空指针异常。函数
咱们以插入节点
和删除结点
来分析指针丢失和内存泄漏的具体状况oop
在节点a和节点b之间插入节点x,b是a的下一节点,p指针指向节点a,学习
p—>next = x;
x—>next = p—>next;
复制代码
这样的代码会形成指针丢失和内存泄漏,由于这会致使x节点的后继指针指向了本身自己。
正确代码应该为:
x—>next = p—>next;
p—>next = x;
复制代码
一样的,在节点a和节点c之间删除节点b,b是a的下一节点,p指针指向节点a,正确的代码应该为:
p—>next = p—>next—>next;
复制代码
在删除节点,考虑到删除的节点多是链表中的第一个节点,咱们一般在链表头部加入哨兵(头结点),这样可使得删除链表的代码是一致的,不用再额外考虑是不是第一个节点的状况。
在链表加入哨兵的代码为:
//定义一个哨兵做为传入链表的头结点
ListNode pre =new ListNode(0);
pre.next=head;
复制代码
处理链表问题时,要充分考虑链表的边界判断条件
,一般状况下,咱们常用如下几种判断条件:
若是链表为空时,代码是否能正常工做?
若是链表只包含一个结点时,代码是否能正常工做?
若是链表只包含两个结点时,代码是否能正常工做?
代码逻辑在处理头结点和尾结点的时候,是否能正常工做?
这些判断条件须要结合本身的实际场景来使用
在上面的学习中,咱们对链表的一些易错的概念进行了解析,下面,咱们就真正的代码实践,我在LeetCode上刷题时发现,链表题目一般分为如下几类:
这几类链表题基本涵盖了大部分知识点
,在下面的学习中,咱们将一一攻克它,相信掌握它们以后,在之后笔试/面试
中,更能为所欲为。
思路:从前日后将每一个节点的指针反向,即.next内的地址换成前一个节点的,但为了防止后面链表的丢失,在每次换以前须要先建立个指针指向下一个节点。
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null||head.next==null){
return head;
}
ListNode p1=head;
//用一个新的链表
ListNode p2=null;
while(p1!=null){
//每次更换指向以前都须要保存下一个节点
ListNode temp=p1.next;
p1.next=p2;
p2=p1;
p1=temp;
}
return p2;
}
}
复制代码
思路:定义两个指针,p1和p2,指针p1每次走一步,指针p2每次走两步,若是链表中存在环,则必然会在某个时刻知足p1==p2
public class Solution {
public boolean hasCycle(ListNode head) {
if(head==null||head.next==null){
return false;
}
ListNode slow=head;
ListNode fast=head.next;
while(fast!=null&&fast.next!=null){
if(slow==fast){
return true;
}
slow=slow.next;
fast=fast.next.next;
}
return false;
}
}
复制代码
NOTE:对于快指针来讲,由于一次跳两步,若是要使用快指针做为判断条件,fast和fast.next都须要判断是否为空。(不可跨级
)
思路
:能够新建立一个链表用于合并后的结果,合并的条件以下
定义一个指针,查找合适的节点并放入新建立链表的下一位置
将不为空的链表放入新建立链表的下一位置
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1==null){
return l2;
}
if(l2==null){
return l1;
}
ListNode result=new ListNode(0);
ListNode temp=result;
//两个链表都不为空
while(l1!=null&&l2!=null){
if(l1.val<=l2.val){
temp.next=l1;
temp=temp.next;
l1=l1.next;
}
else{
temp.next=l2;
temp=temp.next;
l2=l2.next;
}
}
//有一个链表为空
if(l1==null){
temp.next=l2;
}
else{
temp.next=l1;
}
return result.next;
}
}
复制代码
思路:能够在链表头加一个哨兵(头结点),删除链表时先找到删除链表的上一个位置,按照删除规则删除便可。
class Solution {
public ListNode deleteNode(ListNode head, int val) {
if(head==null){
return null;
}
//定义一个哨兵做为传入链表的头结点
ListNode pre =new ListNode(0);
pre.next=head;
ListNode temp=pre;
while(temp!=null){
if(temp.next.val==val){
temp.next=temp.next.next;
break;
}
else{
temp=temp.next;
}
}
return pre.next;
}
}
复制代码
思路:删除节点时要利用好哨兵
(带头结点的链表)
- 遍历数组的长度count
- 找到要删除节点的前一个位置count-n-1
- 删除节点
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode pre=new ListNode(0);
pre.next=head;
ListNode temp1=pre;
ListNode temp2=pre;
int count=0;
while(temp1!=null){
temp1=temp1.next;
count++;
}
while(count-n-1>0){
temp2=temp2.next;
count--;
}
temp2.next=temp2.next.next;
return pre.next;
}
}
复制代码
思路:找出链表中结点的个数count,而后count/2找出中间结点,删除便可。
class Solution {
public ListNode middleNode(ListNode head) {
if(head==null) return null;
ListNode temp=head;
int count=0;
while(temp!=null){
temp=temp.next;
count++;
}
int mid=count/2;
ListNode result=head;
while(mid>0){
result=result.next;
mid--;
}
return result;
}
}
复制代码
Note:实践是检验真理的惟一标准,要真正的学好链表这个知识点,仅仅学理论是不可靠的,咱们须要多敲代码
,多思考
,多写多练
,针对抽象的题目,能够举例画图
,来辅助的本身的思考。
一、 函数中须要移动链表时,最好新建一个指针来移动
,以避免更改原始指针位置。
二、 单链表有带头节点
和不带头结点
的链表之分,通常作题默认头结点是有值的
。
三、 链表的内存时不连续
的,一个节点占一块内存,每块内存中有一块位置(next)存放下一节点的地址。
三、 链表中找环
的思想:快慢指针
,建立两个指针,一个快指针:一次走两步
,一个慢指针:一次走一步
,若相遇则有环,若指向null则无环。
四、 链表找倒数第k个节点思想
:建立两个指针
,第一个指针查询链表中结点的个数count
,而后count-k
肯定删除结点的位置,用第二个指针遍历链表到count-n-1
个位置。
五、 反向链表思想:从前日后
将每一个节点的指针反向,即next内的地址换成前一个节点的
,但为了防止后面链表的丢失,在每次换以前须要先建立个指针指向下一个节点
。
不管学习任何一个知识点,咱们都须要在掌握术(使用方法)
的基础上,学习道(本源)
,学习数据结构与算法也是同样,咱们不只要掌握如何使用它
,更要掌握为何要是用它
,相比其它的方法,它有什么优势
,难道是时间复杂度低
,空间复杂度小
,仍是它的数据结构适合这个场景等等
...
参考文献
[1]王争.数据结构与算法之美
[2]LeetCode中国网站
笔者在过去的3个月时间里整理经常使用的数据结构与算法
和秒杀剑指offer
,公众号分别回复数据结构与算法
,秒杀剑指offer
,便可领取两套电子书,但愿可以帮助你们。
我是Simon郎
,一个想要天天博学一点点的小青年,关注我,让咱们一块儿进阶,一块儿博学。