欢迎关注公众号(通过文章导读关注:【11来了】),及时收到 AI 前沿项目工具及新技术的推送!
在我后台回复 「资料」 可领取
编程高频电子书
!
在我后台回复「面试」可领取硬核面试笔记
!文章导读地址:点击查看文章导读!
感谢你的关注!
阿里秋招高频算法题汇总(中级篇)
这里讲一下阿里秋招中的高频算法题,分为三个部分: 基础篇 、 中级篇 、 进阶篇
目的就是为了应对秋招中的算法题,其实过算法题的诀窍就在于 理解的基础上 + 背会
看到一个题目,首先要了解题目考察的算法是什么,这个算法要理解,至于具体实现的话,就靠背会了(多写、多练),没有什么捷径!
还有一点要注意的是,在大厂的比试中, 可能考察算法的方式是 ACM 模式 ,这一点和力扣上不同,ACM 模式需要我们自己去引入对应的包,以及自己写算法,力扣是将方法框架给定,只需要在方法内写代码就可以了,这一点要注意!
接下来开始阿里秋招算法的算法讲解,文章内的题目都在 LeetCode 上,因此这里只列出对应的题目序号、题目简介!
中级篇
在中级篇中主要考察的算法更加偏向于 链表 、 动态规划 这两个方面,包括:
- 双链表的实现以及基于双链表实现 LRU
- 动态规划
- 链表操作
LC 146. LRU 缓存(中等)
题目简述:
请你设计并实现一个满足LRU (最近最少使用) 缓存约束的数据结构。
实现 LRUCache
类:
LRUCache(int capacity)
以 正整数 作为容量capacity
初始化 LRU 缓存int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回-1
。void put(int key, int value)
如果关键字key
已经存在,则变更其数据值value
;如果不存在,则向缓存中插入该组key-value
。如果插入操作导致关键字数量超过capacity
,则应该 逐出 最久未使用的关键字。
LRU 缓存淘汰策略还是比较常用的,并且实现起来不算复杂,同时考察了对基础数据结构的掌握,因此在面试或者笔试中出现的概率还是不小的,建议要好好掌握一下
实现的话,我们自己定义一个 Node 数据结构,并定义 prev 指针 和 next 指针 ,来自己实现一个双向链表,常用的元素在链表头部,不常用的在尾部,我们向链表中插入一个 虚拟头节点 dummy 就可以在 O(1) 的时间复杂度内获取到头节点和尾节点
如果新插入元素的话,就放在头节点,如果查询一个元素,就将该元素移动到链表头,表示最近刚使用过,如下:
class LRUCache {
// 定义节点,实现双向链表
private static class Node {
int k, v;
Node prev,next;
Node (int k, int v) {
this.k = k;
this.v = v;
}
}
// 虚拟头节点
Node dummy = new Node(-1, -1);
// 存储 key 对应的 Node 节点
Map<Integer, Node> nodes = new HashMap<>();
// LRU 缓存容量
int capacity;
// LRU 中元素数量
int size;
// 初始化
public LRUCache(int capacity) {
this.capacity = capacity;
this.size = 0;
dummy.prev = dummy;
dummy.next = dummy;
}
public int get(int key) {
Node node = nodes.get(key);
if (node == null) return -1;
pushToFront(node);
return node.v;
}
// 将 node 节点移动至链表头
private void pushToFront(Node node) {
// 将 node 从当前位置删除
removeNode(node);
addToFront(node);
}
// 将 node 放到链表头
private void addToFront(Node node) {
node.prev = dummy;
node.next = dummy.next;
dummy.next.prev = node;
dummy.next = node;
}
public void put(int key, int value) {
Node node = nodes.get(key);
// 如果节点不为空,更新值,并放入头部
if (node != null) {
node.v = value;
nodes.put(key, node);
pushToFront(node);
} else {
// 如果节点为空,插入新的节点
size ++;
// 如果超过 LRU 容量,移除最长最久未使用的节点
if (size > capacity) {
Node tail = dummy.prev;
// 移除最后一个不常使用的节点
nodes.remove(tail.k);
removeNode(tail);
this.size --;
}
node = new Node(key, value);
addToFront(node);
nodes.put(key, node);
}
}
// 移除指定节点
private void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
// 打印 LRU 中存储的数据情况,方便看出哪些数据被淘汰
public void printLRUCache() {
Node node = dummy.next;
while (node != dummy) {
System.out.print("k=" + node.k + ":v=" + node.v + " ");
node = node.next;
}
System.out.println();
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
LC 22. 括号生成(中等)
题目描述:
给定一个数字 n ,生成所有有效的括号组合
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
这里就是枚举出来所有有效的括号组合,必须保证 每一个左括号都有一个右括号与之对应
保证括号组合有效的话,我们可以通过剩余未使用的左括号和右括号的数量来快速判断:
- 如果剩余的左括号的数量 大于 右括号的数量,那么说明会存在部分左括号找不到对应的右括号对应,因此肯定不合法
这里解题的话直接使用 dfs 枚举所有的情况,也就是对当前字符串,加左括号和加右括号两种情况都试一下,将不合法的情况给及时回溯掉就可以了
class Solution {
List<String> res = new ArrayList<>();
public List<String> generateParenthesis(int n) {
dfs("", n, n);
return res;
}
// str 表示当前枚举的括号,l 表示剩余可用左括号、r 表示剩余可用右括号
public void dfs(String str, int l, int r) {
// 将错误情况排除掉
if (l < 0 || l > r) return;
// 如果括号用完了,就加入到结果集
if (l == 0 && r == 0) {
res.add(str);
return;
}
// 接下来,要么加左括号,要么加右括号
dfs(str + "(", l - 1, r);
dfs (str + ")", l, r - 1);
}
}
LC 206. 反转链表(简单)
题目描述:
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
这道题目代码不算太难,只是稍微有点绕,而且这一种链表的题目还是比较常见的
这里我们在 原地进行反转链表 ,不额外申请空间(额外申请空间的话,就比较简单了)
那么只需要定义一个 last 、 next 、 now 节点,用来存储当前节点以及上一个和下一个节点,就可以进行翻转了,如下图:
/**
* 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 reverseList(ListNode head) {
ListNode last = null;
ListNode now = head;
while (now != null) {
ListNode next = now.next;
now.next = last;
last = now;
now = next;
}
return last;
}
}
LC 21. 合并两个有序链表(简单)
题目描述:
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
解题思路比较简单,同时遍历两个链表,挑一个数值比较小的加入到结果链表中就可以了!
/**
* 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 mergeTwoLists(ListNode l1, ListNode l2) {
// 定义结果链表的头节点
ListNode head = new ListNode(-1);
ListNode now = head;
while (true) {
// 找到数值较小的节点,加入到结果链表
if (l1 != null && l2 != null) {
if (l1.val < l2.val) {
now.next = new ListNode(l1.val);
now = now.next;
l1 = l1.next;
} else {
now.next = new ListNode(l2.val);
now = now.next;
l2 = l2.next;
}
} else if (l1 == null) {
// 如果 l1 链表后边没元素了,就将 l2 链表后边的元素拼到结果链表后
now.next = l2;
break;
} else if (l2 == null) {
// 如果 l2 链表后边没元素了,就将 l1 链表后边的元素拼到结果链表后
now.next = l1;
break;
}
}
// 返回头节点后的数据
return head.next;
}
}