链表面试算法题
本章节主要总结了关于单链表的高频面试题。
本章的链表定义
public class ListNode {
public int val;
public ListNode next;
public ListNode(int x) {
val = x;
next = null;
}
public static void main(String[] args) {
ListNode listnode=new ListNode(1);
}
}
两个链表的第一个公共子节点问题
链表相交
两个链表的头结点都是已知的,相交之后成为一个单链表,但是相交的位置未知,并且相交之前的结点数也是未知的,请设计算法找到两个链表的合并点。
题目的解法:
如果没有思路的话,把 将常用数据结构和常用算法思想都想一遍
数组、链表、队列、栈、Hash、集合、树、堆
- 暴力法:类似于冒泡排序,将第一个链表中的各个结点与第二个链表中的各个结点进行比较,当出现相等的结点指针时,即为相交结点。
- Hash。第一个集合存放在map,set,或者Arraylist集合中,利用constains方法检测第二个链表中哪个结点出现过,如果有相同的节点指针,即找到。
- 栈。将两个链表分别压入不同的栈中,同时弹出栈顶元素,判断该元素是否一致。一致则存在相交。当首次出现不相同时,则最晚出栈的那一组即要找的位置。
- 拼接法(难想到)。链表A:2-3-4-5 链表B:b-4-5;将其组合为AB,BA。以公共的起始点分成两部分,就可以得到LA RA LB RB 和 LB RB LA RA。实际上是RB 和RA相同的时候,则得到了两个链表的合并点。
- 差和双指针。当链表A,链表B的长度相同时,两个指针同时开始遍历元素时,才有机会对等的遇到两个链表的合并点。设链表A 的长度为lenA,设链表B 的长度为lenB。那么,|lenA - lenB|的差值,是长度更长的先走|lenA -lenB|步,然后再同时向前走,结点一样的时候就是公共结点了。
// 利用set
public static ListNode findFirstCommonNodeBySet(ListNode headA, ListNode headB) {
Set<ListNode> set = new HashSet<>();
while (headA != null) {
set.add(headA);
headA = headA.next;
}
while (headB != null) {
if (set.contains(headB))
return headB;
headB = headB.next;
}
return null;
}
// 利用栈
public static ListNode findFirstCommonNodeByStack(ListNode headA, ListNode headB) {
Stack<ListNode> stackA = new Stack();
Stack<ListNode> stackB = new Stack();
while (headA != null) {
stackA.push(headA);
headA = headA.next;
}
while (headB != null) {
stackB.push(headB);
headB = headB.next;
}
ListNode preNode = null;
while (stackB.size() > 0 && stackA.size() > 0) {
if (stackA.peek() == stackB.peek()) {
preNode = stackA.pop();
stackB.pop();
} else {
break;
}
}
return preNode;
}
// 利用序列拼接
public static ListNode findFirstCommonNodeByCombine(ListNode pHead1, ListNode pHead2) {
if (pHead1 == null || pHead2 == null) {
return null;
}
ListNode p1 = pHead1;
ListNode p2 = pHead2;
while (p1 != p2) {
p1 = p1.next;
p2 = p2.next;
if (p1 != p2) {
if (p1 == null) {
p1 = pHead2;
}
if (p2 == null) {
p2 = pHead1;
}
}
}
return p1;
}
// 利用差 和 指针
public static ListNode findFirstCommonNodeBySub(ListNode pHead1, ListNode pHead2) {
if (pHead1 == null || pHead2 == null) {
return null;
}
int lenA=0,lenB=0;
// 获取长度
ListNode curNode=pHead1;
while(curNode!=null){
curNode=curNode.next;
lenA++;
}
curNode=pHead2;
while(curNode!=null){
curNode=curNode.next;
lenB++;
}
// 获取差值
curNode=pHead1;
ListNode curNode2=pHead2;
int sub=Math.abs(lenA-lenB);
if(lenA>=lenB){
int count=0;
while (count<sub){
curNode=curNode.next;
count++;
}
}else {
int count=0;
while (count<sub){
curNode2=curNode.next;
count++;
}
}
while (curNode!=curNode2){
curNode=curNode.next;
curNode2=curNode2.next;
}
return curNode;
}
链表是否为回文链表
判断一个链表是否为回文链表
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
- 逃避链表,选择数组(面试别这么干)。从数组的两端到中间进行对比。
- 利用栈+遍历。整体的思路是将链表的元素全部压栈中,一边出栈,一遍对比即可。在此思路上,我们想一下,是不是只需要压栈链表一半的元素就可以了:那么先遍历一遍链表,获得总长度、一边遍历链表,一边压栈;到达链表的一半时,不再压栈,选择出栈,一边遍历,一边比较。在此思路上,我们想,既然都需要遍历获取总长度,能不能只遍历一次链表呢:可以一边遍历元素压栈,第二次比较时,只比较一半的元素,即只将一半的元素出栈。
- 反转链表法。创建一个链表newList,将原始链表的元素值逆序保存到newList中,然后重新遍历两个链表,一遍比较元素的值,只要有一个位置元素不一样,就不是回文链表。基于此,与2中类似,也可以只反转一半:先遍历一遍,获取总长度。然后重新遍历到达一半的位置后不再反转,就开始比较两个链表。
- 双指针法(快慢指针)+反转链表。fast一次走两步,slow一次走一步。当fast到尾部时,slow刚好到一半的位置。那么接下来可以从头开开始逆序或者从slow开始逆序一半的元素。
// 利用栈
public static boolean isPalindromeByAllStack(ListNode head) {
ListNode temp = head;
Stack<Integer> stack = new Stack();
//把链表节点的值存放到栈中
while (temp != null) {
stack.push(temp.val);
temp = temp.next;
}
//然后再出栈
while (head != null) {
if (head.val != stack.pop()) {
return false;
}
head = head.next;
}
return true;
}
// 栈 部分压栈
public static boolean isPalindromeByHalfStack(ListNode head) {
if(head==null) return true;
ListNode cur=head;
Stack<Integer> stack=new Stack<>();
int len=0;
while (cur!=null){
stack.push(cur.val);
cur=cur.next;
len++;
}
len>>=1; // len/2
cur=head;
// 出栈
while(len-->0){
if(cur.val !=stack.pop()){
return false;
}
cur=cur.next;
}
return true;
}
// 利用双指针
public static boolean isPalindromeByTwoPoints(ListNode head) {
if (head == null || head.next == null) {
return true;
}
ListNode slow = head, fast = head;
ListNode pre = head, prepre = null;
while (fast != null && fast.next != null) {
pre = slow;
slow = slow.next;
fast = fast.next.next;
pre.next = prepre; // 顺序不可改
prepre = pre;
}
if (fast != null) {
slow = slow.next;
}
while (pre != null && slow != null) {
if (pre.val != slow.val) {
return false;
}
pre = pre.next;
slow = slow.next;
}
return true;
}
合并有序链表
合并有序链表
将两个升序链表合并为一个新的升序链表并返回,新链表是通过拼接给定的两个链表的所有节点组成的。
- 新建一张表,分别遍历两个链表,每次都选择最小的结点接到新链表上,最后排完。
- 将另外一个链表的结点拆下来,逐个合并到另外一个对应位置上去。
// 方案1
public static ListNode mergeTwoListsMoreSimple(ListNode l1, ListNode l2) {
ListNode prehead = new ListNode(-1); //虚拟头结点
ListNode prev = prehead;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
prev.next = l1;
l1 = l1.next;
} else {
prev.next = l2;
l2 = l2.next;
}
prev = prev.next;
}
// 最多只有一个还未被合并完,直接接上去就行了
prev.next = l1 == null ? l2 : l1;
return prehead.next;
}
合并k个链表
在合并有序链表的基础上,先将前两个合并,然后再讲后面的合并进来。
public static ListNode mergeKLists(ListNode[] lists) {
ListNode res = null;
for (ListNode list : lists) {
res = mergeTwoListsMoreSimple(res, list);
}
return res;
}
寻找中间结点
寻找中间结点
给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
- 两个指针slow、fast,一起遍历链表。slow一次走一步,fast一次走两步。当fast走到链表的末尾时,slow必然在中间位置。
public static ListNode middleNode(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
寻找倒数第k个元素
输入一个链表,输出该链表中倒数第k个节点。本题从1开始计数,即链表的尾节点是倒数第1个节点。
示例:给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
- 快慢双指针。fast向走k+1步,到达第k+1个节点,slow指向链表的第一个结点。此时,指针fast和slow二者之间刚好间隔k个节点,之后同步向后走,当fast走到链表的尾部空节点时,slow指针刚好指向链表的倒数第k个节点。
- 栈。
public static ListNode getKthFromEnd(ListNode head, int k) {
ListNode fast = head;
ListNode slow = head;
while (fast != null && k > 0) {
fast = fast.next;
k--;
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
旋转链表
旋转链表
给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
- 反转链表法。整个链表反转变成{5,4,3,2,1},然后再将前K和N-K两个部分分别反转,也就是分别变成了{4,5}和{1,2,3}
- 双指针法。找到倒数K的位置,也就是{1,2,3}和{4,5}两个序列,之后再将两个链表拼接成{4,5,1,2,3} 。再具体的来分析,快指针走k,慢指针在1处,快指针到终点时,慢指针在刚好需要断开的位置。那么快指针指向的结点指向头部,慢指针指向的结点断开与下一个结点的联系(null)。返回慢指针指向结点的下一个结点(恰好断开的位置的下一个)。
// 利用快慢指针
public static ListNode rotateRight(ListNode head, int k) {
if (head == null || k == 0) {
return head;
}
ListNode temp = head;
ListNode fast = head;
ListNode slow = head;
int len = 0;
while (head != null) {
head = head.next;
len++;
}
if (k % len == 0) {
return temp;
}
while ((k % len) > 0) {
k--;
fast = fast.next;
}
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
ListNode res = slow.next;
slow.next = null;
fast.next = temp;
return res;
}
删除链表特定结点
删除指定结点
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回新的头节点 。
删除节点cur时,必须知道其前驱pre节点和后继next节点,然后让pre.next=next
- 虚拟头结点。dummyNode。循环链表找目标元素,遇到目标元素【cur.next=cur.next.next】。注意返回值是dummyNode.next
public static ListNode removeElements(ListNode head, int val) {
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
ListNode temp = dummyHead;
while (temp.next != null) {
if (temp.next.val == val) {
temp.next = temp.next.next;
} else {
temp = temp.next;
}
}
return dummyHead.next;
}
删除链表倒数第n个节点
删除链表的倒数第n个结点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
- 栈。将所有的元素压栈,弹出的第N个时候,就是需要删除的。
- 双指针。来寻找倒数第n个结点,然后执行删除的操作。
- 遍历长度。先遍历一遍,获取总长度len;然后重新遍历,位置[ len-n+1 ]就是我们需要删除的。
// 利用栈
public static ListNode removeNthFromEndByStack(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
Deque<ListNode> stack = new LinkedList<ListNode>();
ListNode cur = dummy;
while (cur != null) {
stack.push(cur);
cur = cur.next;
}
for (int i = 0; i < n; ++i) {
stack.pop();
}
ListNode prev = stack.peek(); //获取但是不弹出
prev.next = prev.next.next;
ListNode ans = dummy.next;
return ans;
}
// 双指针
public static ListNode removeNthFromEndByTwoPoints(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode first = head;
ListNode second = dummy;
for (int i = 0; i < n; ++i) {
first = first.next;
}
while (first != null) {
first = first.next;
second = second.next;
}
second.next = second.next.next;
ListNode ans = dummy.next;
return ans;
}
// 遍历求长度
public static ListNode removeNthFromEndByLength(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
int length = getLength(head);
ListNode cur = dummy;
for (int i = 1; i < length - n + 1; ++i) {
cur = cur.next;
}
cur.next = cur.next.next;
ListNode ans = dummy.next;
return ans;
}
删除链表中的重复元素
重复元素保留一个
给定一个升序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
- 由于给定的链表是排好序的,因此重复的元素在链表中出现的位置是连续的,因此我们只需要对链表进行一次遍历,就可以删除重复的元素。具体地,我们从指针 cur 指向链表的头节点,随后开始对链表进行遍历。如果当前 cur 与cur.next 对应的元素相同,那么我们就将cur.next 从链表中移除;否则说明链表中已经不存在其它与cur 对应的元素相同的节点,因此可以将 cur 指向 cur.next。当遍历完整个链表之后,我们返回链表的头节点即可。另外要注意的是 当我们遍历到链表的最后一个节点
public static ListNode deleteDuplicate(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;
}
重复元素都不保留
给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
- 当一个都不要时,链表只要直接对cur.next 以及 cur.next.next 两个结点进行比较就行了,这里要注意两个node可能为空,稍加判断就行了。
public static ListNode deleteDuplicates(ListNode head) {
if (head == null) {
return head;
}
ListNode dummy = new ListNode(0);
dummy.next = 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;
}