Структуры данных и алгоритмы: 10 распространенных алгоритмов

предисловие

В этой статье в основном объясняются 10 распространенных алгоритмов.


Структуры данных и алгоритмы Список статей

Структура данных и список статей об алгоритмах: нажмите здесь, чтобы перейти к просмотру


Оглавление

10 общих алгоритмов.png


1 Алгоритм бинарного поиска

Двоичный поиск — это распространенный алгоритм поиска целевого значения в упорядоченном массиве. Он может быстро определить позицию целевого значения в массиве, сравнивая целевое значение со средним элементом массива.
Ниже приведены этапы реализации бинарного поиска:

  1. Инициализировать переменные: определить целевое значение , начальный индекс и конечный индекс targetмассива .startend

  2. Циклический поиск: используйте цикл (обычно цикл while) для поиска до тех пор, пока не будет startбольше, чем end, что указывает на то, что целевое значение не найдено.

  3. Вычислить средний индекс: вычислив средний индекс mid, вы можете получить индекс среднего элемента массива.

  4. Сравнить промежуточные элементы: array[mid]сравнивает целевое значение с промежуточными элементами.

    • Если целевое значение равно среднему элементу, это означает, что целевое значение найдено и возвращается средний индекс mid.
    • Если целевое значение меньше среднего элемента, это означает, что целевое значение находится в левой половине, а конечный индекс обновления endравен mid - 1.
    • Если целевое значение больше среднего элемента, это означает, что целевое значение находится в правой половине, а индекс начала обновления startравен mid + 1.
  5. Повторяйте шаги 3 и 4, пока целевое значение не будет найдено или определено как несуществующее.

Ниже приведен пример кода Java, реализующего бинарный поиск:

public class BinarySearch {
    public static int binarySearch(int[] array, int target) {
        int start = 0;
        int end = array.length - 1;

        while (start <= end) {
            int mid = start + (end - start) / 2;

            if (array[mid] == target) {
                return mid; // 找到目标值,返回索引
            } else if (array[mid] < target) {
                start = mid + 1; // 目标值在右半部分,更新起始索引
            } else {
                end = mid - 1; // 目标值在左半部分,更新结束索引
            }
        }

        return -1; // 目标值不存在,返回 -1
    }

    public static void main(String[] args) {
        int[] array = {1, 3, 5, 7, 9};
        int target = 5;

        int index = binarySearch(array, target);
        if (index != -1) {
            System.out.println("目标值 " + target + " 的索引为 " + index);
        } else {
            System.out.println("目标值 " + target + " 不存在");
        }
    }
}

результат операции:

目标值 5 的索引为 2

В приведенном выше примере мы определили binarySearchметод, который принимает упорядоченный массив arrayи целевое значение targetв качестве параметров. Этот метод использует цикл для выполнения двоичного поиска и, наконец, возвращает индекс целевого значения в массиве или -1, если целевое значение не существует.

main 方法中,我们创建了一个有序数组 array,并定义目标值 target 为 5。然后调用 binarySearch 方法进行查找,并输出结果。

2 分治算法

分治算法(Divide and Conquer)是一种将问题划分为更小的子问题,逐个解决子问题,然后将子问题的解合并为原问题解的算法思想。

以下是分治算法的实现步骤:

  1. 划分问题:将原问题划分为更小的子问题。这通常涉及将问题分成两个或更多的子问题。
  2. 解决子问题:递归地解决划分得到的子问题。如果子问题足够小,可以直接求解。
  3. 合并子问题的解:将子问题的解合并为原问题的解。这是分治算法的关键步骤。

下面是一个使用分治算法解决数组中最大值问题的示例代码:

public class DivideAndConquer {
    public static int findMax(int[] array, int start, int end) {
        if (start == end) {
            return array[start]; // 只有一个元素,直接返回
        } else {
            int mid = (start + end) / 2;

            // 递归地解决左右两个子问题
            int leftMax = findMax(array, start, mid);
            int rightMax = findMax(array, mid + 1, end);

            // 合并子问题的解
            return Math.max(leftMax, rightMax);
        }
    }

