Java中使用Stream實現6種演算法教程

banq發表於2024-05-08

在演算法問題解決領域,效率和優雅常常齊頭並進。 Java 作為最廣泛使用的程式語言之一,提供了各種工具和庫來應對此類挑戰。 Java 8 中引入的 Stream API 就是這樣一個強大的工具,它提供了一種處理元素集合的功能方法。

1、Java Stream API:矩陣乘法
矩陣乘法是線性代數中的基本運算,在計算機圖形學、物理模擬和機器學習等各個領域都有廣泛的應用。在 Java 中,高效執行矩陣乘法對於最佳化處理大型資料集的應用程式的效能至關重要。幸運的是,隨著 Java 8 中 Stream API 的引入,可以簡化矩陣乘法的過程,使程式碼簡潔,並且具有潛在的可擴充套件性。

在深入研究實現細節之前,我們先簡單回顧一下矩陣乘法的過程。給定兩個矩陣 A 和 B,其中 A 的維度為 mxn,B 的維度為 nxp,所得矩陣 C 的維度為 mx p。

結果矩陣 C 的第 (i, j) 個元素計算為矩陣 A 第 i 行和矩陣 B 第 j 列的點積:

C[j] = A[0] * B[0][j] + A[1] * B[1][j] + ... + A[n -1] * B[n-1][j]

矩陣乘法的傳統方法:
傳統的矩陣乘法方法涉及巢狀迴圈來迭代矩陣的元素並執行必要的計算。以下是該演算法的基本概要:

public static int[][] multiplyMatrices(int[][] A, int[][] B) {
    int m = A.length;
    int n = A[0].length;
    int p = B[0].length;
    int[][] C = new int[m][p];

    for (int i = 0; i < m; i++) {
        for (int j = 0; j < p; j++) {
            int sum = 0;
            for (int k = 0; k < n; k++) {
                sum += A[i][k] * B[k][j];
            }
            C[i][j] = sum;
        }
    }
    return C;
}

雖然這種方法效果很好,但它的表達能力不是特別強,而且可能很冗長,尤其是對於較大的矩陣。

使用 Stream API 進行矩陣乘法:
隨著 Java 8 中 Stream API 的引入,我們可以利用其函數語言程式設計特性來簡化矩陣乘法過程。我們可以利用流來平行計算並更簡潔地表達乘法邏輯。

以下是我們如何使用 Stream API 實現矩陣乘法:

import java.util.Arrays;

public static int[][] multiplyMatrices(int[][] A, int[][] B) {
    int m = A.length;
    int n = A[0].length;
    int p = B[0].length;

    return Arrays.stream(A)
            .parallel()
            .map(row -> Arrays.stream(transpose(B))
                    .mapToInt(col -> dotProduct(row, col))
                    .toArray())
            .toArray(int[][]::new);
}

private static int[][] transpose(int[][] matrix) {
    int m = matrix.length;
    int n = matrix[0].length;
    int[][] transposed = new int[n][m];
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            transposed[j][i] = matrix[i][j];
        }
    }
    return transposed;
}

private static int dotProduct(int[] a, int[] b) {
    return IntStream.range(0, a.length)
            .map(i -> a[i] * b[i])
            .sum();
}

在此實現中,我們使用流來平行計算結果矩陣的每一行。我們將矩陣 A 的每一行與轉置矩陣 B 的列對映到點積流。最後,我們將結果收集到表示結果矩陣 C 的二維陣列中。


2、Java Stream API:查詢具有最大總和的子陣列
利用 Java Stream API 的強大功能:查詢總和最大的子陣列.

在深入研究解決方案之前,讓我們首先了解當前的問題。給定一個整數陣列,我們的目標是找到總和最大的連續子陣列(連續元素的子序列)。這個問題也稱為“最大子陣列問題”,有多種解決方案,包括暴力破解、動態規劃和分而治之演算法。在這裡,我們將重點使用 Java Stream API 來解決這個問題,它提供了一種簡潔而富有表現力的方法來操作集合。

方法
使用 Stream API 解決此問題的關鍵思想是將陣列分解為所有可能的子陣列,計算它們的總和,然後找到其中的最大總和。這可以透過使用諸如map、reduce和max之類的流操作來實現。

