diff --git a/src/main/java/com/thealgorithms/compression/LZ77.java b/src/main/java/com/thealgorithms/compression/LZ77.java
new file mode 100644
index 000000000000..d02307aa57b5
--- /dev/null
+++ b/src/main/java/com/thealgorithms/compression/LZ77.java
@@ -0,0 +1,168 @@
+package com.thealgorithms.compression;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An implementation of the Lempel-Ziv 77 (LZ77) compression algorithm.
+ *
+ * LZ77 is a lossless data compression algorithm that works by finding repeated
+ * occurrences of data in a sliding window. It replaces subsequent occurrences
+ * with references (offset, length) to the first occurrence within the window.
+ *
+ *
+ * This implementation uses a simple sliding window and lookahead buffer approach.
+ * Output format is a sequence of tuples (offset, length, next_character).
+ *
+ *
+ * Time Complexity: O(n*W) in this naive implementation, where n is the input length
+ * and W is the window size, due to the search for the longest match. More advanced
+ * data structures (like suffix trees) can improve this.
+ *
+ *
+ * References:
+ *
+ *
+ */
+public final class LZ77 {
+
+ private static final int DEFAULT_WINDOW_SIZE = 4096;
+ private static final int DEFAULT_LOOKAHEAD_BUFFER_SIZE = 16;
+ private static final char END_OF_STREAM = '\u0000';
+ private LZ77() {
+ }
+
+ /**
+ * Represents a token in the LZ77 compressed output.
+ * Stores the offset back into the window, the length of the match,
+ * and the next character after the match (or END_OF_STREAM if at end).
+ */
+ public record Token(int offset, int length, char nextChar) {
+ }
+
+ /**
+ * Compresses the input text using the LZ77 algorithm.
+ *
+ * @param text The input string to compress. Must not be null.
+ * @param windowSize The size of the sliding window (search buffer). Must be positive.
+ * @param lookaheadBufferSize The size of the lookahead buffer. Must be positive.
+ * @return A list of {@link Token} objects representing the compressed data.
+ * @throws IllegalArgumentException if windowSize or lookaheadBufferSize are not positive.
+ */
+ public static List compress(String text, int windowSize, int lookaheadBufferSize) {
+ if (text == null) {
+ return new ArrayList<>();
+ }
+ if (windowSize <= 0 || lookaheadBufferSize <= 0) {
+ throw new IllegalArgumentException("Window size and lookahead buffer size must be positive.");
+ }
+
+ List compressedOutput = new ArrayList<>();
+ int currentPosition = 0;
+
+ while (currentPosition < text.length()) {
+ int bestMatchDistance = 0;
+ int bestMatchLength = 0;
+
+ // Define the start of the search window
+ int searchBufferStart = Math.max(0, currentPosition - windowSize);
+ // Define the end of the lookahead buffer (don't go past text length)
+ int lookaheadEnd = Math.min(currentPosition + lookaheadBufferSize, text.length());
+
+ // Search for the longest match in the window
+ for (int i = searchBufferStart; i < currentPosition; i++) {
+ int currentMatchLength = 0;
+
+ // Check how far the match extends into the lookahead buffer
+ // This allows for overlapping matches (e.g., "aaa" can match with offset 1)
+ while (currentPosition + currentMatchLength < lookaheadEnd) {
+ int sourceIndex = i + currentMatchLength;
+
+ // Handle overlapping matches (run-length encoding within LZ77)
+ // When we've matched beyond our starting position, wrap around using modulo
+ if (sourceIndex >= currentPosition) {
+ int offset = currentPosition - i;
+ sourceIndex = i + (currentMatchLength % offset);
+ }
+
+ if (text.charAt(sourceIndex) == text.charAt(currentPosition + currentMatchLength)) {
+ currentMatchLength++;
+ } else {
+ break;
+ }
+ }
+
+ // If this match is longer than the best found so far
+ if (currentMatchLength > bestMatchLength) {
+ bestMatchLength = currentMatchLength;
+ bestMatchDistance = currentPosition - i; // Calculate offset from current position
+ }
+ }
+
+ char nextChar;
+ if (currentPosition + bestMatchLength < text.length()) {
+ nextChar = text.charAt(currentPosition + bestMatchLength);
+ } else {
+ nextChar = END_OF_STREAM;
+ }
+
+ // Add the token to the output
+ compressedOutput.add(new Token(bestMatchDistance, bestMatchLength, nextChar));
+
+ // Move the current position forward
+ // If we're at the end and had a match, just move by the match length
+ if (nextChar == END_OF_STREAM) {
+ currentPosition += bestMatchLength;
+ } else {
+ currentPosition += bestMatchLength + 1;
+ }
+ }
+
+ return compressedOutput;
+ }
+
+ /**
+ * Compresses the input text using the LZ77 algorithm with default buffer sizes.
+ *
+ * @param text The input string to compress. Must not be null.
+ * @return A list of {@link Token} objects representing the compressed data.
+ */
+ public static List compress(String text) {
+ return compress(text, DEFAULT_WINDOW_SIZE, DEFAULT_LOOKAHEAD_BUFFER_SIZE);
+ }
+
+ /**
+ * Decompresses a list of LZ77 tokens back into the original string.
+ *
+ * @param compressedData The list of {@link Token} objects. Must not be null.
+ * @return The original, uncompressed string.
+ */
+ public static String decompress(List compressedData) {
+ if (compressedData == null) {
+ return "";
+ }
+
+ StringBuilder decompressedText = new StringBuilder();
+
+ for (Token token : compressedData) {
+ // Copy matched characters from the sliding window
+ if (token.length > 0) {
+ int startIndex = decompressedText.length() - token.offset;
+
+ // Handle overlapping matches (e.g., when length > offset)
+ for (int i = 0; i < token.length; i++) {
+ decompressedText.append(decompressedText.charAt(startIndex + i));
+ }
+ }
+
+ // Append the next character (if not END_OF_STREAM)
+ if (token.nextChar != END_OF_STREAM) {
+ decompressedText.append(token.nextChar);
+ }
+ }
+
+ return decompressedText.toString();
+ }
+}
diff --git a/src/main/java/com/thealgorithms/compression/LZ78.java b/src/main/java/com/thealgorithms/compression/LZ78.java
new file mode 100644
index 000000000000..904c379cc2a2
--- /dev/null
+++ b/src/main/java/com/thealgorithms/compression/LZ78.java
@@ -0,0 +1,136 @@
+package com.thealgorithms.compression;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An implementation of the Lempel-Ziv 78 (LZ78) compression algorithm.
+ *
+ * LZ78 is a dictionary-based lossless data compression algorithm. It processes
+ * input data sequentially, building a dictionary of phrases encountered so far.
+ * It outputs pairs (dictionary_index, next_character), representing
+ * the longest match found in the dictionary plus the character that follows it.
+ *
+ *
+ * This implementation builds the dictionary dynamically during compression.
+ * The dictionary index 0 represents the empty string (no prefix).
+ *
+ *
+ * Time Complexity: O(n) on average for compression and decompression, assuming
+ * efficient dictionary lookups (using a HashMap), where n is the
+ * length of the input string.
+ *
+ *
+ * References:
+ *
+ *
+ */
+public final class LZ78 {
+
+ /**
+ * Special character used to mark end of stream when needed.
+ */
+ private static final char END_OF_STREAM = '\u0000';
+
+ /**
+ * Private constructor to prevent instantiation of this utility class.
+ */
+ private LZ78() {
+ }
+
+ /**
+ * Represents a token in the LZ78 compressed output.
+ * Stores the index of the matching prefix in the dictionary and the next character.
+ * Index 0 represents the empty string (no prefix).
+ */
+ public record Token(int index, char nextChar) {
+ }
+
+ /**
+ * A node in the dictionary trie structure.
+ * Each node represents a phrase and can have child nodes for extended phrases.
+ */
+ private static final class TrieNode {
+ Map children = new HashMap<>();
+ int index = -1; // -1 means not assigned yet
+ }
+
+ /**
+ * Compresses the input text using the LZ78 algorithm.
+ *
+ * @param text The input string to compress. Must not be null.
+ * @return A list of {@link Token} objects representing the compressed data.
+ */
+ public static List compress(String text) {
+ if (text == null || text.isEmpty()) {
+ return new ArrayList<>();
+ }
+
+ List compressedOutput = new ArrayList<>();
+ TrieNode root = new TrieNode();
+ int nextDictionaryIndex = 1;
+
+ TrieNode currentNode = root;
+ int lastMatchedIndex = 0;
+
+ for (int i = 0; i < text.length(); i++) {
+ char currentChar = text.charAt(i);
+
+ if (currentNode.children.containsKey(currentChar)) {
+ currentNode = currentNode.children.get(currentChar);
+ lastMatchedIndex = currentNode.index;
+ } else {
+ // Output: (index of longest matching prefix, current character)
+ compressedOutput.add(new Token(lastMatchedIndex, currentChar));
+
+ TrieNode newNode = new TrieNode();
+ newNode.index = nextDictionaryIndex++;
+ currentNode.children.put(currentChar, newNode);
+
+ currentNode = root;
+ lastMatchedIndex = 0;
+ }
+ }
+
+ // Handle remaining phrase at end of input
+ if (currentNode != root) {
+ compressedOutput.add(new Token(lastMatchedIndex, END_OF_STREAM));
+ }
+
+ return compressedOutput;
+ }
+
+ /**
+ * Decompresses a list of LZ78 tokens back into the original string.
+ *
+ * @param compressedData The list of {@link Token} objects. Must not be null.
+ * @return The original, uncompressed string.
+ */
+ public static String decompress(List compressedData) {
+ if (compressedData == null || compressedData.isEmpty()) {
+ return "";
+ }
+
+ StringBuilder decompressedText = new StringBuilder();
+ Map dictionary = new HashMap<>();
+ int nextDictionaryIndex = 1;
+
+ for (Token token : compressedData) {
+ String prefix = (token.index == 0) ? "" : dictionary.get(token.index);
+
+ if (token.nextChar == END_OF_STREAM) {
+ decompressedText.append(prefix);
+ } else {
+ String currentPhrase = prefix + token.nextChar;
+ decompressedText.append(currentPhrase);
+ dictionary.put(nextDictionaryIndex++, currentPhrase);
+ }
+ }
+
+ return decompressedText.toString();
+ }
+}
diff --git a/src/test/java/com/thealgorithms/compression/LZ77Test.java b/src/test/java/com/thealgorithms/compression/LZ77Test.java
new file mode 100644
index 000000000000..86732d48a54a
--- /dev/null
+++ b/src/test/java/com/thealgorithms/compression/LZ77Test.java
@@ -0,0 +1,223 @@
+package com.thealgorithms.compression;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class LZ77Test {
+
+ @Test
+ @DisplayName("Test compression and decompression of a simple repeating string")
+ void testSimpleRepeatingString() {
+ String original = "ababcbababaa";
+ List compressed = LZ77.compress(original, 10, 4);
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test compression and decompression of a string with no repeats initially")
+ void testNoInitialRepeats() {
+ String original = "abcdefgh";
+ List compressed = LZ77.compress(original);
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test compression and decompression of a longer example")
+ void testLongerExample() {
+ String original = "TOBEORNOTTOBEORTOBEORNOT";
+ List compressed = LZ77.compress(original, 20, 10);
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test empty string compression and decompression")
+ void testEmptyString() {
+ String original = "";
+ List compressed = LZ77.compress(original);
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+ assertTrue(compressed.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Test null string compression")
+ void testNullStringCompress() {
+ List compressed = LZ77.compress(null);
+ assertTrue(compressed.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Test null list decompression")
+ void testNullListDecompress() {
+ String decompressed = LZ77.decompress(null);
+ assertEquals("", decompressed);
+ }
+
+ @Test
+ @DisplayName("Test invalid buffer sizes throw exception")
+ void testInvalidBufferSizes() {
+ assertThrows(IllegalArgumentException.class, () -> LZ77.compress("test", 0, 5));
+ assertThrows(IllegalArgumentException.class, () -> LZ77.compress("test", 5, 0));
+ assertThrows(IllegalArgumentException.class, () -> LZ77.compress("test", -1, 5));
+ assertThrows(IllegalArgumentException.class, () -> LZ77.compress("test", 5, -1));
+ }
+
+ @Test
+ @DisplayName("Test string with all same characters")
+ void testAllSameCharacters() {
+ String original = "AAAAAA";
+ List compressed = LZ77.compress(original, 10, 5);
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+
+ // Should achieve good compression for repeated characters
+ assertTrue(compressed.size() < original.length());
+ }
+
+ @Test
+ @DisplayName("Test string with all unique characters")
+ void testAllUniqueCharacters() {
+ String original = "abcdefghijklmnop";
+ List compressed = LZ77.compress(original, 10, 5);
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+
+ // No compression expected for unique characters
+ assertEquals(original.length(), compressed.size());
+ }
+
+ @Test
+ @DisplayName("Test single character string")
+ void testSingleCharacter() {
+ String original = "a";
+ List compressed = LZ77.compress(original);
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+ assertEquals(1, compressed.size());
+ }
+
+ @Test
+ @DisplayName("Test match that goes exactly to the end")
+ void testMatchToEnd() {
+ String original = "abcabc";
+ List compressed = LZ77.compress(original, 10, 10);
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test with very small window size")
+ void testSmallWindowSize() {
+ String original = "ababababab";
+ List compressed = LZ77.compress(original, 2, 4);
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test with very small lookahead buffer")
+ void testSmallLookaheadBuffer() {
+ String original = "ababcbababaa";
+ List compressed = LZ77.compress(original, 10, 2);
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test repeating pattern at the end")
+ void testRepeatingPatternAtEnd() {
+ String original = "xyzabcabcabcabc";
+ List compressed = LZ77.compress(original, 15, 8);
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test overlapping matches (run-length encoding case)")
+ void testOverlappingMatches() {
+ String original = "aaaaaa";
+ List compressed = LZ77.compress(original, 10, 10);
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test complex pattern with multiple repeats")
+ void testComplexPattern() {
+ String original = "abcabcabcxyzxyzxyz";
+ List compressed = LZ77.compress(original, 20, 10);
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test with special characters")
+ void testSpecialCharacters() {
+ String original = "hello world! @#$%^&*()";
+ List compressed = LZ77.compress(original);
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test with numbers")
+ void testWithNumbers() {
+ String original = "1234567890123456";
+ List compressed = LZ77.compress(original, 15, 8);
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test long repeating sequence")
+ void testLongRepeatingSequence() {
+ String original = "abcdefgh".repeat(10);
+ List compressed = LZ77.compress(original, 50, 20);
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+
+ // Should achieve significant compression
+ assertTrue(compressed.size() < original.length() / 2);
+ }
+
+ @Test
+ @DisplayName("Test compression effectiveness")
+ void testCompressionEffectiveness() {
+ String original = "ababababababab";
+ List compressed = LZ77.compress(original, 20, 10);
+
+ // Verify that compression actually reduces the data size
+ // Each token represents potentially multiple characters
+ assertTrue(compressed.size() <= original.length());
+
+ // Verify decompression
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test with mixed case letters")
+ void testMixedCase() {
+ String original = "AaBbCcAaBbCc";
+ List compressed = LZ77.compress(original);
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test default parameters")
+ void testDefaultParameters() {
+ String original = "This is a test string with some repeated patterns. This is repeated.";
+ List compressed = LZ77.compress(original);
+ String decompressed = LZ77.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+}
diff --git a/src/test/java/com/thealgorithms/compression/LZ78Test.java b/src/test/java/com/thealgorithms/compression/LZ78Test.java
new file mode 100644
index 000000000000..7889b50b76f3
--- /dev/null
+++ b/src/test/java/com/thealgorithms/compression/LZ78Test.java
@@ -0,0 +1,295 @@
+package com.thealgorithms.compression;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class LZ78Test {
+
+ @Test
+ @DisplayName("Test compression and decompression of a simple repeating string")
+ void testSimpleRepeatingString() {
+ String original = "ababcbababaa";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test compression and decompression example ABAABABAABAB")
+ void testStandardExample() {
+ String original = "ABAABABAABAB";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+
+ // Verify the compression produces expected tokens
+ // Expected: (0,A)(0,B)(1,A)(2,B)(3,A)(4,B)
+ // Where dictionary builds as: 1:A, 2:B, 3:AA, 4:BA, 5:ABA, 6:BAB
+ assertEquals(6, compressed.size());
+ assertEquals(0, compressed.get(0).index());
+ assertEquals('A', compressed.get(0).nextChar());
+ assertEquals(0, compressed.get(1).index());
+ assertEquals('B', compressed.get(1).nextChar());
+ assertEquals(1, compressed.get(2).index());
+ assertEquals('A', compressed.get(2).nextChar());
+ }
+
+ @Test
+ @DisplayName("Test compression and decompression of a longer example")
+ void testLongerExample() {
+ String original = "TOBEORNOTTOBEORTOBEORNOT";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test empty string compression and decompression")
+ void testEmptyString() {
+ String original = "";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+ assertTrue(compressed.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Test null string compression")
+ void testNullStringCompress() {
+ List compressed = LZ78.compress(null);
+ assertTrue(compressed.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Test null list decompression")
+ void testNullListDecompress() {
+ String decompressed = LZ78.decompress(null);
+ assertEquals("", decompressed);
+ }
+
+ @Test
+ @DisplayName("Test string with all same characters")
+ void testAllSameCharacters() {
+ String original = "AAAAAA";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+
+ // Should achieve good compression: (0,A)(1,A)(2,A)...
+ assertTrue(compressed.size() <= 4); // Builds: A, AA, AAA, etc.
+ }
+
+ @Test
+ @DisplayName("Test string with all unique characters")
+ void testAllUniqueCharacters() {
+ String original = "abcdefg";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+
+ // No compression for unique characters
+ assertEquals(original.length(), compressed.size());
+
+ // Each token should have index 0 (empty prefix)
+ for (LZ78.Token token : compressed) {
+ assertEquals(0, token.index());
+ }
+ }
+
+ @Test
+ @DisplayName("Test single character string")
+ void testSingleCharacter() {
+ String original = "a";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+ assertEquals(1, compressed.size());
+ assertEquals(0, compressed.getFirst().index());
+ assertEquals('a', compressed.getFirst().nextChar());
+ }
+
+ @Test
+ @DisplayName("Test two character string")
+ void testTwoCharacters() {
+ String original = "ab";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+ assertEquals(2, compressed.size());
+ }
+
+ @Test
+ @DisplayName("Test repeating pairs")
+ void testRepeatingPairs() {
+ String original = "ababab";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+
+ // Should compress well: (0,a)(0,b)(1,b) or similar
+ assertTrue(compressed.size() < original.length());
+ }
+
+ @Test
+ @DisplayName("Test growing patterns")
+ void testGrowingPatterns() {
+ String original = "abcabcdabcde";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test dictionary building correctness")
+ void testDictionaryBuilding() {
+ String original = "aabaabaab";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+
+ // Verify first few tokens
+ // Expected pattern: (0,a)(1,b)(2,a)(3,b) building dictionary 1:a, 2:ab, 3:aa, 4:aab
+ assertTrue(compressed.size() > 0);
+ assertEquals(0, compressed.getFirst().index()); // First char always has index 0
+ }
+
+ @Test
+ @DisplayName("Test with special characters")
+ void testSpecialCharacters() {
+ String original = "hello world! hello!";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test with numbers")
+ void testWithNumbers() {
+ String original = "1234512345";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+
+ // Should achieve compression
+ assertTrue(compressed.size() < original.length());
+ }
+
+ @Test
+ @DisplayName("Test long repeating sequence")
+ void testLongRepeatingSequence() {
+ String original = "abcdefgh".repeat(5);
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+
+ // LZ78 should achieve some compression for repeating sequences
+ assertTrue(compressed.size() < original.length(), "Compressed size should be less than original length");
+ }
+
+ @Test
+ @DisplayName("Test alternating characters")
+ void testAlternatingCharacters() {
+ String original = "ababababab";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test compression effectiveness")
+ void testCompressionEffectiveness() {
+ String original = "the quick brown fox jumps over the lazy dog the quick brown fox";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+
+ // Should achieve some compression due to repeated phrases
+ assertTrue(compressed.size() < original.length());
+ }
+
+ @Test
+ @DisplayName("Test with mixed case letters")
+ void testMixedCase() {
+ String original = "AaBbCcAaBbCc";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test palindrome string")
+ void testPalindrome() {
+ String original = "abccba";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test highly compressible pattern")
+ void testHighlyCompressible() {
+ String original = "aaaaaaaaaa";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+
+ // Should achieve excellent compression ratio
+ assertTrue(compressed.size() <= 4);
+ }
+
+ @Test
+ @DisplayName("Test empty list decompression")
+ void testEmptyListDecompress() {
+ List compressed = List.of();
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals("", decompressed);
+ }
+
+ @Test
+ @DisplayName("Test binary-like pattern")
+ void testBinaryPattern() {
+ String original = "0101010101";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test nested patterns")
+ void testNestedPatterns() {
+ String original = "abcabcdefabcdefghi";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test whitespace handling")
+ void testWhitespace() {
+ String original = "a b c a b c";
+ List compressed = LZ78.compress(original);
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+
+ @Test
+ @DisplayName("Test token structure correctness")
+ void testTokenStructure() {
+ String original = "abc";
+ List compressed = LZ78.compress(original);
+
+ // All tokens should have valid indices (>= 0)
+ for (LZ78.Token token : compressed) {
+ assertTrue(token.index() >= 0);
+ assertNotNull(token.nextChar());
+ }
+
+ String decompressed = LZ78.decompress(compressed);
+ assertEquals(original, decompressed);
+ }
+}