摘要
【面试必刷101】系列blog目的在于总结面试必刷101中有意思、可能在面试中会被考到的习题。总结通用性的解题方法,针对特殊的习题总结思路。既是写给自己复习使用,也希望与大家交流。
【面试必刷101】递归/回溯算法总结I(十分钟理解回溯算法)
【面试必刷101】递归/回溯算法总结II(十分钟刷爆回溯算法题)
文章目录
1 链表基础知识
单链表
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
双链表
public class ListNode {
int val;
ListNode next = null;
ListNode pre = null;
ListNode(int val) {
this.val = val;
}
}
链表这类题感觉出的比较少,注意以下几个题型就可以了。
2 面试必刷习题
2.1 k个一组反转链表
普通的反转链表:
很简单的一张图,就是将当前节点指向前一个节点,然后将当前节点变成下一个节点来进行遍历。
import java.util.*;
public class Solution {
public class Solution {
public ListNode ReverseList(ListNode cur) {
ListNode pre = null;
ListNode next = null;
while (cur != null) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
然后来看链表中每k个节点一组翻转。
首先可以用递归来实现:每k个一组其实可以先做后面的再做前面的,大问题由很多小问题组成。
然后小问题可以用简单的链表翻转来做。具体看代码,还是很值得学习的。
import java.util.*;
public class Solution {
public ListNode reverseKGroup (ListNode head, int k) {
if (head == null || head.next == null) return head;
ListNode tail = head;
for (int i = 0; i < k; i++) {
if (tail == null) {
return head;
}
tail = tail.next;
}
// 翻转前k个元素
ListNode newHead = reverse(head, tail);
// head已经是最后一个元素了。这个递归很有灵性。
head.next = reverseKGroup(tail, k);
return newHead;
}
// 反转[head, tail)
public ListNode reverse (ListNode cur, ListNode tail) {
ListNode pre = null;
ListNode next = null;
while (cur != tail) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
2.2 合并k个链表
题目链接:BM5 合并k个已排序的链表
为啥这道题值得一做呢?主要考察点有三个:
- 合并策略是啥? 基于归并排序思路的两两合并;优先队列。
- 怎么合并两个有序链表? 建立一个头结点,谁小就插入谁。
import java.util.*;
public class Solution {
public ListNode mergeKLists(ArrayList<ListNode> lists) {
return mergeList(lists, 0, lists.size() - 1);
}
public ListNode mergeList(ArrayList<ListNode> list, int l, int r) {
if (l== r) {
return list.get(l);
}
if (l > r) {
return null;
}
int mid = l + ((r - l) >> 1);
ListNode left = mergeList(list, l, mid);
ListNode right = mergeList(list, mid + 1, r);
return merge2Lists(left, right);
}
// 合并两个链表
public ListNode merge2Lists(ListNode list1, ListNode list2) {
ListNode head = new ListNode(0);
ListNode cur = head;
while (list1 != null && list2 != null) {
if (list1.val > list2.val) {
cur.next = list2;
list2 = list2.next;
} else {
cur.next = list1;
list1 = list1.next;
}
cur = cur.next;
}
if (list1 != null) cur.next = list1;
if (list2 != null) cur.next = list2;
return head.next;
}
}
2.3 链表相加
题目链接:链表相加(二)
乍一眼看起来好难呀?该从哪里开始相加呢?不清楚。为啥不清楚,因为从next Node找不到pre Node,这就是单链表的特点。但是,如果翻转之后就很好做了。
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* }
*/
public class Solution {
/**
*
* @param head1 ListNode类
* @param head2 ListNode类
* @return ListNode类
*/
public ListNode addInList (ListNode head1, ListNode head2) {
ListNode h1 = reverse(head1);
ListNode h2 = reverse(head2);
int tag = 0;
int val = 0;
ListNode cur = null, pre = null;
while (h1 != null || h2 != null) {
if (h1 != null) {
val += h1.val;
h1 = h1.next;
}
if (h2 != null) {
val += h2.val;
h2 = h2.next;
}
val += tag;
if (val >= 10) {
tag = 1;
val = val % 10;
} else {
tag = 0;
}
cur = new ListNode(val);
val = 0;
cur.next = pre;
pre = cur;
}
if (tag == 1) {
cur = new ListNode(1);
cur.next = pre;
}
return cur;
}
public ListNode reverse (ListNode cur) {
ListNode pre = null;
ListNode next = null;
while (cur != null) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
2.4 单链表的排序
题目链接:BM12单链表的排序。
基于归并排序的思想来进行处理。首先进行划分,利用快慢指针找到中点,然后递归,最后讲左右的归并两个有序链表。注意的是:进行左右区间划分的时候需要将左右链表断开,即(slow.next = null)这一步非常重要。
直接上代码:
import java.util.*;
public class Solution {
public ListNode sortInList (ListNode head) {
if (head == null || head.next == null) return head;
// 基于快慢指针查找中点。
ListNode fast = head.next, slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
ListNode ne = slow.next;
slow.next = null;
// 递归实现左右的排序
ListNode left = sortInList (head);
ListNode right = sortInList(ne);
// 合并两个有序链表
ListNode h = new ListNode(0);
ListNode cur = h;
while (left != null && right != null) {
if (left.val < right.val) {
cur.next = left;
left = left.next;
} else {
cur.next = right;
right = right.next;
}
cur = cur.next;
}
if (left != null) cur.next = left;
if (right != null) cur.next = right;
return h.next;
}
}
2.5 判断一个链表是否是回文结构
BM13 判断一个链表是否为回文结构
这个利用的是一个小trick:判断回文结构只需要将后半部分翻转与前半部分做对比就好了,为哈嘞?因为如果整体翻转,在后半部分还是再进行了一次相同的对比。但是,队列的长度奇偶性似乎与划分结果有关。
基于这种思想找到的一定是中点,slow.next后半段的首字符。如果是奇数,那么slow指向的就是中央的奇数那个listNode,如果是偶数,那么slow指向的就是前半部分最后一个节点。所以用slow.next来进行处理,采用后半段的长度判空就可以实现判别了。
import java.util.*;
public class Solution {
public boolean isPail (ListNode head) {
// 找到中点
ListNode slow = head, fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 翻转
ListNode cur = slow.next;
ListNode pre = null;
ListNode next = null;
while (cur != null) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
// 采用后半段来判定
while (pre != null) {
if (head.val != pre.val) {
return false;
}
head = head.next;
pre = pre.next;
}
return true;
}
}
2.6 删除链表的重复元素
这道题就是注意方法(引入头结点)和边界条件(判空)。直接上代码
import java.util.*;
public class Solution {
public ListNode deleteDuplicates (ListNode head) {
if (head == null) return null;
ListNode h = new ListNode(0);
h.next = head;
ListNode cur = h;
while (cur.next != null && cur.next.next != null) {
if (cur.next.val == cur.next.next.val) {
int tmp = cur.next.val;
while (cur.next != null && cur.next.val == tmp) {
cur.next = cur.next.next;
}
} else {
cur = cur.next;
}
}
return h.next;
}
}
3 总结一下知识点
3.1 快慢指针
上面好几个题用到了“快慢指针”,这个小trick可以有以下作用:
(1)寻找中点
ListNode slow = head, fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
(2)判断链表中是否有环
if (head == null) return false;
// 通过快慢指针来做
ListNode slow = head, fast = head.next;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
return true;
}
}
return false;
(3)找到环的中点
我一直觉得这个是一个很棒的trick,可以证明这是正确的,这里就不耍大刀了,随便一搜就有很多了。
import java.util.*;
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead) {
ListNode slow = pHead, fast = pHead;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (slow == fast) {
// 此时存在环,需要进行判断入口是哪 2 (a + b) = a + b + n (b + c)
// a = (n - 1)(b + c) + c
// n == 1时,c==a
ListNode res = pHead;
while (res != slow) {
res = res.next;
slow = slow.next;
}
return res;
}
}
// 不存在环,返回空值
return null;
}
}
3.2 翻转链表
关于翻转链表,有很多解法,如递归啥的。但是我觉得下面这种是最好理解的,上文也介绍了,就看指针是咋变的。
ListNode pre = null;
ListNode next = null;
while (cur != null) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
注意可以有变体:不一定是要全员翻转,可以以k为长度进行翻转,此时就不是cur!= null了,如2.1所示。
3.3 合并链表
不用多说,这个很重要。
// 合并两个链表
public ListNode merge2Lists(ListNode list1, ListNode list2) {
ListNode head = new ListNode(0);
ListNode cur = head;
while (list1 != null && list2 != null) {
if (list1.val > list2.val) {
cur.next = list2;
list2 = list2.next;
} else {
cur.next = list1;
list1 = list1.next;
}
cur = cur.next;
}
if (list1 != null) cur.next = list1;
if (list2 != null) cur.next = list2;
return head.next;
}
3.4 归并的思想
合并k个链表和单链表的排序都用到了。就相当于,希望将O(n)降到O(logn)的复杂度,就可以考虑归并了,这种写法很精髓。
总结
需要做点有创造力的活。