Skip to content

Commit bf6d18b

Browse files
committed
Add FNV-1a hash algorithm implementation
- Implement FNV-1a (Fowler-Noll-Vo) non-cryptographic hash function - Support both 32-bit and 64-bit hash variants - Hash from String or byte array input - Include hex string output methods - All operations have O(n) time complexity where n is input length - Space complexity O(1) Features: - Generic implementation supporting String and byte arrays - Proper null handling with IllegalArgumentException - Helper methods: hash32Hex() and hash64Hex() for hex output - UTF-8 encoding support for Unicode characters - Full JavaDoc documentation FNV-1a properties: - Fast computation using simple XOR and multiply operations - Good hash distribution minimizing collisions - Widely used in hash tables and checksums - Not suitable for cryptographic purposes Tests: - 30+ comprehensive unit tests covering all methods - Edge cases: empty string, null input, Unicode - Consistency tests (same input -> same output) - Distribution tests (different inputs -> different outputs) - Both String and byte array input validation Time Complexity: O(n) where n is input length Space Complexity: O(1)
1 parent bb6385e commit bf6d18b

File tree

2 files changed

+356
-0
lines changed

2 files changed

+356
-0
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package com.thealgorithms.others;
2+
3+
/**
4+
* FNV-1a (Fowler-Noll-Vo) is a non-cryptographic hash function created by Glenn Fowler, Landon
5+
* Curt Noll, and Kiem-Phong Vo.
6+
*
7+
* <p>The FNV-1a variant provides better avalanche characteristics (bit changes distribute more
8+
* uniformly) compared to the original FNV-1.
9+
*
10+
* <p>Key properties:
11+
* <ul>
12+
* <li>Fast computation - simple operations (XOR and multiply)</li>
13+
* <li>Good distribution - minimizes hash collisions</li>
14+
* <li>Non-cryptographic - not suitable for security purposes</li>
15+
* <li>Widely used in hash tables, checksums, and data structures</li>
16+
* </ul>
17+
*
18+
* <p>Algorithm: hash = FNV_offset_basis for each byte in data: hash = hash XOR byte hash = hash *
19+
* FNV_prime
20+
*
21+
* <p>FNV-1a 32-bit: FNV_offset_basis = 2166136261 (0x811c9dc5) FNV_prime = 16777619 (0x01000193)
22+
*
23+
* <p>FNV-1a 64-bit: FNV_offset_basis = 14695981039346656037 (0xcbf29ce484222325) FNV_prime =
24+
* 1099511628211 (0x100000001b3)
25+
*
26+
* <p>Time Complexity: O(n) where n is the length of the input
27+
* <p>Space Complexity: O(1)
28+
*
29+
* @author dinilH
30+
* @see <a href="http://www.isthe.com/chongo/tech/comp/fnv/">FNV Hash</a>
31+
* @see <a href="https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function">FNV on Wikipedia</a>
32+
*/
33+
public final class FNV1aHash {
34+
35+
// FNV-1a 32-bit parameters
36+
private static final int FNV_32_INIT = 0x811c9dc5;
37+
private static final int FNV_32_PRIME = 0x01000193;
38+
39+
// FNV-1a 64-bit parameters
40+
private static final long FNV_64_INIT = 0xcbf29ce484222325L;
41+
private static final long FNV_64_PRIME = 0x100000001b3L;
42+
43+
private FNV1aHash() {
44+
// Utility class - prevent instantiation
45+
}
46+
47+
/**
48+
* Computes the 32-bit FNV-1a hash of the input string.
49+
*
50+
* @param data the input string to hash
51+
* @return the 32-bit hash value as an integer
52+
* @throws IllegalArgumentException if data is null
53+
*/
54+
public static int hash32(String data) {
55+
if (data == null) {
56+
throw new IllegalArgumentException("Input string cannot be null");
57+
}
58+
return hash32(data.getBytes(java.nio.charset.StandardCharsets.UTF_8));
59+
}
60+
61+
/**
62+
* Computes the 32-bit FNV-1a hash of the input byte array.
63+
*
64+
* @param data the input byte array to hash
65+
* @return the 32-bit hash value as an integer
66+
* @throws IllegalArgumentException if data is null
67+
*/
68+
public static int hash32(byte[] data) {
69+
if (data == null) {
70+
throw new IllegalArgumentException("Input byte array cannot be null");
71+
}
72+
73+
int hash = FNV_32_INIT;
74+
75+
for (byte b : data) {
76+
hash ^= (b & 0xff); // XOR with byte (ensure unsigned)
77+
hash *= FNV_32_PRIME; // Multiply by FNV prime
78+
}
79+
80+
return hash;
81+
}
82+
83+
/**
84+
* Computes the 64-bit FNV-1a hash of the input string.
85+
*
86+
* @param data the input string to hash
87+
* @return the 64-bit hash value as a long
88+
* @throws IllegalArgumentException if data is null
89+
*/
90+
public static long hash64(String data) {
91+
if (data == null) {
92+
throw new IllegalArgumentException("Input string cannot be null");
93+
}
94+
return hash64(data.getBytes(java.nio.charset.StandardCharsets.UTF_8));
95+
}
96+
97+
/**
98+
* Computes the 64-bit FNV-1a hash of the input byte array.
99+
*
100+
* @param data the input byte array to hash
101+
* @return the 64-bit hash value as a long
102+
* @throws IllegalArgumentException if data is null
103+
*/
104+
public static long hash64(byte[] data) {
105+
if (data == null) {
106+
throw new IllegalArgumentException("Input byte array cannot be null");
107+
}
108+
109+
long hash = FNV_64_INIT;
110+
111+
for (byte b : data) {
112+
hash ^= (b & 0xff); // XOR with byte (ensure unsigned)
113+
hash *= FNV_64_PRIME; // Multiply by FNV prime
114+
}
115+
116+
return hash;
117+
}
118+
119+
/**
120+
* Computes the 32-bit FNV-1a hash and returns it as a hex string.
121+
*
122+
* @param data the input string to hash
123+
* @return the hash value as an 8-character hex string
124+
* @throws IllegalArgumentException if data is null
125+
*/
126+
public static String hash32Hex(String data) {
127+
return String.format("%08x", hash32(data));
128+
}
129+
130+
/**
131+
* Computes the 64-bit FNV-1a hash and returns it as a hex string.
132+
*
133+
* @param data the input string to hash
134+
* @return the hash value as a 16-character hex string
135+
* @throws IllegalArgumentException if data is null
136+
*/
137+
public static String hash64Hex(String data) {
138+
return String.format("%016x", hash64(data));
139+
}
140+
}
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package com.thealgorithms.others;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
class FNV1aHashTest {
8+
9+
@Test
10+
void testHash32EmptyString() {
11+
// Empty string should return the FNV offset basis
12+
assertEquals(0x811c9dc5, FNV1aHash.hash32(""));
13+
}
14+
15+
@Test
16+
void testHash32SingleCharacter() {
17+
assertEquals(0xe40c292c, FNV1aHash.hash32("a"));
18+
assertEquals(0xe70c2de5, FNV1aHash.hash32("b"));
19+
assertEquals(0xe60c2c52, FNV1aHash.hash32("c"));
20+
}
21+
22+
@Test
23+
void testHash32SimpleStrings() {
24+
assertEquals(0x4ed566da, FNV1aHash.hash32("Hello"));
25+
assertEquals(0xdcfc127b, FNV1aHash.hash32("World"));
26+
assertEquals(0xc0bb652b, FNV1aHash.hash32("Algorithms"));
27+
}
28+
29+
@Test
30+
void testHash32LongString() {
31+
assertEquals(0x048fff90, FNV1aHash.hash32("The quick brown fox jumps over the lazy dog"));
32+
}
33+
34+
@Test
35+
void testHash32Consistency() {
36+
// Same input should always produce same output
37+
String input = "test";
38+
int hash1 = FNV1aHash.hash32(input);
39+
int hash2 = FNV1aHash.hash32(input);
40+
assertEquals(hash1, hash2);
41+
}
42+
43+
@Test
44+
void testHash32Differentiation() {
45+
// Different inputs should produce different outputs
46+
int hash1 = FNV1aHash.hash32("test");
47+
int hash2 = FNV1aHash.hash32("Test");
48+
assertNotEquals(hash1, hash2);
49+
}
50+
51+
@Test
52+
void testHash32WithByteArray() {
53+
byte[] data = "Hello".getBytes(java.nio.charset.StandardCharsets.UTF_8);
54+
assertEquals(0x4ed566da, FNV1aHash.hash32(data));
55+
}
56+
57+
@Test
58+
void testHash32WithEmptyByteArray() {
59+
byte[] data = new byte[0];
60+
assertEquals(0x811c9dc5, FNV1aHash.hash32(data));
61+
}
62+
63+
@Test
64+
void testHash32NullStringThrowsException() {
65+
assertThrows(IllegalArgumentException.class, () -> FNV1aHash.hash32((String) null));
66+
}
67+
68+
@Test
69+
void testHash32NullByteArrayThrowsException() {
70+
assertThrows(IllegalArgumentException.class, () -> FNV1aHash.hash32((byte[]) null));
71+
}
72+
73+
@Test
74+
void testHash64EmptyString() {
75+
// Empty string should return the FNV offset basis
76+
assertEquals(0xcbf29ce484222325L, FNV1aHash.hash64(""));
77+
}
78+
79+
@Test
80+
void testHash64SingleCharacter() {
81+
assertEquals(0xaf63dc4c8601ec8cL, FNV1aHash.hash64("a"));
82+
assertEquals(0xaf63df4c8601f1a5L, FNV1aHash.hash64("b"));
83+
assertEquals(0xaf63de4c8601f012L, FNV1aHash.hash64("c"));
84+
}
85+
86+
@Test
87+
void testHash64SimpleStrings() {
88+
assertEquals(0x63f4e1f2c97e89ebL, FNV1aHash.hash64("Hello"));
89+
assertEquals(0x0f18f77b44424a53L, FNV1aHash.hash64("World"));
90+
assertEquals(0x289a4c6f7f076f3bL, FNV1aHash.hash64("Algorithms"));
91+
}
92+
93+
@Test
94+
void testHash64LongString() {
95+
assertEquals(0xf3f9b7f5e7e47110L, FNV1aHash.hash64("The quick brown fox jumps over the lazy dog"));
96+
}
97+
98+
@Test
99+
void testHash64Consistency() {
100+
// Same input should always produce same output
101+
String input = "test";
102+
long hash1 = FNV1aHash.hash64(input);
103+
long hash2 = FNV1aHash.hash64(input);
104+
assertEquals(hash1, hash2);
105+
}
106+
107+
@Test
108+
void testHash64Differentiation() {
109+
// Different inputs should produce different outputs
110+
long hash1 = FNV1aHash.hash64("test");
111+
long hash2 = FNV1aHash.hash64("Test");
112+
assertNotEquals(hash1, hash2);
113+
}
114+
115+
@Test
116+
void testHash64WithByteArray() {
117+
byte[] data = "Hello".getBytes(java.nio.charset.StandardCharsets.UTF_8);
118+
assertEquals(0x63f4e1f2c97e89ebL, FNV1aHash.hash64(data));
119+
}
120+
121+
@Test
122+
void testHash64WithEmptyByteArray() {
123+
byte[] data = new byte[0];
124+
assertEquals(0xcbf29ce484222325L, FNV1aHash.hash64(data));
125+
}
126+
127+
@Test
128+
void testHash64NullStringThrowsException() {
129+
assertThrows(IllegalArgumentException.class, () -> FNV1aHash.hash64((String) null));
130+
}
131+
132+
@Test
133+
void testHash64NullByteArrayThrowsException() {
134+
assertThrows(IllegalArgumentException.class, () -> FNV1aHash.hash64((byte[]) null));
135+
}
136+
137+
@Test
138+
void testHash32HexFormat() {
139+
assertEquals("4ed566da", FNV1aHash.hash32Hex("Hello"));
140+
assertEquals("dcfc127b", FNV1aHash.hash32Hex("World"));
141+
assertEquals("811c9dc5", FNV1aHash.hash32Hex("")); // Empty string
142+
}
143+
144+
@Test
145+
void testHash64HexFormat() {
146+
assertEquals("63f4e1f2c97e89eb", FNV1aHash.hash64Hex("Hello"));
147+
assertEquals("0f18f77b44424a53", FNV1aHash.hash64Hex("World"));
148+
assertEquals("cbf29ce484222325", FNV1aHash.hash64Hex("")); // Empty string
149+
}
150+
151+
@Test
152+
void testHash32HexNullThrowsException() {
153+
assertThrows(IllegalArgumentException.class, () -> FNV1aHash.hash32Hex(null));
154+
}
155+
156+
@Test
157+
void testHash64HexNullThrowsException() {
158+
assertThrows(IllegalArgumentException.class, () -> FNV1aHash.hash64Hex(null));
159+
}
160+
161+
@Test
162+
void testHash32WithUnicodeCharacters() {
163+
// Test with Unicode characters
164+
assertDoesNotThrow(() -> FNV1aHash.hash32("Hello 世界"));
165+
assertDoesNotThrow(() -> FNV1aHash.hash32("🚀 Rocket"));
166+
167+
// Different Unicode strings should have different hashes
168+
assertNotEquals(FNV1aHash.hash32("Hello"), FNV1aHash.hash32("Hello 世界"));
169+
}
170+
171+
@Test
172+
void testHash64WithUnicodeCharacters() {
173+
// Test with Unicode characters
174+
assertDoesNotThrow(() -> FNV1aHash.hash64("Hello 世界"));
175+
assertDoesNotThrow(() -> FNV1aHash.hash64("🚀 Rocket"));
176+
177+
// Different Unicode strings should have different hashes
178+
assertNotEquals(FNV1aHash.hash64("Hello"), FNV1aHash.hash64("Hello 世界"));
179+
}
180+
181+
@Test
182+
void testHash32Distribution() {
183+
// Test that similar strings produce different hashes
184+
int hash1 = FNV1aHash.hash32("algorithm");
185+
int hash2 = FNV1aHash.hash32("algorithms");
186+
int hash3 = FNV1aHash.hash32("algorithmz");
187+
188+
assertNotEquals(hash1, hash2);
189+
assertNotEquals(hash2, hash3);
190+
assertNotEquals(hash1, hash3);
191+
}
192+
193+
@Test
194+
void testHash64Distribution() {
195+
// Test that similar strings produce different hashes
196+
long hash1 = FNV1aHash.hash64("algorithm");
197+
long hash2 = FNV1aHash.hash64("algorithms");
198+
long hash3 = FNV1aHash.hash64("algorithmz");
199+
200+
assertNotEquals(hash1, hash2);
201+
assertNotEquals(hash2, hash3);
202+
assertNotEquals(hash1, hash3);
203+
}
204+
205+
@Test
206+
void testHash32WithNumericStrings() {
207+
assertNotEquals(FNV1aHash.hash32("123"), FNV1aHash.hash32("1234"));
208+
assertNotEquals(FNV1aHash.hash32("999"), FNV1aHash.hash32("1000"));
209+
}
210+
211+
@Test
212+
void testHash64WithNumericStrings() {
213+
assertNotEquals(FNV1aHash.hash64("123"), FNV1aHash.hash64("1234"));
214+
assertNotEquals(FNV1aHash.hash64("999"), FNV1aHash.hash64("1000"));
215+
}
216+
}

0 commit comments

Comments
 (0)