链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。由于不必须按顺序存储,链表在插入/删除数据的时候可以达到O(1)的复杂度,但是查询相对线性表复杂。
本文整理了大厂面试中比较多见的链表面试题,如果遇到更新颖的题目,欢迎大家留言或者私信我,发出来大家一起学习下。
链表数据结构,如下:
// 单向简单链表
public class ListNode {
int val;
ListNode next;
public ListNode() {}
public ListNode(int val){ this.val = val; }
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
// 复杂链表
public class RandomListNode {
int val;
RandomListNode next;
RandomListNode random;
public RandomListNode(){ }
public RandomListNode(int val){ this.val=val; }
}
1. 从尾到头【打印链表】
// 方法一:利用【栈】的先入后出特性
public static List<Integer> printListFromTailToHead(ListNode listNode) {
List<Integer> list = new ArrayList<>();
if (listNode != null) {
ListNode node = listNode;
Stack<Integer> stack = new Stack<>();
while (node != null) {
stack.add(node.val);
node = node.next;
}
while (!stack.isEmpty()) {
list.add(stack.pop());
}
}
return list;
}
// 方法二:递归
public static List<Integer> printListReverse2(ListNode headNode) {
List<Integer> list = new ArrayList<Integer>()
ListNode node = headNode;
if (node != null) {
if (node.next != null) {
list = printListReverse2(node.next);
}
list.add(node.val);
}
return list;
}
// 方法三:Collections.reverse 反转 List
public static List<Integer> printListReverse2(ListNode headNode) {
List<Integer> list = new ArrayList<Integer>()
if (listNode != null) {
ListNode node = listNode;
while (node != null) {
list.add(node.val);
node = node.next;
}
Collections.reverse(list)
}
return list;
}
2.输出【反转】后的链表
// 解法一:迭代,两个指针,反向输出,时间复杂度:O(n),空间复杂度:O(1)
public static ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode curr = head;
while (curr != null) {
ListNode tmp = curr.next;
curr.next = pre;
pre = curr;
curr = tmp;
}
return pre;
}
// 解法二:递归,时间复杂度:O(n),空间复杂度:O(n)
public static ListNode reverseList2(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode p = reverseList2(head.next);
head.next.next = head.next;
head.next = null;
return p;
}
3.【合并】2个有序链表
// 解法一:递归,时间复杂度:O(m+n),空间复杂度:O(m+n)
public static ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if (list1 == null) {
return list2;
}
if (list2 == null) {
return list1;
}
if (list1.val < list2.val) {
list1.next = mergeTwoLists(list1.next, list2);
return list1;
} else {
list2.next = mergeTwoLists(list1, list2.next);
return list2;
}
}
// 解法二:迭代,时间复杂度:O(m+n),空间复杂度:O(1)
public static ListNode mergeTwoLists2(ListNode list1, ListNode list2) {
ListNode preHead = new ListNode(-1);
ListNode pre = preHead;
while (list1 != null && list2 != null) {
if (list1.val < list2.val) {
pre.next = list1;
list1 = list1.next;
} else {
pre.next = list2;
list2 = list2.next;
}
pre = pre.next;
}
// 最后一次循环后,肯定剩下一个节点,判断该节点然后追加到pre末尾
pre.next = (list1 != null ? list1 : list2);
// pre 与 preHead 实际是相通的,但是指针位置不同
return preHead.next;
}
补充:【合并】多个有序链表
利用有序队列 PriorityQueue<> 存储每个链表的头节点,循环取出元素知道队列元素为空。
public ListNode mergeKListsByQueue(ListNode[] lists) {
// 放入PriorityQueue 的元素,必须重写 compare 方法
PriorityQueue<ListNode> queue = new PriorityQueue<>(new Comparator<ListNode>() {
@Override
public int compare(ListNode o1, ListNode o2) {
return Integer.compare(o1.val,o2.val);
}
});
for (ListNode tempNode : lists) {
queue.offer(tempNode);
}
// retList == tempList
// 但是循环结束以后,tempList的指针在最后,但是retList的指针没有动过,还在最开头
// 此时,才能使用retList.next获取最终结果(因为首位的0是没用的)
ListNode<Integer> retList = new ListNode<>(0);
ListNode<Integer> tempList = retList;
while (!queue.isEmpty()) {
tempList.next = queue.poll();
tempList = tempList.next;
if (tempList.next != null) {
// 把剩下的部分再放回队列,放进去以后,优先级队列自动排序
queue.offer(tempList.next);
}
}
return retList.next;
}
4.求链表中倒数【第 K 个节点】
public ListNode findKthToTail(ListNode listNode, int k) {
if (listNode == null || k < 1) {
return null;
}
ListNode fast = listNode;
ListNode slow = listNode;
while (k-- > 1) {
if (fast.next == null) {
return null;
}
fast = fast.next;
}
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
5.【删除】链表节点
如果题目的限制条件给的很足,完全可以写的很精简:
- 链表至少包含两个节点。
- 链表中所有节点的值都是唯一的。
- 给定的节点为非末尾节点并且一定是链表中的一个有效节点。
- 不要从你的函数中返回任何结果。
public void deleteNode(ListNode node) {
node.val = node.next.val;
node.next = node.next.next;
}
否则,就需要我们先去验证上述的 3 种情况之后,再使用上面的代码:
public static void deleteNode(ListNode head, ListNode deListNode) {
if (deListNode == null || head == null)
return;
if (head == deListNode) {
head = null;
} else {
// 若删除节点是末尾节点,往后移一个
if (deListNode.next == null) {
ListNode pointListNode = head;
while (pointListNode.next.next != null) {
pointListNode = pointListNode.next;
}
pointListNode.next = null;
}
// 关键代码
else {
deListNode.val = deListNode.next.val;
deListNode.next = deListNode.next.next;
}
}
}
补充:【删除】链表倒数第K个节点
思路:利用快慢指针法,定位并删除倒数第K个节点
public ListNode removeNthFromEnd(ListNode head, int n) {
// 这一部很重要,可以防止删除首节点时,next为空的情况
ListNode dummy = new ListNode(-1);
dummy.next = head;
// 快慢指针,fast 先走n步,然后一起走
ListNode slow = dummy;
ListNode fast = dummy;
while(n-- > 0){
fast = fast.next;
}
// 当fast.next为null时,slow 位于目标的前一个节点
while(fast.next != null){
slow = slow.next;
fast = fast.next;
}
// 跳过目标节点
slow.next = slow.next.next;
return dummy.next;
}
6.两个链表的第一个【公共节点】
思路一:使用两个指针 nodeA,nodeB 分别指向两个链表的头节点 headA,headB,然后同时遍历:
- 当nodeA节点到达链表A的结尾时,重新指向headB链表的头结点;
- 当nodeB结点到达链表B的结尾时,重新指向headA链表的额头结点。
public static ListNode FindFirstCommonNode1(ListNode pHead1, ListNode pHead2) {
ListNode p1 = pHead1;
ListNode p2 = pHead2;
// 注意这个位置,不是 nodeA.val != nodeB.val,
// 他们两个都是链表的结点,所以直接比较就行;
// 如果遇到 nodeA 这个结点和一个值的比较时,可以这样写:int a=1; nodeA.val == a;
while (p1 != p2){
p1 = (p1 != null ? p1.next : pHead2);
// nodaA 到达链表A的结尾时,重新指向 headB 的头结点哦,不是nodeB!!
p2 = (p2 != null ? p2.next : pHead1);
}
return p1;
}
思路二:先计算链表长度,链表长的先走,移动到和另一个链表的节点数相等的位置,然后再一起移动,判断是否相等。
public static ListNode FindFirstCommonNode2(ListNode pHead1, ListNode pHead2) {
if(pHead1 == null || pHead2 == null)
return null;
ListNode a = pHead1, b = pHead2;
int lengthA = length(pHead1), lengthB = length(pHead2);
if(lengthA > lengthB){
for(int i=0; i<lengthA-lengthB; i++)
a = a.next;
}else{
for(int i=0; i<lengthB-lengthA; i++)
b = b.next;
}
while(a != b){
a = a.next;
b = b.next;
}
return a;
}
public static int length(ListNode node){
ListNode tmp = node;
int count = 0;
while(tmp != null){
tmp = tmp.next;
count++;
}
return count;
}
7.链表中环的【入口节点】
思路一:快慢指针法,用两个指针,一个fast指针,每次走两步,一个slow指针,每次走一步。当fast指针与slow指针相遇时,再让fast指向链表头部,slow位置不变。同时走,同时每次一步,相遇点即为环起点。
public static ListNode EntryNodeOfLoop(ListNode pHead) {
ListNode fast = pHead;
ListNode slow = pHead;
while (fast != null && fast.next != null) {
// 条件里 fast.next != null,不然这里可能会出问题
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
fast = pHead;
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
return null;
}
思路二:用HashSet来解决,重复的节点就是起点
public static ListNode EntryNodeOfLoop2(ListNode pHead){
Set<ListNode> set = new HashSet<>();
ListNode head = pHead;
while (head != null) {
if (!set.add(head)) {
return head;
}
head = head.next;
}
return null;
}
8.【删除】排序链表中重复的节点
public ListNode deleteDuplicates(ListNode head) {
if(head == null) {
return head;
}
ListNode dummy = new ListNode(0, head);
ListNode cur = dummy;
while(cur.next != null && cur.next.next != null) {
if(cur.next.val == cur.next.next.val) {
int x = cur.next.val;
// 循环向下,一直删除重复元素
while(cur.next != null && cur.next.val == x) {
cur.next = cur.next.next;
}
} else {
cur = cur.next;
}
}
return dummy.next;
}
9.【删除】排序链表中重复的元素
head
,请你删除所有重复的元素,使每个元素 【
只出现一次】 。
public ListNode deleteDuplicates(ListNode head) {
if (head == null) {
return head;
}
ListNode cur = head;
while (cur.next != null){
if(cur.val == cur.next.val){
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return head;
}
10.【复杂链表】的复制
public static RandomListNode RandomClone (RandomListNode pHead) {
if(pHead == null)
return null;
RandomListNode head = new RandomListNode(pHead.val);
RandomListNode temp = head ;
while(pHead.next != null) {
temp.next = new RandomListNode(pHead.next.val);
if(pHead.random != null) {
temp.random = new RandomListNode(pHead.random.val);
}
pHead = pHead.next ;
temp = temp.next ;
}
return head ;
}
总结
- 欢迎大家留言补充~~