diff --git a/DIRECTORY.md b/DIRECTORY.md index 9697dbc09869..71d4b7d013cb 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -1,5 +1,7 @@ # Project Structure +## src + - 📁 **main** - 📁 **java** - 📁 **com** @@ -130,6 +132,7 @@ - 📁 **caches** - 📄 [FIFOCache](src/main/java/com/thealgorithms/datastructures/caches/FIFOCache.java) - 📄 [LFUCache](src/main/java/com/thealgorithms/datastructures/caches/LFUCache.java) + - 📄 [LIFOCache](src/main/java/com/thealgorithms/datastructures/caches/LIFOCache.java) - 📄 [LRUCache](src/main/java/com/thealgorithms/datastructures/caches/LRUCache.java) - 📄 [MRUCache](src/main/java/com/thealgorithms/datastructures/caches/MRUCache.java) - 📄 [RRCache](src/main/java/com/thealgorithms/datastructures/caches/RRCache.java) @@ -165,12 +168,10 @@ - 📄 [Kruskal](src/main/java/com/thealgorithms/datastructures/graphs/Kruskal.java) - 📄 [MatrixGraphs](src/main/java/com/thealgorithms/datastructures/graphs/MatrixGraphs.java) - 📄 [PrimMST](src/main/java/com/thealgorithms/datastructures/graphs/PrimMST.java) - - 📄 [README](src/main/java/com/thealgorithms/datastructures/graphs/README.md) - 📄 [TarjansAlgorithm](src/main/java/com/thealgorithms/datastructures/graphs/TarjansAlgorithm.java) - 📄 [UndirectedAdjacencyListGraph](src/main/java/com/thealgorithms/datastructures/graphs/UndirectedAdjacencyListGraph.java) - 📄 [WelshPowell](src/main/java/com/thealgorithms/datastructures/graphs/WelshPowell.java) - 📁 **hashmap** - - 📄 [Readme](src/main/java/com/thealgorithms/datastructures/hashmap/Readme.md) - 📁 **hashing** - 📄 [GenericHashMapUsingArray](src/main/java/com/thealgorithms/datastructures/hashmap/hashing/GenericHashMapUsingArray.java) - 📄 [GenericHashMapUsingArrayList](src/main/java/com/thealgorithms/datastructures/hashmap/hashing/GenericHashMapUsingArrayList.java) @@ -194,7 +195,6 @@ - 📄 [MergeKSortedArrays](src/main/java/com/thealgorithms/datastructures/heaps/MergeKSortedArrays.java) - 📄 [MinHeap](src/main/java/com/thealgorithms/datastructures/heaps/MinHeap.java) - 📄 [MinPriorityQueue](src/main/java/com/thealgorithms/datastructures/heaps/MinPriorityQueue.java) - - 📄 [Readme](src/main/java/com/thealgorithms/datastructures/heaps/Readme.md) - 📁 **lists** - 📄 [CircleLinkedList](src/main/java/com/thealgorithms/datastructures/lists/CircleLinkedList.java) - 📄 [CountSinglyLinkedListRecursion](src/main/java/com/thealgorithms/datastructures/lists/CountSinglyLinkedListRecursion.java) @@ -205,7 +205,6 @@ - 📄 [MergeSortedArrayList](src/main/java/com/thealgorithms/datastructures/lists/MergeSortedArrayList.java) - 📄 [MergeSortedSinglyLinkedList](src/main/java/com/thealgorithms/datastructures/lists/MergeSortedSinglyLinkedList.java) - 📄 [QuickSortLinkedList](src/main/java/com/thealgorithms/datastructures/lists/QuickSortLinkedList.java) - - 📄 [README](src/main/java/com/thealgorithms/datastructures/lists/README.md) - 📄 [RandomNode](src/main/java/com/thealgorithms/datastructures/lists/RandomNode.java) - 📄 [ReverseKGroup](src/main/java/com/thealgorithms/datastructures/lists/ReverseKGroup.java) - 📄 [RotateSinglyLinkedLists](src/main/java/com/thealgorithms/datastructures/lists/RotateSinglyLinkedLists.java) @@ -222,12 +221,10 @@ - 📄 [PriorityQueues](src/main/java/com/thealgorithms/datastructures/queues/PriorityQueues.java) - 📄 [Queue](src/main/java/com/thealgorithms/datastructures/queues/Queue.java) - 📄 [QueueByTwoStacks](src/main/java/com/thealgorithms/datastructures/queues/QueueByTwoStacks.java) - - 📄 [README](src/main/java/com/thealgorithms/datastructures/queues/README.md) - 📄 [SlidingWindowMaximum](src/main/java/com/thealgorithms/datastructures/queues/SlidingWindowMaximum.java) - 📄 [TokenBucket](src/main/java/com/thealgorithms/datastructures/queues/TokenBucket.java) - 📁 **stacks** - 📄 [NodeStack](src/main/java/com/thealgorithms/datastructures/stacks/NodeStack.java) - - 📄 [README](src/main/java/com/thealgorithms/datastructures/stacks/README.md) - 📄 [ReverseStack](src/main/java/com/thealgorithms/datastructures/stacks/ReverseStack.java) - 📄 [Stack](src/main/java/com/thealgorithms/datastructures/stacks/Stack.java) - 📄 [StackArray](src/main/java/com/thealgorithms/datastructures/stacks/StackArray.java) @@ -259,7 +256,6 @@ - 📄 [PreOrderTraversal](src/main/java/com/thealgorithms/datastructures/trees/PreOrderTraversal.java) - 📄 [PrintTopViewofTree](src/main/java/com/thealgorithms/datastructures/trees/PrintTopViewofTree.java) - 📄 [QuadTree](src/main/java/com/thealgorithms/datastructures/trees/QuadTree.java) - - 📄 [README](src/main/java/com/thealgorithms/datastructures/trees/README.md) - 📄 [RedBlackBST](src/main/java/com/thealgorithms/datastructures/trees/RedBlackBST.java) - 📄 [SameTreesCheck](src/main/java/com/thealgorithms/datastructures/trees/SameTreesCheck.java) - 📄 [SegmentTree](src/main/java/com/thealgorithms/datastructures/trees/SegmentTree.java) @@ -308,6 +304,8 @@ - 📄 [KadaneAlgorithm](src/main/java/com/thealgorithms/dynamicprogramming/KadaneAlgorithm.java) - 📄 [Knapsack](src/main/java/com/thealgorithms/dynamicprogramming/Knapsack.java) - 📄 [KnapsackMemoization](src/main/java/com/thealgorithms/dynamicprogramming/KnapsackMemoization.java) + - 📄 [KnapsackZeroOne](src/main/java/com/thealgorithms/dynamicprogramming/KnapsackZeroOne.java) + - 📄 [KnapsackZeroOneTabulation](src/main/java/com/thealgorithms/dynamicprogramming/KnapsackZeroOneTabulation.java) - 📄 [LevenshteinDistance](src/main/java/com/thealgorithms/dynamicprogramming/LevenshteinDistance.java) - 📄 [LongestAlternatingSubsequence](src/main/java/com/thealgorithms/dynamicprogramming/LongestAlternatingSubsequence.java) - 📄 [LongestArithmeticSubsequence](src/main/java/com/thealgorithms/dynamicprogramming/LongestArithmeticSubsequence.java) @@ -493,6 +491,7 @@ - 📄 [Volume](src/main/java/com/thealgorithms/maths/Volume.java) - 📁 **matrix** - 📄 [InverseOfMatrix](src/main/java/com/thealgorithms/matrix/InverseOfMatrix.java) + - 📄 [MatrixMultiplication](src/main/java/com/thealgorithms/matrix/MatrixMultiplication.java) - 📄 [MatrixRank](src/main/java/com/thealgorithms/matrix/MatrixRank.java) - 📄 [MatrixTranspose](src/main/java/com/thealgorithms/matrix/MatrixTranspose.java) - 📄 [MedianOfMatrix](src/main/java/com/thealgorithms/matrix/MedianOfMatrix.java) @@ -677,7 +676,6 @@ - 📄 [SelectionSort](src/main/java/com/thealgorithms/sorts/SelectionSort.java) - 📄 [SelectionSortRecursive](src/main/java/com/thealgorithms/sorts/SelectionSortRecursive.java) - 📄 [ShellSort](src/main/java/com/thealgorithms/sorts/ShellSort.java) - - 📄 [SimpleSort](src/main/java/com/thealgorithms/sorts/SimpleSort.java) - 📄 [SlowSort](src/main/java/com/thealgorithms/sorts/SlowSort.java) - 📄 [SortAlgorithm](src/main/java/com/thealgorithms/sorts/SortAlgorithm.java) - 📄 [SortUtils](src/main/java/com/thealgorithms/sorts/SortUtils.java) @@ -749,7 +747,6 @@ - 📄 [ValidParentheses](src/main/java/com/thealgorithms/strings/ValidParentheses.java) - 📄 [WordLadder](src/main/java/com/thealgorithms/strings/WordLadder.java) - 📁 **zigZagPattern** - - 📄 [README](src/main/java/com/thealgorithms/strings/zigZagPattern/README.md) - 📄 [ZigZagPattern](src/main/java/com/thealgorithms/strings/zigZagPattern/ZigZagPattern.java) - 📁 **tree** - 📄 [HeavyLightDecomposition](src/main/java/com/thealgorithms/tree/HeavyLightDecomposition.java) @@ -875,6 +872,7 @@ - 📁 **caches** - 📄 [FIFOCacheTest](src/test/java/com/thealgorithms/datastructures/caches/FIFOCacheTest.java) - 📄 [LFUCacheTest](src/test/java/com/thealgorithms/datastructures/caches/LFUCacheTest.java) + - 📄 [LIFOCacheTest](src/test/java/com/thealgorithms/datastructures/caches/LIFOCacheTest.java) - 📄 [LRUCacheTest](src/test/java/com/thealgorithms/datastructures/caches/LRUCacheTest.java) - 📄 [MRUCacheTest](src/test/java/com/thealgorithms/datastructures/caches/MRUCacheTest.java) - 📄 [RRCacheTest](src/test/java/com/thealgorithms/datastructures/caches/RRCacheTest.java) @@ -1012,6 +1010,8 @@ - 📄 [KadaneAlgorithmTest](src/test/java/com/thealgorithms/dynamicprogramming/KadaneAlgorithmTest.java) - 📄 [KnapsackMemoizationTest](src/test/java/com/thealgorithms/dynamicprogramming/KnapsackMemoizationTest.java) - 📄 [KnapsackTest](src/test/java/com/thealgorithms/dynamicprogramming/KnapsackTest.java) + - 📄 [KnapsackZeroOneTabulationTest](src/test/java/com/thealgorithms/dynamicprogramming/KnapsackZeroOneTabulationTest.java) + - 📄 [KnapsackZeroOneTest](src/test/java/com/thealgorithms/dynamicprogramming/KnapsackZeroOneTest.java) - 📄 [LevenshteinDistanceTests](src/test/java/com/thealgorithms/dynamicprogramming/LevenshteinDistanceTests.java) - 📄 [LongestAlternatingSubsequenceTest](src/test/java/com/thealgorithms/dynamicprogramming/LongestAlternatingSubsequenceTest.java) - 📄 [LongestArithmeticSubsequenceTest](src/test/java/com/thealgorithms/dynamicprogramming/LongestArithmeticSubsequenceTest.java) @@ -1048,6 +1048,7 @@ - 📄 [GrahamScanTest](src/test/java/com/thealgorithms/geometry/GrahamScanTest.java) - 📄 [MidpointCircleTest](src/test/java/com/thealgorithms/geometry/MidpointCircleTest.java) - 📄 [MidpointEllipseTest](src/test/java/com/thealgorithms/geometry/MidpointEllipseTest.java) + - 📄 [PointTest](src/test/java/com/thealgorithms/geometry/PointTest.java) - 📁 **graph** - 📄 [ConstrainedShortestPathTest](src/test/java/com/thealgorithms/graph/ConstrainedShortestPathTest.java) - 📄 [StronglyConnectedComponentOptimizedTest](src/test/java/com/thealgorithms/graph/StronglyConnectedComponentOptimizedTest.java) @@ -1182,6 +1183,7 @@ - 📄 [PrimeFactorizationTest](src/test/java/com/thealgorithms/maths/prime/PrimeFactorizationTest.java) - 📁 **matrix** - 📄 [InverseOfMatrixTest](src/test/java/com/thealgorithms/matrix/InverseOfMatrixTest.java) + - 📄 [MatrixMultiplicationTest](src/test/java/com/thealgorithms/matrix/MatrixMultiplicationTest.java) - 📄 [MatrixRankTest](src/test/java/com/thealgorithms/matrix/MatrixRankTest.java) - 📄 [MatrixTransposeTest](src/test/java/com/thealgorithms/matrix/MatrixTransposeTest.java) - 📄 [MatrixUtilTest](src/test/java/com/thealgorithms/matrix/MatrixUtilTest.java) @@ -1345,7 +1347,6 @@ - 📄 [SelectionSortRecursiveTest](src/test/java/com/thealgorithms/sorts/SelectionSortRecursiveTest.java) - 📄 [SelectionSortTest](src/test/java/com/thealgorithms/sorts/SelectionSortTest.java) - 📄 [ShellSortTest](src/test/java/com/thealgorithms/sorts/ShellSortTest.java) - - 📄 [SimpleSortTest](src/test/java/com/thealgorithms/sorts/SimpleSortTest.java) - 📄 [SlowSortTest](src/test/java/com/thealgorithms/sorts/SlowSortTest.java) - 📄 [SortUtilsRandomGeneratorTest](src/test/java/com/thealgorithms/sorts/SortUtilsRandomGeneratorTest.java) - 📄 [SortUtilsTest](src/test/java/com/thealgorithms/sorts/SortUtilsTest.java) @@ -1416,4 +1417,4 @@ - 📁 **zigZagPattern** - 📄 [ZigZagPatternTest](src/test/java/com/thealgorithms/strings/zigZagPattern/ZigZagPatternTest.java) - 📁 **tree** - - 📄 [HeavyLightDecompositionTest](src/test/java/com/thealgorithms/tree/HeavyLightDecompositionTest.java) \ No newline at end of file + - 📄 [HeavyLightDecompositionTest](src/test/java/com/thealgorithms/tree/HeavyLightDecompositionTest.java) diff --git a/pom.xml b/pom.xml index f5f1e0be12f7..c8f98d78a2d7 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ UTF-8 21 21 - 3.27.3 + 3.27.4 @@ -20,7 +20,7 @@ org.junit junit-bom - 5.13.3 + 5.13.4 pom import @@ -128,7 +128,7 @@ com.mebigfatguy.fb-contrib fb-contrib - 7.6.11 + 7.6.12 com.h3xstream.findsecbugs diff --git a/src/main/java/com/thealgorithms/datastructures/caches/LIFOCache.java b/src/main/java/com/thealgorithms/datastructures/caches/LIFOCache.java new file mode 100644 index 000000000000..df3d4da912fe --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/caches/LIFOCache.java @@ -0,0 +1,563 @@ +package com.thealgorithms.datastructures.caches; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiConsumer; + +/** + * A thread-safe generic cache implementation using the Last-In-First-Out eviction policy. + *

+ * The cache holds a fixed number of entries, defined by its capacity. When the cache is full and a + * new entry is added, the youngest entry in the cache is selected and evicted to make space. + *

+ * Optionally, entries can have a time-to-live (TTL) in milliseconds. If a TTL is set, entries will + * automatically expire and be removed upon access or insertion attempts. + *

+ * Features: + *

