链表的学习
单链表的基本技巧,每个技巧都对应着至少一道算法题:
1、合并两个有序链表
2、链表的分解
3、合并 k
个有序链表
4、寻找单链表的倒数第 k
个节点
5、寻找单链表的中点
6、判断单链表是否包含环并找出环起点
7、判断两个单链表是否相交并找出交点
这些解法都用到了双指针技巧,所以说对于单链表相关的题目,双指针的运用是非常广泛的。
二、21. 合并两个有序链表
合并两个有序链表就用到了双指针技巧。
一个指针指向链表1,另一个指针指向链表2
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy=new ListNode(-1);
ListNode p=dummy;
ListNode p1=list1,p2=list2;
while(p1!=null&&p2!=null){
//只要有一个为null说明不用再比较了
if(p1.val>p2.val){
p.next=p2;
p2=p2.next;
}else{
p.next=p1;
p1=p1.next;
}
p=p.next;
}
//跳出循环说明已经比较完了,所以只需要让当前新链表的指针指向剩下的就行了
if(p1!=null){
p.next=p1;
}
if(p2!=null){
p.next=p2;
}
//因此此时p已经在链尾下一个了,所以如果返回p的话就是null了,因此在此之前我们要定义一个虚拟的头节点。
//而且头节点的下一个才是真正意义上的头节点
return dummy.next;
}
三、23. 合并K个升序链表
public ListNode mergeKLists(ListNode[] lists) {
int length=lists.length;
if(length==0) return null;
//虚拟头节点
ListNode dummy=new ListNode(-1);
ListNode p=dummy;
//优先队列,最小堆
PriorityQueue<ListNode> pq=new PriorityQueue<>(length,(a,b)->(a.val-b.val));
//将k个链表的头节点加入最小堆
for(ListNode head:lists){
//如果不为空则加入到优先队列中
if(head!=null){
pq.add(head);
}
}
//头节点加入之后开始循环
while(!pq.isEmpty()){
//获取最小节点,接到结果链表中
ListNode node=pq.poll();
p.next=node;
//为什么是node.next呢,因为node已经加入那个结果链表了所以要把它原先链表中的下一个加进来
if(node.next!=null){
pq.add(node.next);
}
//结果链表指针不断前进
p=p.next;
}
return dummy.next;
}
四、剑指 Offer 22. 链表中倒数第k个节点
单指针解法(即顺序解法,需要遍历两次链表)
public ListNode getKthFromEnd(ListNode head, int k) {
int length=0;
ListNode node=head;
//这里不能动head,算出链表的长度
while(node!=null){
node=node.next;
length++;
}
//System.out.println(length);
//虚拟头节点
ListNode p=new ListNode(-1);
for(int i=length;i>=k;i--){
//System.out.println(head.val);
p.next=head;
head=head.next;
}
return p.next;
}
双指针解法:
- 让指针p1指向链表的头节点head,然后走k步(即k次p1=p1.next)
分析:只要p1再向前走n-k步就会走到链表末尾的空指针
- 让指针p2指向链表的头节点head
分析:p2向前走n-k步就会到达n-k+1的位置,即第k个节点上
- 让指针p1和p2同时向前走n-k步
public ListNode getKthFromEnd(ListNode head, int k) {
/*
int length=0;
ListNode node=head;
//这里不能动head,算出链表的长度
while(node!=null){
node=node.next;
length++;
}
//System.out.println(length);
//虚拟头节点
ListNode p=new ListNode(-1);
for(int i=length;i>=k;i--){
//System.out.println(head.val);
p.next=head;
head=head.next;
}
return p.next;
*/
ListNode p1=new ListNode(-1);
ListNode p2=new ListNode(-1);
p1=head;
p2=head;
//p1先走k步
for(int i=1;i<=k;i++){
p1=p1.next;
}
//p1和p2一起走n-k步,这里不是真的用到n-k,如果真的是n-k还要再遍历一次链表找链表长度
while(p1!=null){
p1=p1.next;
p2=p2.next;
}
return p2;
}
疑问:为什么不直接让p2走n-k步呢?双指针的意义是什么?
因为我们想知道n就需要遍历一次链表才能得到链表长度
双指针的意义在于p1和p2同时走,p1作为p2走n-k步结束的条件,而p2可以到达第k个节点
五、19. 删除链表的倒数第 N 个结点
注意:要删除链表中的某一个节点B,应该先找到它的上一个节点A,然后让A的指针指向B的下一个节点C
所以这道题实际上找的是倒数第n+1个节点
那么找倒数节点就可以利用上一题的双指针法啦,只是k换成n+1,
但是在这里我们要使用了虚拟头结点的技巧,这是为了防止出现空指针的情况。
比如说链表总共有 5 个节点,题目就让你删除倒数第 5 个节点,也就是第一个节点,那按照算法逻辑,应该首先找到倒数第 6 个节点。但第一个节点前面已经没有节点了,这就会出错。
不过有了我们虚拟节点 dummy
的存在,就避免了这个问题,能够对这种情况进行正确的删除。
public ListNode removeNthFromEnd(ListNode head, int n) {
// 虚拟头结点
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode p1=dummy;
ListNode p2=dummy;
//这样p2到达的倒数第n+1个节点
for(int i=1;i<=n+1;i++){
p1=p1.next;
}
while(p1!=null){
p2=p2.next;
p1=p1.next;
}
//p2=p2.next.next;//这样没有删除到
p2.next=p2.next.next;//要删除链表中的某一个节点B,应该先找到它的上一个节点A,然后让A的指针指向B的下一个节点C
//而且因为p2一开始等于头节点 所以相当于直接对这个链表进行操作了,不会出现换个别的节点等于头节点还是输出没删除前的链表
return dummy.next;
}
六、876. 链表的中间结点(快慢指针)
- 链表长度为奇数时:
结束的条件是p1!=null p1.next==null
- 链表长度为偶数时:
结束的条件是p1==null
综上所述,循环的条件是p1!=null&&p1.next!=null
public ListNode middleNode(ListNode head) {
//双指针解法,这里是快慢指针
//快指针走两步,慢指针走一步,当快指针走到尾时,慢指针就到了中间
ListNode quick=head;
ListNode slow=head;
while(quick!=null&&quick.next!=null){
quick=quick.next.next;
slow=slow.next;
}
return slow;
}
七、141. 环形链表(判断链表是否存在环)
在这里也是使用快慢指针。如果 fast
最终遇到空指针,说明链表中没有环;如果 fast
最终和 slow
相遇,那肯定是 fast
超过了 slow
一圈,说明链表中含有环。
但是需要注意以下两个问题:
- 如果让快慢指针都等于头节点的话就会出现这个问题,所以快慢指针都指向虚拟头节点,然后快指针走两步到head.next,慢指针走一步到head;
- 如果要快慢指针都指向头节点那就先让指针走再判断快慢指针是否相等
- 如果头节点为空说明不存在环,需要提前做出判断
public boolean hasCycle(ListNode head) {
if(head==null) return false;
ListNode quick=head.next;
ListNode slow=head;
while(quick!=null&&quick.next!=null){
if(quick==slow){
return true;
}
quick=quick.next.next;
slow=slow.next;
}
return false;
}
八、142. 环形链表 II(判断链表是否存在环并找出头节点)
判断有无环很简单,用快慢指针,只要相遇就有环
这题的关键在于找出头节点:当快慢指针相遇时,让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。
分析:
假设快慢指针相遇时,慢指针 slow
走了 k
步,那么快指针 quick
一定走了 2k
步
- 也就是说
quick指针
一定比slow指针
多走了k
步,这多走的k
步其实就是fast
指针在环里转圈圈,所以k
的值就是环长度的整数倍
假设相遇点距环的起点的距离为 m
,所以环起点距头结点 head
的距离为 k - m
- 也就是说如果从
head
前进k - m
步就能到达环起点。 - 而从相遇点继续前进
k - m
步,也恰好到达环起点。(因为quick指针从相遇点开始走k步可以转回到相遇点,所以走k - m
步就会走到环起点)
综上所述:只要我们把快慢指针中的任一个重新指向 head
,然后两个指针同速前进,k - m
步后一定会相遇,相遇之处就是环的起点了。
public ListNode detectCycle(ListNode head) {
if(head==null) return null;
ListNode quick=head;
ListNode slow=head;
boolean flag=false;
while(quick!=null&&quick.next!=null){
quick=quick.next.next;
slow=slow.next;
if(quick==slow){
flag=true;
break;
}
}
//如果有环开始找环起点
if(flag==true){
slow=head;
while(quick!=slow){
quick=quick.next;
slow=slow.next;
}
return slow;
}
//否则没有环
return null;
}
九、160. 相交链表
若是只用两个指针的话怎么做呢?
- 若两条链表长度不相等时,如果只是用指针p1和p2分别在两条链表上前进的话是无法找到公共节点的
那我们通过某些方式使让 p1和 p2能够同时到达相交节点?
- 我们可以让 p1 遍历完链表 A之后开始遍历链表 B,让 p2 遍历完链表 B 之后开始遍历链表 A,这样相当于逻辑上两条链表接在了一起。
- 如果这样进行拼接,就可以让
p1
和p2
同时进入公共部分,也就是同时到达相交节点
那如果两个链表没有相交点,是否能够正确的返回 null 呢?
- 这个逻辑可以覆盖这种情况的,相当于相交节点是 null 空指针,可以正确返回 null。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p1=headA;
ListNode p2=headB;
/*
while(p1!=null){
p1=p1.next;
}
p1=headB;
while(p2!=null){
p2=p2.next;
}
p2=headA;
//长度相等的时候停下来
while(p1!=p2){
p1=p1.next;
//p2=p2.next;
}
*/
//只有放在同一个循环里面才能保证走的同步,然后才能找到公共点
//像上面的那个这样写的话 除非A和B一开始长度就相等 不然是找不到的
//那其实说到长度会不一样,那我们就很容易想到那就先算出两条链表的长度然后让p1和p2走到同一个长度再开始一起遍历就好啦
while(p1!=p2){
if(p1==null) p1=headB;
else p1=p1.next;
if(p2==null) p2=headA;
else p2=p2.next;
}
return p1;
}