Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions src/main/java/com/thealgorithms/strings/Isogram.java
Original file line number Diff line number Diff line change
@@ -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<Character> uniqueChars = new HashSet<>();
for (char ch : str.toCharArray()) {
uniqueChars.add(ch);
}
return uniqueChars.size() == str.length();
}
}
117 changes: 117 additions & 0 deletions src/test/java/com/thealgorithms/strings/IsogramTest.java
Original file line number Diff line number Diff line change
@@ -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<IsogramTestCase> 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<IsogramTestCase> 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("!@#$%^&*()"));
}
}