文章目录
五、链表的面试题
- 对于笔试,不用在意空间复杂度,一切为了时间复杂度
- 对于笔试,时间复杂度放在第一位,一定找到空间最优的方法
- 笔试技巧:
- 使用容器,哈希表、集合、优先队列、栈、队列
- 快慢指针
- 笔试求稳,面试求骚
1、快慢指针
1.1 给定一个链表头节点,找到链表的中点位置
-
如果节点个数为奇数,则返回中间的节点
-
如果为偶数,返回中间的前一个位置和后一个位置(两个方法)
-
思想:准备两个指针,一个slow,一个fast。slow每次走一步,fast一次走两步,当fast走到链表尾部的时候,slow刚好走在链表中间,但是要注意边界判断
package list;
public class LinkedListMid {
/**
* 返回链表的中间节点,如果总节点数为偶数,则返回中间位置的后一个位置节点
* @param head
* @param <T>
* @return
*/
public static <T> Node<T> getListMidAfter(Node<T> head){
if (head == null || head.next == null){
return head;
}
Node<T> slow = head;
Node<T> fast = head;
//fast.next != null时候进入循环不会发生空指针问题
while (fast != null && fast.next != null){
//慢指针先走,这样快指针到末尾时候,如果节点总数为偶数则慢指针刚好在中间偏后的一个位置
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
/**
* 返回链表的中间节点,如果总节点数为偶数,则返回中间位置的前一个一个位置节点
* @param head
* @param <T>
* @return
*/
public static <T> Node<T> getListMidPre(Node<T> head){
if (head == null || head.next == null){
return head;
}
Node<T> slow = head;
Node<T> fast = head;
//fast.next != null时候进入循环不会发生空指针问题
while (fast != null && fast.next != null){
//快指针先走,加一次判断,如果快指针到末尾时候,则慢指针不需要走了,直接跳出,如果节点总数为偶数则慢指针刚好在中间偏前的一个位置
fast = fast.next.next;
if(fast == null){
break;
}
slow = slow.next;
}
return slow;
}
}
1.2 给定一个链表的头结点,判断是否为回文结构
- 笔试时候,直接用栈结构,所有节点入栈然后逆序弹出与原链表对比。时间复杂度O(N),空间复杂度O(N)
- 面试技巧:
- 找到链表的中点,然后把后半部分翻转
- 依次遍历两个链表相应位置,判断是否相同
- 最后要把链表还原
package list;
import java.time.Period;
import java.util.Stack;
public class IsPalindromeList {
/**
* 判断链表是否为回文结构,用栈实现,时间空间复杂度都为O(N)
* @param head
* @param <T>
* @return
*/
public static <T> boolean isPalindromeListByStack(Node<T> head){
if(head == null || head.next == null){
return true;
}
Stack<Node<T>> stack = new Stack<>();
Node<T> cur = head;
while (cur != null){
stack.push(cur);
cur = cur.next;
}
while (head != null){
if (!head.value.equals(stack.pop().value)){
return false;
}
head = head.next;
}
return true;
}
/**
* 判断链表是否为回文结构,用逆序链表实现,时间复杂度O(N),空间复杂度为O(1)
* @param head
* @param <T>
* @return
*/
public static <T> boolean isPalindromeListByMe(Node<T> head){
if(head == null || head.next == null){
return true;
}
//找出链表中点位置的节点,偶数时候找较后位置的一个节点
Node<T> slow = head;
Node<T> fast = head;
while (fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
//翻转后半部分链表
Node<T> pre = null;
Node<T> next = null;
while (slow != null){
next = slow.next;
slow.next = pre;
pre = slow;
slow = next;
}
//开始比较两个回文结构
boolean isPalindrome = true;
Node<T> leftHead = head;
Node<T> rightHead = pre;
while (rightHead != null){
if(!leftHead.value.equals(rightHead.value)){
isPalindrome = false;
break;
}
rightHead = rightHead.next;
leftHead = leftHead.next;
}
//还原链表
Node<T> before = null;
while (pre != null){
next = pre.next;
pre.next = before;
before = pre;
pre = next;
}
return isPalindrome;
}
//打印链表数据的静态方法,方便查看链表是否被还原
public static <T> void print(Node<T> head){
while (head != null){
System.out.print(head.value + " -> ");
head = head.next;
}
System.out.println();
}
// public static void main(String[] args) {
// Node<Integer> node1 = new Node<>(1);
// node1.next = new Node<>(2);
// node1.next.next = new Node<>(3);
// node1.next.next.next = new Node<>(2);
// node1.next.next.next.next = new Node<>(1);
// print(node1);
// System.out.println(isPalindromeListByStack(node1));
// print(node1);
// System.out.println(isPalindromeListByMe(node1));
// print(node1);
// }
}
2、链表复制问题
- 给定一个链表,每个节点除了有指向下一个节点的指针之外,还带有一个随机指针指向链表的随机一个节点,请完成链表的复制(建立一条新的链表)
- 两种方法:
- 方法一: 用一个map,先遍历一遍链表,key存储当前链表节点,value存储值相同的新节点(新创建的节点),next和random指针置空。再遍历一遍链表,根据key的next和random指针找到对应key的value进行赋值。第一遍不能赋值next和random指针,因为表中还没有放进去相应位置的节点。
- 方法二: 在原始链表上为每个节点后面插入一个copy节点,与前面的值相同。再次遍历链表,根据原始节点的random找到next为copy节点的random赋值,最后将两个链表拆分即可。
package list;
import java.util.HashMap;
import java.util.HashSet;
public class LinkedListRandomCopy {
public static class RandomNode{
public int value;
public RandomNode next;
public RandomNode random;
public RandomNode(int value){
this.value = value;
next = null;
random = null;
}
}
/**
* 用hash表完成链表复制
* @param head
* @return
*/
public static RandomNode copyByHashMap(RandomNode head){
if(head == null){
return null;
}
HashMap<RandomNode, RandomNode> map = new HashMap<>();
RandomNode cur = head;
//第一遍将节点放入map
while (cur != null){
map.put(cur,new RandomNode(cur.value));
cur = cur.next;
}
cur = head;
while (cur != null){
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
return map.get(head);
}
/**
* 用有限个变量完成链表复制
* @param head
* @return
*/
public static RandomNode copyLinkedListByMe(RandomNode head){
if(head == null){
return null;
}
RandomNode cur = head;
RandomNode next = null;
//给每个节点后面加一个复制节点
while (cur != null){
next = cur.next;
cur.next = new RandomNode(cur.value);
cur.next.next = next;
cur = next;
}
//再次遍历链表,为random指针赋值
cur = head;
while (cur != null){
cur.next.random = cur.random.next;
cur = cur.next.next;
}
//分离链表
cur = head;
RandomNode copyHead = head.next;
RandomNode copyCur = copyHead;
while (true){
cur.next = copyCur.next;
if(copyCur.next == null){
break;
}
cur = cur.next;
copyCur.next = cur.next;
copyCur = copyCur.next;
}
return copyHead;
}
//生成制定个数的随机链表
public static RandomNode getRandomNode(int num){
if(num < 0){
return null;
}
HashMap<Integer, RandomNode> map = new HashMap<>();
RandomNode head = new RandomNode((int) (Math.random() * 100));
map.put(0,head);
RandomNode cur = head;
for (int i = 1;i < num;i++){
cur.next = new RandomNode((int) (Math.random() * 100));
cur = cur.next;
map.put(i,cur);
}
cur = head;
while (cur != null){
cur.random = map.get((int) (Math.random() * num));
cur = cur.next;
}
return head;
}
//打印链表元素,方便测试
public static void print(RandomNode head){
while (head != null){
System.out.print(head.value + " -> ");
head = head.next;
}
System.out.println();
}
public static void main(String[] args) {
RandomNode head = getRandomNode(10);
print(head);
RandomNode copyOne = copyByHashMap(head);
print(copyOne);
print(head);
RandomNode copyTwo = copyLinkedListByMe(head);
print(copyTwo);
print(head);
}
}
3、判断两个有环或者无环链表是否相交,如果有就返回第一个相交节点,如果没有则返回null
1. 先判断一个链表是否有环,如果有环就返回第一个入环节点,如果没有就返回null
- 生成快慢两个指针,快指针一次两步,慢指针一次一步,如果没有环,则快指针一定会走到链表末尾,这时直接返回空即可。
- 如果链表有环,那么快慢指针一定会在环上某个节点相遇。因为快慢指针一旦入环,都会在环上循环,而快指针每次比慢指针快一步,那么最多经过N(环上节点的个数)步必定追上慢指针。
- 当有环并且快慢指针相遇时,让快指针回到链表头结点,并且每次只有一步,当再次相遇时就是入环节点。
public static class Node {
public int value;
public Node next;
public Node() {
}
public Node(int value) {
this.value = value;
}
}
/**
* 判断链表是否有环,有返回第一个入环节点,没有返回null
* @param head 头结点
* @return 第一个入环节点
*/
public static Node getLoopNode(Node head){
if(head == null || head.next == null || head.next.next == null){
return null;
}
Node slow = head.next;
Node fast = head.next.next;
//如果没有环就返回null,不会在继续执行。如果有环则继续向下执行
while (slow != fast){
if(fast.next == null || fast.next.next == null){
return null;
}
fast = fast.next.next;
slow = slow.next;
}
//fast来到头结点,每次也走一步,这样相遇时返回就是入环节点
fast = head;
while (fast != slow){
fast = fast.next;
slow = slow.next;
}
return slow;
}
2、判断两个无环链表是否相交
- 给定两个无环链表的头结点(链表是否有环可以用上面判断),如果两个链表相交则返回相交节点,没有则返回null
- 思路:
- 遍历两个链表,分别记录两个链表的长度,并记录两个链表的尾结点
- 如果两个链表最后一个节点相等(指的是内存地址,直接==),则必定相交,继续下一步
- 让长链表先走(n1 - n2)步(n1为长链表的长度),然后两个链表一起走,直到遇见相等的节点
/**
* 判断两个无环链表是否相交,是返回第一个相交节点,否则返回null
* @param head1
* @param head2
* @return
*/
public static Node noLoop(Node head1, Node head2){
if(head1 == null || head2 == null){
return null;
}
Node cur1 = head1;
Node cur2 = head2;
int n1 = 0,n2 = 0;
while (cur1.next != null){
n1++;
cur1 = cur1.next;
}
while (cur2.next != null){
n2++;
cur2 = cur2.next;
}
if(cur1 != cur2){
return null;
}
//让cur1来到长链表的头结点,cur2来到短链表的头节点
int diff = n1 - n2;
cur1 = diff >= 0 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
//长链表先走diff步
diff = Math.abs(diff);
while (diff-- > 0){
cur1 = cur1.next;
}
//两个链表开始一起走,直到遇见相交节点
while (cur1 != cur2){
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
3. 判断两个有环链表是否相交
-
判断两个有环链表是否相交
-
都有环但不相交
-
未入环之前相交
-
在环上相交
3.1 入环处交于一点
3.2 在环上不同点相交
-
/**
* 判断两个有环链表是否相交,若相交则返回第一个相交节点
* 1. 都有环但不相交
* 2. 未入环之前相交
* 3. 在环上相交
* 3.1 入环处交于一点
* 3.2 在环上不同点相交
* @param head1 第一个链表头结点
* @param head2 第二个链表头结点
* @param loop1 第一个链表入环节点
* @param loop2 第二个链表入环节点
* @return
*/
public static Node bothLoop(Node head1,Node head2,Node loop1,Node loop2){
//如果入环节点相同
//在入环节点相交
//在未入环处相交
if(loop1 == loop2){
Node cur1 = head1;
Node cur2 = head2;
//找出每个链表(不包括环的长度)
int n1 = 0;
int n2 = 0;
while (cur1 != loop1){
n1++;
cur1 = cur1.next;
}
while (cur2 != loop2){
n2++;
cur2 = cur2.next;
}
//让cur1来到长链表的头结点,cur2来到短链表的头节点
int diff = n1 - n2;
cur1 = diff >= 0 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
//长链表先走diff步
diff = Math.abs(diff);
while (diff-- > 0){
cur1 = cur1.next;
}
//两个链表开始一起走,直到遇见相交节点
while (cur1 != cur2){
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}else {
//两种情况,不相交或者在环上相交
//如果第一个链表把环走一圈没有遇到loop2.则说明不相交
//如果相交,返回loop1或loop2中的任意一个
Node cur1 = loop1.next;
while (cur1 != loop1){
if(cur1 == loop2){
return loop2;
}
cur1 = cur1.next;
}
return null;
}
4、整合
public static Node getIntersectNode(Node head1,Node head2){
if(head1 == null || head2 == null){
return null;
}
//1. 先判断两个链表是否有环
Node loop1 = getLoopNode(head1);
Node loop2 = getLoopNode(head2);
//2.两个链表都有环
if(loop1 != null && loop2 != null){
return bothLoop(head1,head2,loop1,loop2);
}
//3.两个链表都无环
if(loop1 == null && loop2 == null){
return noLoop(head1,head2);
}
//4.其中一个有环,一个无环。这种情况下不可能相交(如果相交,则必定会有环)
return null;
}