    public static void main(String[] args) {
        int[] array = {7, 2, 9, 1, 5};
        int max = findMax(array, 0, array.length - 1);
        System.out.println("数组中的最大值为: " + max);
    }
}

运行结果:

数组中的最大值为: 9

在上述示例中,我们定义了 findMax 方法,接受一个数组 array、起始索引 start 和结束索引 end 作为参数。该方法使用分治算法递归地解决子问题,并返回数组中的最大值。

main 方法中,我们创建了一个数组 array,并调用 findMax 方法找到数组中的最大值,并输出结果。

3 动态规划算法

动态规划(Dynamic Programming)是一种通过将问题划分为重叠子问题并使用记忆化技术来加速计算的算法思想。它适用于具有重叠子问题和最优子结构性质的问题。

以下是动态规划算法的实现步骤:

  1. 定义状态:确定问题的状态,并定义状态表示。状态是问题的关键信息,它们的组合构成了问题的解空间。
  2. 定义状态转移方程:根据问题的最优子结构性质,定义问题的状态转移方程。状态转移方程描述了状态之间的关系,用于计算当前状态的值。
  3. 初始化:确定初始状态的值。初始状态是问题解空间中的边界条件,它们是解决问题的基础。
  4. 递推计算:按照状态转移方程进行递推计算,计算出所有状态的值。
  5. 解的表示:根据问题的要求,确定如何表示问题的解。

下面是一个使用动态规划解决斐波那契数列问题的示例代码:

public class DynamicProgramming {
    public static int fibonacci(int n) {
        if (n <= 1) {
            return n; // 基本情况
        }

        int[] dp = new int[n + 1];
        dp[0] = 0;
        dp[1] = 1;

        for (int i = 2; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2]; // 状态转移方程
        }

        return dp[n]; // 返回最终结果
    }

    public static void main(String[] args) {
        int n = 6;
        int result = fibonacci(n);
        System.out.println("斐波那契数列第 " + n + " 项为: " + result);
    }
}

运行结果:

斐波那契数列第 6 项为: 8

在上述示例中,我们定义了 fibonacci 方法,接受一个整数 n 作为参数,计算斐波那契数列的第 n 项。该方法使用动态规划算法,使用一个数组 dp 存储中间状态值。通过递推计算和状态转移方程 dp[i] = dp[i - 1] + dp[i - 2],得到最终结果。

main 方法中,我们调用 fibonacci 方法计算斐波那契数列的第 6 项,并输出结果。

4 KMP算法

KMP算法(Knuth-Morris-Pratt Algorithm)是一种字符串匹配算法,用于在一个文本串中查找一个模式串的出现位置。它通过利用已经匹配过的部分信息,避免不必要的回溯,从而提高匹配的效率。

以下是KMP算法的实现步骤:

  1. 构建部分匹配表(Partial Match Table,PMT):对于模式串,构建一个部分匹配表,用于记录模式串中每个位置的最长公共前后缀长度。
  2. 根据部分匹配表进行匹配:在文本串中查找模式串,利用部分匹配表进行匹配过程,避免不必要的回溯。

下面是一个使用KMP算法进行字符串匹配的示例代码:

public class KMPAlgorithm {
    public static int kmpSearch(String text, String pattern) {
        int[] pmt = buildPMT(pattern);

        int i = 0; // 文本串指针
        int j = 0; // 模式串指针

        while (i < text.length() && j < pattern.length()) {
            if (text.charAt(i) == pattern.charAt(j)) {
                i++;
                j++;
            } else if (j > 0) {
                j = pmt[j - 1]; // 根据部分匹配表回溯
            } else {
                i++;
            }
        }

        if (j == pattern.length()) {
            return i - j; // 返回模式串在文本串中的起始位置
        } else {
            return -1; // 未找到匹配
        }
    }

    private static int[] buildPMT(String pattern) {
        int[] pmt = new int[pattern.length()];

        int i = 0;
        int j = 1;

        while (j < pattern.length()) {
            if (pattern.charAt(i) == pattern.charAt(j)) {
                pmt[j] = i + 1;
                i++;
                j++;
            } else if (i > 0) {
                i = pmt[i - 1]; // 根据部分匹配表回溯
            } else {
                pmt[j] = 0;
                j++;
            }
        }

        return pmt;
    }

