常见算法设计

常见算法设计

1. 图

1) 概念

图是由顶点(vertex)和边(edge)组成的数据结构,例如

A
B
C
D

该图有四个顶点:A、B、C、D 以及四条有向边,有向图中,边是单向的

有向 vs 无向

如果是无向图,那么边是双向的,下面是一个无向图的例子

A
B
C
D

是指与该顶点相邻的边的数量

A
B
C
D
E
F

例如上图中

  • A、B、C、E、F 这几个顶点度数为 2
  • D 顶点度数为 4

有向图中,细分为入度出度,参见下图

A
B
C
D
E
F
  • A (2 out / 0 in)
  • B、C、E (1 out / 1 in)
  • D (2 out / 2 in)
  • F (0 out / 2 in)

边可以有权重,代表从源顶点到目标顶点的距离、费用、时间或其他度量。

北京
武汉
广州
上海
800km
1900km
1200km
1050km
700km
路径

路径被定义为从一个顶点到另一个顶点的一系列连续边,例如上图中【北京】到【上海】有多条路径

  • 北京 - 上海
  • 北京 - 武汉 - 上海

路径长度

  • 不考虑权重,长度就是边的数量
  • 考虑权重,一般就是权重累加

在有向图中,从一个顶点开始,可以通过若干条有向边返回到该顶点,那么就形成了一个环

A
B
C
D
E
图的连通性

如果两个顶点之间存在路径,则这两个顶点是连通的,所有顶点都连通,则该图被称之为连通图,若子图连通,则称为连通分量

A
B
C
D
E
F
G
H
I
J

2) 图的表示

比如说,下面的图

A
B
C
D

邻接矩阵可以表示为:

  A B C D
A 0 1 1 0
B 1 0 0 1 
C 1 0 0 1
D 0 1 1 0

邻接表可以表示为:

A -> B -> C
B -> A -> D
C -> A -> D
D -> B -> C

有向图的例子

A
B
C
D
  A B C D
A 0 1 1 0
B 0 0 0 1
C 0 0 0 1
D 0 0 0 0
A - B - C
B - D
C - D
D - empty

3) Java 表示

顶点

public class Vertex {
    String name;
    List<Edge> edges;
    
    // 拓扑排序相关
    int inDegree;
    int status; // 状态 0-未访问 1-访问中 2-访问过,用在拓扑排序

    // dfs, bfs 相关
    boolean visited;

    // 求解最短距离相关
    private static final int INF = Integer.MAX_VALUE;
    int dist = INF;
    Vertex prev = null;
}

public class Edge {

    Vertex linked;
    int weight;

    public Edge(Vertex linked) {
        this(linked, 1);
    }

    public Edge(Vertex linked, int weight) {
        this.linked = linked;
        this.weight = weight;
    }
}

4) DFS

public class Dfs {
    public static void main(String[] args) {
        Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");
        Vertex v5 = new Vertex("v5");
        Vertex v6 = new Vertex("v6");

        v1.edges = List.of(new Edge(v3), new Edge(v2), new Edge(v6));
        v2.edges = List.of(new Edge(v4));
        v3.edges = List.of(new Edge(v4), new Edge(v6));
        v4.edges = List.of(new Edge(v5));
        v5.edges = List.of();
        v6.edges = List.of(new Edge(v5));

        dfs1(v1);
    }

    private static void dfs2(Vertex v) {
        LinkedList<Vertex> stack = new LinkedList<>();
        stack.push(v);
        while (!stack.isEmpty()) {
            Vertex pop = stack.pop();
            pop.visited = true;
            System.out.println(pop.name);
            for (Edge edge : pop.edges) {
                if (!edge.linked.visited) {
                    stack.push(edge.linked);
                }
            }
        }
    }

    private static void dfs1(Vertex v) {
        v.visited = true;
        System.out.println(v.name);
        for (Edge edge : v.edges) {
            if (!edge.linked.visited) {
                dfs(edge.linked);
            }
        }
    }
}

5) BFS

public class Bfs {
    public static void main(String[] args) {
        Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");
        Vertex v5 = new Vertex("v5");
        Vertex v6 = new Vertex("v6");

        v1.edges = List.of(new Edge(v3), new Edge(v2), new Edge(v6));
        v2.edges = List.of(new Edge(v4));
        v3.edges = List.of(new Edge(v4), new Edge(v6));
        v4.edges = List.of(new Edge(v5));
        v5.edges = List.of();
        v6.edges = List.of(new Edge(v5));

        bfs(v1);
    }

    private static void bfs(Vertex v) {
        LinkedList<Vertex> queue = new LinkedList<>();
        v.visited = true;
        queue.offer(v);
        while (!queue.isEmpty()) {
            Vertex poll = queue.poll();
            System.out.println(poll.name);
            for (Edge edge : poll.edges) {
                if (!edge.linked.visited) {
                    edge.linked.visited = true;
                    queue.offer(edge.linked);
                }
            }
        }
    }
}

6) 拓扑排序

网页基础
Java Web
Java 基础
数据库
Spring框架
微服务框架
实战项目
Kahn
public class TopologicalSort {
    public static void main(String[] args) {
        Vertex v1 = new Vertex("网页基础");
        Vertex v2 = new Vertex("Java基础");
        Vertex v3 = new Vertex("JavaWeb");
        Vertex v4 = new Vertex("Spring框架");
        Vertex v5 = new Vertex("微服务框架");
        Vertex v6 = new Vertex("数据库");
        Vertex v7 = new Vertex("实战项目");

        v1.edges = List.of(new Edge(v3)); // +1
        v2.edges = List.of(new Edge(v3)); // +1
        v3.edges = List.of(new Edge(v4));
        v6.edges = List.of(new Edge(v4));
        v4.edges = List.of(new Edge(v5));
        v5.edges = List.of(new Edge(v7));
        v7.edges = List.of();

        List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6, v7);
        // 1. 统计每个顶点的入度
        for (Vertex v : graph) {
            for (Edge edge : v.edges) {
                edge.linked.inDegree++;
            }
        }
        /*for (Vertex vertex : graph) {
            System.out.println(vertex.name + " " + vertex.inDegree);
        }*/
        // 2. 将入度为0的顶点加入队列
        LinkedList<Vertex> queue = new LinkedList<>();
        for (Vertex v : graph) {
            if (v.inDegree == 0) {
                queue.offer(v);
            }
        }
        // 3. 队列中不断移除顶点,每移除一个顶点,把它相邻顶点入度减1,若减到0则入队
        List<String> result = new ArrayList<>();
        while (!queue.isEmpty()) {
            Vertex poll = queue.poll();
//            System.out.println(poll.name);
            result.add(poll.name);
            for (Edge edge : poll.edges) {
                edge.linked.inDegree--;
                if (edge.linked.inDegree == 0) {
                    queue.offer(edge.linked);
                }
            }
        }
        if (result.size() != graph.size()) {
            System.out.println("出现环");
        } else {
            for (String key : result) {
                System.out.println(key);
            }
        }
    }
}
DFS
public class TopologicalSortDFS {

    public static void main(String[] args) {
        Vertex v1 = new Vertex("网页基础");
        Vertex v2 = new Vertex("Java基础");
        Vertex v3 = new Vertex("JavaWeb");
        Vertex v4 = new Vertex("Spring框架");
        Vertex v5 = new Vertex("微服务框架");
        Vertex v6 = new Vertex("数据库");
        Vertex v7 = new Vertex("实战项目");

        v1.edges = List.of(new Edge(v3));
        v2.edges = List.of(new Edge(v3));
        v3.edges = List.of(new Edge(v4));
        v6.edges = List.of(new Edge(v4));
        v4.edges = List.of(new Edge(v5));
        v5.edges = List.of(new Edge(v7));
        v7.edges = List.of();

        List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6, v7);
        LinkedList<String> result = new LinkedList<>();
        for (Vertex v : graph) {
            if(v.status==0) {
                dfs(v, result);
            }
        }
        System.out.println(result);
    }

    private static void dfs(Vertex v, LinkedList<String> result) {
        if (v.status == 2) {
            return;
        }
        if (v.status == 1) {
            throw new RuntimeException("发现环");
        }
        v.status = 1;
        for (Edge edge : v.edges) {
            dfs(edge.linked, result);
        }
        v.status = 2;
        result.push(v.name);
    }
}

7) 最短路径

Dijkstra

Edsger Wybe Dijkstra

艾兹格·维布·迪克斯特拉(Edsger Wybe Dijkstra,/ˈdaɪkstrə/ DYKE-strə;荷兰语:[ˈɛtsxər ˈʋibə ˈdɛikstra] 1930年5月11日-2002年8月6日)是一位荷兰计算机科学家、程序员、软件工程师、系统科学家和科学散文家。他因对开发结构化编程语言做出的基础贡献而获得了1972年的图灵奖,并担任德克萨斯大学奥斯汀分校的斯伦贝谢百年计算机科学主席,任职时间从1984年到2000年。在他于2002年去世前不久,他因其在程序计算的自稳定性方面的工作而获得了ACM PODC分布式计算有影响力论文奖。为了纪念他,该年度奖项在接下来的一年更名为迪克斯特拉奖。

迪克斯特拉在计算机科学领域的贡献

  1. 最短路径算法,也称为迪克斯特拉算法,现代计算机科学本科课程中广泛教授
  2. Shunting yard算法
  3. THE OS 操作系统
  4. 银行家算法
  5. 用于协调多个处理器和程序的信号量构造
  6. 在分布式计算领域提出概念:自稳定性
7
9
14
9
2
15
11
6
1
2
3
4
5
6

算法描述:

  1. 将所有顶点标记为未访问。创建一个未访问顶点的集合。
  2. 为每个顶点分配一个临时距离值
    • 对于我们的初始顶点,将其设置为零
    • 对于所有其他顶点,将其设置为无穷大。
  3. 每次选择最小临时距离的未访问顶点,作为新的当前顶点
  4. 对于当前顶点,遍历其所有未访问的邻居,并更新它们的临时距离为更小
    • 例如,1->6 的距离是 14,而1->3->6 的距离是11。这时将距离更新为 11
    • 否则,将保留上次距离值
  5. 当前顶点的邻居处理完成后,把它从未访问集合中删除
public class Dijkstra {
    public static void main(String[] args) {
        Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");
        Vertex v5 = new Vertex("v5");
        Vertex v6 = new Vertex("v6");

        v1.edges = List.of(new Edge(v3, 9), new Edge(v2, 7), new Edge(v6, 14));
        v2.edges = List.of(new Edge(v4, 15));
        v3.edges = List.of(new Edge(v4, 11), new Edge(v6, 2));
        v4.edges = List.of(new Edge(v5, 6));
        v5.edges = List.of();
        v6.edges = List.of(new Edge(v5, 9));

        List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6);

        dijkstra(graph, v1);
    }

    private static void dijkstra(List<Vertex> graph, Vertex source) {
        ArrayList<Vertex> list = new ArrayList<>(graph);
        source.dist = 0;

        while (!list.isEmpty()) {
            // 3. 选取当前顶点
            Vertex curr = chooseMinDistVertex(list);
            // 4. 更新当前顶点邻居距离
            updateNeighboursDist(curr, list);
            // 5. 移除当前顶点
            list.remove(curr);
        }

        for (Vertex v : graph) {
            System.out.println(v.name + " " + v.dist);
        }
    }

    private static void updateNeighboursDist(Vertex curr, ArrayList<Vertex> list) {
        for (Edge edge : curr.edges) {
            Vertex n = edge.linked;
            if (list.contains(n)) {
                int dist = curr.dist + edge.weight;
                if (dist < n.dist) {
                    n.dist = dist;
                }
            }
        }
    }

    private static Vertex chooseMinDistVertex(ArrayList<Vertex> list) {
        Vertex min = list.get(0);
        for (int i = 1; i < list.size(); i++) {
            if (list.get(i).dist < min.dist) {
                min = list.get(i);
            }
        }
        return min;
    }

}

改进 - 优先级队列

  1. 创建一个优先级队列,放入所有顶点(队列大小会达到边的数量)
  2. 为每个顶点分配一个临时距离值
    • 对于我们的初始顶点,将其设置为零
    • 对于所有其他顶点,将其设置为无穷大。
  3. 每次选择最小临时距离的未访问顶点,作为新的当前顶点
  4. 对于当前顶点,遍历其所有未访问的邻居,并更新它们的临时距离为更小,若距离更新需加入队列
    • 例如,1->6 的距离是 14,而1->3->6 的距离是11。这时将距离更新为 11
    • 否则,将保留上次距离值
  5. 当前顶点的邻居处理完成后,把它从队列中删除
public class DijkstraPriorityQueue {
    public static void main(String[] args) {
        Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");
        Vertex v5 = new Vertex("v5");
        Vertex v6 = new Vertex("v6");

        v1.edges = List.of(new Edge(v3, 9), new Edge(v2, 7), new Edge(v6, 14));
        v2.edges = List.of(new Edge(v4, 15));
        v3.edges = List.of(new Edge(v4, 11), new Edge(v6, 2));
        v4.edges = List.of(new Edge(v5, 6));
        v5.edges = List.of();
        v6.edges = List.of(new Edge(v5, 9));

        List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6);

        dijkstra(graph, v1);
    }

    private static void dijkstra(List<Vertex> graph, Vertex source) {
        PriorityQueue<Vertex> queue = new PriorityQueue<>(Comparator.comparingInt(v -> v.dist));
        source.dist = 0;
        for (Vertex v : graph) {
            queue.offer(v);
        }

        while (!queue.isEmpty()) {
            System.out.println(queue);
            // 3. 选取当前顶点
            Vertex curr = queue.peek();
            // 4. 更新当前顶点邻居距离
            if(!curr.visited) {
                updateNeighboursDist(curr, queue);
                curr.visited = true;
            }
            // 5. 移除当前顶点
            queue.poll();
        }

        for (Vertex v : graph) {
            System.out.println(v.name + " " + v.dist + " " + (v.prev != null ? v.prev.name : "null"));
        }
    }

    private static void updateNeighboursDist(Vertex curr, PriorityQueue<Vertex> queue) {
        for (Edge edge : curr.edges) {
            Vertex n = edge.linked;
            if (!n.visited) {
                int dist = curr.dist + edge.weight;
                if (dist < n.dist) {
                    n.dist = dist;
                    n.prev = curr;
                    queue.offer(n);
                }
            }
        }
    }

}

