From 20ce6deb85a8fd7c142a7cfc0ebce26b4863d106 Mon Sep 17 00:00:00 2001
From: dev1 <dev1@techeese.net>
Date: Thu, 2 May 2024 09:58:30 +0700
Subject: [PATCH 1/2] Add JUnit for AnagramsInString, BuySellStocks,
 CountAndSay, MajorityElement, SearchInsertPosition

---
 src/test/AnagramsInStringTest.java     | 81 ++++++++++++++++++++++++++
 src/test/BuySellStocksTest.java        | 43 ++++++++++++++
 src/test/CountAndSayTest.java          | 37 ++++++++++++
 src/test/MajorityElementTest.java      | 31 ++++++++++
 src/test/SearchInsertPositionTest.java | 31 ++++++++++
 5 files changed, 223 insertions(+)
 create mode 100644 src/test/AnagramsInStringTest.java
 create mode 100644 src/test/BuySellStocksTest.java
 create mode 100644 src/test/CountAndSayTest.java
 create mode 100644 src/test/MajorityElementTest.java
 create mode 100644 src/test/SearchInsertPositionTest.java

diff --git a/src/test/AnagramsInStringTest.java b/src/test/AnagramsInStringTest.java
new file mode 100644
index 00000000..58caadb6
--- /dev/null
+++ b/src/test/AnagramsInStringTest.java
@@ -0,0 +1,81 @@
+package com.leetcode.arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.Test;
+import java.util.Arrays;
+import java.util.List;
+
+public class AnagramsInStringTest {
+
+    @Test
+    public void testFindAllAnagramsInTextWithMultipleAnagrams() {
+        String text = "cbaebabacd";
+        String pattern = "abc";
+        List<Integer> expected = Arrays.asList(0, 6);
+        assertEquals(expected, AnagramsInString.findAllAnagramsInText(text, pattern),
+                "Phải trả về các vị trí bắt đầu của tất cả các anagram của chuỗi mẫu trong văn bản.");
+    }
+
+    @Test
+    public void testFindAllAnagramsInTextWithNoAnagrams() {
+        String text = "abcdefg";
+        String pattern = "xyz";
+        List<Integer> expected = Arrays.asList();
+        assertEquals(expected, AnagramsInString.findAllAnagramsInText(text, pattern),
+                "Phải trả về danh sách trống khi không tìm thấy anagram nào.");
+    }
+
+    @Test
+    public void testFindAllAnagramsInTextWhenPatternLargerThanText() {
+        String text = "ab";
+        String pattern = "abc";
+        List<Integer> expected = Arrays.asList();
+        assertEquals(expected, AnagramsInString.findAllAnagramsInText(text, pattern),
+                "Phải trả về danh sách trống khi chuỗi mẫu lớn hơn văn bản.");
+    }
+
+    @Test
+    public void testFindAllAnagramsInTextWithExactMatch() {
+        String text = "abab";
+        String pattern = "abab";
+        List<Integer> expected = Arrays.asList(0);
+        assertEquals(expected, AnagramsInString.findAllAnagramsInText(text, pattern),
+                "Phải trả về vị trí bắt đầu khi chuỗi mẫu khớp hoàn hảo với văn bản.");
+    }
+
+    @Test
+    public void testFindAllAnagramsInTextNaiveWithMultipleAnagrams() {
+        String text = "cbaebabacd";
+        String pattern = "abc";
+        List<Integer> expected = Arrays.asList(0, 6);
+        assertEquals(expected, AnagramsInString.findAllAnagramsInTextNaive(text, pattern),
+                "Phải trả về các vị trí bắt đầu của tất cả các anagram của chuỗi mẫu trong văn bản.");
+    }
+
+    @Test
+    public void testFindAllAnagramsInTextNaiveWithNoAnagrams() {
+        String text = "abcdefg";
+        String pattern = "xyz";
+        List<Integer> expected = Arrays.asList();
+        assertEquals(expected, AnagramsInString.findAllAnagramsInTextNaive(text, pattern),
+                "Phải trả về danh sách trống khi không tìm thấy anagram nào.");
+    }
+
+    @Test
+    public void testFindAllAnagramsInTextNaiveWhenPatternLargerThanText() {
+        String text = "ab";
+        String pattern = "abc";
+        List<Integer> expected = Arrays.asList();
+        assertEquals(expected, AnagramsInString.findAllAnagramsInTextNaive(text, pattern),
+                "Phải trả về danh sách trống khi chuỗi mẫu lớn hơn văn bản.");
+    }
+
+    @Test
+    public void testFindAllAnagramsInTextNaiveWithExactMatch() {
+        String text = "abab";
+        String pattern = "abab";
+        List<Integer> expected = Arrays.asList(0);
+        assertEquals(expected, AnagramsInString.findAllAnagramsInTextNaive(text, pattern),
+                "Phải trả về vị trí bắt đầu khi chuỗi mẫu khớp hoàn hảo với văn bản.");
+    }
+}
diff --git a/src/test/BuySellStocksTest.java b/src/test/BuySellStocksTest.java
new file mode 100644
index 00000000..48bbe2f9
--- /dev/null
+++ b/src/test/BuySellStocksTest.java
@@ -0,0 +1,43 @@
+package com.leetcode.arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.Test;
+
+public class BuySellStocksTest {
+
+    @Test
+    public void testMaxProfitSimpleCase() {
+        assertEquals(5, BuySellStocks.maxProfit(new int[]{7, 1, 5, 3, 6, 4}), 
+                     "Phải trả về lợi nhuận tối đa là 5");
+    }
+
+    @Test
+    public void testMaxProfitNoTransaction() {
+        assertEquals(0, BuySellStocks.maxProfit(new int[]{7, 6, 4, 3, 1}), 
+                     "Phải trả về lợi nhuận tối đa là 0 khi không có giao dịch nào được thực hiện");
+    }
+
+    @Test
+    public void testMaxProfitWithZeroPrice() {
+        assertEquals(6, BuySellStocks.maxProfit(new int[]{7, 1, 5, 0, 6, 4}), 
+                     "Phải trả về lợi nhuận tối đa là 6 khi có giá là 0");
+    }
+
+    @Test
+    public void testMaxProfitDescendingOrder() {
+        assertEquals(0, BuySellStocks.maxProfit(new int[]{4, 3, 2, 1}), 
+                     "Phải trả về lợi nhuận tối đa là 0 với giá giảm dần");
+    }
+
+    @Test
+    public void testMaxProfitEmptyArray() {
+        assertEquals(0, BuySellStocks.maxProfit(new int[]{}), 
+                     "Phải trả về lợi nhuận tối đa là 0 với mảng trống");
+    }
+
+    @Test
+    public void testMaxProfitSingleElement() {
+        assertEquals(0, BuySellStocks.maxProfit(new int[]{1}), 
+                     "Phải trả về lợi nhuận tối đa là 0 với mảng chỉ một phần tử");
+    }
+}
diff --git a/src/test/CountAndSayTest.java b/src/test/CountAndSayTest.java
new file mode 100644
index 00000000..ddd9f241
--- /dev/null
+++ b/src/test/CountAndSayTest.java
@@ -0,0 +1,37 @@
+package com.leetcode.arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.Test;
+
+public class CountAndSayTest {
+
+    @Test
+    public void testCountAndSayBaseCase() {
+        assertEquals("1", CountAndSay.countAndSay(1),
+                "Phải trả về '1' khi n = 1.");
+    }
+
+    @Test
+    public void testCountAndSaySecond() {
+        assertEquals("11", CountAndSay.countAndSay(2),
+                "Phải trả về '11' khi n = 2.");
+    }
+
+    @Test
+    public void testCountAndSayThird() {
+        assertEquals("21", CountAndSay.countAndSay(3),
+                "Phải trả về '21' khi n = 3.");
+    }
+
+    @Test
+    public void testCountAndSayFourth() {
+        assertEquals("1211", CountAndSay.countAndSay(4),
+                "Phải trả về '1211' khi n = 4.");
+    }
+
+    @Test
+    public void testCountAndSayFifth() {
+        assertEquals("111221", CountAndSay.countAndSay(5),
+                "Phải trả về '111221' khi n = 5.");
+    }
+}
diff --git a/src/test/MajorityElementTest.java b/src/test/MajorityElementTest.java
new file mode 100644
index 00000000..c19bdaea
--- /dev/null
+++ b/src/test/MajorityElementTest.java
@@ -0,0 +1,31 @@
+package com.leetcode.arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.Test;
+
+public class MajorityElementTest {
+
+    @Test
+    public void testMajorityElementSimple() {
+        assertEquals(5, MajorityElement.majorityElement(new int[]{2, 2, 1, 1, 5, 5, 5}),
+                "Phải trả về phần tử đa số 5.");
+    }
+
+    @Test
+    public void testMajorityElementAllSame() {
+        assertEquals(1, MajorityElement.majorityElement(new int[]{1, 1, 1, 1}),
+                "Phải trả về phần tử đa số 1 khi tất cả các phần tử giống nhau.");
+    }
+
+    @Test
+    public void testMajorityElementNoInitialMajor() {
+        assertEquals(3, MajorityElement.majorityElement(new int[]{1, 1, 2, 3, 3, 3, 3}),
+                "Phải trả về phần tử đa số 3 khi phần tử đa số không phải là phần tử đầu tiên.");
+    }
+
+    @Test
+    public void testMajorityElementChangeMajority() {
+        assertEquals(1, MajorityElement.majorityElement(new int[]{3, 1, 3, 1, 1}),
+                "Phải trả về phần tử đa số 1 khi phần tử đa số thay đổi trong quá trình duyệt.");
+    }
+}
diff --git a/src/test/SearchInsertPositionTest.java b/src/test/SearchInsertPositionTest.java
new file mode 100644
index 00000000..aa4577b2
--- /dev/null
+++ b/src/test/SearchInsertPositionTest.java
@@ -0,0 +1,31 @@
+package com.leetcode.arrays;
+
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Test;
+
+class SearchInsertPositionTest {
+    @Test
+    public void testTargetFound() {
+        assertEquals(2, SearchInsertPosition.searchInsert(new int[]{1,3,5,6}, 5));
+    }
+
+    @Test
+    public void testInsertPosition() {
+        assertEquals(1, SearchInsertPosition.searchInsert(new int[]{1,3,5,6}, 2));
+    }
+
+    @Test
+    public void testInsertAtEnd() {
+        assertEquals(4, SearchInsertPosition.searchInsert(new int[]{1,3,5,6}, 7));
+    }
+
+    @Test
+    public void testInsertAtBeginning() {
+        assertEquals(0, SearchInsertPosition.searchInsert(new int[]{1,3,5,6}, 0));
+    }
+
+    @Test
+    public void testEmptyArray() {
+        assertEquals(0, SearchInsertPosition.searchInsert(new int[]{}, 3));
+    }
+}
\ No newline at end of file

