+ * The cache holds a fixed number of entries, defined by its capacity. When the cache is full and a + * new entry is added, one of the existing entries is selected at random 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: + *
This constructor initializes the cache with the specified capacity and default TTL,
+ * sets up internal data structures (a {@code HashMap} for cache entries and an {@code ArrayList}
+ * for key tracking), and configures eviction and randomization behavior.
+ *
+ * @param builder the {@code Builder} object containing configuration parameters
+ */
+ private RRCache(Builder 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);
+
+ CacheEntry 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 updated and its TTL is reset. If the key
+ * does not exist and the cache is full, a random 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 (cache.containsKey(key)) {
+ cache.put(key, new CacheEntry<>(value, ttlMillis));
+ return;
+ }
+
+ evictExpired();
+
+ if (cache.size() >= capacity) {
+ int idx = random.nextInt(keys.size());
+ K evictKey = keys.remove(idx);
+ CacheEntry This method iterates through the list of cached keys and checks each associated
+ * entry for expiration. Expired entries are removed from both the key tracking list
+ * and the cache map. For each eviction, the eviction listener is notified.
+ */
+ private int evictExpired() {
+ Iterator This method deletes the key from both the cache map and the key tracking list.
+ *
+ * @param key the key to remove from the cache
+ */
+ private void removeKey(K key) {
+ cache.remove(key);
+ keys.remove(key);
+ }
+
+ /**
+ * 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 {
+ int cachedSize = cache.size();
+ int evictedCount = evictionStrategy.onAccess(this);
+ if (evictedCount > 0) {
+ return cachedSize - evictedCount;
+ }
+
+ // This runs if periodic eviction does not occur
+ int count = 0;
+ for (Map.Entry 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 {
+ Map Implementations decide whether and when to trigger {@link RRCache#evictExpired()} based
+ * on cache usage patterns. This allows for flexible eviction behaviour such as periodic cleanup,
+ * or no automatic cleanup.
+ *
+ * @param This deterministic strategy ensures cleanup occurs at predictable intervals,
+ * ideal for moderately active caches where memory usage is a concern.
+ *
+ * @param Allows configuring capacity, default TTL, random eviction behavior, eviction listener,
+ * and a pluggable eviction strategy. Call {@link #build()} to create the configured cache instance.
+ *
+ * @param Time Complexity: O(E log V), where E is the number of edges and V is the number of vertices.
+ * Run-time complexity: O(n log n)
+ * What is special about this 2-stack technique is that it enables us to remove element a[i] and find gcd(a[i+1],...,a[i+l]) in amortized O(1) time.
+ * For 'remove' worst-case would be O(n) operation, but this happens rarely.
+ * Main observation is that each element gets processed a constant amount of times, hence complexity will be:
+ * O(n log n), where log n comes from complexity of gcd.
+ *
+ * More generally, the 2-stack technique enables us to 'remove' an element fast if it is known how to 'add' an element fast to the set.
+ * In our case 'adding' is calculating d' = gcd(a[i],...,a[i+l+1]), when d = gcd(a[i],...a[i]) with d' = gcd(d, a[i+l+1]).
+ * and removing is find gcd(a[i+1],...,a[i+l]). We don't calculate it explicitly, but it is pushed in the stack which we can pop in O(1).
+ *
+ * One can change methods 'legalSegment' and function 'f' in DoubleStack to adapt this code to other sliding-window type problems.
+ * I recommend this article for more explanations: "CF Article">Article 1 or USACO Article
+ *
+ * Another method to solve this problem is through segment trees. Then query operation would have O(log n), not O(1) time, but runtime complexity would still be O(n log n)
+ *
+ * @author DomTr (Github)
+ */
+public final class ShortestCoprimeSegment {
+ // Prevent instantiation
+ private ShortestCoprimeSegment() {
+ }
+
+ /**
+ * @param arr is the input array
+ * @return shortest segment in the array which has gcd equal to 1. If no such segment exists or array is empty, returns empty array
+ */
+ public static long[] shortestCoprimeSegment(long[] arr) {
+ if (arr == null || arr.length == 0) {
+ return new long[] {};
+ }
+ DoubleStack front = new DoubleStack();
+ DoubleStack back = new DoubleStack();
+ int n = arr.length;
+ int l = 0;
+ int shortestLength = n + 1;
+ int beginsAt = -1; // beginning index of the shortest coprime segment
+ for (int i = 0; i < n; i++) {
+ back.push(arr[i]);
+ while (legalSegment(front, back)) {
+ remove(front, back);
+ if (shortestLength > i - l + 1) {
+ beginsAt = l;
+ shortestLength = i - l + 1;
+ }
+ l++;
+ }
+ }
+ if (shortestLength > n) {
+ shortestLength = -1;
+ }
+ if (shortestLength == -1) {
+ return new long[] {};
+ }
+ return Arrays.copyOfRange(arr, beginsAt, beginsAt + shortestLength);
+ }
+
+ private static boolean legalSegment(DoubleStack front, DoubleStack back) {
+ return gcd(front.top(), back.top()) == 1;
+ }
+
+ private static long gcd(long a, long b) {
+ if (a < b) {
+ return gcd(b, a);
+ } else if (b == 0) {
+ return a;
+ } else {
+ return gcd(a % b, b);
+ }
+ }
+
+ /**
+ * This solves the problem of removing elements quickly.
+ * Even though the worst case of 'remove' method is O(n), it is a very pessimistic view.
+ * We will need to empty out 'back', only when 'from' is empty.
+ * Consider element x when it is added to stack 'back'.
+ * After some time 'front' becomes empty and x goes to 'front'. Notice that in the for-loop we proceed further and x will never come back to any stacks 'back' or 'front'.
+ * In other words, every element gets processed by a constant number of operations.
+ * So 'remove' amortized runtime is actually O(n).
+ */
+ private static void remove(DoubleStack front, DoubleStack back) {
+ if (front.isEmpty()) {
+ while (!back.isEmpty()) {
+ front.push(back.pop());
+ }
+ }
+ front.pop();
+ }
+
+ /**
+ * DoubleStack serves as a collection of two stacks. One is a normal stack called 'stack', the other 'values' stores gcd-s up until some index.
+ */
+ private static class DoubleStack {
+ LinkedList
* For more details @see TimSort Algorithm
*/
+@SuppressWarnings({"rawtypes", "unchecked"})
class TimSort implements SortAlgorithm {
private static final int SUB_ARRAY_SIZE = 32;
private Comparable[] aux;
diff --git a/src/main/java/com/thealgorithms/strings/Anagrams.java b/src/main/java/com/thealgorithms/strings/Anagrams.java
index 4b24979e2689..5b97af0758f2 100644
--- a/src/main/java/com/thealgorithms/strings/Anagrams.java
+++ b/src/main/java/com/thealgorithms/strings/Anagrams.java
@@ -23,7 +23,9 @@ private Anagrams() {
* @param t the second string
* @return true if the strings are anagrams, false otherwise
*/
- public static boolean approach1(String s, String t) {
+ public static boolean areAnagramsBySorting(String s, String t) {
+ s = s.toLowerCase().replaceAll("[^a-z]", "");
+ t = t.toLowerCase().replaceAll("[^a-z]", "");
if (s.length() != t.length()) {
return false;
}
@@ -43,17 +45,18 @@ public static boolean approach1(String s, String t) {
* @param t the second string
* @return true if the strings are anagrams, false otherwise
*/
- public static boolean approach2(String s, String t) {
- if (s.length() != t.length()) {
- return false;
+ public static boolean areAnagramsByCountingChars(String s, String t) {
+ s = s.toLowerCase().replaceAll("[^a-z]", "");
+ t = t.toLowerCase().replaceAll("[^a-z]", "");
+ int[] dict = new int[128];
+ for (char ch : s.toCharArray()) {
+ dict[ch]++;
}
- int[] charCount = new int[26];
- for (int i = 0; i < s.length(); i++) {
- charCount[s.charAt(i) - 'a']++;
- charCount[t.charAt(i) - 'a']--;
+ for (char ch : t.toCharArray()) {
+ dict[ch]--;
}
- for (int count : charCount) {
- if (count != 0) {
+ for (int e : dict) {
+ if (e != 0) {
return false;
}
}
@@ -70,7 +73,9 @@ public static boolean approach2(String s, String t) {
* @param t the second string
* @return true if the strings are anagrams, false otherwise
*/
- public static boolean approach3(String s, String t) {
+ public static boolean areAnagramsByCountingCharsSingleArray(String s, String t) {
+ s = s.toLowerCase().replaceAll("[^a-z]", "");
+ t = t.toLowerCase().replaceAll("[^a-z]", "");
if (s.length() != t.length()) {
return false;
}
@@ -96,7 +101,9 @@ public static boolean approach3(String s, String t) {
* @param t the second string
* @return true if the strings are anagrams, false otherwise
*/
- public static boolean approach4(String s, String t) {
+ public static boolean areAnagramsUsingHashMap(String s, String t) {
+ s = s.toLowerCase().replaceAll("[^a-z]", "");
+ t = t.toLowerCase().replaceAll("[^a-z]", "");
if (s.length() != t.length()) {
return false;
}
@@ -123,7 +130,9 @@ public static boolean approach4(String s, String t) {
* @param t the second string
* @return true if the strings are anagrams, false otherwise
*/
- public static boolean approach5(String s, String t) {
+ public static boolean areAnagramsBySingleFreqArray(String s, String t) {
+ s = s.toLowerCase().replaceAll("[^a-z]", "");
+ t = t.toLowerCase().replaceAll("[^a-z]", "");
if (s.length() != t.length()) {
return false;
}
diff --git a/src/main/java/com/thealgorithms/strings/CheckAnagrams.java b/src/main/java/com/thealgorithms/strings/CheckAnagrams.java
deleted file mode 100644
index 7bf7cd9a7c66..000000000000
--- a/src/main/java/com/thealgorithms/strings/CheckAnagrams.java
+++ /dev/null
@@ -1,110 +0,0 @@
-package com.thealgorithms.strings;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Two strings are anagrams if they are made of the same letters arranged
- * differently (ignoring the case).
- */
-public final class CheckAnagrams {
- private CheckAnagrams() {
- }
- /**
- * Check if two strings are anagrams or not
- *
- * @param s1 the first string
- * @param s2 the second string
- * @return {@code true} if two string are anagrams, otherwise {@code false}
- */
- public static boolean isAnagrams(String s1, String s2) {
- int l1 = s1.length();
- int l2 = s2.length();
- s1 = s1.toLowerCase();
- s2 = s2.toLowerCase();
- Map
- * The main "trick":
- * To map each character from the first string 's1' we need to subtract an integer value of 'a' character
- * as 'dict' array starts with 'a' character.
- *
- * @param s1 the first string
- * @param s2 the second string
- * @return true if two string are anagrams, otherwise false
- */
- public static boolean isAnagramsOptimised(String s1, String s2) {
- // 26 - English alphabet length
- int[] dict = new int[26];
- for (char ch : s1.toCharArray()) {
- checkLetter(ch);
- dict[ch - 'a']++;
- }
- for (char ch : s2.toCharArray()) {
- checkLetter(ch);
- dict[ch - 'a']--;
- }
- for (int e : dict) {
- if (e != 0) {
- return false;
- }
- }
- return true;
- }
-
- private static void checkLetter(char ch) {
- int index = ch - 'a';
- if (index < 0 || index >= 26) {
- throw new IllegalArgumentException("Strings must contain only lowercase English letters!");
- }
- }
-}
diff --git a/src/test/java/com/thealgorithms/datastructures/caches/RRCacheTest.java b/src/test/java/com/thealgorithms/datastructures/caches/RRCacheTest.java
new file mode 100644
index 000000000000..100c73ea2a5b
--- /dev/null
+++ b/src/test/java/com/thealgorithms/datastructures/caches/RRCacheTest.java
@@ -0,0 +1,222 @@
+package com.thealgorithms.datastructures.caches;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+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 RRCacheTest {
+
+ private RRCache