    public static void main(String[] args) {
        String text = "ABCABDABACDABABCABDE";
        String pattern = "ABABCABDE";
        int index = kmpSearch(text, pattern);
        
        if (index != -1) {
            System.out.println("模式串在文本串中的起始位置:" + index);
        } else {
            System.out.println("未找到匹配");
        }
    }
}

运行结果:

模式串在文本串中的起始位置:10

在上述示例中,我们定义了 kmpSearch 方法,接受一个文本串 text 和一个模式串 pattern 作为参数,利用KMP算法在文本串中查找模式串的起始位置。

kmpSearch 方法中,我们首先调用 buildPMT 方法构建部分匹配表。然后,使用两个指针 ij 分别指向文本串和模式串,通过比较字符进行匹配。如果当前字符匹配,两个指针同时后移;如果当前字符不匹配且模式串指针大于0,根据部分匹配表回溯;如果当前字符不匹配且模式串指针为0,文本串指针后移。最终返回模式串在文本串中的起始位置。

main 方法中,我们定义了一个文本串 text 和一个模式串 pattern,调用 kmpSearch 方法进行匹配,并输出结果。

5 贪心算法

贪心算法(Greedy Algorithm)是一种在每一步选择中都选择当前最优解的算法思想。它通过贪心的选择方式,希望最终得到全局最优解。然而,贪心算法不能保证一定能得到最优解,因此在使用贪心算法时需要注意问题的性质和是否适用贪心策略。

以下是贪心算法的实现步骤:

  1. 确定问题的贪心选择方式:根据问题的性质,确定每一步的贪心选择方式,即当前最优解。
  2. 构造问题的解:通过贪心选择方式,逐步构造问题的解。
  3. 检查解的有效性:检查构造的解是否满足问题的约束条件和要求。
  4. 更新问题的状态:根据问题的性质,更新问题的状态,继续下一步的贪心选择。

下面是一个使用贪心算法解决找零钱问题的示例代码:

import java.util.Arrays;

public class GreedyAlgorithm {
    public static int[] findMinCoins(int[] coins, int amount) {
        Arrays.sort(coins); // 将零钱面额按升序排序
        int[] result = new int[coins.length];

        for (int i = coins.length - 1; i >= 0; i--) {
            if (amount >= coins[i]) {
                result[i] = amount / coins[i]; // 计算当前面额的零钱数量
                amount %= coins[i]; // 更新剩余金额
            }
        }

        return result;
    }

    public static void main(String[] args) {
        int[] coins = {1, 2, 5, 10, 20, 50};
        int amount = 123;
        int[] minCoins = findMinCoins(coins, amount);

        System.out.println("找零 " + amount + " 元的最少硬币数量为:");
        for (int i = coins.length - 1; i >= 0; i--) {
            if (minCoins[i] > 0) {
                System.out.println(coins[i] + " 元硬币:" + minCoins[i] + " 枚");
            }
        }
    }
}

运行结果:

找零 123 元的最少硬币数量为:
50 元硬币:2 枚
20 元硬币:1 枚
2 元硬币:1 枚
1 元硬币:1 枚

在上述示例中,我们定义了 findMinCoins 方法,接受一个零钱面额数组 coins 和一个金额 amount 作为参数,使用贪心算法找零。首先对零钱面额进行升序排序,然后从最大面额开始,计算当前面额的零钱数量,并更新剩余金额。最后返回每个面额的零钱数量。

main 方法中,我们创建了一个零钱面额数组 coins 和一个金额 amount,调用 findMinCoins 方法进行找零,并输出结果。

6 普里姆算法

普里姆算法(Prim’s Algorithm)是一种用于求解最小生成树的算法。给定一个连通加权无向图,普里姆算法通过逐步选择边,将顶点逐渐加入生成树中,直到生成树包含图中的所有顶点。

以下是普里姆算法的实现步骤:

  1. 初始化:选择一个起始顶点作为生成树的根节点,将其加入生成树集合,并将其所有邻接边加入候选边集合。
  2. 选择边:从候选边集合中选择一条最小权重的边,将其加入生成树集合。
  3. 更新候选边:将新加入的顶点的所有邻接边中,权重小于已有边的边加入候选边集合。
  4. 重复步骤2和步骤3,直到生成树包含图中的所有顶点。