From 32299495bfa6854948b2768343f502d0860d06cc Mon Sep 17 00:00:00 2001
From: dev1 <dev1@techeese.net>
Date: Thu, 2 May 2024 10:15:47 +0700
Subject: [PATCH 2/2] Add refactors of code smells for GraphValidTree.java and
 WordLadder.java

---
 .../com/leetcode/graphs/GraphValidTree.java   | 114 ++++++++----------
 .../java/com/leetcode/graphs/WordLadder.java  | 103 ++++++----------
 2 files changed, 87 insertions(+), 130 deletions(-)

diff --git a/src/main/java/com/leetcode/graphs/GraphValidTree.java b/src/main/java/com/leetcode/graphs/GraphValidTree.java
index 15950143..b9fb1e44 100644
--- a/src/main/java/com/leetcode/graphs/GraphValidTree.java
+++ b/src/main/java/com/leetcode/graphs/GraphValidTree.java
@@ -28,95 +28,77 @@
  * @since 2019-08-05
  */
 public class GraphValidTree {
-
+    
     /**
-     *
-     * @param n
-     * @param edges
-     * @return
+     * Checks if the given edges form a valid tree using BFS.
      */
     public static boolean isValidTree(int n, int[][] edges) {
-        List<List<Integer>> adjacencyList = new ArrayList<>(n);
+        List<List<Integer>> adjacencyList = buildAdjacencyList(n, edges);
+        return isTreeBFS(n, adjacencyList);
+    }
 
+    /**
+     * Builds the adjacency list from the given edges.
+     */
+    private static List<List<Integer>> buildAdjacencyList(int n, int[][] edges) {
+        List<List<Integer>> adjacencyList = new ArrayList<>(n);
         for (int i = 0; i < n; i++) {
             adjacencyList.add(new ArrayList<>());
         }
-
-        for (int i = 0; i < edges.length; i++) {
-            adjacencyList.get(edges[i][0]).add(edges[i][1]);
-        }
-
-        boolean[] visited = new boolean[n];
-
-        if (hasCycle(adjacencyList, 0, -1, visited)) {
-            return false;
-        }
-
-        for (int i = 0; i < n; i++) {
-            if (!visited[i]) {
-                return false;
-            }
+        for (int[] edge : edges) {
+            adjacencyList.get(edge[0]).add(edge[1]);
+            adjacencyList.get(edge[1]).add(edge[0]);  // Since the graph is undirected
         }
-
-        return true;
+        return adjacencyList;
     }
 
-    private static boolean hasCycle(List<List<Integer>> adjacencyList, int node1, int exclude, boolean[] visited) {
-        visited[node1] = true;
-
-        for (int i = 0; i < adjacencyList.get(node1).size(); i++) {
-            int node2 = adjacencyList.get(node1).get(i);
-
-            if ((visited[node2] && exclude != node2) || (!visited[node2] && hasCycle(adjacencyList, node2, node1, visited))) {
-                return true;
+    /**
+     * Uses BFS to check for cycles and disconnected components.
+     */
+    private static boolean isTreeBFS(int n, List<List<Integer>> adjacencyList) {
+        Set<Integer> visited = new HashSet<>();
+        Queue<Integer> queue = new LinkedList<>();
+        queue.offer(0);
+        visited.add(0);
+
+        while (!queue.isEmpty()) {
+            int node = queue.poll();
+            for (int neighbor : adjacencyList.get(node)) {
+                if (!visited.add(neighbor)) {  // if 'add' returns false, 'neighbor' is already visited
+                    return false;
+                }
+                queue.offer(neighbor);
             }
         }
-
-        return false;
+        return visited.size() == n;
     }
 
-
     /**
-     * Union-find algorithm: We keep all connected nodes in one set in the union operation and in find operation we
-     * check whether two nodes belong to the same set. If yes then there's a cycle and if not then no cycle.
-     *
-     * Good articles on union-find:
-     * - https://www.hackerearth.com/practice/notes/disjoint-set-union-union-find/
-     * - https://www.youtube.com/watch?v=wU6udHRIkcc
-     *
-     * @param n
-     * @param edges
-     * @return
+     * Checks if the given edges form a valid tree using the Union-Find algorithm.
      */
     public static boolean isValidTreeUsingUnionFind(int n, int[][] edges) {
-        int[] roots = new int[n];
+        int[] parent = new int[n];
+        Arrays.fill(parent, -1);
 
-        for (int i = 0; i < n; i++) {
-            roots[i] = i;
-        }
+        for (int[] edge : edges) {
+            int x = find(parent, edge[0]);
+            int y = find(parent, edge[1]);
 
-        for (int i = 0; i < edges.length; i++) {
-            // find operation
-            if (roots[edges[i][0]] == roots[edges[i][1]]) {
-                return false;
-            }
-            // union operation
-            roots[edges[i][1]] = findRoot(roots, roots[edges[i][0]]); // note: we can optimize this even further by
-            // considering size of each side and then join the side with smaller size to the one with a larger size (weighted union).
-            // We can use another array called size to keep count of the size or we can use the same root array with
-            // negative values, i.e, negative resembles that the node is pointing to itself and the number will represent
-            // the size. For example, roots = [-2, -1, -1, 0] means that node 3 is pointing to node 0 and node 0 is pointing
-            // to itself and is has 2 nodes under it including itself.
+            if (x == y) return false; // x and y are in the same set
+
+            // Union operation
+            parent[y] = x;
         }
 
-        return edges.length == n - 1;
+        return edges.length == n - 1; // Tree should have exactly n-1 edges
     }
 
-    private static int findRoot(int[] roots, int node) {
-        while (roots[node] != node) {
-            node = roots[node];
-        }
-        return node;
+    /**
+     * Finds the root of the node 'i' using path compression.
+     */
+    private static int find(int[] parent, int i) {
+        if (parent[i] == -1) return i;
+        return find(parent, parent[i]);
     }
 
     public static void main(String[] args) {
diff --git a/src/main/java/com/leetcode/graphs/WordLadder.java b/src/main/java/com/leetcode/graphs/WordLadder.java
index 61e706ce..c10b020f 100644
--- a/src/main/java/com/leetcode/graphs/WordLadder.java
+++ b/src/main/java/com/leetcode/graphs/WordLadder.java
@@ -1,119 +1,94 @@
 package com.leetcode.graphs;
 
-
 import javafx.util.Pair;
-
 import java.util.*;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
 /**
  * Level: Medium
  * Link: https://leetcode.com/problems/word-ladder/
  * Description:
  * Given two words (beginWord and endWord), and a dictionary's word list, find the length of shortest transformation
  * sequence from beginWord to endWord, such that:
- * <p>
+ *
  * Only one letter can be changed at a time. Each transformed word must exist in the word list. Note that beginWord
  * is not a transformed word.
- * <p>
+ *
  * Note:
  * - Return 0 if there is no such transformation sequence.
  * - All words have the same length.
  * - All words contain only lowercase alphabetic characters.
  * - You may assume no duplicates in the word list.
  * - You may assume beginWord and endWord are non-empty and are not the same.
- * <p>
+ *
  * Example 1:
  * Input:
  * beginWord = "hit",
  * endWord = "cog",
  * wordList = ["hot","dot","dog","lot","log","cog"]
- * <p>
+ *
  * Output: 5
- * <p>
+ *
  * Explanation: As one shortest transformation is "hit" -> "hot" -> "dot" -> "dog" -> "cog",
  * return its length 5.
- * <p>
+ *
  * Example 2:
  * Input:
  * beginWord = "hit"
  * endWord = "cog"
  * wordList = ["hot","dot","dog","lot","log"]
- * <p>
+ *
  * Output: 0
- * <p>
- * Explanation: The endWord "cog" is not in wordList, therefore no possible transformation.
  *
- * @author rampatra
- * @since 2019-08-15
+ * Explanation: The endWord "cog" is not in wordList, therefore no possible transformation.
  */
 public class WordLadder {
 
-    /**
-     * Runtime: <a href="https://leetcode.com/submissions/detail/251960230/">79 ms</a>.
-     *
-     * @param beginWord
-     * @param endWord
-     * @param wordList
-     * @return
-     */
+    private static final char WILDCARD_CHAR = '*';
+
     public static int ladderLength(String beginWord, String endWord, List<String> wordList) {
-        int L = beginWord.length();
-        Map<String, Set<String>> transformedToOriginalWordMap = new HashMap<>();
-        Queue<Pair<String, Integer>> queue = new LinkedList<>();
+        Map<String, Set<String>> allComboDict = preprocessWords(wordList);
+        return performBFS(beginWord, endWord, allComboDict);
+    }
 
+    private static Map<String, Set<String>> preprocessWords(List<String> wordList) {
+        Map<String, Set<String>> allComboDict = new HashMap<>();
+        int L = wordList.get(0).length();
         wordList.forEach(word -> {
-                    String transformedWord;
-                    for (int i = 0; i < L; i++) {
-                        transformedWord = word.substring(0, i) + "*" + word.substring(i + 1, L);
-                        transformedToOriginalWordMap.putIfAbsent(transformedWord, new HashSet<>());
-                        transformedToOriginalWordMap.get(transformedWord).add(word);
-                    }
-                }
-        );
+            for (int i = 0; i < L; i++) {
+                String newWord = word.substring(0, i) + WILDCARD_CHAR + word.substring(i + 1, L);
+                allComboDict.computeIfAbsent(newWord, k -> new HashSet<>()).add(word);
+            }
+        });
+        return allComboDict;
+    }
 
-        Set<String> visited = new HashSet<>();
+    private static int performBFS(String beginWord, String endWord, Map<String, Set<String>> allComboDict) {
+        Queue<Pair<String, Integer>> queue = new LinkedList<>();
         queue.add(new Pair<>(beginWord, 1));
+
+        Set<String> visited = new HashSet<>();
         visited.add(beginWord);
 
         while (!queue.isEmpty()) {
-            Pair<String, Integer> currPair = queue.poll();
-            String word = currPair.getKey();
-            Integer level = currPair.getValue();
-
-            if (word.equals(endWord)) {
-                return level;
-            }
-
-            String transformedWord;
-            for (int i = 0; i < L; i++) {
-                transformedWord = word.substring(0, i) + "*" + word.substring(i + 1, L);
-
-                for (String originalWord : transformedToOriginalWordMap.getOrDefault(transformedWord, Collections.emptySet())) {
-                    if (!visited.contains(originalWord)) {
-                        queue.add(new Pair<>(originalWord, level + 1));
-                        visited.add(originalWord);
+            Pair<String, Integer> node = queue.remove();
+            String word = node.getKey();
+            int level = node.getValue();
+            for (int i = 0; i < word.length(); i++) {
+                String newWord = word.substring(0, i) + WILDCARD_CHAR + word.substring(i + 1);
+                for (String adjacentWord : allComboDict.getOrDefault(newWord, new HashSet<>())) {
+                    if (adjacentWord.equals(endWord)) {
+                        return level + 1;
+                    }
+                    if (!visited.contains(adjacentWord)) {
+                        visited.add(adjacentWord);
+                        queue.add(new Pair<>(adjacentWord, level + 1));
                     }
                 }
             }
         }
-
         return 0;
     }
 
-    /**
-     * TODO: Optimized both end BFS solution
-     *
-     * @param beginWord
-     * @param endWord
-     * @param wordList
-     * @return
-     */
-    public static int ladderLengthOptimized(String beginWord, String endWord, List<String> wordList) {
-        return -1;
-    }
-
     public static void main(String[] args) {
         assertEquals(5, ladderLength("hit", "cog", Arrays.asList("hot", "dot", "dog", "lot", "log", "cog")));
         assertEquals(0, ladderLength("hit", "cog", Arrays.asList("hot", "dot", "dog", "lot", "log")));