面试笔试中链表的题目都比较死,几种类型反复做反复记忆就好了
本文整理了面试高频的十二个链表相关的算法题,覆盖了几乎所有常考的情况
刷熟了这几个题,再遇到手撕链表题就没太大压力了!
本文的每个标题都是leetcode中国站的直达链接,如果想要动手试一试就点击进去吧
本文的所有代码均为我学习了大量题解后写出的的较为优美的代码,推荐反复观看记忆
本题是典型的快慢指针法应用题,比较容易想到的就是采用额外的空间进行存储,如解法一的set存储。但是最经典的解法还是采用快慢指针,可以一次遍历并且不使用额外的空间,面试时使用快慢指针会是一个大的亮点
解法一:Hash查找法
public class Solution {
public boolean hasCycle(ListNode head) {
Set<ListNode> set = new HashSet<ListNode>();
ListNode node = head;
while (node != null) {
if (set.contains(node)) {
return true;
}
set.add(node);
node = node.next;
}
return false;
}
}
解法二:快慢指针法(推荐)
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode fast = head.next;
ListNode slow = head;
while (fast != slow) {
if (fast == null || fast.next == null) {
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
}
本题同样是对快慢指针法的典型应用,相比第一题更为巧妙,尽量理解,如果实在不能理解可以采用记忆,记住结论就好了
解法一:Hash记录法
public class Solution {
public ListNode detectCycle(ListNode head) {
Set<ListNode> set = new HashSet<ListNode>();
ListNode cur = head;
while (cur != null) {
if (set.contains(cur)) {
return cur;
}
set.add(cur);
cur = cur.next;
}
return null;
}
}
(F+2a+b+2x)=2(F+a+x)
,,解得F=bpublic class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (true) {
if (fast == null || fast.next == null) return null;
fast = fast.next.next;
slow = slow.next;
if (fast == slow) break;
}
fast = head;
//第二阶段,找环接入点
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
return fast;
}
}
public class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode pre = new ListNode(0);
ListNode cur = pre;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
cur.next = l1;
cur = cur.next;
l1 = l1.next;
} else {
cur.next = l2;
cur = cur.next;
l2 = l2.next;
}
}
//如果l1空了,就把l2接上去
if (l1 == null) {
cur.next = l2;
} else {
cur.next = l1;
}
return pre.next;
}
}
反转链表如果没有训练过可能不太好想象到,所以需要反复的多刷几次,动手多画一画
反转链表非常重要!!!! 在复杂的程序设计题中常作为一个小的部分,如后面的回文链表
原地反转比较好想到,递归就太妙了,建议就记住原地反转就好了,容易理解记忆
迭代法:原地反转(推荐)
public class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while (cur != null){
ListNode tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
}
public class Solution {
public ListNode reverseList(ListNode head) {
//递归终止条件
if (head == null || head.next == null) {
return head;
}
//下探
ListNode cur = reverseList(head.next);
head.next.next = head;
head.next = null;
//每一次都返回最后一个节点
return cur;
}
}
图片引自leetcode题解
两两交换也是很重要的一类题型,推荐熟练的写迭代的方式,并掌握原理,如果进阶可能会是交换多个结点或者以交换n个结点作为程序的一部分
解法一:递归(看别人写递归觉得自己就是傻子)
public class Solution {
//每次看人家写递归都觉得自己像个傻子,看得懂想不到
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode next = head.next;
head.next = swapPairs(next.next);
next.next = head;
return next;
}
}
解法二:迭代(推荐)
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode node = new ListNode(-1);
node.next = head;
ListNode pre = node;
while (pre.next != null && pre.next.next != null) {
ListNode l1 = pre.next, l2 = pre.next.next;
ListNode next = l2.next;
l1.next = next;
l2.next = l1;
pre.next = l2;
pre = l1;
}
return node.next;
}
}
本题的要点就是交替遍历两条路
已上图为例,一条路径遍历a-c-b,另一条遍历b-c-a
当两条路径遍历相遇时,就是交点
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) return null;
ListNode node1 = headA;
ListNode node2 = headB;
while (node1 != node2) {
node1 = node1 == null ? headB : node1.next;
node2 = node2 == null ? headA : node2.next;
}
return node1;
}
}
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null) return head;
ListNode node = head;
while (node.next != null && node != null) {
if (node.val == node.next.val){
node.next = node.next.next;
}else {
node = node.next;
}
}
return head;
}
递归实现:
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null) return head;
head.next = deleteDuplicates(head.next);
return head.val == head.next.val ? head = head.next : head;
}
本题思路:
一趟扫描:快慢指针法
public class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
if (head == null || head.next == null) return null;
ListNode fast = head;
ListNode slow = head;
//将fast和slow的步调差调成n,当fast到达末尾了,slow到达指定节点
while (n-- > 0) {
fast = fast.next;
}
//倒数的数就是头结点的情况,此时fast是null
if (fast == null){
return head.next;
}
//倒数的数不是头结点,按顺序移动fast和slow节点,直到倒数第二个,测试slow节点到达删除节点的上一个
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
//slow是待删除节点上一个节点,删除slow.next节点
slow.next = slow.next.next;
return head;
}
}
非常非常经典高频的考题,如果没有练过真的很难想象,一定要记得大致的步骤
public class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
//初始化stack
Stack<Integer> stack1 = buildStack(l1);
Stack<Integer> stack2 = buildStack(l2);
//新建个前驱结点方便返回
ListNode pre = new ListNode(-1);
int carry = 0;//进位数
while (!stack1.isEmpty() || !stack2.isEmpty() || carry != 0) {
int x = stack1.isEmpty() ? 0 : stack1.pop();
int y = stack2.isEmpty() ? 0 : stack2.pop();
int sum = x + y + carry;
ListNode node = new ListNode(sum % 10);//计算出本位的值
//将node插入到头结点
node.next = pre.next;
pre.next = node;
//计算是否有进位数
carry = sum / 10;
}
return pre.next;
}
private Stack<Integer> buildStack(ListNode head) {
ListNode node = head;
Stack<Integer> stack = new Stack<>();
while (node != null) {
stack.push(node.val);
node = node.next;
}
return stack;
}
}
public class Solution {
/**
* 快慢指针找到中点
* 选择一半进行翻转
* 翻转后再次比较
*/
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) return true;
ListNode slow = head;
ListNode fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
if (fast != null) slow = slow.next;//偶数个结点,让slow指向下一个,作为后半段的开头
//将链表分为以head和slow开头的等长的两段
cut(head, slow);
return isEqual(head, reverse(slow));
}
//翻转链表
private ListNode reverse(ListNode head) {
ListNode newHead = null;
while (head != null) {
ListNode tmp = head.next;
head.next = newHead;
newHead = head;
head = tmp;
}
return newHead;
}
//判定两个链表是否相等
private boolean isEqual(ListNode head1, ListNode head2) {
while (head1 != null && head2 != null) {
if (head1.val != head2.val) return false;
head1 = head1.next;
head2 = head2.next;
}
return true;
}
//分割链表
private void cut(ListNode head, ListNode slow) {
while (head.next != slow) {
head = head.next;
}
head.next = null;
}
}
public class Solution {
public ListNode oddEvenList(ListNode head) {
if (head == null || head.next == null) return head;
ListNode odd = head;//奇数节点
ListNode even = head.next;//偶数节点
ListNode evenHead = even;//暂存一个偶数节点头,方便重组完进行连接
while (even != null && even.next != null) {
odd.next = even.next;
odd = odd.next;
even.next = odd.next;
even = even.next;
}
//奇数链表后接上偶数链表
odd.next = evenHead;
return head;
}
}
public class Solution {
public ListNode[] splitListToParts(ListNode root, int k) {
if (root == null) return new ListNode[k];
ListNode cur = root;
int count = 0;
//统计节点数
while (cur != null) {
count++;
cur = cur.next;
}
ListNode[] nodes = new ListNode[k];
cur = root;
//每个子链表不超过1个节点
if (count <= k) {
for (int i = 0; i < count; i++) {
nodes[i] = new ListNode(cur.val);
cur = cur.next;
}
} else {
//计算每个子链表的节点数
int remain = count % k;
int preCount = count / k;
//记录每部分的节点个数
int[] counts = new int[k];
for (int i = 0; i < k; i++) {
//前remain个链表长度为precount+1
counts[i] = remain-- > 0 ? preCount + 1 : preCount;
}
//遍历链表,存储元素
for (int i = 0; i < k; i++) {
//初始化子链表的头结点和个数
int num = counts[i];
nodes[i] = cur;
while (--num > 0) {
cur = cur.next;
}
//截断链表
ListNode tmp = cur.next;
cur.next = null;
cur = tmp;
}
}
return nodes;
}
}