Skip to content

算法模板

基础算法

gcd / lcm

java
public static int gcd(int a, int b){
    return b == 0 ? a : gcd(b, a % b);
}

public static int lcm(int a, int b){
    return a * b / gcd(a, b);
}

质数

  • p 和 q 为两个正整数且互质,那么 px + qy 不能表示的最大正整数为 (p - 1) * (q - 1) -1
  • 任何一个正整数都可以分解为有穷个质数幂之积,如 6936 = 2^3^ * 3 * 17^2^
java
// 试除法
boolean isPrime(long x){
  if (x < 2) return false;
  for (long i = 2; i * i <= x; i++){
    if (x % i == 0) return false;
  }
  return true;
}

// 分解质因数

二分

java
public int binarySearch(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        }
    }
    return -1;
}

// 找到 target 的左边界,找不到返回第一个比他大的数
public int leftBinarySearch(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            right = mid - 1;   // 锁定左侧边界,收缩右侧边界
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        }
    }
    if (left < 0 || left >= nums.length)
        return -1;
    return nums[left] == target ? left : -1;
}

// 找到 target 的有边界,找不到返回最后一个比他小的数
public int rightBinarySearch(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            left = mid + 1;  // 锁定右侧边界,收缩左侧边界
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        }
    }
    if (right < 0 || right >= nums.length) {
        return -1;
    }
    return nums[right] == target ? right : -1;
}


public static void main(String[] args) {
    int[] nums = new int[]{1, 2, 2, 2, 3};
    System.out.println(new TEMP().binarySearch(nums, 2));       // 2
    System.out.println(new TEMP().leftBinarySearch(nums, 2));   // 1
    System.out.println(new TEMP().rightBinarySearch(nums, 2));  // 3
}

位运算

txt
求n的第k位数字: n >> k & 1
返回n的最后一位1:lowbit(n) = n & -n

双指针

数据结构

并查集

java
class UF {
    // 连通分量个数
    private int count;
    // 存储一棵树
    private int[] parent;
    // 记录树的「重量」
    private int[] size;

    // n 为图中节点的个数
    public UF(int n) {
        this.count = n;
        parent = new int[n];
        size = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
            size[i] = 1;
        }
    }

    // 将节点 p 和节点 q 连通
    public void union(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        if (rootP == rootQ)
            return;

        // 小树接到大树下面,较平衡
        if (size[rootP] > size[rootQ]) {
            parent[rootQ] = rootP;
            size[rootP] += size[rootQ];
        } else {
            parent[rootP] = rootQ;
            size[rootQ] += size[rootP];
        }
        // 两个连通分量合并成一个连通分量
        count--;
    }

    // 判断节点 p 和节点 q 是否连通
    public boolean connected(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        return rootP == rootQ;
    }

    // 返回节点 x 的连通分量根节点
    private int find(int x) {
        while (parent[x] != x) {
            // 进行路径压缩
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return x;
    }

    // 返回图中的连通分量个数
    public int count() {
        return count;
    }
}

DFS & BFS

java
int ans = 0;
boolean[][] visited;
int[][] dir = {
        {0, 1}, //right
        {1, 0}, //down
        {-1, 0}, //up
        {0, -1} //left
};

public void dfs(char[][] grid, int x, int y) {
    if (visited[x][y] || grid[x][y] == '0') {
        return;
    }
    visited[x][y] = true;
    for (int i = 0; i < 4; i++) {
        int nextX = x + dir[i][0];
        int nextY = y + dir[i][1];
        if (nextX >= 0 && nextX < grid.length && nextY >= 0 && nextY < grid[0].length && !visited[nextX][nextY] && grid[nextX][nextY] == '1') {
            dfs(grid, nextX, nextY);
        }
    }
}

public void bfs(char[][] grid, int x, int y) {
    visited[x][y] = true;
    LinkedList<int[]> queue = new LinkedList<>();
    queue.addLast(new int[]{x, y});
    while (!queue.isEmpty()) {
        int[] pop = queue.pop();
        for (int i = 0; i < 4; i++) {
            int nextX = pop[0] + dir[i][0];
            int nextY = pop[1] + dir[i][1];
            if (nextX >= 0 && nextX < grid.length && nextY >= 0 && nextY < grid[0].length && !visited[nextX][nextY] && grid[nextX][nextY] == '1') {
                queue.addLast(new int[]{nextX, nextY});
                visited[nextX][nextY] = true;
            }
        }
    }
}