问题

2
1
-2
1
v1
v2
v3
v4

按照 Dijkstra 算法,得出

  • v1 -> v2 最短距离2
  • v1 -> v3 最短距离1
  • v1 -> v4 最短距离2

事实应当是

  • v1 -> v2 最短距离2
  • v1 -> v3 最短距离0
  • v1 -> v4 最短距离1
Bellman-Ford
public class BellmanFord {
    public static void main(String[] args) {
        // 正常情况
        /*Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");
        Vertex v5 = new Vertex("v5");
        Vertex v6 = new Vertex("v6");

        v1.edges = List.of(new Edge(v3, 9), new Edge(v2, 7), new Edge(v6, 14));
        v2.edges = List.of(new Edge(v4, 15));
        v3.edges = List.of(new Edge(v4, 11), new Edge(v6, 2));
        v4.edges = List.of(new Edge(v5, 6));
        v5.edges = List.of();
        v6.edges = List.of(new Edge(v5, 9));

        List graph = List.of(v4, v5, v6, v1, v2, v3);*/

        // 负边情况
        /*Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");

        v1.edges = List.of(new Edge(v2, 2), new Edge(v3, 1));
        v2.edges = List.of(new Edge(v3, -2));
        v3.edges = List.of(new Edge(v4, 1));
        v4.edges = List.of();
        List graph = List.of(v1, v2, v3, v4);*/

        // 负环情况
        Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");

        v1.edges = List.of(new Edge(v2, 2));
        v2.edges = List.of(new Edge(v3, -4));
        v3.edges = List.of(new Edge(v4, 1), new Edge(v1, 1));
        v4.edges = List.of();
        List<Vertex> graph = List.of(v1, v2, v3, v4);

        bellmanFord(graph, v1);
    }

    private static void bellmanFord(List<Vertex> graph, Vertex source) {
        source.dist = 0;
        int size = graph.size();
        // 1. 进行 顶点个数 - 1 轮处理
        for (int i = 0; i < size - 1; i++) {
            // 2. 遍历所有的边
            for (Vertex s : graph) {
                for (Edge edge : s.edges) {
                    // 3. 处理每一条边
                    Vertex e = edge.linked;
                    if (s.dist != Integer.MAX_VALUE && s.dist + edge.weight < e.dist) {
                        e.dist = s.dist + edge.weight;
                        e.prev = s;
                    }
                }
            }
        }
        for (Vertex v : graph) {
            System.out.println(v + " " + (v.prev != null ? v.prev.name : "null"));
        }
    }
}

负环

2
-4
1
1
v1
v2
v3
v4

如果在【顶点-1】轮处理完成后,还能继续找到更短距离,表示发现了负环

Floyd-Warshall
-2
4
3
2
-1
v1
v3
v2
v4
public class FloydWarshall {
    public static void main(String[] args) {
        Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");

        v1.edges = List.of(new Edge(v3, -2));
        v2.edges = List.of(new Edge(v1, 4), new Edge(v3, 3));
        v3.edges = List.of(new Edge(v4, 2));
        v4.edges = List.of(new Edge(v2, -1));
        List<Vertex> graph = List.of(v1, v2, v3, v4);

        /*
                直接连通
                v1  v2  v3  v4
            v1  0   ∞   -2  ∞
            v2  4   0   3   ∞
            v3  ∞   ∞   0   2
            v4  ∞   -1  ∞   0

                k=0 借助v1到达其它顶点
                v1  v2  v3  v4
            v1  0   ∞   -2  ∞
            v2  4   0   2   ∞
            v3  ∞   ∞   0   2
            v4  ∞   -1  ∞   0

                k=1 借助v2到达其它顶点
                v1  v2  v3  v4
            v1  0   ∞   -2  ∞
            v2  4   0   2   ∞
            v3  ∞   ∞   0   2
            v4  3   -1  1   0

                k=2 借助v3到达其它顶点
                v1  v2  v3  v4
            v1  0   ∞   -2  0
            v2  4   0   2   4
            v3  ∞   ∞   0   2
            v4  3   -1  1   0

                k=3 借助v4到达其它顶点
                v1  v2  v3  v4
            v1  0   -1   -2  0
            v2  4   0   2   4
            v3  5   1   0   2
            v4  3   -1  1   0
         */
        floydWarshall(graph);
    }

    static void floydWarshall(List<Vertex> graph) {
        int size = graph.size();
        int[][] dist = new int[size][size];
        Vertex[][] prev = new Vertex[size][size];
        // 1)初始化
        for (int i = 0; i < size; i++) {
            Vertex v = graph.get(i); // v1 (v3)
            Map<Vertex, Integer> map = v.edges.stream().collect(Collectors.toMap(e -> e.linked, e -> e.weight));
            for (int j = 0; j < size; j++) {
                Vertex u = graph.get(j); // v3
                if (v == u) {
                    dist[i][j] = 0;
                } else {
                    dist[i][j] = map.getOrDefault(u, Integer.MAX_VALUE);
                    prev[i][j] = map.get(u) != null ? v : null;
                }
            }
        }
        print(prev);
        // 2)看能否借路到达其它顶点
        /*
            v2->v1          v1->v?
            dist[1][0]   +   dist[0][0]
            dist[1][0]   +   dist[0][1]
            dist[1][0]   +   dist[0][2]
            dist[1][0]   +   dist[0][3]
         */
        for (int k = 0; k < size; k++) {
            for (int i = 0; i < size; i++) {
                for (int j = 0; j < size; j++) {
//                    dist[i][k]   +   dist[k][j] // i行的顶点,借助k顶点,到达j列顶点
//                    dist[i][j]                  // i行顶点,直接到达j列顶点
                    if (dist[i][k] != Integer.MAX_VALUE &&
                            dist[k][j] != Integer.MAX_VALUE &&
                            dist[i][k] + dist[k][j] < dist[i][j]) {
                        dist[i][j] = dist[i][k] + dist[k][j];
                        prev[i][j] = prev[k][j];
                    }
                }
            }
//            print(dist);
        }
        print(prev);
    }

    static void path(Vertex[][] prev, List<Vertex> graph, int i, int j) {
        LinkedList<String> stack = new LinkedList<>();
        System.out.print("[" + graph.get(i).name + "," + graph.get(j).name + "] ");
        stack.push(graph.get(j).name);
        while (i != j) {
            Vertex p = prev[i][j];
            stack.push(p.name);
            j = graph.indexOf(p);
        }
        System.out.println(stack);
    }

    static void print(int[][] dist) {
        System.out.println("-------------");
        for (int[] row : dist) {
            System.out.println(Arrays.stream(row).boxed()
                    .map(x -> x == Integer.MAX_VALUE ? "∞" : String.valueOf(x))
                    .map(s -> String.format("%2s", s))
                    .collect(Collectors.joining(",", "[", "]")));
        }
    }

    static void print(Vertex[][] prev) {
        System.out.println("-------------------------");
        for (Vertex[] row : prev) {
            System.out.println(Arrays.stream(row).map(v -> v == null ? "null" : v.name)
                    .map(s -> String.format("%5s", s))
                    .collect(Collectors.joining(",", "[", "]")));
        }
    }

}

负环

如果在 3 层循环结束后,在 dist 数组的对角线处(i==j 处)发现了负数,表示出现了负环

8) 最小生成树

Prim
public class Prim {
    public static void main(String[] args) {
        Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");
        Vertex v5 = new Vertex("v5");
        Vertex v6 = new Vertex("v6");
        Vertex v7 = new Vertex("v7");

        v1.edges = List.of(new Edge(v2, 2), new Edge(v3, 4), new Edge(v4, 1));
        v2.edges = List.of(new Edge(v1, 2), new Edge(v4, 3), new Edge(v5, 10));
        v3.edges = List.of(new Edge(v1, 4), new Edge(v4, 2), new Edge(v6, 5));
        v4.edges = List.of(new Edge(v1, 1), new Edge(v2, 3), new Edge(v3, 2),
                new Edge(v5, 7), new Edge(v6, 8), new Edge(v7, 4));
        v5.edges = List.of(new Edge(v2, 10), new Edge(v4, 7), new Edge(v7, 6));
        v6.edges = List.of(new Edge(v3, 5), new Edge(v4, 8), new Edge(v7, 1));
        v7.edges = List.of(new Edge(v4, 4), new Edge(v5, 6), new Edge(v6, 1));

        List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6, v7);

        prim(graph, v1);

    }

    static void prim(List<Vertex> graph, Vertex source) {
        ArrayList<Vertex> list = new ArrayList<>(graph);
        source.dist = 0;

        while (!list.isEmpty()) {
            Vertex min = chooseMinDistVertex(list);
            updateNeighboursDist(min);
            list.remove(min);
            min.visited = true;
            System.out.println("---------------");
            for (Vertex v : graph) {
                System.out.println(v);
            }
        }


    }

    private static void updateNeighboursDist(Vertex curr) {
        for (Edge edge : curr.edges) {
            Vertex n = edge.linked;
            if (!n.visited) {
                int dist = edge.weight;
                if (dist < n.dist) {
                    n.dist = dist;
                    n.prev = curr;
                }
            }
        }
    }

    private static Vertex chooseMinDistVertex(ArrayList<Vertex> list) {
        Vertex min = list.get(0);
        for (int i = 1; i < list.size(); i++) {
            if (list.get(i).dist < min.dist) {
                min = list.get(i);
            }
        }
        return min;
    }
}
Kruskal
public class Kruskal {
    static class Edge implements Comparable<Edge> {
        List<Vertex> vertices;
        int start;
        int end;
        int weight;

        public Edge(List<Vertex> vertices, int start, int end, int weight) {
            this.vertices = vertices;
            this.start = start;
            this.end = end;
            this.weight = weight;
        }

        public Edge(int start, int end, int weight) {
            this.start = start;
            this.end = end;
            this.weight = weight;
        }

        @Override
        public int compareTo(Edge o) {
            return Integer.compare(this.weight, o.weight);
        }

        @Override
        public String toString() {
            return vertices.get(start).name + "<->" + vertices.get(end).name + "(" + weight + ")";
        }
    }

    public static void main(String[] args) {
        Vertex v1 = new Vertex("v1");
        Vertex v2 = new Vertex("v2");
        Vertex v3 = new Vertex("v3");
        Vertex v4 = new Vertex("v4");
        Vertex v5 = new Vertex("v5");
        Vertex v6 = new Vertex("v6");
        Vertex v7 = new Vertex("v7");

        List<Vertex> vertices = List.of(v1, v2, v3, v4, v5, v6, v7);
        PriorityQueue<Edge> queue = new PriorityQueue<>(List.of(
                new Edge(vertices,0, 1, 2),
                new Edge(vertices,0, 2, 4),
                new Edge(vertices,0, 3, 1),
                new Edge(vertices,1, 3, 3),
                new Edge(vertices,1, 4, 10),
                new Edge(vertices,2, 3, 2),
                new Edge(vertices,2, 5, 5),
                new Edge(vertices,3, 4, 7),
                new Edge(vertices,3, 5, 8),
                new Edge(vertices,3, 6, 4),
                new Edge(vertices,4, 6, 6),
                new Edge(vertices,5, 6, 1)
        ));

        kruskal(vertices.size(), queue);
    }

    static void kruskal(int size, PriorityQueue<Edge> queue) {
        List<Edge> result = new ArrayList<>();
        DisjointSet set = new DisjointSet(size);
        while (result.size() < size - 1) {
            Edge poll = queue.poll();
            int s = set.find(poll.start);
            int e = set.find(poll.end);
            if (s != e) {
                result.add(poll);
                set.union(s, e);
            }
        }

        for (Edge edge : result) {
            System.out.println(edge);
        }
    }
}

9) 不相交集合(并查集合)

基础
public class DisjointSet {
    int[] s;
    // 索引对应顶点
    // 元素是用来表示与之有关系的顶点
    /*
        索引  0  1  2  3  4  5  6
        元素 [0, 1, 2, 3, 4, 5, 6] 表示一开始顶点直接没有联系(只与自己有联系)

    */

    public DisjointSet(int size) {
        s = new int[size];
        for (int i = 0; i < size; i++) {
            s[i] = i;
        }
    }

    // find 是找到老大
    public int find(int x) {
        if (x == s[x]) {
            return x;
        }
        return find(s[x]);
    }

    // union 是让两个集合“相交”,即选出新老大,x、y 是原老大索引
    public void union(int x, int y) {
        s[y] = x;
    }

    @Override
    public String toString() {
        return Arrays.toString(s);
    }

}
路径压缩
public int find(int x) { // x = 2
    if (x == s[x]) {
        return x;
    }
    return s[x] = find(s[x]); // 0    s[2]=0
}
Union By Size

想法:
*将一个顶点个数少的连接到个数顶点多的集合里
****减少找老大距离

public class DisjointSetUnionBySize {
    int[] s;
    int[] size;
    public DisjointSetUnionBySize(int size) {
        s = new int[size];
        this.size = new int[size];
        for (int i = 0; i < size; i++) {
            s[i] = i;
            this.size[i] = 1;
        }
    }

    // find 是找到老大 - 优化:路径压缩
    public int find(int x) { // x = 2
        if (x == s[x]) {
            return x;
        }
        return s[x] = find(s[x]); // 0    s[2]=0
    }

    // union 是让两个集合“相交”,即选出新老大,x、y 是原老大索引
    public void union(int x, int y) {
//        s[y] = x;
        if (size[x] < size[y]) {
            int t = x;
            x = y;
            y = t;
        }
        s[y] = x;
        size[x] = size[x] + size[y];
    }

    @Override
    public String toString() {
        return "内容:"+Arrays.toString(s) + "\n大小:" + Arrays.toString(size);
    }

    public static void main(String[] args) {
        DisjointSetUnionBySize set = new DisjointSetUnionBySize(5);

        set.union(1, 2);
        set.union(3, 4);
        set.union(1, 3);
        System.out.println(set);
    }


}

图-相关题目

