【我想找一份实习】算法篇
笔者注:之前为了准备蓝桥杯等系列算法比赛写了很多算法博客,也真的让自己在算法方面提升很大,收获了很多奖项。现在,目标变成了【我想找一份实习】,所以,这一系列文章,将会以实习为导向,完成算法、八股文等多方面维度的学习和准备,希望能在最后拿到一份自己想要的实习。
本文为“算法篇”,学习路径源自大佬labuladong:https://labuladong.github.io/algo/
数据结构
链表
21 合并两个有序链表(简单)
基本操作,注意使用虚拟头结点,方便结果处理
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
// 虚拟头结点
ListNode dummy = new ListNode();
ListNode p = dummy;
while (list1 != null && list2 != null) {
if (list1.val > list2.val) {
p.next = list2;
list2 = list2.next;
} else {
p.next = list1;
list1 = list1.next;
}
// 新链表的节点继续往下走
p = p.next;
}
if (list1 == null) {
// 说明list2可能还有剩
p.next = list2;
}
if (list2 == null) {
// 说明list1可能还有剩
p.next = list1;
}
return dummy.next;
}
}
86 分隔链表(中等)
具体来说,我们可以把原链表分成两个小链表,一个链表中的元素大小都小于 x,另一个链表中的元素都大于等于 x,最后再把这两条链表接到一起,就得到了题目想要的结果。
注意合并时,要将p2.next置为null,否则当最后一个节点值小于x时,合并后会成环,可以画图试试
class Solution {
public ListNode partition(ListNode head, int x) {
ListNode dummy1 = new ListNode();
ListNode dummy2 = new ListNode();
ListNode p1 = dummy1;
ListNode p2 = dummy2;
while (head != null) {
if (head.val < x) {
// 放p1中
p1.next = head;
p1 = p1.next;
} else {
// 放p2中
p2.next = head;
p2 = p2.next;
}
head = head.next;
}
// 连接两个链表,注意要让更大的链表的next=null否则当最后一个节点值小于x时,会成环
p2.next = null;
p1.next = dummy2.next;
return dummy1.next;
}
}
※23 合并K个升序链表(困难)
合并 k 个有序链表的逻辑类似合并两个有序链表,难点在于,如何快速得到 k 个节点中的最小节点,接到结果链表上?
这里我们就要用到 优先级队列(二叉堆) 这种数据结构,把链表节点放入一个最小堆,就可以每次获得 k 个节点中的最小节点:
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> pq = new PriorityQueue<>(new Comparator<>(){
@Override
public int compare(ListNode a, ListNode b) {
return a.val - b.val; // 降序排列
}
});
for (ListNode node : lists) {
if (node != null) {
// 先只添加k个链表的头结点,后面在遍历时把头结点之后的结点加入
pq.add(node);
}
}
// 和前面合并有序链表一样
ListNode dummy = new ListNode();
ListNode p = dummy;
while (!pq.isEmpty()) {
// 获取所有链表的当前最小节点放入新链表中
ListNode tmp = pq.poll();
p.next = tmp;
// 对于当前拿走了一个结点的链表,还需要把剩余节点加入pq队列
if (tmp.next != null) {
pq.add(tmp.next);
}
// 新链表的节点继续往前走
p = p.next;
}
return dummy.next;
}
}
※19 删除链表的倒数第 N 个结点(中等)
这道题涉及一个问题:如何快速找到倒数第N个节点?
画图解决:
1、先让一个节点p1,从头结点开始先走N步
2、引入一个新的节点p2,指向头结点
3、p1、p2同时走,当p1 == null时,p2所在位置即为倒数第N个节点
ListNode p1 = head;
// 先走k步
for (int i = 0; i < k; i++) {
p1 = p1.next;
}
ListNode p2 = head;
// 再同时走
while (p1 != null) {
p2 = p2.next;
p1 = p1.next;
}
// 最后p2所指即为所需节点
return p2;
但是本题要删除倒数第N个节点,那我们就得找到倒数第N+1个节点,但是为了避免需要删除头结点而找不到第N+1个节点的情况,可以给原始链表加上一个虚拟头结点:
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
// 虚拟头结点,给head再增加一个节点,避免删除头结点时出错
// 如果链表长度=4,n=4,我们现在要去找第n+1个节点,找不到呀?
ListNode dummy = new ListNode();
dummy.next = head;
// 有了虚拟头结点就可以处理删除头结点的问题
ListNode p1 = dummy;
// 删除倒数第 n 个,要先找倒数第 n + 1 个节点
n += 1;
for (int i = 0; i < n; i++) {p1 = p1.next;}
// 再来个p2 和 p1一起走
ListNode p2 = dummy;
while (p1 != null) {
p1 = p1.next;
p2 = p2.next;
}
// 现在p2所处位置就是要删除节点的前一个节点
// 删掉倒数第 n 个节点
p2.next = p2.next.next;
return dummy.next;
}
}
※876 链表的中间结点(简单)
经典问题了,之前也刷过几遍,就是快慢指针
class Solution {
public ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next; // 如果是偶数个节点,返回的是中间靠后的节点
}
return slow;
}
}
环形链表三连问: 1. 是否有环(141) 2. 找出环的入口(142) 3. 环中节点个数
※141 环形链表(简单)
同上,依旧使用快慢指针,如果有环,则快指针必然会和慢指针相遇:
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) return true;
}
return false;
}
}
※142 环形链表Ⅱ(中等)
在上一题基础上需要找到入环结点,也是模板题:
关键是让slow指针最后要重新指向头结点,然后再让slow、fast指针一起走,最后相遇的地方就是入环结点
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
// 先判断有无环
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
// 成环了
break;
}
}
// 若无环则直接null
if (fast == null || fast.next == null) {
return null;
}
// 让slow指针指向头结点,再和fast指针一起走,直到两者相遇点就是入环点
slow = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
环中结点个数
slow与fast相遇在环的入口, 让fast或者slow单独在环里再走一圈, 并进行计数, 当fast.next = slow时, 返回count+1,就是环中节点的个数。
160 相交链表(简单)
如果两个链表的长度一致的话还好处理,直接从head开始往后遍历,判断有相同的结点[==],就说明是相交的,关键是长度不相同,但注意:相交点之后的两条链表合为一条链,那我们就让长的链表先把多的结点数走掉,再让两个链表一起走,如果相交,走到后面总会有结点一致;如果不相交,到最后都为null,也一样了。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode pA = headA;
ListNode pB = headB;
int lenA = 0;
int lenB = 0;
while (pA != null) {
lenA++;
pA = pA.next;
}
while (pB != null) {
lenB++;
pB = pB.next;
}
// 让长的链表先走一段
if (lenA > lenB) {
for (int i = 0; i < lenA - lenB; i++) {
headA = headA.next;
}
} else {
for (int i = 0; i < lenB - lenA; i++) {
headB = headB.next;
}
}
// 最后再判断是否相交,不相交也能正确返回null
while (headA != headB) {
headA = headA.next;
headB = headB.next;
}
return headA;
}
}
206 反转链表(简单)
用递归可以写得很简洁明了,因为递归的重点在于函数含义的理解,而不用真正搞懂内部递归调用的过程。
递归函数定义为:输入一个节点 head,将「以 head 为起点」的链表反转,并返回反转之后的头结点。
那么,就可以画图进行分析:
关键是最后这里,head.next = null,一方面是断开head正向的连接,另一方面也让反转后的最后一个节点的next=null。
同时要注意base基础条件,没有结点、或只有一个结点,反转就是自身,直接返回即可。
// 定义:输入一个单链表头结点,将该链表反转,返回新的头结点
ListNode reverse(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode last = reverse(head.next);
head.next.next = head;
head.next = null;
return last;
}
迭代解法:
/**
* 迭代方法
* 1 -> 2 -> 3 -> 4 -> null
* null <- 1 <- 2 <- 3 <- 4
*
* @param head
* @return
*/
public static ListNode reverseListIterative(ListNode head) {
ListNode prev = null; //前指针节点
ListNode curr = head; //当前指针节点
//每次循环,都将当前节点指向它前面的节点,然后当前节点和前节点后移
while (curr != null) {
ListNode nextTemp = curr.next; //临时节点,暂存当前节点的下一节点,用于后移
curr.next = prev; //将当前节点指向它前面的节点
prev = curr; //前指针后移
curr = nextTemp; //当前指针后移
}
return prev;
}
反转链表前N个结点
// 将链表的前 n 个节点反转(n <= 链表长度)
ListNode reverseN(ListNode head, int n)
之前反转是让头结点的next直接等于null,但是这里需要将头结点的next连接到后驱结点(第n+1个结点),因为只需要反转一部分。
class Solution {
ListNode postNode;
public ListNode reverseList(ListNode head, int n) {
if (n == 1) { // 到达了需要反转的最后一个结点
postNode = head.next; // 记录n+1后驱结点
return head; // 避免只有单个结点的情况
}
// 下面和反转整个链表的过程差不多,区别就是最后还需要连接到后驱节点上
ListNode last = reverseList(head.next, n - 1);
head.next.next = head;
// 这里下一个结点要连接到后驱节点上
head.next = postNode;
return last;
}
}
92 反转链表 II(中等)
现在需要按照题目需求,反转一个范围内[m, n]的链表,链表的索引从1开始,当m==1时,就相当于上一题反转前N个结点,当m != 1时,怎么办?
如果我们把 head 的索引视为 1,那么我们是想从第 m 个元素开始反转对吧;如果把 head.next 的索引视为 1 呢?那么相对于 head.next,反转的区间应该是从第 m - 1 个元素开始的;那么对于 head.next.next 呢……
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
if (left == 1) {
// 就相当于是反转前right个结点
return reverseN(head, right);
}
// 如果不是从第一个结点开始,那就把下一个结点当作第一个结点
head.next = reverseBetween(head.next, left - 1, right - 1);
return head;
}
// 反转前N个结点的函数,别忘了记录后驱结点
ListNode postNode = null;
public ListNode reverseN(ListNode head, int n) {
if (n == 1) {
postNode = head.next; // 找到n+1结点作为后去节点
return head;
}
ListNode last = reverseN(head.next, n - 1);
head.next.next = head;
// 这里要连接到后驱结点
head.next = postNode;
return last;
}
}
迭代写法
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode dummy = new ListNode();
dummy.next = head; // 避免边界情况
// 走left-1到达leftNode的preNode
ListNode leftPrev = dummy;
for (int i = 0; i < left - 1; i++) {
leftPrev = leftPrev.next;
}
// 在leftPrev的基础上再走right - left + 1到达rightEnd结点
ListNode rightEnd = leftPrev;
for (int i = 0; i < right - left + 1; i++) {
rightEnd = rightEnd.next;
}
// 再记录下leftNext、rightNext结点,用于最后反转后的连接
ListNode leftNext = leftPrev.next;
ListNode rightNext = rightEnd.next;
// 断开左右结点的连接
leftPrev.next = null;
rightEnd.next = null;
// 从leftNode开始反转
ListNode newHead = reverse(leftNext);
// 再连接起来
leftPrev.next = newHead;
leftNext.next = rightNext;
return dummy.next;
}
// 简单的链表反转函数
public ListNode reverse(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
ListNode tmp = cur.next;
cur.next = pre;
pre = cur; // pre指针往后移动
cur = tmp;
}
return pre;
}
}
值得一提的是,递归操作链表并不高效。和迭代解法相比,虽然时间复杂度都是 O(N),但是迭代解法的空间复杂度是 O(1),而递归解法需要堆栈,空间复杂度是 O(N)