執行
讓我們深入瞭解一下實現:

import java.util.Arrays;

public class MaximumSubarrayUsingStream {

    public static void main(String[] args) {
        int[] array = { -2, 1, -3, 4, -1, 2, 1, -5, 4 }; <font>// Example array<i>
        int[] maxSubarray = findMaxSubarray(array);
        System.out.println(
"Maximum subarray: " + Arrays.toString(maxSubarray));
    }

    public static int[] findMaxSubarray(int[] array) {
        return Arrays.stream(array)
                .mapToObj(start -> Arrays.stream(array)
                        .skip(start)
                        .mapToObj(end -> Arrays.copyOfRange(array, start, end + 1)))
                .flatMap(subArrays -> subArrays)
                .max((arr1, arr2) -> Arrays.stream(arr1).sum() - Arrays.stream(arr2).sum())
                .orElse(new int[0]);
    }
}


解釋

  • - 我們首先使用 Arrays.stream(array) 迭代陣列中的每個元素。
  • - 對於每個元素,我們使用 mapToObj 生成從該元素開始的子陣列流。
  • - 在巢狀流中,我們使用skip(start)跳過當前元素之前的元素,並生成以每個後續元素結尾的子陣列。
  • - 使用 flatMap 將生成的子陣列展平為單個流。
  • - 最後,我們透過使用 max 比較每個子陣列的和來找到最大子陣列。


3、Java Stream API:查詢最長公共子序列
使用 Java Stream API 查詢兩個字串之間的最長公共子序列

給定兩個字串,我們需要找到最長公共子序列的長度和實際子序列本身。例如,對於字串“ABCBDAB”和“BDCAB”,最長公共子序列是“BCAB”,長度為4。

方法:
我們將使用動態規劃方法來解決這個問題。我們將建立一個二維陣列 dp[][],其中 dp[j] 將儲存第一個字串的前 i 個字元和第二個字串的前 j 個字元之間的最長公共子序列的長度。

然後我們將迭代兩個字串的字元。如果字元相等,我們將最長公共子序列的長度加1。否則,我們將取前一個字元的最長公共子序列長度的最大值。

最後,我們將透過dp[][]陣列回溯構造最長公共子序列。

使用 Stream API 的 Java 程式碼:

import java.util.stream.IntStream;

public class LongestCommonSubsequence {

    public static String findLCS(String s1, String s2) {
        int m = s1.length();
        int n = s2.length();

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

        IntStream.rangeClosed(1, m).forEach(i ->
                IntStream.rangeClosed(1, n).forEach(j -> {
                    if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                        dp[i][j] = dp[i - 1][j - 1] + 1;
                    } else {
                        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                    }
                }));

        StringBuilder lcs = new StringBuilder();
        int i = m, j = n;
        while (i > 0 && j > 0) {
            if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                lcs.insert(0, s1.charAt(i - 1));
                i--;
                j--;
            } else if (dp[i - 1][j] > dp[i][j - 1]) {
                i--;
            } else {
                j--;
            }
        }

        return lcs.toString();
    }

    public static void main(String[] args) {
        String s1 = <font>"ABCBDAB";
        String s2 =
"BDCAB";

        System.out.println(
"Longest Common Subsequence: " + findLCS(s1, s2));
    }
}


解釋:

  • - 我們首先建立一個二維陣列 dp[][] 來儲存最長公共子序列的長度。
  • - 然後我們使用巢狀的 IntStream 迭代兩個字串的每個字元並計算最長公共子序列的長度。
  • - 最後,我們回溯 dp[][] 陣列來構造最長公共子序列。

4、Java Stream API:實現 Prim 演算法
Prim 演算法是一種貪心演算法,用於為帶權無向圖找到最小生成樹。它從任意節點開始,並透過新增將樹連線到新節點的最便宜的邊來增長生成樹。重複此過程,直到所有節點都包含在樹中。

在深入實現之前,我們先簡單回顧一下 Prim 演算法所涉及的步驟:
1. 選擇任意一個節點作為起點。
2. 初始化一個優先順序佇列來儲存邊,鍵為邊的權重。
3. 雖然仍有節點需要包含在樹中:
   A。找到將樹中的節點連線到樹外的節點的權重最小的邊。
   b.將此邊新增到樹中。
   C。將新新增的節點標記為包含在樹中。