题目编号 题目标题 算法思想
547 省份数量 DFS、BFS、并查集
797 所有可能路径 DFS、BFS
1584 连接所有点的最小费用 最小生成树
743 网络延迟时间 单源最短路径
787 K 站中转内最便宜的航班 单源最短路径
207 课程表 拓扑排序
210 课程表 II 拓扑排序

2. 算法设计 - Greedy algorithm

1) 贪心例子

称之为贪心算法或贪婪算法,核心思想是

  1. 将寻找最优解的问题分为若干个步骤
  2. 每一步骤都采用贪心原则,选取当前最优解
  3. 因为没有考虑所有可能,局部最优的堆叠不一定让最终解最优

贪心算法是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。这种算法通常用于求解优化问题,如最小生成树、背包问题等。

贪心算法的应用:

  1. 背包问题:给定一组物品和一个背包,每个物品有一定的重量和价值,要求在不超过背包容量的情况下,尽可能多地装入物品。
  2. 活动选择问题:在一个活动集合中,每次只能参加一个活动,问如何安排时间以最大化所有活动的收益。
  3. 编辑距离问题:给定两个字符串,求它们之间的最小编辑距离(即将一个字符串转换为另一个字符串所需的最少操作次数)。
  4. 网络流问题:给定一张有向图和一些起点和终点,求最大流量。
  5. 找零问题:给定一定数量的硬币和需要找零的金额,求使用最少的硬币数。

常见问题及解答:

  1. 贪心算法一定会找到最优解吗?
    答:不一定。贪心算法只保证在每一步选择中都是最优的,但并不能保证整个问题的最优解。例如,背包问题中的贪心算法可能会导致最后一个物品没有被装入背包。
  2. 如何判断一个问题是否适合用贪心算法解决?
    答:一个问题如果可以用递归的方式分解成若干个子问题,且每个子问题都有明确的最优解(即局部最优),那么这个问题就可以用贪心算法解决。
  3. 贪心算法的时间复杂度是多少?
    答:贪心算法的时间复杂度取决于问题的规模和具体实现。一般来说,对于规模较小的问题,贪心算法的时间复杂度可以达到O(nlogn)或O(n2);对于规模较大的问题,可能需要O(n3)或更高。

几个贪心的例子

Dijkstra
// ...
while (!list.isEmpty()) {
    // 选取当前【距离最小】的顶点
    Vertex curr = chooseMinDistVertex(list);
    // 更新当前顶点邻居距离
    updateNeighboursDist(curr);
    // 移除当前顶点
    list.remove(curr);
    // 标记当前顶点已经处理过
    curr.visited = true;
}
  • 没找到最短路径的例子:负边存在时,可能得不到正确解
  • 问题出在贪心的原则会认为本次已经找到了该顶点的最短路径,下次不会再处理它(curr.visited = true)
  • 与之对比,Bellman-Ford 并没有考虑局部距离最小的顶点,而是每次都处理所有边,所以不会出错,当然效率不如 Dijkstra
Prim
// ...
while (!list.isEmpty()) {
    // 选取当前【距离最小】的顶点
    Vertex curr = chooseMinDistVertex(list);
    // 更新当前顶点邻居距离
    updateNeighboursDist(curr);
    // 移除当前顶点
    list.remove(curr);
    // 标记当前顶点已经处理过
    curr.visited = true;
}
Kruskal
// ...
while (list.size() < size - 1) {
    // 选取当前【距离最短】的边
    Edge poll = queue.poll();
    // 判断两个集合是否相交
    int i = set.find(poll.start);
    int j = set.find(poll.end);
    if (i != j) { // 未相交
        list.add(poll);
        set.union(i, j); // 相交
    }
}

其它贪心的例子

  • 选择排序、堆排序

  • 拓扑排序

  • 并查集合中的 union by size 和 union by height

  • 哈夫曼编码

  • 钱币找零,英文搜索关键字

    • change-making problem
    • find Minimum number of Coins
  • 任务编排

  • 求复杂问题的近似解

2) 零钱兑换问题

有几个解(零钱兑换 II)
public class Leetcode518 {
    public int change(int[] coins, int amount) {
        return rec(0, coins, amount, new LinkedList<>(), true);
    }

    /**
     * 求凑成剩余金额的解的个数
     *
     * @param index     当前硬币索引
     * @param coins     硬币面值数组
     * @param remainder 剩余金额
     * @param stack     -
     * @param first     -
     * @return 解的个数
     */
    public int rec(int index, int[] coins, int remainder, LinkedList<Integer> stack, boolean first) {
        if(!first) {
            stack.push(coins[index]);
        }
        // 情况1:剩余金额 < 0 - 无解
        int count = 0;
        if (remainder < 0) {
            print("无解:", stack);
        }
        // 情况2:剩余金额 == 0 - 有解
        else if (remainder == 0) {
            print("有解:", stack);
            count = 1;
        }
        // 情况3:剩余金额 > 0 - 继续递归
        else {
            for (int i = index; i < coins.length; i++) {
                count += rec(i, coins, remainder - coins[i], stack, false);
            }
        }
        if (!stack.isEmpty()) {
            stack.pop();
        }
        return count;
    }

    private static void print(String prompt, LinkedList<Integer> stack) {
        ArrayList<Integer> print = new ArrayList<>();
        ListIterator<Integer> iterator = stack.listIterator(stack.size());
        while (iterator.hasPrevious()) {
            print.add(iterator.previous());
        }
        System.out.println(prompt + print);
    }

    public static void main(String[] args) {
        Leetcode518 leetcode = new Leetcode518();
//        int count = leetcode.coinChange(new int[]{1, 5, 10, 25}, 41);
//        int count = leetcode.coinChange(new int[]{25, 10, 5, 1}, 41);
//        int count = leetcode.coinChange(new int[]{5, 2, 1}, 5);
//        int count = leetcode.coinChange(new int[]{1, 2, 5}, 5);
        int count = leetcode.change(new int[]{15, 10, 1}, 21);
        System.out.println(count);
    }

}
最优解(零钱兑换)- 穷举法
public class Leetcode322 {
    static int min = -1; // 需要的最少硬币数  2 3

    public int coinChange(int[] coins, int amount) {
        rec(0, coins, amount, new AtomicInteger(-1), new LinkedList<>(), true);
        return min;
    }

    // count 代表某一组合 钱币的总数
    public void rec(int index, int[] coins, int remainder, AtomicInteger count, LinkedList<Integer> stack, boolean first) {
        if (!first) {
            stack.push(coins[index]);
        }
        count.incrementAndGet(); // count++
        if (remainder == 0) {
            System.out.println(stack);
            if (min == -1) {
                min = count.get();
            } else {
                min = Integer.min(min, count.get());
            }
        } else if (remainder > 0) {
            for (int i = index; i < coins.length; i++) {
                rec(i, coins, remainder - coins[i], count, stack, false);
            }
        }
        count.decrementAndGet(); // count--
        if (!stack.isEmpty()) {
            stack.pop();
        }
    }

    public static void main(String[] args) {
        Leetcode322 leetcode = new Leetcode322();
//        int count = leetcode.coinChange(new int[]{5, 2, 1}, 5);
        int count = leetcode.coinChange(new int[]{25, 10, 5, 1}, 41);
//        int count = leetcode.coinChange(new int[]{2}, 3);
//        int count = leetcode.coinChange(new int[]{15, 10, 1}, 21);
        System.out.println(count);
    }
}
最优解(零钱兑换)- 贪心法
public class Leetcode322 {
    public int coinChange(int[] coins, int amount) {
        int remainder = amount;
        int count = 0;
        for (int coin : coins) {
            while (remainder - coin > 0) {
                remainder -= coin;
                count++;
            }
            if (remainder - coin == 0) {
                remainder = 0;
                count++;
                break;
            }
        }
        if (remainder > 0) {
            return -1;
        } else {
            return count;
        }
    }

    public static void main(String[] args) {
        Leetcode322 leetcode = new Leetcode322();
        int count = leetcode.coinChange(new int[]{5, 2, 1}, 5);
//        int count = leetcode.coinChange(new int[]{25, 10, 5, 1}, 41);
//        int count = leetcode.coinChange(new int[]{2}, 3);
        
        // 问题1 没有回头,导致找到更差的解
//        int count = leetcode.coinChange(new int[]{15, 10, 1}, 21);  
        // 问题2 没有回头,导致无解
//        int count = leetcode.coinChange(new int[]{15, 10}, 20);  
        System.out.println(count);
    }
}

3) Huffman 编码问题

问题引入

什么是编码?

简单说就是建立【字符】到【数字】的对应关系,如下面大家熟知的 ASC II 编码表,例如,可以查表得知字符【a】对应的数字是十六进制数【0x61】