下面是一个使用普里姆算法构建最小生成树的示例代码:

import java.util.*;

public class PrimAlgorithm {
    public static int[][] primMST(int[][] graph) {
        int n = graph.length; // 图的顶点数量
        boolean[] visited = new boolean[n]; // 记录顶点是否已被访问
        int[][] mst = new int[n][n]; // 存储最小生成树的邻接矩阵

        for (int i = 0; i < n; i++) {
            Arrays.fill(mst[i], Integer.MAX_VALUE);
        }

        int start = 0; // 起始顶点
        visited[start] = true;

        PriorityQueue<Edge> pq = new PriorityQueue<>(); // 候选边集合
        addEdges(graph, pq, start);

        while (!pq.isEmpty()) {
            Edge edge = pq.poll();
            int u = edge.u;
            int v = edge.v;
            int weight = edge.weight;

            if (visited[v]) {
                continue; // 跳过已访问的顶点
            }

            visited[v] = true;
            mst[u][v] = weight;
            mst[v][u] = weight;

            addEdges(graph, pq, v);
        }

        return mst;
    }

    private static void addEdges(int[][] graph, PriorityQueue<Edge> pq, int v) {
        for (int u = 0; u < graph.length; u++) {
            int weight = graph[u][v];
            if (weight > 0) {
                pq.offer(new Edge(u, v, weight));
            }
        }
    }

    public static void main(String[] args) {
        int[][] graph = {
            {0, 2, 0, 6, 0},
            {2, 0, 3, 8, 5},
            {0, 3, 0, 0, 7},
            {6, 8, 0, 0, 9},
            {0, 5, 7, 9, 0}
        };

        int[][] mst = primMST(graph);

        System.out.println("最小生成树的邻接矩阵为:");
        for (int i = 0; i < mst.length; i++) {
            for (int j = 0; j < mst[i].length; j++) {
                System.out.print(mst[i][j] + " ");
            }
            System.out.println();
        }
    }
}

class Edge implements Comparable<Edge> {
    int u; // 边的一个顶点
    int v; // 边的另一个顶点
    int weight; // 边的权重

    public Edge(int u, int v, int weight) {
        this.u = u;
        this.v = v;
        this.weight = weight;
    }

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

运行结果:

最小生成树的邻接矩阵为:
0 2 0 6 0 
2 0 3 0 5 
0 3 0 0 7 
6 0 0 0 9 
0 5 7 9 0

在上述示例中,我们定义了 primMST 方法,接受一个邻接矩阵表示的图作为参数,使用普里姆算法构建最小生成树。

primMST 方法中,我们使用一个布尔数组 visited 记录顶点是否已被访问,一个二维数组 mst 存储最小生成树的邻接矩阵。通过使用优先队列 pq 来存储候选边集合。我们选择起始顶点,并将其标记为已访问。然后,将起始顶点的所有邻接边加入候选边集合。在每一步迭代中,从候选边集合中选择一条权重最小的边,将其加入最小生成树,并将新加入顶点的邻接边加入候选边集合。重复此过程,直到最小生成树包含图中的所有顶点。

main 方法中,我们定义了一个邻接矩阵 graph,调用 primMST 方法构建最小生成树,并输出结果。

需要注意的是,上述示例中的 Edge 类用于表示边的信息,并实现了 Comparable 接口,以便优先队列能够按照边的权重进行排序。

7 克鲁斯卡尔算法

克鲁斯卡尔算法(Kruskal’s Algorithm)是一种用于求解最小生成树的算法。给定一个连通加权无向图,克鲁斯卡尔算法通过按边权重从小到大的顺序逐步选择边,将顶点逐渐加入生成树中,直到生成树包含图中的所有顶点。

以下是克鲁斯卡尔算法的实现步骤:

  1. 初始化:将每个顶点看作一个独立的集合。
  2. 排序边:按边的权重从小到大进行排序。
  3. 选择边:从排序后的边集合中选择一条边。
  4. 检查环路:检查选择的边是否会产生环路,如果不会,则将该边加入最小生成树。
  5. 重复步骤3和步骤4,直到生成树包含图中的所有顶点。

下面是一个使用克鲁斯卡尔算法构建最小生成树的示例代码:

import java.util.*;

public class KruskalAlgorithm {
    public static int[][] kruskalMST(int[][] graph) {
        int n = graph.length; // 图的顶点数量
        PriorityQueue<Edge> pq = new PriorityQueue<>(); // 存储边的优先队列

        // 将图中的边加入优先队列
        for (int u = 0; u < n; u++) {
            for (int v = u + 1; v < n; v++) {
                int weight = graph[u][v];
                if (weight > 0) {
                    pq.offer(new Edge(u, v, weight));
                }
            }
        }

        DisjointSet ds = new DisjointSet(n); // 并查集,用于检查环路
        int[][] mst = new int[n][n]; // 存储最小生成树的邻接矩阵

        while (!pq.isEmpty()) {
            Edge edge = pq.poll();
            int u = edge.u;
            int v = edge.v;
            int weight = edge.weight;

            if (ds.find(u) != ds.find(v)) {
                ds.union(u, v); // 合并两个集合
                mst[u][v] = weight;
                mst[v][u] = weight;
            }
        }

        return mst;
    }

    public static void main(String[] args) {
        int[][] graph = {
            {0, 2, 0, 6, 0},
            {2, 0, 3, 8, 5},
            {0, 3, 0, 0, 7},
            {6, 8, 0, 0, 9},
            {0, 5, 7, 9, 0}
        };

        int[][] mst = kruskalMST(graph);

        System.out.println("最小生成树的邻接矩阵为:");
        for (int i = 0; i < mst.length; i++) {
            for (int j = 0; j < mst[i].length; j++) {
                System.out.print(mst[i][j] + " ");
            }
            System.out.println();
        }
    }
}

class Edge implements Comparable<Edge> {
    int u; // 边的一个顶点
    int v; // 边的另一个顶点
    int weight; // 边的权重

    public Edge(int u, int v, int weight) {
        this.u = u;
        this.v = v;
        this.weight = weight;
    }

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

class DisjointSet {
    int[] parent;

    public DisjointSet(int size) {
        parent = new int[size];
        Arrays.fill(parent, -1);
    }

    public int find(int x) {
        if (parent[x] < 0) {
            return x;
        } else {
            parent[x] = find(parent[x]); // 路径压缩
            return parent[x];
        }
    }

    public void union(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);

        if (parent[rootX] <= parent[rootY]) {
            parent[rootX] += parent[rootY];
            parent[rootY] = rootX;
        } else {
            parent[rootY] += parent[rootX];
            parent[rootX] = rootY;
        }
    }
}

运行结果:

最小生成树的邻接矩阵为:
0 2 0 0 0 
2 0 3 0 5 
0 3 0 0 7 
0 0 0 0 9 
0 5 7 9 0

在上述示例中,我们定义了 kruskalMST 方法,接受一个邻接矩阵表示的图作为参数,使用克鲁斯卡尔算法构建最小生成树。

kruskalMST 方法中,我们使用一个优先队列 pq 存储边,按照边的权重从小到大进行排序。然后,使用并查集 ds 来检查边是否会产生环路。我们遍历优先队列中的边,如果边的两个顶点不在同一个集合中,则将它们合并,并将边加入最小生成树。最终,返回最小生成树的邻接矩阵。

main 方法中,我们定义了一个邻接矩阵 graph,调用 kruskalMST 方法构建最小生成树,并输出结果。

需要注意的是,上述示例中的 Edge 类用于表示边的信息,并实现了 Comparable 接口,以便优先队列能够按照边的权重进行排序。DisjointSet 类实现了并查集数据结构,用于检查边是否会产生环路。

8 迪杰斯特拉算法

迪杰斯特拉算法(Dijkstra’s Algorithm)是一种用于求解单源最短路径的算法。给定一个加权有向图和起始顶点,迪杰斯特拉算法可以找到从起始顶点到其他所有顶点的最短路径。

以下是迪杰斯特拉算法的实现步骤:

  1. 初始化距离:将起始顶点的距离设为0,其他顶点的距离设为无穷大。

  2. 创建一个优先队列,用于存储顶点及其距离,起始时将起始顶点加入队列。

  3. 循环执行以下步骤,直到优先队列为空:

    • 从优先队列中取出距离最小的顶点。
    • 遍历该顶点的所有邻接顶点,计算通过当前顶点到达邻接顶点的距离,如果该距离小于邻接顶点的当前距离,则更新邻接顶点的距离。
    • 如果邻接顶点不在优先队列中,则将其加入队列。
  4. 执行完循环后,所有顶点的最短路径已经计算完成。

下面是一个使用迪杰斯特拉算法求解最短路径的示例代码:

import java.util.*;

public class DijkstraAlgorithm {
    public static int[] dijkstra(int[][] graph, int source) {
        int n = graph.length; // 图的顶点数量
        int[] dist = new int[n]; // 存储起始顶点到其他顶点的最短距离
        Arrays.fill(dist, Integer.MAX_VALUE); // 初始化距离为无穷大

        dist[source] = 0; // 起始顶点到自身的距离为0

        PriorityQueue<Vertex> pq = new PriorityQueue<>(); // 优先队列,按距离从小到大排序
        pq.offer(new Vertex(source, 0));

        while (!pq.isEmpty()) {
            Vertex vertex = pq.poll();
            int u = vertex.index;

            for (int v = 0; v < n; v++) {
                int weight = graph[u][v];
                if (weight > 0) {
                    int newDist = dist[u] + weight;
                    if (newDist < dist[v]) {
                        dist[v] = newDist;
                        pq.offer(new Vertex(v, newDist));
                    }
                }
            }
        }

        return dist;
    }

    public static void main(String[] args) {
        int[][] graph = {
            {0, 4, 0, 0, 0, 0, 0, 8, 0},
            {4, 0, 8, 0, 0, 0, 0, 11, 0},
            {0, 8, 0, 7, 0, 4, 0, 0, 2},
            {0, 0, 7, 0, 9, 14, 0, 0, 0},
            {0, 0, 0, 9, 0, 10, 0, 0, 0},
            {0, 0, 4, 14, 10, 0, 2, 0, 0},
            {0, 0, 0, 0, 0, 2, 0, 1, 6},
            {8, 11, 0, 0, 0, 0, 1, 0, 7},
            {0, 0, 2, 0, 0, 0, 6, 7, 0}
        };

        int source = 0; // 起始顶点
        int[] shortestPaths = dijkstra(graph, source);

        System.out.println("从顶点 " + source + " 到其他顶点的最短距离为:");
        for (int i = 0; i < shortestPaths.length; i++) {
            System.out.println("到顶点 " + i + " 的距离为: " + shortestPaths[i]);
        }
    }
}

class Vertex implements Comparable<Vertex> {
    int index; // 顶点索引
    int distance; // 距离

    public Vertex(int index, int distance) {
        this.index = index;
        this.distance = distance;
    }

    @Override
    public int compareTo(Vertex other) {
        return Integer.compare(this.distance, other.distance);
    }
}

运行结果:

从顶点 0 到其他顶点的最短距离为:
到顶点 0 的距离为: 0
到顶点 1 的距离为: 4
到顶点 2 的距离为: 12
到顶点 3 的距离为: 19
到顶点 4 的距离为: 21
到顶点 5 的距离为: 11
到顶点 6 的距离为: 9
到顶点 7 的距离为: 8
到顶点 8 的距离为: 14

在上述示例中,我们定义了 dijkstra 方法,接受一个邻接矩阵表示的图和起始顶点作为参数,使用迪杰斯特拉算法计算最短路径。

dijkstra 方法中,我们首先初始化距离数组 dist,将起始顶点到其他顶点的距离设为无穷大。然后,创建一个优先队列 pq,用于存储顶点及其距离。我们将起始顶点加入优先队列,并将其距离设为0。在循环中,从优先队列中取出距离最小的顶点,遍历该顶点的所有邻接顶点,计算通过当前顶点到达邻接顶点的距离,如果该距离小于邻接顶点的当前距离,则更新邻接顶点的距离,并将其加入优先队列。最终,返回最短距离数组。

main 方法中,我们定义了一个邻接矩阵 graph 和一个起始顶点 source,调用 dijkstra 方法计算最短路径,并输出结果。

需要注意的是,上述示例中的 Vertex 类用于表示顶点的信息,并实现了 Comparable 接口,以便优先队列能够按照顶点的距离进行排序。

9 弗洛伊德算法

弗洛伊德算法(Floyd-Warshall Algorithm)是一种用于求解所有顶点对之间最短路径的算法。给定一个加权有向图,弗洛伊德算法可以计算出图中任意两个顶点之间的最短路径及其距离。

以下是弗洛伊德算法的实现步骤:

