diff --git a/src/main/java/com/thealgorithms/strings/Isogram.java b/src/main/java/com/thealgorithms/strings/Isogram.java new file mode 100644 index 000000000000..b37f085b075d --- /dev/null +++ b/src/main/java/com/thealgorithms/strings/Isogram.java @@ -0,0 +1,92 @@ +package com.thealgorithms.strings; + +import java.util.HashSet; +import java.util.Set; + +/** + * An isogram (also called heterogram or nonpattern word) is a word in which no + * letter of the word occurs more than once. Each character appears exactly + * once. + * + * For example, the word "uncopyrightable" is the longest common English isogram + * with 15 unique letters. Other examples include "dermatoglyphics" (15 + * letters), + * "background" (10 letters), "python" (6 letters), and "keyboard" (8 letters). + * But words like "hello" and "programming" are not isograms because some + * letters + * appear multiple times ('l' appears twice in "hello", while 'r', 'm', 'g' + * repeat + * in "programming"). + * + * Isograms are particularly valuable in creating substitution ciphers and are + * studied in recreational linguistics. A perfect pangram, which uses all 26 + * letters + * of the alphabet exactly once, is a special type of isogram. + * + * Reference from https://en.wikipedia.org/wiki/Heterogram_(literature)#Isograms + */ +public final class Isogram { + /** + * Private constructor to prevent instantiation of utility class. + */ + private Isogram() { + } + + /** + * Checks if a string is an isogram using boolean array approach. + * + * Time Complexity: O(n) + * Space Complexity: O(1) + * + * @param str the input string + * @return true if the string is an isogram, false otherwise + * @throws IllegalArgumentException if the string contains non-alphabetic + * characters + */ + public static boolean isAlphabeticIsogram(String str) { + if (str == null || str.isEmpty()) { + return true; + } + + str = str.toLowerCase(); + + for (int i = 0; i < str.length(); i++) { + char ch = str.charAt(i); + if (ch < 'a' || ch > 'z') { + throw new IllegalArgumentException("Input contains non-alphabetic character: '" + ch + "'"); + } + } + + boolean[] seenChars = new boolean[26]; + for (int i = 0; i < str.length(); i++) { + char ch = str.charAt(i); + int index = ch - 'a'; + if (seenChars[index]) { + return false; + } + seenChars[index] = true; + } + return true; + } + + /** + * Checks if a string is an isogram using length comparison approach. + * Time Complexity: O(n) + * Space Complexity: O(k) where k is the number of unique characters + * + * @param str the input string + * @return true if the string is an isogram, false otherwise + */ + public static boolean isFullIsogram(String str) { + if (str == null || str.isEmpty()) { + return true; + } + str = str.toLowerCase(); + + Set uniqueChars = new HashSet<>(); + for (char ch : str.toCharArray()) { + uniqueChars.add(ch); + } + return uniqueChars.size() == str.length(); + } +} diff --git a/src/test/java/com/thealgorithms/strings/IsogramTest.java b/src/test/java/com/thealgorithms/strings/IsogramTest.java new file mode 100644 index 000000000000..9dd844dc7cf9 --- /dev/null +++ b/src/test/java/com/thealgorithms/strings/IsogramTest.java @@ -0,0 +1,117 @@ +package com.thealgorithms.strings; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class IsogramTest { + + record IsogramTestCase(String input, boolean expected) { + } + + private static Stream isAlphabeticIsogram() { + return Stream.of( + // Valid isograms (only checks letters) + new IsogramTestCase("uncopyrightable", true), new IsogramTestCase("dermatoglyphics", true), new IsogramTestCase("background", true), new IsogramTestCase("python", true), new IsogramTestCase("keyboard", true), new IsogramTestCase("clipboard", true), new IsogramTestCase("flowchart", true), + new IsogramTestCase("bankruptcy", true), new IsogramTestCase("computer", true), new IsogramTestCase("algorithms", true), + + // Not isograms - letters repeat + new IsogramTestCase("hello", false), new IsogramTestCase("programming", false), new IsogramTestCase("java", false), new IsogramTestCase("coffee", false), new IsogramTestCase("book", false), new IsogramTestCase("letter", false), new IsogramTestCase("mississippi", false), + new IsogramTestCase("google", false), + + // Edge cases + new IsogramTestCase("", true), new IsogramTestCase("a", true), new IsogramTestCase("ab", true), new IsogramTestCase("abc", true), new IsogramTestCase("aa", false), new IsogramTestCase("abcdefghijklmnopqrstuvwxyz", true), + + // Case insensitive + new IsogramTestCase("Python", true), new IsogramTestCase("BACKGROUND", true), new IsogramTestCase("Hello", false), new IsogramTestCase("PROGRAMMING", false)); + } + + private static Stream isFullIsogram() { + return Stream.of( + // Valid isograms (checks all characters) + new IsogramTestCase("uncopyrightable", true), new IsogramTestCase("dermatoglyphics", true), new IsogramTestCase("background", true), new IsogramTestCase("python", true), new IsogramTestCase("keyboard", true), new IsogramTestCase("clipboard", true), new IsogramTestCase("flowchart", true), + new IsogramTestCase("bankruptcy", true), new IsogramTestCase("computer", true), new IsogramTestCase("algorithms", true), + + // Not isograms - characters repeat + new IsogramTestCase("hello", false), new IsogramTestCase("programming", false), new IsogramTestCase("java", false), new IsogramTestCase("coffee", false), new IsogramTestCase("book", false), new IsogramTestCase("letter", false), new IsogramTestCase("mississippi", false), + new IsogramTestCase("google", false), + + // Edge cases + new IsogramTestCase("", true), new IsogramTestCase("a", true), new IsogramTestCase("ab", true), new IsogramTestCase("abc", true), new IsogramTestCase("aa", false), new IsogramTestCase("abcdefghijklmnopqrstuvwxyz", true), + + // Case insensitive + new IsogramTestCase("Python", true), new IsogramTestCase("BACKGROUND", true), new IsogramTestCase("Hello", false), new IsogramTestCase("PROGRAMMING", false), + + // Strings with symbols and numbers + new IsogramTestCase("abc@def", true), // all characters unique + new IsogramTestCase("test-case", false), // 't', 's', 'e' repeat + new IsogramTestCase("python123", true), // all characters unique + new IsogramTestCase("hello@123", false), // 'l' repeats + new IsogramTestCase("abc123!@#", true), // all characters unique + new IsogramTestCase("test123test", false), // 't', 'e', 's' repeat + new IsogramTestCase("1234567890", true), // all digits unique + new IsogramTestCase("12321", false), // '1' and '2' repeat + new IsogramTestCase("!@#$%^&*()", true) // all special characters unique + ); + } + + @ParameterizedTest + @MethodSource("isAlphabeticIsogram") + void testIsogramByArray(IsogramTestCase testCase) { + assertEquals(testCase.expected(), Isogram.isAlphabeticIsogram(testCase.input())); + } + + @ParameterizedTest + @MethodSource("isFullIsogram") + void testIsogramByLength(IsogramTestCase testCase) { + assertEquals(testCase.expected(), Isogram.isFullIsogram(testCase.input())); + } + + @Test + void testNullInputByArray() { + assertTrue(Isogram.isAlphabeticIsogram(null)); + } + + @Test + void testNullInputByLength() { + assertTrue(Isogram.isFullIsogram(null)); + } + + @Test + void testEmptyStringByArray() { + assertTrue(Isogram.isAlphabeticIsogram("")); + } + + @Test + void testEmptyStringByLength() { + assertTrue(Isogram.isFullIsogram("")); + } + + @Test + void testAlphabeticIsogramThrowsException() { + // Test that IllegalArgumentException is thrown for non-alphabetic characters + assertThrows(IllegalArgumentException.class, () -> Isogram.isAlphabeticIsogram("1")); + assertThrows(IllegalArgumentException.class, () -> Isogram.isAlphabeticIsogram("@")); + assertThrows(IllegalArgumentException.class, () -> Isogram.isAlphabeticIsogram("python!")); + assertThrows(IllegalArgumentException.class, () -> Isogram.isAlphabeticIsogram("123algorithm")); + assertThrows(IllegalArgumentException.class, () -> Isogram.isAlphabeticIsogram("hello123")); + assertThrows(IllegalArgumentException.class, () -> Isogram.isAlphabeticIsogram("!@@#$%^&*()")); + } + + @Test + void testFullIsogramWithMixedCharacters() { + // Test that full isogram method handles all character types without exceptions + assertTrue(Isogram.isFullIsogram("abc123")); + assertFalse(Isogram.isFullIsogram("test@email")); // 'e' repeats + assertFalse(Isogram.isFullIsogram("hello123")); // 'l' repeats + assertTrue(Isogram.isFullIsogram("1234567890")); + assertFalse(Isogram.isFullIsogram("12321")); // '1' and '2' repeat + assertTrue(Isogram.isFullIsogram("!@#$%^&*()")); + } +}