今天给大家带来一些面试常见的链表相关的算法题,具体每一步的含义笔者尽量写清楚一些,因为算法题的步骤很难单纯的靠文字去描述,希望大家可以按照思路在草稿上画一下,自己把思路理一遍,真正理解每一步的含义了,再用编程的语言来写出来。
什么是链表?
链表是一种常见的数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。也就是说链表不需要按顺序来存储数据,在插入的时候可以达到 O(1) 的复杂度。但是在查找的时候需要一个结点一个结点的遍历,所以查找的时间复杂度为O(n)。 链表中的结点包含两个组成一部:结点中的值和指向下一个结点的指针。由此可以构造出链表结点的数据结构:public class ListNode{ int val; //值 ListNode next; // 下一个结点的指针 public ListNode(int x){ // 有参构造 this.val = x; } }
黑铁:链表翻转问题
题1:翻转单链表(要求掌握递归和迭代的两种方法)
https://leetcode-cn.com/problems/reverse-linked-list/
//迭代解法public ListNode reverseList(ListNode head) { //1. 数据校验 if(head == null || head.next == null) return head; //2. 建立一个为null的指针,翻转之后头结点要指向null ListNode pre = null; //3. 开始迭代翻转链表 ListNode cur = head; while(cur != null){ ListNode help = cur.next; // pre cur help cur.next = pre; //开始翻转 pre = cur; //pre 和 cur 分别往后移动一个结点 cur = help; } return pre; }// 递归解法public ListNode reverseList(ListNode head) { //1. 数据校验 if(head == null || head.next == null) return head; //2 . 先递归翻转后面的结点 ListNode cur = reverseList(head.next); head.next.next = head; //头结点的后面一个结点的指针指向头结点 head.next = null; //头结点的下一个指针指向null return cur; }
进阶1 :每两个结点翻转一次单链表
https://leetcode-cn.com/problems/swap-nodes-in-pairs/
public ListNode swapPairs(ListNode head) { //1. 数据校验 if(head == null || head.next == null) return head; //2. 定义一个虚拟头结点,使其next指针指向head ListNode dummy = new ListNode(0); dummy.next = head; //3. 开始翻转 ListNode cur = dummy; //因为要保证后面还有2个结点可以交换,所以后面两个结点不为空 while(cur.next != null && cur.next.next != null){ //定义两个指针保存位置 ListNode a = cur.next; ListNode b = cur.next.next; //开始交换,先用 c 来保存后面一个节点的位置 // cur a b ListNode c = b.next; cur.next = b; b.next = a; a.next = c; //cur 向后移动 cur = a; } return dummy.next; }
进阶2:每k个结点翻转一次单链表
https://leetcode-cn.com/problems/reverse-nodes-in-k-group/
public ListNode reverseKGroup(ListNode head, int k) { //1. 数据校验 if(head == null || head.next == null) return head; ListNode cur = head; for(int i = 0 ; i < k ; i++){ if(cur == null) return head; //如果没有k个结点了,则直接返回头 cur = cur.next; } //2. 此时cur指向第k个结点 ListNode newHead = reverse(head , cur); //翻转前K个结点,返回的是头结点 //递归调用(此时head已经是前k个结点的最后一个,和后面的结点要连接起来) head.next = reverseKGroup(cur , k); return newHead; } //翻转head - tail 之间的单链表,返回翻转后的头结点 ListNode reverse(ListNode head , ListNode tail){ ListNode pre = null; ListNode cur = head; while(cur != tail){ ListNode help = cur.next; // pre cur help cur.next = pre; pre = cur; cur = help; } return pre; }
进阶3:每翻转中间m - n个结点的单链表
https://leetcode-cn.com/problems/reverse-linked-list-ii/
public ListNode reverseBetween(ListNode head, int m, int n) { //1. 建立虚拟头结点 ListNode dummy = new ListNode(-1); dummy.next = head; //2. 找到第m-1个结点,也就是开始翻转的结点,用cur保存 ListNode cur = dummy; for(int i = 1; i < m ; i++) cur = cur.next; //(注意这里是从dummy结点开始的) //3. a 就是第m个结点 ListNode a = cur.next; ListNode tail = cur.next; //用tail来保存这个结点,翻转之后是尾结点,注意和之前的链表连接起来 //4. 翻转单链表的操作 ListNode pre = null; ListNode help = null; for(int i = m ; i <= n ;i++){ help = a.next; a.next = pre; pre = a; a = help; } //5. 翻转完之后pre 是头结点,tail 是尾结点,help是尾结点的下一个结点, cur.next = pre; tail.next = help; return dummy.next; }
进阶4:验证回文链表
https://leetcode-cn.com/problems/palindrome-linked-list/
思路1:用stack全部压进去再一个个出栈比较(空间复杂度On,不推荐)思路2:翻转后半个单链表,一个个比较(空间复杂度O1,推荐)public boolean isPalindrome(ListNode head) { //1. 数据校验 if(head == null) return true; //2. 快慢指针找到链表中点的位置(模板记住!!!) ListNode fast = head; ListNode slow = head; while(fast.next != null && fast.next.next != null){ fast = fast.next.next; slow = slow.next; } //3.翻转后面半个链表 ListNode a = reverse(slow.next); slow.next = null;//断开结点 //4.开始比较前半个链表和后半个链表,如果出现不同返回false ListNode b = head; while(a != null){ if(a.val != b.val) return false; a = a.next; b = b.next; } return true; } // 翻转单链表 ListNode reverse(ListNode head){ ListNode pre = null; ListNode cur= head; while(cur != null){ ListNode help = cur.next; cur.next = pre; pre = cur; cur = help; } return pre; }
黄铜:链表相交和环的判断
题1:判断链表中是否有环
https://leetcode-cn.com/problems/linked-list-cycle/
public boolean hasCycle(ListNode head) { //1. 数据校验 if(head == null || head.next == null) return false; //2. 定义一个快慢指针,快指针走两步,慢指针走一步,如果链表有环,则肯定相遇 ListNode fast = head; ListNode slow = head; while(fast != null && fast.next != null){ fast = fast.next.next; slow = slow.next; if(fast == slow) return true; } //3. 快指针都到头了,说明链表没有环 return false; }
进阶1:如果有环找到环结点,没有返回null
https://leetcode-cn.com/problems/linked-list-cycle-ii/
public ListNode detectCycle(ListNode head) { //1. 数据校验 if(head == null || head.next == null) return null; //2. 和前一题相似的思路 ListNode fast = head; ListNode slow = head; while(fast != null && fast.next != null){ fast = fast.next.next; slow = slow.next; if(fast == slow){ //说明有环,下一步就是要找出入环的结点 //将fast指针指向head ,和慢指针每次都直走一步,相遇的就是入环的结点 fast = head; while(fast != slow){ // 这里肯定会相遇,不用考虑死循环的问题 fast = fast.next; slow = slow.next; } return fast; } } return null; }
题2:找到两个单链表相交的结点
https://leetcode-cn.com/problems/intersection-of-two-linked-lists/
public ListNode getIntersectionNode(ListNode headA, ListNode headB) { //1. 数据校验 if(headA == null || headB == null) return null; //2. 定义两个指针分别指向头结点 ListNode l1 = headA , l2 = headB; //3. l1 一直往后移动,如果到了最后则到另外一个链表的头结点 // a + c (链表1) // b + c (链表2) // 这个思路就是 a + c + b = b + c + a 肯定会到相交的结点 while(l1 != l2){ l1 = l1 == null ? headB : l1.next; l2 = l2 == null ? headA : l2.next; } return l1; }
白银:合并有序链表问题
题1:合并两个有序链表
https://leetcode-cn.com/problems/merge-two-sorted-lists/
public ListNode mergeTwoLists(ListNode l1, ListNode l2) { //1. 数据校验 if(l1 == null || l2 == null) return l1 == null ? l2 : l1; //2. 建立虚拟头结点 ListNode dummy = new ListNode(-1); ListNode cur = dummy; //3. 两个指针分别向后移动,那个结点小就接到cur的后面 while(l1 != null && l2 != null){ if(l1.val < l2.val){ cur.next = new ListNode(l1.val); cur = cur.next; //cur 也要向后移动一个 l1 = l1.next; }else{ cur.next = new ListNode(l2.val); cur = cur.next; l2 = l2.next; } } //4.剩下的链表直接接上去即可,因为是已经排好序的链表了 cur.next = l1 == null ? l2 : l1; return dummy.next; }
题2:合并K个有序链表
https://leetcode-cn.com/problems/merge-k-sorted-lists/
public ListNode mergeKLists(ListNode[] lists) { //1. 建立小根堆 Queue heap = new PriorityQueue<>((a , b) -> (a.val - b.val)); //2. 将所有头结点放入堆中,这样堆顶就是最小的结点 for(ListNode node : lists){ if(node != null) heap.add(node); } //3. 建立虚拟结点 ListNode dummy = new ListNode(-1); ListNode cur = dummy; //4. 一直弹出堆顶的结点,放入cur的后面,并将弹出的结点的下一个结点再压入堆中 while(!heap.isEmpty()){ ListNode minNode = heap.poll(); cur.next = minNode; cur = cur.next; if(minNode.next != null) heap.add(minNode.next); } return dummy.next; }
一些小建议:
算法题分类刷,就想笔者这样整理,有利于复习
模板题要记得很清楚,比如翻转单链表,快慢指针找到中点
面试时直接写最优解,时间复杂度和空间复杂度能低就低
面试时可以先和面试官交流思路再开始写代码
理解最重要,不要背代码,看不懂可以找人问。
最后,看过的帮我点下拼多多哈哈~