链表和算法简直不是一个东西,纵然做了很多算法题,没有专门训练过链表是很容易成为炮灰的,用几天时间把力扣上的链表题刷了记录一下。
主要操作有快慢指针、链表合并、拆分链表、重组链表、链表移位、链表成环、链表相交、链表反转、链表排序等,一般都是指针指来指去就够了,尽量不要用数组存储,否则体现不出水平,时常需要判空。
以下题目大致按难度排序
思路:1.常规的遍历求长度,再跑到中点停下来 2.快慢指针,一个走一步,一个走两步,快指针跑到末尾时慢指针就在中点
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode middleNode(ListNode head) { ListNode fast=head,slow=head; while(fast!=null && fast.next!=null){ slow=slow.next; fast=fast.next.next; } return slow; } }
思路:遍历过程中,下一个节点需要删除的话,则把当前节点的指针指向下下个节点。特判头节点。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode deleteNode(ListNode head, int val) { if(head==null) return null; ListNode cur=head; //特判第一个节点 if(head.val==val) return head.next; while(cur!=null){ //下一个节点要删除,就直接指向 下下个节点,退出 if(cur.next.val==val){ cur.next=cur.next.next; break; } cur=cur.next; } return head; } }
思路1:计算链表长度差k,长的先跑k步,短的再开始跑,会在交点相遇,若无交点,最后同时走到null。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { * val = x; * next = null; * } * } */ public class Solution { public ListNode getIntersectionNode(ListNode headA, ListNode headB) { if(headA==headB) return headA; int len1=size(headA),len2=size(headB); //保证A是短的,B是长的 if(len1>len2){ ListNode temp=headA; headA=headB; headB=temp; } int k=Math.abs(len1-len2); ListNode cur1=headA,cur2=headB; //长的链表B先走k步 for(int i=0;i<k;i++){ cur2=cur2.next; } while(cur1!=cur2){ cur1=cur1.next; cur2=cur2.next; } return cur1; } //获取链表长度 public static int size(ListNode head){ int res=0; while(head!=null){ res++; head=head.next; } return res; } }
思路2:一起跑,跑完换线跑,有交点会相遇,没交点最后也都是跑完全程同时到null,不会死循环。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { * val = x; * next = null; * } * } */ public class Solution { public ListNode getIntersectionNode(ListNode headA, ListNode headB) { ListNode cur1=headA,cur2=headB; while(cur1!=cur2){ cur1=cur1==null?headB:cur1.next; cur2=cur2==null?headA:cur2.next; } return cur1; } }
力扣141:环形链表(腾讯面试)
思路:快慢指针,如果有环的话,不会遇到null;一快一慢跑进环里总会相遇,一旦遇到null就可以退出。注意判空避免空指针异常。
/** * Definition for singly-linked list. * class ListNode { * int val; * ListNode next; * ListNode(int x) { * val = x; * next = null; * } * } */ public class Solution { public boolean hasCycle(ListNode head) { if(head==null) return false; ListNode cur1=head,cur2=head.next; while(cur1!=null && cur2!=null){ if(cur1==cur2){ return true; } cur1=cur1.next; if(cur2.next!=null) cur2=cur2.next.next; else break; } return false; } }
思路:逆序存储,从首位开始往后走,注意三个点,进位、一条链表先为空、走完全程还有进位。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode addTwoNumbers(ListNode l1, ListNode l2) { ListNode cur1=l1,cur2=l2,ans=new ListNode(0),res=ans,last=ans; int x=0,sum=0;//进位 while( cur1!=null || cur2!=null || x!=0){ sum=x; if(cur1!=null && cur2!=null){ sum=cur1.val+cur2.val+x; cur1=cur1.next; cur2=cur2.next; }else if(cur1!=null && cur2==null){ sum=cur1.val+x; cur1=cur1.next; }else if(cur1==null && cur2!=null){ sum=cur2.val+x; cur2=cur2.next; } x=sum/10; last=ans; ans.val=sum%10; ans.next=new ListNode(0); ans=ans.next; } last.next=null; return res; } }
思路:移动数位对长度求模,看移位把链表拆成两条,记得断开第一条链表末尾,尾再接上头。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode rotateRight(ListNode head, int k) { if(head==null) return null; int len=size(head); k=k%len; if(k==0 || head.next==null) return head; ListNode cur=head; for(int i=1;i<(len-k);i++){ cur=cur.next; } ListNode temp=cur.next;//后面部分链表起点,不会是null cur.next=null;//前面部分末尾置空 cur=temp; //找到末尾节点 while(cur.next!=null){ cur=cur.next; } //末尾节点->头节点 cur.next=head; return temp; } public static int size(ListNode head){ int res=0; ListNode cur=head; while(cur!=null){ res++; cur=cur.next; } return res; } }
思路:标记一下G中的数,遍历一次链表,对连续存在于G的数区间计数,累计到答案中。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public int numComponents(ListNode head, int[] G) { //最终答案 连续存在于G的数的数量 int ans=0,num=0; //对G中的数做一下标记 HashMap<Integer,Integer> map=new HashMap<>(); for(int i=0;i<G.length;i++) map.put(G[i],1); ListNode cur=head; while(cur!=null){ if( map.get(cur.val)!=null ){ num++; }else { if(num!=0) ans++; num=0; } cur=cur.next; } if(num!=0) ans++; return ans; } }
思路:从头走到尾,将当前结点指向上一个结点。
实现:上一个结点用last记录,当前结点用cur记录。下一个结点用t临时存储,因为修改了当前结点的指向就失去了下一个结点的位置,需要存储。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode reverseList(ListNode head) { if(head==null) return null; ListNode cur=head; ListNode t,last=null; while(cur != null){//当前不为空,把当前指向上一个 t=cur.next;//临时存储下一个结点 cur.next=last;//当前结点指向上一个结点 last=cur;//更新下一轮的【上一个结点】为【当前结点】 if(t==null) break; cur=t;//更新下一轮的【当前结点】为【下一个结点】 } return cur; } }
思路:将链表分为3段,第1段正常,第2段反转,第3段正常。注意第1段和第3段为空的情况。详看代码,附图解。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode reverseBetween(ListNode head, int m, int n) { if(head==null || head.next==null) return head; ListNode temp=new ListNode(-1),cur=head; //第1段为空 则 直接返回第2第3段链表 if(m==1) return reverse(head,n-m+1); temp.next=head; int i=1; //止步于 第2段链表前 while(i<(m-1)){ cur=cur.next; i++; } cur.next=reverse(cur.next,n-m+1); return temp.next; } public static ListNode reverse(ListNode head,int x){ ListNode tail=head,cur=head,last=null,temp; while(x-->0){ temp=cur.next;//临时存储下一个节点 cur.next=last;//当前节点指向上一个节点 last=cur;//更新上一个节点,准备下一轮 cur=temp;//移动当前节点 } /* 第3段链表不为空 tail last cur null ← 0 ← 0 ← 0 0 → 0 → 0 第3段链表为空 tail last cur null ← 0 ← 0 ← 0 null */ tail.next=cur; return last; } }
题意:链表多出一个随即指针随机指向,要求复制出一模一样的链表。
思路:主流解法都是在原链表中插入复制链表,最后拆分链表。需要保持原链表的内存地址不变,而不是单单分离出复制链表。
/* // Definition for a Node. class Node { int val; Node next; Node random; public Node(int val) { this.val = val; this.next = null; this.random = null; } } */ class Solution { public Node copyRandomList(Node head) { if(head==null) return null; //1.原链表的每个节点后插入复制节点 Node cur=head;// while(cur!=null){ //复制出当前节点 Node temp=new Node(cur.val); //复制点插入cur和cur.next之间 temp.next=cur.next; cur.next=temp; //移动当前节点 cur=temp.next; } //2.处理random指针 cur=head; while(cur!=null){ if( cur.random!=null ) cur.next.random=cur.random.next;//复制点->random的复制点 cur=cur.next.next; } //3.分离链表 cur=head.next;//第一个复制点 Node res=head.next,pre=head; while(cur!=null){ pre.next=cur.next; pre=cur.next; if(pre==null){//特判末尾节点 cur.next=null; break; } cur.next=pre.next; cur=pre.next; } return res; } }
力扣143:重排链表(招银面试)
题意:L0→L1→L2→L3→L4...→Ln-1→Ln 变成 L0→Ln→L1→Ln-1→L2→Ln-2...
思路:1.数组存储再双指针从新组成 2.快慢指针找中点,拆分成两条链表,第二条反转,再合并;融合几个简单操作。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public void reorderList(ListNode head) { if(head==null || head.next==null) return; ListNode mid=mid(head); ListNode cur=head; //断开链表 while(cur!=null){ if(cur.next==mid) cur.next=null; cur=cur.next; } mid=reverse(mid);//反转后半部分链表 merge(head,mid); } //获取链表中点 public static ListNode mid(ListNode head){ //快慢指针 ListNode fast=head,slow=head; while(fast!=null && fast.next!=null){ slow=slow.next; fast=fast.next.next; } return slow; } //反转链表 public static ListNode reverse(ListNode head){ if(head==null) return null; ListNode cur=head,t,last=null; while(cur != null){//当前不为空,把当前指向上一个 t=cur.next;//临时存储下一个结点 cur.next=last;//当前结点指向上一个结点 last=cur;//更新下一轮的【上一个结点】为【当前结点】 if(t==null) break; cur=t;//更新下一轮的【当前结点】为【下一个结点】 } return cur; } //交替合并链表 public static ListNode merge(ListNode l1,ListNode l2){ ListNode cur1=l1,cur2=l2,temp1,temp2; while(cur1!=null && cur2!=null){ temp1=cur1.next; cur1.next=cur2; temp2=cur2.next; //l1.size()<=l2.size(),防止丢失l2最后一个 if(temp1==null) break; cur2.next=temp1; cur1=temp1; cur2=temp2; } cur1.next=cur2; return l1; } }
思路:样例看了很久才看明白什么意思,小于x的放前面,大于等于x的放后面,在初始链表中的相对位置不变。小于x的节点按顺序放在一条链表里,大于等于x的节点按顺序放在另一条链表里,最后把两条链表接起来。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode partition(ListNode head, int x) { ListNode l1=new ListNode(-1),l2=new ListNode(-1),cur=head; ListNode res1=l1,res2=l2; while(cur!=null){ if(cur.val<x){ l1.next=cur; l1=l1.next; }else{ l2.next=cur; l2=l2.next; } cur=cur.next; } l2.next=null; l1.next=res2.next; return res1.next; } }
思路:均分长度取整,多出的节点分给前面的链表,人均一个。截断链表时注意记录下一条链表的起点,节点若不够分则后面的链表为空,需要判空。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode[] splitListToParts(ListNode root, int k) { ListNode[] ans=new ListNode[k]; if(root==null) return ans; int len=0; ListNode cur=root; while(cur!=null){ len++; cur=cur.next; } //均分之后的余数,注定前r个链表多1个节点,x是均分节点数 int r=len%k,x=len/k; //第i个链表 当前链表长度 int i=0,num=0; cur=root; ListNode start,temp=root; //获取完最后一个链表没有对cur置空,需要用i<k退出 while( cur!=null && i<k){ //r个较长的链表不会必定遇到空的情况 if(i<r){ start=temp; cur=start; for(int j=1;j<(1+x);j++){ cur=cur.next; } temp=cur.next;//保存下一个起点 cur.next=null;//截断链表尾 ans[i]=start;//保存每段链表头 }else{ start=temp; cur=start; for(int j=1;j<x && cur!=null;j++) cur=cur.next; //若遇到空说明链表已经被拆完了,直接退出 if(cur==null){ ans[i]=start; break; } temp=cur.next; cur.next=null; ans[i]=start; } i++; } return ans; } }
思路:归并排序,找中点分割+合并。空间复杂度不符合题目要求。但就是不想看迭代,遇到了算我倒霉。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode sortList(ListNode head) { return segment(head); } //归并排序,分割一条链表为两个链表再合并成一条链表 public static ListNode segment(ListNode head){ if(head==null) return null; if(head.next==null) return head; //快慢指针找中点 ListNode slow=head,fast=head.next; while(fast!=null && fast.next!=null){ slow=slow.next; fast=fast.next.next; } ListNode l2=segment(slow.next); slow.next=null;//可能并非均分,将就着断 ListNode l1=segment(head); return merge(l1,l2); } //合并有序链表 public static ListNode merge(ListNode l1,ListNode l2){ ListNode res=new ListNode(0); ListNode cur1=l1,cur2=l2,now=res; while(cur1!=null && cur2!=null){ if(cur1.val<cur2.val){ now.next=cur1; cur1=cur1.next; }else{ now.next=cur2; cur2=cur2.next; } now=now.next; } //谁没跑完就把它接到尾巴 now.next=cur2==null?cur1:cur2; return res.next; } }
思路:找中点分割,第二条反转,一起从头遍历看值是否相同。注意空链表返回true。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public boolean isPalindrome(ListNode head) { if(head==null || head.next==null) return true; ListNode mid=mid(head); ListNode cur=head; //断开链表 while(cur!=null){ if(cur.next==mid) cur.next=null; cur=cur.next; } mid=reverse(mid); ListNode cur1=head,cur2=mid; //cur1.size()<=cur2.size() while(cur1!=null){ if(cur1.val!=cur2.val) return false; cur1=cur1.next; cur2=cur2.next; } return true; } //获取链表中点 public static ListNode mid(ListNode head){ //快慢指针 ListNode fast=head,slow=head; while(fast!=null && fast.next!=null){ slow=slow.next; fast=fast.next.next; } return slow; } //反转链表 public static ListNode reverse(ListNode head){ if(head==null) return null; ListNode cur=head,t,last=null; while(cur != null){//当前不为空,把当前指向上一个 t=cur.next;//临时存储下一个结点 cur.next=last;//当前结点指向上一个结点 last=cur;//更新下一轮的【上一个结点】为【当前结点】 if(t==null) break; cur=t;//更新下一轮的【当前结点】为【下一个结点】 } return cur; } }
思路:蓄水池抽样算法
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { /** @param head The linked list's head. Note that the head is guaranteed to be not null, so it contains at least one node. */ static ListNode head; public Solution(ListNode head) { this.head=head; } /** Returns a random node's value. */ public int getRandom() { int cnt=0,ans=0; Random r=new Random(); ListNode cur=head; while(cur!=null){ cnt++; if(r.nextInt()%cnt==0) ans=cur.val; cur=cur.next; } return ans; } } /** * Your Solution object will be instantiated and called as such: * Solution obj = new Solution(head); * int param_1 = obj.getRandom(); */
力扣328:奇偶链表(映客笔试)
思路:分别用两条链表存储奇偶节点,最后拼接,注意偶链表末尾置空,记录偶链表头。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode oddEvenList(ListNode head) { if(head==null || head.next==null) return head; ListNode cur1=head,cur2=head.next,cur=head.next.next; ListNode temp=head.next;//防止丢失偶数链表的头部 int cnt=1; while(cur!=null){ if(cnt%2==1){//奇数 cur1.next=cur; cur1=cur1.next; }else{ cur2.next=cur; cur2=cur2.next; } cur=cur.next; cnt++; } cur1.next=temp; cur2.next=null; cur=head; return head; } }
思路:反转,遍历,累计val*2的幂次方。
class Solution { static int[] two=new int[30]; public int getDecimalValue(ListNode head) { if(head==null) return 0; two[0]=1; for(int i=1;i<30;i++) two[i]=two[i-1]*2; head=reverse(head); ListNode cur=head; int i=0,ans=0; while(cur!=null){ ans+=two[i++]*cur.val; cur=cur.next; } return ans; } //反转链表 public static ListNode reverse(ListNode head){ if(head==null) return null; ListNode cur=head,t,last=null; while(cur != null){//当前不为空,把当前指向上一个 t=cur.next;//临时存储下一个结点 cur.next=last;//当前结点指向上一个结点 last=cur;//更新下一轮的【上一个结点】为【当前结点】 if(t==null) break; cur=t;//更新下一轮的【当前结点】为【下一个结点】 } return cur; } }
思路:bfs过程中,每次把一层的节点弄成链表,先放在ArrayList里,最后再用toArray转数组。
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */ /** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode[] listOfDepth(TreeNode tree) { if(tree==null) return null; LinkedList<TreeNode> list=new LinkedList<>(); ArrayList<ListNode> res = new ArrayList<>(); list.add(tree); ListNode temp=new ListNode(-1),cur;//伪节点头 while(list.size()>0){ cur=temp; //获取当前层的节点数,拼接一条链表 int len=list.size(); for(int i=0;i<len;i++){ TreeNode now=list.poll(); cur.next=new ListNode(now.val); cur=cur.next; //略过空的 if(now.left!=null) list.add(now.left); if(now.right!=null) list.add(now.right); } res.add(temp.next); } return res.toArray(new ListNode[]{});//ArrayList转数组 } }
力扣23:合并K个升序链表(映客笔试)
思路1:优先队列
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode mergeKLists(ListNode[] lists) { //优先队列 Comparator<ListNode> comparator=new Comparator<ListNode>() { public int compare(ListNode o1, ListNode o2) { return o1.val<o2.val?-1:1; } }; Queue<ListNode> que=new PriorityQueue<ListNode>(comparator); ListNode temp,cur; for(int i=0;i<lists.length;i++){ cur=lists[i]; while(cur!=null){ que.add(cur); temp=cur; cur=cur.next; temp.next=null;//每个节点的next都置空,防止成环 } } ListNode head=new ListNode(-1); cur=head; while(que.size()>0){ cur.next=que.poll(); cur=cur.next; } return head.next; } }
思路2:归并排序
class Solution { public ListNode mergeKLists(ListNode[] lists) { if(lists.length==0) return null; return sort(0,lists.length-1,lists); } public static ListNode sort(int l,int r,ListNode[] lists){ if(l==r) return lists[l]; int mid=(l+r)/2; ListNode l1=sort(l,mid,lists); ListNode l2=sort(mid+1,r,lists); return merge(l1,l2); } //合并有序链表 public static ListNode merge(ListNode l1,ListNode l2){ ListNode res=new ListNode(0); ListNode cur1=l1,cur2=l2,now=res; while(cur1!=null && cur2!=null){ if(cur1.val<cur2.val){ now.next=cur1; cur1=cur1.next; }else{ now.next=cur2; cur2=cur2.next; } now=now.next; } //谁没跑完就把它接到尾巴 now.next=cur2==null?cur1:cur2; return res.next; } }
思路:遍历过程中,维持前面部分有序,严格递增。当前节点需要调整位置时,从头遍历找到适当的插入位置(前面小于当前节点,后面大于当前节点)。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode insertionSortList(ListNode head) { if(head==null || head.next==null) return head; //伪头节点 ListNode dummy=new ListNode(0); dummy.next=head; //pre为cur的前一个节点,通过cur=pre.next去移动即可 ListNode pre=head,cur=head.next,temp; while(cur!=null){ if(pre.val<=cur.val){ pre=pre.next; }else{ temp=dummy; //找到最后一个位置比cur小的位置,保证递增严格 while(temp.next.val<=cur.val){ temp=temp.next; } /* temp pre cur 1 → 2 → 4 → 8 → 5 → 6... */ pre.next=cur.next; cur.next=temp.next; temp.next=cur; /* temp cur pre 1 → 2 → 4 → 5 → 8 → 6... */ } cur=pre.next; } return dummy.next; } }
思路:对每一小段需要翻转k个节点的子链表,需要子链表的头节点,子链表的前驱节点,最后返回子链表的尾节点当作新的前驱节点。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode reverseKGroup(ListNode head, int k) { ListNode dummy=new ListNode(-1),cur=head; dummy.next=head; int len=0; while(cur!=null){ cur=cur.next; len++; } cur=dummy; int t=len/k;//反转t段长度为k的链表 while(t-->0){ cur=reverseK(cur,cur.next,k); } cur=dummy.next; return dummy.next; } //对后续k个节点进行反转 public static ListNode reverseK(ListNode pre,ListNode head,int k){ if(head==null) return head; //反转时需要指向上一个节点,当前节点,临时存储下一个节点 ListNode last=null,cur=head,temp; while(k-->0 && cur!=null){ //整条链的前驱节点不断往后指,改变链表指向 pre.next=cur; temp=cur.next; cur.next=last; last=pre.next; cur=temp; } //返回反转后的链表尾,当作下k个节点链表的前驱节点 head.next=cur; return head; } }
写在最后,Java用容器很容易解出这些题,例如用ArrayList存起来后,翻转、找中点等操作用下标即可。笔试的时候可以骗分,实在不行写一下极端情况(head==null || head.next==null)返回head大概率也能骗点分,悔不当初;面试手撕用容器就落了下乘。做题需要留意的有这些变量,头节点head、伪头节点(头节点的前驱)dummy、遍历到的当前节点cur、当前节点的上一个节点last、临时存储下一个节点temp(翻转后无法通过cur=cur.next获取,先用temp存一下),以及各种情况下判空break,尽量有属于自己的命名风格以及修改操作习惯,看了题解也尝试用自己的风格写出来,在草稿纸上涂涂画画,以后遇到就不成问题了。别无他长,惟手熟尔。