public int numIslands(char[][] grid) {
    visited = new boolean[grid.length][grid[0].length];

    for (int i = 0; i < grid.length; i++) {
        for (int j = 0; j < grid[0].length; j++) {
            if (!visited[i][j] && grid[i][j] == '1') {
                ans++;
//                        dfs(grid, i, j);
                bfs(grid, i, j);
            }
        }
    }
    return ans;
}

拓扑排序

java
public boolean canFinish(int numCourses, int[][] prerequisites) {
    int[] in = new int[numCourses];
    List<List<Integer>> map = new ArrayList<>();
    for (int i = 0; i < numCourses; i++) {
        map.add(new ArrayList<>());
    }
    for (int[] prerequisite : prerequisites) {
        int v = prerequisite[0];
        int u = prerequisite[1];
        map.get(u).add(v);
        in[v]++;
    }
    int num = 0;
    Stack<Integer> stack = new Stack<>();
    for (int i = 0; i < numCourses; i++) {
        if (in[i] == 0) {
            stack.push(i);
            num++;
        }
    }
    while (!stack.isEmpty()) {
        int u = stack.pop();
        for (Integer v : map.get(u)) {
            in[v]--;
            if (in[v] == 0) {
                num++;
                stack.push(v);
            }
        }
    }
    return num == numCourses;
}

Kruskal 最短路径算法

所谓最小生成树,就是图中若干边的集合(我们后文称这个集合为 mst,最小生成树的英文缩写),你要保证这些边:

1、包含图中的所有节点。

2、形成的结构是树结构(即不存在环)。

3、权重和最小。

有之前题目的铺垫,前两条其实可以很容易地利用 Union-Find 算法做到,关键在于第 3 点,如何保证得到的这棵生成树是权重和最小的。

这里就用到了贪心思路:

将所有边按照权重从小到大排序,从权重最小的边开始遍历,如果这条边和 mst 中的其它边不会形成环,则这条边是最小生成树的一部分,将它加入 mst 集合;否则,这条边不是最小生成树的一部分,不要把它加入 mst 集合

java
class Solution {
    int[] father;
    // 连通分量个数
    int count;

    int find(int u) {
        while (father[u] != u) {
            father[u] = father[father[u]];
            u = father[u];
        }
        return u;
    }

    void union(int u1, int u2) {
        u1 = find(u1);
        u2 = find(u2);
        if (u1 == u2) return;
        father[u1] = u2;
        count--;
    }

    boolean connected(int u1, int u2) {
        return father[u1] == u2;
    }

    public int minimumCost(int n, int[][] connections) {
        father = new int[n];
        for (int i = 0; i < n; i++) {
            father[i] = i;
        }
        count = n;

        Arrays.sort(connections, Comparator.comparingInt(a -> a[2]));
        int mst = 0;
        for (int[] connection : connections) {
            int u = connection[0] - 1;
            int v = connection[1] - 1;
            if (connected(u, v)) continue;
            union(u, v);
            mst += connection[2];
        }
        return count == 1 ? mst : -1;
    }
}

Dijkstra 算法

计算 start 到其他节点的最短距离

java
class State {
    // 图节点的 id
    int id;
    // 从 start 节点到当前节点的距离
    int distFromStart;

    State(int id, int distFromStart) {
        this.id = id;
        this.distFromStart = distFromStart;
    }
}

// 返回节点 from 到节点 to 之间的边的权重
int weight(int from, int to);

// 输入节点 s 返回 s 的相邻节点
List<Integer> adj(int s);