  1. 初始化距离矩阵:创建一个二维数组 dist,用于存储任意两个顶点之间的最短路径距离。如果两个顶点之间存在边,则将其距离存入 dist,否则将其距离设为无穷大。
  2. 三重循环更新距离:使用三重循环遍历所有顶点对 (i, j, k),其中 k 是中间顶点的索引。对于每一对 (i, j),比较通过中间顶点 k 的路径距离和直接路径距离,如果通过中间顶点 k 的路径距离更短,则更新 dist[i][j] 的值。
  3. 循环结束后,dist 数组中存储了任意两个顶点之间的最短路径距离。

下面是一个使用弗洛伊德算法求解最短路径的示例代码:

import java.util.Arrays;

public class FloydWarshallAlgorithm {
    public static int[][] floydWarshall(int[][] graph) {
        int n = graph.length; // 图的顶点数量
        int[][] dist = new int[n][n]; // 存储最短路径距离

        // 初始化距离矩阵
        for (int i = 0; i < n; i++) {
            System.arraycopy(graph[i], 0, dist[i], 0, n);
        }

        // 三重循环更新距离
        for (int k = 0; k < n; k++) {
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    if (dist[i][k] != Integer.MAX_VALUE && dist[k][j] != Integer.MAX_VALUE) {
                        dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]);
                    }
                }
            }
        }

        return dist;
    }

    public static void main(String[] args) {
        int[][] graph = {
            {0, 4, 6, Integer.MAX_VALUE, Integer.MAX_VALUE},
            {4, 0, 3, 7, Integer.MAX_VALUE},
            {6, 3, 0, 8, 2},
            {Integer.MAX_VALUE, 7, 8, 0, 5},
            {Integer.MAX_VALUE, Integer.MAX_VALUE, 2, 5, 0}
        };

        int[][] shortestPaths = floydWarshall(graph);

        System.out.println("任意两个顶点之间的最短路径距离为:");
        for (int i = 0; i < shortestPaths.length; i++) {
            for (int j = 0; j < shortestPaths[i].length; j++) {
                System.out.print(shortestPaths[i][j] + " ");
            }
            System.out.println();
        }
    }
}

运行结果:

任意两个顶点之间的最短路径距离为:
0 4 6 10 8 
4 0 3 7 9 
6 3 0 8 2 
12 7 5 0 5 
8 11 2 5 0 

在上述示例中,我们定义了 floydWarshall 方法,接受一个邻接矩阵表示的图作为参数,使用弗洛伊德算法计算任意两个顶点之间的最短路径。

floydWarshall 方法中,我们首先初始化距离矩阵 dist,将其赋值为图中的距离数组。然后,使用三重循环遍历所有顶点对 (i, j, k),并根据中间顶点 k 更新顶点对 (i, j) 的最短路径距离。最终,返回 dist 数组,其中存储了任意两个顶点之间的最短路径距离。

main 方法中,我们定义了一个邻接矩阵 graph,调用 floydWarshall 方法计算最短路径,并输出结果。

需要注意的是,上述示例中使用了 Integer.MAX_VALUE 表示两个顶点之间不存在直接边的情况。

10 马踏棋盘算法

马踏棋盘算法(Knight’s Tour Algorithm)是一种用于解决马在棋盘上走遍所有格子的问题的算法。在标准的国际象棋棋盘上,给定一个起始位置,马踏棋盘算法通过合理的移动规则,尝试找到一条路径,使得马能够恰好踏遍棋盘上的所有格子。

