diff --git a/src/main/java/com/thealgorithms/others/PageRank.java b/src/main/java/com/thealgorithms/others/PageRank.java index c7be7a9882bc..2899b80bcee8 100644 --- a/src/main/java/com/thealgorithms/others/PageRank.java +++ b/src/main/java/com/thealgorithms/others/PageRank.java @@ -2,94 +2,306 @@ import java.util.Scanner; -class PageRank { +/** + * PageRank Algorithm Implementation + * + *
+ * The PageRank algorithm is used by Google Search to rank web pages in their + * search engine + * results. It was named after Larry Page, one of the founders of Google. + * PageRank is a way of + * measuring the importance of website pages. + * + *
+ * Algorithm: 1. Initialize PageRank values for all pages to 1/N (where N is the + * total number + * of pages) 2. For each iteration: - For each page, calculate the new PageRank + * by summing the + * contributions from all incoming links - Apply the damping factor: PR(page) = + * (1-d) + d * + * sum(PR(incoming_page) / outgoing_links(incoming_page)) 3. Repeat until + * convergence + * + * @see PageRank Algorithm + */ +public final class PageRank { + private static final int MAX_NODES = 10; + private static final double DEFAULT_DAMPING_FACTOR = 0.85; + private static final int DEFAULT_ITERATIONS = 2; + + private int[][] adjacencyMatrix; + private double[] pageRankValues; + private int nodeCount; + + /** + * Constructor to initialize PageRank with specified number of nodes + * + * @param numberOfNodes the number of nodes/pages in the graph + * @throws IllegalArgumentException if numberOfNodes is less than 1 or greater + * than MAX_NODES + */ + public PageRank(int numberOfNodes) { + if (numberOfNodes < 1 || numberOfNodes > MAX_NODES) { + throw new IllegalArgumentException("Number of nodes must be between 1 and " + MAX_NODES); + } + this.nodeCount = numberOfNodes; + this.adjacencyMatrix = new int[MAX_NODES][MAX_NODES]; + this.pageRankValues = new double[MAX_NODES]; + } + + /** + * Default constructor for interactive mode + */ + public PageRank() { + this.adjacencyMatrix = new int[MAX_NODES][MAX_NODES]; + this.pageRankValues = new double[MAX_NODES]; + } + + /** + * Main method for interactive PageRank calculation + * + * @param args command line arguments (not used) + */ public static void main(String[] args) { - int nodes; - int i; - int j; - Scanner in = new Scanner(System.in); - System.out.print("Enter the Number of WebPages: "); - nodes = in.nextInt(); - PageRank p = new PageRank(); - System.out.println("Enter the Adjacency Matrix with 1->PATH & 0->NO PATH Between two WebPages: "); - for (i = 1; i <= nodes; i++) { - for (j = 1; j <= nodes; j++) { - p.path[i][j] = in.nextInt(); - if (j == i) { - p.path[i][j] = 0; + try (Scanner scanner = new Scanner(System.in)) { + System.out.print("Enter the Number of WebPages: "); + int nodes = scanner.nextInt(); + + PageRank pageRank = new PageRank(nodes); + System.out.println("Enter the Adjacency Matrix with 1->PATH & 0->NO PATH Between two WebPages: "); + + for (int i = 1; i <= nodes; i++) { + for (int j = 1; j <= nodes; j++) { + int value = scanner.nextInt(); + pageRank.setEdge(i, j, value); } } + + pageRank.calculatePageRank(nodes, DEFAULT_DAMPING_FACTOR, DEFAULT_ITERATIONS, true); } - p.calc(nodes); } - public int[][] path = new int[10][10]; - public double[] pagerank = new double[10]; + /** + * Sets an edge in the adjacency matrix + * + * @param from source node (1-indexed) + * @param to destination node (1-indexed) + * @param value 1 if edge exists, 0 otherwise + */ + public void setEdge(int from, int to, int value) { + if (from == to) { + adjacencyMatrix[from][to] = 0; // No self-loops + } else { + adjacencyMatrix[from][to] = value; + } + } - public void calc(double totalNodes) { - double initialPageRank; - double outgoingLinks = 0; - double dampingFactor = 0.85; - double[] tempPageRank = new double[10]; - int externalNodeNumber; - int internalNodeNumber; - int k = 1; // For Traversing - int iterationStep = 1; - initialPageRank = 1 / totalNodes; - System.out.printf(" Total Number of Nodes :" + totalNodes + "\t Initial PageRank of All Nodes :" + initialPageRank + "\n"); + /** + * Sets the adjacency matrix for the graph + * + * @param matrix the adjacency matrix (1-indexed) + */ + public void setAdjacencyMatrix(int[][] matrix) { + for (int i = 1; i <= nodeCount; i++) { + for (int j = 1; j <= nodeCount; j++) { + setEdge(i, j, matrix[i][j]); + } + } + } - // 0th ITERATION _ OR _ INITIALIZATION PHASE // - for (k = 1; k <= totalNodes; k++) { - this.pagerank[k] = initialPageRank; + /** + * Gets the PageRank value for a specific node + * + * @param node the node index (1-indexed) + * @return the PageRank value + */ + public double getPageRank(int node) { + if (node < 1 || node > nodeCount) { + throw new IllegalArgumentException("Node index out of bounds"); } - System.out.print("\n Initial PageRank Values , 0th Step \n"); + return pageRankValues[node]; + } + + /** + * Gets all PageRank values + * + * @return array of PageRank values (1-indexed) + */ + public double[] getAllPageRanks() { + return pageRankValues.clone(); + } + + /** + * Calculates PageRank using the default damping factor and iterations + * + * @param totalNodes the total number of nodes + * @return array of PageRank values + */ + public double[] calculatePageRank(int totalNodes) { + return calculatePageRank(totalNodes, DEFAULT_DAMPING_FACTOR, DEFAULT_ITERATIONS, false); + } + + /** + * Calculates PageRank with custom parameters + * + * @param totalNodes the total number of nodes + * @param dampingFactor the damping factor (typically 0.85) + * @param iterations number of iterations to perform + * @param verbose whether to print detailed output + * @return array of PageRank values + */ + public double[] calculatePageRank(int totalNodes, double dampingFactor, int iterations, boolean verbose) { + validateInputParameters(totalNodes, dampingFactor, iterations); - for (k = 1; k <= totalNodes; k++) { - System.out.printf(" Page Rank of " + k + " is :\t" + this.pagerank[k] + "\n"); + this.nodeCount = totalNodes; + double initialPageRank = 1.0 / totalNodes; + + if (verbose) { + System.out.printf("Total Number of Nodes: %d\tInitial PageRank of All Nodes: %.6f%n", totalNodes, initialPageRank); } - while (iterationStep <= 2) { // Iterations - // Store the PageRank for All Nodes in Temporary Array - for (k = 1; k <= totalNodes; k++) { - tempPageRank[k] = this.pagerank[k]; - this.pagerank[k] = 0; - } + initializePageRanks(totalNodes, initialPageRank, verbose); + performIterations(totalNodes, dampingFactor, iterations, verbose); - for (internalNodeNumber = 1; internalNodeNumber <= totalNodes; internalNodeNumber++) { - for (externalNodeNumber = 1; externalNodeNumber <= totalNodes; externalNodeNumber++) { - if (this.path[externalNodeNumber][internalNodeNumber] == 1) { - k = 1; - outgoingLinks = 0; // Count the Number of Outgoing Links for each externalNodeNumber - while (k <= totalNodes) { - if (this.path[externalNodeNumber][k] == 1) { - outgoingLinks = outgoingLinks + 1; // Counter for Outgoing Links - } - k = k + 1; - } - // Calculate PageRank - this.pagerank[internalNodeNumber] += tempPageRank[externalNodeNumber] * (1 / outgoingLinks); - } - } - System.out.printf("\n After " + iterationStep + "th Step \n"); + if (verbose) { + System.out.println("\nFinal PageRank:"); + printPageRanks(totalNodes); + } - for (k = 1; k <= totalNodes; k++) { - System.out.printf(" Page Rank of " + k + " is :\t" + this.pagerank[k] + "\n"); - } + return pageRankValues.clone(); + } + + /** + * Validates input parameters for PageRank calculation + * + * @param totalNodes the total number of nodes + * @param dampingFactor the damping factor + * @param iterations number of iterations + * @throws IllegalArgumentException if parameters are invalid + */ + private void validateInputParameters(int totalNodes, double dampingFactor, int iterations) { + if (totalNodes < 1 || totalNodes > MAX_NODES) { + throw new IllegalArgumentException("Total nodes must be between 1 and " + MAX_NODES); + } + if (dampingFactor < 0 || dampingFactor > 1) { + throw new IllegalArgumentException("Damping factor must be between 0 and 1"); + } + if (iterations < 1) { + throw new IllegalArgumentException("Iterations must be at least 1"); + } + } + + /** + * Initializes PageRank values for all nodes + * + * @param totalNodes the total number of nodes + * @param initialPageRank the initial PageRank value + * @param verbose whether to print output + */ + private void initializePageRanks(int totalNodes, double initialPageRank, boolean verbose) { + for (int i = 1; i <= totalNodes; i++) { + pageRankValues[i] = initialPageRank; + } + + if (verbose) { + System.out.println("\nInitial PageRank Values, 0th Step"); + printPageRanks(totalNodes); + } + } - iterationStep = iterationStep + 1; + /** + * Performs the iterative PageRank calculation + * + * @param totalNodes the total number of nodes + * @param dampingFactor the damping factor + * @param iterations number of iterations + * @param verbose whether to print output + */ + private void performIterations(int totalNodes, double dampingFactor, int iterations, boolean verbose) { + for (int iteration = 1; iteration <= iterations; iteration++) { + double[] tempPageRank = storeCurrentPageRanks(totalNodes); + calculateNewPageRanks(totalNodes, tempPageRank); + applyDampingFactor(totalNodes, dampingFactor); + + if (verbose) { + System.out.printf("%nAfter %d iteration(s)%n", iteration); + printPageRanks(totalNodes); } + } + } - // Add the Damping Factor to PageRank - for (k = 1; k <= totalNodes; k++) { - this.pagerank[k] = (1 - dampingFactor) + dampingFactor * this.pagerank[k]; + /** + * Stores current PageRank values in a temporary array + * + * @param totalNodes the total number of nodes + * @return temporary array with current PageRank values + */ + private double[] storeCurrentPageRanks(int totalNodes) { + double[] tempPageRank = new double[MAX_NODES]; + for (int i = 1; i <= totalNodes; i++) { + tempPageRank[i] = pageRankValues[i]; + pageRankValues[i] = 0; + } + return tempPageRank; + } + + /** + * Calculates new PageRank values based on incoming links + * + * @param totalNodes the total number of nodes + * @param tempPageRank temporary array with previous PageRank values + */ + private void calculateNewPageRanks(int totalNodes, double[] tempPageRank) { + for (int targetNode = 1; targetNode <= totalNodes; targetNode++) { + for (int sourceNode = 1; sourceNode <= totalNodes; sourceNode++) { + if (adjacencyMatrix[sourceNode][targetNode] == 1) { + int outgoingLinks = countOutgoingLinks(sourceNode, totalNodes); + if (outgoingLinks > 0) { + pageRankValues[targetNode] += tempPageRank[sourceNode] / outgoingLinks; + } + } } + } + } - // Display PageRank - System.out.print("\n Final Page Rank : \n"); - for (k = 1; k <= totalNodes; k++) { - System.out.printf(" Page Rank of " + k + " is :\t" + this.pagerank[k] + "\n"); + /** + * Applies the damping factor to all PageRank values + * + * @param totalNodes the total number of nodes + * @param dampingFactor the damping factor + */ + private void applyDampingFactor(int totalNodes, double dampingFactor) { + for (int i = 1; i <= totalNodes; i++) { + pageRankValues[i] = (1 - dampingFactor) + dampingFactor * pageRankValues[i]; + } + } + + /** + * Counts the number of outgoing links from a node + * + * @param node the source node (1-indexed) + * @param totalNodes total number of nodes + * @return the count of outgoing links + */ + private int countOutgoingLinks(int node, int totalNodes) { + int count = 0; + for (int i = 1; i <= totalNodes; i++) { + if (adjacencyMatrix[node][i] == 1) { + count++; } } + return count; + } + + /** + * Prints the PageRank values for all nodes + * + * @param totalNodes the total number of nodes + */ + private void printPageRanks(int totalNodes) { + for (int i = 1; i <= totalNodes; i++) { + System.out.printf("PageRank of %d: %.6f%n", i, pageRankValues[i]); + } } } diff --git a/src/test/java/com/thealgorithms/others/PageRankTest.java b/src/test/java/com/thealgorithms/others/PageRankTest.java new file mode 100644 index 000000000000..de96403dd1fc --- /dev/null +++ b/src/test/java/com/thealgorithms/others/PageRankTest.java @@ -0,0 +1,319 @@ +package com.thealgorithms.others; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +/** + * Test class for PageRank algorithm implementation + * + * @author Hardvan + */ +class PageRankTest { + + private static final double EPSILON = 0.0001; // Tolerance for floating point comparisons + + /** + * Test basic PageRank calculation with a simple 3-node graph + * Graph: 1 -> 2, 2 -> 3, 3 -> 1 + */ + @Test + void testSimpleThreeNodeGraph() { + PageRank pageRank = new PageRank(3); + + // Create a simple circular graph: 1 -> 2 -> 3 -> 1 + int[][] adjacencyMatrix = new int[10][10]; + adjacencyMatrix[1][2] = 1; // Node 1 links to Node 2 + adjacencyMatrix[2][3] = 1; // Node 2 links to Node 3 + adjacencyMatrix[3][1] = 1; // Node 3 links to Node 1 + + pageRank.setAdjacencyMatrix(adjacencyMatrix); + double[] result = pageRank.calculatePageRank(3); + + // All nodes should have equal PageRank in a circular graph + assertNotNull(result); + assertEquals(result[1], result[2], EPSILON); + assertEquals(result[2], result[3], EPSILON); + } + + /** + * Test PageRank with a two-node graph where one node points to another + */ + @Test + void testTwoNodeGraph() { + PageRank pageRank = new PageRank(2); + + // Node 1 links to Node 2 + pageRank.setEdge(1, 2, 1); + + double[] result = pageRank.calculatePageRank(2); + + // Node 2 should have higher PageRank than Node 1 (after 2 iterations) + assertNotNull(result); + assertEquals(0.2775, result[2], EPSILON); + assertEquals(0.15, result[1], EPSILON); + } + + /** + * Test PageRank with a single node (no links) + */ + @Test + void testSingleNode() { + PageRank pageRank = new PageRank(1); + + double[] result = pageRank.calculatePageRank(1); + + // Single node should have (1-d) = 0.15 after applying damping + assertNotNull(result); + assertEquals(0.15, result[1], EPSILON); + } + + /** + * Test PageRank with a hub-and-spoke configuration + * Node 1 is the hub, pointing to nodes 2, 3, and 4 + */ + @Test + void testHubAndSpokeGraph() { + PageRank pageRank = new PageRank(4); + + // Hub node (1) links to all other nodes + pageRank.setEdge(1, 2, 1); + pageRank.setEdge(1, 3, 1); + pageRank.setEdge(1, 4, 1); + + // All spokes link back to hub + pageRank.setEdge(2, 1, 1); + pageRank.setEdge(3, 1, 1); + pageRank.setEdge(4, 1, 1); + + double[] result = pageRank.calculatePageRank(4); + + assertNotNull(result); + // Hub should have higher PageRank + assertEquals(result[2], result[3], EPSILON); + assertEquals(result[3], result[4], EPSILON); + } + + /** + * Test PageRank with multiple iterations + */ + @Test + void testMultipleIterations() { + PageRank pageRank = new PageRank(3); + + pageRank.setEdge(1, 2, 1); + pageRank.setEdge(2, 3, 1); + pageRank.setEdge(3, 1, 1); + + double[] result2Iterations = pageRank.calculatePageRank(3, 0.85, 2, false); + double[] result5Iterations = pageRank.calculatePageRank(3, 0.85, 5, false); + + assertNotNull(result2Iterations); + assertNotNull(result5Iterations); + } + + /** + * Test getPageRank method for individual node + */ + @Test + void testGetPageRank() { + PageRank pageRank = new PageRank(2); + + pageRank.setEdge(1, 2, 1); + pageRank.calculatePageRank(2); + + double node1PageRank = pageRank.getPageRank(1); + double node2PageRank = pageRank.getPageRank(2); + + assertEquals(0.15, node1PageRank, EPSILON); + assertEquals(0.2775, node2PageRank, EPSILON); + } + + /** + * Test getAllPageRanks method + */ + @Test + void testGetAllPageRanks() { + PageRank pageRank = new PageRank(2); + + pageRank.setEdge(1, 2, 1); + pageRank.calculatePageRank(2); + + double[] allPageRanks = pageRank.getAllPageRanks(); + + assertNotNull(allPageRanks); + assertEquals(0.15, allPageRanks[1], EPSILON); + assertEquals(0.2775, allPageRanks[2], EPSILON); + } + + /** + * Test that self-loops are not allowed + */ + @Test + void testNoSelfLoops() { + PageRank pageRank = new PageRank(2); + + // Try to set a self-loop + pageRank.setEdge(1, 1, 1); + pageRank.setEdge(1, 2, 1); + + double[] result = pageRank.calculatePageRank(2); + + assertNotNull(result); + // Self-loop should be ignored + } + + /** + * Test exception when node count is too small + */ + @Test + void testInvalidNodeCountTooSmall() { + assertThrows(IllegalArgumentException.class, () -> new PageRank(0)); + } + + /** + * Test exception when node count is too large + */ + @Test + void testInvalidNodeCountTooLarge() { + assertThrows(IllegalArgumentException.class, () -> new PageRank(11)); + } + + /** + * Test exception for invalid damping factor (negative) + */ + @Test + void testInvalidDampingFactorNegative() { + PageRank pageRank = new PageRank(2); + pageRank.setEdge(1, 2, 1); + + assertThrows(IllegalArgumentException.class, () -> pageRank.calculatePageRank(2, -0.1, 2, false)); + } + + /** + * Test exception for invalid damping factor (greater than 1) + */ + @Test + void testInvalidDampingFactorTooLarge() { + PageRank pageRank = new PageRank(2); + pageRank.setEdge(1, 2, 1); + + assertThrows(IllegalArgumentException.class, () -> pageRank.calculatePageRank(2, 1.5, 2, false)); + } + + /** + * Test exception for invalid iterations (less than 1) + */ + @Test + void testInvalidIterations() { + PageRank pageRank = new PageRank(2); + pageRank.setEdge(1, 2, 1); + + assertThrows(IllegalArgumentException.class, () -> pageRank.calculatePageRank(2, 0.85, 0, false)); + } + + /** + * Test exception when getting PageRank for invalid node + */ + @Test + void testGetPageRankInvalidNode() { + PageRank pageRank = new PageRank(2); + pageRank.calculatePageRank(2); + + assertThrows(IllegalArgumentException.class, () -> pageRank.getPageRank(3)); + } + + /** + * Test exception when getting PageRank for node less than 1 + */ + @Test + void testGetPageRankNodeLessThanOne() { + PageRank pageRank = new PageRank(2); + pageRank.calculatePageRank(2); + + assertThrows(IllegalArgumentException.class, () -> pageRank.getPageRank(0)); + } + + /** + * Test complex graph with multiple incoming and outgoing links + */ + @Test + void testComplexGraph() { + PageRank pageRank = new PageRank(4); + + // Create a more complex graph + pageRank.setEdge(1, 2, 1); + pageRank.setEdge(1, 3, 1); + pageRank.setEdge(2, 3, 1); + pageRank.setEdge(3, 4, 1); + pageRank.setEdge(4, 1, 1); + + double[] result = pageRank.calculatePageRank(4); + + assertNotNull(result); + // Node 3 should have high PageRank (receives links from nodes 1 and 2) + // After 2 iterations, the sum will not equal total nodes + double sum = result[1] + result[2] + result[3] + result[4]; + assertEquals(1.8325, sum, EPSILON); + } + + /** + * Test that PageRank values sum after 2 iterations + */ + @Test + void testPageRankSum() { + PageRank pageRank = new PageRank(5); + + // Create arbitrary graph + pageRank.setEdge(1, 2, 1); + pageRank.setEdge(2, 3, 1); + pageRank.setEdge(3, 4, 1); + pageRank.setEdge(4, 5, 1); + pageRank.setEdge(5, 1, 1); + + double[] result = pageRank.calculatePageRank(5); + + double sum = 0; + for (int i = 1; i <= 5; i++) { + sum += result[i]; + } + + // Sum after 2 iterations with default damping factor + assertEquals(2.11, sum, EPSILON); + } + + /** + * Test graph with isolated node (no incoming or outgoing links) + */ + @Test + void testGraphWithIsolatedNode() { + PageRank pageRank = new PageRank(3); + + // Node 1 and 2 are connected, Node 3 is isolated + pageRank.setEdge(1, 2, 1); + pageRank.setEdge(2, 1, 1); + + double[] result = pageRank.calculatePageRank(3); + + assertNotNull(result); + // Isolated node should have some PageRank due to damping factor + assertEquals(0.15, result[3], EPSILON); + } + + /** + * Test verbose mode (should not throw exception) + */ + @Test + void testVerboseMode() { + PageRank pageRank = new PageRank(2); + + pageRank.setEdge(1, 2, 1); + + // This should execute without throwing an exception + double[] result = pageRank.calculatePageRank(2, 0.85, 2, true); + + assertNotNull(result); + } +}