算法合集(持续更新)
- 数学
- 最短路径
- 二分 + BFS
- 并查集
- 线段树
- 记忆化搜索
- 单例模式
1. 数学
1.1 Cnm计算
思路
用公式 Cnm = Cn-1m + Cn-1m-1
处理好边界值 C0^0^ = 1 , C
0^1^ = 0 , C1^0^ = 11
2
3
4
5
6
7
8
9
10
11//
public static long calCnm1(int n, int m) {
if (n == m)
return 1;
if (n == 0)
return 0;
if (m == 0) {
return 1;
}
return calCnm1(n - 1, m) + calCnm1(n - 1, m - 1);
}直接用展开式阶乘计算
1
2
3
4
5
6
7
8
9
10public static long calCnm2(int n, int m) {
if (m > n || m < 0) return 0;
//消除分子、分母重叠部分,只是为了简化计算,去掉这行也能运行
m = Math.min(m, n - m);
long result = 1;
for (int k = 1; k <= m; k++) {
result = result * (n - k + 1) / k;
}
return result;
}
1.2 快速幂
思路:
幂计算可以暴力O(n)复杂度直接for循环乘 x 得到,但是会重复计算部分值。
考虑将n用位运算按二进制进行拆分 (例:5^9^ = 520 * 523 即 5^9^ = 5^1^ * 5^8^)。5^1^*5^1^ = 5^2^ 5^2^ * 5^2^ = 5^4^ 5^4^ * 5^4^ = 5^8^ 这样可以将时间复杂度缩减到 O(log n)
1 | public class fastPow { |
1.3 质数筛选 (埃氏筛)
时间复杂度为O(nloglog(n))
1 | public int countPrime(int n){ |
2. 最短路径
2.1 Dijkstra
2.1.1 LeetCode_1514
前提:路径无负值。
思路(贪心):
一点与其相邻点之间的距离最短的那一点一定是最短距离。
例:A的相邻点和距离分别为{B,C,D,E,F} ,{5,2,3,4,9}。那么A点到C点的最短距离一定是2。
求一点到另一点的最短路径,可以由一点和他相邻点的最短路径推出来。
接着上面的例子,假设C到F的距离是1,因为 9 > 2+1 那么A与其他点的距离为{B,C,E,F} {5,3,4,3},那么A到F点的最短距离是3,到C点的最短距离也为3。
路径信息不建议使用数组存放,数组会记录许多无效路径,可能超内存。
数组版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32//LeetCode_1514为例,数组版会超时
public double djst1(int n, int start, int end, int[][] edges, double[] succProb) {
//dist存放其它节点与起点最近距离
double[] dist = new double[n];
//优先队列获取最小值,避免O(n)时间复杂度遍历dist数组
Queue<Integer> queue = new PriorityQueue<>((o1, o2) -> Double.compare(dist[o2], dist[o1]));
double[][] graph = new double[n][n];
//1.初始化
for (int i = 0; i < edges.length; i++) {
graph[edges[i][0]][edges[i][1]] = succProb[i];
graph[edges[i][1]][edges[i][0]] = succProb[i];
if (edges[i][0] == start) {
dist[edges[i][1]] = succProb[i];
queue.add(edges[i][1]);
} else if (edges[i][1] == start) {
dist[edges[i][0]] = succProb[i];
queue.add(edges[i][0]);
}
}
//2.具体更新距离逻辑
while (!queue.isEmpty()) {
int node = queue.poll();
for (int i = 0; i < n; i++) {
//更新距离逻辑,可根据题目变化更改具体的判断逻辑,只要符合求最小值的逻辑即可
if (graph[node][i] != 0 && dist[i] < graph[node][i] * dist[node]) {
dist[i] = graph[node][i] * dist[node];
queue.add(i);
}
}
}
return dist[end];
}邻接矩阵版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36public double djst2(int n, int start, int end, int[][] edges, double[] succProb) {
double[] dist = new double[n];
Queue<Integer> queue = new PriorityQueue<>((o1, o2) -> Double.compare(dist[o2], dist[o1]));
double[][] graph = new double[n][n];
for (int i = 0; i < edges.length; i++) {
graph[edges[i][0]][edges[i][1]] = succProb[i];
graph[edges[i][1]][edges[i][0]] = succProb[i];
if (edges[i][0] == start) {
dist[edges[i][1]] = succProb[i];
queue.add(edges[i][1]);
} else if (edges[i][1] == start) {
dist[edges[i][0]] = succProb[i];
queue.add(edges[i][0]);
}
}
while (!queue.isEmpty()) {
int node = queue.poll();
for (int i = 0; i < n; i++) {
if (graph[node][i] != 0 && dist[i] < graph[node][i] * dist[node]) {
dist[i] = graph[node][i] * dist[node];
queue.add(i);
}
}
}
return dist[end];
}
class Pair {
double succProb;
int node;
public Pair(double succProb, int node) {
this.succProb = succProb;
this.node = node;
}
}
2.2 Floyd
思路:动态规划
- 节点i、j之间的最小值一定由i、k与k、j之间转移过来
1 | public void floyd(int n,int[][] arr) { |
Floyd时间复杂度为O(n^3^),Dijkstra为O(n^2^),不过Floyd可以求每两点之前最小值,并且把min改成max就可以求最大路径了。
3. 二分 答案
3.1 华为2024_9_19笔试_2(二分+BFS)
描述
有一个NxN 大小的迷宫。初始状态下,配送员位于迷官的左上角,他希望前往迷宫的右下角。
配送员只能沿着上下左右四个方向移动,从每个格子移动到相邻格子所需要的时间是1个单位,他必须用最多 K个(也可以少于 K 个)单位时间到达右下角格子。
迷宫的每个格子都有辐射值,配送员必须穿着防护能力不低于相应辐射值的防护服,才能通过该格子。他希望知道,防护服的防护能力最少要达到多少,他才能顺利完成任务。注意:配送员需要通过迷官的左上角和右下角,因此防护服的防护能力必须大于等于这两个格子的辐射值。
输入描述
前两行各包含一个正整数,分别对应N和K,后 N 行各包含 N 整数,以空格分隔,表示地图上每个位置的辐射值。
2≤N≤100 。K≥2N-2,以保证题目有解。所有辐射值都是非负整数,绝对值不超过 10^4
输出描述
一个整数,表示配送员穿着防护服的最低防护能力。
用例输入
1 | 5 |
用例输出
1 | 3 |
1 | import java.util.LinkedList; |
3.2 分割数组的最大值
LeetCode_410 分割数组的最大值
1 | class Solution { |
4. 并查集
1 | /** |
5. 线段树
作用:线段树可以在O(log N)的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。
1 | /** |
1 | ``` |
6.2 小马的数组构造
描述
小马拿到了一个数组a,她准备构造一个数组b满足:
- b的每一位都和a对应位置不同,即 bi != ai
- b 的所有元素之和都和 a 相同。
- b的数组均为正整数。请你告诉小马有多少种构造方式。由于答案过大,请对 10^9+7取模。
输入描述
第一行输入一个正整数n,代表数组的大小。第二行输入n个正整数ai,代表小美拿到的数组
1<=n<=100, 1<=ai<=300, 1<=Σai <= 500
输出描述
一个整数,代表构造方式对 10^9+7取模的值。
用例输入 1
1 | 3 |
用例输出 1
1 | 1 |
用例输入 2
1 | 3 |
用例输出 2
1 | 0 |
dfs遍历每一个位置的可能数,使用数组存放信息。
1 | import java.util.*; |
6.3 人员派遣
描述
某公司有n名员工,第i名员工具有的能力可以用一个正整数ai描述,称为员工的能力值,现在,公司有一个项目需要交给恰好[n/2]名员工负责。为了保证项目能顺利进行,要求负责该项目的所有员工能力值之和大于等于x。
公司希望你可以帮忙求出,有多少种不同的派遣员工来负责这个项目的方案。
上文中,[x]风表示大于等于x的最小整数,例[4] =4,[4.2]=5。认为两个方案不同,当且仅当存在一名员工在一种方案中负责该项目,而在另一种方案中不负责.
输入描述
输入包含多组数据,输入第一行包含一个整数T (1<=T<=10) ,表示数据组数.
接下来2T行,每两行描述了一组数据.
每组数据第一行包含两个正整数n(1<=n<=16) 和x (1<=x<=210^4),分别表示公司的员工总数和项目对负责员工能力值之和的要求。
每组数据第二行包含n个整数,第i个整数表示第i名员工的能力值ai(1<=ai<=10^3)。
对于100%的数据,满足1<=n<=16,1<=x<=2104,1<=T<=10,1<=ai<=103。
输出描述
输出包含T行。对于每组数据输出一行一个整数,表示可行的派遣方案数.
用例输入 1
1 | 3 |
用例输出 1
1 | 7 |
1 | public class ProjectDispatch { |
6.4 总结
记忆化搜索一般用于构造数据有多少种情况之类的题目,这类题目通常需要将多个位置排列组合后判断是否合理,需要通过dfs,dfs会通过重复路径,这时可用dp数组或者map之类的数据存放已经计算出的结果,减少消耗时间。
dfs步骤:
- 边界判断
- 判断是否已经计算过该种情况
- 按题意进行dfs
- 记录结果
1 | public static int dfs(int sum,int[] arr,int start){ |
7. 动态规划
[参考牛马OJ][https://niumacode.com/archives]
7.1 背包问题模版
普通背包问题
1
2
3
4
5
6
7
8
9
10
11
12
13int n, C; //n个物品, C表示背包容量
int[] v, w; //v[i]表示第i个物品的价格/体积 w[i]表示第i个物品的价值
int[][] dp = new int[n + 1][C + 1]; //容器规模
dp[0][j]; //初始化
for (int i = 1 ; i <= n ; i++) {
for (int j = 0 ; j <= C ; j++) {
if (j >= v[i - 1])
dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i-1]] + w[i - 1]);
else
dp[i][j] = dp[i - 1][j];
}
}
return dp[n][C];完全背包问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14int n, C; //n个物品, C表示背包容量
int[] v, w; //v[i]表示第i个物品的价格/体积 w[i]表示第i个物品的价值
int[][] dp = new int[n + 1][C + 1]; //容器规模
dp[0][j]; //初始化
for (int i = 1 ; i <= n ; i++) {
for (int j = 0 ; j <= C ; j++) {
if (j >= v[i - 1])
//选择该物品或者不选该物品
dp[i][j] = max(dp[i-1][j], dp[i][j-v[i-1]] + w[i - 1]);
else
dp[i][j] = dp[i - 1][j];
}
}
return dp[n][C];
注意:使用滚动数组优化时注意状态变更情况,可能需要从大到小遍历。
面试
1. 单例模式
1.1 类直接持有单例
1 | /** |
1.2 类间接持有属性
1.2.1 双检校验
1 | /** |
1.2.2 静态内部类
1 | /** |
1.2.3 枚举
最推荐的实现方式,枚举可以懒汉式的加载,又可以防止反射和反序列化攻击,使得对象单例的保证。
防止反射攻击:
- 枚举的构造方法在
java.lang.reflect.Constructor
中有特殊检查,无法通过反射调用
防止反序列化攻击:
- 序列化过程中,枚举类型使用
Enum.readResolve()
方法,确保反序列化返回相同的枚举实例,而不是创建新的对象。
1 | /** |
2. 链表
2.1 leetcode19. 删除链表的倒数第 N 个结点
思路:
两次遍历:统计链表长度,定位到要删除的节点
一次遍历:快慢指针,快指针先走n步,慢指针再随着快指针一直遍历到尾部(快指针比慢指针快n步,到尾部时慢指针后一个节点就是倒数第n个节点)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48class 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 removeNthFromEnd1(ListNode head, int n) {
ListNode p = head;
int num = 0;
// num为链表长度+1
while(p!=null){
p=p.next;
num++;
}
p = head;
//遍历到链表后面有n个节点
for (int i = 0; i < num - n - 1; i++) {
p=p.next;
}
//边界情况判断,可以通过添加前导节点解决。
if(n == num){
head = head.next;
}else{
p.next = p.next.next;
}
return head;
}
//一次遍历
public ListNode removeNthFromEnd(ListNode head, int n) {
//添加前导节点处理要删除的是头结点情况
ListNode dummy = new ListNode();
dummy.next = head;
ListNode fast = dummy;
ListNode slow = dummy;
for (int i = 0; i < n; i++) {
fast = fast.next;
}
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return dummy.next;
}
}
2.2 leetcode_143. 重排链表
美团、途虎
思路:
- 链表顺序访问变随机访问:使用map记录结点位置
- 快慢指针反转后后半部分:使用快慢指针(快走两步、慢走一步),反转后半部分链表后再遍历。
1 |