實施 Prim 演算法
為了實現 Prim 演算法,我們將圖表示為邊列表,其中每條邊都是一個包含源節點、目標節點和邊權重的元組。我們將使用 Set 來跟蹤樹中包含的節點,並使用 PriorityQueue 來儲存按權重排序的邊。

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

class Edge {
    int src, dest, weight;

    Edge(int src, int dest, int weight) {
        this.src = src;
        this.dest = dest;
        this.weight = weight;
    }
}

public class PrimAlgorithm {

    public static List<Edge> primMST(List<Edge> edges, int numVertices) {
        Set<Integer> visited = new HashSet<>();
        PriorityQueue<Edge> pq = new PriorityQueue<>(Comparator.comparingInt(e -> e.weight));
        List<Edge> mst = new ArrayList<>();

        <font>// Start from the first node<i>
        int startNode = 0;
        visited.add(startNode);

       
// Add all edges from the starting node to the priority queue<i>
        pq.addAll(edges.stream()
                       .filter(e -> e.src == startNode || e.dest == startNode)
                       .collect(Collectors.toList()));

        while (!pq.isEmpty() && visited.size() < numVertices) {
            Edge minEdge = pq.poll();
            int nextNode = visited.contains(minEdge.src) ? minEdge.dest : minEdge.src;

            if (!visited.contains(nextNode)) {
                visited.add(nextNode);
                mst.add(minEdge);

                pq.addAll(edges.stream()
                               .filter(e -> e.src == nextNode || e.dest == nextNode)
                               .collect(Collectors.toList()));
            }
        }

        return mst;
    }

    public static void main(String[] args) {
        List<Edge> edges = Arrays.asList(
                new Edge(0, 1, 2),
                new Edge(0, 2, 3),
                new Edge(1, 2, 1),
                new Edge(1, 3, 4),
                new Edge(2, 4, 5),
                new Edge(3, 4, 6)
        );

        List<Edge> mst = primMST(edges, 5);

        System.out.println(
"Minimum Spanning Tree:");
        for (Edge edge : mst) {
            System.out.println(edge.src +
" - " + edge.dest + ": " + edge.weight);
        }
    }
}


在這個實現中,我們從第一個節點開始,重複新增連線樹中節點和樹外節點的最小權重邊,直到所有節點都包含在樹中。 PriorityQueue 確保我們始終有效地選擇最小權重邊。


5、Java Stream API:實現 Dijkstra 演算法
Dijkstra 演算法是一種經典演算法,用於查詢圖中從單個源節點到所有其他節點的最短路徑。

Dijkstra 演算法的工作原理是迭代選擇距源節點最短距離的節點並更新到其鄰居節點的最短距離。它維護一個優先順序佇列(或最小堆)以有效地選擇下一個要訪問的節點。

我們將使用鄰接列表來表示該圖,其中每個節點都與其相鄰節點及其相應的邊權重的列表相關聯。

import java.util.*;

class Graph {
    private final Map<Integer, List<Edge>> adjacencyList = new HashMap<>();

    public void addEdge(int source, int destination, int weight) {
        adjacencyList.computeIfAbsent(source, k -> new ArrayList<>()).add(new Edge(destination, weight));
        adjacencyList.computeIfAbsent(destination, k -> new ArrayList<>()).add(new Edge(source, weight)); <font>// for undirected graph<i>
    }

    public List<Edge> getNeighbors(int node) {
        return adjacencyList.getOrDefault(node, Collections.emptyList());
    }