// 输入一幅图和一个起点 start,计算 start 到其他节点的最短距离
int[] dijkstra(int start, List<Integer>[] graph) {
    // 图中节点的个数
    int V = graph.length;
    // 记录最短路径的权重,你可以理解为 dp table
    // 定义:distTo[i] 的值就是节点 start 到达节点 i 的最短路径权重
    int[] distTo = new int[V];
    // 求最小值,所以 dp table 初始化为正无穷
    Arrays.fill(distTo, Integer.MAX_VALUE);
    // base case,start 到 start 的最短距离就是 0
    distTo[start] = 0;

    // 优先级队列,distFromStart 较小的排在前面
    Queue<State> pq = new PriorityQueue<>((a, b) -> {
        return a.distFromStart - b.distFromStart;
    });

    // 从起点 start 开始进行 BFS
    pq.offer(new State(start, 0));

    while (!pq.isEmpty()) {
        State curState = pq.poll();
        int curNodeID = curState.id;
        int curDistFromStart = curState.distFromStart;

        if (curDistFromStart > distTo[curNodeID]) {
            // 已经有一条更短的路径到达 curNode 节点了
            continue;
        }
        // 将 curNode 的相邻节点装入队列
        for (int nextNodeID : adj(curNodeID)) {
            // 看看从 curNode 达到 nextNode 的距离是否会更短
            int distToNextNode = distTo[curNodeID] + weight(curNodeID, nextNodeID);
            if (distTo[nextNodeID] > distToNextNode) {
                // 更新 dp table
                distTo[nextNodeID] = distToNextNode;
                // 将这个节点以及距离放入队列
                pq.offer(new State(nextNodeID, distToNextNode));
            }
        }
    }
    return distTo;
}

floyd 算法

java
// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
    // 初始化
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i == j) d[i][j] = 0;
   			else d[i][j] = INF;
    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

最小栈

java
class MinStack {
    private final Stack<Integer> stack;

    private final Stack<Integer> minStack;

    public MinStack() {
        stack = new Stack<>();
        minStack = new Stack<>();
    }

    public void push(int x) {
        stack.push(x);
        if (minStack.isEmpty() || x <= minStack.peek()) {
            minStack.push(x);
        }
    }

    public void pop() {
        if (stack.pop().equals(minStack.peek())) {
            minStack.pop();
        }
    }

    public int top() {
        return stack.peek();
    }

    public int getMin() {
        return minStack.peek();
    }
}

排列组合问题(回溯)

两种解法:对于每个数字枚举选和不选(不适用去重和排列问题),对于每个位置枚举能选择的数字

组合问题

  • 对于组合问题,下一个数字不能和之前相同,因此需要从 start 开始
  • 若数字可重复,start 与上一次回溯相同 i,不可重复为 i + 1
  • 若需要去重(数组存在重复元素),需要先排序数组,在回溯时添加判断 i > start && x [i] == x [i - 1],第一个判断不是该层第一个节点,第二个判断去重
java
List<List<Integer>> ans = new ArrayList<>();
List<Integer> path = new ArrayList<>();

void backTrace(int[] candidates, int target, int start, int sum) {
    if (target == sum) {
        ans.add(new ArrayList<>(path));
        return;
    }
    if (target < sum) {
        return;
    }
    for (int i = start; i < candidates.length; i++) {
        // 出现重复节点,同层的第一个节点已经被访问过,所以直接跳过
        if (i > start && candidates[i] == candidates[i - 1]) {
            continue;
        }
        path.add(candidates[i]);
        backTrace(candidates, target, i + 1, sum + candidates[i]);
        path.remove(path.size() - 1);
    }
}

public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    Arrays.sort(candidates);
    backTrace(candidates, target, 0, 0);
    return new ArrayList<>(ans);
}

