From 20ce6deb85a8fd7c142a7cfc0ebce26b4863d106 Mon Sep 17 00:00:00 2001 From: dev1 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 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 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 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 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 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 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 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 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 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> adjacencyList = new ArrayList<>(n); + List> adjacencyList = buildAdjacencyList(n, edges); + return isTreeBFS(n, adjacencyList); + } + /** + * Builds the adjacency list from the given edges. + */ + private static List> buildAdjacencyList(int n, int[][] edges) { + List> 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> 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> adjacencyList) { + Set visited = new HashSet<>(); + Queue 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: - *

+ * * 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. - *

+ * * 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. - *

+ * * Example 1: * Input: * beginWord = "hit", * endWord = "cog", * wordList = ["hot","dot","dog","lot","log","cog"] - *

+ * * Output: 5 - *

+ * * Explanation: As one shortest transformation is "hit" -> "hot" -> "dot" -> "dog" -> "cog", * return its length 5. - *

+ * * Example 2: * Input: * beginWord = "hit" * endWord = "cog" * wordList = ["hot","dot","dog","lot","log"] - *

+ * * Output: 0 - *

- * 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: 79 ms. - * - * @param beginWord - * @param endWord - * @param wordList - * @return - */ + private static final char WILDCARD_CHAR = '*'; + public static int ladderLength(String beginWord, String endWord, List wordList) { - int L = beginWord.length(); - Map> transformedToOriginalWordMap = new HashMap<>(); - Queue> queue = new LinkedList<>(); + Map> allComboDict = preprocessWords(wordList); + return performBFS(beginWord, endWord, allComboDict); + } + private static Map> preprocessWords(List wordList) { + Map> 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 visited = new HashSet<>(); + private static int performBFS(String beginWord, String endWord, Map> allComboDict) { + Queue> queue = new LinkedList<>(); queue.add(new Pair<>(beginWord, 1)); + + Set visited = new HashSet<>(); visited.add(beginWord); while (!queue.isEmpty()) { - Pair 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 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 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")));