\ 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
0000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
0010 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f
0020 20 ! " # $ % & ( ) * + , - . /
0030 0 1 2 3 4 5 6 7 8 9 : ; < = > ?
0040 @ A B C D E F G H I J K L M N O
0050 P Q R S T U V W X Y Z [ \ ] ^ _
0060 ` a b c d e f g h i j k l m n o
0070 p q r s t u v w x y z { | } ~ 7f

注:一些直接以十六进制数字标识的是那些不可打印字符

传输时的编码

  • java 中每个 char 对应的数字会占用固定长度 2 个字节
  • 如果在传输中仍采用上述规则,传递 abbccccccc 这 10 个字符
    • 实际的字节为 0061006200620063006300630063006300630063(16进制表示)
    • 总共 20 个字节,不经济

现在希望找到一种最节省字节的传输方式,怎么办?

假设传输的字符中只包含 a,b,c 这 3 个字符,有同学重新设计一张二进制编码表,见下图

  • 0 表示 a
  • 1 表示 b
  • 10 表示 c

现在还是传递 abbccccccc 这 10 个字符

  • 实际的字节为 01110101010101010 (二进制表示)
  • 总共需要 17 bits,也就是 2 个字节多一点,行不行?

不行,因为解码会出现问题,因为 10 会被错误的解码成 ba,而不是 c

  • 解码后结果为 abbbababababababa,是错误的

怎么解决?必须保证编码后的二进制数字,要能区分它们的前缀(prefix-free)

用满二叉树结构编码,可以确保前缀不重复

  • 向左走 0,向右走 1
  • 走到叶子字符,累计起来的 0 和 1 就是该字符的二进制编码

再来试一遍

  • a 的编码 0
  • b 的编码 10
  • c 的编码 11

现在还是传递 abbccccccc 这 10 个字符

  • 实际的字节为 0101011111111111111(二进制表示)
  • 总共需要 19 bits,也是 2 个字节多一点,并且解码没有问题了,行不行?

这回解码没问题了,但并非最少字节,因为 c 的出现频率高(7 次)a 的出现频率低(1 次),因此出现频率高的字符编码成短数字更经济

考察下面的树

  • 00 表示 a
  • 01 表示 b
  • 1 表示 c

现在还是传递 abbccccccc 这 10 个字符

  • 实际的字节为 000101 1111111 (二进制表示)
  • 总共需要 13 bits,这棵树就称之为 Huffman 树
  • 根据 Huffman 树对字符和数字进行编解码,就是 Huffman 编解码
Huffman 树
public class HuffmanTree {

    /*
        Huffman 树的构建过程

        1. 将统计了出现频率的字符,放入优先级队列

        2. 每次出队两个频次最低的元素,给它俩找个爹
        3. 把爹重新放入队列,重复 2~3
        4. 当队列只剩一个元素时,Huffman 树构建完成
     */

    static class Node {
        Character ch; // 字符
        int freq;     // 频次
        Node left;
        Node right;
        String code;  // 编码

        public Node(Character ch) {
            this.ch = ch;
        }

        public Node(int freq, Node left, Node right) {
            this.freq = freq;
            this.left = left;
            this.right = right;
        }

        int freq() {
            return freq;
        }

        boolean isLeaf() {
            return left == null;
        }

        @Override
        public String toString() {
            return "Node{" +
                    "ch=" + ch +
                    ", freq=" + freq +
                    '}';
        }
    }

    String str;
    Map<Character, Node> map = new HashMap<>();

    public HuffmanTree(String str) {
        this.str = str;
        // 功能1:统计频率
        char[] chars = str.toCharArray();
        for (char c : chars) {
            /*if (!map.containsKey(c)) {
                map.put(c, new Node(c));
            }
            Node node = map.get(c);
            node.freq++;*/
            Node node = map.computeIfAbsent(c, Node::new);
            node.freq++;
        }
        // 功能2: 构造树
        PriorityQueue<Node> queue = new PriorityQueue<>(Comparator.comparingInt(Node::freq));
        queue.addAll(map.values());
        while (queue.size() >= 2) {
            Node x = queue.poll();
            Node y = queue.poll();
            int freq = x.freq + y.freq;
            queue.offer(new Node(freq, x, y));
        }
        Node root = queue.poll();
        // 功能3:计算每个字符的编码, 功能4:字符串编码后占用 bits
        int sum = dfs(root, new StringBuilder());
        for (Node node : map.values()) {
            System.out.println(node + " " + node.code);
        }
        System.out.println("总共会占用 bits:" + sum);
    }

    private int dfs(Node node, StringBuilder code) {
        int sum = 0;
        if (node.isLeaf()) {
            node.code = code.toString();
            sum = node.freq * code.length();
        } else {
            sum += dfs(node.left, code.append("0"));
            sum += dfs(node.right, code.append("1"));
        }
        if (code.length() > 0) {
            code.deleteCharAt(code.length() - 1);
        }
        return sum;
    }

    public static void main(String[] args) {
        new HuffmanTree("abbccccccc");
    }
}

注意

  • Node::new 是一个 Function,根据 key(即字符)生成 Node 对象
  • 对应的是 public Node(Character ch) 有参构造
Huffman 编解码

补充两个方法,注意为了简单期间用了编解码都用字符串演示,实际应该按 bits 编解码

public class HuffmanTree {
    // ...
    
    // 编码
    public String encode() {
        char[] chars = str.toCharArray();
        StringBuilder sb = new StringBuilder();
        for (char c : chars) {
            sb.append(map.get(c).code);
        }
        return sb.toString();
    }

    // 解码
    public String decode(String str) {
        /*
            从根节点,寻找数字对应的字符
                数字是 0 向左走
                数字是 1 向右走
                如果没走到头,每走一步数字的索引 i++
            走到头就可以找到解码字符,再将 node 重置为根节点
         */
        char[] chars = str.toCharArray();
        int i = 0;
        StringBuilder sb = new StringBuilder();
        Node node = root;
        while (i < chars.length) {
            if (!node.isLeaf()) { // 非叶子
                if(chars[i] == '0') { // 向左走
                    node = node.left;
                } else if(chars[i] == '1') { // 向右走
                    node = node.right;
                }
                i++;
            }
            if (node.isLeaf()) {
                sb.append(node.ch);
                node = root;
            }
        }
        return sb.toString();
    }
    
    @SuppressWarnings("all")
    public static void main(String[] args) {
        HuffmanTree tree = new HuffmanTree("abbccccccc");
        String encoded = tree.encode();
        System.out.println(encoded);
        System.out.println(tree.decode(encoded));
    }
}

注意

  • 循环中非叶子节点 i 要自增,但叶子节点 i 暂不自增
  • 第一个非叶子的 if 判断结束后,仍需要第二个叶子的 if 判断,因为在第一个 if 内 node 发生了变化
相关题目
题目编号 题目标题 算法思路
1167(Plus 题目) 连接棒材的最低费用 Huffman 树、贪心

4) 活动选择问题

public class ActivitySelectionProblem {

    /*
        要在一个会议室举办 n 个活动
        - 每个活动有它们各自的起始和结束时间
        - 找出在时间上互不冲突的活动组合,能够最充分利用会议室(举办的活动次数最多)

        例1
            0   1   2   3   4   5   6   7   8   9
                |-------)
                    |-------)
                        |-------)
        例2
            0   1   2   3   4   5   6   7   8   9
                |---)
                        |---)
            |-----------------------)
                                |-------)
                                            |---)
                                |---------------)





        几种贪心策略
        1. 优先选择持续时间最短的活动
            0   1   2   3   4   5   6   7   8   9
                |---------------)
                            |-------)
                                |---------------)

        2. 优先选择冲突最少的活动
            0   1   2   3   4   5   6   7   8   9
            |-------)                                       3
                |-------)                                   4
                |-------)                                   4
                |-------)                                   4
                    |-------)                               4
                        |-------)                           2
                            |-----------)                   4
                                    |-------)               4
                                    |-------)               4
                                    |-------)               4
                                        |-------)           3

        3. 优先选择最先开始的活动
            0   1   2   3   4   5   6   7   8   9
            |-----------------------------------)
                |---)
                    |---)
                        |---)

        4. 优先选择最后结束的活动
     */

    static class Activity {
        int index;
        int start;
        int finish;

        public Activity(int index, int start, int finish) {
            this.index = index;
            this.start = start;
            this.finish = finish;
        }

        @Override
        public String toString() {
            return "Activity(" + index + ")";
        }
    }

    public static void main(String[] args) {
        Activity[] activities = new Activity[]{
                new Activity(0, 1, 3),
                new Activity(1, 2, 4),
                new Activity(2, 3, 5)
        };
//        Activity[] activities = new Activity[]{
//                new Activity(0, 1, 2),
//                new Activity(1, 3, 4),
//                new Activity(2, 0, 6),
//                new Activity(3, 5, 7),
//                new Activity(4, 8, 9),
//                new Activity(5, 5, 9)
//        };
        select(activities, activities.length);
    }

    public static void select(Activity[] activities, int n) {
        List<Activity> result = new ArrayList<>();
        int i, j;
        i = 0;
        result.add(activities[i]);
        for (j = 1; j < n; j++) {
            if (activities[j].start >= activities[i].finish) {
                result.add(activities[j]);
                i = j;
            }
        }
        System.out.println(result);
    }
}
相关题目
题目编号 题目标题 算法思路
435 无重叠区间 贪心

题解

// 下面代码为 Leetcode 435 题解
public int eraseOverlapIntervals(int[][] intervals) {
    Arrays.sort(intervals, Comparator.comparingInt(a -> a[1]));
    int i, j;
    i = 0;
    int count = 1;
    for (j = 1; j < intervals.length; j++) {
        if (intervals[j][0] >= intervals[i][1]) {
            i = j;
            count++;
        }
    }
    return intervals.length - count;
}
  • 找到不重叠的最多的活动数(count),即活动选择问题原始需求
  • 在此基础上,活动总数 - count,就是题目要的排除数量

5) 分数背包问题

贪心法
public class FractionalKnapsackProblem {

    /*
    1. n个物品都是液体,有重量和价值
    2. 现在你要取走 10升 的液体
    3. 每次可以不拿,全拿,或拿一部分,问最高价值是多少

        编号 重量(升) 价值
        0   4       24      水
        1   8       160     牛奶       选中 7/8
        2   2       4000    五粮液     选中
        3   6       108     可乐
        4   1       4000    茅台       选中

        8140

        简化起见,给出的数据都是【价值/重量】能够整除,避免计算结果中出现小数,增加心算难度
     */

    static class Item {
        int index;
        int weight;
        int value;

        public Item(int index, int weight, int value) {
            this.index = index;
            this.weight = weight;
            this.value = value;
        }

        int unitPrice() {
            return value / weight;
        }

        @Override
        public String toString() {
            return "Item(" + index + ")";
        }
    }

    public static void main(String[] args) {
        Item[] items = new Item[]{
                new Item(0, 4, 24),
                new Item(1, 8, 160),
                new Item(2, 2, 4000),
                new Item(3, 6, 108),
                new Item(4, 1, 4000),
        };
        select(items, 10);
    }

    static void select(Item[] items, int total) {
        Arrays.sort(items, Comparator.comparingInt(Item::unitPrice).reversed());
        int remainder = total;
        int max = 0;
        for (Item item : items) {
            if (remainder - item.weight > 0) {
                max += item.value;
                remainder -= item.weight;
            } else {
                max += remainder * item.unitPrice();
                break;
            }
        }
        System.out.println("最高价值为:" + max);
    }


}

6) 0-1 背包问题

贪心法

可能得不到最优解

public class KnapsackProblem {
    /*
    1. n个物品都是固体,有重量和价值
    2. 现在你要取走不超过 10克 的物品
    3. 每次可以不拿或全拿,问最高价值是多少

        编号 重量(g)  价值(元)
        0   1       1_000_000      钻戒一枚
        1   4       1600           黄金一块
        2   8       2400           红宝石戒指一枚
        3   5       30             白银一块

     */

    static class Item {
        int index;
        int weight;
        int value;

        public Item(int index, int weight, int value) {
            this.index = index;
            this.weight = weight;
            this.value = value;
        }

        public int unitValue() {
            return value / weight;
        }

        @Override
        public String toString() {
            return "Item(" + index + ")";
        }
    }

    public static void main(String[] args) {
        Item[] items = new Item[]{
                new Item(0, 1, 1_000_000),
                new Item(1, 4, 1600),
                new Item(2, 8, 2400),
                new Item(3, 5, 30)
        };
        select(items, 10);
    }

    static void select(Item[] items, int total) {
        Arrays.sort(items, Comparator.comparingInt(Item::unitValue).reversed());
        int max = 0; // 最大价值
        for (Item item : items) {
            System.out.println(item);
            if (total >= item.weight) { // 可以拿完
                total -= item.weight;
                max += item.value;
            } else { // 拿不完
//                max += total * item.unitValue();
//                break;
            }
        }
        System.out.println("最大价值是:" + max);
    }
}

贪心算法的局限

问题名称 是否能用贪心得到最优解 替换解法
Dijkstra(不存在负边) ✔️
Dijkstra(存在负边) Bellman-Ford
Prim ✔️
Kruskal ✔️
零钱兑换 动态规划
Huffman 树 ✔️
活动选择问题 ✔️
分数背包问题 ✔️
0-1 背包问题 动态规划

3. 算法设计 - Dynamic-Programming

Dynamic-Programming动态编程

Programming是指使用数学方法找出根据子问题求解当前的公式

Dynamic是指缓存上一步的结果根据上一步的结果求解当前结果

1)求解斐波那契数列

动态规划

package Algorithm.DynamicProgramming;

package Algorithm.DynamicProgramming;

/**
 * 求斐波那契数列的第n项(使用动态规划)
 */
public class FIbonaccl {
    /*public static int fibonacci(int n) {
        int[] dp = new int[n + 1];//用来缓存结果
        if (n == 0) {
            dp[0] = 0;
            return 0;
        }
        if (n == 1) {
            dp[1] = 1;
            return 1;
        }
        for (int i = 2; i < n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }*/
    public static int fibonacci(int n) {
        if (n == 0) {
            return 0;
        }
        if (n == 1) {

            return 1;
        }
        //使用两个变量代替数组
        int a = 0;
        int b = 1;
        for (int i = 2; i <= n; i++) {
            int c = b + a;
            a = b;
            b = c;
        }
        return b;
    }

    public static void main(String[] args) {
        System.out.println(fibonacci(13));
    }

}

2)Bellman-Ford算法

与之前一样都是属于动态规划

import java.util.*;
import java.util.stream.Collectors;

/**
 * 

Bellman-Ford 算法,可以处理负边

不能出现负环(自我解决) */
public class BellmanFord { static class Edge { int from; int to; int weight; public Edge(int from, int to, int weight) { this.from = from; this.to = to; this.weight = weight; } } /* f(v)表示从起点出发,到v这个顶点的最短距离 初始时 f(v) = 0 当v==起点 时 f(v) = 无穷 当v !=起点 时 之后 新 旧 所有from f(to) = min(f(to),f(from)+from.weight) v1 v2 v3 v4 v5 v6 0 无穷 无穷 无穷 无穷 无穷 */ public static void main(String[] args) { List<Edge> edges = List.of( new Edge(6,5,9), new Edge(4,5,6), new Edge(1,6,14), new Edge(3,6,2), new Edge(3,4,11), new Edge(2,4,15), new Edge(1,3,9), new Edge(1,2,7) ); int[] dp = new int[7];//一维数组缓存结果 dp[1] = 0; for (int i = 2; i < dp.length; i++) { dp[i] = Integer.MAX_VALUE; } for (int i = 0; i < edges.size() - 1; i++) { for (Edge e: edges){ if (dp[e.from] != Integer.MAX_VALUE){ dp[e.to] = Integer.min(dp[e.to],dp[e.from]+e.weight ); } } } print(dp); } static void print(int[] dp){ System.out.println(Arrays.stream(dp) .mapToObj(i->i == Integer.MAX_VALUE?"∞":String.valueOf(i)) .collect(Collectors.joining(",","[","]"))); } }

3)不同路径

import java.util.Arrays;
import java.util.stream.IntStream;

public class UniquePaths {
    //二维
    /*public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        for (int i = 0; i < m; i++) {
            dp[i][0] = 1;
        }
        for (int i = 0; i < n; i++) {
            dp[0][i] = 1;
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j 

    /**使用一维数组
     * 思路:
     *     用一位数组自己加上前一个更新的值
     * @param m
     * @param n
     * @return
     */

    public int uniquePaths(int m, int n) {
        int[] dp = new int[n];
        Arrays.fill(dp, 1);
        for (int i = 1; i < m; i++) {
            for (int j = 1; j <n; j++) {
                dp[j] = dp[j]+dp[j-1];
            }
        }
        System.out.println(Arrays.toString(dp));
        return dp[n-1];
    }
    static void print(int[][] dp){
        System.out.println("-".repeat(20));
        Object[] array = IntStream.range(0,dp[0].length+1).boxed().toArray();
        System.out.printf(("%2d".repeat(dp[0].length))+"%n",array);
        for (int[]d:dp){
            array = Arrays.stream(d).boxed().toArray();
            System.out.printf(("%2d".repeat(dp[0].length))+"%n",array);
        }
    }

    public static void main(String[] args) {
        System.out.println(new UniquePaths().uniquePaths(3,5));
    }
}

4)01背包问题

import java.util.Arrays;

public class KnapsackProblem {
    /*
    1. n个物品都是固体,有重量和价值
    2. 现在你要取走不超过 10克 的物品
    3. 每次可以不拿或全拿,问最高价值是多少

        编号 重量(g)  价值(元)                         简称
        0   1       1_000_000      钻戒一枚             D
        1   4       1600           黄金一块             A
        2   8       2400           红宝石戒指一枚        R
        3   5       30             白银一块             S
           1 001 630 贪心解
           1 002 400 正确解 
     */

    /**
     *      0    1   2   3   4   5   6   7   8   9   10
     *
     * 0    0    0   0   0   A   A   A   A   A   A   A      黄金
     *
     * 1    0    0   0   0   A   A   A   A   R   R   R      红宝石
     *
     * 2    0    0   0   0   A   A   A   A   R   R   R      白银
     *
     * 3    0    0   D   D   D   DA  DA  DA   DA  DR  DR    砖石
     * if(装不下){
     *      保持上一列价值不变
     *      dp[i][j] = dp[i-1][j]
     * }else{装得下
     *      当前选择价值加上 上一行剩余重量可加入价值比较
     *     dp[i][j]=max(dp[i-1][j],item.value+dp[i-1][j-item.weight])
     * }
     */

    static class Item {
        int index;
        int weight;
        int value;

        public Item(int index, int weight, int value) {
            this.index = index;
            this.weight = weight;
            this.value = value;
        }

        public int unitValue() {
            return value / weight;
        }

        @Override
        public String toString() {
            return "Item(" + index + ")";
        }
    }

    public static void main(String[] args) {
        Item[] items = new Item[]{
                new Item(0, 1, 1_0_000),
                new Item(1, 4, 1600),
                new Item(2, 8, 2400),
                new Item(3, 5, 30)
        };
        select1(items, 10);
    }
    //二维
    static void select2(Item[] items, int total) {
        int[][]dp = new int[items.length][total+1];
        Item item0 = items[0];
        for (int j = 0; j < total + 1; j++) {
            if (j>=item0.weight){
                dp[0][j] = item0.value;
            }else {
                dp[0][j] = 0;
            }
        }
        for (int i = 1; i < dp.length; i++) {
            Item item = items[i];
            for (int j = 0; j < total + 1; j++) {
                if (j>=item.weight){
                    dp[i][j]=Integer.max(dp[i-1][j],item.value+dp[i-1][j-item.weight]);
                }else{
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        for (int[] ints : dp) {
            System.out.println(Arrays.toString(ints));
        }
        System.out.println(dp[dp.length-1][total]);
    }
    //改动成1维
    static void select1(Item[] items, int total) {
        int[]dp = new int[total+1];
        Item item0 = items[0];
        for (int j = 0; j < total + 1; j++) {
            if (j>=item0.weight){
                dp[j] = item0.value;
            }else {
                dp[j] = 0;
            }
        }
        //从左到右不行了,改为从右到左
        for (int i = 1; i < items.length; i++) {
            Item item = items[i];
            for (int j = total; j > 0; j--) {
                if (j>=item.weight){
                    dp[j]=Integer.max(dp[j],item.value+dp[j-item.weight]);
                }
            }
        }

            System.out.println(Arrays.toString(dp));

    }
}

5)完全背包问题

import java.util.Arrays;

/**
 * 完全背包问题
 */
public class KnapsackProblemComplete {
    static class Item {
        int index;
        String name;
        int weight;
        int value;

        public Item(int index, int weight, int value) {
            this.index = index;
            this.weight = weight;
            this.value = value;
        }

        public Item(int index, String name, int weight, int value) {
            this.index = index;
            this.name = name;
            this.weight = weight;
            this.value = value;
        }


        @Override
        public String toString() {
            return "Item(" + index + ")";
        }
    }

    public static void main(String[] args) {
        Item[] items = new Item[]{
                new Item(1, "青铜",2, 3),//c
                new Item(2,  "白银",3, 4),//s
                new Item(3,  "黄金",4, 7),//a
        };
        System.out.println(select1(items,6));
    }
    /*
            0   1   2   3   4   5   6
        1   0   0   c   c   cc  cc  ccc
        2   0   0   c   s   cc  sc  ccc>ss
        3   0   0   c   s   a   a   ac>ccc
        if(放的下){
            dp[i][j] = max(dp[i-1][j-1],item.value+dp[i][j-item.weight])
        }eles{
            dp[i][j] = dp[i-1][j]
        }
     */

    /**
     * 二维
     * @param items 当前物品
     * @param total 可以装入最大价值
     * @return
     */
    private static int select2(Item[] items,int total){
        int[][]dp = new int[items.length][total+1];
        Item item0 =items[0];
        for (int j = 0; j < total + 1; j++) {
            if (j>=item0.weight){
                dp[0][j] = dp[0][j- item0.weight]+ item0.value;
            }else {
                dp[0][j] = 0;
            }
        }
        for (int i = 1;i<items.length;i++){
            Item item = items[i];
            for (int j = 0; j <total+1; j++) {

                if (j>=item.weight){
                    dp[i][j] = Integer.max(dp[i][j- item.weight]+ item.value,dp[i-1][j]);
                }else {
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        System.out.println(Arrays.toString(dp[items.length-1]));
        return dp[items.length-1][total];
    }

    /**
     * 一维
     * 第一行初始化可以省略了
     * @param items
     * @param total
     * @return
     */
    private static int select1(Item[] items,int total){
        int[]dp = new int[total+1];
        for (Item item : items) {
            for (int j = 0; j < total + 1; j++) {
                if (j >= item.weight) {
                    dp[j] = Integer.max(dp[j - item.weight] + item.value, dp[j]);
                }
            }
        }
        System.out.println(Arrays.toString(dp));
        return dp[total];
    }
}

比较完全背包和01背包:

完全背包是当前行的剩余价值,01背包是上一次 的剩余价值

6)零钱兑换

与完全背包问题类似,不过会出现凑不齐金额的情况(采取初始化最大价值 amount+1),

不使用Interge.max_value由于做加法 会超出范围

package Algorithm.DynamicProgramming;

import java.util.Arrays;

/**
 * 零钱兑换 - 动态规划
 * 凑成总金额的凑法中,需要硬币最少个数是几个
 */
public class ChangeMakingProblem {
    /**
     * 面值   0   1   2   3   4   5
     *  1    0   1   11  111 1111 11111
     *  2    0   1   2   21  22  221
     *  5    0   1   2   21  22   5
     *  if(装的下){
     *      dp[i][j] = min(dp[i-1][j],goValue(coins[i])+dp[i][j-coins[i]])
     *  }else{
     *      dp[i][j] = dp[i-1][j]
     *  }
     * @param coins   每个金额类比物品重量
     * @param amount  类比背包容量
     * @return   每个物品的价值都为1
     */
    public int coinChange(int[] coins, int amount) {
            int[]dp = new int[amount+1];
        for (int j = 1; j < amount + 1; j++) {
            if (j>=coins[0]){
                dp[j] = dp[j-coins[0]]+goValue(coins[0]);
            }else {
                dp[j] = amount+1;//最大值
            }
        }
        for (int i = 1; i < coins.length; i++) {
            for (int j = 1; j =coins[i]){
                    dp[j] = Integer.min(dp[j],goValue(coins[i])+dp[j-coins[i]]);
                }
            }
        }
        //System.out.println(Arrays.toString(dp));
        if (dp[amount] <= amount){
            return dp[amount];
        }
        return -1;
    }
    public int goValue(int coin){
        return 1;
    }

    public static void main(String[] args) {
        ChangeMakingProblem leetCode322 = new ChangeMakingProblem();
        int i = leetCode322.coinChange(new int[]{2}, 11);
        System.out.println(i);
    }
}

7)零钱兑换II

import java.util.Arrays;

/**
 * 零钱兑换II - 动态规划
 * 凑成总金额有几种凑发
 */
public class ChangeMakingProblemTwo518 {
    /**
     * 面值   0   1   2   3   4       5
     *  1    1   1   11  111 1111   11111
     *
     *  2    1  1   11  21   22     221
     *               2   111  211    11111
     *                        1111   2111
     *  5    1   1   2    21    22    5
     *           1   11   21    22    221
     *               2   111   211    11111
     *                        1111    2111
     *  if(装的下){
     *      dp[i][j] = dp[i-1][j]++dp[i][j-coins[i]]
     *  }else{
     *      dp[i][j] = dp[i-1][j]
     *  }
     * @param coins   每个金额类比物品重量
     * @param amount  类比背包容量
     * @return   每个物品的凑法都在上一次的基础上进行增加
     */
    public int coinChange(int[] coins, int amount) {
            int[]dp = new int[amount+1];
            dp[0] = 1;
        for (int coin : coins) {
            for (int j = coin; j < amount + 1; j++) {
                /*if (j >= coin) {
                    dp[j] += dp[j - coin];
                }*/
                //优化 j大于coin 则从大于时开始
                dp[j] += dp[j - coin];
            }
        }
        System.out.println(Arrays.toString(dp));
        return dp[amount];
    }

    public static void main(String[] args) {
        ChangeMakingProblemTwo518 leetCode322 = new ChangeMakingProblemTwo518();
        int i = leetCode322.coinChange(new int[]{1,2,5}, 5);
        System.out.println(i);
    }
}

8)钢条切割问题

类似的完全背包问题

import java.util.Arrays;

/**
 * 钢条切割问题-动态规划
 * 类似的完全背包问题
 */
public class CutRodProblem {
    /*
      0  1  2   3   4   5   6   7   8   9   10
      0  1  5   8   9   10  17  17  20  24  30
      4
         0  1  2   3   4      钢条长度 = 背包容量
 长度:1     1  11  111 1111
 value     (1) (2) (3)  (4)
      2     1  11  111 1111
               2   21   22
                        211
           (1) (5) (6)  (10)
      3     1  11  111 1111
               2   21   22
                   3    211
                         31
            (1) (5) (8) (10)
      4     1  11  111 1111
               2   21   22
                   3    211
                         31
                         4
             (1) (5) (8) (10)
        物品重量
        if(放的下){
        dp[i][j] = max(dp[i-1][j],当前物品价值+剩余物品价值=values[i]+dp[i][j-长度i])
        }else{
        dp[i][j] = dp[i-1][j]
        }
     */
    static int Cut(int[] values,int n){
        int[] dp = new int[n+1];
        for (int i = 1 ;i<values.length;i++ ) {
            var value =  values[i];
            for (int j = i; j < n + 1; j++) {
                dp[j] = Integer.max(dp[j],value+dp[j - i]);
            }
        }
        System.out.println(Arrays.toString(dp));
        return dp[n];
    }

    public static void main(String[] args) {
        System.out.println(Cut(new int[]{0,1,5,8,9},4));
    }
}

9)最长公共子串

/**
 * 最长公共子串
 */
public class LCSubstring {
    /*      经过发现  当前和上一次都相同 来自于对角线的值
            b   i   s   h   e   n   y   a   n   g
        s   0   0   1   0   0   0   0   0   0   0
        h   0   0   0   2   0   0   0   0   0   0
        e   0   0   0   0   3   0   0   0   0   0
        a   0   0   0   0   0   0   0   1   0   0
        n   0   0   0   0   0   0   0   0   2   0
        if(相同的字符){
              dp[i][j] = dp[i-1][j-1]+1;
        }else{
               dp[i][j] = 0;
        }
     */
    public static int longestCommonSubsequence(String a, String b) {
        int[][] dp = new int[a.length()][b.length()];
        int max = 0;
        for (int i = 0; i <a.length(); i++) {
            for (int j = 0; j < b.length(); j++) {
                if (a.charAt(i) == b.charAt(j)){
                    if (i==0||j==0){
                        dp[i][j]=1;
                    }else{
                        dp[i][j] = dp[i-1][j-1]+1;
                    }
                    max = Integer.max(dp[i][j],max);
                }else {
                    dp[i][j] = 0;
                }
            }
        }
        return max;
    }

    public static void main(String[] args) {
        System.out.println(longestCommonSubsequence("bishenyang","shen"));
    }
}

10)最长公共子序列

与最长公共子串的差别 **:**公共子序列不要求连续,只要求顺序保持正确。

/**
 * 最长公共子序列
 */
public class LCSubqueue {
    /*      多出来一列不用考虑边界条件
                a   b   c   x   y   z
            0   0   0   0   0   0   0
         a  0   1   1   1   1   1   1
         b  0   1   2   2   2   2   2
         x  0   1   2   2   3   3   3
         y  0   1   2   2   3   4   4
         z  0   1   2   2   3   4   5
         if(相同字符){
         dp[i][j] = dp[i-1][j-1]+1;  上一行上一列(左上角)+1
         }else 不同字符{
         dp[i][j] = max(dp[i-1][j],dp[i][j-1]);max(上一行,上一列)
         }
     */
    public static int longestCommonSubsequence(String a, String b) {
        int m = a.length();
        int n = b.length();
        int[][]dp = new int[m+1][n+1];
        for (int i = 0; i < m + 1; i++) {
            dp[i][0] = 0;
        }
        for (int j = 1; j < n + 1; j++) {
            dp[0][j] = 0;
        }
        for (int i = 1; i < m+1; i++) {
            for (int j = 1; j < n+1; j++) {
                if (a.charAt(i-1)==b.charAt(j-1)){
                    dp[i][j] = dp[i-1][j-1]+1;
                }else {
                    dp[i][j] = Integer.max(dp[i-1][j],dp[i][j-1]);
                }
            }
        }
        return dp[m][n];
    }

    public static void main(String[] args) {
        System.out.println(longestCommonSubsequence("abcxyz","abxyz"));
    }
}

LeetCode583为子序列变形

class Solution {
    public int minDistance(String word1, String word2) {
        int common = longestCommonSubsequence(word1,word2);
        return word1.length()+word2.length()-2*common;
    }
    public  int longestCommonSubsequence(String a, String b) {
        int m = a.length();
        int n = b.length();
        int[][]dp = new int[m+1][n+1];
        for (int i = 0; i < m + 1; i++) {
            dp[i][0] = 0;
        }
        for (int j = 1; j < n + 1; j++) {
            dp[0][j] = 0;
        }
        for (int i = 1; i < m+1; i++) {
            for (int j = 1; j < n+1; j++) {
                if (a.charAt(i-1)==b.charAt(j-1)){
                    dp[i][j] = dp[i-1][j-1]+1;
                }else {
                    dp[i][j] = Integer.max(dp[i-1][j],dp[i][j-1]);
                }
            }
        }
        return dp[m][n];
    }
}

11)最长递增子序列

/**
 * 最长递增子序列
 */
public class LeetCode300 {
    /*      1   2   3   4
        1   3   6   4   9
  初始   1   3   6   4   9
        1   13  16  14  19
                136 134 139
                        169
                        1369
                        149
                        1349
        (1  (2 (3 (3 (4
                        4
     */
    public int lengthOfLIS(int[] nums) {
        int[] dp = new int[nums.length];
        Arrays.fill(dp,1);
        for (int i = 1; i < nums.length; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i]>nums[j]){//满足了升序条件的数组
                    dp[i] = Integer.max(dp[i],dp[j]+1);
                }
            }
        }
        //结尾的数不一定是最大的
        return Arrays.stream(dp).max().getAsInt();
    }
}

12)卡特兰数

这几种节点组成二叉搜索树有多少种可能

C(0) = 1种

C(1) =1种

C(2) = 2种

C(3)时以1为头节点,剩余节点都大于1,其余可以看成2个节点组合,即C(2),以2为根节点固定了,只有一种,以3为根节点剩余节点都比3小,看作**C(2)**即可。

所以:

C(3) = C(2)+C(1)+C(2) = 5

c(4)时以1组成的节点为头节点,其余节点都大于1,可以看作有C(3)种情况,以2为根节点,左侧节点已经固定为1,右侧两个节点组合可以看成C(2),以3为根节点可以同理以2为根节点

C(4)=2 C(3)+2*C(2)* = 14

同理可看出

C(5) = C(0)C(4)+C(1)(3)+C(2)C(2)+C(3)C(1)+C(4) C(0)=14+5+4+5+14=42种

可得拆分图

/**  几种数能组成多少种二叉搜索数
 * 卡特兰数:
 *      c(1) = 1
 *      c(2) = 2
 *      c(3) = c(2)+c(1)+c(2) = 5
 *      C(4) = 2*C(3)+2*C(2) = 14
 */
public class Catalan {

    static int catalan(int n){
        int[]dp = new int[n+1];
        dp[0] = 1;
        dp[1] = 1;
        for (int j = 2; j < n + 1; j++) {
            for (int i = 0; i < j; i++) {
                System.out.printf("(%d,%d)\t",i,j-1-i);
                dp[j] += dp[i]*dp[j-1-i];
            }
            System.out.println(dp[j]);
        }


        return 0;
    }

    public static void main(String[] args) {
        System.out.println(catalan(5));
    }
}

卡特兰数的应用:

2.出栈总数:

3.括号生成

public List<String> generateParenthesis(int n) {
        ArrayList<String>[]dp = new ArrayList[n + 1];
        dp[0] = new ArrayList<>(List.of(""));//""
        dp[1] = new ArrayList<>(List.of("()"));//()
        //dp[2] ()(),(())
        //dp[3] ()()(),()(()),(())(),((()))
        for (int j = 2; j < n + 1; j++) {
            dp[j] = new ArrayList<>();
            for (int i = 0; i < j; i++) {//第j个卡特兰数的拆分
                System.out.printf("(%d,%d)\t",i,j-1-i);//数字个数代表了平级,和包含关系的括号个数
                //i对应了内层要嵌套的括号,j对应的集合是平级要拼接的括号
                for (String k1 : dp[i]) {
                    for (String k2 : dp[j - 1 - i]) {
                       dp[j].add("("+k1+")"+k2) ;
                    }
                }
            }
            System.out.println(dp[j]);
        }
题目编号 题目标题
96 不同的二叉搜索树
22 括号生成

13)打家劫舍

/**
 * 打家劫舍
 */
public class HouserRobber198 {
    /*
    价值   0  1   2   3   4
          0  0   0   0   0
    0(2)  2  0   0   0   0
    1(7)  2  7   0   0   0
    2(9)  2  7   11  0   0
    3(3)  2  7   11  11(10) 0
    4(1)  2  7   11  11  12
        dp[j] = max(dp[j-1],dp[j-2]+item.value)
     */
    public int rob(int[] nums) {
        if (nums.length == 1) {
            return nums[0];
        }
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        dp[1] = Integer.max(dp[0],nums[1]);

        for (int i = 2; i < nums.length; i++) {
            dp[i] = Integer.max(dp[i - 1], dp[i - 2] + nums[i]);
        }
        return dp[nums.length - 1];

    }

    public static void main(String[] args) {
        System.out.println(new HouserRobber198().rob(new int[]{2, 7, 9, 3, 1}));
    }
}

14)旅行商问题

求经过所有点路径最短

import java.util.Arrays;
import java.util.stream.Collectors;

public class TravellingSalesmanProblem {
    /*
        北京->
        上海->
        武汉->
        西安->

                3*2
        北京->上海->
                武汉->
                    西安-> 北京->
                西安->
                     武汉->北京->
             武汉->
                上海->
                    西安->北京->
                西安->
                    上海->北京->
             西安->
                上海->
                    武汉->北京->
                武汉->
                    上海->北京->
        d(从出发城市,剩余城市集合)==>从出发城市开始,走完剩余城市,花费的最小代价
        d(0,1|2|3)==>g[0][1]+d(1,2|3)
                                g[1][3]+d(3,2)
                                        ==>g[3][2]+d(2,空)
                                                    g[2][0]
                                g[1][2]+d(2,3)
                     g[0][3]+d(3,1|2)
                     g[0][2]+d(2,1|3)
       从出发城市    =i
       剩余城市集合  =j
               遍历j时的变量叫k(剩余的一个城市)
       d(i,j)==>min(
                g[i][k],d(k,j去掉k)
                g[i][k],d(k,j去掉k)
                g[i][k],d(k,j去掉k)
                )
                d(k,空) ==》从k回到起点
        0       1       2     3       4     5     6     7
        没城市   1       2     1|2     3     1|3   2|3     1|2|3
     0
     1
     2
     3
     000 没城市    0
     001 1号      1
     010 2号      2
     100 3号      4
     011 2号和1号  3

     同理


     */
    public static void main(String[] args) {
        int[][] graph = {
                {0,1,2,3},
                {1,0,6,4},
                {2,6,0,5},
                {3,4,5,6}
        };
        System.out.println(tsp(graph));
    }

    private static int tsp(int[][] g) {
        int m = g.length;//城市数
        int n = 1<<(m-1);//剩余城市的组合数 //用二进制表达 2的n次方
        int[][]dp = new int[m][n];
        for (int k = 0; k < m; k++) {
            dp[k][0] = g[k][0];
        }
        for (int j = 1; j < n; j++) {
            for (int i = 0; i < m; i++) {
                dp[i][j] = Integer.MAX_VALUE/2;
                if (contains(j,i))continue;//从剩余城市集合已包含出发城市
                //填充单元格
                for (int k = 0; k < m; k++) {
                    if (contains(j,k)){
                      dp[i][j] =Integer.min(g[i][k] + dp[k][exclude(j,k)],dp[i][j]);
                    }
                }
            }
        }
        print(dp);
        return dp[0][n-1];//最终结果在右上角

    }
    static void print(int[][] dist) {
        System.out.println("-------------");
        for (int[] row : dist) {
            System.out.println(Arrays.stream(row)//使用Arrays.stream()方法将整型数组row转换为一个流(Stream)
                    .boxed()//使用boxed()方法将流中的元素装箱为对应的包装类型,这里是将整型转为Integer类型。
                    .map(x -> x == Integer.MAX_VALUE/2 ? "∞" : String.valueOf(x))//接下来,使用map()方法将流中的每个元素进行映射操作。如果元素值是Integer.MAX_VALUE,将其替换为"∞",否则转换为对应的字符串表示。
                    .map(s -> String.format("%2s", s))//继续使用map()方法,对每个字符串进行格式化操作,使用String.format()将每个字符串格式化为长度为2的字符串。
                    .collect(Collectors.joining(",", "[", "]")));//使用collect()方法将格式化后的字符串元素收集到一个列表中,并使用Collectors.joining()方法将列表中的元素用逗号连接起来,同时在最前面加上"[“,在最后面加上”]",形成最终的字符串表示。
        }
    }
    static boolean contains(int set,int city){
        return (set>>(city-1)&1) == 1;
    }
    //011 去掉1
    //010
    static int exclude(int set,int city){//把集合内部去掉一个城市
        return set^(1<<(city-1));
    }
}

4.算法设计- DivideAndConquer

分治

把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。在计算机科学中,分治法就是运用分治思想的一种很重要的算法。分治法是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)等等

与动态规划对比

分治法与动态规划主要共同点:

二者都要求原问题具有最优子结构性质,都是将原问题分而治之,分解成若干个规模较小(小到很容易解决的程序)的子问题.然后将子问题的解合并,形成原问题的解.

  1. 分治法与动态规划实现方法:

​ 分治法通常利用递归求解.
动态规划通常利用迭代法自底向上求解,但也能用具有记忆功能的递归法自顶向下求解.

  1. 分治法与动态规划主要区别:

    分治法将分解后的子问题看成相互独立的.
    动态规划将分解后的子问题理解为相互间有联系,有重叠部分.

1)快速选择排序算法

import java.util.concurrent.ThreadLocalRandom;

/*
    快速选择算法-分儿治之
 */
public class QuickSelect {
    /*
    求排在第 i名的元素 i从0开始,由小到达排序
    6   5   1   2   4
     */
    public static int quick(int[]array, int left,int right,int i){
        /*
            6   5   1   2   [4]
            并不是直接排好序,而是找到正确基准点返
                    2
            1   2   4   6   5
            使用快排 直接把正确位置的元素取出
         */
        int p = partition(array, left, right);//基准点元素的索引
        if(p == i){
            return array[p];
        }
        if (i<p){
           return quick(array,left,p-1,i);
        }else{
          return  quick(array, p+1, right, i);
        }
    }
    private static int partition(int[] a, int left, int right) {
        int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
        swap(a, left, idx);
        int pv = a[left];
        int i = left + 1;
        int j = right;
        while (i <= j) {
            // i 从左向右找大的或者相等的
            while (i <= j && a[i] < pv) {
                i++;
            }
            // j 从右向左找小的或者相等的
            while (i <= j && a[j] > pv) {
                j--;
            }
            if (i <= j) {
                swap(a, i, j);
                i++;
                j--;
            }
        }
        swap(a, j, left);
        return j;
    }

    private static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

    public static void main(String[] args) {
        int[] array = {6,5,1,2,4};
        System.out.println(quick(array,0,array.length-1,0));
        System.out.println(quick(array,0,array.length-1,1));
        System.out.println(quick(array,0,array.length-1,2));
        System.out.println(quick(array,0,array.length-1,3));
        System.out.println(quick(array,0,array.length-1,4));
    }
}

2)快速幂 分治

/**
 * 快速幂 -- 分治
 */
public class QuickPow {
    public double myPow(double x, int n) {
        long p = n;
        if (p < 0) {
            p = -p;
        }
        double r = myPowPositive(x, p);
        return n < 0 ? 1 / r : r;
    }


    public static double myPowPositive(double x, long n) {
        if (n == 0) {
            return 1;
        }
        if (n == 1) {
            return x;
        }
        double y = myPowPositive(x, n / 2);
        /*
            可以用位运算判断寄偶
            奇数&1为1偶数为0

         */
        if (n % 2 == 0) {
            return y * y;
        } else {
            return x * y * y;
        }
    }

    public static void main(String[] args) {
        System.out.println(myPowPositive(2, 16));
    }
}

3)求平方根的整数部分

/**
 * 求平方根的整数部分
 */
public class SqrtLQ69 {
    int mySqrt(int x) {
        int l = 0, r = x, ans = -1;
        while (l <= r) {
            int mid = (r + l) >>> 1;
            if ((long) mid * mid <= x) { //可以乘法改除法防越界
                ans = mid;
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        return ans;
    }
}

4)数组中的中位数 - 快速选择排序

/**
 * 数组中的中位数 - 快速选择排序
 *
 */

public class FindMedian {
    /*
        寄数个
           1    4   5           ==》 4
           1    3   4   5   6   ==》 4
        偶数个
            1   3   4   5       ==》3.5
     */
    public static double findMedian(int[] nums){
        if (nums.length%2 == 1){//奇数
            return QuickSelect.quick(nums,0,nums.length-1,nums.length/2);
        }else {//偶数
            return (double) (QuickSelect.quick(nums, 0, nums.length - 1, nums.length / 2-1) +
                    QuickSelect.quick(nums, 0, nums.length - 1, nums.length / 2)) /2;
        }
    }

    public static void main(String[] args) {
        System.out.println(" 奇数");
        System.out.println(findMedian(new int[]{4,5,6}));
        System.out.println(findMedian(new int[]{4,5,1,6,3}));
        System.out.println("偶数");
        System.out.println(findMedian(new int[]{3,1,5,4}));
    }
}

5)至少k个字符的最长子串

public class LongestSubstring395 {
    public static int longestSubstring(String s, int k) {
        //终止条件 子串长度
        if (s.length()<k){
            return 0;//落选了 返回长度0
        }
        //1.统计出现次数
        int[] counts = new int[26];//索引对应字符  值用来存储该字符出现了几次
        char[] chars = s.toCharArray();
        for (char c : chars) {//'a' -> 0 ‘b’->1
            counts[c - 'a']++;
        }
        System.out.println(Arrays.toString(counts));
        for (int i = 0; i < chars.length; i++) {
            char c = chars[i];
            int count = counts[c - 'a'];//字符出现次数
            if (count > 0 && count < k) {
                int j = i + 1;
                while (j < s.length() && counts[chars[j] - 'a'] < k){
                    j++;
                }
                System.out.println(s.substring(0, i) + "\t" + s.substring(j));//切分字符
                return Integer.max(
                        longestSubstring(s.substring(0, i),k)
                        ,longestSubstring(s.substring(j),k));

            }
        }
        //子串入选
        return s.length();
        //1.先将出现次数小于k次的剔除
    }

    public static void main(String[] args) {
        System.out.println(longestSubstring("dddxaabaaabaacciiiiefbff", 3));
    /*
            dddxaabaaabaacciiiiefbff
         第一次剔除   ddd  aabaaabaa  iiii  fbff
                          aa aaa aa         ff

         统计字符串中每个字符的出现次数,移除那些出现次数
        String str = "aHello";
        byte[] byteArray = str.getBytes(); // 使用默认字符集进行转换

// 遍历字节数组并打印每个字节的值
        for (byte b : byteArray) {
            System.out.println(b);
        }
    }
}

5.算法设计-Backtracking

1)基本回溯

/**
 * 回溯
 *     -程序在运行过程中分成了多个阶段
 *     -通过某些手段,将数据恢复到之前某一阶段,这就称之为回溯
 *     -手段包括
 *        +方法栈
 *        +自定义栈
 */
public class Backtracking {
    public static void main(String[] args) {
        rec(1,new LinkedList<>());
    }
    static void rec(int n, LinkedList<String> list){
        if (n == 3){
            return;
        }
        System.out.println(" befor"+list);
        list.push("a");//加入
        rec(n+1,list);
        list.pop();//恢复数据,在方法后,进行对称操作
        System.out.println("befor "+list);
    }
}

2)全排列*-*回溯

import java.util.LinkedList;
import java.util.List;

/**
 * 全排列-回溯
 */
public class Permute {
    public static List<List<Integer>> permute(int[] nums) {
        boolean[] isvisited = new boolean[nums.length];
        List<List<Integer>> list = new LinkedList<>();
        dfs(nums,isvisited,new LinkedList<>(),0,list);

        return list;
    }
    static void dfs(int[] nums, boolean[] isvisited, LinkedList<Integer> stack, int j, List<List<Integer>> list){
        if (stack.size() == nums.length){
            list.add(new LinkedList<>(stack));
            System.out.println(stack);
            return;
        }
        for (int i = j; i < nums.length; i++) {
            if (!isvisited[i]){
                stack.push(nums[i]);
                isvisited[i] = true;
                dfs(nums, isvisited, stack,j, list);
                //回溯当前状态
                isvisited[i] = false;
                stack.pop();
            }

        }
    }
    public static void main(String[] args) {
        List<List<Integer>> permute = permute(new int[]{1, 2, 3});
        System.out.println(permute);
    }
}

3)不重复的全排列

/**
 * 不重复的全排列
 */
public class PermuteUnique {
    public static List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);

        boolean[] isvisited = new boolean[nums.length];
        List<List<Integer>> list = new LinkedList<>();
        dfs(nums,isvisited,new LinkedList<>(),0,list);

        return list;
    }
    static void dfs(int[] nums, boolean[] isvisited, LinkedList<Integer> stack, int j, List<List<Integer>> list){
        if (stack.size() == nums.length){
            list.add(new LinkedList<>(stack));
            System.out.println(stack);
            return;
        }
        for (int i = j; i < nums.length; i++) {
            if (i >0&&nums[i] == nums[i-1] &&!isvisited[i-1]){//找出重复数字
                continue;
            }
            if (!isvisited[i]){
                stack.push(nums[i]);
                isvisited[i] = true;
                dfs(nums, isvisited, stack,j, list);
                //回溯当前状态
                isvisited[i] = false;
                stack.pop();
            }

        }
    }
    public static void main(String[] args) {
        List<List<Integer>> permute = permuteUnique(new int[]{1, 1, 3});
        System.out.println(permute);
    }
}

4)组合

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * LeetCode 77 组合
 */
public class Combination {
    public static List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> result = new ArrayList<>();
        dfs(1,n,k,result,new LinkedList<Integer>());
        return result;
    }

    private static void dfs(int start,int n, int k, List<List<Integer>> result, LinkedList<Integer> stack) {
        if (stack.size() == k){
            result.add(new ArrayList<>(stack));
            return;
        }
        //i = 1 2 3 4
        for (int i = start; i <= n; i++) {
            // k k-stack.length //还差几个填满
            //n-i+1 还剩几个备用数字
            if (k-stack.size()>n-i+1){
                continue;
            }
            stack.push(i);//i = 1
            dfs(i+1,n,k,result,stack);
            stack.pop();//回溯
        }
    }

    public static void main(String[] args) {
        List<List<Integer>> lists = combine(4,2);
        lists.forEach(System.out::println);
    }
}

5)组合总和

/**
 * 组合总和
 */
public class combinationSum {
    public static List<List<Integer>> combinationSum(int[] candidates, int target) {

        List<List<Integer>> result = new ArrayList<>();
        dfs(0,candidates,target,result,new LinkedList<Integer>());
        return result;
    }

    private static void dfs(int start,int[] candidates, int target, List<List<Integer>> result, LinkedList<Integer> stack) {
        if (target == 0){
            result.add(new ArrayList<>(stack));
        }
       /* if (target<0){
            return;
        }*/
        for (int i = start; i < candidates.length; i++) {
            if (target < candidates[i]){
                continue;//剪枝后去掉小于0
            }
            stack.push(candidates[i]);
            dfs(i,candidates,target-candidates[i],result,stack);
            stack.pop();
        }
    }

    public static void main(String[] args) {
        List<List<Integer>> lists = combinationSum(new int[]{2,3,6,7},7);
        lists.forEach(System.out::println);
    }
}

6)组合总数2

import java.util.*;

/**
 * 组数总和2
 */
public class CombinationSumTwo {
    public static List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        boolean[] nums = new boolean[candidates.length];
        List<List<Integer>> result = new ArrayList<>();
        dfs(nums,0,candidates,target,result,new LinkedList<Integer>());
        return result;
    }

    private static void dfs(boolean[] isVisited, int start, int[] candidates, int target, List<List<Integer>> result, LinkedList<Integer> stack) {
        if (target == 0){
            result.add(new ArrayList<>(stack));
        }
       /* if (target<0){
            return;
        }*/
        for (int i = start; i < candidates.length; i++) {

            if (target < candidates[i]){
                continue;//剪枝后去掉小于0
            }
            if (i >0&&candidates[i] == candidates[i-1] &&!isVisited[i-1]){//找出重复数字
                continue;
            }
                stack.push(candidates[i]);
                isVisited[i] = true;
                dfs(isVisited, i+1,candidates,target-candidates[i],result,stack);
                stack.pop();
                isVisited[i] = false;



        }
    }

    public static void main(String[] args) {
        List<List<Integer>> lists = combinationSum(new int[]{10,1,2,7,6,1,5},8);
        lists.forEach(System.out::println);
    }
}

7)组合总数3

import java.util.*;

/**
 * 组数总和2
 */
public class CombinationSumTwo {
    public static List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        boolean[] nums = new boolean[candidates.length];
        List<List<Integer>> result = new ArrayList<>();
        dfs(nums,0,candidates,target,result,new LinkedList<Integer>());
        return result;
    }

    private static void dfs(boolean[] isVisited, int start, int[] candidates, int target, List<List<Integer>> result, LinkedList<Integer> stack) {
        if (target == 0){
            result.add(new ArrayList<>(stack));
        }
       /* if (target<0){
            return;
        }*/
        for (int i = start; i < candidates.length; i++) {

            if (target < candidates[i]){
                continue;//剪枝后去掉小于0
            }
            if (i >0&&candidates[i] == candidates[i-1] &&!isVisited[i-1]){//找出重复数字
                continue;
            }
                stack.push(candidates[i]);
                isVisited[i] = true;
                dfs(isVisited, i+1,candidates,target-candidates[i],result,stack);
                stack.pop();
                isVisited[i] = false;



        }
    }

    public static void main(String[] args) {
        List<List<Integer>> lists = combinationSum(new int[]{10,1,2,7,6,1,5},8);
        lists.forEach(System.out::println);
    }
}

8)N皇后问题

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

/**
 * N皇后问题
 */
public class NQueen51 {
    public static List<List<String>> solveNQueens(int n) {
        List<List<String>> map = new ArrayList<>();
        boolean[] ca = new boolean[n];//记录列冲突
        //i+j 行加列就是列数
        boolean[] cb = new boolean[2*n-1];//左斜线冲突
        //n-1-(i-j)
        boolean[] cc = new boolean[2*n-1];//右斜线冲突
        char[][] table = new char[n][n];//'.'   'Q'
        for (char[] chars : table) {
            Arrays.fill(chars,'.');
        }
        /*for (char[] chars : table) {
            System.out.println(Arrays.toString(chars));
        }*/
        dfs(map,0,n,table,ca,cb,cc);
        return map;
    }

    private static void dfs(List<List<String>> map, int i, int n, char[][] table, boolean[] ca, boolean[] cb, boolean[] cc) {
        if (i == n){//找到解
            List<String> kk = new ArrayList<>();
            for (char[] chars : table) {
                StringBuilder str  = new StringBuilder();
                for (char aChar : chars) {
                    str.append(aChar);
                }
                //System.out.println(str);
                kk.add(String.valueOf(str));
            }
            map.add(kk);
            return;
        }
        for (int j = 0; j <n; j++) {
            if (ca[j] || cb[i+j] || cc[n-1-(i-j)]){
                continue;
            }
            table[i][j] = 'Q';
            ca[j] = cb[i+j] = cc[n-1-(i-j)] = true;
            dfs(map, i+1,n,table,ca,cb,cc);
            ca[j] = cb[i+j] = cc[n-1-(i-j)] = false;
            table[i][j] = '.';
        }

    }

    public static void main(String[] args) {
        System.out.println(solveNQueens(4));
    }
}

9)解数独

/**
 * 解数独
 */
public class SudokuLeetcode37 {
    /**
     * 1. 不断遍历每个未填入的空格
     * 逐一尝试1~9 若行。列,九宫格内有冲突,则填入
     * 2.一旦1~9都尝试失败,回溯上一次状态,换数字重试
     * 3.关键还是要记录冲突状态
     */
    public static void solveSudoku(char[][] table) {
        //用二维数组记录行冲突状态   ca[i]表示某一行  【】表示这一行的数字冲突状态
        boolean[][] ca = new boolean[9][9];
        //列冲突
        boolean[][] cb = new boolean[9][9];
        //九宫格冲突状态  单元格和九宫格对应关系 i/3*3+j/3
        boolean[][] cc = new boolean[9][9];
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                char c = table[i][j];
                if (c != '.') {//初始冲突状态
                    ca[i][c - '1'] = true;  //字符5 - 字符4 差值就是4
                    ca[j][c - '1'] = true;
                    cc[i / 3 * 3 + j / 3][c - '1'] = true;
                }
            }
        }
        dfs(0, 0, table, ca, cb, cc);
    }

    //针对某一个方法进行填空
    static boolean dfs(int i, int j, char[][] table, boolean[][] ca, boolean[][] cb, boolean[][] cc) {
        while (table[i][j] != '.') {//查找下一个空格
            if (++j >= 9) {
                j = 0;
                i++;
            }
            if (i >= 9) {
                return true;//找到解了
            }
        }
        //填空
        for (int x = 1; x <= 9; x++) {
            //检查冲突
            if (ca[i][x-1] || ca[i / 3 * 3 + j / 3][x - 1]){
                continue;
            }
            table[i][j] = (char) (x + '0');  // 1 + '0' =>'1'
            /**
             * ca[0][0] = true 更新冲突 第0行不能存储1了
             * cb[0][0] = true 更新冲突 第0行不能存储1了
             * cc[0][0] = true 更新冲突 第0行不能存储1了
             */
            ca[i][x - 1] = cb[j][x - 1] = ca[i / 3 * 3 + j / 3][x - 1] = true;
            if (dfs(i,j,table,ca,cb,cc)){
                return true;
            };
            table[i][j] = '.';
            ca[i][x - 1] = cb[j][x - 1] = ca[i / 3 * 3 + j / 3][x - 1] = false;
        }
        return false;
    }

}

10)四数之和

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

/**
 * 四数之和
 */
public class FourSum {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        Arrays.sort(nums);
        List<List<Integer>> result = new LinkedList<>();
        dfs(4,0,nums.length-1,target,nums,new LinkedList<>(),result);
        return result;
    }
    static void dfs(int n, int i, int j, int target, int[] nums, LinkedList<Integer> stack, List<List<Integer>> result) {
        if (n == 2) {
            //用两数之和求解
            twoSum(i,j,nums,target,stack,result);
            return;
        }
        for (int k = i; k < j-(n-2); k++) {//四数之和 i
            //检查重复
            if (k > i && nums[k] == nums[k - 1]) {
                continue;
            }
            //固定一个数字,再尝试 n-1个数字之和
            stack.push(nums[k]);
            dfs(n - 1, k + 1, j, target - nums[k], nums, stack, result);
            stack.pop();
        }
    }

    static public void twoSum(int i, int j, int[] nums, int target
            , LinkedList<Integer> stack, List<List<Integer>> result) {
        while (i < j) {
            int sum = nums[i] + nums[j];
            if (sum < target) {
                i++;
            } else if (sum > target) {
                j--;
            } else {
                List<Integer> list = new ArrayList<>(stack);
                list.add(nums[i]);
                list.add(nums[j]);
                result.add(list);
                //继续查找其他的解
                i++;
                j--;
                while(i<j&&nums[i] == nums[i-1]){
                    i++;
                }
                while(i<j&&nums[j] == nums[j+1]){
                    j--;
                }
            }
        }
    }
}

5.其他结构

1)设计跳表

package Algorithm.advance;

import java.util.Random;

/**
 * 设计跳表
 *      
 */
class Skiplist {
    private static final int Max = 10;// redis 32 java 62
    private final Node head = new Node(-1);

    static class Node {
        int val;//值
        Node[] next = new Node[Max];//next数组

        public Node(int val) {
            this.val = val;
        }
    }

    public Skiplist() {

    }
	//下楼梯寻找
    public Node[] find(int val) {
        Node[] path = new Node[Max];//记录经过路径
        Node curr = head;
        for (int level = Max - 1; level >= 0; level--) {//从上向下找
            while (curr.next[level] != null && curr.next[level].val < val) {//向右
                curr = curr.next[level];
            }
            path[level] = curr;
        }
        System.out.println(curr);
        return path;
    }

    public boolean search(int target) {
        Node[] path = find(target);
        Node node = path[0].next[0];//路径最底层节点
        return node != null && node.val == target;
    }

    public void add(int val) {
        //1.确定添加位置 (把 val 当作目标查询,经历的路径就可以确定添加位置)
        Node[] path = find(val);
        //2.创建新节点随机高度
        Node node = new Node(val);
        int level = randomLevel(Max);
        //3.修改路径节点 next 指针,以及新节点next 指针
        for (int i = 0; i < level; i++) {
            node.next[i] = path[i].next[i];//当前节点指向原来路径的下一节点
            path[i].next[i] = node;
        }
    }

    public boolean erase(int val) {
        Node[] path = find(val);
        Node node = path[0].next[0];
        if (node == null || node.val != val){
            return false;
        }
        for (int i = 0; i < Max; i++) {
            if (path[i].next[i] != node){
                break;
            }
            path[i].next[i] = node.next[i];
        }
        return true;
    }

    /*
        设计一个方法,方法会随机返回1~max 的数字
        从 1 开始,数字的几率逐级减半,例如max = 4 让打概
            -50% 的几率返回 1
            -25% 的几率返回 2
            -12.5% 的几率返回 3
            -12.5% 的几率返回 4
     */
    static Random random = new Random();

    static int randomLevel(int max) {
        /*return random.nextBoolean() ? 1 : 50%
                random.nextBoolean() ? 2 :  25%
                        random.nextBoolean() ? 3 : 4; 12.5%  */
        int x = 1;
        while (x < max) {
            if (random.nextBoolean()) {
                return x;
            }
            x++;
        }
        return x;
    }
}

2)LFU缓存-最不常使用缓存

package Algorithm.advance;

import java.util.HashMap;

/**
 * LFU缓存
 * 最不常使用缓存
 */
class LFUCache {
    static class Node {
        Node pre;
        Node next;
        int key;
        int value;
        int freq = 1;//频度

        public Node() {
        }

        public Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }

    static class DoublyLinkedList {
        Node head;
        Node tail;
        int size = 0;

        public DoublyLinkedList() {
            this.head = tail = new Node();//哨兵节点
            head.next = tail;
            tail.pre = head;
        }

        //头部添加
        public void addFirst(Node newFirst) {//O(1)
            Node oldFirst = head.next;
            newFirst.pre = head;
            newFirst.next = oldFirst;
            head.next = newFirst;
            oldFirst.pre = newFirst;
            size++;
        }

        //已知节点删除
        public void remove(Node node) {
            Node prev = node.pre;
            Node next = node.next;
            prev.next = next;
            next.pre = prev;
            size--;
        }

        //尾部删除
        public Node removeLast() {
            Node last = tail.pre;
            remove(last);
            return last;
        }

        public boolean isEmpty() {
            return size == 0;
        }
    }

    private HashMap<Integer, Node> kvMap = new HashMap<>();
    private HashMap<Integer, DoublyLinkedList> freqMap = new HashMap<>();
    private final int capacity;//容量
    private int minFreq = 1;//最小频度

    public LFUCache(int capacity) {
        this.capacity = capacity;
    }

    /*
        key 不存在
            返回-1
        key 存在
            返回value值
            增加节点使用频度,将其转移到频度+1的链表当中
     */
    public int get(int key) {
        if (!kvMap.containsKey(key)) {
            return -1;
        }
        Node node = kvMap.get(key);
        DoublyLinkedList list = freqMap.get(node.freq);//旧链表
        list.remove(node);
        if (list.isEmpty() && node.freq == minFreq) {
            minFreq++;
        }
        node.freq++;
        /*DoublyLinkedList list = freqMap.get(node.freq);
        if (list == null) {
            list = new DoublyLinkedList();
            freqMap.put(node.freq, list);
        }
        list.addFirst(node);*/
        freqMap.computeIfAbsent(node.freq, k -> new DoublyLinkedList())
                .addFirst(node);
        return node.value;
    }

    /*
        更新
            将节点的 value 更新,增加节点的使用频度,将其转移到频度+1的链表中
        新增
            检查是否超过容量,若超过,淘汰minFreq 链表的最后节点
            创建新节点,放入kvMap,并加入频度为1的双向链表
     */
    public void put(int key, int value) {
        if (kvMap.containsKey(key)) {//更新
            Node node = kvMap.get(key);
            DoublyLinkedList list = freqMap.get(node.freq);
            list.remove(node);
            if (list.isEmpty() && node.freq == minFreq) {
                minFreq++;
            }
            node.freq++;
            freqMap.computeIfAbsent(node.freq, k -> new DoublyLinkedList())
                    .addFirst(node);
            node.value = value;
        } else {//新增
            if (kvMap.size() == capacity){
                Node node = freqMap.get(minFreq).removeLast();
                kvMap.remove(node.key);
            }
            Node node = new Node(key, value);
            kvMap.put(key, node);
            freqMap.computeIfAbsent(1,k->new DoublyLinkedList()).addFirst(node);
            minFreq = 1;
        }
    }
}

3)LRU**缓存 (最近最少使用)**最近不使用缓存

package Algorithm.advance;

import java.util.HashMap;

/**
 * LRU缓存
 * (最近最少使用)最近不使用缓存
 */
public class LRUCache {
    static class Node {
        Node pre;
        Node next;
        int key;
        int value;

        public Node() {
        }

        public Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }


    private final HashMap<Integer,Node> map = new HashMap<>();
    private final DoublyLinkedList list = new DoublyLinkedList();
    private final int capaciity;
    public LRUCache(int capacity) {
        this.capaciity = capacity;
    }
    static class DoublyLinkedList {
        Node head;
        Node tail;

        public DoublyLinkedList() {
            this.head = tail = new Node();//哨兵节点
            head.next = tail;
            tail.pre = head;
        }
        //头部添加
        public void addFirst(Node newFirst){//O(1)
            Node oldFirst = head.next;
            newFirst.pre = head;
            newFirst.next = oldFirst;
            head.next = newFirst;
            oldFirst.pre = newFirst;
        }
        //已知节点删除
        public void remove(Node node){
            Node prev = node.pre;
            Node next = node.next;
            prev.next = next;
            next.pre = prev;
        }
        //尾部删除
        public Node removeLast(){
            Node last = tail.pre;
            remove(last);
            return last;
        }
    }

    public int get(int key) {
        if (!map.containsKey(key)){
            return -1;
        }
        Node node = map.get(key);
        list.remove(node);
        list.addFirst(node);
        return node.value;
    }

    public void put(int key, int value) {
        if (map.containsKey(key)){//更新
            Node node = map.get(key);
            node.value = value;
            list.remove(node);
            list.addFirst(node);
        }else{//新增
            Node node = new Node(key,value);
            map.put(key,node);
            list.addFirst(node);
            if (map.size() > capaciity){
                Node removed = list.removeLast();
                map.remove(removed.key);
            }
        }
    }
}

4)最小栈

方法1:使用两个栈结构,一个记录元素,一个记录当前最小值

方法2:创建一个结构同时包含节点和当前最小值,入栈

package Algorithm.advance;

import java.util.LinkedList;

/**
 * 最小栈
 *      最快找到最小值
 *      使用两个栈,其中一个栈记录当时元素时的最小值
 */
public class MinStack {
    LinkedList<Integer> stack = new LinkedList<>();
    LinkedList<Integer> min = new LinkedList<>();
    public MinStack() {
        min.push(Integer.MAX_VALUE);
    }

    public void push(int val) {
        stack.push(val);
        min.push(Math.min(val,min.peek()));
    }

    public void pop() {
        if (stack.isEmpty()){
            return;
        }
        stack.pop();
        min.pop();
    }

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

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

5.设计短网址

package Algorithm.advance;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;

/**
 * TinyURL 的加密与解密
 * 设计短网址 短网址服务
 * 使网址加密变短 请求后解密成为真正的网址
 */
public class TinyURL {
    /**
     * 要让长网址和短网址一一对应起来
     * 1.随机数
     * 2.Hash码
     * 3.递增数
     */
    static class CodeRandom {

        private Map<String, String> longToShort = new HashMap<>();
        private Map<String, String> shortToLong = new HashMap<>();
        private static final String SHORT_PREFIX = "http://tinyurl.com/";

        // Encodes a URL to a shortened URL.
        public String encode(String longUrl) {
            String shortUrl = longToShort.get(longUrl);
            if (shortUrl != null) {
                return shortUrl;
            }
            //生成短网址
            while (true) {
                int id = ThreadLocalRandom.current().nextInt();//int
                shortUrl = SHORT_PREFIX + id;
                if (!shortToLong.containsKey(shortUrl)) {
                    longToShort.put(longUrl, shortUrl);
                    shortToLong.put(shortUrl, longUrl);
                    break;
                }
            }
            return shortUrl;
        }

        // Decodes a shortened URL to its original URL.
        public String decode(String shortUrl) {
            return shortToLong.get(shortUrl);
        }
    }

    static class CodeHashCode {
        private Map<String, String> longToShort = new HashMap<>();
        private Map<String, String> shortToLong = new HashMap<>();
        private static final String SHORT_PREFIX = "http://tinyurl.com/";

        // Encodes a URL to a shortened URL.
        public String encode(String longUrl) {
            String shortUrl = longToShort.get(longUrl);
            if (shortUrl != null) {
                return shortUrl;
            }
            //生成短网址
            int id = longUrl.hashCode();
            while (true) {
                ;//int
                shortUrl = SHORT_PREFIX + id;
                if (!shortToLong.containsKey(shortUrl)) {
                    longToShort.put(longUrl, shortUrl);
                    shortToLong.put(shortUrl, longUrl);
                    break;
                }
                id++;
            }
            return shortUrl;
        }

        // Decodes a shortened URL to its original URL.
        public String decode(String shortUrl) {
            return shortToLong.get(shortUrl);
        }
    }

    /*
        不能多线程使用
         不能分布式
         4e9iAk 怎么生成的
         a-z 0-9 A-Z  62进制数字
         十进制 ==》 62 进制
         31 %16 = 15
         31 % 16 = 1
         1  % 16 = 1
         1  %16  = 0
     */
    static class CodeSequence {
        private Map<String, String> longToShort = new HashMap<>();
        private Map<String, String> shortToLong = new HashMap<>();
        private static final String SHORT_PREFIX = "http://tinyurl.com/";
        private static final char[] toBase62 = {
                'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
                'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
                'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
                'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
                '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
        };

        public static String toBase62(int number) {
            if (number == 0){
                return String.valueOf(toBase62[0]);
            }
            StringBuilder sb = new StringBuilder();
            while (number > 0) {
                int r = number % 62;
                sb.append(toBase62[r]);
                number = number / 62;
            }
            return sb.toString();
        }

        private static int id = 1;

        // Encodes a URL to a shortened URL.
        public String encode(String longUrl) {
            String shortUrl = longToShort.get(longUrl);
            if (shortUrl != null) {
                return shortUrl;
            }
            //生成短网址
            shortUrl = SHORT_PREFIX + id;
            longToShort.put(longUrl, shortUrl);
            shortToLong.put(shortUrl, longUrl);
            id++;
            return shortUrl;
        }

        // Decodes a shortened URL to its original URL.
        public String decode(String shortUrl) {
            return shortToLong.get(shortUrl);
        }
    }

}

6)设计推特

package Algorithm.advance;

import java.util.*;

/**
 * 设计推特
 */
/*public class Twitter {
    public Twitter() {
        time =0;
    }
    static class Tweet{
        int id;
        int time;
        Tweet next;

        public Tweet(int id, int time, Tweet next) {
            this.id = id;
            this.time = time;
            this.next = next;
        }

        public int getId() {
            return id;
        }

        public int getTime() {
            return time;
        }
    }
    static class User{
        int id;

        public User(int id) {
            this.id = id;
        }
        Set followees = new HashSet<>();
        Tweet head = new Tweet(-1,-1,null);
    }
    private Map userMap = new HashMap<>();
    private static int time;
    //发布文章
    public void postTweet(int userId, int tweetId) {
        User user = userMap.computeIfAbsent(userId, User::new);
        user.head.next = new Tweet(tweetId, time++, user.head.next);
    }
    //新增关注
    public void follow(int userId, int followeeId) {
        User user = userMap.computeIfAbsent(userId, User::new);
        User followee = userMap.computeIfAbsent(followeeId, User::new);
        user.followees.add(followee.id);
    }
    //取消关注
    public void unfollow(int userId, int followeeId) {
        User user = userMap.get(userId);
        if (user != null){
            user.followees.remove(followeeId);
        }
    }
    //获取最新10篇文章 (包括用户自己和关注用户)
    public List getNewsFeed(int userId) {
        User user = userMap.get(userId);
        if (user == null){
            return List.of();
        }
        PriorityQueue queue = new PriorityQueue<>(Comparator.comparingInt(Tweet::getId).reversed());
        if (user.head.next != null){
            queue.offer(user.head.next);
        }

        for (Integer id : user.followees) {
            User followee = userMap.get(id);
            if (followee.head.next != null){
                queue.offer(followee.head.next);
            }
        }
        List res = new ArrayList<>();
        int count = 0;
        while (!queue.isEmpty()&&count<10){
            Tweet max = queue.poll();
            res.add(max.id);
            if (max.next!= null){
                queue.add(max.next);
            }
            count++;
        }
        return res;
    }




}*/
class Twitter {
    private class Node {
        // 哈希表存储关注人的 Id
        Set<Integer> followee;
        // 用链表存储 tweetId
        LinkedList<Integer> tweet;

        Node() {
            followee = new HashSet<Integer>();
            tweet = new LinkedList<Integer>();
        }
    }

    // getNewsFeed 检索的推文的上限以及 tweetId 的时间戳
    private int recentMax, time;
    // tweetId 对应发送的时间
    private Map<Integer, Integer> tweetTime;
    // 每个用户存储的信息
    private Map<Integer, Node> user;

    public Twitter() {
        time = 0;
        recentMax = 10;
        tweetTime = new HashMap<Integer, Integer>();
        user = new HashMap<Integer, Node>();
    }

    // 初始化
    public void init(int userId) {
        user.put(userId, new Node());
    }

    public void postTweet(int userId, int tweetId) {
        if (!user.containsKey(userId)) {
            init(userId);
        }
        // 达到限制,剔除链表末尾元素
        if (user.get(userId).tweet.size() == recentMax) {
            user.get(userId).tweet.remove(recentMax - 1);
        }
        user.get(userId).tweet.addFirst(tweetId);
        tweetTime.put(tweetId, ++time);
    }

    public List<Integer> getNewsFeed(int userId) {
        LinkedList<Integer> ans = new LinkedList<Integer>();
        for (int it : user.getOrDefault(userId, new Node()).tweet) {
            ans.addLast(it);
        }
        for (int followeeId : user.getOrDefault(userId, new Node()).followee) {
            if (followeeId == userId) { // 可能出现自己关注自己的情况
                continue;
            }
            LinkedList<Integer> res = new LinkedList<Integer>();
            int tweetSize = user.get(followeeId).tweet.size();
            Iterator<Integer> it = user.get(followeeId).tweet.iterator();
            int i = 0;
            int j = 0;
            int curr = -1;
            // 线性归并
            if (j < tweetSize) {
                curr = it.next();
                while (i < ans.size() && j < tweetSize) {
                    if (tweetTime.get(curr) > tweetTime.get(ans.get(i))) {
                        res.addLast(curr);
                        ++j;
                        if (it.hasNext()) {
                            curr = it.next();
                        }
                    } else {
                        res.addLast(ans.get(i));
                        ++i;
                    }
                    // 已经找到这两个链表合起来后最近的 recentMax 条推文
                    if (res.size() == recentMax) {
                        break;
                    }
                }
            }
            for (; i < ans.size() && res.size() < recentMax; ++i) {
                res.addLast(ans.get(i));
            }
            if (j < tweetSize && res.size() < recentMax) {
                res.addLast(curr);
                for (; it.hasNext() && res.size() < recentMax;) {
                    res.addLast(it.next());
                }
            }
            ans = new LinkedList<Integer>(res);
        }
        return ans;
    }

    public void follow(int followerId, int followeeId) {
        if (!user.containsKey(followerId)) {
            init(followerId);
        }
        if (!user.containsKey(followeeId)) {
            init(followeeId);
        }
        user.get(followerId).followee.add(followeeId);
    }

    public void unfollow(int followerId, int followeeId) {
        user.getOrDefault(followerId, new Node()).followee.remove(followeeId);
    }
}

二. 习题

你可能感兴趣的:(算法)