    static class Edge {
        int destination;
        int weight;

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


實施 Dijkstra 演算法
我們將使用 PriorityQueue 來維護與源節點的最短距離尚未最終確定的節點集。我們還將使用 Map 來跟蹤從源節點到圖中每個節點的最短距離。

import java.util.*;

class Dijkstra {
    public static Map<Integer, Integer> shortestPaths(Graph graph, int source) {
        Map<Integer, Integer> distances = new HashMap<>();
        PriorityQueue<Integer> pq = new PriorityQueue<>(Comparator.comparingInt(distances::get));
        Set<Integer> visited = new HashSet<>();

        distances.put(source, 0);
        pq.add(source);

        while (!pq.isEmpty()) {
            int current = pq.poll();
            if (visited.contains(current)) {
                continue;
            }
            visited.add(current);

            for (Graph.Edge neighbor : graph.getNeighbors(current)) {
                int newDistance = distances.get(current) + neighbor.weight;
                if (!distances.containsKey(neighbor.destination) || newDistance < distances.get(neighbor.destination)) {
                    distances.put(neighbor.destination, newDistance);
                    pq.add(neighbor.destination);
                }
            }
        }

        return distances;
    }
}

使用 Java Stream API 實現簡潔
現在,讓我們使用Java Stream API 重寫shortestPaths 方法,使其更加簡潔和富有表現力。

import java.util.*;

class Dijkstra {
    public static Map<Integer, Integer> shortestPaths(Graph graph, int source) {
        Map<Integer, Integer> distances = new HashMap<>();
        PriorityQueue<Integer> pq = new PriorityQueue<>(Comparator.comparingInt(distances::get));
        Set<Integer> visited = new HashSet<>();

        distances.put(source, 0);
        pq.add(source);

        while (!pq.isEmpty()) {
            int current = pq.poll();
            if (visited.contains(current)) {
                continue;
            }
            visited.add(current);

            graph.getNeighbors(current).stream()
                    .filter(neighbor -> !visited.contains(neighbor.destination))
                    .forEach(neighbor -> {
                        int newDistance = distances.get(current) + neighbor.weight;
                        distances.put(neighbor.destination, Math.min(distances.getOrDefault(neighbor.destination, Integer.MAX_VALUE), newDistance));
                        pq.add(neighbor.destination);
                    });
        }

        return distances;
    }
}


使用 Stream API,我們消除了顯式迴圈並將其替換為流操作,從而簡化了迴圈內的程式碼。這使得程式碼更具可讀性並保持了程式設計的函式式風格。


6、Java Stream API:實現深度優先搜尋演算法
使用 Java Stream API 實現圖遍歷的深度優先搜尋演算法

圖遍歷是一個基本的演算法問題,涉及訪問圖的所有節點。深度優先搜尋 (DFS) 是遍歷圖的方法之一,從特定節點開始,然後以深度運動遞迴訪問其鄰居,然後再移動到下一個鄰居。

首先,讓我們使用鄰接列表定義一個簡單的圖形表示。我們將使用“Map”來表示圖,其中鍵是節點,值是相鄰節點的列表。

import java.util.*;

public class Graph {
    private Map<Integer, List<Integer>> adjacencyList;

    public Graph(int vertices) {
        adjacencyList = new HashMap<>();
        for (int i = 0; i < vertices; i++) {
            adjacencyList.put(i, new LinkedList<>());
        }
    }

    public void addEdge(int source, int destination) {
        adjacencyList.get(source).add(destination);
    }

    public List<Integer> getNeighbors(int vertex) {
        return adjacencyList.get(vertex);
    }

    public int getVertexCount() {
        return adjacencyList.size();
    }
}


深度優先搜尋演算法
現在,讓我們使用 Java Stream API 來實現 DFS 演算法。 DFS的基本思想是從給定的節點開始,將其標記為已訪問,然後遞迴地訪問其所有尚未訪問過的鄰居。

import java.util.*;

public class DepthFirstSearch {

    public static void main(String[] args) {
        Graph graph = new Graph(5);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(1, 3);
        graph.addEdge(1, 4);
        graph.addEdge(2, 4);

        dfs(graph, 0);
    }

    public static void dfs(Graph graph, int start) {
        Set<Integer> visited = new HashSet<>();
        dfsRecursive(graph, start, visited);
    }

    private static void dfsRecursive(Graph graph, int current, Set<Integer> visited) {
        visited.add(current);
        System.out.println(<font>"Visiting node: " + current);

        graph.getNeighbors(current).stream()
            .filter(neighbor -> !visited.contains(neighbor))
            .forEach(neighbor -> dfsRecursive(graph, neighbor, visited));
    }
}


在此實現中,我們從節點 0 開始 DFS 遍歷。“dfsRecursive”方法將當前節點標記為已訪問,列印其值,然後在當前節點的所有未訪問鄰居上遞迴呼叫自身。


 <i><i><i><i>

相關文章