+ * + * @param the type of keys maintained by this cache + * @param the type of mapped values + * See LIFO + * @author Kevin Babu (GitHub) + */ +public final class LIFOCache { + + private final int capacity; + private final long defaultTTL; + private final Map> cache; + private final Lock lock; + private final Deque keys; + private long hits = 0; + private long misses = 0; + private final BiConsumer evictionListener; + private final EvictionStrategy evictionStrategy; + + /** + * Internal structure to store value + expiry timestamp. + * + * @param the type of the value being cached + */ + private static class CacheEntry { + V value; + long expiryTime; + + /** + * Constructs a new {@code CacheEntry} with the specified value and time-to-live (TTL). + * If TTL is 0, the entry is kept indefinitely, that is, unless it is the first value, + * then it will be removed according to the LIFO principle + * + * @param value the value to cache + * @param ttlMillis the time-to-live in milliseconds + */ + CacheEntry(V value, long ttlMillis) { + this.value = value; + if (ttlMillis == 0) { + this.expiryTime = Long.MAX_VALUE; + } else { + this.expiryTime = System.currentTimeMillis() + ttlMillis; + } + } + + /** + * Checks if the cache entry has expired. + * + * @return {@code true} if the current time is past the expiration time; {@code false} otherwise + */ + boolean isExpired() { + return System.currentTimeMillis() > expiryTime; + } + } + + /** + * Constructs a new {@code LIFOCache} instance using the provided {@link Builder}. + * + *

This constructor initializes the cache with the specified capacity and default TTL, + * sets up internal data structures (a {@code HashMap} for cache entries, + * {an @code ArrayDeque}, for key storage, and configures eviction. + * + * @param builder the {@code Builder} object containing configuration parameters + */ + private LIFOCache(Builder builder) { + this.capacity = builder.capacity; + this.defaultTTL = builder.defaultTTL; + this.cache = new HashMap<>(builder.capacity); + this.keys = new ArrayDeque<>(builder.capacity); + this.lock = new ReentrantLock(); + this.evictionListener = builder.evictionListener; + this.evictionStrategy = builder.evictionStrategy; + } + + /** + * Retrieves the value associated with the specified key from the cache. + * + *

If the key is not present or the corresponding entry has expired, this method + * returns {@code null}. If an expired entry is found, it will be removed and the + * eviction listener (if any) will be notified. Cache hit-and-miss statistics are + * also updated accordingly. + * + * @param key the key whose associated value is to be returned; must not be {@code null} + * @return the cached value associated with the key, or {@code null} if not present or expired + * @throws IllegalArgumentException if {@code key} is {@code null} + */ + public V get(K key) { + if (key == null) { + throw new IllegalArgumentException("Key must not be null"); + } + + lock.lock(); + try { + evictionStrategy.onAccess(this); + + final CacheEntry entry = cache.get(key); + if (entry == null || entry.isExpired()) { + if (entry != null) { + cache.remove(key); + keys.remove(key); + notifyEviction(key, entry.value); + } + misses++; + return null; + } + hits++; + return entry.value; + } finally { + lock.unlock(); + } + } + + /** + * Adds a key-value pair to the cache using the default time-to-live (TTL). + * + *

The key may overwrite an existing entry. The actual insertion is delegated + * to the overloaded {@link #put(K, V, long)} method. + * + * @param key the key to cache the value under + * @param value the value to be cached + */ + public void put(K key, V value) { + put(key, value, defaultTTL); + } + + /** + * Adds a key-value pair to the cache with a specified time-to-live (TTL). + * + *

If the key already exists, its value is removed, re-inserted at tail and its TTL is reset. + * If the key does not exist and the cache is full, the youngest entry is evicted to make space. + * Expired entries are also cleaned up prior to any eviction. The eviction listener + * is notified when an entry gets evicted. + * + * @param key the key to associate with the cached value; must not be {@code null} + * @param value the value to be cached; must not be {@code null} + * @param ttlMillis the time-to-live for this entry in milliseconds; must be >= 0 + * @throws IllegalArgumentException if {@code key} or {@code value} is {@code null}, or if {@code ttlMillis} is negative + */ + public void put(K key, V value, long ttlMillis) { + if (key == null || value == null) { + throw new IllegalArgumentException("Key and value must not be null"); + } + if (ttlMillis < 0) { + throw new IllegalArgumentException("TTL must be >= 0"); + } + + lock.lock(); + try { + // If key already exists, remove it. It will later be re-inserted at top of stack + keys.remove(key); + final CacheEntry oldEntry = cache.remove(key); + if (oldEntry != null && !oldEntry.isExpired()) { + notifyEviction(key, oldEntry.value); + } + + // Evict expired entries to make space for new entry + evictExpired(); + + // If no expired entry was removed, remove the youngest + if (cache.size() >= capacity) { + final K youngestKey = keys.pollLast(); + final CacheEntry youngestEntry = cache.remove(youngestKey); + notifyEviction(youngestKey, youngestEntry.value); + } + + // Insert new entry at tail + keys.add(key); + cache.put(key, new CacheEntry<>(value, ttlMillis)); + } finally { + lock.unlock(); + } + } + + /** + * Removes all expired entries from the cache. + * + *

This method iterates through the list of cached keys and checks each associated + * entry for expiration. Expired entries are removed the cache map. For each eviction, + * the eviction listener is notified. + */ + private int evictExpired() { + int count = 0; + final Iterator it = keys.iterator(); + + while (it.hasNext()) { + final K k = it.next(); + final CacheEntry entry = cache.get(k); + if (entry != null && entry.isExpired()) { + it.remove(); + cache.remove(k); + notifyEviction(k, entry.value); + count++; + } + } + + return count; + } + + /** + * Removes the specified key and its associated entry from the cache. + * + * @param key the key to remove from the cache; + * @return the value associated with the key; or {@code null} if no such key exists + */ + public V removeKey(K key) { + if (key == null) { + throw new IllegalArgumentException("Key cannot be null"); + } + lock.lock(); + try { + final CacheEntry entry = cache.remove(key); + keys.remove(key); + + // No such key in cache + if (entry == null) { + return null; + } + + notifyEviction(key, entry.value); + return entry.value; + } finally { + lock.unlock(); + } + } + + /** + * Notifies the eviction listener, if one is registered, that a key-value pair has been evicted. + * + *

If the {@code evictionListener} is not {@code null}, it is invoked with the provided key + * and value. Any exceptions thrown by the listener are caught and logged to standard error, + * preventing them from disrupting cache operations. + * + * @param key the key that was evicted + * @param value the value that was associated with the evicted key + */ + private void notifyEviction(K key, V value) { + if (evictionListener != null) { + try { + evictionListener.accept(key, value); + } catch (Exception e) { + System.err.println("Eviction listener failed: " + e.getMessage()); + } + } + } + + /** + * Returns the number of successful cache lookups (hits). + * + * @return the number of cache hits + */ + public long getHits() { + lock.lock(); + try { + return hits; + } finally { + lock.unlock(); + } + } + + /** + * Returns the number of failed cache lookups (misses), including expired entries. + * + * @return the number of cache misses + */ + public long getMisses() { + lock.lock(); + try { + return misses; + } finally { + lock.unlock(); + } + } + + /** + * Returns the current number of entries in the cache, excluding expired ones. + * + * @return the current cache size + */ + public int size() { + lock.lock(); + try { + evictionStrategy.onAccess(this); + + int count = 0; + for (CacheEntry entry : cache.values()) { + if (!entry.isExpired()) { + ++count; + } + } + return count; + } finally { + lock.unlock(); + } + } + + /** + * Removes all entries from the cache, regardless of their expiration status. + * + *

This method clears the internal cache map entirely, resets the hit-and-miss counters, + * and notifies the eviction listener (if any) for each removed entry. + * Note that expired entries are treated the same as active ones for the purpose of clearing. + * + *

This operation acquires the internal lock to ensure thread safety. + */ + public void clear() { + lock.lock(); + try { + for (Map.Entry> entry : cache.entrySet()) { + notifyEviction(entry.getKey(), entry.getValue().value); + } + keys.clear(); + cache.clear(); + hits = 0; + misses = 0; + } finally { + lock.unlock(); + } + } + + /** + * Returns a set of all keys currently stored in the cache that have not expired. + * + *

This method iterates through the cache and collects the keys of all non-expired entries. + * Expired entries are ignored but not removed. If you want to ensure expired entries are cleaned up, + * consider invoking {@link EvictionStrategy#onAccess(LIFOCache)} or calling {@link #evictExpired()} manually. + * + *

This operation acquires the internal lock to ensure thread safety. + * + * @return a set containing all non-expired keys currently in the cache + */ + public Set getAllKeys() { + lock.lock(); + try { + final Set result = new HashSet<>(); + + for (Map.Entry> entry : cache.entrySet()) { + if (!entry.getValue().isExpired()) { + result.add(entry.getKey()); + } + } + + return result; + } finally { + lock.unlock(); + } + } + + /** + * Returns the current {@link EvictionStrategy} used by this cache instance. + + * @return the eviction strategy currently assigned to this cache + */ + public EvictionStrategy getEvictionStrategy() { + return evictionStrategy; + } + + /** + * Returns a string representation of the cache, including metadata and current non-expired entries. + * + *

The returned string includes the cache's capacity, current size (excluding expired entries), + * hit-and-miss counts, and a map of all non-expired key-value pairs. This method acquires a lock + * to ensure thread-safe access. + * + * @return a string summarizing the state of the cache + */ + @Override + public String toString() { + lock.lock(); + try { + final Map visible = new LinkedHashMap<>(); + for (Map.Entry> entry : cache.entrySet()) { + if (!entry.getValue().isExpired()) { + visible.put(entry.getKey(), entry.getValue().value); + } + } + return String.format("Cache(capacity=%d, size=%d, hits=%d, misses=%d, entries=%s)", capacity, visible.size(), hits, misses, visible); + } finally { + lock.unlock(); + } + } + + /** + * A strategy interface for controlling when expired entries are evicted from the cache. + * + *

Implementations decide whether and when to trigger {@link LIFOCache#evictExpired()} based + * on cache usage patterns. This allows for flexible eviction behaviour such as periodic cleanup, + * or no automatic cleanup. + * + * @param the type of keys maintained by the cache + * @param the type of cached values + */ + public interface EvictionStrategy { + /** + * Called on each cache access (e.g., {@link LIFOCache#get(Object)}) to optionally trigger eviction. + * + * @param cache the cache instance on which this strategy is applied + * @return the number of expired entries evicted during this access + */ + int onAccess(LIFOCache cache); + } + + /** + * An eviction strategy that performs eviction of expired entries on each call. + * + * @param the type of keys + * @param the type of values + */ + public static class ImmediateEvictionStrategy implements EvictionStrategy { + @Override + public int onAccess(LIFOCache cache) { + return cache.evictExpired(); + } + } + + /** + * An eviction strategy that triggers eviction on every fixed number of accesses. + * + *

This deterministic strategy ensures cleanup occurs at predictable intervals, + * ideal for moderately active caches where memory usage is a concern. + * + * @param the type of keys + * @param the type of values + */ + public static class PeriodicEvictionStrategy implements EvictionStrategy { + private final int interval; + private final AtomicInteger counter = new AtomicInteger(); + + /** + * Constructs a periodic eviction strategy. + * + * @param interval the number of accesses between evictions; must be > 0 + * @throws IllegalArgumentException if {@code interval} is less than or equal to 0 + */ + public PeriodicEvictionStrategy(int interval) { + if (interval <= 0) { + throw new IllegalArgumentException("Interval must be > 0"); + } + this.interval = interval; + } + + @Override + public int onAccess(LIFOCache cache) { + if (counter.incrementAndGet() % interval == 0) { + return cache.evictExpired(); + } + + return 0; + } + } + + /** + * A builder for constructing a {@link LIFOCache} instance with customizable settings. + * + *

Allows configuring capacity, default TTL, eviction listener, and a pluggable eviction + * strategy. Call {@link #build()} to create the configured cache instance. + * + * @param the type of keys maintained by the cache + * @param the type of values stored in the cache + */ + public static class Builder { + private final int capacity; + private long defaultTTL = 0; + private BiConsumer evictionListener; + private EvictionStrategy evictionStrategy = new LIFOCache.ImmediateEvictionStrategy<>(); + /** + * Creates a new {@code Builder} with the specified cache capacity. + * + * @param capacity the maximum number of entries the cache can hold; must be > 0 + * @throws IllegalArgumentException if {@code capacity} is less than or equal to 0 + */ + public Builder(int capacity) { + if (capacity <= 0) { + throw new IllegalArgumentException("Capacity must be > 0"); + } + this.capacity = capacity; + } + + /** + * Sets the default time-to-live (TTL) in milliseconds for cache entries. + * + * @param ttlMillis the TTL duration in milliseconds; must be >= 0 + * @return this builder instance for chaining + * @throws IllegalArgumentException if {@code ttlMillis} is negative + */ + public Builder defaultTTL(long ttlMillis) { + if (ttlMillis < 0) { + throw new IllegalArgumentException("Default TTL must be >= 0"); + } + this.defaultTTL = ttlMillis; + return this; + } + + /** + * Sets an eviction listener to be notified when entries are evicted from the cache. + * + * @param listener a {@link BiConsumer} that accepts evicted keys and values; must not be {@code null} + * @return this builder instance for chaining + * @throws IllegalArgumentException if {@code listener} is {@code null} + */ + public Builder evictionListener(BiConsumer listener) { + if (listener == null) { + throw new IllegalArgumentException("Listener must not be null"); + } + this.evictionListener = listener; + return this; + } + + /** + * Builds and returns a new {@link LIFOCache} instance with the configured parameters. + * + * @return a fully configured {@code LIFOCache} instance + */ + public LIFOCache build() { + return new LIFOCache<>(this); + } + + /** + * Sets the eviction strategy used to determine when to clean up expired entries. + * + * @param strategy an {@link EvictionStrategy} implementation; must not be {@code null} + * @return this builder instance + * @throws IllegalArgumentException if {@code strategy} is {@code null} + */ + public Builder evictionStrategy(EvictionStrategy strategy) { + if (strategy == null) { + throw new IllegalArgumentException("Eviction strategy must not be null"); + } + this.evictionStrategy = strategy; + return this; + } + } +} diff --git a/src/main/java/com/thealgorithms/datastructures/disjointsetunion/DisjointSetUnion.java b/src/main/java/com/thealgorithms/datastructures/disjointsetunion/DisjointSetUnion.java index 583800998c81..55951be82c8a 100644 --- a/src/main/java/com/thealgorithms/datastructures/disjointsetunion/DisjointSetUnion.java +++ b/src/main/java/com/thealgorithms/datastructures/disjointsetunion/DisjointSetUnion.java @@ -1,53 +1,65 @@ package com.thealgorithms.datastructures.disjointsetunion; /** - * Disjoint Set Union or DSU is useful for solving problems related to connected components, - * cycle detection in graphs, and maintaining relationships in disjoint sets of data. - * It is commonly employed in graph algorithms and problems. + * Disjoint Set Union (DSU), also known as Union-Find, is a data structure that tracks a set of elements + * partitioned into disjoint (non-overlapping) subsets. It supports two primary operations efficiently: * - * @see Disjoint Set Union + *

    + *
  • Find: Determine which subset a particular element belongs to.
  • + *
  • Union: Merge two subsets into a single subset.
  • + *
+ * + * @see Disjoint Set Union (Wikipedia) */ public class DisjointSetUnion { /** - * Creates a new node of DSU with parent initialised as same node + * Creates a new disjoint set containing the single specified element. + * + * @param value the element to be placed in a new singleton set + * @return a node representing the new set */ - public Node makeSet(final T x) { - return new Node(x); + public Node makeSet(final T value) { + return new Node<>(value); } /** - * Finds and returns the representative (root) element of the set to which a given element belongs. - * This operation uses path compression to optimize future findSet operations. + * Finds and returns the representative (root) of the set containing the given node. + * This method applies path compression to flatten the tree structure for future efficiency. + * + * @param node the node whose set representative is to be found + * @return the representative (root) node of the set */ public Node findSet(Node node) { - while (node != node.parent) { - node = node.parent; + if (node != node.parent) { + node.parent = findSet(node.parent); } - return node; + return node.parent; } /** - * Unions two sets by merging their representative elements. The merge is performed based on the rank of each set - * to ensure efficient merging and path compression to optimize future findSet operations. + * Merges the sets containing the two given nodes. Union by rank is used to attach the smaller tree under the larger one. + * If both sets have the same rank, one becomes the parent and its rank is incremented. + * + * @param x a node in the first set + * @param y a node in the second set */ - public void unionSets(final Node x, final Node y) { - Node nx = findSet(x); - Node ny = findSet(y); + public void unionSets(Node x, Node y) { + Node rootX = findSet(x); + Node rootY = findSet(y); - if (nx == ny) { - return; // Both elements already belong to the same set. + if (rootX == rootY) { + return; // They are already in the same set } // Merging happens based on rank of node, this is done to avoid long chaining of nodes and reduce time // to find root of the component. Idea is to attach small components in big, instead of other way around. - if (nx.rank > ny.rank) { - ny.parent = nx; - } else if (ny.rank > nx.rank) { - nx.parent = ny; + if (rootX.rank > rootY.rank) { + rootY.parent = rootX; + } else if (rootY.rank > rootX.rank) { + rootX.parent = rootY; } else { - // Both sets have the same rank; choose one as the parent and increment the rank. - ny.parent = nx; - nx.rank++; + rootY.parent = rootX; + rootX.rank++; } } } diff --git a/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/Intersection.java b/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/Intersection.java index 0e49218d6348..5760d39f1df7 100644 --- a/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/Intersection.java +++ b/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/Intersection.java @@ -8,60 +8,66 @@ /** * The {@code Intersection} class provides a method to compute the intersection of two integer arrays. - * The intersection is defined as the set of common elements present in both arrays. *

- * This class utilizes a HashMap to efficiently count occurrences of elements in the first array, - * allowing for an efficient lookup of common elements in the second array. + * This intersection includes duplicate values — meaning elements are included in the result + * as many times as they appear in both arrays (i.e., multiset intersection). *

* *

- * Example: - *

+ * The algorithm uses a {@link java.util.HashMap} to count occurrences of elements in the first array,
+ * then iterates through the second array to collect common elements based on these counts.
+ * 

+ * + *

+ * Example usage: + *

{@code
  * int[] array1 = {1, 2, 2, 1};
  * int[] array2 = {2, 2};
- * List result = Intersection.intersection(array1, array2); // result will contain [2, 2]
- * 
+ * List result = Intersection.intersection(array1, array2); // result: [2, 2] + * }
*

* *

- * Note: The order of the returned list may vary since it depends on the order of elements - * in the input arrays. + * Note: The order of elements in the returned list depends on the order in the second input array. *

*/ public final class Intersection { + private Intersection() { + // Utility class; prevent instantiation + } + /** - * Computes the intersection of two integer arrays. + * Computes the intersection of two integer arrays, preserving element frequency. + * For example, given [1,2,2,3] and [2,2,4], the result will be [2,2]. + * * Steps: - * 1. Count the occurrences of each element in the first array using a HashMap. - * 2. Iterate over the second array and check if the element is present in the HashMap. - * If it is, add it to the result list and decrement the count in the HashMap. - * 3. Return the result list containing the intersection of the two arrays. + * 1. Count the occurrences of each element in the first array using a map. + * 2. Iterate over the second array and collect common elements. * * @param arr1 the first array of integers * @param arr2 the second array of integers - * @return a list containing the intersection of the two arrays, or an empty list if either array is null or empty + * @return a list containing the intersection of the two arrays (with duplicates), + * or an empty list if either array is null or empty */ public static List intersection(int[] arr1, int[] arr2) { if (arr1 == null || arr2 == null || arr1.length == 0 || arr2.length == 0) { return Collections.emptyList(); } - Map cnt = new HashMap<>(16); - for (int v : arr1) { - cnt.put(v, cnt.getOrDefault(v, 0) + 1); + Map countMap = new HashMap<>(); + for (int num : arr1) { + countMap.put(num, countMap.getOrDefault(num, 0) + 1); } - List res = new ArrayList<>(); - for (int v : arr2) { - if (cnt.containsKey(v) && cnt.get(v) > 0) { - res.add(v); - cnt.put(v, cnt.get(v) - 1); + List result = new ArrayList<>(); + for (int num : arr2) { + if (countMap.getOrDefault(num, 0) > 0) { + result.add(num); + countMap.computeIfPresent(num, (k, v) -> v - 1); } } - return res; - } - private Intersection() { + return result; } } diff --git a/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/MajorityElement.java b/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/MajorityElement.java index 915e4228b618..5fd6b2e7f3cb 100644 --- a/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/MajorityElement.java +++ b/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/MajorityElement.java @@ -1,8 +1,10 @@ package com.thealgorithms.datastructures.hashmap.hashing; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; /** * This class provides a method to find the majority element(s) in an array of integers. @@ -18,13 +20,18 @@ private MajorityElement() { * Returns a list of majority element(s) from the given array of integers. * * @param nums an array of integers - * @return a list containing the majority element(s); returns an empty list if none exist + * @return a list containing the majority element(s); returns an empty list if none exist or input is null/empty */ public static List majority(int[] nums) { - HashMap numToCount = new HashMap<>(); + if (nums == null || nums.length == 0) { + return Collections.emptyList(); + } + + Map numToCount = new HashMap<>(); for (final var num : nums) { numToCount.merge(num, 1, Integer::sum); } + List majorityElements = new ArrayList<>(); for (final var entry : numToCount.entrySet()) { if (entry.getValue() >= nums.length / 2) { diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/KnapsackZeroOne.java b/src/main/java/com/thealgorithms/dynamicprogramming/KnapsackZeroOne.java new file mode 100644 index 000000000000..abc1e321ca8f --- /dev/null +++ b/src/main/java/com/thealgorithms/dynamicprogramming/KnapsackZeroOne.java @@ -0,0 +1,53 @@ +package com.thealgorithms.dynamicprogramming; + +/** + * The {@code KnapsackZeroOne} provides Recursive solution for the 0/1 Knapsack + * problem. Solves by exploring all combinations of items using recursion. No + * memoization or dynamic programming optimizations are applied. + * + * Time Complexity: O(2^n) — explores all subsets. + * Space Complexity: O(n) — due to recursive call stack. + * + * Problem Reference: https://en.wikipedia.org/wiki/Knapsack_problem + */ +public final class KnapsackZeroOne { + + private KnapsackZeroOne() { + // Prevent instantiation + } + + /** + * Solves the 0/1 Knapsack problem using recursion. + * + * @param values the array containing values of the items + * @param weights the array containing weights of the items + * @param capacity the total capacity of the knapsack + * @param n the number of items + * @return the maximum total value achievable within the given weight limit + * @throws IllegalArgumentException if input arrays are null, empty, or + * lengths mismatch + */ + public static int compute(final int[] values, final int[] weights, final int capacity, final int n) { + if (values == null || weights == null) { + throw new IllegalArgumentException("Input arrays cannot be null."); + } + if (values.length != weights.length) { + throw new IllegalArgumentException("Value and weight arrays must be of the same length."); + } + if (capacity < 0 || n < 0) { + throw new IllegalArgumentException("Invalid input: arrays must be non-empty and capacity/n " + + "non-negative."); + } + if (n == 0 || capacity == 0 || values.length == 0) { + return 0; + } + + if (weights[n - 1] <= capacity) { + final int include = values[n - 1] + compute(values, weights, capacity - weights[n - 1], n - 1); + final int exclude = compute(values, weights, capacity, n - 1); + return Math.max(include, exclude); + } else { + return compute(values, weights, capacity, n - 1); + } + } +} diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/KnapsackZeroOneTabulation.java b/src/main/java/com/thealgorithms/dynamicprogramming/KnapsackZeroOneTabulation.java new file mode 100644 index 000000000000..c560efc61c71 --- /dev/null +++ b/src/main/java/com/thealgorithms/dynamicprogramming/KnapsackZeroOneTabulation.java @@ -0,0 +1,69 @@ +package com.thealgorithms.dynamicprogramming; + +/** + * Tabulation (Bottom-Up) Solution for 0-1 Knapsack Problem. + * This method uses dynamic programming to build up a solution iteratively, + * filling a 2-D array where each entry dp[i][w] represents the maximum value + * achievable with the first i items and a knapsack capacity of w. + * + * The tabulation approach is efficient because it avoids redundant calculations + * by solving all subproblems in advance and storing their results, ensuring + * each subproblem is solved only once. This is a key technique in dynamic programming, + * making it possible to solve problems that would otherwise be infeasible due to + * exponential time complexity in naive recursive solutions. + * + * Time Complexity: O(n * W), where n is the number of items and W is the knapsack capacity. + * Space Complexity: O(n * W) for the DP table. + * + * For more information, see: + * https://en.wikipedia.org/wiki/Knapsack_problem#Dynamic_programming + */ +public final class KnapsackZeroOneTabulation { + + private KnapsackZeroOneTabulation() { + // Prevent instantiation + } + + /** + * Solves the 0-1 Knapsack problem using the bottom-up tabulation technique. + * @param values the values of the items + * @param weights the weights of the items + * @param capacity the total capacity of the knapsack + * @param itemCount the number of items + * @return the maximum value that can be put in the knapsack + * @throws IllegalArgumentException if input arrays are null, of different lengths,or if capacity or itemCount is invalid + */ + public static int compute(final int[] values, final int[] weights, final int capacity, final int itemCount) { + if (values == null || weights == null) { + throw new IllegalArgumentException("Values and weights arrays must not be null."); + } + if (values.length != weights.length) { + throw new IllegalArgumentException("Values and weights arrays must be non-null and of same length."); + } + if (capacity < 0) { + throw new IllegalArgumentException("Capacity must not be negative."); + } + if (itemCount < 0 || itemCount > values.length) { + throw new IllegalArgumentException("Item count must be between 0 and the length of the values array."); + } + + final int[][] dp = new int[itemCount + 1][capacity + 1]; + + for (int i = 1; i <= itemCount; i++) { + final int currentValue = values[i - 1]; + final int currentWeight = weights[i - 1]; + + for (int w = 1; w <= capacity; w++) { + if (currentWeight <= w) { + final int includeItem = currentValue + dp[i - 1][w - currentWeight]; + final int excludeItem = dp[i - 1][w]; + dp[i][w] = Math.max(includeItem, excludeItem); + } else { + dp[i][w] = dp[i - 1][w]; + } + } + } + + return dp[itemCount][capacity]; + } +} diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/SubsetSumSpaceOptimized.java b/src/main/java/com/thealgorithms/dynamicprogramming/SubsetSumSpaceOptimized.java index 946da46cb292..5c0167977ae4 100644 --- a/src/main/java/com/thealgorithms/dynamicprogramming/SubsetSumSpaceOptimized.java +++ b/src/main/java/com/thealgorithms/dynamicprogramming/SubsetSumSpaceOptimized.java @@ -1,35 +1,39 @@ package com.thealgorithms.dynamicprogramming; -/* -The Sum of Subset problem determines whether a subset of elements from a -given array sums up to a specific target value. -*/ + +/** + * Utility class for solving the Subset Sum problem using a space-optimized dynamic programming approach. + * + *

This algorithm determines whether any subset of a given array sums up to a specific target value.

+ * + *

Time Complexity: O(n * sum)

+ *

Space Complexity: O(sum)

+ */ public final class SubsetSumSpaceOptimized { private SubsetSumSpaceOptimized() { } + /** - * This method checks whether the subset of an array - * contains a given sum or not. This is an space - * optimized solution using 1D boolean array - * Time Complexity: O(n * sum), Space complexity: O(sum) + * Determines whether there exists a subset of the given array that adds up to the specified sum. + * This method uses a space-optimized dynamic programming approach with a 1D boolean array. * - * @param arr An array containing integers - * @param sum The target sum of the subset - * @return True or False + * @param nums The array of non-negative integers + * @param targetSum The desired subset sum + * @return {@code true} if such a subset exists, {@code false} otherwise */ - public static boolean isSubsetSum(int[] arr, int sum) { - int n = arr.length; - // Declare the boolean array with size sum + 1 - boolean[] dp = new boolean[sum + 1]; + public static boolean isSubsetSum(int[] nums, int targetSum) { + if (targetSum < 0) { + return false; // Subset sum can't be negative + } - // Initialize the first element as true - dp[0] = true; + boolean[] dp = new boolean[targetSum + 1]; + dp[0] = true; // Empty subset always sums to 0 - // Find the subset sum using 1D array - for (int i = 0; i < n; i++) { - for (int j = sum; j >= arr[i]; j--) { - dp[j] = dp[j] || dp[j - arr[i]]; + for (int number : nums) { + for (int j = targetSum; j >= number; j--) { + dp[j] = dp[j] || dp[j - number]; } } - return dp[sum]; + + return dp[targetSum]; } } diff --git a/src/main/java/com/thealgorithms/maths/Convolution.java b/src/main/java/com/thealgorithms/maths/Convolution.java index 93e103f8c7cf..a86ecabce933 100644 --- a/src/main/java/com/thealgorithms/maths/Convolution.java +++ b/src/main/java/com/thealgorithms/maths/Convolution.java @@ -23,24 +23,21 @@ public static double[] convolution(double[] a, double[] b) { double[] convolved = new double[a.length + b.length - 1]; /* - The discrete convolution of two signals A and B is defined as: - - A.length - C[i] = Σ (A[k]*B[i-k]) - k=0 - - It's obvious that: 0 <= k <= A.length , 0 <= i <= A.length + B.length - 2 and 0 <= i-k <= - B.length - 1 From the last inequality we get that: i - B.length + 1 <= k <= i and thus we get - the conditions below. + * Discrete convolution formula: + * C[i] = Σ A[k] * B[i - k] + * where k ranges over valid indices so that both A[k] and B[i-k] are in bounds. */ + for (int i = 0; i < convolved.length; i++) { - convolved[i] = 0; - int k = Math.max(i - b.length + 1, 0); + double sum = 0; + int kStart = Math.max(0, i - b.length + 1); + int kEnd = Math.min(i, a.length - 1); - while (k < i + 1 && k < a.length) { - convolved[i] += a[k] * b[i - k]; - k++; + for (int k = kStart; k <= kEnd; k++) { + sum += a[k] * b[i - k]; } + + convolved[i] = sum; } return convolved; diff --git a/src/main/java/com/thealgorithms/maths/Median.java b/src/main/java/com/thealgorithms/maths/Median.java index e4daec8fc11a..fd2aab84e4e9 100644 --- a/src/main/java/com/thealgorithms/maths/Median.java +++ b/src/main/java/com/thealgorithms/maths/Median.java @@ -13,8 +13,13 @@ private Median() { * Calculate average median * @param values sorted numbers to find median of * @return median of given {@code values} + * @throws IllegalArgumentException If the input array is empty or null. */ public static double median(int[] values) { + if (values == null || values.length == 0) { + throw new IllegalArgumentException("Values array cannot be empty or null"); + } + Arrays.sort(values); int length = values.length; return length % 2 == 0 ? (values[length / 2] + values[length / 2 - 1]) / 2.0 : values[length / 2]; diff --git a/src/main/java/com/thealgorithms/maths/Mode.java b/src/main/java/com/thealgorithms/maths/Mode.java index f0b747cf02ec..657f8ece2a50 100644 --- a/src/main/java/com/thealgorithms/maths/Mode.java +++ b/src/main/java/com/thealgorithms/maths/Mode.java @@ -3,46 +3,49 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; +import java.util.Map; -/* - * Find the mode of an array of numbers - * - * The mode of an array of numbers is the most frequently occurring number in the array, - * or the most frequently occurring numbers if there are multiple numbers with the same frequency +/** + * Utility class to calculate the mode(s) of an array of integers. + *

+ * The mode of an array is the integer value(s) that occur most frequently. + * If multiple values have the same highest frequency, all such values are returned. + *

*/ public final class Mode { private Mode() { } - /* - * Find the mode of an array of integers + /** + * Computes the mode(s) of the specified array of integers. + *

+ * If the input array is empty, this method returns {@code null}. + * If multiple numbers share the highest frequency, all are returned in the result array. + *

* - * @param numbers array of integers - * @return mode of the array + * @param numbers an array of integers to analyze + * @return an array containing the mode(s) of the input array, or {@code null} if the input is empty */ public static int[] mode(final int[] numbers) { if (numbers.length == 0) { return null; } - HashMap count = new HashMap<>(); + Map count = new HashMap<>(); for (int num : numbers) { - if (count.containsKey(num)) { - count.put(num, count.get(num) + 1); - } else { - count.put(num, 1); - } + count.put(num, count.getOrDefault(num, 0) + 1); } int max = Collections.max(count.values()); - ArrayList modes = new ArrayList<>(); + List modes = new ArrayList<>(); for (final var entry : count.entrySet()) { if (entry.getValue() == max) { modes.add(entry.getKey()); } } - return modes.stream().mapToInt(n -> n).toArray(); + return modes.stream().mapToInt(Integer::intValue).toArray(); } } diff --git a/src/main/java/com/thealgorithms/matrix/MatrixMultiplication.java b/src/main/java/com/thealgorithms/matrix/MatrixMultiplication.java new file mode 100644 index 000000000000..6467a438577b --- /dev/null +++ b/src/main/java/com/thealgorithms/matrix/MatrixMultiplication.java @@ -0,0 +1,69 @@ +package com.thealgorithms.matrix; + +/** + * This class provides a method to perform matrix multiplication. + * + *

Matrix multiplication takes two 2D arrays (matrices) as input and + * produces their product, following the mathematical definition of + * matrix multiplication. + * + *

For more details: + * https://www.geeksforgeeks.org/java/java-program-to-multiply-two-matrices-of-any-size/ + * https://en.wikipedia.org/wiki/Matrix_multiplication + * + *

Time Complexity: O(n^3) – where n is the dimension of the matrices + * (assuming square matrices for simplicity). + * + *

Space Complexity: O(n^2) – for storing the result matrix. + * + * + * @author Nishitha Wihala Pitigala + * + */ + +public final class MatrixMultiplication { + private MatrixMultiplication() { + } + + /** + * Multiplies two matrices. + * + * @param matrixA the first matrix rowsA x colsA + * @param matrixB the second matrix rowsB x colsB + * @return the product of the two matrices rowsA x colsB + * @throws IllegalArgumentException if the matrices cannot be multiplied + */ + public static double[][] multiply(double[][] matrixA, double[][] matrixB) { + // Check the input matrices are not null + if (matrixA == null || matrixB == null) { + throw new IllegalArgumentException("Input matrices cannot be null"); + } + + // Check for empty matrices + if (matrixA.length == 0 || matrixB.length == 0 || matrixA[0].length == 0 || matrixB[0].length == 0) { + throw new IllegalArgumentException("Input matrices must not be empty"); + } + + // Validate the matrix dimensions + if (matrixA[0].length != matrixB.length) { + throw new IllegalArgumentException("Matrices cannot be multiplied: incompatible dimensions."); + } + + int rowsA = matrixA.length; + int colsA = matrixA[0].length; + int colsB = matrixB[0].length; + + // Initialize the result matrix with zeros + double[][] result = new double[rowsA][colsB]; + + // Perform matrix multiplication + for (int i = 0; i < rowsA; i++) { + for (int j = 0; j < colsB; j++) { + for (int k = 0; k < colsA; k++) { + result[i][j] += matrixA[i][k] * matrixB[k][j]; + } + } + } + return result; + } +} diff --git a/src/main/java/com/thealgorithms/matrix/MedianOfMatrix.java b/src/main/java/com/thealgorithms/matrix/MedianOfMatrix.java index c710c60a2d2a..1ec977af07c6 100644 --- a/src/main/java/com/thealgorithms/matrix/MedianOfMatrix.java +++ b/src/main/java/com/thealgorithms/matrix/MedianOfMatrix.java @@ -14,19 +14,19 @@ private MedianOfMatrix() { } public static int median(Iterable> matrix) { - // Flatten the matrix into a 1D list - List linear = new ArrayList<>(); + List flattened = new ArrayList<>(); + for (List row : matrix) { - linear.addAll(row); + if (row != null) { + flattened.addAll(row); + } } - // Sort the 1D list - Collections.sort(linear); - - // Calculate the middle index - int mid = (0 + linear.size() - 1) / 2; + if (flattened.isEmpty()) { + throw new IllegalArgumentException("Matrix must contain at least one element."); + } - // Return the median - return linear.get(mid); + Collections.sort(flattened); + return flattened.get((flattened.size() - 1) / 2); } } diff --git a/src/main/java/com/thealgorithms/sorts/AdaptiveMergeSort.java b/src/main/java/com/thealgorithms/sorts/AdaptiveMergeSort.java index 2c71bae8b557..09ea349875ac 100644 --- a/src/main/java/com/thealgorithms/sorts/AdaptiveMergeSort.java +++ b/src/main/java/com/thealgorithms/sorts/AdaptiveMergeSort.java @@ -30,7 +30,7 @@ private > void merge(T[] array, T[] aux, int low, int mi array[k] = aux[j++]; } else if (j > high) { array[k] = aux[i++]; - } else if (aux[j].compareTo(aux[i]) < 0) { + } else if (SortUtils.less(aux[j], aux[i])) { array[k] = aux[j++]; } else { array[k] = aux[i++]; diff --git a/src/main/java/com/thealgorithms/sorts/BinaryInsertionSort.java b/src/main/java/com/thealgorithms/sorts/BinaryInsertionSort.java index b6f5d92e7928..b8086bd0ebca 100644 --- a/src/main/java/com/thealgorithms/sorts/BinaryInsertionSort.java +++ b/src/main/java/com/thealgorithms/sorts/BinaryInsertionSort.java @@ -22,7 +22,7 @@ public > T[] sort(T[] array) { while (low <= high) { final int mid = (low + high) >>> 1; - if (temp.compareTo(array[mid]) < 0) { + if (SortUtils.less(temp, array[mid])) { high = mid - 1; } else { low = mid + 1; diff --git a/src/main/java/com/thealgorithms/sorts/BitonicSort.java b/src/main/java/com/thealgorithms/sorts/BitonicSort.java index 90d204818729..1c1a3ac45540 100644 --- a/src/main/java/com/thealgorithms/sorts/BitonicSort.java +++ b/src/main/java/com/thealgorithms/sorts/BitonicSort.java @@ -64,7 +64,7 @@ private > void bitonicMerge(T[] array, int low, int cnt, if (cnt > 1) { final int k = cnt / 2; - final BiPredicate areSorted = (direction == Direction.ASCENDING) ? (a, b) -> a.compareTo(b) < 0 : (a, b) -> a.compareTo(b) > 0; + final BiPredicate areSorted = (direction == Direction.ASCENDING) ? (a, b) -> SortUtils.less(a, b) : (a, b) -> SortUtils.greater(a, b); for (int i = low; i < low + k; i++) { if (!areSorted.test(array[i], array[i + k])) { SortUtils.swap(array, i, i + k); diff --git a/src/main/java/com/thealgorithms/sorts/BucketSort.java b/src/main/java/com/thealgorithms/sorts/BucketSort.java index 62c5e929593b..8882a1cb4bbd 100644 --- a/src/main/java/com/thealgorithms/sorts/BucketSort.java +++ b/src/main/java/com/thealgorithms/sorts/BucketSort.java @@ -111,7 +111,7 @@ private > int hash(final T element, final T min, final T private > T findMin(T[] array) { T min = array[0]; for (T element : array) { - if (element.compareTo(min) < 0) { + if (SortUtils.less(element, min)) { min = element; } } @@ -121,7 +121,7 @@ private > T findMin(T[] array) { private > T findMax(T[] array) { T max = array[0]; for (T element : array) { - if (element.compareTo(max) > 0) { + if (SortUtils.greater(element, max)) { max = element; } } diff --git a/src/main/java/com/thealgorithms/sorts/CircleSort.java b/src/main/java/com/thealgorithms/sorts/CircleSort.java index b9b41be16701..2863a40a2075 100644 --- a/src/main/java/com/thealgorithms/sorts/CircleSort.java +++ b/src/main/java/com/thealgorithms/sorts/CircleSort.java @@ -36,7 +36,7 @@ private > boolean doSort(final T[] array, final int left int high = right; while (low < high) { - if (array[low].compareTo(array[high]) > 0) { + if (SortUtils.greater(array[low], array[high])) { SortUtils.swap(array, low, high); swapped = true; } @@ -44,7 +44,7 @@ private > boolean doSort(final T[] array, final int left high--; } - if (low == high && array[low].compareTo(array[high + 1]) > 0) { + if (low == high && SortUtils.greater(array[low], array[high + 1])) { SortUtils.swap(array, low, high + 1); swapped = true; } diff --git a/src/main/java/com/thealgorithms/sorts/DutchNationalFlagSort.java b/src/main/java/com/thealgorithms/sorts/DutchNationalFlagSort.java index abfcb452b29a..f7e12da06568 100644 --- a/src/main/java/com/thealgorithms/sorts/DutchNationalFlagSort.java +++ b/src/main/java/com/thealgorithms/sorts/DutchNationalFlagSort.java @@ -26,11 +26,11 @@ private > T[] dutchNationalFlagSort(final T[] array, fin int k = array.length - 1; while (j <= k) { - if (0 > array[j].compareTo(intendedMiddle)) { + if (SortUtils.less(array[j], intendedMiddle)) { SortUtils.swap(array, i, j); j++; i++; - } else if (0 < array[j].compareTo(intendedMiddle)) { + } else if (SortUtils.greater(array[j], intendedMiddle)) { SortUtils.swap(array, j, k); k--; } else { diff --git a/src/main/java/com/thealgorithms/sorts/ExchangeSort.java b/src/main/java/com/thealgorithms/sorts/ExchangeSort.java index 67e94b889671..a58caaf1031a 100644 --- a/src/main/java/com/thealgorithms/sorts/ExchangeSort.java +++ b/src/main/java/com/thealgorithms/sorts/ExchangeSort.java @@ -31,7 +31,7 @@ class ExchangeSort implements SortAlgorithm { public > T[] sort(T[] array) { for (int i = 0; i < array.length - 1; i++) { for (int j = i + 1; j < array.length; j++) { - if (array[i].compareTo(array[j]) > 0) { + if (SortUtils.greater(array[i], array[j])) { SortUtils.swap(array, i, j); } } diff --git a/src/main/java/com/thealgorithms/sorts/IntrospectiveSort.java b/src/main/java/com/thealgorithms/sorts/IntrospectiveSort.java index 12ef197b931b..6f846c7b9a8f 100644 --- a/src/main/java/com/thealgorithms/sorts/IntrospectiveSort.java +++ b/src/main/java/com/thealgorithms/sorts/IntrospectiveSort.java @@ -63,7 +63,7 @@ private static > int partition(T[] array, final int low, final T pivot = array[high]; int i = low - 1; for (int j = low; j < high; j++) { - if (array[j].compareTo(pivot) <= 0) { + if (SortUtils.greaterOrEqual(pivot, array[j])) { i++; SortUtils.swap(array, i, j); } @@ -84,7 +84,7 @@ private static > void insertionSort(T[] array, final int for (int i = low + 1; i <= high; i++) { final T key = array[i]; int j = i - 1; - while (j >= low && array[j].compareTo(key) > 0) { + while (j >= low && SortUtils.greater(array[j], key)) { array[j + 1] = array[j]; j--; } @@ -125,10 +125,10 @@ private static > void heapify(T[] array, final int i, fi final int right = 2 * i + 2; int largest = i; - if (left < n && array[low + left].compareTo(array[low + largest]) > 0) { + if (left < n && SortUtils.greater(array[low + left], array[low + largest])) { largest = left; } - if (right < n && array[low + right].compareTo(array[low + largest]) > 0) { + if (right < n && SortUtils.greater(array[low + right], array[low + largest])) { largest = right; } if (largest != i) { diff --git a/src/main/java/com/thealgorithms/sorts/OddEvenSort.java b/src/main/java/com/thealgorithms/sorts/OddEvenSort.java index ac94982c1474..b854842e9645 100644 --- a/src/main/java/com/thealgorithms/sorts/OddEvenSort.java +++ b/src/main/java/com/thealgorithms/sorts/OddEvenSort.java @@ -30,7 +30,7 @@ public > T[] sort(T[] array) { private > boolean performOddSort(T[] array) { boolean sorted = true; for (int i = 1; i < array.length - 1; i += 2) { - if (array[i].compareTo(array[i + 1]) > 0) { + if (SortUtils.greater(array[i], array[i + 1])) { SortUtils.swap(array, i, i + 1); sorted = false; } @@ -41,7 +41,7 @@ private > boolean performOddSort(T[] array) { private > boolean performEvenSort(T[] array) { boolean sorted = true; for (int i = 0; i < array.length - 1; i += 2) { - if (array[i].compareTo(array[i + 1]) > 0) { + if (SortUtils.greater(array[i], array[i + 1])) { SortUtils.swap(array, i, i + 1); sorted = false; } diff --git a/src/main/java/com/thealgorithms/sorts/SelectionSort.java b/src/main/java/com/thealgorithms/sorts/SelectionSort.java index dbb2b88ffcef..db7732d7e218 100644 --- a/src/main/java/com/thealgorithms/sorts/SelectionSort.java +++ b/src/main/java/com/thealgorithms/sorts/SelectionSort.java @@ -21,7 +21,7 @@ public > T[] sort(T[] array) { private static > int findIndexOfMin(T[] array, final int startIndex) { int minIndex = startIndex; for (int i = startIndex + 1; i < array.length; i++) { - if (array[i].compareTo(array[minIndex]) < 0) { + if (SortUtils.less(array[i], array[minIndex])) { minIndex = i; } } diff --git a/src/main/java/com/thealgorithms/sorts/SelectionSortRecursive.java b/src/main/java/com/thealgorithms/sorts/SelectionSortRecursive.java index 9d24542de592..f220c2d8f994 100644 --- a/src/main/java/com/thealgorithms/sorts/SelectionSortRecursive.java +++ b/src/main/java/com/thealgorithms/sorts/SelectionSortRecursive.java @@ -56,6 +56,6 @@ private static > int findMinIndex(T[] array, final int s final int minIndexInRest = findMinIndex(array, start + 1); // Return the index of the smaller element between array[start] and the minimum element in the rest of the array - return array[start].compareTo(array[minIndexInRest]) < 0 ? start : minIndexInRest; + return SortUtils.less(array[start], array[minIndexInRest]) ? start : minIndexInRest; } } diff --git a/src/main/java/com/thealgorithms/sorts/SimpleSort.java b/src/main/java/com/thealgorithms/sorts/SimpleSort.java deleted file mode 100644 index a03223cb01a1..000000000000 --- a/src/main/java/com/thealgorithms/sorts/SimpleSort.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.thealgorithms.sorts; - -public class SimpleSort implements SortAlgorithm { - @Override - public > T[] sort(T[] array) { - for (int i = 0; i < array.length; i++) { - for (int j = i + 1; j < array.length; j++) { - if (SortUtils.less(array[j], array[i])) { - SortUtils.swap(array, i, j); - } - } - } - return array; - } -} diff --git a/src/main/java/com/thealgorithms/sorts/StalinSort.java b/src/main/java/com/thealgorithms/sorts/StalinSort.java index 5aaf530fd94c..fe07a6f6d986 100644 --- a/src/main/java/com/thealgorithms/sorts/StalinSort.java +++ b/src/main/java/com/thealgorithms/sorts/StalinSort.java @@ -8,7 +8,7 @@ public > T[] sort(T[] array) { } int currentIndex = 0; for (int i = 1; i < array.length; i++) { - if (array[i].compareTo(array[currentIndex]) >= 0) { + if (SortUtils.greaterOrEqual(array[i], array[currentIndex])) { currentIndex++; array[currentIndex] = array[i]; } diff --git a/src/main/java/com/thealgorithms/stacks/DecimalToAnyUsingStack.java b/src/main/java/com/thealgorithms/stacks/DecimalToAnyUsingStack.java index ff6402c92695..2852454fd096 100644 --- a/src/main/java/com/thealgorithms/stacks/DecimalToAnyUsingStack.java +++ b/src/main/java/com/thealgorithms/stacks/DecimalToAnyUsingStack.java @@ -2,17 +2,29 @@ import java.util.Stack; +/** + * Utility class for converting a non-negative decimal (base-10) integer + * to its representation in another radix (base) between 2 and 16, inclusive. + * + *

This class uses a stack-based approach to reverse the digits obtained from + * successive divisions by the target radix. + * + *

This class cannot be instantiated.

+ */ public final class DecimalToAnyUsingStack { + private DecimalToAnyUsingStack() { } + private static final char[] DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + /** * Convert a decimal number to another radix. * * @param number the number to be converted * @param radix the radix * @return the number represented in the new radix as a String - * @throws IllegalArgumentException if number is negative or radix is not between 2 and 16 inclusive + * @throws IllegalArgumentException if number is negative or radix is not between 2 and 16 inclusive */ public static String convert(int number, int radix) { if (number < 0) { @@ -26,18 +38,17 @@ public static String convert(int number, int radix) { return "0"; } - char[] tables = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; - - Stack bits = new Stack<>(); + Stack digitStack = new Stack<>(); while (number > 0) { - bits.push(tables[number % radix]); - number = number / radix; + digitStack.push(DIGITS[number % radix]); + number /= radix; } - StringBuilder result = new StringBuilder(); - while (!bits.isEmpty()) { - result.append(bits.pop()); + StringBuilder result = new StringBuilder(digitStack.size()); + while (!digitStack.isEmpty()) { + result.append(digitStack.pop()); } + return result.toString(); } } diff --git a/src/main/java/com/thealgorithms/strings/ReverseString.java b/src/main/java/com/thealgorithms/strings/ReverseString.java index 54a9b779e828..996aa68bb307 100644 --- a/src/main/java/com/thealgorithms/strings/ReverseString.java +++ b/src/main/java/com/thealgorithms/strings/ReverseString.java @@ -1,5 +1,7 @@ package com.thealgorithms.strings; +import java.util.Stack; + /** * Reverse String using different version */ @@ -57,4 +59,31 @@ public static String reverse3(String string) { } return sb.toString(); } + /** + * Reverses the given string using a stack. + * This method uses a stack to reverse the characters of the string. + * * @param str The input string to be reversed. + * @return The reversed string. + */ + public static String reverseStringUsingStack(String str) { + // Check if the input string is null + if (str == null) { + throw new IllegalArgumentException("Input string cannot be null"); + } + Stack stack = new Stack<>(); + StringBuilder reversedString = new StringBuilder(); + // Check if the input string is empty + if (str.isEmpty()) { + return str; + } + // Push each character of the string onto the stack + for (char ch : str.toCharArray()) { + stack.push(ch); + } + // Pop each character from the stack and append to the StringBuilder + while (!stack.isEmpty()) { + reversedString.append(stack.pop()); + } + return reversedString.toString(); + } } diff --git a/src/test/java/com/thealgorithms/bitmanipulation/ParityCheckTest.java b/src/test/java/com/thealgorithms/bitmanipulation/ParityCheckTest.java index 90147a61207b..1654c8ddfc1e 100644 --- a/src/test/java/com/thealgorithms/bitmanipulation/ParityCheckTest.java +++ b/src/test/java/com/thealgorithms/bitmanipulation/ParityCheckTest.java @@ -6,10 +6,30 @@ import org.junit.jupiter.api.Test; public class ParityCheckTest { + @Test + public void testIsEvenParity() { + assertTrue(ParityCheck.checkParity(0)); // 0 -> 0 ones + assertTrue(ParityCheck.checkParity(3)); // 11 -> 2 ones + assertTrue(ParityCheck.checkParity(5)); // 101 -> 2 ones + assertTrue(ParityCheck.checkParity(10)); // 1010 -> 2 ones + assertTrue(ParityCheck.checkParity(15)); // 1111 -> 4 ones + assertTrue(ParityCheck.checkParity(1023)); // 10 ones + } + @Test public void testIsOddParity() { - assertTrue(ParityCheck.checkParity(5)); // 101 has 2 ones (even parity) - assertFalse(ParityCheck.checkParity(7)); // 111 has 3 ones (odd parity) - assertFalse(ParityCheck.checkParity(8)); // 1000 has 1 one (odd parity) + assertFalse(ParityCheck.checkParity(1)); // 1 -> 1 one + assertFalse(ParityCheck.checkParity(2)); // 10 -> 1 one + assertFalse(ParityCheck.checkParity(7)); // 111 -> 3 ones + assertFalse(ParityCheck.checkParity(8)); // 1000 -> 1 one + assertFalse(ParityCheck.checkParity(11)); // 1011 -> 3 ones + assertFalse(ParityCheck.checkParity(31)); // 11111 -> 5 ones + } + + @Test + public void testLargeNumbers() { + assertTrue(ParityCheck.checkParity(0b10101010)); // 4 ones + assertFalse(ParityCheck.checkParity(0b100000000)); // 1 one + assertTrue(ParityCheck.checkParity(0xAAAAAAAA)); // Alternating bits, 16 ones } } diff --git a/src/test/java/com/thealgorithms/datastructures/bag/BagTest.java b/src/test/java/com/thealgorithms/datastructures/bag/BagTest.java index b7e64851383c..8212793dfb79 100644 --- a/src/test/java/com/thealgorithms/datastructures/bag/BagTest.java +++ b/src/test/java/com/thealgorithms/datastructures/bag/BagTest.java @@ -6,7 +6,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import com.thealgorithms.datastructures.bags.Bag; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; import org.junit.jupiter.api.Test; class BagTest { @@ -156,4 +159,116 @@ void testIteratorWithDuplicates() { } assertEquals(3, count, "Iterator should traverse all 3 items including duplicates"); } + + @Test + void testCollectionElements() { + Bag> bag = new Bag<>(); + List list1 = new ArrayList<>(); + list1.add("a"); + list1.add("b"); + + List list2 = new ArrayList<>(); + list2.add("c"); + + List emptyList = new ArrayList<>(); + + bag.add(list1); + bag.add(list2); + bag.add(emptyList); + bag.add(list1); // Duplicate + + assertEquals(4, bag.size(), "Bag should contain 4 list elements"); + assertTrue(bag.contains(list1), "Bag should contain list1"); + assertTrue(bag.contains(list2), "Bag should contain list2"); + assertTrue(bag.contains(emptyList), "Bag should contain empty list"); + } + + @Test + void testIteratorConsistency() { + Bag bag = new Bag<>(); + bag.add("first"); + bag.add("second"); + bag.add("third"); + + // Multiple iterations should return same elements + List firstIteration = new ArrayList<>(); + for (String item : bag) { + firstIteration.add(item); + } + + List secondIteration = new ArrayList<>(); + for (String item : bag) { + secondIteration.add(item); + } + + assertEquals(firstIteration.size(), secondIteration.size(), "Both iterations should have same size"); + assertEquals(3, firstIteration.size(), "First iteration should have 3 elements"); + assertEquals(3, secondIteration.size(), "Second iteration should have 3 elements"); + } + + @Test + void testMultipleIterators() { + Bag bag = new Bag<>(); + bag.add("item1"); + bag.add("item2"); + + Iterator iter1 = bag.iterator(); + Iterator iter2 = bag.iterator(); + + assertTrue(iter1.hasNext(), "First iterator should have next element"); + assertTrue(iter2.hasNext(), "Second iterator should have next element"); + + String first1 = iter1.next(); + String first2 = iter2.next(); + + org.junit.jupiter.api.Assertions.assertNotNull(first1, "First iterator should return non-null element"); + org.junit.jupiter.api.Assertions.assertNotNull(first2, "Second iterator should return non-null element"); + } + + @Test + void testIteratorHasNextConsistency() { + Bag bag = new Bag<>(); + bag.add("single"); + + Iterator iter = bag.iterator(); + assertTrue(iter.hasNext(), "hasNext should return true"); + assertTrue(iter.hasNext(), "hasNext should still return true after multiple calls"); + + String item = iter.next(); + assertEquals("single", item, "Next should return the single item"); + + assertFalse(iter.hasNext(), "hasNext should return false after consuming element"); + assertFalse(iter.hasNext(), "hasNext should still return false"); + } + + @Test + void testIteratorNextOnEmptyBag() { + Bag bag = new Bag<>(); + Iterator iter = bag.iterator(); + + assertFalse(iter.hasNext(), "hasNext should return false for empty bag"); + assertThrows(NoSuchElementException.class, iter::next, "next() should throw NoSuchElementException on empty bag"); + } + + @Test + void testBagOrderIndependence() { + Bag bag1 = new Bag<>(); + Bag bag2 = new Bag<>(); + + // Add same elements in different order + bag1.add("first"); + bag1.add("second"); + bag1.add("third"); + + bag2.add("third"); + bag2.add("first"); + bag2.add("second"); + + assertEquals(bag1.size(), bag2.size(), "Bags should have same size"); + + // Both bags should contain all elements + assertTrue(bag1.contains("first") && bag2.contains("first")); + assertTrue(bag1.contains("second") && bag2.contains("second")); + assertTrue(bag1.contains("third") && bag2.contains("third")); + } } diff --git a/src/test/java/com/thealgorithms/datastructures/bloomfilter/BloomFilterTest.java b/src/test/java/com/thealgorithms/datastructures/bloomfilter/BloomFilterTest.java index 048eb7e481a7..9e1ba88adaee 100644 --- a/src/test/java/com/thealgorithms/datastructures/bloomfilter/BloomFilterTest.java +++ b/src/test/java/com/thealgorithms/datastructures/bloomfilter/BloomFilterTest.java @@ -1,5 +1,12 @@ package com.thealgorithms.datastructures.bloomfilter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -113,4 +120,140 @@ void testBoundaryConditions() { Assertions.assertTrue(filter.contains("d"), "Filter should contain 'd'"); Assertions.assertFalse(filter.contains("e"), "Filter should not contain 'e' which was not inserted"); } + + @Test + void testLongDataType() { + BloomFilter filter = new BloomFilter<>(5, 1000); + Long[] values = {Long.MIN_VALUE, Long.MAX_VALUE}; + + for (Long value : values) { + filter.insert(value); + } + + for (Long value : values) { + Assertions.assertTrue(filter.contains(value), "Filter should contain " + value); + } + } + + @Test + void testFloatDataType() { + BloomFilter filter = new BloomFilter<>(3, 200); + Float[] values = {1.5f, -3.7f, 0.0f, Float.MAX_VALUE, Float.MIN_VALUE}; + + for (Float value : values) { + filter.insert(value); + } + + for (Float value : values) { + Assertions.assertTrue(filter.contains(value), "Filter should contain " + value); + } + + Assertions.assertFalse(filter.contains(88.88f), "Filter should not contain uninserted value"); + } + + @Test + void testBooleanDataType() { + BloomFilter filter = new BloomFilter<>(2, 50); + filter.insert(Boolean.TRUE); + filter.insert(Boolean.FALSE); + + Assertions.assertTrue(filter.contains(Boolean.TRUE), "Filter should contain true"); + Assertions.assertTrue(filter.contains(Boolean.FALSE), "Filter should contain false"); + } + + @Test + void testListDataType() { + BloomFilter> filter = new BloomFilter<>(4, 200); + List list1 = Arrays.asList("apple", "banana"); + List list2 = Arrays.asList("cat", "dog"); + List emptyList = new ArrayList<>(); + + filter.insert(list1); + filter.insert(list2); + filter.insert(emptyList); + + Assertions.assertTrue(filter.contains(list1), "Filter should contain list1"); + Assertions.assertTrue(filter.contains(list2), "Filter should contain list2"); + Assertions.assertTrue(filter.contains(emptyList), "Filter should contain empty list"); + Assertions.assertFalse(filter.contains(Arrays.asList("elephant", "tiger")), "Filter should not contain uninserted list"); + } + + @Test + void testMapDataType() { + BloomFilter> filter = new BloomFilter<>(3, 150); + Map map1 = new HashMap<>(); + map1.put("key1", 1); + map1.put("key2", 2); + + Map map2 = new HashMap<>(); + map2.put("key3", 3); + + Map emptyMap = new HashMap<>(); + + filter.insert(map1); + filter.insert(map2); + filter.insert(emptyMap); + + Assertions.assertTrue(filter.contains(map1), "Filter should contain map1"); + Assertions.assertTrue(filter.contains(map2), "Filter should contain map2"); + Assertions.assertTrue(filter.contains(emptyMap), "Filter should contain empty map"); + } + + @Test + void testSetDataType() { + BloomFilter> filter = new BloomFilter<>(3, 100); + Set set1 = new HashSet<>(Arrays.asList(1, 2, 3)); + Set set2 = new HashSet<>(Arrays.asList(4, 5)); + Set emptySet = new HashSet<>(); + + filter.insert(set1); + filter.insert(set2); + filter.insert(emptySet); + + Assertions.assertTrue(filter.contains(set1), "Filter should contain set1"); + Assertions.assertTrue(filter.contains(set2), "Filter should contain set2"); + Assertions.assertTrue(filter.contains(emptySet), "Filter should contain empty set"); + Assertions.assertFalse(filter.contains(new HashSet<>(Arrays.asList(6, 7, 8))), "Filter should not contain uninserted set"); + } + + @Test + void testArrayDataType() { + BloomFilter filter = new BloomFilter<>(3, 100); + int[] array1 = {1, 2, 3}; + int[] array2 = {4, 5}; + int[] emptyArray = {}; + + filter.insert(array1); + filter.insert(array2); + filter.insert(emptyArray); + + Assertions.assertTrue(filter.contains(array1), "Filter should contain array1"); + Assertions.assertTrue(filter.contains(array2), "Filter should contain array2"); + Assertions.assertTrue(filter.contains(emptyArray), "Filter should contain empty array"); + Assertions.assertFalse(filter.contains(new int[] {6, 7, 8}), "Filter should not contain different array"); + } + + @Test + void testSpecialFloatingPointValues() { + BloomFilter filter = new BloomFilter<>(3, 100); + Double[] specialValues = {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, -0.0, 0.0}; + + for (Double value : specialValues) { + filter.insert(value); + } + + for (Double value : specialValues) { + Assertions.assertTrue(filter.contains(value), "Filter should contain " + value); + } + } + + @Test + void testVerySmallBloomFilter() { + BloomFilter smallFilter = new BloomFilter<>(1, 5); + smallFilter.insert("test1"); + smallFilter.insert("test2"); + + Assertions.assertTrue(smallFilter.contains("test1")); + Assertions.assertTrue(smallFilter.contains("test2")); + } } diff --git a/src/test/java/com/thealgorithms/datastructures/buffers/CircularBufferTest.java b/src/test/java/com/thealgorithms/datastructures/buffers/CircularBufferTest.java index b115fc187b1a..69af422e7175 100644 --- a/src/test/java/com/thealgorithms/datastructures/buffers/CircularBufferTest.java +++ b/src/test/java/com/thealgorithms/datastructures/buffers/CircularBufferTest.java @@ -2,7 +2,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -68,11 +67,11 @@ void testFullBuffer() { @Test void testIllegalArguments() { - assertThrows(IllegalArgumentException.class, () -> new CircularBuffer<>(0)); - assertThrows(IllegalArgumentException.class, () -> new CircularBuffer<>(-1)); + org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, () -> new CircularBuffer<>(0)); + org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, () -> new CircularBuffer<>(-1)); CircularBuffer buffer = new CircularBuffer<>(1); - assertThrows(IllegalArgumentException.class, () -> buffer.put(null)); + org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, () -> buffer.put(null)); } @Test @@ -85,4 +84,149 @@ void testLargeBuffer() { buffer.put(1000); // This should overwrite 0 assertEquals(1, buffer.get()); } + + @Test + void testPutAfterGet() { + CircularBuffer buffer = new CircularBuffer<>(2); + buffer.put(10); + buffer.put(20); + assertEquals(10, buffer.get()); + buffer.put(30); + assertEquals(20, buffer.get()); + assertEquals(30, buffer.get()); + assertNull(buffer.get()); + } + + @Test + void testMultipleWrapArounds() { + CircularBuffer buffer = new CircularBuffer<>(3); + for (int i = 1; i <= 6; i++) { + buffer.put(i); + buffer.get(); // add and immediately remove + } + assertTrue(buffer.isEmpty()); + assertNull(buffer.get()); + } + + @Test + void testOverwriteMultipleTimes() { + CircularBuffer buffer = new CircularBuffer<>(2); + buffer.put("X"); + buffer.put("Y"); + buffer.put("Z"); // overwrites "X" + buffer.put("W"); // overwrites "Y" + assertEquals("Z", buffer.get()); + assertEquals("W", buffer.get()); + assertNull(buffer.get()); + } + + @Test + void testIsEmptyAndIsFullTransitions() { + CircularBuffer buffer = new CircularBuffer<>(2); + assertTrue(buffer.isEmpty()); + org.junit.jupiter.api.Assertions.assertFalse(buffer.isFull()); + + buffer.put(1); + org.junit.jupiter.api.Assertions.assertFalse(buffer.isEmpty()); + org.junit.jupiter.api.Assertions.assertFalse(buffer.isFull()); + + buffer.put(2); + assertTrue(buffer.isFull()); + + buffer.get(); + org.junit.jupiter.api.Assertions.assertFalse(buffer.isFull()); + + buffer.get(); + assertTrue(buffer.isEmpty()); + } + + @Test + void testInterleavedPutAndGet() { + CircularBuffer buffer = new CircularBuffer<>(3); + buffer.put("A"); + buffer.put("B"); + assertEquals("A", buffer.get()); + buffer.put("C"); + assertEquals("B", buffer.get()); + assertEquals("C", buffer.get()); + assertNull(buffer.get()); + } + + @Test + void testRepeatedNullInsertionThrows() { + CircularBuffer buffer = new CircularBuffer<>(5); + for (int i = 0; i < 3; i++) { + int finalI = i; + org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, () -> buffer.put(null), "Iteration: " + finalI); + } + } + @Test + void testFillThenEmptyThenReuseBuffer() { + CircularBuffer buffer = new CircularBuffer<>(3); + + buffer.put(1); + buffer.put(2); + buffer.put(3); + assertTrue(buffer.isFull()); + + assertEquals(1, buffer.get()); + assertEquals(2, buffer.get()); + assertEquals(3, buffer.get()); + + assertTrue(buffer.isEmpty()); + + buffer.put(4); + buffer.put(5); + assertEquals(4, buffer.get()); + assertEquals(5, buffer.get()); + assertTrue(buffer.isEmpty()); + } + + @Test + void testPutReturnsTrueOnlyIfPreviouslyEmpty() { + CircularBuffer buffer = new CircularBuffer<>(2); + + assertTrue(buffer.put("one")); // was empty + org.junit.jupiter.api.Assertions.assertFalse(buffer.put("two")); // not empty + org.junit.jupiter.api.Assertions.assertFalse(buffer.put("three")); // overwrite + } + + @Test + void testOverwriteAndGetAllElementsCorrectly() { + CircularBuffer buffer = new CircularBuffer<>(3); + + buffer.put(1); + buffer.put(2); + buffer.put(3); + buffer.put(4); // Overwrites 1 + buffer.put(5); // Overwrites 2 + + assertEquals(3, buffer.get()); + assertEquals(4, buffer.get()); + assertEquals(5, buffer.get()); + assertNull(buffer.get()); + } + + @Test + void testBufferWithOneElementCapacity() { + CircularBuffer buffer = new CircularBuffer<>(1); + + assertTrue(buffer.put("first")); + assertEquals("first", buffer.get()); + assertNull(buffer.get()); + + assertTrue(buffer.put("second")); + assertEquals("second", buffer.get()); + } + + @Test + void testPointerWraparoundWithExactMultipleOfCapacity() { + CircularBuffer buffer = new CircularBuffer<>(3); + for (int i = 0; i < 6; i++) { + buffer.put(i); + buffer.get(); // keep buffer size at 0 + } + assertTrue(buffer.isEmpty()); + assertNull(buffer.get()); + } } diff --git a/src/test/java/com/thealgorithms/datastructures/caches/LIFOCacheTest.java b/src/test/java/com/thealgorithms/datastructures/caches/LIFOCacheTest.java new file mode 100644 index 000000000000..df60a393b136 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/caches/LIFOCacheTest.java @@ -0,0 +1,341 @@ +package com.thealgorithms.datastructures.caches; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +class LIFOCacheTest { + private LIFOCache cache; + private Set evictedKeys; + private List evictedValues; + + @BeforeEach + void setUp() { + evictedKeys = new HashSet<>(); + evictedValues = new ArrayList<>(); + + cache = new LIFOCache.Builder(3) + .defaultTTL(1000) + .evictionListener((k, v) -> { + evictedKeys.add(k); + evictedValues.add(v); + }) + .build(); + } + + @Test + void testPutAndGet() { + cache.put("a", "apple"); + Assertions.assertEquals("apple", cache.get("a")); + } + + @Test + void testOverwriteValue() { + cache.put("a", "apple"); + cache.put("a", "avocado"); + Assertions.assertEquals("avocado", cache.get("a")); + } + + @Test + void testExpiration() throws InterruptedException { + cache.put("temp", "value", 100); + Thread.sleep(200); + Assertions.assertNull(cache.get("temp")); + Assertions.assertTrue(evictedKeys.contains("temp")); + } + + @Test + void testEvictionOnCapacity() { + cache.put("a", "alpha"); + cache.put("b", "bravo"); + cache.put("c", "charlie"); + cache.put("d", "delta"); + + int size = cache.size(); + Assertions.assertEquals(3, size); + Assertions.assertEquals(1, evictedKeys.size()); + Assertions.assertEquals(1, evictedValues.size()); + Assertions.assertEquals("charlie", evictedValues.getFirst()); + } + + @Test + void testEvictionListener() { + cache.put("x", "one"); + cache.put("y", "two"); + cache.put("z", "three"); + cache.put("w", "four"); + + Assertions.assertFalse(evictedKeys.isEmpty()); + Assertions.assertFalse(evictedValues.isEmpty()); + } + + @Test + void testHitsAndMisses() { + cache.put("a", "apple"); + Assertions.assertEquals("apple", cache.get("a")); + Assertions.assertNull(cache.get("b")); + Assertions.assertEquals(1, cache.getHits()); + Assertions.assertEquals(1, cache.getMisses()); + } + + @Test + void testSizeExcludesExpired() throws InterruptedException { + cache.put("a", "a", 100); + cache.put("b", "b", 100); + cache.put("c", "c", 100); + Thread.sleep(150); + Assertions.assertEquals(0, cache.size()); + } + + @Test + void testSizeIncludesFresh() { + cache.put("a", "a", 1000); + cache.put("b", "b", 1000); + cache.put("c", "c", 1000); + Assertions.assertEquals(3, cache.size()); + } + + @Test + void testToStringDoesNotExposeExpired() throws InterruptedException { + cache.put("live", "alive"); + cache.put("dead", "gone", 100); + Thread.sleep(150); + String result = cache.toString(); + Assertions.assertTrue(result.contains("live")); + Assertions.assertFalse(result.contains("dead")); + } + + @Test + void testNullKeyGetThrows() { + Assertions.assertThrows(IllegalArgumentException.class, () -> cache.get(null)); + } + + @Test + void testPutNullKeyThrows() { + Assertions.assertThrows(IllegalArgumentException.class, () -> cache.put(null, "v")); + } + + @Test + void testPutNullValueThrows() { + Assertions.assertThrows(IllegalArgumentException.class, () -> cache.put("k", null)); + } + + @Test + void testPutNegativeTTLThrows() { + Assertions.assertThrows(IllegalArgumentException.class, () -> cache.put("k", "v", -1)); + } + + @Test + void testBuilderNegativeCapacityThrows() { + Assertions.assertThrows(IllegalArgumentException.class, () -> new LIFOCache.Builder<>(0)); + } + + @Test + void testBuilderNullEvictionListenerThrows() { + LIFOCache.Builder builder = new LIFOCache.Builder<>(1); + Assertions.assertThrows(IllegalArgumentException.class, () -> builder.evictionListener(null)); + } + + @Test + void testEvictionListenerExceptionDoesNotCrash() { + LIFOCache listenerCache = new LIFOCache.Builder(1).evictionListener((k, v) -> { throw new RuntimeException("Exception"); }).build(); + + listenerCache.put("a", "a"); + listenerCache.put("b", "b"); + Assertions.assertDoesNotThrow(() -> listenerCache.get("a")); + } + + @Test + void testTtlZeroThrowsIllegalArgumentException() { + Executable exec = () -> new LIFOCache.Builder(3).defaultTTL(-1).build(); + Assertions.assertThrows(IllegalArgumentException.class, exec); + } + + @Test + void testPeriodicEvictionStrategyEvictsAtInterval() throws InterruptedException { + LIFOCache periodicCache = new LIFOCache.Builder(10).defaultTTL(50).evictionStrategy(new LIFOCache.PeriodicEvictionStrategy<>(3)).build(); + + periodicCache.put("x", "1"); + Thread.sleep(100); + int ev1 = periodicCache.getEvictionStrategy().onAccess(periodicCache); + int ev2 = periodicCache.getEvictionStrategy().onAccess(periodicCache); + int ev3 = periodicCache.getEvictionStrategy().onAccess(periodicCache); + + Assertions.assertEquals(0, ev1); + Assertions.assertEquals(0, ev2); + Assertions.assertEquals(1, ev3, "Eviction should happen on the 3rd access"); + Assertions.assertEquals(0, periodicCache.size()); + } + + @Test + void testPeriodicEvictionStrategyThrowsExceptionIfIntervalLessThanOrEqual0() { + Executable executable = () -> new LIFOCache.Builder(10).defaultTTL(50).evictionStrategy(new LIFOCache.PeriodicEvictionStrategy<>(0)).build(); + + Assertions.assertThrows(IllegalArgumentException.class, executable); + } + + @Test + void testImmediateEvictionStrategyStrategyEvictsOnEachCall() throws InterruptedException { + LIFOCache immediateEvictionStrategy = new LIFOCache.Builder(10).defaultTTL(50).evictionStrategy(new LIFOCache.ImmediateEvictionStrategy<>()).build(); + + immediateEvictionStrategy.put("x", "1"); + Thread.sleep(100); + int evicted = immediateEvictionStrategy.getEvictionStrategy().onAccess(immediateEvictionStrategy); + + Assertions.assertEquals(1, evicted); + } + + @Test + void testBuilderThrowsExceptionIfEvictionStrategyNull() { + Executable executable = () -> new LIFOCache.Builder(10).defaultTTL(50).evictionStrategy(null).build(); + + Assertions.assertThrows(IllegalArgumentException.class, executable); + } + + @Test + void testReturnsCorrectStrategyInstance() { + LIFOCache.EvictionStrategy strategy = new LIFOCache.ImmediateEvictionStrategy<>(); + + LIFOCache newCache = new LIFOCache.Builder(10).defaultTTL(1000).evictionStrategy(strategy).build(); + + Assertions.assertSame(strategy, newCache.getEvictionStrategy(), "Returned strategy should be the same instance"); + } + + @Test + void testDefaultStrategyIsImmediateEvictionStrategy() { + LIFOCache newCache = new LIFOCache.Builder(5).defaultTTL(1000).build(); + + Assertions.assertInstanceOf(LIFOCache.ImmediateEvictionStrategy.class, newCache.getEvictionStrategy(), "Default strategy should be ImmediateEvictionStrategyStrategy"); + } + + @Test + void testGetEvictionStrategyIsNotNull() { + LIFOCache newCache = new LIFOCache.Builder(5).build(); + + Assertions.assertNotNull(newCache.getEvictionStrategy(), "Eviction strategy should never be null"); + } + + @Test + void testRemoveKeyRemovesExistingKey() { + cache.put("A", "Alpha"); + cache.put("B", "Beta"); + + Assertions.assertEquals("Alpha", cache.get("A")); + Assertions.assertEquals("Beta", cache.get("B")); + + String removed = cache.removeKey("A"); + Assertions.assertEquals("Alpha", removed); + + Assertions.assertNull(cache.get("A")); + Assertions.assertEquals(1, cache.size()); + } + + @Test + void testRemoveKeyReturnsNullIfKeyNotPresent() { + cache.put("X", "X-ray"); + + Assertions.assertNull(cache.removeKey("NonExistent")); + Assertions.assertEquals(1, cache.size()); + } + + @Test + void testRemoveKeyHandlesExpiredEntry() throws InterruptedException { + LIFOCache expiringCache = new LIFOCache.Builder(2).defaultTTL(100).evictionStrategy(new LIFOCache.ImmediateEvictionStrategy<>()).build(); + + expiringCache.put("T", "Temporary"); + + Thread.sleep(200); + + String removed = expiringCache.removeKey("T"); + Assertions.assertEquals("Temporary", removed); + Assertions.assertNull(expiringCache.get("T")); + } + + @Test + void testRemoveKeyThrowsIfKeyIsNull() { + Assertions.assertThrows(IllegalArgumentException.class, () -> cache.removeKey(null)); + } + + @Test + void testRemoveKeyTriggersEvictionListener() { + AtomicInteger evictedCount = new AtomicInteger(); + + LIFOCache localCache = new LIFOCache.Builder(2).evictionListener((key, value) -> evictedCount.incrementAndGet()).build(); + + localCache.put("A", "Apple"); + localCache.put("B", "Banana"); + + localCache.removeKey("A"); + + Assertions.assertEquals(1, evictedCount.get(), "Eviction listener should have been called once"); + } + + @Test + void testRemoveKeyDoestNotAffectOtherKeys() { + cache.put("A", "Alpha"); + cache.put("B", "Beta"); + cache.put("C", "Gamma"); + + cache.removeKey("B"); + + Assertions.assertEquals("Alpha", cache.get("A")); + Assertions.assertNull(cache.get("B")); + Assertions.assertEquals("Gamma", cache.get("C")); + } + + @Test + void testEvictionListenerExceptionDoesNotPropagate() { + LIFOCache localCache = new LIFOCache.Builder(1).evictionListener((key, value) -> { throw new RuntimeException(); }).build(); + + localCache.put("A", "Apple"); + + Assertions.assertDoesNotThrow(() -> localCache.put("B", "Beta")); + } + + @Test + void testGetKeysReturnsAllFreshKeys() { + cache.put("A", "Alpha"); + cache.put("B", "Beta"); + cache.put("G", "Gamma"); + + Set expectedKeys = Set.of("A", "B", "G"); + Assertions.assertEquals(expectedKeys, cache.getAllKeys()); + } + + @Test + void testGetKeysIgnoresExpiredKeys() throws InterruptedException { + cache.put("A", "Alpha"); + cache.put("B", "Beta"); + cache.put("G", "Gamma", 100); + + Set expectedKeys = Set.of("A", "B"); + Thread.sleep(200); + Assertions.assertEquals(expectedKeys, cache.getAllKeys()); + } + + @Test + void testClearRemovesAllEntries() { + cache.put("A", "Alpha"); + cache.put("B", "Beta"); + cache.put("G", "Gamma"); + + cache.clear(); + Assertions.assertEquals(0, cache.size()); + } + + @Test + void testGetExpiredKeyIncrementsMissesCount() throws InterruptedException { + LIFOCache localCache = new LIFOCache.Builder(3).evictionStrategy(cache -> 0).defaultTTL(10).build(); + localCache.put("A", "Alpha"); + Thread.sleep(100); + String value = localCache.get("A"); + Assertions.assertEquals(1, localCache.getMisses()); + Assertions.assertNull(value); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/disjointsetunion/DisjointSetUnionTest.java b/src/test/java/com/thealgorithms/datastructures/disjointsetunion/DisjointSetUnionTest.java index a10a99d40496..581bac6151dd 100644 --- a/src/test/java/com/thealgorithms/datastructures/disjointsetunion/DisjointSetUnionTest.java +++ b/src/test/java/com/thealgorithms/datastructures/disjointsetunion/DisjointSetUnionTest.java @@ -1,8 +1,9 @@ package com.thealgorithms.datastructures.disjointsetunion; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class DisjointSetUnionTest { @@ -12,7 +13,7 @@ public void testMakeSet() { DisjointSetUnion dsu = new DisjointSetUnion<>(); Node node = dsu.makeSet(1); assertNotNull(node); - Assertions.assertEquals(node, node.parent); + assertEquals(node, node.parent); } @Test @@ -33,19 +34,197 @@ public void testUnionFindSet() { Node root3 = dsu.findSet(node3); Node root4 = dsu.findSet(node4); - Assertions.assertEquals(node1, node1.parent); - Assertions.assertEquals(node1, node2.parent); - Assertions.assertEquals(node1, node3.parent); - Assertions.assertEquals(node1, node4.parent); + assertEquals(node1, node1.parent); + assertEquals(node1, node2.parent); + assertEquals(node1, node3.parent); + assertEquals(node1, node4.parent); - Assertions.assertEquals(node1, root1); - Assertions.assertEquals(node1, root2); - Assertions.assertEquals(node1, root3); - Assertions.assertEquals(node1, root4); + assertEquals(node1, root1); + assertEquals(node1, root2); + assertEquals(node1, root3); + assertEquals(node1, root4); - Assertions.assertEquals(1, node1.rank); - Assertions.assertEquals(0, node2.rank); - Assertions.assertEquals(0, node3.rank); - Assertions.assertEquals(0, node4.rank); + assertEquals(1, node1.rank); + assertEquals(0, node2.rank); + assertEquals(0, node3.rank); + assertEquals(0, node4.rank); + } + + @Test + public void testFindSetOnSingleNode() { + DisjointSetUnion dsu = new DisjointSetUnion<>(); + Node node = dsu.makeSet("A"); + assertEquals(node, dsu.findSet(node)); + } + + @Test + public void testUnionAlreadyConnectedNodes() { + DisjointSetUnion dsu = new DisjointSetUnion<>(); + Node node1 = dsu.makeSet(1); + Node node2 = dsu.makeSet(2); + Node node3 = dsu.makeSet(3); + + dsu.unionSets(node1, node2); + dsu.unionSets(node2, node3); + + // Union nodes that are already connected + dsu.unionSets(node1, node3); + + // All should have the same root + Node root = dsu.findSet(node1); + assertEquals(root, dsu.findSet(node2)); + assertEquals(root, dsu.findSet(node3)); + } + + @Test + public void testRankIncrease() { + DisjointSetUnion dsu = new DisjointSetUnion<>(); + Node node1 = dsu.makeSet(1); + Node node2 = dsu.makeSet(2); + Node node3 = dsu.makeSet(3); + Node node4 = dsu.makeSet(4); + + dsu.unionSets(node1, node2); // rank of node1 should increase + dsu.unionSets(node3, node4); // rank of node3 should increase + dsu.unionSets(node1, node3); // union two trees of same rank -> rank increases + + assertEquals(2, dsu.findSet(node1).rank); + } + + @Test + public void testMultipleMakeSets() { + DisjointSetUnion dsu = new DisjointSetUnion<>(); + Node node1 = dsu.makeSet(1); + Node node2 = dsu.makeSet(2); + Node node3 = dsu.makeSet(3); + + assertNotEquals(node1, node2); + assertNotEquals(node2, node3); + assertNotEquals(node1, node3); + + assertEquals(node1, node1.parent); + assertEquals(node2, node2.parent); + assertEquals(node3, node3.parent); + } + + @Test + public void testPathCompression() { + DisjointSetUnion dsu = new DisjointSetUnion<>(); + Node node1 = dsu.makeSet(1); + Node node2 = dsu.makeSet(2); + Node node3 = dsu.makeSet(3); + + dsu.unionSets(node1, node2); + dsu.unionSets(node2, node3); + + // After findSet, path compression should update parent to root directly + Node root = dsu.findSet(node3); + assertEquals(root, node1); + assertEquals(node1, node3.parent); + } + + @Test + public void testUnionByRankSmallerToLarger() { + DisjointSetUnion dsu = new DisjointSetUnion<>(); + Node node1 = dsu.makeSet(1); + Node node2 = dsu.makeSet(2); + Node node3 = dsu.makeSet(3); + + // Create tree with node1 as root and rank 1 + dsu.unionSets(node1, node2); + assertEquals(1, node1.rank); + assertEquals(0, node2.rank); + + // Union single node (rank 0) with tree (rank 1) + // Smaller rank tree should attach to larger rank tree + dsu.unionSets(node3, node1); + assertEquals(node1, dsu.findSet(node3)); + assertEquals(1, node1.rank); // Rank should not increase + } + + @Test + public void testUnionByRankEqualRanks() { + DisjointSetUnion dsu = new DisjointSetUnion<>(); + Node node1 = dsu.makeSet(1); + Node node2 = dsu.makeSet(2); + Node node3 = dsu.makeSet(3); + Node node4 = dsu.makeSet(4); + + // Create two trees of equal rank (1) + dsu.unionSets(node1, node2); + dsu.unionSets(node3, node4); + assertEquals(1, node1.rank); + assertEquals(1, node3.rank); + + // Union two trees of equal rank + dsu.unionSets(node1, node3); + Node root = dsu.findSet(node1); + assertEquals(2, root.rank); // Rank should increase by 1 + } + + @Test + public void testLargeChainPathCompression() { + DisjointSetUnion dsu = new DisjointSetUnion<>(); + Node node1 = dsu.makeSet(1); + Node node2 = dsu.makeSet(2); + Node node3 = dsu.makeSet(3); + Node node4 = dsu.makeSet(4); + Node node5 = dsu.makeSet(5); + + // Create a long chain: 1 -> 2 -> 3 -> 4 -> 5 + dsu.unionSets(node1, node2); + dsu.unionSets(node2, node3); + dsu.unionSets(node3, node4); + dsu.unionSets(node4, node5); + + // Find from the deepest node + Node root = dsu.findSet(node5); + + // Path compression should make all nodes point directly to root + assertEquals(root, node5.parent); + assertEquals(root, node4.parent); + assertEquals(root, node3.parent); + assertEquals(root, node2.parent); + assertEquals(root, node1.parent); + } + + @Test + public void testMultipleDisjointSets() { + DisjointSetUnion dsu = new DisjointSetUnion<>(); + Node node1 = dsu.makeSet(1); + Node node2 = dsu.makeSet(2); + Node node3 = dsu.makeSet(3); + Node node4 = dsu.makeSet(4); + Node node5 = dsu.makeSet(5); + Node node6 = dsu.makeSet(6); + + // Create two separate components + dsu.unionSets(node1, node2); + dsu.unionSets(node2, node3); + + dsu.unionSets(node4, node5); + dsu.unionSets(node5, node6); + + // Verify they are separate + assertEquals(dsu.findSet(node1), dsu.findSet(node2)); + assertEquals(dsu.findSet(node2), dsu.findSet(node3)); + assertEquals(dsu.findSet(node4), dsu.findSet(node5)); + assertEquals(dsu.findSet(node5), dsu.findSet(node6)); + + assertNotEquals(dsu.findSet(node1), dsu.findSet(node4)); + assertNotEquals(dsu.findSet(node3), dsu.findSet(node6)); + } + + @Test + public void testEmptyValues() { + DisjointSetUnion dsu = new DisjointSetUnion<>(); + Node emptyNode = dsu.makeSet(""); + Node nullNode = dsu.makeSet(null); + + assertEquals(emptyNode, dsu.findSet(emptyNode)); + assertEquals(nullNode, dsu.findSet(nullNode)); + + dsu.unionSets(emptyNode, nullNode); + assertEquals(dsu.findSet(emptyNode), dsu.findSet(nullNode)); } } diff --git a/src/test/java/com/thealgorithms/datastructures/lists/CountSinglyLinkedListRecursionTest.java b/src/test/java/com/thealgorithms/datastructures/lists/CountSinglyLinkedListRecursionTest.java index 1a3efe8a5572..3d3f62fc5132 100644 --- a/src/test/java/com/thealgorithms/datastructures/lists/CountSinglyLinkedListRecursionTest.java +++ b/src/test/java/com/thealgorithms/datastructures/lists/CountSinglyLinkedListRecursionTest.java @@ -1,8 +1,10 @@ package com.thealgorithms.datastructures.lists; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; public class CountSinglyLinkedListRecursionTest { @@ -15,35 +17,112 @@ public void setUp() { } @Test + @DisplayName("Count of an empty list should be 0") public void testCountEmptyList() { - // An empty list should have a count of 0 - assertEquals(0, list.count(), "Count of an empty list should be 0."); + assertEquals(0, list.count()); } @Test + @DisplayName("Count after inserting a single element should be 1") public void testCountSingleElementList() { - // Insert a single element and check the count list.insert(1); - assertEquals(1, list.count(), "Count of a single-element list should be 1."); + assertEquals(1, list.count()); } @Test + @DisplayName("Count after inserting multiple distinct elements") public void testCountMultipleElements() { - // Insert multiple elements and check the count for (int i = 1; i <= 5; i++) { list.insert(i); } - assertEquals(5, list.count(), "Count of a list with 5 elements should be 5."); + assertEquals(5, list.count()); } @Test + @DisplayName("Count should reflect total number of nodes with duplicate values") public void testCountWithDuplicateElements() { - // Insert duplicate elements and verify the count is correct - list.insert(1); list.insert(2); list.insert(2); list.insert(3); list.insert(3); - assertEquals(5, list.count(), "Count of a list with duplicate elements should match total node count."); + list.insert(1); + assertEquals(5, list.count()); + } + + @Test + @DisplayName("Count should return 0 after clearing the list") + public void testCountAfterClearingList() { + for (int i = 1; i <= 4; i++) { + list.insert(i); + } + list.clear(); // assumed to exist + assertEquals(0, list.count()); + } + + @Test + @DisplayName("Count on a very large list should be accurate") + public void testCountOnVeryLargeList() { + int n = 1000; + for (int i = 0; i < n; i++) { + list.insert(i); + } + assertEquals(n, list.count()); + } + + @Test + @DisplayName("Count should work correctly with negative values") + public void testCountOnListWithNegativeNumbers() { + list.insert(-1); + list.insert(-2); + list.insert(-3); + assertEquals(3, list.count()); + } + + @Test + @DisplayName("Calling count multiple times should return the same value if list is unchanged") + public void testCountIsConsistentWithoutModification() { + list.insert(1); + list.insert(2); + int count1 = list.count(); + int count2 = list.count(); + assertEquals(count1, count2); + } + + @Test + @DisplayName("Count should reflect total even if all values are the same") + public void testCountAllSameValues() { + for (int i = 0; i < 5; i++) { + list.insert(42); + } + assertEquals(5, list.count()); + } + + @Test + @DisplayName("Count should remain correct after multiple interleaved insert and count operations") + public void testCountAfterEachInsert() { + assertEquals(0, list.count()); + list.insert(1); + assertEquals(1, list.count()); + list.insert(2); + assertEquals(2, list.count()); + list.insert(3); + assertEquals(3, list.count()); + } + + @Test + @DisplayName("List should not throw on edge count (0 nodes)") + public void testEdgeCaseNoElements() { + assertDoesNotThrow(() -> list.count()); + } + + @Test + @DisplayName("Should count accurately after inserting then removing all elements") + public void testCountAfterInsertAndClear() { + for (int i = 0; i < 10; i++) { + list.insert(i); + } + assertEquals(10, list.count()); + list.clear(); + assertEquals(0, list.count()); } } diff --git a/src/test/java/com/thealgorithms/datastructures/lists/CursorLinkedListTest.java b/src/test/java/com/thealgorithms/datastructures/lists/CursorLinkedListTest.java index bf5501826994..20bf24d79159 100644 --- a/src/test/java/com/thealgorithms/datastructures/lists/CursorLinkedListTest.java +++ b/src/test/java/com/thealgorithms/datastructures/lists/CursorLinkedListTest.java @@ -109,4 +109,189 @@ void testMemoryLimitExceeded() { } }); } + + @Test + void testSingleElementOperations() { + // Test operations with just one element + list.append("Only"); + assertEquals("Only", list.get(0)); + assertEquals(0, list.indexOf("Only")); + + list.remove("Only"); + assertNull(list.get(0)); + assertEquals(-1, list.indexOf("Only")); + } + + @Test + void testDuplicateElements() { + // Test handling of duplicate elements + list.append("Duplicate"); + list.append("Other"); + list.append("Duplicate"); + + assertEquals(0, list.indexOf("Duplicate")); // Should return first occurrence + assertEquals("Duplicate", list.get(0)); + assertEquals("Duplicate", list.get(2)); + + list.remove("Duplicate"); // Should remove first occurrence + assertEquals("Other", list.get(0)); + assertEquals("Duplicate", list.get(1)); + } + + @Test + void testRemoveByIndexEdgeCases() { + list.append("First"); + list.append("Second"); + list.append("Third"); + + // Test removing invalid indices + list.removeByIndex(-1); // Should not crash + list.removeByIndex(10); // Should not crash + + // Verify list unchanged + assertEquals("First", list.get(0)); + assertEquals("Second", list.get(1)); + assertEquals("Third", list.get(2)); + + // Test removing first element by index + list.removeByIndex(0); + assertEquals("Second", list.get(0)); + assertEquals("Third", list.get(1)); + } + + @Test + void testRemoveByIndexLastElement() { + list.append("First"); + list.append("Second"); + list.append("Third"); + + // Remove last element by index + list.removeByIndex(2); + assertEquals("First", list.get(0)); + assertEquals("Second", list.get(1)); + assertNull(list.get(2)); + } + + @Test + void testConsecutiveOperations() { + // Test multiple consecutive operations + list.append("A"); + list.append("B"); + list.append("C"); + list.append("D"); + + list.remove("B"); + list.remove("D"); + + assertEquals("A", list.get(0)); + assertEquals("C", list.get(1)); + assertNull(list.get(2)); + + list.append("E"); + assertEquals("E", list.get(2)); + } + + @Test + void testMemoryReclamation() { + // Test that removed elements free up memory space + for (int i = 0; i < 50; i++) { + list.append("Element" + i); + } + + // Remove some elements + for (int i = 0; i < 25; i++) { + list.remove("Element" + i); + } + + // Should be able to add more elements (testing memory reclamation) + for (int i = 100; i < 150; i++) { + list.append("New" + i); + } + + // Verify some elements exist + assertEquals("Element25", list.get(0)); + assertEquals("New100", list.get(25)); + } + + @Test + void testSpecialCharacters() { + // Test with strings containing special characters + list.append("Hello World!"); + list.append("Test@123"); + list.append("Special#$%"); + list.append(""); // Empty string + + assertEquals("Hello World!", list.get(0)); + assertEquals("Test@123", list.get(1)); + assertEquals("Special#$%", list.get(2)); + assertEquals("", list.get(3)); + + assertEquals(3, list.indexOf("")); + } + + @Test + void testLargeIndices() { + list.append("Test"); + + // Test very large indices + assertNull(list.get(Integer.MAX_VALUE)); + assertNull(list.get(1000)); + } + + @Test + void testSequentialRemovalByIndex() { + list.append("A"); + list.append("B"); + list.append("C"); + list.append("D"); + + // Remove elements sequentially by index + list.removeByIndex(1); // Remove "B" + assertEquals("A", list.get(0)); + assertEquals("C", list.get(1)); + assertEquals("D", list.get(2)); + + list.removeByIndex(1); // Remove "C" + assertEquals("A", list.get(0)); + assertEquals("D", list.get(1)); + assertNull(list.get(2)); + } + + @Test + void testAppendAfterRemoval() { + list.append("First"); + list.append("Second"); + + list.remove("First"); + list.append("Third"); + + assertEquals("Second", list.get(0)); + assertEquals("Third", list.get(1)); + assertEquals(1, list.indexOf("Third")); + } + + @Test + void testPerformanceWithManyOperations() { + // Test with many mixed operations + for (int i = 0; i < 50; i++) { + list.append("Item" + i); + } + + // Remove every other element + for (int i = 0; i < 50; i += 2) { + list.remove("Item" + i); + } + + // Verify remaining elements + assertEquals("Item1", list.get(0)); + assertEquals("Item3", list.get(1)); + assertEquals("Item5", list.get(2)); + + // Add more elements + for (int i = 100; i < 110; i++) { + list.append("New" + i); + } + + assertEquals("New100", list.get(25)); + } } diff --git a/src/test/java/com/thealgorithms/datastructures/lists/MergeKSortedLinkedListTest.java b/src/test/java/com/thealgorithms/datastructures/lists/MergeKSortedLinkedListTest.java index 99a890112d31..8d3150d18ed0 100644 --- a/src/test/java/com/thealgorithms/datastructures/lists/MergeKSortedLinkedListTest.java +++ b/src/test/java/com/thealgorithms/datastructures/lists/MergeKSortedLinkedListTest.java @@ -90,4 +90,88 @@ private int[] getListValues(Node head) { } return Arrays.copyOf(values, i); // return only filled part } + + @Test + void testMergeWithNullListsInArray() { + Node list1 = new Node(1, new Node(3)); + Node list2 = null; + Node list3 = new Node(2, new Node(4)); + Node[] lists = {list1, list2, list3}; + + MergeKSortedLinkedList merger = new MergeKSortedLinkedList(); + Node mergedHead = merger.mergeKList(lists, lists.length); + + int[] expectedValues = {1, 2, 3, 4}; + int[] actualValues = getListValues(mergedHead); + assertArrayEquals(expectedValues, actualValues, "Should handle null lists mixed with valid lists"); + } + + @Test + void testMergeWithDuplicateValues() { + Node list1 = new Node(1, new Node(1, new Node(3))); + Node list2 = new Node(1, new Node(2, new Node(3))); + Node list3 = new Node(3, new Node(3)); + Node[] lists = {list1, list2, list3}; + + MergeKSortedLinkedList merger = new MergeKSortedLinkedList(); + Node mergedHead = merger.mergeKList(lists, lists.length); + + int[] expectedValues = {1, 1, 1, 2, 3, 3, 3, 3}; + int[] actualValues = getListValues(mergedHead); + assertArrayEquals(expectedValues, actualValues, "Should handle duplicate values correctly"); + } + + @Test + void testMergeWithZeroLength() { + Node[] lists = {}; + + MergeKSortedLinkedList merger = new MergeKSortedLinkedList(); + Node mergedHead = merger.mergeKList(lists, 0); + + assertNull(mergedHead, "Should return null for zero-length array"); + } + + @Test + void testMergeWithNegativeNumbers() { + Node list1 = new Node(-5, new Node(-1, new Node(3))); + Node list2 = new Node(-3, new Node(0, new Node(2))); + Node[] lists = {list1, list2}; + + MergeKSortedLinkedList merger = new MergeKSortedLinkedList(); + Node mergedHead = merger.mergeKList(lists, lists.length); + + int[] expectedValues = {-5, -3, -1, 0, 2, 3}; + int[] actualValues = getListValues(mergedHead); + assertArrayEquals(expectedValues, actualValues, "Should handle negative numbers correctly"); + } + + @Test + void testMergeIdenticalLists() { + Node list1 = new Node(1, new Node(2, new Node(3))); + Node list2 = new Node(1, new Node(2, new Node(3))); + Node list3 = new Node(1, new Node(2, new Node(3))); + Node[] lists = {list1, list2, list3}; + + MergeKSortedLinkedList merger = new MergeKSortedLinkedList(); + Node mergedHead = merger.mergeKList(lists, lists.length); + + int[] expectedValues = {1, 1, 1, 2, 2, 2, 3, 3, 3}; + int[] actualValues = getListValues(mergedHead); + assertArrayEquals(expectedValues, actualValues, "Should merge identical lists correctly"); + } + + @Test + void testMergeAlreadySortedSequence() { + Node list1 = new Node(1, new Node(2)); + Node list2 = new Node(3, new Node(4)); + Node list3 = new Node(5, new Node(6)); + Node[] lists = {list1, list2, list3}; + + MergeKSortedLinkedList merger = new MergeKSortedLinkedList(); + Node mergedHead = merger.mergeKList(lists, lists.length); + + int[] expectedValues = {1, 2, 3, 4, 5, 6}; + int[] actualValues = getListValues(mergedHead); + assertArrayEquals(expectedValues, actualValues, "Should handle already sorted sequence"); + } } diff --git a/src/test/java/com/thealgorithms/datastructures/lists/SkipListTest.java b/src/test/java/com/thealgorithms/datastructures/lists/SkipListTest.java index c572739ffbbf..16d1a015a4d9 100644 --- a/src/test/java/com/thealgorithms/datastructures/lists/SkipListTest.java +++ b/src/test/java/com/thealgorithms/datastructures/lists/SkipListTest.java @@ -1,110 +1,115 @@ package com.thealgorithms.datastructures.lists; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Arrays; import java.util.stream.IntStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; class SkipListTest { + private SkipList skipList; + + @BeforeEach + void setUp() { + skipList = new SkipList<>(); + } + @Test - void add() { - SkipList skipList = new SkipList<>(); + @DisplayName("Add element and verify size and retrieval") + void testAdd() { assertEquals(0, skipList.size()); skipList.add("value"); - print(skipList); assertEquals(1, skipList.size()); + assertEquals("value", skipList.get(0)); } @Test - void get() { - SkipList skipList = new SkipList<>(); + @DisplayName("Get retrieves correct element by index") + void testGet() { skipList.add("value"); - - String actualValue = skipList.get(0); - - print(skipList); - assertEquals("value", actualValue); + assertEquals("value", skipList.get(0)); } @Test - void contains() { - SkipList skipList = createSkipList(); - print(skipList); - - boolean contains = skipList.contains("b"); - - assertTrue(contains); + @DisplayName("Contains returns true if element exists") + void testContains() { + skipList = createSkipList(); + assertTrue(skipList.contains("b")); + assertTrue(skipList.contains("a")); + assertFalse(skipList.contains("z")); // negative test } @Test - void removeFromHead() { - SkipList skipList = createSkipList(); - String mostLeftElement = skipList.get(0); + @DisplayName("Remove element from head and check size and order") + void testRemoveFromHead() { + skipList = createSkipList(); + String first = skipList.get(0); int initialSize = skipList.size(); - print(skipList); - skipList.remove(mostLeftElement); + skipList.remove(first); - print(skipList); assertEquals(initialSize - 1, skipList.size()); + assertFalse(skipList.contains(first)); } @Test - void removeFromTail() { - SkipList skipList = createSkipList(); - String mostRightValue = skipList.get(skipList.size() - 1); + @DisplayName("Remove element from tail and check size and order") + void testRemoveFromTail() { + skipList = createSkipList(); + String last = skipList.get(skipList.size() - 1); int initialSize = skipList.size(); - print(skipList); - skipList.remove(mostRightValue); + skipList.remove(last); - print(skipList); assertEquals(initialSize - 1, skipList.size()); + assertFalse(skipList.contains(last)); } @Test - void checkSortedOnLowestLayer() { - SkipList skipList = new SkipList<>(); + @DisplayName("Elements should be sorted at base level") + void testSortedOrderOnBaseLevel() { String[] values = {"d", "b", "a", "c"}; Arrays.stream(values).forEach(skipList::add); - print(skipList); String[] actualOrder = IntStream.range(0, values.length).mapToObj(skipList::get).toArray(String[] ::new); - assertArrayEquals(new String[] {"a", "b", "c", "d"}, actualOrder); + org.junit.jupiter.api.Assertions.assertArrayEquals(new String[] {"a", "b", "c", "d"}, actualOrder); } - private SkipList createSkipList() { - SkipList skipList = new SkipList<>(); - String[] values = { - "a", - "b", - "c", - "d", - "e", - "f", - "g", - "h", - "i", - "j", - "k", - }; + @Test + @DisplayName("Duplicate elements can be added and count correctly") + void testAddDuplicates() { + skipList.add("x"); + skipList.add("x"); + assertEquals(2, skipList.size()); + assertEquals("x", skipList.get(0)); + assertEquals("x", skipList.get(1)); + } + + @Test + @DisplayName("Add multiple and remove all") + void testClearViaRemovals() { + String[] values = {"a", "b", "c"}; Arrays.stream(values).forEach(skipList::add); - return skipList; + + for (String v : values) { + skipList.remove(v); + } + + assertEquals(0, skipList.size()); } - /** - * Print Skip List representation to console. - * Optional method not involved in testing process. Used only for visualisation purposes. - * @param skipList to print - */ - private void print(SkipList skipList) { - System.out.println(skipList); + private SkipList createSkipList() { + SkipList s = new SkipList<>(); + String[] values = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"}; + Arrays.stream(values).forEach(s::add); + return s; } } diff --git a/src/test/java/com/thealgorithms/datastructures/lists/SortedLinkedListTest.java b/src/test/java/com/thealgorithms/datastructures/lists/SortedLinkedListTest.java index 82e0853da374..71f932465eef 100644 --- a/src/test/java/com/thealgorithms/datastructures/lists/SortedLinkedListTest.java +++ b/src/test/java/com/thealgorithms/datastructures/lists/SortedLinkedListTest.java @@ -128,4 +128,81 @@ public void testIsEmptyAfterDeletion() { list.delete(10); assertTrue(list.isEmpty()); } + + @Test + public void testInsertNegativeNumbers() { + list.insert(-10); + list.insert(-5); + list.insert(-20); + assertEquals("[-20, -10, -5]", list.toString()); + } + + @Test + public void testInsertMixedPositiveAndNegativeNumbers() { + list.insert(0); + list.insert(-1); + list.insert(1); + assertEquals("[-1, 0, 1]", list.toString()); + } + + @Test + public void testMultipleDeletesUntilEmpty() { + list.insert(2); + list.insert(4); + list.insert(6); + assertTrue(list.delete(4)); + assertTrue(list.delete(2)); + assertTrue(list.delete(6)); + assertTrue(list.isEmpty()); + assertEquals("[]", list.toString()); + } + + @Test + public void testDeleteDuplicateValuesOnlyDeletesOneInstance() { + list.insert(5); + list.insert(5); + list.insert(5); + assertTrue(list.delete(5)); + assertEquals("[5, 5]", list.toString()); + assertTrue(list.delete(5)); + assertEquals("[5]", list.toString()); + assertTrue(list.delete(5)); + assertEquals("[]", list.toString()); + } + + @Test + public void testSearchOnListWithDuplicates() { + list.insert(7); + list.insert(7); + list.insert(7); + assertTrue(list.search(7)); + assertFalse(list.search(10)); + } + + @Test + public void testToStringOnEmptyList() { + assertEquals("[]", list.toString()); + } + + @Test + public void testDeleteAllDuplicates() { + list.insert(4); + list.insert(4); + list.insert(4); + assertTrue(list.delete(4)); + assertTrue(list.delete(4)); + assertTrue(list.delete(4)); + assertFalse(list.delete(4)); // nothing left to delete + assertEquals("[]", list.toString()); + } + + @Test + public void testInsertAfterDeletion() { + list.insert(1); + list.insert(3); + list.insert(5); + assertTrue(list.delete(3)); + list.insert(2); + assertEquals("[1, 2, 5]", list.toString()); + } } diff --git a/src/test/java/com/thealgorithms/datastructures/queues/DequeTest.java b/src/test/java/com/thealgorithms/datastructures/queues/DequeTest.java index 1244a2e260d2..ada314383c74 100644 --- a/src/test/java/com/thealgorithms/datastructures/queues/DequeTest.java +++ b/src/test/java/com/thealgorithms/datastructures/queues/DequeTest.java @@ -87,4 +87,111 @@ void testToString() { deque.addFirst(5); assertEquals("Head -> 5 <-> 10 <-> 20 <- Tail", deque.toString()); } + + @Test + void testAlternatingAddRemove() { + Deque deque = new Deque<>(); + deque.addFirst(1); + deque.addLast(2); + deque.addFirst(0); + assertEquals(0, deque.pollFirst()); + assertEquals(2, deque.pollLast()); + assertEquals(1, deque.pollFirst()); + org.junit.jupiter.api.Assertions.assertTrue(deque.isEmpty()); + } + + @Test + void testSizeAfterOperations() { + Deque deque = new Deque<>(); + assertEquals(0, deque.size()); + deque.addFirst(1); + deque.addLast(2); + deque.addFirst(3); + assertEquals(3, deque.size()); + deque.pollFirst(); + deque.pollLast(); + assertEquals(1, deque.size()); + } + + @Test + void testNullValues() { + Deque deque = new Deque<>(); + deque.addFirst(null); + assertNull(deque.peekFirst()); + assertNull(deque.pollFirst()); + org.junit.jupiter.api.Assertions.assertTrue(deque.isEmpty()); + } + + @Test + void testMultipleAddFirst() { + Deque deque = new Deque<>(); + deque.addFirst(1); + deque.addFirst(2); + deque.addFirst(3); + + assertEquals(3, deque.peekFirst(), "First element should be the last added to front"); + assertEquals(1, deque.peekLast(), "Last element should be the first added to front"); + assertEquals(3, deque.size(), "Size should reflect all additions"); + } + + @Test + void testMultipleAddLast() { + Deque deque = new Deque<>(); + deque.addLast(1); + deque.addLast(2); + deque.addLast(3); + + assertEquals(1, deque.peekFirst(), "First element should be the first added to back"); + assertEquals(3, deque.peekLast(), "Last element should be the last added to back"); + assertEquals(3, deque.size(), "Size should reflect all additions"); + } + + @Test + void testSingleElementOperations() { + Deque deque = new Deque<>(); + deque.addFirst(1); + + assertEquals(1, deque.peekFirst(), "Single element should be both first and last"); + assertEquals(1, deque.peekLast(), "Single element should be both first and last"); + assertEquals(1, deque.size()); + + assertEquals(1, deque.pollFirst(), "Should be able to poll single element from front"); + org.junit.jupiter.api.Assertions.assertTrue(deque.isEmpty(), "Deque should be empty after polling single element"); + } + + @Test + void testSingleElementPollLast() { + Deque deque = new Deque<>(); + deque.addLast(1); + + assertEquals(1, deque.pollLast(), "Should be able to poll single element from back"); + org.junit.jupiter.api.Assertions.assertTrue(deque.isEmpty(), "Deque should be empty after polling single element"); + } + + @Test + void testMixedNullAndValues() { + Deque deque = new Deque<>(); + deque.addFirst("first"); + deque.addLast(null); + deque.addFirst(null); + deque.addLast("last"); + + assertEquals(4, deque.size(), "Should handle mixed null and non-null values"); + assertNull(deque.pollFirst(), "Should poll null from front"); + assertEquals("first", deque.pollFirst(), "Should poll non-null value"); + assertNull(deque.pollLast().equals("last") ? null : deque.peekLast(), "Should handle null correctly"); + } + + @Test + void testSymmetricOperations() { + Deque deque = new Deque<>(); + + // Test that addFirst/pollFirst and addLast/pollLast are symmetric + deque.addFirst(1); + deque.addLast(2); + + assertEquals(1, deque.pollFirst(), "addFirst/pollFirst should be symmetric"); + assertEquals(2, deque.pollLast(), "addLast/pollLast should be symmetric"); + org.junit.jupiter.api.Assertions.assertTrue(deque.isEmpty(), "Deque should be empty after symmetric operations"); + } } diff --git a/src/test/java/com/thealgorithms/datastructures/queues/PriorityQueuesTest.java b/src/test/java/com/thealgorithms/datastructures/queues/PriorityQueuesTest.java index 1a3b5aadebb2..e97fe091c556 100644 --- a/src/test/java/com/thealgorithms/datastructures/queues/PriorityQueuesTest.java +++ b/src/test/java/com/thealgorithms/datastructures/queues/PriorityQueuesTest.java @@ -55,4 +55,60 @@ void testPQExtra() { Assertions.assertEquals(myQueue.peek(), 2); Assertions.assertEquals(myQueue.getSize(), 1); } + + @Test + void testInsertUntilFull() { + PriorityQueue pq = new PriorityQueue(3); + pq.insert(1); + pq.insert(4); + pq.insert(2); + Assertions.assertTrue(pq.isFull()); + Assertions.assertEquals(4, pq.peek()); + } + + @Test + void testRemoveFromEmpty() { + PriorityQueue pq = new PriorityQueue(3); + Assertions.assertThrows(RuntimeException.class, pq::remove); + } + + @Test + void testInsertDuplicateValues() { + PriorityQueue pq = new PriorityQueue(5); + pq.insert(5); + pq.insert(5); + pq.insert(3); + Assertions.assertEquals(5, pq.peek()); + pq.remove(); + Assertions.assertEquals(5, pq.peek()); + pq.remove(); + Assertions.assertEquals(3, pq.peek()); + } + + @Test + void testSizeAfterInsertAndRemove() { + PriorityQueue pq = new PriorityQueue(4); + Assertions.assertEquals(0, pq.getSize()); + pq.insert(2); + Assertions.assertEquals(1, pq.getSize()); + pq.insert(10); + Assertions.assertEquals(2, pq.getSize()); + pq.remove(); + Assertions.assertEquals(1, pq.getSize()); + pq.remove(); + Assertions.assertEquals(0, pq.getSize()); + } + + @Test + void testInsertAndRemoveAll() { + PriorityQueue pq = new PriorityQueue(3); + pq.insert(8); + pq.insert(1); + pq.insert(6); + Assertions.assertTrue(pq.isFull()); + pq.remove(); + pq.remove(); + pq.remove(); + Assertions.assertTrue(pq.isEmpty()); + } } diff --git a/src/test/java/com/thealgorithms/datastructures/queues/QueueByTwoStacksTest.java b/src/test/java/com/thealgorithms/datastructures/queues/QueueByTwoStacksTest.java index 87f136a84631..491cb7634302 100644 --- a/src/test/java/com/thealgorithms/datastructures/queues/QueueByTwoStacksTest.java +++ b/src/test/java/com/thealgorithms/datastructures/queues/QueueByTwoStacksTest.java @@ -33,19 +33,19 @@ public void testDequeue() { queue.put(10); queue.put(20); queue.put(30); - assertEquals(10, queue.get()); // First item out - assertEquals(20, queue.get()); // Second item out - assertEquals(30, queue.get()); // Third item out + assertEquals(10, queue.get()); + assertEquals(20, queue.get()); + assertEquals(30, queue.get()); } @Test public void testInterleavedOperations() { queue.put(10); queue.put(20); - assertEquals(10, queue.get()); // Dequeue first item + assertEquals(10, queue.get()); queue.put(30); - assertEquals(20, queue.get()); // Dequeue second item - assertEquals(30, queue.get()); // Dequeue third item + assertEquals(20, queue.get()); + assertEquals(30, queue.get()); } @Test @@ -62,8 +62,76 @@ public void testQueueSize() { @Test public void testEmptyQueueException() { - assertThrows(NoSuchElementException.class, () -> { - queue.get(); // Attempting to dequeue from empty queue - }); + assertThrows(NoSuchElementException.class, () -> queue.get()); + } + + @Test + public void testDequeueAllElements() { + for (int i = 1; i <= 5; i++) { + queue.put(i); + } + for (int i = 1; i <= 5; i++) { + assertEquals(i, queue.get()); + } + assertEquals(0, queue.size()); + } + + @Test + public void testLargeNumberOfOperations() { + int n = 1000; + for (int i = 0; i < n; i++) { + queue.put(i); + } + for (int i = 0; i < n; i++) { + assertEquals(i, queue.get()); + } + assertEquals(0, queue.size()); + } + + @Test + public void testRefillDuringDequeue() { + queue.put(1); + queue.put(2); + assertEquals(1, queue.get()); + queue.put(3); + queue.put(4); + assertEquals(2, queue.get()); + assertEquals(3, queue.get()); + assertEquals(4, queue.get()); + } + + @Test + public void testAlternatingPutAndGet() { + queue.put(1); + assertEquals(1, queue.get()); + queue.put(2); + queue.put(3); + assertEquals(2, queue.get()); + queue.put(4); + assertEquals(3, queue.get()); + assertEquals(4, queue.get()); + } + + @Test + public void testSizeStability() { + queue.put(100); + int size1 = queue.size(); + int size2 = queue.size(); + assertEquals(size1, size2); + } + + @Test + public void testMultipleEmptyDequeues() { + assertThrows(NoSuchElementException.class, () -> queue.get()); + assertThrows(NoSuchElementException.class, () -> queue.get()); + } + + @Test + public void testQueueWithStrings() { + QueueByTwoStacks stringQueue = new QueueByTwoStacks<>(); + stringQueue.put("a"); + stringQueue.put("b"); + assertEquals("a", stringQueue.get()); + assertEquals("b", stringQueue.get()); } } diff --git a/src/test/java/com/thealgorithms/datastructures/queues/QueueTest.java b/src/test/java/com/thealgorithms/datastructures/queues/QueueTest.java index 9a4f50d5e216..86ef940beab6 100644 --- a/src/test/java/com/thealgorithms/datastructures/queues/QueueTest.java +++ b/src/test/java/com/thealgorithms/datastructures/queues/QueueTest.java @@ -124,4 +124,204 @@ void testQueueCapacityException() { Assertions.assertThrows(IllegalArgumentException.class, () -> new Queue<>(0)); Assertions.assertThrows(IllegalArgumentException.class, () -> new Queue<>(-5)); } + + @Test + void testCircularBehavior() { + // Test that queue behaves correctly after multiple insert/remove cycles + queue.insert(1); + queue.insert(2); + queue.insert(3); + + // Remove all elements + queue.remove(); // removes 1 + queue.remove(); // removes 2 + queue.remove(); // removes 3 + + // Add elements again to test circular behavior + queue.insert(4); + queue.insert(5); + queue.insert(6); + + Assertions.assertEquals(4, queue.peekFront()); + Assertions.assertEquals(6, queue.peekRear()); + Assertions.assertEquals(3, queue.getSize()); + Assertions.assertTrue(queue.isFull()); + } + + @Test + void testMixedInsertRemoveOperations() { + // Test interleaved insert and remove operations + queue.insert(1); + queue.insert(2); + + Assertions.assertEquals(1, queue.remove()); + queue.insert(3); + queue.insert(4); + + Assertions.assertEquals(2, queue.remove()); + Assertions.assertEquals(3, queue.remove()); + + queue.insert(5); + queue.insert(6); + + Assertions.assertEquals(4, queue.peekFront()); + Assertions.assertEquals(6, queue.peekRear()); + Assertions.assertEquals(3, queue.getSize()); + } + + @Test + void testSingleElementOperations() { + // Test operations with single element + queue.insert(42); + + Assertions.assertEquals(42, queue.peekFront()); + Assertions.assertEquals(42, queue.peekRear()); + Assertions.assertEquals(1, queue.getSize()); + Assertions.assertFalse(queue.isEmpty()); + Assertions.assertFalse(queue.isFull()); + + Assertions.assertEquals(42, queue.remove()); + Assertions.assertTrue(queue.isEmpty()); + Assertions.assertEquals(0, queue.getSize()); + } + + @Test + void testNullValueHandling() { + // Test queue with null values (if supported) + Queue stringQueue = new Queue<>(3); + + Assertions.assertTrue(stringQueue.insert(null)); + Assertions.assertTrue(stringQueue.insert("test")); + Assertions.assertTrue(stringQueue.insert(null)); + + Assertions.assertNull(stringQueue.peekFront()); + Assertions.assertNull(stringQueue.peekRear()); + Assertions.assertEquals(3, stringQueue.getSize()); + + Assertions.assertNull(stringQueue.remove()); + Assertions.assertEquals("test", stringQueue.peekFront()); + } + + @Test + void testStringDataType() { + // Test queue with String data type + Queue stringQueue = new Queue<>(2); + stringQueue.insert("first"); + stringQueue.insert("second"); + + Assertions.assertEquals("first", stringQueue.peekFront()); + Assertions.assertEquals("second", stringQueue.peekRear()); + } + + @Test + void testLargerCapacityQueue() { + // Test queue with larger capacity + Queue largeQueue = new Queue<>(10); + + // Fill the queue + for (int i = 1; i <= 10; i++) { + Assertions.assertTrue(largeQueue.insert(i)); + } + + Assertions.assertTrue(largeQueue.isFull()); + Assertions.assertFalse(largeQueue.insert(11)); + + // Remove half the elements + for (int i = 1; i <= 5; i++) { + Assertions.assertEquals(i, largeQueue.remove()); + } + + Assertions.assertEquals(6, largeQueue.peekFront()); + Assertions.assertEquals(10, largeQueue.peekRear()); + Assertions.assertEquals(5, largeQueue.getSize()); + } + + @Test + void testQueueCapacityOne() { + // Test queue with capacity of 1 + Queue singleQueue = new Queue<>(1); + + Assertions.assertTrue(singleQueue.isEmpty()); + Assertions.assertFalse(singleQueue.isFull()); + + Assertions.assertTrue(singleQueue.insert(100)); + Assertions.assertTrue(singleQueue.isFull()); + Assertions.assertFalse(singleQueue.isEmpty()); + Assertions.assertFalse(singleQueue.insert(200)); + + Assertions.assertEquals(100, singleQueue.peekFront()); + Assertions.assertEquals(100, singleQueue.peekRear()); + + Assertions.assertEquals(100, singleQueue.remove()); + Assertions.assertTrue(singleQueue.isEmpty()); + } + + @Test + void testQueueWraparoundIndexing() { + // Test that internal array indexing wraps around correctly + queue.insert(1); + queue.insert(2); + queue.insert(3); // Queue full + + // Remove one element + queue.remove(); // removes 1 + + // Add another element (should wrap around) + queue.insert(4); + + Assertions.assertEquals("[2, 3, 4]", queue.toString()); + Assertions.assertEquals(2, queue.peekFront()); + Assertions.assertEquals(4, queue.peekRear()); + + // Continue the pattern + queue.remove(); // removes 2 + queue.insert(5); + + Assertions.assertEquals(3, queue.peekFront()); + Assertions.assertEquals(5, queue.peekRear()); + } + + @Test + void testQueueStateAfterMultipleCycles() { + // Test queue state after multiple complete fill/empty cycles + for (int cycle = 0; cycle < 3; cycle++) { + // Fill the queue + for (int i = 1; i <= 3; i++) { + queue.insert(i + cycle * 10); + } + + // Verify state + Assertions.assertTrue(queue.isFull()); + Assertions.assertEquals(3, queue.getSize()); + + // Empty the queue + for (int i = 1; i <= 3; i++) { + queue.remove(); + } + + // Verify empty state + Assertions.assertTrue(queue.isEmpty()); + Assertions.assertEquals(0, queue.getSize()); + } + } + + @Test + void testQueueConsistencyAfterOperations() { + // Test that queue maintains consistency after various operations + queue.insert(10); + queue.insert(20); + + int firstRemoved = queue.remove(); + queue.insert(30); + queue.insert(40); + + int secondRemoved = queue.remove(); + queue.insert(50); + + // Verify the order is maintained + Assertions.assertEquals(10, firstRemoved); + Assertions.assertEquals(20, secondRemoved); + Assertions.assertEquals(30, queue.peekFront()); + Assertions.assertEquals(50, queue.peekRear()); + } } diff --git a/src/test/java/com/thealgorithms/datastructures/stacks/NodeStackTest.java b/src/test/java/com/thealgorithms/datastructures/stacks/NodeStackTest.java index 7ac0d8bc324b..8a89382211ba 100644 --- a/src/test/java/com/thealgorithms/datastructures/stacks/NodeStackTest.java +++ b/src/test/java/com/thealgorithms/datastructures/stacks/NodeStackTest.java @@ -1,15 +1,26 @@ package com.thealgorithms.datastructures.stacks; 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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; class NodeStackTest { + private NodeStack intStack; + private NodeStack stringStack; + + @BeforeEach + void setUp() { + intStack = new NodeStack<>(); + stringStack = new NodeStack<>(); + } + @Test + @DisplayName("Test push operation") void testPush() { NodeStack stack = new NodeStack<>(); stack.push(10); @@ -18,6 +29,7 @@ void testPush() { } @Test + @DisplayName("Test pop operation") void testPop() { NodeStack stack = new NodeStack<>(); stack.push("First"); @@ -27,12 +39,14 @@ void testPop() { } @Test + @DisplayName("Test pop on empty stack throws exception") void testPopOnEmptyStack() { NodeStack stack = new NodeStack<>(); assertThrows(IllegalStateException.class, stack::pop, "Popping an empty stack should throw IllegalStateException."); } @Test + @DisplayName("Test peek operation") void testPeek() { NodeStack stack = new NodeStack<>(); stack.push(5); @@ -43,22 +57,25 @@ void testPeek() { } @Test + @DisplayName("Test peek on empty stack throws exception") void testPeekOnEmptyStack() { NodeStack stack = new NodeStack<>(); assertThrows(IllegalStateException.class, stack::peek, "Peeking an empty stack should throw IllegalStateException."); } @Test + @DisplayName("Test isEmpty method") void testIsEmpty() { NodeStack stack = new NodeStack<>(); assertTrue(stack.isEmpty(), "Newly initialized stack should be empty."); stack.push('A'); - assertFalse(stack.isEmpty(), "Stack should not be empty after a push operation."); + org.junit.jupiter.api.Assertions.assertFalse(stack.isEmpty(), "Stack should not be empty after a push operation."); stack.pop(); assertTrue(stack.isEmpty(), "Stack should be empty after popping the only element."); } @Test + @DisplayName("Test size method") void testSize() { NodeStack stack = new NodeStack<>(); assertEquals(0, stack.size(), "Size of empty stack should be 0."); @@ -70,4 +87,164 @@ void testSize() { stack.pop(); assertEquals(0, stack.size(), "Size should be 0 after popping all elements."); } + + @Test + @DisplayName("Test push and pop with null values") + void testPushPopWithNull() { + stringStack.push(null); + stringStack.push("not null"); + stringStack.push(null); + + assertEquals(3, stringStack.size(), "Stack should contain 3 elements including nulls"); + org.junit.jupiter.api.Assertions.assertNull(stringStack.pop(), "Should pop null value"); + assertEquals("not null", stringStack.pop(), "Should pop 'not null' value"); + org.junit.jupiter.api.Assertions.assertNull(stringStack.pop(), "Should pop null value"); + assertTrue(stringStack.isEmpty(), "Stack should be empty after popping all elements"); + } + + @Test + @DisplayName("Test LIFO (Last In First Out) behavior") + void testLifoBehavior() { + int[] values = {1, 2, 3, 4, 5}; + + // Push values in order + for (int value : values) { + intStack.push(value); + } + + // Pop values should be in reverse order + for (int i = values.length - 1; i >= 0; i--) { + assertEquals(values[i], intStack.pop(), "Elements should be popped in LIFO order"); + } + } + + @Test + @DisplayName("Test peek doesn't modify stack") + void testPeekDoesNotModifyStack() { + intStack.push(1); + intStack.push(2); + intStack.push(3); + + int originalSize = intStack.size(); + int peekedValue = intStack.peek(); + + assertEquals(3, peekedValue, "Peek should return top element"); + assertEquals(originalSize, intStack.size(), "Peek should not change stack size"); + assertEquals(3, intStack.peek(), "Multiple peeks should return same value"); + org.junit.jupiter.api.Assertions.assertFalse(intStack.isEmpty(), "Peek should not make stack empty"); + } + + @Test + @DisplayName("Test mixed push and pop operations") + void testMixedOperations() { + // Test interleaved push/pop operations + intStack.push(1); + assertEquals(1, intStack.pop()); + assertTrue(intStack.isEmpty()); + + intStack.push(2); + intStack.push(3); + assertEquals(3, intStack.pop()); + intStack.push(4); + assertEquals(4, intStack.peek()); + assertEquals(2, intStack.size()); + + assertEquals(4, intStack.pop()); + assertEquals(2, intStack.pop()); + assertTrue(intStack.isEmpty()); + } + + @Test + @DisplayName("Test stack with duplicate values") + void testStackWithDuplicates() { + intStack.push(1); + intStack.push(1); + intStack.push(1); + + assertEquals(3, intStack.size(), "Stack should handle duplicate values"); + assertEquals(1, intStack.peek(), "Peek should return duplicate value"); + + assertEquals(1, intStack.pop(), "Should pop first duplicate"); + assertEquals(1, intStack.pop(), "Should pop second duplicate"); + assertEquals(1, intStack.pop(), "Should pop third duplicate"); + assertTrue(intStack.isEmpty(), "Stack should be empty after popping all duplicates"); + } + + @Test + @DisplayName("Test stack with different data types") + void testDifferentDataTypes() { + NodeStack charStack = new NodeStack<>(); + NodeStack booleanStack = new NodeStack<>(); + + // Test with Character + charStack.push('A'); + charStack.push('Z'); + assertEquals('Z', charStack.peek(), "Should handle Character values"); + + // Test with Boolean + booleanStack.push(Boolean.TRUE); + booleanStack.push(Boolean.FALSE); + assertEquals(Boolean.FALSE, booleanStack.peek(), "Should handle Boolean values"); + } + + @Test + @DisplayName("Test stack state consistency after exceptions") + void testStateConsistencyAfterExceptions() { + // Stack should remain consistent after exception-throwing operations + intStack.push(1); + intStack.push(2); + + // Try to peek and pop normally first + assertEquals(2, intStack.peek()); + assertEquals(2, intStack.pop()); + assertEquals(1, intStack.size()); + + // Pop remaining element + assertEquals(1, intStack.pop()); + assertTrue(intStack.isEmpty()); + + // Now stack is empty, operations should throw exceptions + assertThrows(IllegalStateException.class, intStack::peek); + assertThrows(IllegalStateException.class, intStack::pop); + + // Stack should still be in valid empty state + assertTrue(intStack.isEmpty()); + assertEquals(0, intStack.size()); + + // Should be able to push after exceptions + intStack.push(3); + org.junit.jupiter.api.Assertions.assertFalse(intStack.isEmpty()); + assertEquals(1, intStack.size()); + assertEquals(3, intStack.peek()); + } + + @Test + @DisplayName("Test single element stack operations") + void testSingleElementStack() { + intStack.push(2); + + org.junit.jupiter.api.Assertions.assertFalse(intStack.isEmpty(), "Stack with one element should not be empty"); + assertEquals(1, intStack.size(), "Size should be 1"); + assertEquals(2, intStack.peek(), "Peek should return the single element"); + assertEquals(1, intStack.size(), "Peek should not change size"); + + assertEquals(2, intStack.pop(), "Pop should return the single element"); + assertTrue(intStack.isEmpty(), "Stack should be empty after popping single element"); + assertEquals(0, intStack.size(), "Size should be 0 after popping single element"); + } + + @Test + @DisplayName("Test toString method if implemented") + void testToString() { + // This test assumes NodeStack has a toString method + // If not implemented, this test can be removed or NodeStack can be enhanced + intStack.push(1); + intStack.push(2); + intStack.push(3); + + String stackString = intStack.toString(); + // Basic check that toString doesn't throw exception and returns something + assertTrue(stackString != null, "toString should not return null"); + assertTrue(stackString.length() > 0, "toString should return non-empty string"); + } } diff --git a/src/test/java/com/thealgorithms/datastructures/stacks/StackArrayTest.java b/src/test/java/com/thealgorithms/datastructures/stacks/StackArrayTest.java index 74de7ad6435a..392cdf2329fc 100644 --- a/src/test/java/com/thealgorithms/datastructures/stacks/StackArrayTest.java +++ b/src/test/java/com/thealgorithms/datastructures/stacks/StackArrayTest.java @@ -126,4 +126,62 @@ void testToString() { stack.push(3); Assertions.assertEquals("StackArray [1, 2, 3]", stack.toString()); } + + @Test + void testSingleElementOperations() { + // Test operations with a single element + stack.push(2); + Assertions.assertEquals(1, stack.size()); + Assertions.assertFalse(stack.isEmpty()); + Assertions.assertEquals(2, stack.peek()); + Assertions.assertEquals(2, stack.pop()); + Assertions.assertTrue(stack.isEmpty()); + } + + @Test + void testAlternatingPushPop() { + // Test alternating push and pop operations + stack.push(1); + Assertions.assertEquals(1, stack.pop()); + + stack.push(2); + stack.push(3); + Assertions.assertEquals(3, stack.pop()); + + stack.push(4); + Assertions.assertEquals(4, stack.pop()); + Assertions.assertEquals(2, stack.pop()); + Assertions.assertTrue(stack.isEmpty()); + } + + @Test + void testPushNullElements() { + // Test pushing null values + stack.push(null); + Assertions.assertEquals(1, stack.size()); + Assertions.assertNull(stack.peek()); + Assertions.assertNull(stack.pop()); + + // Mix null and non-null values + stack.push(1); + stack.push(null); + stack.push(2); + + Assertions.assertEquals(2, stack.pop()); + Assertions.assertNull(stack.pop()); + Assertions.assertEquals(1, stack.pop()); + } + + @Test + void testWithDifferentDataTypes() { + // Test with String type + StackArray stringStack = new StackArray<>(3); + stringStack.push("first"); + stringStack.push("second"); + stringStack.push("third"); + + Assertions.assertEquals("third", stringStack.pop()); + Assertions.assertEquals("second", stringStack.peek()); + Assertions.assertEquals(2, stringStack.size()); + } } diff --git a/src/test/java/com/thealgorithms/datastructures/stacks/StackOfLinkedListTest.java b/src/test/java/com/thealgorithms/datastructures/stacks/StackOfLinkedListTest.java index 58af66bc38f4..2dfe4c242e1c 100644 --- a/src/test/java/com/thealgorithms/datastructures/stacks/StackOfLinkedListTest.java +++ b/src/test/java/com/thealgorithms/datastructures/stacks/StackOfLinkedListTest.java @@ -118,4 +118,104 @@ public void testSequentialPushAndPop() { } assertTrue(stack.isEmpty(), "Stack should be empty after popping all elements"); } + + @Test + public void testPushZeroAndNegativeValues() { + stack.push(0); + stack.push(-1); + stack.push(-1); + + assertEquals(-1, stack.pop(), "Should handle negative values correctly"); + assertEquals(-1, stack.pop(), "Should handle negative values correctly"); + assertEquals(0, stack.pop(), "Should handle zero value correctly"); + } + + @Test + public void testPushDuplicateValues() { + stack.push(1); + stack.push(1); + stack.push(1); + + assertEquals(3, stack.getSize(), "Should allow duplicate values"); + assertEquals(1, stack.pop()); + assertEquals(1, stack.pop()); + assertEquals(1, stack.pop()); + } + + @Test + public void testPushAfterEmptyingStack() { + stack.push(1); + stack.push(2); + stack.pop(); + stack.pop(); + + assertTrue(stack.isEmpty(), "Stack should be empty"); + + stack.push(10); + assertEquals(10, stack.peek(), "Should work correctly after emptying and refilling"); + assertEquals(1, stack.getSize(), "Size should be correct after refilling"); + } + + @Test + public void testPeekDoesNotModifyStack() { + stack.push(1); + + int firstPeek = stack.peek(); + int secondPeek = stack.peek(); + int thirdPeek = stack.peek(); + + assertEquals(firstPeek, secondPeek, "Multiple peeks should return same value"); + assertEquals(secondPeek, thirdPeek, "Multiple peeks should return same value"); + assertEquals(1, stack.getSize(), "Peek should not modify stack size"); + assertEquals(1, stack.pop(), "Element should still be poppable after peeking"); + } + + @Test + public void testAlternatingPushAndPop() { + stack.push(1); + assertEquals(1, stack.pop()); + + stack.push(2); + stack.push(3); + assertEquals(3, stack.pop()); + + stack.push(4); + assertEquals(4, stack.pop()); + assertEquals(2, stack.pop()); + + assertTrue(stack.isEmpty(), "Stack should be empty after alternating operations"); + } + + @Test + public void testToStringWithSingleElement() { + stack.push(42); + assertEquals("42", stack.toString(), "String representation with single element should not have arrows"); + } + + @Test + public void testStackIntegrity() { + // Test that internal state remains consistent + for (int i = 0; i < 10; i++) { + stack.push(i); + assertEquals(i + 1, stack.getSize(), "Size should be consistent during pushes"); + assertEquals(i, stack.peek(), "Peek should return last pushed value"); + } + + for (int i = 9; i >= 0; i--) { + assertEquals(i, stack.peek(), "Peek should return correct value before pop"); + assertEquals(i, stack.pop(), "Pop should return values in LIFO order"); + assertEquals(i, stack.getSize(), "Size should be consistent during pops"); + } + } + + @Test + public void testMixedDataTypes() { + // If your stack supports Object types, test with different data types + + stack.push(1); + stack.push(2); + + assertEquals(Integer.valueOf(2), stack.pop()); + assertEquals(Integer.valueOf(1), stack.pop()); + } } diff --git a/src/test/java/com/thealgorithms/divideandconquer/CountingInversionsTest.java b/src/test/java/com/thealgorithms/divideandconquer/CountingInversionsTest.java index d12614d6fd06..f8356a87eb31 100644 --- a/src/test/java/com/thealgorithms/divideandconquer/CountingInversionsTest.java +++ b/src/test/java/com/thealgorithms/divideandconquer/CountingInversionsTest.java @@ -29,4 +29,35 @@ public void testAllInversions() { int[] arr = {5, 4, 3, 2, 1}; assertEquals(10, CountingInversions.countInversions(arr)); } + + @Test + public void testEmptyArray() { + int[] arr = {}; + assertEquals(0, CountingInversions.countInversions(arr)); + } + + @Test + public void testArrayWithDuplicates() { + int[] arr = {1, 3, 2, 3, 1}; + // Inversions: (3,2), (3,1), (3,1), (2,1) + assertEquals(4, CountingInversions.countInversions(arr)); + } + + @Test + public void testLargeArray() { + int n = 1000; + int[] arr = new int[n]; + for (int i = 0; i < n; i++) { + arr[i] = n - i; // descending order -> max inversions = n*(n-1)/2 + } + int expected = n * (n - 1) / 2; + assertEquals(expected, CountingInversions.countInversions(arr)); + } + + @Test + public void testArrayWithAllSameElements() { + int[] arr = {7, 7, 7, 7}; + // No inversions since all elements are equal + assertEquals(0, CountingInversions.countInversions(arr)); + } } diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/KnapsackZeroOneTabulationTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/KnapsackZeroOneTabulationTest.java new file mode 100644 index 000000000000..6c61ad2130e3 --- /dev/null +++ b/src/test/java/com/thealgorithms/dynamicprogramming/KnapsackZeroOneTabulationTest.java @@ -0,0 +1,78 @@ +package com.thealgorithms.dynamicprogramming; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class KnapsackZeroOneTabulationTest { + + @Test + public void basicCheck() { + int[] values = {60, 100, 120}; + int[] weights = {10, 20, 30}; + int capacity = 50; + int itemCount = values.length; + + int expected = 220; // Best choice: item 1 (100) and item 2 (120) + int result = KnapsackZeroOneTabulation.compute(values, weights, capacity, itemCount); + assertEquals(expected, result); + } + + @Test + public void emptyKnapsack() { + int[] values = {}; + int[] weights = {}; + int capacity = 50; + int itemCount = 0; + + assertEquals(0, KnapsackZeroOneTabulation.compute(values, weights, capacity, itemCount)); + } + + @Test + public void zeroCapacity() { + int[] values = {60, 100, 120}; + int[] weights = {10, 20, 30}; + int capacity = 0; + int itemCount = values.length; + + assertEquals(0, KnapsackZeroOneTabulation.compute(values, weights, capacity, itemCount)); + } + + @Test + public void negativeCapacity() { + int[] values = {10, 20, 30}; + int[] weights = {1, 1, 1}; + int capacity = -10; + int itemCount = values.length; + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> KnapsackZeroOneTabulation.compute(values, weights, capacity, itemCount)); + assertEquals("Capacity must not be negative.", exception.getMessage()); + } + + @Test + public void mismatchedLengths() { + int[] values = {60, 100}; // Only 2 values + int[] weights = {10, 20, 30}; // 3 weights + int capacity = 50; + int itemCount = 2; // Matches `values.length` + + // You could either expect 0 or throw an IllegalArgumentException in your compute function + assertThrows(IllegalArgumentException.class, () -> { KnapsackZeroOneTabulation.compute(values, weights, capacity, itemCount); }); + } + + @Test + public void nullInputs() { + int[] weights = {1, 2, 3}; + int capacity = 10; + int itemCount = 3; + + IllegalArgumentException exception1 = assertThrows(IllegalArgumentException.class, () -> KnapsackZeroOneTabulation.compute(null, weights, capacity, itemCount)); + assertEquals("Values and weights arrays must not be null.", exception1.getMessage()); + + int[] values = {1, 2, 3}; + + IllegalArgumentException exception2 = assertThrows(IllegalArgumentException.class, () -> KnapsackZeroOneTabulation.compute(values, null, capacity, itemCount)); + assertEquals("Values and weights arrays must not be null.", exception2.getMessage()); + } +} diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/KnapsackZeroOneTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/KnapsackZeroOneTest.java new file mode 100644 index 000000000000..0ca2b91fc273 --- /dev/null +++ b/src/test/java/com/thealgorithms/dynamicprogramming/KnapsackZeroOneTest.java @@ -0,0 +1,68 @@ +package com.thealgorithms.dynamicprogramming; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class KnapsackZeroOneTest { + + @Test + void basicCheck() { + int[] values = {60, 100, 120}; + int[] weights = {10, 20, 30}; + int capacity = 50; + int expected = 220; + + int result = KnapsackZeroOne.compute(values, weights, capacity, values.length); + assertEquals(expected, result); + } + + @Test + void zeroCapacity() { + int[] values = {10, 20, 30}; + int[] weights = {1, 1, 1}; + int capacity = 0; + + int result = KnapsackZeroOne.compute(values, weights, capacity, values.length); + assertEquals(0, result); + } + + @Test + void zeroItems() { + int[] values = {}; + int[] weights = {}; + int capacity = 10; + + int result = KnapsackZeroOne.compute(values, weights, capacity, 0); + assertEquals(0, result); + } + + @Test + void weightsExceedingCapacity() { + int[] values = {10, 20}; + int[] weights = {100, 200}; + int capacity = 50; + + int result = KnapsackZeroOne.compute(values, weights, capacity, values.length); + assertEquals(0, result); + } + + @Test + void throwsOnNullArrays() { + assertThrows(IllegalArgumentException.class, () -> KnapsackZeroOne.compute(null, new int[] {1}, 10, 1)); + assertThrows(IllegalArgumentException.class, () -> KnapsackZeroOne.compute(new int[] {1}, null, 10, 1)); + } + + @Test + void throwsOnMismatchedArrayLengths() { + assertThrows(IllegalArgumentException.class, () -> KnapsackZeroOne.compute(new int[] {10, 20}, new int[] {5}, 15, 2)); + } + + @Test + void throwsOnNegativeInputs() { + assertThrows(IllegalArgumentException.class, () -> KnapsackZeroOne.compute(new int[] {10}, new int[] {5}, -1, 1)); + + assertThrows(IllegalArgumentException.class, () -> KnapsackZeroOne.compute(new int[] {10}, new int[] {5}, 5, -1)); + } +} diff --git a/src/test/java/com/thealgorithms/geometry/PointTest.java b/src/test/java/com/thealgorithms/geometry/PointTest.java new file mode 100644 index 000000000000..12901364b458 --- /dev/null +++ b/src/test/java/com/thealgorithms/geometry/PointTest.java @@ -0,0 +1,117 @@ +package com.thealgorithms.geometry; + +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 org.junit.jupiter.api.Test; + +public class PointTest { + + @Test + void testCompareTo() { + Point p1 = new Point(1, 2); + Point p2 = new Point(5, -1); + Point p3 = new Point(3, 9); + Point p4 = new Point(3, 9); + assertEquals(1, p1.compareTo(p2)); + assertEquals(-1, p2.compareTo(p3)); + assertEquals(0, p3.compareTo(p4)); + } + + @Test + void testToString() { + Point p = new Point(-3, 5); + assertEquals("(-3, 5)", p.toString()); + } + + @Test + void testPolarOrder() { + Point p = new Point(0, 0); + assertNotNull(p.polarOrder()); + } + + @Test + void testOrientation() { + // setup points + Point pA = new Point(0, 0); + Point pB = new Point(1, 0); + Point pC = new Point(1, 1); + + // test for left curve + assertEquals(1, Point.orientation(pA, pB, pC)); + + // test for right curve + pB = new Point(0, 1); + assertEquals(-1, Point.orientation(pA, pB, pC)); + + // test for left curve + pC = new Point(-1, 1); + assertEquals(1, Point.orientation(pA, pB, pC)); + + // test for right curve + pB = new Point(1, 0); + pC = new Point(1, -1); + assertEquals(-1, Point.orientation(pA, pB, pC)); + + // test for collinearity + pB = new Point(1, 1); + pC = new Point(2, 2); + assertEquals(0, Point.orientation(pA, pB, pC)); + } + + @Test + void testPolarOrderCompare() { + Point ref = new Point(0, 0); + + Point pA = new Point(1, 1); + Point pB = new Point(1, -1); + assertTrue(ref.polarOrder().compare(pA, pB) < 0); + + pA = new Point(3, 0); + pB = new Point(2, 0); + assertTrue(ref.polarOrder().compare(pA, pB) < 0); + + pA = new Point(0, 1); + pB = new Point(-1, 1); + assertTrue(ref.polarOrder().compare(pA, pB) < 0); + + pA = new Point(1, 1); + pB = new Point(2, 2); + assertEquals(0, ref.polarOrder().compare(pA, pB)); + + pA = new Point(1, 2); + pB = new Point(2, 1); + assertTrue(ref.polarOrder().compare(pA, pB) > 0); + + pA = new Point(2, 1); + pB = new Point(1, 2); + assertTrue(ref.polarOrder().compare(pA, pB) < 0); + + pA = new Point(-1, 0); + pB = new Point(-2, 0); + assertTrue(ref.polarOrder().compare(pA, pB) < 0); + + pA = new Point(2, 3); + pB = new Point(2, 3); + assertEquals(0, ref.polarOrder().compare(pA, pB)); + + pA = new Point(0, 1); + pB = new Point(0, -1); + assertTrue(ref.polarOrder().compare(pA, pB) < 0); + + ref = new Point(1, 1); + + pA = new Point(1, 2); + pB = new Point(2, 2); + assertTrue(ref.polarOrder().compare(pA, pB) > 0); + + pA = new Point(2, 1); + pB = new Point(2, 0); + assertTrue(ref.polarOrder().compare(pA, pB) < 0); + + pA = new Point(0, 1); + pB = new Point(1, 0); + assertTrue(ref.polarOrder().compare(pA, pB) < 0); + } +} diff --git a/src/test/java/com/thealgorithms/maths/AbsoluteValueTest.java b/src/test/java/com/thealgorithms/maths/AbsoluteValueTest.java index f87652253641..907d5cb45ef9 100644 --- a/src/test/java/com/thealgorithms/maths/AbsoluteValueTest.java +++ b/src/test/java/com/thealgorithms/maths/AbsoluteValueTest.java @@ -12,4 +12,28 @@ public class AbsoluteValueTest { void testGetAbsValue() { Stream.generate(() -> ThreadLocalRandom.current().nextInt()).limit(1000).forEach(number -> assertEquals(Math.abs(number), AbsoluteValue.getAbsValue(number))); } + + @Test + void testZero() { + assertEquals(0, AbsoluteValue.getAbsValue(0)); + } + + @Test + void testPositiveNumbers() { + assertEquals(5, AbsoluteValue.getAbsValue(5)); + assertEquals(123456, AbsoluteValue.getAbsValue(123456)); + assertEquals(Integer.MAX_VALUE, AbsoluteValue.getAbsValue(Integer.MAX_VALUE)); + } + + @Test + void testNegativeNumbers() { + assertEquals(5, AbsoluteValue.getAbsValue(-5)); + assertEquals(123456, AbsoluteValue.getAbsValue(-123456)); + assertEquals(Integer.MAX_VALUE, AbsoluteValue.getAbsValue(-Integer.MAX_VALUE)); + } + + @Test + void testMinIntEdgeCase() { + assertEquals(Integer.MIN_VALUE, AbsoluteValue.getAbsValue(Integer.MIN_VALUE)); + } } diff --git a/src/test/java/com/thealgorithms/maths/AverageTest.java b/src/test/java/com/thealgorithms/maths/AverageTest.java index c5c751938f5d..638739bc4fda 100644 --- a/src/test/java/com/thealgorithms/maths/AverageTest.java +++ b/src/test/java/com/thealgorithms/maths/AverageTest.java @@ -1,33 +1,48 @@ package com.thealgorithms.maths; -import org.junit.jupiter.api.Assertions; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class AverageTest { private static final double SMALL_VALUE = 0.00001d; - @Test - public void testAverageDouble12() { - double[] numbers = {3d, 6d, 9d, 12d, 15d, 18d, 21d}; - Assertions.assertEquals(12d, Average.average(numbers), SMALL_VALUE); + @ParameterizedTest(name = "average({0}) should be approximately {1}") + @MethodSource("provideDoubleArrays") + void testAverageDouble(double[] numbers, double expected) { + assertEquals(expected, Average.average(numbers), SMALL_VALUE); } - @Test - public void testAverageDouble20() { - double[] numbers = {5d, 10d, 15d, 20d, 25d, 30d, 35d}; - Assertions.assertEquals(20d, Average.average(numbers), SMALL_VALUE); + @ParameterizedTest(name = "average({0}) should be {1}") + @MethodSource("provideIntArrays") + void testAverageInt(int[] numbers, long expected) { + assertEquals(expected, Average.average(numbers)); } @Test - public void testAverageDouble() { - double[] numbers = {1d, 2d, 3d, 4d, 5d, 6d, 7d, 8d}; - Assertions.assertEquals(4.5d, Average.average(numbers), SMALL_VALUE); + void testAverageDoubleThrowsExceptionOnNullOrEmpty() { + assertThrows(IllegalArgumentException.class, () -> Average.average((double[]) null)); + assertThrows(IllegalArgumentException.class, () -> Average.average(new double[0])); } @Test - public void testAverageInt() { - int[] numbers = {2, 4, 10}; - Assertions.assertEquals(5, Average.average(numbers)); + void testAverageIntThrowsExceptionOnNullOrEmpty() { + assertThrows(IllegalArgumentException.class, () -> Average.average((int[]) null)); + assertThrows(IllegalArgumentException.class, () -> Average.average(new int[0])); + } + + private static Stream provideDoubleArrays() { + return Stream.of(Arguments.of(new double[] {3d, 6d, 9d, 12d, 15d, 18d, 21d}, 12d), Arguments.of(new double[] {5d, 10d, 15d, 20d, 25d, 30d, 35d}, 20d), Arguments.of(new double[] {1d, 2d, 3d, 4d, 5d, 6d, 7d, 8d}, 4.5d), Arguments.of(new double[] {0d, 0d, 0d}, 0d), + Arguments.of(new double[] {-1d, -2d, -3d}, -2d), Arguments.of(new double[] {1e-10, 1e-10, 1e-10}, 1e-10)); + } + + private static Stream provideIntArrays() { + return Stream.of(Arguments.of(new int[] {2, 4, 10}, 5L), Arguments.of(new int[] {0, 0, 0}, 0L), Arguments.of(new int[] {-1, -2, -3}, -2L), Arguments.of(new int[] {1, 1, 1, 1, 1}, 1L)); } } diff --git a/src/test/java/com/thealgorithms/maths/BinaryPowTest.java b/src/test/java/com/thealgorithms/maths/BinaryPowTest.java index f9b019e8fad4..632dfbd1d65e 100644 --- a/src/test/java/com/thealgorithms/maths/BinaryPowTest.java +++ b/src/test/java/com/thealgorithms/maths/BinaryPowTest.java @@ -13,4 +13,34 @@ void testBinPow() { assertEquals(729, BinaryPow.binPow(9, 3)); assertEquals(262144, BinaryPow.binPow(8, 6)); } + + @Test + void testZeroExponent() { + assertEquals(1, BinaryPow.binPow(2, 0)); + assertEquals(1, BinaryPow.binPow(100, 0)); + assertEquals(1, BinaryPow.binPow(-5, 0)); + } + + @Test + void testZeroBase() { + assertEquals(0, BinaryPow.binPow(0, 5)); + assertEquals(1, BinaryPow.binPow(0, 0)); + } + + @Test + void testOneBase() { + assertEquals(1, BinaryPow.binPow(1, 100)); + assertEquals(1, BinaryPow.binPow(1, 0)); + } + + @Test + void testNegativeBase() { + assertEquals(-8, BinaryPow.binPow(-2, 3)); + assertEquals(16, BinaryPow.binPow(-2, 4)); + } + + @Test + void testLargeExponent() { + assertEquals(1073741824, BinaryPow.binPow(2, 30)); + } } diff --git a/src/test/java/com/thealgorithms/maths/MedianTest.java b/src/test/java/com/thealgorithms/maths/MedianTest.java index d2b637abd3cb..560feb695792 100644 --- a/src/test/java/com/thealgorithms/maths/MedianTest.java +++ b/src/test/java/com/thealgorithms/maths/MedianTest.java @@ -1,6 +1,7 @@ package com.thealgorithms.maths; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; @@ -34,4 +35,10 @@ void medianNegativeValues() { int[] arr = {-27, -16, -7, -4, -2, -1}; assertEquals(-5.5, Median.median(arr)); } + + @Test + void medianEmptyArrayThrows() { + int[] arr = {}; + assertThrows(IllegalArgumentException.class, () -> Median.median(arr)); + } } diff --git a/src/test/java/com/thealgorithms/matrix/MatrixMultiplicationTest.java b/src/test/java/com/thealgorithms/matrix/MatrixMultiplicationTest.java new file mode 100644 index 000000000000..9463d33a18cb --- /dev/null +++ b/src/test/java/com/thealgorithms/matrix/MatrixMultiplicationTest.java @@ -0,0 +1,101 @@ +package com.thealgorithms.matrix; + +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 org.junit.jupiter.api.Test; + +public class MatrixMultiplicationTest { + + private static final double EPSILON = 1e-9; // for floating point comparison + + @Test + void testMultiply1by1() { + double[][] matrixA = {{1.0}}; + double[][] matrixB = {{2.0}}; + double[][] expected = {{2.0}}; + + double[][] result = MatrixMultiplication.multiply(matrixA, matrixB); + assertMatrixEquals(expected, result); + } + + @Test + void testMultiply2by2() { + double[][] matrixA = {{1.0, 2.0}, {3.0, 4.0}}; + double[][] matrixB = {{5.0, 6.0}, {7.0, 8.0}}; + double[][] expected = {{19.0, 22.0}, {43.0, 50.0}}; + + double[][] result = MatrixMultiplication.multiply(matrixA, matrixB); + assertMatrixEquals(expected, result); // Use custom method due to floating point issues + } + + @Test + void testMultiply3by2and2by1() { + double[][] matrixA = {{1.0, 2.0}, {3.0, 4.0}, {5.0, 6.0}}; + double[][] matrixB = {{7.0}, {8.0}}; + double[][] expected = {{23.0}, {53.0}, {83.0}}; + + double[][] result = MatrixMultiplication.multiply(matrixA, matrixB); + assertMatrixEquals(expected, result); + } + + @Test + void testMultiplyNonRectangularMatrices() { + double[][] matrixA = {{1.0, 2.0, 3.0}, {4.0, 5.0, 6.0}}; + double[][] matrixB = {{7.0, 8.0}, {9.0, 10.0}, {11.0, 12.0}}; + double[][] expected = {{58.0, 64.0}, {139.0, 154.0}}; + + double[][] result = MatrixMultiplication.multiply(matrixA, matrixB); + assertMatrixEquals(expected, result); + } + + @Test + void testNullMatrixA() { + double[][] b = {{1, 2}, {3, 4}}; + assertThrows(IllegalArgumentException.class, () -> MatrixMultiplication.multiply(null, b)); + } + + @Test + void testNullMatrixB() { + double[][] a = {{1, 2}, {3, 4}}; + assertThrows(IllegalArgumentException.class, () -> MatrixMultiplication.multiply(a, null)); + } + + @Test + void testMultiplyNull() { + double[][] matrixA = {{1.0, 2.0}, {3.0, 4.0}}; + double[][] matrixB = null; + + Exception exception = assertThrows(IllegalArgumentException.class, () -> MatrixMultiplication.multiply(matrixA, matrixB)); + + String expectedMessage = "Input matrices cannot be null"; + String actualMessage = exception.getMessage(); + + assertTrue(actualMessage.contains(expectedMessage)); + } + + @Test + void testIncompatibleDimensions() { + double[][] a = {{1.0, 2.0}}; + double[][] b = {{1.0, 2.0}}; + assertThrows(IllegalArgumentException.class, () -> MatrixMultiplication.multiply(a, b)); + } + + @Test + void testEmptyMatrices() { + double[][] a = new double[0][0]; + double[][] b = new double[0][0]; + assertThrows(IllegalArgumentException.class, () -> MatrixMultiplication.multiply(a, b)); + } + + private void assertMatrixEquals(double[][] expected, double[][] actual) { + assertEquals(expected.length, actual.length, "Row count mismatch"); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i].length, actual[i].length, "Column count mismatch at row " + i); + for (int j = 0; j < expected[i].length; j++) { + assertEquals(expected[i][j], actual[i][j], EPSILON, "Mismatch at (" + i + "," + j + ")"); + } + } + } +} diff --git a/src/test/java/com/thealgorithms/matrix/MedianOfMatrixTest.java b/src/test/java/com/thealgorithms/matrix/MedianOfMatrixTest.java index db66bb2d187b..b9b97014f3fc 100644 --- a/src/test/java/com/thealgorithms/matrix/MedianOfMatrixTest.java +++ b/src/test/java/com/thealgorithms/matrix/MedianOfMatrixTest.java @@ -1,9 +1,11 @@ package com.thealgorithms.matrix; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Test; @@ -31,4 +33,18 @@ public void testMedianWithEvenNumberOfElements() { assertEquals(2, result); } + + @Test + public void testMedianSingleElement() { + List> matrix = new ArrayList<>(); + matrix.add(List.of(1)); + + assertEquals(1, MedianOfMatrix.median(matrix)); + } + + @Test + void testEmptyMatrixThrowsException() { + Iterable> emptyMatrix = Collections.emptyList(); + assertThrows(IllegalArgumentException.class, () -> MedianOfMatrix.median(emptyMatrix), "Expected median() to throw, but it didn't"); + } } diff --git a/src/test/java/com/thealgorithms/misc/RangeInSortedArrayTest.java b/src/test/java/com/thealgorithms/misc/RangeInSortedArrayTest.java index 7630d3e78dc7..543c66130449 100644 --- a/src/test/java/com/thealgorithms/misc/RangeInSortedArrayTest.java +++ b/src/test/java/com/thealgorithms/misc/RangeInSortedArrayTest.java @@ -32,4 +32,26 @@ private static Stream provideGetCountLessThanTestCases() { return Stream.of(Arguments.of(new int[] {1, 2, 3, 3, 4, 5}, 3, 4, "Count of elements less than existing key"), Arguments.of(new int[] {1, 2, 3, 3, 4, 5}, 4, 5, "Count of elements less than non-existing key"), Arguments.of(new int[] {1, 2, 2, 3}, 5, 4, "Count with all smaller elements"), Arguments.of(new int[] {2, 3, 4, 5}, 1, 0, "Count with no smaller elements"), Arguments.of(new int[] {}, 1, 0, "Count in empty array")); } + + @ParameterizedTest(name = "Edge case {index}: {3}") + @MethodSource("provideEdgeCasesForSortedRange") + void testSortedRangeEdgeCases(int[] nums, int key, int[] expectedRange, String description) { + assertArrayEquals(expectedRange, RangeInSortedArray.sortedRange(nums, key), description); + } + + private static Stream provideEdgeCasesForSortedRange() { + return Stream.of(Arguments.of(new int[] {5, 5, 5, 5, 5}, 5, new int[] {0, 4}, "All elements same as key"), Arguments.of(new int[] {1, 2, 3, 4, 5}, 1, new int[] {0, 0}, "Key is first element"), Arguments.of(new int[] {1, 2, 3, 4, 5}, 5, new int[] {4, 4}, "Key is last element"), + Arguments.of(new int[] {1, 2, 3, 4, 5}, 0, new int[] {-1, -1}, "Key less than all elements"), Arguments.of(new int[] {1, 2, 3, 4, 5}, 6, new int[] {-1, -1}, "Key greater than all elements"), + Arguments.of(new int[] {1, 2, 2, 2, 3, 3, 3, 4}, 3, new int[] {4, 6}, "Multiple occurrences spread"), Arguments.of(new int[] {2}, 2, new int[] {0, 0}, "Single element array key exists"), Arguments.of(new int[] {2}, 3, new int[] {-1, -1}, "Single element array key missing")); + } + + @ParameterizedTest(name = "Edge case {index}: {3}") + @MethodSource("provideEdgeCasesForGetCountLessThan") + void testGetCountLessThanEdgeCases(int[] nums, int key, int expectedCount, String description) { + assertEquals(expectedCount, RangeInSortedArray.getCountLessThan(nums, key), description); + } + + private static Stream provideEdgeCasesForGetCountLessThan() { + return Stream.of(Arguments.of(new int[] {1, 2, 3, 4, 5}, 0, 0, "Key less than all elements"), Arguments.of(new int[] {1, 2, 3, 4, 5}, 6, 5, "Key greater than all elements"), Arguments.of(new int[] {1}, 0, 0, "Single element greater than key")); + } } diff --git a/src/test/java/com/thealgorithms/others/CRCAlgorithmTest.java b/src/test/java/com/thealgorithms/others/CRCAlgorithmTest.java index a581a35bf963..3dc61f2c6569 100644 --- a/src/test/java/com/thealgorithms/others/CRCAlgorithmTest.java +++ b/src/test/java/com/thealgorithms/others/CRCAlgorithmTest.java @@ -1,29 +1,163 @@ - package com.thealgorithms.others; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; public class CRCAlgorithmTest { @Test - void test1() { + void testNoErrorsWithZeroBER() { CRCAlgorithm c = new CRCAlgorithm("10010101010100101010010000001010010101010", 10, 0.0); + c.generateRandomMess(); + c.divideMessageWithP(false); + c.changeMess(); + c.divideMessageWithP(true); + assertEquals(0, c.getWrongMess(), "BER=0 should produce no wrong messages"); + assertEquals(0, c.getWrongMessCaught(), "No errors, so no caught wrong messages"); + assertEquals(0, c.getWrongMessNotCaught(), "No errors, so no uncaught wrong messages"); + assertTrue(c.getCorrectMess() > 0, "Should have some correct messages"); + } + + @Test + void testAllErrorsWithBEROne() { + CRCAlgorithm c = new CRCAlgorithm("10010101010100101010010000001010010101010", 10, 1.0); + c.generateRandomMess(); + c.divideMessageWithP(false); + c.changeMess(); + c.divideMessageWithP(true); + assertTrue(c.getWrongMess() > 0, "BER=1 should produce wrong messages"); + assertEquals(0, c.getCorrectMess(), "BER=1 should produce no correct messages"); + } + + @Test + void testIntermediateBER() { + CRCAlgorithm c = new CRCAlgorithm("1101", 4, 0.5); + c.generateRandomMess(); + for (int i = 0; i < 1000; i++) { + c.refactor(); + c.generateRandomMess(); + c.divideMessageWithP(false); + c.changeMess(); + c.divideMessageWithP(true); + } + assertTrue(c.getWrongMess() > 0, "Some wrong messages expected with BER=0.5"); + assertTrue(c.getWrongMessCaught() >= 0, "Wrong messages caught counter >= 0"); + assertTrue(c.getWrongMessNotCaught() >= 0, "Wrong messages not caught counter >= 0"); + assertTrue(c.getCorrectMess() >= 0, "Correct messages counter >= 0"); + assertEquals(c.getWrongMess(), c.getWrongMessCaught() + c.getWrongMessNotCaught(), "Sum of caught and not caught wrong messages should equal total wrong messages"); + } - // A bit-error rate of 0.0 should not provide any wrong messages + @Test + void testMessageChangedFlag() { + CRCAlgorithm c = new CRCAlgorithm("1010", 4, 1.0); + c.generateRandomMess(); + c.divideMessageWithP(false); c.changeMess(); + assertTrue(c.getWrongMess() > 0, "Message should be marked as changed with BER=1"); + } + + @Test + void testSmallMessageSize() { + CRCAlgorithm c = new CRCAlgorithm("11", 2, 0.0); + c.generateRandomMess(); c.divideMessageWithP(false); - assertEquals(c.getWrongMess(), 0); + c.changeMess(); + c.divideMessageWithP(true); + assertEquals(0, c.getWrongMess(), "No errors expected for BER=0 with small message"); } @Test - void test2() { - CRCAlgorithm c = new CRCAlgorithm("10010101010100101010010000001010010101010", 10, 1.0); + void testLargeMessageSize() { + CRCAlgorithm c = new CRCAlgorithm("1101", 1000, 0.01); + c.generateRandomMess(); + c.divideMessageWithP(false); + c.changeMess(); + c.divideMessageWithP(true); + // Just ensure counters are updated, no exceptions + assertTrue(c.getWrongMess() >= 0); + assertTrue(c.getCorrectMess() >= 0); + } + + @Test + void testSingleBitMessage() { + CRCAlgorithm c = new CRCAlgorithm("11", 1, 0.0); + c.generateRandomMess(); + c.divideMessageWithP(false); + c.changeMess(); + c.divideMessageWithP(true); + + assertTrue(c.getCorrectMess() >= 0, "Single bit message should be handled"); + } - // A bit error rate of 1.0 should not provide any correct messages + @Test + void testPolynomialLongerThanMessage() { + CRCAlgorithm c = new CRCAlgorithm("11010101", 3, 0.0); + c.generateRandomMess(); + c.divideMessageWithP(false); c.changeMess(); + c.divideMessageWithP(true); + + // Should not crash, counters should be valid + assertTrue(c.getCorrectMess() + c.getWrongMess() >= 0); + } + + @Test + void testPolynomialWithOnlyOnes() { + CRCAlgorithm c = new CRCAlgorithm("1111", 5, 0.1); + c.generateRandomMess(); c.divideMessageWithP(false); - assertEquals(c.getCorrectMess(), 0); + c.changeMess(); + c.divideMessageWithP(true); + + assertTrue(c.getCorrectMess() + c.getWrongMess() >= 0); + } + + @Test + void testMultipleRefactorCalls() { + CRCAlgorithm c = new CRCAlgorithm("1101", 5, 0.2); + + for (int i = 0; i < 5; i++) { + c.refactor(); + c.generateRandomMess(); + c.divideMessageWithP(false); + c.changeMess(); + c.divideMessageWithP(true); + } + + // Counters should accumulate across multiple runs + assertTrue(c.getCorrectMess() + c.getWrongMess() > 0); + } + + @Test + void testCounterConsistency() { + CRCAlgorithm c = new CRCAlgorithm("1101", 10, 0.3); + + for (int i = 0; i < 100; i++) { + c.refactor(); + c.generateRandomMess(); + c.divideMessageWithP(false); + c.changeMess(); + c.divideMessageWithP(true); + } + + // Total messages processed should equal correct + wrong + int totalProcessed = c.getCorrectMess() + c.getWrongMess(); + assertEquals(100, totalProcessed, "Total processed messages should equal iterations"); + + // Wrong messages should equal caught + not caught + assertEquals(c.getWrongMess(), c.getWrongMessCaught() + c.getWrongMessNotCaught(), "Wrong messages should equal sum of caught and not caught"); + } + + @Test + void testGetterMethodsInitialState() { + CRCAlgorithm c = new CRCAlgorithm("1101", 10, 0.1); + + // Check initial state + assertEquals(0, c.getCorrectMess(), "Initial correct messages should be 0"); + assertEquals(0, c.getWrongMess(), "Initial wrong messages should be 0"); + assertEquals(0, c.getWrongMessCaught(), "Initial caught wrong messages should be 0"); + assertEquals(0, c.getWrongMessNotCaught(), "Initial not caught wrong messages should be 0"); } } diff --git a/src/test/java/com/thealgorithms/recursion/GenerateSubsetsTest.java b/src/test/java/com/thealgorithms/recursion/GenerateSubsetsTest.java index b92d1406b0a7..983552722781 100644 --- a/src/test/java/com/thealgorithms/recursion/GenerateSubsetsTest.java +++ b/src/test/java/com/thealgorithms/recursion/GenerateSubsetsTest.java @@ -1,36 +1,40 @@ package com.thealgorithms.recursion; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import java.util.Arrays; import java.util.List; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; public final class GenerateSubsetsTest { @Test - void subsetRecursionTestOne() { - String str = "abc"; - String[] expected = new String[] {"abc", "ab", "ac", "a", "bc", "b", "c", ""}; - - List ans = GenerateSubsets.subsetRecursion(str); - assertArrayEquals(ans.toArray(), expected); + @DisplayName("Subsets of 'abc'") + void testSubsetsOfABC() { + assertSubsets("abc", Arrays.asList("abc", "ab", "ac", "a", "bc", "b", "c", "")); } @Test - void subsetRecursionTestTwo() { - String str = "cbf"; - String[] expected = new String[] {"cbf", "cb", "cf", "c", "bf", "b", "f", ""}; + @DisplayName("Subsets of 'cbf'") + void testSubsetsOfCBF() { + assertSubsets("cbf", Arrays.asList("cbf", "cb", "cf", "c", "bf", "b", "f", "")); + } - List ans = GenerateSubsets.subsetRecursion(str); - assertArrayEquals(ans.toArray(), expected); + @Test + @DisplayName("Subsets of 'aba' with duplicates") + void testSubsetsWithDuplicateChars() { + assertSubsets("aba", Arrays.asList("aba", "ab", "aa", "a", "ba", "b", "a", "")); } @Test - void subsetRecursionTestThree() { - String str = "aba"; - String[] expected = new String[] {"aba", "ab", "aa", "a", "ba", "b", "a", ""}; + @DisplayName("Subsets of empty string") + void testEmptyInput() { + assertSubsets("", List.of("")); + } - List ans = GenerateSubsets.subsetRecursion(str); - assertArrayEquals(ans.toArray(), expected); + private void assertSubsets(String input, Iterable expected) { + List actual = GenerateSubsets.subsetRecursion(input); + assertIterableEquals(expected, actual, "Subsets do not match for input: " + input); } } diff --git a/src/test/java/com/thealgorithms/sorts/SimpleSortTest.java b/src/test/java/com/thealgorithms/sorts/SimpleSortTest.java deleted file mode 100644 index 887b314e46ff..000000000000 --- a/src/test/java/com/thealgorithms/sorts/SimpleSortTest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.thealgorithms.sorts; - -public class SimpleSortTest extends SortingAlgorithmTest { - @Override - SortAlgorithm getSortAlgorithm() { - return new SimpleSort(); - } -} diff --git a/src/test/java/com/thealgorithms/stacks/DuplicateBracketsTest.java b/src/test/java/com/thealgorithms/stacks/DuplicateBracketsTest.java index e2cc6acb8112..bc7b3266d98e 100644 --- a/src/test/java/com/thealgorithms/stacks/DuplicateBracketsTest.java +++ b/src/test/java/com/thealgorithms/stacks/DuplicateBracketsTest.java @@ -4,20 +4,23 @@ 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.Arguments; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; class DuplicateBracketsTest { @ParameterizedTest - @CsvSource({"'((a + b) + (c + d))'", "'(a + b)'", "'a + b'", "'('", "''"}) + @CsvSource({"'((a + b) + (c + d))'", "'(a + b)'", "'a + b'", "'('", "''", "'a + (b * c) - d'", "'(x + y) * (z)'", "'(a + (b - c))'"}) void testInputReturnsFalse(String input) { assertFalse(DuplicateBrackets.check(input)); } @ParameterizedTest - @CsvSource({"'(a + b) + ((c + d))'", "'((a + b))'", "'((((a + b)))))'"}) + @CsvSource({"'(a + b) + ((c + d))'", "'((a + b))'", "'((((a + b)))))'", "'((x))'", "'((a + (b)))'", "'(a + ((b)))'", "'(((a)))'", "'(((())))'"}) void testInputReturnsTrue(String input) { assertTrue(DuplicateBrackets.check(input)); } @@ -26,4 +29,27 @@ void testInputReturnsTrue(String input) { void testInvalidInput() { assertThrows(IllegalArgumentException.class, () -> DuplicateBrackets.check(null)); } + + @ParameterizedTest(name = "Should be true: \"{0}\"") + @MethodSource("provideInputsThatShouldReturnTrue") + void testDuplicateBracketsTrueCases(String input) { + assertTrue(DuplicateBrackets.check(input)); + } + + static Stream provideInputsThatShouldReturnTrue() { + return Stream.of(Arguments.of("()"), Arguments.of("(( ))")); + } + + @ParameterizedTest(name = "Should be false: \"{0}\"") + @MethodSource("provideInputsThatShouldReturnFalse") + void testDuplicateBracketsFalseCases(String input) { + assertFalse(DuplicateBrackets.check(input)); + } + + static Stream provideInputsThatShouldReturnFalse() { + return Stream.of(Arguments.of("( )"), // whitespace inside brackets + Arguments.of("abc + def"), // no brackets + Arguments.of("(a + (b * c)) - (d / e)") // complex, but no duplicates + ); + } } diff --git a/src/test/java/com/thealgorithms/stacks/MinStackUsingTwoStacksTest.java b/src/test/java/com/thealgorithms/stacks/MinStackUsingTwoStacksTest.java index e5deb17e9a8f..36bdde49b235 100644 --- a/src/test/java/com/thealgorithms/stacks/MinStackUsingTwoStacksTest.java +++ b/src/test/java/com/thealgorithms/stacks/MinStackUsingTwoStacksTest.java @@ -1,38 +1,112 @@ package com.thealgorithms.stacks; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.EmptyStackException; import org.junit.jupiter.api.Test; public class MinStackUsingTwoStacksTest { @Test - public void testMinStackOperations() { + public void testBasicOperations() { MinStackUsingTwoStacks minStack = new MinStackUsingTwoStacks(); minStack.push(3); minStack.push(5); - assertEquals(3, minStack.getMin()); + assertEquals(3, minStack.getMin(), "Min should be 3"); minStack.push(2); minStack.push(1); - assertEquals(1, minStack.getMin()); + assertEquals(1, minStack.getMin(), "Min should be 1"); minStack.pop(); - assertEquals(2, minStack.getMin()); + assertEquals(2, minStack.getMin(), "Min should be 2 after popping 1"); + + assertEquals(2, minStack.top(), "Top should be 2"); + } + + @Test + public void testPushDuplicateMins() { + MinStackUsingTwoStacks minStack = new MinStackUsingTwoStacks(); + minStack.push(2); + minStack.push(2); + minStack.push(1); + minStack.push(1); + assertEquals(1, minStack.getMin(), "Min should be 1"); + + minStack.pop(); + assertEquals(1, minStack.getMin(), "Min should still be 1 after popping one 1"); + + minStack.pop(); + assertEquals(2, minStack.getMin(), "Min should be 2 after popping both 1s"); + + minStack.pop(); + assertEquals(2, minStack.getMin(), "Min should still be 2 after popping one 2"); + + minStack.pop(); + // Now stack is empty, expect exception on getMin + assertThrows(EmptyStackException.class, minStack::getMin); } @Test - public void testMinStackOperations2() { + public void testPopOnEmptyStack() { MinStackUsingTwoStacks minStack = new MinStackUsingTwoStacks(); + assertThrows(EmptyStackException.class, minStack::pop); + } + + @Test + public void testTopOnEmptyStack() { + MinStackUsingTwoStacks minStack = new MinStackUsingTwoStacks(); + assertThrows(EmptyStackException.class, minStack::top); + } + + @Test + public void testGetMinOnEmptyStack() { + MinStackUsingTwoStacks minStack = new MinStackUsingTwoStacks(); + assertThrows(EmptyStackException.class, minStack::getMin); + } + + @Test + public void testSingleElementStack() { + MinStackUsingTwoStacks minStack = new MinStackUsingTwoStacks(); + minStack.push(10); + assertEquals(10, minStack.getMin()); + assertEquals(10, minStack.top()); + + minStack.pop(); + assertThrows(EmptyStackException.class, minStack::getMin); + } + + @Test + public void testIncreasingSequence() { + MinStackUsingTwoStacks minStack = new MinStackUsingTwoStacks(); + minStack.push(1); + minStack.push(2); minStack.push(3); - minStack.push(5); - assertEquals(3, minStack.getMin()); + minStack.push(4); + + assertEquals(1, minStack.getMin()); + assertEquals(4, minStack.top()); + + minStack.pop(); + minStack.pop(); + assertEquals(1, minStack.getMin()); + assertEquals(2, minStack.top()); + } + @Test + public void testDecreasingSequence() { + MinStackUsingTwoStacks minStack = new MinStackUsingTwoStacks(); + minStack.push(4); + minStack.push(3); minStack.push(2); minStack.push(1); + assertEquals(1, minStack.getMin()); + assertEquals(1, minStack.top()); minStack.pop(); assertEquals(2, minStack.getMin()); + assertEquals(2, minStack.top()); } } diff --git a/src/test/java/com/thealgorithms/stacks/PostfixEvaluatorTest.java b/src/test/java/com/thealgorithms/stacks/PostfixEvaluatorTest.java index 882fe644ccd5..682240acd752 100644 --- a/src/test/java/com/thealgorithms/stacks/PostfixEvaluatorTest.java +++ b/src/test/java/com/thealgorithms/stacks/PostfixEvaluatorTest.java @@ -4,24 +4,41 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.EmptyStackException; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; public class PostfixEvaluatorTest { - @Test - public void testValidExpressions() { - assertEquals(22, PostfixEvaluator.evaluatePostfix("5 6 + 2 *")); - assertEquals(27, PostfixEvaluator.evaluatePostfix("7 2 + 3 *")); - assertEquals(3, PostfixEvaluator.evaluatePostfix("10 5 / 1 +")); + @ParameterizedTest(name = "Expression: \"{0}\" → Result: {1}") + @CsvSource({"'5 6 + 2 *', 22", "'7 2 + 3 *', 27", "'10 5 / 1 +', 3", "'8', 8", "'3 4 +', 7"}) + @DisplayName("Valid postfix expressions") + void testValidExpressions(String expression, int expected) { + assertEquals(expected, PostfixEvaluator.evaluatePostfix(expression)); } @Test - public void testInvalidExpression() { + @DisplayName("Should throw EmptyStackException for incomplete expression") + void testInvalidExpression() { assertThrows(EmptyStackException.class, () -> PostfixEvaluator.evaluatePostfix("5 +")); } @Test - public void testMoreThanOneStackSizeAfterEvaluation() { + @DisplayName("Should throw IllegalArgumentException for extra operands") + void testExtraOperands() { assertThrows(IllegalArgumentException.class, () -> PostfixEvaluator.evaluatePostfix("5 6 + 2 * 3")); } + + @Test + @DisplayName("Should throw ArithmeticException for division by zero") + void testDivisionByZero() { + assertThrows(ArithmeticException.class, () -> PostfixEvaluator.evaluatePostfix("1 0 /")); + } + + @Test + @DisplayName("Should throw IllegalArgumentException for invalid characters") + void testInvalidToken() { + assertThrows(IllegalArgumentException.class, () -> PostfixEvaluator.evaluatePostfix("1 a +")); + } } diff --git a/src/test/java/com/thealgorithms/stacks/PrefixEvaluatorTest.java b/src/test/java/com/thealgorithms/stacks/PrefixEvaluatorTest.java index e2faa61955b3..ba67163fd49e 100644 --- a/src/test/java/com/thealgorithms/stacks/PrefixEvaluatorTest.java +++ b/src/test/java/com/thealgorithms/stacks/PrefixEvaluatorTest.java @@ -4,24 +4,28 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.EmptyStackException; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; public class PrefixEvaluatorTest { - @Test - public void testValidExpressions() { - assertEquals(10, PrefixEvaluator.evaluatePrefix("+ * 2 3 4")); - assertEquals(5, PrefixEvaluator.evaluatePrefix("- + 7 3 5")); - assertEquals(6, PrefixEvaluator.evaluatePrefix("/ * 3 2 1")); + @ParameterizedTest(name = "Expression: \"{0}\" → Result: {1}") + @CsvSource({"'+ * 2 3 4', 10", "'- + 7 3 5', 5", "'/ * 3 2 1', 6"}) + void testValidExpressions(String expression, int expected) { + assertEquals(expected, PrefixEvaluator.evaluatePrefix(expression)); } @Test - public void testInvalidExpression() { + @DisplayName("Should throw EmptyStackException for incomplete expression") + void testInvalidExpression() { assertThrows(EmptyStackException.class, () -> PrefixEvaluator.evaluatePrefix("+ 3")); } @Test - public void testMoreThanOneStackSizeAfterEvaluation() { + @DisplayName("Should throw IllegalArgumentException if stack not reduced to one result") + void testMoreThanOneStackSizeAfterEvaluation() { assertThrows(IllegalArgumentException.class, () -> PrefixEvaluator.evaluatePrefix("+ 3 4 5")); } } diff --git a/src/test/java/com/thealgorithms/stacks/SortStackTest.java b/src/test/java/com/thealgorithms/stacks/SortStackTest.java index b9f2f1b6f106..9747af5337e8 100644 --- a/src/test/java/com/thealgorithms/stacks/SortStackTest.java +++ b/src/test/java/com/thealgorithms/stacks/SortStackTest.java @@ -74,4 +74,187 @@ public void testSortWithDuplicateElements() { assertEquals(2, (int) stack.pop()); assertEquals(1, (int) stack.pop()); } + + @Test + public void testSortReverseSortedStack() { + // Test worst case scenario - completely reverse sorted + stack.push(5); + stack.push(4); + stack.push(3); + stack.push(2); + stack.push(1); + SortStack.sortStack(stack); + + assertEquals(5, stack.size()); + assertEquals(5, (int) stack.pop()); + assertEquals(4, (int) stack.pop()); + assertEquals(3, (int) stack.pop()); + assertEquals(2, (int) stack.pop()); + assertEquals(1, (int) stack.pop()); + } + + @Test + public void testSortWithAllSameElements() { + // Test stack with all identical elements + stack.push(7); + stack.push(7); + stack.push(7); + stack.push(7); + SortStack.sortStack(stack); + + assertEquals(4, stack.size()); + assertEquals(7, (int) stack.pop()); + assertEquals(7, (int) stack.pop()); + assertEquals(7, (int) stack.pop()); + assertEquals(7, (int) stack.pop()); + } + + @Test + public void testSortWithNegativeNumbers() { + // Test with negative numbers + stack.push(-3); + stack.push(1); + stack.push(-5); + stack.push(2); + stack.push(-1); + SortStack.sortStack(stack); + + assertEquals(5, stack.size()); + assertEquals(2, (int) stack.pop()); + assertEquals(1, (int) stack.pop()); + assertEquals(-1, (int) stack.pop()); + assertEquals(-3, (int) stack.pop()); + assertEquals(-5, (int) stack.pop()); + } + + @Test + public void testSortWithAllNegativeNumbers() { + // Test with only negative numbers + stack.push(-10); + stack.push(-5); + stack.push(-15); + stack.push(-1); + SortStack.sortStack(stack); + + assertEquals(4, stack.size()); + assertEquals(-1, (int) stack.pop()); + assertEquals(-5, (int) stack.pop()); + assertEquals(-10, (int) stack.pop()); + assertEquals(-15, (int) stack.pop()); + } + + @Test + public void testSortWithZero() { + // Test with zero included + stack.push(3); + stack.push(0); + stack.push(-2); + stack.push(1); + SortStack.sortStack(stack); + + assertEquals(4, stack.size()); + assertEquals(3, (int) stack.pop()); + assertEquals(1, (int) stack.pop()); + assertEquals(0, (int) stack.pop()); + assertEquals(-2, (int) stack.pop()); + } + + @Test + public void testSortLargerStack() { + // Test with a larger number of elements + int[] values = {15, 3, 9, 1, 12, 6, 18, 4, 11, 8}; + for (int value : values) { + stack.push(value); + } + + SortStack.sortStack(stack); + + assertEquals(10, stack.size()); + + // Verify sorted order (largest to smallest when popping) + int[] expectedOrder = {18, 15, 12, 11, 9, 8, 6, 4, 3, 1}; + for (int expected : expectedOrder) { + assertEquals(expected, (int) stack.pop()); + } + } + + @Test + public void testSortTwoElements() { + // Test edge case with exactly two elements + stack.push(5); + stack.push(2); + SortStack.sortStack(stack); + + assertEquals(2, stack.size()); + assertEquals(5, (int) stack.pop()); + assertEquals(2, (int) stack.pop()); + } + + @Test + public void testSortTwoElementsAlreadySorted() { + // Test two elements already in correct order + stack.push(2); + stack.push(5); + SortStack.sortStack(stack); + + assertEquals(2, stack.size()); + assertEquals(5, (int) stack.pop()); + assertEquals(2, (int) stack.pop()); + } + + @Test + public void testSortStackWithMinAndMaxValues() { + // Test with Integer.MAX_VALUE and Integer.MIN_VALUE + stack.push(0); + stack.push(Integer.MAX_VALUE); + stack.push(Integer.MIN_VALUE); + stack.push(100); + SortStack.sortStack(stack); + + assertEquals(4, stack.size()); + assertEquals(Integer.MAX_VALUE, (int) stack.pop()); + assertEquals(100, (int) stack.pop()); + assertEquals(0, (int) stack.pop()); + assertEquals(Integer.MIN_VALUE, (int) stack.pop()); + } + + @Test + public void testSortWithManyDuplicates() { + // Test with multiple sets of duplicates + stack.push(3); + stack.push(1); + stack.push(3); + stack.push(1); + stack.push(2); + stack.push(2); + stack.push(3); + SortStack.sortStack(stack); + + assertEquals(7, stack.size()); + assertEquals(3, (int) stack.pop()); + assertEquals(3, (int) stack.pop()); + assertEquals(3, (int) stack.pop()); + assertEquals(2, (int) stack.pop()); + assertEquals(2, (int) stack.pop()); + assertEquals(1, (int) stack.pop()); + assertEquals(1, (int) stack.pop()); + } + + @Test + public void testOriginalStackIsModified() { + // Verify that the original stack is modified, not a copy + Stack originalReference = stack; + stack.push(3); + stack.push(1); + stack.push(2); + + SortStack.sortStack(stack); + + // Verify it's the same object reference + assertTrue(stack == originalReference); + assertEquals(3, stack.size()); + assertEquals(3, (int) stack.pop()); + assertEquals(2, (int) stack.pop()); + assertEquals(1, (int) stack.pop()); + } } diff --git a/src/test/java/com/thealgorithms/strings/ReverseStringTest.java b/src/test/java/com/thealgorithms/strings/ReverseStringTest.java index 08f5fb586d82..476765b00dfc 100644 --- a/src/test/java/com/thealgorithms/strings/ReverseStringTest.java +++ b/src/test/java/com/thealgorithms/strings/ReverseStringTest.java @@ -1,8 +1,10 @@ package com.thealgorithms.strings; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.stream.Stream; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -31,4 +33,15 @@ public void testReverseString2(String input, String expectedOutput) { public void testReverseString3(String input, String expectedOutput) { assertEquals(expectedOutput, ReverseString.reverse3(input)); } + + @ParameterizedTest + @MethodSource("testCases") + public void testReverseStringUsingStack(String input, String expectedOutput) { + assertEquals(expectedOutput, ReverseString.reverseStringUsingStack(input)); + } + + @Test + public void testReverseStringUsingStackWithNullInput() { + assertThrows(IllegalArgumentException.class, () -> ReverseString.reverseStringUsingStack(null)); + } } diff --git a/src/test/java/com/thealgorithms/strings/ValidParenthesesTest.java b/src/test/java/com/thealgorithms/strings/ValidParenthesesTest.java index 23d41b159fe2..411b11e743b8 100644 --- a/src/test/java/com/thealgorithms/strings/ValidParenthesesTest.java +++ b/src/test/java/com/thealgorithms/strings/ValidParenthesesTest.java @@ -1,15 +1,33 @@ package com.thealgorithms.strings; 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 org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; public class ValidParenthesesTest { @ParameterizedTest(name = "Input: \"{0}\" → Expected: {1}") - @CsvSource({"'()', true", "'()[]{}', true", "'(]', false", "'{[]}', true", "'([{}])', true", "'([)]', false", "'', true", "'(', false", "')', false"}) - void testIsValid(String input, boolean expected) { + @CsvSource({"'()', true", "'()[]{}', true", "'(]', false", "'{[]}', true", "'([{}])', true", "'([)]', false", "'', true", "'(', false", "')', false", "'{{{{}}}}', true", "'[({})]', true", "'[(])', false", "'[', false", "']', false", "'()()()()', true", "'(()', false", "'())', false", + "'{[()()]()}', true"}) + void + testIsValid(String input, boolean expected) { assertEquals(expected, ValidParentheses.isValid(input)); } + + @Test + void testNullInputThrows() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> ValidParentheses.isValid(null)); + assertEquals("Input string cannot be null", ex.getMessage()); + } + + @ParameterizedTest(name = "Input: \"{0}\" → throws IllegalArgumentException") + @CsvSource({"'a'", "'()a'", "'[123]'", "'{hello}'", "'( )'", "'\t'", "'\n'", "'@#$%'"}) + void testInvalidCharactersThrow(String input) { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> ValidParentheses.isValid(input)); + assertTrue(ex.getMessage().startsWith("Unexpected character")); + } }