一)常用技巧:
1)画图:非常直观+形象最终便于我们理解
2)引入虚拟头节点:做链表算法题的时候,所做的都是不带头结点的,就是从第一个节点开始已经存储有效数据了,像这种链表需要考虑很多边界情况,我们可以创建一个新的头节点,在这个头节点里面并不会存储有效的数据,只是起到一个哨兵的作用
优点:便于处理边界情况,方便针对链表进行操作
3)不要吝啬空间,大胆去定义变量:
4)快慢双指针:链表中判断环,找链表中环的入口,以及找链表中倒数第N个节点
二)常见习题:
一)反转链表:
剑指 Offer 24. 反转链表 - 力扣(LeetCode)
算法原理:只需要定义一个指针current遍历整个链表,创建一个虚拟的头节点然后进行头插操作,最后返回newHead.next即可
class Solution { public ListNode reverseList(ListNode head) { ListNode newHead=new ListNode(-1); ListNode current=head; while(current!=null){ ListNode CurNext=current.next; if(newHead.next==null){ newHead.next=current; current.next=null;//防止链表的最后一个元素成环 } else{ //使用链表头插法来解决此问题 ListNode next=newHead.next; newHead.next=current; current.next=next; } current=CurNext; } return newHead.next; } }
二)两数相加:
2. 两数相加 - 力扣(LeetCode)
算法原理:模拟两数相加的过程即可,并使用虚拟头节点来连接我们最终的结果
class Solution { public ListNode addTwoNumbers(ListNode l1, ListNode l2) { ListNode newHead=new ListNode(-1); ListNode current=newHead; int carry=0,sum=0; while(l1!=null&&l2!=null){ sum=(l1.val+l2.val+carry)%10; carry=(l1.val+l2.val+carry)/10; ListNode temp=new ListNode(sum); current.next=temp; current=current.next; l1=l1.next; l2=l2.next; } //1.处理空余的第一个链表 while(l1!=null){ sum=(l1.val+carry)%10; carry=(l1.val+carry)/10; ListNode temp=new ListNode(sum); current.next=temp; current=current.next; l1=l1.next; } //2.处理空余的第二个链表 while(l2!=null){ sum=(l2.val+carry)%10; carry=(l2.val+carry)/10; ListNode temp=new ListNode(sum); current.next=temp; current=current.next; l2=l2.next; } //3.两个链表都处理完成之后处理最后一个carry也就是最后一个进位数 if(carry!=0){ ListNode temp=new ListNode(carry); current.next=temp; current=current.next; } return newHead.next; } }
三)两两交换链表中的节点:
24. Swap Nodes in Pairs - 力扣(LeetCode)
解法1:递归解法:找到重复子问题
解法2:迭代:交换两个指针的指向,需要两个节点指针,还需要被前面的指针连起来,还需要连接后面的指针,所有是需要四个节点的
1)直接引入虚拟头节点
2)定义四个变量即可,当我们定义四个变量以后,这些节点的交换工作直接看着图就可以完成,不需要担心顺序问题和链表断开的问题
3)循环结束的条件:current和next有一个为空的时候,直接返回
3.1)如下图所示,当节点个数是偶数个的时候,current==null,恰好所有节点都已经交换成功了,此时应该退出循环
3.2)或者说当链表中有奇数个节点,current!=null,但是next等于空了,此时不需要进行交换,直接返回即可
class Solution { public ListNode swapPairs(ListNode head) { if(head==null||head.next==null) return head; ListNode newHead=new ListNode(-1); ListNode prev=newHead; ListNode current=head; ListNode curNext=current.next; ListNode tailNext=curNext.next; newHead.next=curNext; while(current!=null&&curNext!=null){ //1.如果全部交换完成current==null,或者只剩下了一个节点curNext==null prev.next=curNext; curNext.next=current; current.next=tailNext; //2.交换完成之后重置节点 prev=current; current=prev.next; //3.剩下的两个指针指向为空,所以需要额外判断一下 if(current==null) break; curNext=current.next; if(curNext==null) break; tailNext=curNext.next; } return newHead.next; } }
这么写也是可以的:
四)重排链表:
143. 重排链表 - 力扣(LeetCode)
算法原理:
1)先找到链表的中间节点:快慢双指针
2)将中间节点后面的链表进行逆序操作:反转链表,双指针+头插法+递归
让第一个链表的最后一个位置指向null,让第二个链表的最后一个位置指向null
可以把包括中间节点的后面的链表翻转,也可以不包括中间节点后面的链表翻转
3)合并两个链表:合并两个有序链表,不需要进行比较,只是需要按照一定的顺序来进行合并
class Solution { public void reorderList(ListNode head) { if(head==null||head.next==null||head.next.next==null) return; //1.先找到链表的中间节点,一定要画图分析slow的落点 ListNode fast=head; ListNode slow=head; while(fast!=null&&fast.next!=null){ fast=fast.next.next; slow=slow.next; } //2.翻转中间节点后面的链表 ListNode prev=null; ListNode current=slow.next; if(current==null) return; ListNode CurNext=current.next; while(current!=null){ CurNext=current.next; current.next=prev; prev=current; current=CurNext; } //3.合并两个链表 ListNode newHead=new ListNode(-1); current=newHead; ListNode l1=head; ListNode l2=prev; while(l1!=null&&l2!=null){ current.next=l1; l1=l1.next; current=current.next; current.next=l2; l2=l2.next; current=current.next; } if(l1!=null) current.next=l1; if(l2!=null) current.next=l2; //4.特殊处理一下遗漏的节点,如果是奇数个节点,我们还是需要让这个奇数节点的下一个节点指向空,将两个链表分离 slow.next=null; } }
class Solution { public void reorderList(ListNode head) { //1.先找到链表的中间节点 ListNode fast=head; ListNode slow=head; while(fast!=null&&fast.next!=null){ fast=fast.next.next; slow=slow.next; } //2.翻转中间节点后面的链表,采用头插法 ListNode tempHead=new ListNode(-1); ListNode current=slow.next; while(current!=null){ ListNode CurNext=current.next; current.next=tempHead.next; tempHead.next=current; current=CurNext; } //3.合并两个链表 ListNode newHead=new ListNode(-1); current=newHead; ListNode l1=head; ListNode l2=tempHead.next; while(l1!=null&&l2!=null){ current.next=l1; l1=l1.next; current=current.next; current.next=l2; l2=l2.next; current=current.next; } if(l1!=null) current.next=l1; if(l2!=null) current.next=l2; //4.特殊处理一下遗漏的节点,如果是奇数个节点,我们还是需要让这个奇数节点的下一个节点指向空 slow.next=null; } }
五)合并k个升序链表:
23. 合并 K 个升序链表 - 力扣(LeetCode)
解法1:暴力破解
依次合并两个有序链表即可,假设每一个链表的平均长度是N,时间复杂度是K^2*N
N*(k-1)+N*(k-2)+N*(k-3)+N(k-4)+N
第一个链表需要向下合并(k-1)次,第二个链表需要向下合并(k-2)次,第三个链表需要向下合并(k-3)次,所以将这些表达式进行求和就可以得出时间复杂度是N*(k*(k-1)/2)
class Solution { public ListNode merge(ListNode head1,ListNode head2){ //这里面是合并两个有序链表的逻辑 if(head1==null) return head2; if(head2==null) return head1; ListNode newHead=new ListNode(-1); ListNode current=newHead; while(head1!=null&&head2!=null){ if(head1.val>head2.val){ current.next=head2; head2=head2.next; current=current.next; }else{ current.next=head1; head1=head1.next; current=current.next; } } if(head1!=null){ current.next=head1; }else{ current.next=head2; } return newHead.next; } public ListNode mergeKLists(ListNode[] lists) { ListNode newHead=null; for(int i=0;i
解法2:利用优先级队列来做优化(推荐):
1)可以先定义指针k1和k2和k3指向每一个链表的第一个节点,并且使用一个优先级队列创建一个小根堆,再来创建一个虚拟头节点来保存最终的结果,将每一个链表的头节点的最小的那个部分先存放到头节点,堆顶元素就是我们要最终要拼接在newHead后面的结果
2)这个逻辑和合并两个有序链表的逻辑是相同的,先找到各个链表头结点的最小值然后拼接到最后的结果里面,那么这里面的时间复杂度就是O(N*K),仅需要让指针遍历一遍即可
3)刚一开始将所有的链表的头结点都入队,那么出堆顶元素的头节点拼接在结果后面,让指针右移,再将节点入队,时间复杂度就是O(N*logK)
class Solution { public ListNode mergeKLists(ListNode[] lists) { ListNode newHead=new ListNode(-1); ListNode currrent=newHead; if(lists==null||lists.length==0) return newHead.next; //1.创建一个小根堆 PriorityQueue
MinHeap=new PriorityQueue (new Comparator () { @Override public int compare(ListNode o1, ListNode o2) { return o1.val-o2.val; } }); //2.将所有链表的头节点放入到小根堆里面即可 for(ListNode node:lists){ if(node!=null) MinHeap.offer(node); } //3.合并链表 while(!MinHeap.isEmpty()){ ListNode temp=MinHeap.poll(); currrent.next=temp; currrent=currrent.next; temp=temp.next; if(temp!=null) MinHeap.offer(temp); } //4.返回最终结果 return newHead.next; } } 解法3:分治->递归
1)重复子问题:给一个数组,先将左边区间的所有链表合并,再将右边区间的所有链表合并,最后就直接合并两个有序链表,如果实在不理解可以先写一下归并排序
2)设计dfs函数:给定一个链表数组,将给定区间的所有链表进行合并
3)时间复杂度:每一层的链表都会执行logK次合并,所以时间复杂度就是NK*logK
class Solution { public ListNode mergeSort(ListNode[] lists,int left,int right){ //这个函数被赋予的使 public ListNode merge(ListNode head1,ListNode head2){ //这里面是合并两个有序链表的逻辑 if(head1==null) return head2; if(head2==null) return head1; ListNode newHead=new ListNode(-1); ListNode current=newHead; while(head1!=null&&head2!=null){ if(head1.val>head2.val){ current.next=head2; head2=head2.next; current=current.next; }else{ current.next=head1; head1=head1.next; current=current.next; } } if(head1!=null){ current.next=head1; }else{ current.next=head2; } return newHead.next; }命就是将指定区间内的链表合并成一个大的链表 if(left==right) return lists[left];//此时数组中只有一个链表 if(left>right) return null;//此时数组中没有链表 int mid=(left+right)/2;[left,mid][mid+1,right]; //1.先将左边的链表进行合并,递归处理左部分 ListNode head1=mergeSort(lists,left,mid); //2.再将右边的链表进行合并,递归处理右部分 ListNode head2=mergeSort(lists,mid+1,right); //3.最后在合并两个有序链表 ListNode resultHead=merge(head1,head2); return resultHead; } public ListNode mergeKLists(ListNode[] lists) { return mergeSort(lists,0,lists.length-1); } }
六)k个一组反转链表
25. K 个一组翻转链表 - 力扣(LeetCode)
解法1:递归:
解法2:迭代+模拟操作+一定要画图
1)先进行计算出整个链表的长度
2)采取头插法+实现长度为K的链表的逆序即可
3)使用虚拟头节点来进行拼接并采取头插法
class Solution { public ListNode reverseKGroup(ListNode head, int k) { //1.先求出数组中的总长度计算循环次数 ListNode current=head; ListNode newHead=new ListNode(-1); int len=0; while(current!=null){ len++; current=current.next; } int count=len/k; current=head; ListNode newCurrent=newHead; //2.进行反转链表 for(int i=0;i
七)删除链表的倒数第N个节点
19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)
首先先回顾一下找到链表倒数第K个节点这道题,定义一个fast指针和slow种子很一开始都是指向链表