以下是马踏棋盘算法的实现步骤:

  1. 创建棋盘:创建一个二维数组来表示棋盘,初始化所有格子为未访问状态。
  2. 设定起始位置:选择一个起始位置,将其标记为已访问。
  3. Рекурсивный возврат: начиная с исходного положения, двигайтесь в соответствии с правилами лошади и рекурсивно пробуйте все возможные пути движения, пока не будет найден полный путь или движение невозможно.
  4. Правило движения: Правило движения лошади на шахматной доске основано на текущей позиции и перемещается в соответствии с фиксированными относительными координатами. Лошадь может двигаться в 8 направлениях, включая вверх, вниз, влево, вправо, по диагонали вверх и по диагонали вниз.
  5. Оценка границы и статуса доступа: на каждом шаге хода необходимо определить, находится ли следующая позиция лошади в пределах досягаемости шахматной доски и не была ли посещена.
  6. Отметьте путь и возврат: каждый раз, когда вы двигаетесь, отмечайте текущую позицию как посещенную и рекурсивно пробуйте следующий ход. Если полный путь найден, алгоритм завершает работу; если невозможно перейти к следующей позиции, он возвращается к предыдущему шагу, снимает отметку с текущей позиции и пробует другие возможные пути перемещения.

Ниже приведен пример кода для решения задачи с использованием алгоритма шахматной доски шагающего коня:

public class KnightsTourAlgorithm {
    private static final int BOARD_SIZE = 8; // 棋盘大小
    private static final int[] ROW_OFFSETS = {-2, -1, 1, 2, 2, 1, -1, -2}; // 行的相对偏移量
    private static final int[] COL_OFFSETS = {1, 2, 2, 1, -1, -2, -2, -1}; // 列的相对偏移量

    public static void knightsTour(int[][] board, int row, int col, int move) {
        board[row][col] = move; // 标记当前位置为已访问

        if (move == BOARD_SIZE * BOARD_SIZE) {
            printBoard(board); // 找到一条完整路径,打印棋盘
        } else {
            for (int i = 0; i < 8; i++) {
                int nextRow = row + ROW_OFFSETS[i];
                int nextCol = col + COL_OFFSETS[i];

                if (isValidMove(board, nextRow, nextCol)) {
                    knightsTour(board, nextRow, nextCol, move + 1);
                }
            }
        }

        board[row][col] = 0; // 取消当前位置的标记,回溯
    }

    public static boolean isValidMove(int[][] board, int row, int col) {
        return row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE && board[row][col] == 0;
    }

    public static void printBoard(int[][] board) {
        for (int[] row : board) {
            for (int cell : row) {
                System.out.printf("%2d ", cell);
            }
            System.out.println();
        }
        System.out.println();
    }

    public static void main(String[] args) {
        int[][] board = new int[BOARD_SIZE][BOARD_SIZE];

        int startRow = 0; // 起始位置的行坐标
        int startCol = 0; // 起始位置的列坐标
        int move = 1; // 当前移动步数

        knightsTour(board, startRow, startCol, move);
    }
}

Все полные пути будут распечатаны в рабочем результате. Поскольку существует несколько решений задачи о шаге лошади на шахматной доске, существует много возможностей для вывода результатов. В приведенном выше примере мы определили knightsTourметод для реализации алгоритма шахматной доски шагающего коня.

В knightsTourметоде мы сначала помечаем текущее местоположение как посещенное и определяем, был ли найден полный путь. Если полный путь не найден, мы перебираем 8 возможных направлений движения и рекурсивно пробуем каждое из них. Если путь перемещения может продолжать рекурсию вниз, рекурсивный вызов продолжается. Если невозможно перейти к следующей позиции, вернитесь к предыдущему шагу, снимите отметку с текущей позиции и попробуйте другие возможные пути движения.

В mainметоде мы создаем массив шахматных досок board, выбираем начальную позицию, а затем вызываем knightsTourметод, чтобы начать поиск решения конной шахматной доски.

おすすめ

転載: juejin.im/post/7255590272556335163