From 8011be5a9499aeab28202422c28115e9d82133ee Mon Sep 17 00:00:00 2001 From: owenong1 Date: Mon, 17 Jul 2023 21:33:26 +0800 Subject: [PATCH] bfs --- src/algorithms/graphs/breadthFirstSearch.java | 133 ++++++++++++++++++ src/algorithms/graphs/depthFirstSearch.java | 24 ++-- .../graphs/util/BinaryTreeNode.java | 39 +++++ src/algorithms/graphs/util/GraphNode.java | 44 ++++++ src/algorithms/graphs/util/TreeNode.java | 39 ----- .../graphs/breadthFirstSearchTest.java | 87 ++++++++++++ .../graphs/depthFirstSearchTest.java | 12 +- 7 files changed, 323 insertions(+), 55 deletions(-) create mode 100644 src/algorithms/graphs/breadthFirstSearch.java create mode 100644 src/algorithms/graphs/util/BinaryTreeNode.java create mode 100644 src/algorithms/graphs/util/GraphNode.java delete mode 100644 src/algorithms/graphs/util/TreeNode.java create mode 100644 test/algorithms/graphs/breadthFirstSearchTest.java diff --git a/src/algorithms/graphs/breadthFirstSearch.java b/src/algorithms/graphs/breadthFirstSearch.java new file mode 100644 index 00000000..b739fcdc --- /dev/null +++ b/src/algorithms/graphs/breadthFirstSearch.java @@ -0,0 +1,133 @@ +package src.algorithms.graphs; + +import java.util.*; + +import src.algorithms.graphs.util.BinaryTreeNode; +import src.algorithms.graphs.util.GraphNode; + +/** + * Implementation of BFS + * + * Breadth-First search is a graph traversal algorithm that utilizes a queue (FIFO) data structure + * It is useful in finding a shortest-hops solution in graphs + * This method is also used to obtain level order traversals of trees. + * + * In general, BFS works as such: + * - Start with a queue that contains the root node + * - While the queue is not empty: + * - Pop a vertex from the queue + * - Push all neighbours to the queue if they have not been visited yet + * - Update any variables as needed + * + * Time: O(V + E), where V is the number of vertices/nodes, and E is the number of edges in the graph + * Explanation: Each vertex is popped in O(1) time from the stack exactly once, hence O(V) + * For each edge, we must check in O(1) time if we have visited the adjacent vertex to know + * whether to push it into the queue, hence O(E). + * Note that if the graph is a forest, BFS will likely terminate earlier, as fewer vertices are traversed + * + * Space: O(V): We utilize a Hashset to store the vertices we have visited already. In the worst case we have a + * connected graph where all vertices are traversed, and our Hashset stores all O(V) vertices. + * Further, we use a queue to hold the vertices to be traversed. In the worst case, the root will have all + * other vertices as neighbours, and our queue will contain O(V) vertices. + * + * ** Note: The above description assumes an adjacency list in order to consider neighbours in an efficient manner + * If an adjacency matrix were used, it would cost O(V) to find neighbours for a single vertex, making our + * average case time complexity O(V^2) for a connected graph + * + * The implementation demonstrates the use of BFS in finding the level-order (Root, Left, Right) traversal of a binary tree + * The tree is represented using a custom BinaryTreeNode class + * + */ +public class breadthFirstSearch { + + // Prints level order traversal from left to right + public static List levelOrder(BinaryTreeNode root) { + if (root == null) { return new ArrayList<>(); } + List traversal = new ArrayList<>(); + Queue queue = new LinkedList<>(); + queue.add(root); + + while (!queue.isEmpty()) { + BinaryTreeNode curr = queue.remove(); + traversal.add((Integer) curr.getVal()); + if (curr.getLeft() != null) { queue.add(curr.getLeft()); } + if (curr.getRight() != null) { queue.add(curr.getRight()); } + } + + return traversal; + } + + // Finds the number of friend hops needed to go from person A to person B + // Uses GraphNode, where a node holds a String of the persons name, and an edge represents a friendship + public static int friendHops(GraphNode personA, GraphNode personB) { + // Hashset to store the people we have seen already + HashSet checked = new HashSet<>(); + // Hashmap to remember how many hops were needed to get to a specific friend * + HashMap map = new HashMap<>(); + + Queue queue = new LinkedList<>(); + queue.add(personA); + // the number of hops to the person themselves is 0, so we map: personA -> 0 + map.put(personA, 0); + int hops = 0; + + while (!queue.isEmpty()) { + // poll the queue to get the next person to consider + GraphNode currPerson = queue.remove(); + // add the person to the checked hashset so we don't consider them again + checked.add(currPerson); + // grab the number of hops from the hashmap and add 1 + hops = map.get(currPerson) + 1; + + List neighbours = currPerson.neighbours(); + for (GraphNode neighbour : neighbours) { + if (neighbour == personB) { return hops; } + if (!checked.contains(neighbour)) { + queue.add(neighbour); + map.put(neighbour, hops); + } + } + } + // Returns -1 if person not found + return -1; + + // * Note that we can actually use just the hashmap instead of both the hashmap and the hashset! + // This is because the hashmap supports the map.containsKey() function. + } + + public static int friendHopsVisualize(GraphNode personA, GraphNode personB) { + // Hashset to store the people we have seen already + HashSet checked = new HashSet<>(); + // Hashmap to remember how many hops were needed to get to a specific friend * + HashMap map = new HashMap<>(); + + Queue queue = new LinkedList<>(); + queue.add(personA); + // the number of hops to the person themselves is 0, so we map: personA -> 0 + map.put(personA, 0); + int hops = 0; + + while (!queue.isEmpty()) { + System.out.println("Current queue: " + String.valueOf(queue) + ", current hops: " + hops); + // poll the queue to get the next person to consider + GraphNode currPerson = queue.remove(); + // add the person to the checked hashset so we don't consider them again + checked.add(currPerson); + // grab the number of hops from the hashmap and add 1 + hops = map.get(currPerson) + 1; + + System.out.println("Looking at friends of: " + currPerson.toString()); + List neighbours = currPerson.neighbours(); + for (GraphNode neighbour : neighbours) { + if (neighbour == personB) { return hops; } + if (!checked.contains(neighbour)) { + queue.add(neighbour); + map.put(neighbour, hops); + } + } + System.out.println("Current queue: " + String.valueOf(queue) + ", current hops: " + hops); + } + // Returns -1 if person not found + return -1; + } +} diff --git a/src/algorithms/graphs/depthFirstSearch.java b/src/algorithms/graphs/depthFirstSearch.java index 10c05c1c..e8bb2fed 100644 --- a/src/algorithms/graphs/depthFirstSearch.java +++ b/src/algorithms/graphs/depthFirstSearch.java @@ -1,12 +1,12 @@ package src.algorithms.graphs; import java.util.*; -import src.algorithms.graphs.util.TreeNode; +import src.algorithms.graphs.util.BinaryTreeNode; /** * Implementation of DFS * - * Depth-First search is a graph traversal algorithm that utilizes a stack (FIFO) data structure + * Depth-First search is a graph traversal algorithm that utilizes a stack (LIFO) data structure * It is useful in finding a shortest-path solution (IN TREES), or a single solution in graphs * This method is also used to obtain order-traversals of trees. * @@ -22,28 +22,32 @@ * For each edge, we must check in O(1) time if we have visited the adjacent vertex to know * whether to push it into the stack, hence O(E). * Note that if the graph is a forest, DFS will likely terminate earlier, as fewer vertices are traversed + * * Space: O(V): We utilize a Hashset to store the vertices we have visited already. In the worst case we have a - * connected graph where all vertices are traversed, and our Hashset stores O(V) vertices. + * connected graph where all vertices are traversed, and our Hashset stores all O(V) vertices. * Further, we use a stack to hold the vertices to be traversed. In the worst case, the root will have all * other vertices as neighbours, and our stack will contain O(V) vertices. + * * ** Note: The above description assumes an adjacency list in order to consider neighbours in an efficient manner * If an adjacency matrix were used, it would cost O(V) to find neighbours for a single vertex, making our * average case time complexity O(V^2) for a connected graph * * The implementation demonstrates the use of DFS in finding the pre-order (Root, Left, Right) traversal of a binary tree - * The tree is represented using a custom TreeNode class + * The tree is represented using a custom BinaryTreeNode class + * + * TODO: Add new examples, and algo for all orderings */ public class depthFirstSearch { - public static List preOrder(TreeNode root) { + public static List preOrder(BinaryTreeNode root) { if (root == null) { return new ArrayList<>(); } List traversal = new ArrayList<>(); - Stack stack = new Stack<>(); + Stack stack = new Stack<>(); stack.push(root); while (!stack.empty()) { - TreeNode curr = stack.pop(); + BinaryTreeNode curr = stack.pop(); traversal.add(curr.getVal()); if (curr.getRight() != null) { stack.push(curr.getRight()); } if (curr.getLeft() != null) { stack.push(curr.getLeft()); } @@ -53,15 +57,15 @@ public static List preOrder(TreeNode root) { } // call this for visualization of process - public static List preOrderVisualize(TreeNode root) { + public static List preOrderVisualize(BinaryTreeNode root) { if (root == null) { return new ArrayList<>(); } List traversal = new ArrayList<>(); - Stack stack = new Stack<>(); + Stack stack = new Stack<>(); stack.push(root); while (!stack.empty()) { System.out.println("Current stack: " + stack.toString() + ", Current traversal: " + traversal.toString()); - TreeNode curr = stack.pop(); + BinaryTreeNode curr = stack.pop(); System.out.println("Popped Node: " + Integer.toString(curr.getVal())); traversal.add(curr.getVal()); System.out.println("Current stack: " + stack.toString() + ", Current traversal: " + traversal.toString()); diff --git a/src/algorithms/graphs/util/BinaryTreeNode.java b/src/algorithms/graphs/util/BinaryTreeNode.java new file mode 100644 index 00000000..ff6a4296 --- /dev/null +++ b/src/algorithms/graphs/util/BinaryTreeNode.java @@ -0,0 +1,39 @@ +package src.algorithms.graphs.util; + +public class BinaryTreeNode { + int val; + BinaryTreeNode left = null; + BinaryTreeNode right = null; + + public BinaryTreeNode(int val) { + this.val = val; + } + + public BinaryTreeNode(int val, BinaryTreeNode left, BinaryTreeNode right) { + this.val = val; + this.left = left; + this.right = right; + } + + public int getVal() { return this.val; } + public BinaryTreeNode getLeft() { return this.left; } + public BinaryTreeNode getRight() { return this.right; } + + public void setLeft(BinaryTreeNode left) { this.left = left; } + public void setRight(BinaryTreeNode right) { this.right = right; } + + @Override + public boolean equals(Object other) { + if (other == this) { return true; } + if (!(other instanceof BinaryTreeNode)) { return false; } + BinaryTreeNode node = (BinaryTreeNode) other; + return this.val == node.val; + } + + @Override + public int hashCode() { return this.val; } + + @Override + public String toString() { return String.valueOf(this.val); } + +} diff --git a/src/algorithms/graphs/util/GraphNode.java b/src/algorithms/graphs/util/GraphNode.java new file mode 100644 index 00000000..1fe478dd --- /dev/null +++ b/src/algorithms/graphs/util/GraphNode.java @@ -0,0 +1,44 @@ +package src.algorithms.graphs.util; + +import java.util.*; + +public class GraphNode { + public T val; + public List neighbour; + + public GraphNode(T val) { + this.val = val; + neighbour = new ArrayList<>(); + } + + public GraphNode(T val, List neighbours) { + this.val = val; + this.neighbour = neighbours; + } + + public static void connect(GraphNode a, GraphNode b) { + a.neighbour.add(b); + b.neighbour.add(a); + } + + + public List neighbours() { return this.neighbour; } + + public T getVal() { return this.val; } + + @Override + public String toString() { return String.valueOf(this.val); } + + @Override + public boolean equals(Object other) { + if (other == this) { return true; } + if (!(other instanceof GraphNode)) { return false; } + GraphNode node = (GraphNode) other; + return this.val == node.val; + } + + @Override + public int hashCode() { + return val.hashCode(); + } +} diff --git a/src/algorithms/graphs/util/TreeNode.java b/src/algorithms/graphs/util/TreeNode.java deleted file mode 100644 index e215dbe6..00000000 --- a/src/algorithms/graphs/util/TreeNode.java +++ /dev/null @@ -1,39 +0,0 @@ -package src.algorithms.graphs.util; - -public class TreeNode { - int val; - TreeNode left = null; - TreeNode right = null; - - public TreeNode(int val) { - this.val = val; - } - - public TreeNode(int val, TreeNode left, TreeNode right) { - this.val = val; - this.left = left; - this.right = right; - } - - public int getVal() { return this.val; } - public TreeNode getLeft() { return this.left; } - public TreeNode getRight() { return this.right; } - - public void setLeft(TreeNode left) { this.left = left; } - public void setRight(TreeNode right) { this.right = right; } - - @Override - public boolean equals(Object other) { - if (other == this) { return true; } - if (!(other instanceof TreeNode)) { return false; } - TreeNode Treenode = (TreeNode) other; - return this.val == Treenode.val; - } - - @Override - public int hashCode() { return this.val; } - - @Override - public String toString() { return String.valueOf(this.val); } - -} diff --git a/test/algorithms/graphs/breadthFirstSearchTest.java b/test/algorithms/graphs/breadthFirstSearchTest.java new file mode 100644 index 00000000..75537193 --- /dev/null +++ b/test/algorithms/graphs/breadthFirstSearchTest.java @@ -0,0 +1,87 @@ +package test.algorithms.graphs; + +import org.junit.Test; + +import src.algorithms.graphs.breadthFirstSearch; +import src.algorithms.graphs.util.BinaryTreeNode; +import src.algorithms.graphs.util.GraphNode; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class breadthFirstSearchTest { + + @Test + public void bfs_levelOrderTraversal_shouldReturnAccurate() { + // empty tree + List firstList = new ArrayList<>(); + BinaryTreeNode root1 = null; + List firstResult = breadthFirstSearch.levelOrder(root1); + + //standard tree + // 1 + // / \ + // 2 3 + // / \ + // 4 5 + List secondList = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); + BinaryTreeNode rootRight2 = new BinaryTreeNode(3, new BinaryTreeNode(4), new BinaryTreeNode(5)); + BinaryTreeNode root2 = new BinaryTreeNode(1, new BinaryTreeNode(2), rootRight2); + List secondResult = breadthFirstSearch.levelOrder(root2); + + //standard tree 2 + // 1 + // / \ + // 2 7 + // / \ + // 3 5 + // / / + // 4 6 + List thirdList = new ArrayList<>(Arrays.asList(1, 2, 7, 3, 5, 4, 6)); + BinaryTreeNode rootLeft3 = new BinaryTreeNode(2, new BinaryTreeNode(3, new BinaryTreeNode(4), null), new BinaryTreeNode(5, new BinaryTreeNode(6), null)); + BinaryTreeNode root3 = new BinaryTreeNode(1, rootLeft3, new BinaryTreeNode(7)); + List thirdResult = breadthFirstSearch.levelOrder(root3); + + assert firstResult.equals(firstList); + System.out.println(secondResult.toString()); + assert secondResult.equals(secondList); + System.out.println(thirdResult.toString()); + assert thirdResult.equals(thirdList); + } + + @Test + public void bfs_friendHops_shouldReturnAccurate() { + + // Tests based on the following friend graph: + // Andre ------ Ben ------- Diana ------- Evelyn ------- Gerald + // | | | + // Cathy Felix-------------------------- + // | + // Harold ------ Iris Anonymous + GraphNode andre = new GraphNode<>("andre"); + GraphNode ben = new GraphNode<>("ben"); + GraphNode cathy = new GraphNode<>("cathy"); + GraphNode diana = new GraphNode<>("diana"); + GraphNode evelyn = new GraphNode<>("evelyn"); + GraphNode felix = new GraphNode<>("felix"); + GraphNode gerald = new GraphNode<>("gerald"); + GraphNode harold = new GraphNode<>("harold"); + GraphNode iris = new GraphNode<>("iris"); + GraphNode anonymous = new GraphNode<>("anonymous"); + GraphNode.connect(andre, ben); + GraphNode.connect(andre, cathy); + GraphNode.connect(cathy, harold); + GraphNode.connect(harold, iris); + GraphNode.connect(ben, felix); + GraphNode.connect(ben, diana); + GraphNode.connect(diana, evelyn); + GraphNode.connect(evelyn, felix); + GraphNode.connect(evelyn, gerald); + + assert breadthFirstSearch.friendHops(anonymous, diana) == -1; + assert breadthFirstSearch.friendHops(iris, gerald) == 7; + assert breadthFirstSearch.friendHops(andre, gerald) == 4; + assert breadthFirstSearch.friendHops(felix, harold) == 4; + } +} \ No newline at end of file diff --git a/test/algorithms/graphs/depthFirstSearchTest.java b/test/algorithms/graphs/depthFirstSearchTest.java index 5816ba72..84cef22a 100644 --- a/test/algorithms/graphs/depthFirstSearchTest.java +++ b/test/algorithms/graphs/depthFirstSearchTest.java @@ -3,7 +3,7 @@ import org.junit.Test; import src.algorithms.graphs.depthFirstSearch; -import src.algorithms.graphs.util.TreeNode; +import src.algorithms.graphs.util.BinaryTreeNode; import java.util.ArrayList; import java.util.Arrays; @@ -15,7 +15,7 @@ public class depthFirstSearchTest { public void dfs_preOrderTraversal_shouldReturnAccurate() { // empty tree List firstList = new ArrayList<>(); - TreeNode root1 = null; + BinaryTreeNode root1 = null; List firstResult = depthFirstSearch.preOrder(root1); //standard tree @@ -25,8 +25,8 @@ public void dfs_preOrderTraversal_shouldReturnAccurate() { // / \ // 4 5 List secondList = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); - TreeNode rootRight2 = new TreeNode(3, new TreeNode(4), new TreeNode(5)); - TreeNode root2 = new TreeNode(1, new TreeNode(2), rootRight2); + BinaryTreeNode rootRight2 = new BinaryTreeNode(3, new BinaryTreeNode(4), new BinaryTreeNode(5)); + BinaryTreeNode root2 = new BinaryTreeNode(1, new BinaryTreeNode(2), rootRight2); List secondResult = depthFirstSearch.preOrder(root2); //standard tree 2 @@ -38,8 +38,8 @@ public void dfs_preOrderTraversal_shouldReturnAccurate() { // / / // 4 6 List thirdList = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7)); - TreeNode rootLeft3 = new TreeNode(2, new TreeNode(3, new TreeNode(4), null), new TreeNode(5, new TreeNode(6), null)); - TreeNode root3 = new TreeNode(1, rootLeft3, new TreeNode(7)); + BinaryTreeNode rootLeft3 = new BinaryTreeNode(2, new BinaryTreeNode(3, new BinaryTreeNode(4), null), new BinaryTreeNode(5, new BinaryTreeNode(6), null)); + BinaryTreeNode root3 = new BinaryTreeNode(1, rootLeft3, new BinaryTreeNode(7)); List thirdResult = depthFirstSearch.preOrder(root3); assert firstResult.equals(firstList);