1.相交链表(难度等级:简单)
2个链表,很容易联想到双指针法。那么,指针该怎么移动,移动到什么时刻能代表考虑完这个问题呢?这里,我们还需要先思考一下。
其实,当两个指针共同移动完headA长度+headB长度后,若还未出现相交节点,既可以判定不存在相交节点了。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode tempA = headA, tempB = headB;
while(tempA != null || tempB != null){
if(tempA == tempB){
return tempA;
}
tempA = tempA==null ? headB : tempA.next;
tempB = tempB==null ? headA : tempB.next;
}
return null;
}
}
2.回文链表(难度等级:简单)
最直观想法就是,拿一个容器装顺序遍历的结果,然后再用双指针来判断值是否相等。
这种做法也是比较好写出来的。答案放在下面啦~
class Solution {
public boolean isPalindrome(ListNode head) {
if(head == null || head.next == null){
return true;
}
ArrayList<Integer> res = new ArrayList<>();
ListNode temp = head;
int m = 0;
while(temp != null){
res.add(temp.val);
temp = temp.next;
m++;
}
while(m>0){
if(head.val != res.get(m-1)){
return false;
}
m--;
head = head.next;
}
return true;
}
}
更进阶的,我们需要考虑能否采用O(1)的空间复杂度呢?那必然是得在链表本身进行修改了!我们可以反转链表的后半段,然后再用快慢指针去遍历链表。不过需要注意的是,在实际业务中,我们是不希望链表被修改的!!!所以严谨的做法需要再判断完成之后恢复原始数据。这个因为代码比较繁琐,所以面试时可以考虑优先写上面的解法~
class Solution {
public boolean isPalindrome(ListNode head) {
if (head == null) {
return true;
}
// 找到前半部分链表的尾节点并反转后半部分链表
ListNode firstHalfEnd = endOfFirstHalf(head);
ListNode secondHalfStart = reverseList(firstHalfEnd.next);
// 判断是否回文
ListNode p1 = head;
ListNode p2 = secondHalfStart;
boolean result = true;
while (result && p2 != null) {
if (p1.val != p2.val) {
result = false;
}
p1 = p1.next;
p2 = p2.next;
}
// 还原链表并返回结果
firstHalfEnd.next = reverseList(secondHalfStart);
return result;
}
private ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
private ListNode endOfFirstHalf(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast.next != null && fast.next.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
}
(注:上面的解法中包含了反转链表的写法,所以反转链表那道题就不再阐述了)
3.环形链表(难度等级:简单)
做这道题呢,首先需要有一个常识:若有环存在的话,一个快指针和一个慢指针会在环中相遇(类似于运动会中的“套圈”);如果没有环存在的话,快指针会移动到null。
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head.next.next;
while (slow != fast) {
if (fast == null || fast.next == null) {
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
}
4.环形链表 II(难度等级:中等)
区别于上一道题,这道题开始要求返回入环的节点了噢!
那么,我们就需要用数学去分析,如何表示入环节点了!数学推导可以得出结论,但我们只需记住:当发现 slow 与 fast 相遇时,我们再额外使用一个指针 ptr。起始,它指向链表头部;随后,它和 slow 每次向后移动一个位置。最终,它们会在入环点相遇。
public class Solution {
public ListNode detectCycle(ListNode head) {
if(head == null) return null;
ListNode slow = head, fast = head;
while(fast != null){
slow = slow.next;
if(fast.next != null){
fast = fast.next.next;
}else{
return null;
}
if(slow == fast){
fast = head;
while(slow != fast){
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
return null;
}
}
5.合并两个有序链表(难度等级:简单)
其实这一题,就很像数组里的“合并2个数组”了,自然而然地我们就能想到用双指针法。只不过这里,我们还要额外注意一下如何操作链表。
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if(list1 == null || list2 == null) return list1 == null ? list2 : list1;
ListNode res = new ListNode();
ListNode temp = res;
while(list2 != null || list1 != null){
if(list1 == null){
temp.next = list2;
break;
}
if(list2 == null){
temp.next = list1;
break;
}
if(list1.val <= list2.val){
temp.next = list1;
temp = temp.next;
list1 = list1.next;
}else{
temp.next = list2;
temp = temp.next;
list2 = list2.next;
}
}
return res.next;
}
}
6.两数相加(难度等级:中)
这道题的思路还是很好想的,跟上一道题很类似。但是我在动手写代码的时候,就发现不是这么回事了,譬如 创造了多余节点(即创造新节点的时机不对)、以为两个链表都走到最末就结束了(其实还要考虑最后一位节点相加是否产生进位,如果有还要再多创一个节点),以及写代码时考虑不周的问题!(括号有没有加,有没有考虑null值情况)
我想,这道题难度设置为中,也有上面因素的影响吧(hhh
话不多说,先附上我的垃圾解法:
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode res = null;
ListNode temp = res;
int add = 0, sum = 0;
while(l1 != null || l2 != null){
//如果上一个节点有进位,则需要加上进位
if(l1 == null || l2 == null) {
sum = (l1 == null ? l2.val : l1.val) + add;
}else{
sum = l1.val + l2.val + add;
}
//当前节点的加法和
int num = sum % 10;
if(temp == null){
res = temp = new ListNode(num);
}else{
temp.next = new ListNode(num);
temp = temp.next;
}
//获得当前加法对下一节点的进位
add = sum / 10;
if (l1 != null) {
l1 = l1.next;
}
if (l2 != null) {
l2 = l2.next;
}
}
//最后还要判断有没有上一个节点的进位来构成新的节点
if(add > 0) temp.next = new ListNode(add);
return res;
}
}
再看leetcode官方题解,你会发现逻辑一样,但它比我的优雅多了…
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode head = null, tail = null;
int carry = 0;
while (l1 != null || l2 != null) {
int n1 = l1 != null ? l1.val : 0;
int n2 = l2 != null ? l2.val : 0;
int sum = n1 + n2 + carry;
if (head == null) {
head = tail = new ListNode(sum % 10);
} else {
tail.next = new ListNode(sum % 10);
tail = tail.next;
}
carry = sum / 10;
if (l1 != null) {
l1 = l1.next;
}
if (l2 != null) {
l2 = l2.next;
}
}
if (carry > 0) {
tail.next = new ListNode(carry);
}
return head;
}
}
7. 删除链表的倒数第 N 个结点(难度等级:中)
这道题...本人今天貌似在xhs上面经刚见过。这种处理链表 倒数第n个节点 的问题呀,其实用快慢指针就可以解决——让快指针先走n步,随后快慢指针同时向前移动,当快指针移动到最后时,慢指针就移动到倒数第n个节点啦!
嘿!我心想这不是很简单嘛~说来就来,结果在敲代码过程中就发现考虑还是不周全了...
首先,需要删除倒数第n个节点,那么对于链表的操作来说,我们需要慢指针指向倒数第n+1个节点,才能进行删除。其次,如果需要删除的是头节点该怎么办呢?
这时候,我死去的回忆开始攻击我了:需要考虑删除头节点的情况???那造一个虚拟头节点不就完事了!于是,我的代码最终如下:
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
if(head == null || head.next == null) return null;
ListNode fast = new ListNode();
fast.next = head;
ListNode slow = fast;
//需要创造一个虚拟头节点,以防止原头节点被删除!
ListNode pre = fast;
while(n>0){
fast = fast.next;
n--;
}
//获取需要删除的第n个节点的前一个节点
while(fast != null && fast.next != null){
fast = fast.next;
slow = slow.next;
}
if(slow.next != null){
slow.next = slow.next.next;
}else{
slow.next = null;
}
return pre.next;
}
}
8.两两交换链表中的节点(难度等级:中)
这道题我原始思路是用迭代法做的,但做着做着发现不对劲:这好像不是单纯的2个2个节点单独交换了,还要考虑前后2个节点整体的连接关系。
然后我就突然脑洞大发——咦?操作都是一样的,那岂不是可以用递归啊!
于是我先尝试了递归,事实证明,递归的代码真的很短(但是空间复杂度会高于迭代法)
class Solution {
public ListNode swapPairs(ListNode head) {
//终止条件
if(head == null || head.next == null) {
return head;
}
//获取当前节点的下一个节点
ListNode nextHead = head.next;
//交换操作
head.next = swapPairs(nextHead.next);
nextHead.next = head;
return nextHead;
}
}
下面是leetcode官方的迭代法答案:
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
ListNode temp = dummyHead;
while (temp.next != null && temp.next.next != null) {
ListNode node1 = temp.next;
ListNode node2 = temp.next.next;
temp.next = node2;
node1.next = node2.next;
node2.next = node1;
temp = node1;
}
return dummyHead.next;
}
}
9.K 个一组翻转链表(难度等级:困难)
第一道困难题诞生了!看起来,它是上一道题的进阶版,延续上一题的思路试试看吧
需要注意的是,在翻转子链表的时候,我们不仅需要子链表头节点 head
,还需要有 head
的上一个节点 pre
,以便翻转完后把子链表再接回 pre
。
其实思路真的不难,但是代码会比较长,面试的时候非常容易出错!所以还是要多练练的~
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dump = new ListNode();
dump.next = head;
ListNode pre = dump;
while(head != null){
ListNode temp = pre;
//查看剩余节点是否小于长度k
for(int i=0; i<k; i++){
temp = temp.next;
if(temp == null){
return dump.next;
}
}
//不小于长度K,则开始操作
ListNode nextHead = temp.next; //下一组k节点的头
//操作当前k个节点
ListNode[] result = reverse(head, temp);
head = result[0];
temp = result[1];
//拼接回原链表
pre.next = head;
temp.next = nextHead;
//移动指针指向下k个待翻转节点
pre = temp;
head = temp.next;
}
return dump.next;
}
public ListNode[] reverse(ListNode head, ListNode tail){
ListNode prev = tail.next;
ListNode p = head;
while (prev != tail) {
ListNode nex = p.next;
p.next = prev;
prev = p;
p = nex;
}
return new ListNode[]{tail, head};
}
}
10.随机链表的复制(难度等级:中等)
唉,到这道题就不再是普通链表了噢,而是自定义的一种新的链式结构。不过,除了多了一个指针以外,好像也没有什么区别呢?那复制这个链表与复制普通链表也应该是差不多的处理逻辑吧?
but我在动手写的过程中就发现问题了!新链表的random可不能随便指呀,如果下一个random已经存在,就不能再创建新节点再指向了
OK这道题我是真没思路,让我们看看力扣官方的思路吧(记住它):
遇到没思路的题时,首先看题解,看完自己动手实现!如果实在写不出来,再copy代码噢
class Solution {
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
for (Node node = head; node != null; node = node.next.next) {
Node nodeNew = new Node(node.val);
nodeNew.next = node.next;
node.next = nodeNew;
}
for (Node node = head; node != null; node = node.next.next) {
Node nodeNew = node.next;
nodeNew.random = (node.random != null) ? node.random.next : null;
}
Node headNew = head.next;
for (Node node = head; node != null; node = node.next) {
Node nodeNew = node.next;
node.next = node.next.next;
nodeNew.next = (nodeNew.next != null) ? nodeNew.next.next : null;
}
return headNew;
}
}
11.排序链表(难度等级:中等)
对不起,看到这题,脑袋空空的我只会想到先遍历一遍将结果放进ArrayList,然后调用API将数组排成升序,最后构造新的链表。
但是考虑空间复杂度和时间复杂度的情况下,这肯定不是最优解啊!sort排序的复杂度就很高了...
ok又是没有思路的一题(这真的是中等难度嘛?!),不过当我看到答案的时候,好像有点get到意思了。其实,这一题属于排序问题,那自然而然就回想到回溯了呀!所以,用递归二分的方式,先“剪断”链表,直至拆分到只有<=2个节点相连。这个时候,就相当于只用处理2个节点的排序了!然后,我们再慢慢合并。这里需要注意的是!合并的时候,2组链表并不是随便连的噢,还要大小情况。最后还有一点也需要注意,不要忘记处理链表节点为单数的情况!
代码放在下面了,要好好品味呀~
class Solution {
public ListNode sortList(ListNode head) {
if(head == null || head.next == null){
return null;
}
//二分分割
ListNode fast = head;
ListNode slow = head;
while(fast != null){
fast = fast.next.next;
slow = slow.next;
}
ListNode nextHead = slow.next;
slow.next = null;
//递归
ListNode left = sortList(head);
ListNode right = sortList(nextHead);
//创造一个虚拟头节点
ListNode temp = new ListNode();
ListNode res = temp;
//合并节点
while(left != null && right != null){
if(left.val < right.val){
temp.next = left;
left = left.next;
}else{
temp.next = right;
right = right.next;
}
temp = temp.next;
}
//最后还要考虑最末节点(节点数为奇数的情况)
temp.next = left != null ? left : right;
return res.next;
}
}
12.合并 K 个升序链表(难度等级:困难)
困难题它又向我走来了啊啊啊
这里,需要用到的是分而治之的思想。其本质也是把一个复杂问题划分成多个重复操作的子问题,那么对于这道题,最小子问题应该就是2个有序链表的合并啦。这道题,我们再次用到了递归的思想,但是逻辑比较绕,要多看多练!
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
//递归终止条件
if(lists == null || lists.length == 0){
return null;
}
return merge(lists, 0, lists.length-1);
}
private ListNode merge(ListNode[] lists, int left, int right){
if(left == right) return lists[left];
int mid = left + (right - left) / 2; //下一次要合并的链表
//分而治之
ListNode l1 = merge(lists, left, mid);
ListNode l2 = merge(lists, mid+1, right);
//直到只有2组链表需要合并,返回合并结果
return mergeTwoLists(l1, l2);
}
//两个链表的合并
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) return l2;
if (l2 == null) return l1;
if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1,l2.next);
return l2;
}
}
}
13.LRU 缓存(难度等级:中等)
最近看很多大厂提前批的面经,都有这道题的出现,大家要格外注意噢~
emm很多弯弯绕绕没明白,但首先要记住,这里采用的数据结构是双向链表 + HashMap噢~很多面试官可能会要求手动封装一个双向链表,所以代码里我自己写了一个。HashMap是用于维护每个节点(data)的访问频率,双向链表是用于根据访问频率来维护缓存的(将访问频率高的节点放在链表头部)
至于为什么要用双向链表而不是单向链表,看到一个回答是这样解释的:
将某个节点移动到链表头部或者将链表尾部节点删去,都要用到删除链表中某个节点这个操作。你想要删除链表中的某个节点,需要找到该节点的前驱节点和后继节点。对于寻找后继节点,单向链表和双向链表都能通过 next 指针在O(1)时间内完成;对于寻找前驱节点,单向链表需要从头开始找,也就是要O(n)时间,双向链表可以通过前向指针直接找到,需要O(1)时间。
记住了数据结构,在实现过程中还要注意类的常量定义以及构造函数的写法(真的没有那么容易!多写写吧)
class LRUCache {
//双向链表
class DlistNode{
int key;
int value;
DlistNode next;
DlistNode prev;
public DlistNode(){}
public DlistNode(int _key, int _value) {
key = _key; value = _value;
}
}
private Map<Integer, DlistNode> freq = new HashMap<>();
private int size;
private int capacity;
private DlistNode head, tail;
public LRUCache(int capacity) {
this.size = 0; //当前存储量
this.capacity = capacity; //最大容量
//使用伪头部和伪尾部节点
head = new DlistNode();
tail = new DlistNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DlistNode res = freq.get(key);
if(res == null){
return -1;
}else{
moveToHead(res); //移动节点位置
return res.value;
}
}
public void put(int key, int value) {
DlistNode node = freq.get(key); //判断是否已经存在
if(node == null){ //存入链表
DlistNode newNode = new DlistNode(key, value);
freq.put(key, newNode);
addToHead(newNode); //放到链表的头部
size++;
if(size > capacity){
DlistNode tail = removeTail();
freq.remove(tail.key);
size--;
}
}else{
node.value = value;
moveToHead(node);
}
}
private void addToHead(DlistNode node){
node.prev = head; //头节点
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DlistNode node){
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DlistNode node){
removeNode(node);
addToHead(node);
}
private DlistNode removeTail(){
DlistNode res = tail.prev;
removeNode(res);
return res;
}
}
好了,链表终于完结撒花啦!总的来说,首先,你要知道链表的概念,以及如何操作链表(增删改)。其次,双指针(快慢指针等)是链表中非常常用的思想!还有一些问题需要一些小巧思,例如4、10。剩下的题,就是经常会涉及到递归思想的啦~所以,要学会将困难问题拆解,这在算法思想里一直都很重要!