排列问题

  • 对于排列问题,需要从 0 开始,并且添加判断 vis
  • 若需要去重(数组存在重复元素),需要先排序数组,在回溯时添加判断 i > 0 && nums [i] == nums [i - 1] && ! visited [i - 1],如果前一个“相同元素”未被使用过,则不使用当前元素。例如 [1 2 2'] 可能出现 [1 2 2'] 和 [1 2' 2] 的情况 如果“存在前一个相同元素” 且“未被使用过”, 当现有排列是 [1 2'] 时,原来的数组 [1 2 2'] 中 2’存在前一个元素 2 与其相同,且此时 2 未被访问过,跳过。[1 2 2'] 的排列先于 [1 2' 2] 存在,因此可以去除。
java
List<List<Integer>> ans = new ArrayList<>();

List<Integer> path = new ArrayList<>();

boolean[] visited;

public List<List<Integer>> permuteUnique(int[] nums) {
    Arrays.sort(nums);
    visited = new boolean[nums.length];
    backTrack(nums, 0);
    return ans;
}

public void backTrack(int[] nums, int cnt) {
    if (cnt == nums.length) {
        ans.add(new ArrayList<>(path));
        return;
    }

    for (int i = 0; i < nums.length; i++) {
        if (visited[i]) {
            continue;
        }
        if (i > 0 && nums[i] == nums[i - 1] && !visited[i - 1]) {
            continue;
        }
        visited[i] = true;
        path.add(nums[i]);
        backTrack(nums, cnt + 1);
        path.remove(path.size() - 1);
        visited[i] = false;
    }
}

子集问题(类似组合问题)

  • 子集问题不需要选择完所有元素,因此每次回溯都会添加到结果集中
  • 如需要去重,与组合问题一样添加判断 i > start && nums [i] == nums [i - 1]
java
List<Integer> path = new ArrayList<>();

List<List<Integer>> ans = new ArrayList<>();

public List<List<Integer>> subsetsWithDup(int[] nums) {
    Arrays.sort(nums);
    backTrack(nums, 0);
    return ans;
}

public void backTrack(int[] nums, int start) {
    ans.add(new ArrayList<>(path));

    for (int i = start; i < nums.length; i++) {
        if (i > start && nums[i] == nums[i - 1]) {
            continue;
        }
        path.add(nums[i]);
        backTrack(nums, i + 1);
        path.remove(path.size() - 1);
    }
}

背包问题(动态规划)

01 背包

java
public int bag01(int[] weight, int[] value, int bagWeight) {
    int[][] dp = new int[weight.length][bagWeight + 1];
    for (int j = weight[0]; j <= bagWeight; j++) {
        dp[0][j] = value[0];
    }
    for (int i = 1; i < weight.length; i++) {
        for (int j = 0; j <= bagWeight; j++) {
            // 无法选
            if (weight[i] > j) {
                dp[i][j] = dp[i - 1][j];
            } else {
                // 选和不选的最大值
                dp[i][j] = Math.max(value[i] + dp[i - 1][j - weight[i]], dp[i - 1][j]);
            }
        }
    }
    return dp[weight.length - 1][bagWeight];
}

滚动数组:01 背包反向遍历

java
public int bag01(int[] weight, int[] value, int bagWeight) {
    int[] dp = new int[bagWeight + 1];
    for (int i = 0; i < weight.length; i++) {
        for (int j = bagWeight; j >= weight[i]; j--) {
            dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    return dp[bagWeight];
}

完全背包

java
public int bagAll(int[] weight, int[] value, int bagWeight) {
    int[] dp = new int[bagWeight + 1];
    for (int i = 0; i < weight.length; i++) {
        for (int j = weight[i]; j <= bagWeight; j++) {
            dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    return dp[bagWeight];
}

组合排列问题(动态规划)

  • 对于组合排列若只需求一个数(方案数,目标和),无需求路径,则可以使用动态规划解决
  • 如果求组合数就是外层 for 循环遍历物品,内层 for 遍历背包
  • 如果求排列数就是外层 for 遍历背包,内层 for 循环遍历物品
  • 若只能选一次(01 背包),则反向遍历,可重复(完全背包),则正向遍历

组合问题

P494 目标和

java
public int findTargetSumWays(int[] nums, int target) {
    int sum = Arrays.stream(nums).sum();
    sum -= Math.abs(target);
    if (sum < 0 || sum % 2 != 0) {
        return 0;
    }
    sum /= 2;

    int[] dp = new int[sum + 1];
    dp[0] = 1;
    // 组合问题:先遍历物品,再遍历背包
    for (int num : nums) {
    	// 单一背包:反向遍历
        for (int j = sum; j >= num; j--) {
            dp[j] = dp[j] + dp[j - num];
        }
    }
    return dp[sum];
}

P518 零钱总换 II

java
public int change(int amount, int[] coins) {
    int[] dp = new int[amount + 1];
    dp[0] = 1;
    // 组合问题:先遍历物品,再遍历背包
    for (int coin : coins) {
        // 完全背包:正向遍历
        for (int j = coin; j <= amount; j++) {
            dp[j] += dp[j - coin];
        }
    }
    return dp[amount];
}

排列问题

P377 组合总和 IV

java
public int combinationSum4(int[] nums, int target) {
    int[] dp = new int[target + 1];
    dp[0] = 1;
    // 排列问题:先遍历背包,再遍历物品
    for (int i = 0; i <= target; i++) {
        for (int num : nums) {
            if (i >= num) {
                dp[i] += dp[i - num];
            }
        }
    }
    return dp[target];
}