Hashing in data structures is a technique used to locate a data record, given its search key, quickly. It transforms the search key into a unique hash code through a hash function, which maps extensive or variable-length data into a fixed-size value. This hash code is used as an index to access data in a hash table, a data structure optimized for rapid data retrieval.

The key advantages of hashing include its efficiency in search, insert, and delete operations, often achieving average-case time complexities of O(1)O(1)O(1). This efficiency relies on a good hash function that minimizes collisions, where multiple keys produce the same hash code. Collisions are managed using techniques like chaining (storing collided keys in linked lists within the hash table) or open addressing (probing to find alternative slots).

Applications of hashing range widely, from database indexing and associative arrays to implementing caches and symbol tables in compilers. Its versatility lies in balancing memory usage (through the hash table size) with performance (via the load factor, which indicates how full the hash table is). While hashing offers rapid access to data, careful design of the hash function and collision resolution strategy is crucial to ensuring optimal performance and avoiding pitfalls like clustering (many keys hashing to the same index).

What is Hashing in Data Structure?

Hashing in data structures is a systematic approach to efficiently organizing and retrieving data using a hash function. This function takes an input, typically a key or identifier, and computes a fixed-size output called a hash code or value. This hash value is a unique index or address within a hash table, a data structure designed for rapid access.

The main advantage of hashing lies in its ability to provide average-case O(1)O(1)O(1) time complexity for operations like insertions, deletions, and lookups, assuming a well-designed hash function and effective collision resolution strategy. Collisions occur when multiple keys hash to the same index, managed through chaining (where each slot in the hash table holds a linked list of colliding keys) or open addressing (which seeks alternative slots within the table).

Hashing finds application in many fields, including databases, compilers, and caching mechanisms, where quick data retrieval is critical. Its efficiency stems from the direct computation of storage locations based on keys, making it an indispensable tool in optimizing algorithm performance across various computational tasks.

Real-Time Example

Hashing in a Password System

Imagine you have a system where users need to log in with a username and password. Storing passwords securely is crucial, so instead of storing plain text passwords, you hash them.

1. Hashing Function: You have a hashing function (such as SHA-256 or bcrypt) that takes a password and converts it into a fixed-size string of characters. This process is deterministic, meaning the same input always produces the same output.

2. Storing Passwords: When a user creates an account or updates their password, you hash their chosen password using the hashing function. For example, if a user chooses "password123", the hashing function might convert it to something like "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8".

3. Storing Hashes: Instead of storing "password123" in your database, you store the hash ("5e884898da28047151d0e56f8dc629277

3603d0d6aabbdd62a11ef721d1542d8"). This way, even if someone gains access to your database, they cannot easily determine the original passwords because the hashing process is not reversible (one-way function).

4 .Verification: When a user attempts to log in, you hash the password they provide using the same function and compare the resulting hash with the stored hash for that user. If they match, the user provided the correct password.

5. Collision Handling: Good hashing algorithms have very low collision probabilities, meaning different passwords are unlikely to hash to the same value. This ensures that different passwords produce different hash outputs.

In this example, hashing serves the purpose of securely storing and verifying passwords without storing sensitive information directly. It showcases how hashing is used in practical applications to enhance security and protect user data.

How Hashing Works

Hashing uses a hash function to convert an input (or critical) into a unique hash code, which determines the index where data associated with that key is stored in a hash table. This allows for rapid data retrieval because the hash code directly points to the location of the data.

When collisions occur (multiple keys hashing to the same index), methods like chaining (using linked lists) or open addressing (probing for alternative slots) are used to manage them. 

Hashing is widely used in applications requiring fast data access, such as databases and caches, due to its ability to achieve average-case constant-time complexity for insert, delete, and search operations when implemented efficiently with a good hash function and collision resolution strategy.

Hash Function

  • Input: The process begins with an input, often called a key. This key can be of any size and represent a piece of data, such as a string or an object.
  • Output: The hash function processes this input using a deterministic algorithm to produce a fixed-size hash code or value. The output is usually a numeric value that serves as an index or address where the data associated with the input key will be stored or retrieved.

Hash Table

  • Data Structure: Hashing typically involves using a hash table, an array-like data structure consisting of slots or buckets.
  • Indexing: The hash code produced by the hash function directly determines the index within the hash table where the data associated with the key will be stored. This direct indexing allows for rapid access to data, as the location is computed based on the key itself.

Collision Resolution

Occurrences: Since different keys may hash to the same index (collision), strategies are employed to handle collisions:

  • Chaining: Each slot in the hash table contains a linked list (or another data structure) where multiple key-value pairs that hash to the same index are stored.
  • Open Addressing: This approach involves probing within the hash table to find an alternative location for the collided key, ensuring that all keys eventually find a unique slot.

Application

  • Data Retrieval: Hashing is widely used in data retrieval applications where quick access to stored data is essential, such as databases, caches, and symbol tables in compilers.
  • Efficiency: The efficiency of hashing primarily depends on the quality of the hash function and the effectiveness of collision resolution techniques. A good hash function minimises collisions, while efficient collision resolution ensures optimal overall performance.

Hash Keys

A hash key (also known as a hash value or hash code) is the result of applying a hash function to a specific input data. This key serves as a unique identifier or representation of the original data. Here’s how hash keys are used and their characteristics:

Characteristics:

  • Uniqueness: Ideally, each unique input should produce a unique hash key. However, due to the fixed size of hash keys and the potential for an infinite number of inputs (in practical scenarios), collisions can occur where different inputs produce the same hash key.
  • Fixed Length: Hash keys have a fixed length determined by the hash function used. For example, the SHA-256 hash function always produces a hash key that is 256 bits (32 bytes) long.
  • Security: In cryptographic applications, the security of a hash function relies on the difficulty of finding two different inputs that produce the same hash key (collision resistance).

Applications of Hash Keys

1. Data Structures: Hash keys are fundamental to hash tables, which are data structures that use hash keys to index data. Hash tables allow efficient storage and retrieval of data based on keys, making them ideal for applications where quick access to data is essential.

2. Cryptography: Hash keys play a crucial role in digital signatures, password storage, and data integrity checks. For example, passwords are often stored as hash keys (not in plain text) to enhance security. When a user logs in, their entered password is hashed and compared to the stored hash key rather than the original password.

3. Database Indexing: In databases, hash keys can be used to index records based on unique identifiers (like primary keys), enabling fast lookup operations.

Example

Suppose you have a simple hash function that adds up the ASCII values of the characters in a string and takes the remainder when divided by a certain prime number. For the string "hello":

  • ASCII values: 'h' = 104, 'e' = 101, 'l' = 108, 'o' = 111
  • Sum = 104 + 101 + 108 + 108 + 111 = 532
  • If we use a prime number 11 for example, approach

Hash Function

A hash function is a fundamental component in computer science and cryptography that takes an input (or 'message') and produces a fixed-size string of bytes, typically a hash value or hash code. Here are key characteristics and functions of hash functions:

Purpose

  • Data Integrity: Hash functions verify data integrity by generating a unique hash value for each unique input. Even a minor change in the input data should produce a significantly different hash value.
  • Efficient Data Retrieval: In data structures like hash tables, hash functions enable rapid retrieval by mapping keys to specific indices or addresses.

Properties

  • Deterministic: A hash function always produces the same output for the same input, ensuring predictability and consistency.
  • Fixed Output Size: Hash functions generate fixed-length hash values regardless of the input data size.
  • Avalanche Effect: A slight change in the input should result in a significantly different hash value, making it difficult to predict or manipulate the output.
  • Uniform Distribution: Ideally, hash functions distribute hash values uniformly across the entire range of possible outputs, minimising the likelihood of collisions (where different inputs produce the same hash value).

Common Hash Functions

  • MD5 (Message Digest Algorithm 5): Despite its widespread use in the past, MD5 is now considered weak due to vulnerabilities that allow for collisions.
  • SHA-1 (Secure Hash Algorithm 1): Also susceptible to collision attacks and is being deprecated in favour of more substantial alternatives.
  • SHA-256, SHA-384, SHA-512: Part of the SHA-2 family, these algorithms offer more robust security with hash values of different lengths (256, 384, and 512 bits, respectively).
  • SHA-3 (Keccak): The latest standard in hashing, offering enhanced security features and flexibility in hash sizes.

Applications

  • Data Integrity: Hash functions ensure data integrity during transmission or storage. One can verify if the data has been altered by comparing hash values before and after transmission.
  • Password Storage: Hash functions are used to store passwords in databases securely. They ensure passwords are not easily retrieved in plaintext, even if the database is compromised.
  • Digital Signatures: Hash functions combined with asymmetric cryptography are used to create and verify digital signatures, ensuring the authenticity and integrity of messages or documents.

Considerations

  • Collision Resistance: A good hash function should minimise the chances of two different inputs producing the same hash value (collision).
  • Speed: Hash functions should be computationally efficient to calculate, primarily when used in applications requiring rapid data processing.
  • Security: Regular updates and transitions to more vital hash functions are necessary as computing power increases and new vulnerabilities are discovered.

Hash functions play a critical role in computing, from ensuring data integrity and security to facilitating efficient data retrieval in data structures. Choosing the proper hash function depends on specific security, speed, and compatibility requirements with existing systems.

Types of Hash Function

Hash functions are fundamental components of hash tables, used extensively in computer science for efficient data storage and retrieval. They transform input data (keys) into indices within a fixed-size array (the hash table), enabling quick access to stored information based on its unique key. Various types of hash functions exist, each with distinct methods of generating hash values from keys. 

These methods include the division method, mid-square method, folding method, and multiplication method, each suited to different data distributions and computational requirements. Choosing the right hash function is critical for optimizing performance and minimizing collisions in hash table operations.

Division Method

The division method for hashing involves computing the hash value by taking the remainder of the key divided by a prime number or a suitable table size. This method is simple to implement and generally efficient for keys that are uniformly distributed.

However, its effectiveness can be reduced if the table size is not a prime number or if the keys have a common divisor with the table size, leading to clustering and increased collisions. Despite its simplicity, it may require careful selection of the divisor to achieve optimal performance in practice.

Mid Square Method

The mid square method squares the key and then extracts a portion of the middle digits to derive the hash value. It's a straightforward approach but can suffer from uneven distribution if the initial squaring leads to a biased selection of digits.

This method is less commonly used due to its reliance on the key's specific numeric properties and the potential for clustering when the middle digits don't adequately represent the original key's distribution. It's typically used in applications where key values are well-behaved numerically and when simpler methods like division or multiplication aren't suitable.

Folding Method

The folding method splits the key into smaller parts (often of equal size), adds these parts together, and then optionally takes the modulus with the table size to determine the hash value. This method aims to distribute the bits of the key across the hash table more evenly, reducing the likelihood of collisions.

It's useful when keys are larger than the table size or when dealing with variable-length keys. The folding method requires careful consideration of how the key is split and added together to ensure that the resulting hash values are uniformly distributed across the table.

Multiplication Method

The multiplication method involves multiplying the key by a constant (typically a fraction of a power of 2) and extracting the fractional part of the product to determine the hash value. This method leverages the properties of multiplication to distribute keys more uniformly across the hash table, reducing collisions. The choice of the multiplication constant is crucial; it should ideally be a number that efficiently distributes the hash values across the table size.

The multiplication method is widely used due to its balance between simplicity and effectiveness in handling a wide range of key distributions, making it suitable for many practical hashing applications.

Need for Hash Data Structure

The hash data structure, represented primarily through hash tables, is indispensable in computer science for its efficiency in data storage and retrieval. By employing a hash function to compute unique indices for each key, hash tables enable constant-time average-case complexity O(1)O(1)O(1) for insertion, deletion, and lookup operations.

This efficiency is pivotal in applications requiring rapid data access, such as databases, compilers, and caching systems. Hash tables also offer flexibility in storing key-value pairs and efficiently handle collisions through techniques like chaining or open addressing, ensuring optimal performance even with large datasets.

Their memory efficiency and ability to dynamically resize make hash tables suitable for dynamic data sets and diverse applications in algorithm design, data structures, and software engineering. Overall, hash tables simplify complex problems by providing a straightforward and robust mechanism for associative storage and retrieval of data, essential for modern computational tasks.

A Hash Table In Data Structure

A hash table is a data structure for efficient data retrieval based on keys. It utilizes a hash function to compute a unique index (hash code) for each key, mapping it directly to a specific slot in an array. This direct addressing allows for rapid insertion, deletion, and lookup operations with an average time complexity of O(1)O(1)O(1), assuming collisions are managed effectively.

Collisions occur when different keys produce the same hash code, and hash tables employ various strategies. Chaining resolves collisions by storing multiple elements that hash to the same index in a linked list associated with that slot. Alternatively, open addressing attempts to find alternative slots within the table to place collided elements, often through sequential probing.

Hash tables find wide applications in scenarios requiring fast access to data, such as databases for indexing, caches for quick retrieval of frequently accessed items, and symbol tables in compilers to manage identifiers efficiently. Their efficiency stems from the direct computation of storage locations based on keys, making them indispensable for optimising data management in diverse computational tasks.

Example Implementation in C++:

1#include <iostream>
2#include <list>
3using namespace std;
4
5class HashTable {
6private:
7    static const int hashGroups = 10;
8    list<pair<int, int>> table[hashGroups]; // Array of lists to store key-value pairs
9    
10    // Hash function to determine hash group
11    int hashFunction(int key) {
12        return key % hashGroups;
13    }
14
15public:
16    // Insert function
17    bool insertItem(int key, int value) {
18        int hashGroup = hashFunction(key); // Determine the hash group
19        
20        // Check if the key already exists in the hash table
21        for (auto& kv : table[hashGroup]) {
22            if (kv.first == key) { // If key found, update the value
23                kv.second = value;
24                return true;
25            }
26        }
27        
28        // If key does not exist, insert new key-value pair
29        table[hashGroup].emplace_back(key, value);
30        return true;
31    }
32    
33    // Search function
34    int searchItem(int key) {
35        int hashGroup = hashFunction(key); // Determine the hash group
36        
37        // Search for the key in the corresponding list
38        for (auto& kv : table[hashGroup]) {
39            if (kv.first == key) {
40                return kv.second; // Return the value if key found
41            }
42        }
43        
44        return -1; // Return -1 if key not found
45    }
46    
47    // Delete function
48    void deleteItem(int key) {
49        int hashGroup = hashFunction(key); // Determine the hash group
50        
51        // Iterate through the list at the hash group
52        for (auto it = table[hashGroup].begin(); it != table[hashGroup].end(); ++it) {
53            if (it->first == key) { // If key found, erase it from the list
54                table[hashGroup].erase(it);
55                break;
56            }
57        }
58    }
59    
60    // Display function to show the hash table
61    void displayTable() {
62        for (int i = 0; i < hashGroups; i++) {
63            cout << i;
64            for (auto& kv : table[i]) {
65                cout << " --> (" << kv.first << ", " << kv.second << ")";
66            }
67            cout << endl;
68        }
69    }
70};
71
72// Main function to test the hash table
73int main() {
74    HashTable ht;
75    
76    // Insert some key-value pairs
77    ht.insertItem(1, 10);
78    ht.insertItem(2, 20);
79    ht.insertItem(3, 30);
80    ht.insertItem(4, 40);
81    ht.insertItem(5, 50);
82    
83    // Display the hash table
84    cout << "Hash Table:" << endl;
85    ht.displayTable();
86    
87    // Search for a key
88    int keyToSearch = 3;
89    cout << "\nSearching for key " << keyToSearch << ": ";
90    int result = ht.searchItem(keyToSearch);
91    if (result != -1) {
92        cout << "Found! Value = " << result << endl;
93    } else {
94        cout << "Not Found!" << endl;
95    }
96    
97    // Delete a key
98    int keyToDelete = 4;
99    cout << "\nDeleting key " << keyToDelete << " from hash table." << endl;
100    ht.deleteItem(keyToDelete);
101    
102    // Display the hash table after deletion
103    cout << "\nHash Table after deletion:" << endl;
104    ht.displayTable();
105    
106    return 0;
107}


  • HashTable Class: Defines a hash table with hashGroups number of buckets using an array of lists (std::list<pair<int, int>>). Each list stores key-value pairs where the key is an integer and the value is also an integer.
  • Hash Function (hashFunction): Computes the hash group (bucket index) for a given key using modulo operation (key % hashGroups).
  • Insert (insertItem): Inserts a key-value pair into the hash table. If the key already exists, it updates the value.
  • Search (searchItem): Searches for a key in the hash table and returns its corresponding value if found, otherwise returns -1.
  • Delete (deleteItem): Deletes a key-value pair from the hash table based on the key.
  • Display (displayTable): Prints the contents of the hash table, showing each bucket and its key-value pairs.
  • Main Function: Demonstrates basic operations on the hash table such as insertion, search, and deletion. It inserts key-value pairs, searches for a specific key, deletes a key-value pair, and displays the hash table before and after deletion.

Inserting Data into a Hash Table

Inserting data into a hash table involves using a hash function to determine where in the table the data should be placed based on its key. Here’s how you can implement the insertion process step-by-step in C++:

#include <iostream>
#include <list>
using namespace std;

// Define a class for Hash Table
class HashTable {
private:
    static const int hashGroups = 10; // Number of hash groups (buckets)
    list<pair<int, int>> table[hashGroups]; // Array of lists to store key-value pairs
    
    // Hash function to determine hash group
    int hashFunction(int key) {
        return key % hashGroups;
    }

public:
    // Function to insert key-value pair into the hash table
    void insertItem(int key, int value) {
        int hashGroup = hashFunction(key); // Determine the hash group
        
        // Check if the key already exists in the hash table
        for (auto& kv : table[hashGroup]) {
            if (kv.first == key) { // If key found, update the value
                kv.second = value;
                cout << "Key " << key << " updated with value " << value << endl;
                return;
            }
        }
        
        // If key does not exist, insert new key-value pair
        table[hashGroup].emplace_back(key, value);
        cout << "Key " << key << " inserted with value " << value << endl;
    }
};

// Main function to test the hash table
int main() {
    HashTable ht;
    
    // Insert some key-value pairs
    ht.insertItem(1, 10);
    ht.insertItem(2, 20);
    ht.insertItem(3, 30);
    ht.insertItem(4, 40);
    ht.insertItem(5, 50);
    
    return 0;
}

Inserting data into a hash table involves using a hash function to determine where in the table the data should be placed based on its key. Here’s how you can implement the insertion process step-by-step in C++:

Implementation of Inserting Data into a Hash Table

#include <iostream>
#include <list>
using namespace std;

// Define a class for Hash Table
class HashTable {
private:
    static const int hashGroups = 10; // Number of hash groups (buckets)
    list<pair<int, int>> table[hashGroups]; // Array of lists to store key-value pairs
    
    // Hash function to determine hash group
    int hashFunction(int key) {
        return key % hashGroups;
    }

public:
    // Function to insert key-value pair into the hash table
    void insertItem(int key, int value) {
        int hashGroup = hashFunction(key); // Determine the hash group
        
        // Check if the key already exists in the hash table
        for (auto& kv : table[hashGroup]) {
            if (kv.first == key) { // If key found, update the value
                kv.second = value;
                cout << "Key " << key << " updated with value " << value << endl;
                return;
            }
        }
        
        // If key does not exist, insert new key-value pair
        table[hashGroup].emplace_back(key, value);
        cout << "Key " << key << " inserted with value " << value << endl;
    }
};

// Main function to test the hash table
int main() {
    HashTable ht;
    
    // Insert some key-value pairs
    ht.insertItem(1, 10);
    ht.insertItem(2, 20);
    ht.insertItem(3, 30);
    ht.insertItem(4, 40);
    ht.insertItem(5, 50);
    
    return 0;
}

Explanation:

1. Hash Table Class (HashTable):

  • hashGroups: Number of hash groups (or buckets) in the hash table.
  • table[hashGroups]: Array of lists (using std::list) to store key-value pairs.

2. Hash Function (hashFunction):

  • Simple modulo operation to determine the hash group based on the key.

3. Insertion Function (insertItem):

  • Calculates the hash group using hashFunction.
  • Searches through the linked list at table[hashGroup] to find if the key already exists.
  • If the key exists, updates the corresponding value; otherwise, inserts a new key-value pair.
  • Outputs a message indicating whether the key was inserted or updated.

4. Main Function:

  • Creates an instance of HashTable (ht).
  • Inserts several key-value pairs using insertItem.

Example Output:

When you run the above code, it will output the following messages indicating the insertion or update of key-value pairs:

Key 1 inserted with value 10
Key 2 inserted with value 20
Key 3 inserted with value 30
Key 4 inserted with value 40
Key 5 inserted with value 50

This demonstrates the basic process of inserting data into a hash table in C++. Adjustments can be made based on specific requirements, such as handling collisions or incorporating resizing mechanisms to optimize performance as the table grows.

Implementation of Hashing in C++

Hashing in C++ involves using a hash function to map keys to indices in an array, allowing efficient storage and retrieval of data. Here's a basic implementation of hashing in C++ using a hash table with chaining to handle collisions:

Implementation of Hashing in C++

#include <iostream>
#include <list>
using namespace std;

// Define a class for Hash Table
class HashTable {
private:
    static const int hashGroups = 10; // Number of hash groups (buckets)
    list<pair<int, int>> table[hashGroups]; // Array of lists to store key-value pairs
    
    // Hash function to determine hash group
    int hashFunction(int key) {
        return key % hashGroups;
    }

public:
    // Function to insert key-value pair into the hash table
    void insertItem(int key, int value) {
        int hashGroup = hashFunction(key); // Determine the hash group
        
        // Check if the key already exists in the hash table
        for (auto& kv : table[hashGroup]) {
            if (kv.first == key) { // If key found, update the value
                kv.second = value;
                cout << "Key " << key << " updated with value " << value << endl;
                return;
            }
        }
        
        // If key does not exist, insert new key-value pair
        table[hashGroup].emplace_back(key, value);
        cout << "Key " << key << " inserted with value " << value << endl;
    }
    
    // Function to search for a key in the hash table
    int searchItem(int key) {
        int hashGroup = hashFunction(key); // Determine the hash group
        
        // Search for the key in the corresponding list
        for (auto& kv : table[hashGroup]) {
            if (kv.first == key) {
                return kv.second; // Return the value if key found
            }
        }
        
        return -1; // Return -1 if key not found
    }
    
    // Function to delete a key from the hash table
    void deleteItem(int key) {
        int hashGroup = hashFunction(key); // Determine the hash group
        
        // Iterate through the list at the hash group
        for (auto it = table[hashGroup].begin(); it != table[hashGroup].end(); ++it) {
            if (it->first == key) { // If key found, erase it from the list
                table[hashGroup].erase(it);
                cout << "Key " << key << " deleted from hash table" << endl;
                return;
            }
        }
        
        cout << "Key " << key << " not found in hash table" << endl;
    }
    
    // Function to display the hash table
    void displayTable() {
        for (int i = 0; i < hashGroups; i++) {
            cout << i;
            for (auto& kv : table[i]) {
                cout << " --> (" << kv.first << ", " << kv.second << ")";
            }
            cout << endl;
        }
    }
};

// Main function to test the hash table
int main() {
    HashTable ht;
    
    // Insert some key-value pairs
    ht.insertItem(1, 10);
    ht.insertItem(2, 20);
    ht.insertItem(3, 30);
    ht.insertItem(4, 40);
    ht.insertItem(5, 50);
    
    // Display the hash table
    cout << "Hash Table:" << endl;
    ht.displayTable();
    
    // Search for a key
    int keyToSearch = 3;
    cout << "\nSearching for key " << keyToSearch << ": ";
    int result = ht.searchItem(keyToSearch);
    if (result != -1) {
        cout << "Found! Value = " << result << endl;
    } else {
        cout << "Not Found!" << endl;
    }
    
    // Delete a key
    int keyToDelete = 4;
    cout << "\nDeleting key " << keyToDelete << " from hash table." << endl;
    ht.deleteItem(keyToDelete);
    
    // Display the hash table after deletion
    cout << "\nHash Table after deletion:" << endl;
    ht.displayTable();
    
    return 0;
}

When you run the above code, it will output the following:

Key 1 inserted with value 10
Key 2 inserted with value 20
Key 3 inserted with value 30
Key 4 inserted with value 40
Key 5 inserted with value 50
Hash Table:
0
1 --> (1, 10)
2 --> (2, 20)
3 --> (3, 30)
4 --> (4, 40)
5 --> (5, 50)
6
7
8
9

Searching for key 3: Found! Value = 30

Deleting key 4 from hash table.
Key 4 deleted from hash table

Hash Table after deletion:
0
1 --> (1, 10)
2 --> (2, 20)
3 --> (3, 30)
5 --> (5, 50)

This implementation demonstrates the basic functionalities of a hash table in C++, including insertion, searching, and deletion operations. Adjustments can be made to handle more complex scenarios, such as resizing the hash table dynamically or implementing different collision resolution techniques like open addressing or double hashing.

Conceptual Hash Table

Assume we have a hash table represented as an array with 10 slots (0 to 9):

hash table=[_,_,_,_,_,_,_,_,_,_]\text{hash table} = [ \_, \_, \_, \_, \_, \_, \_, \_, \_, \_ ]hash table=[_,_,_,_,_,_,_,_,_,_]

Each slot in the array can hold either a single key-value pair or a linked list of key-value pairs (in case of collisions).

Hash Function

Let's consider a simple hash function that calculates the index based on the sum of ASCII values of characters in the key and takes modulo 10 to fit within our array size (assuming keys are strings and values are integers):

hash_function(key)=(sum of ASCII values of key)mod  10\text{hash\_function}(key) = (\text{sum of ASCII values of key}) \mod 10hash_function(key)=(sum of ASCII values of key)mod10

Insert Operation

1. Inserting ("John", 42):

Compute hash code for key "John":

  • ASCII values: 'J' = 74, 'o' = 111, 'h' = 104, 'n' = 110
  • Sum: 74 + 111 + 104 + 110 = 399
  • Hash code: 399 % 10 = 9 (index 9)

Place the key-value pair in the hash table at index 9: hash table=[_,_,_,_,_,_,_,_,_,("John", 42)]\text{hash table} = [ \_, \_, \_, \_, \_, \_, \_, \_, \_, \text{("John", 42)} ]hash table=[_,_,_,_,_,_,_,_,_,("John", 42)]

Collision Handling with Chaining

1. Inserting ("Jane", 55):

Compute hash code for key "Jane":

  • ASCII values: 'J' = 74, 'a' = 97, 'n' = 110, 'e' = 101
  • Sum: 74 + 97 + 110 + 101 = 382
  • Hash code: 382 % 10 = 2 (index 2)

Collision occurs at index 2, so we use chaining: hash table=[_,_,("Jane", 55),_,_,_,_,_,_,("John", 42)]\text{hash table} = [ \_, \_, (\text{"Jane", 55}), \_, \_, \_, \_, \_, \_, (\text{"John", 42)} ]hash table=[_,_,("Jane", 55),_,_,_,_,_,_,("John", 42)]
Here, index 2 contains a linked list with ("Jane", 55) added to it.

Search Operation

1. Searching for "Jane":

Compute hash code for key "Jane":

  • Hash code: 382 % 10 = 2 (index 2)
  • Traverse the linked list at index 2 to find "Jane"
  • Found: ("Jane", 55)

Delete Operation

Deleting "John":

Compute hash code for key "John":

  • Hash code: 399 % 10 = 9 (index 9)

Remove ("John", 42) from index 9: hash table=[_,_,("Jane", 55),_,_,_,_,_,_,_]\text{hash table} = [ \_, \_, (\text{"Jane", 55}), \_, \_, \_, \_, \_, \_, \_ ]hash table=[_,_,("Jane," 55),_,_,_,_,_,_,_]
Now, the hash table contains only ("Jane",55) at index two after deleting ("John," 42).

How Does Hashing Work In Data Structure

Hashing works in data structures by using a hash function to map data (keys) to indices in a data structure (usually an array) where values are stored. This allows for efficient insertion, deletion, and retrieval operations based on keys. Here's how it typically works in Python, Java, and C++ with examples:

Python Example

Python provides built-in support for hash tables through dictionaries. Here's how you can use hashing in Python:

Python

# Creating a hash table (dictionary)
hash_table = {}

# Inserting key-value pairs
hash_table['apple'] = 10
hash_table['banana'] = 20
hash_table['cherry'] = 30

# Retrieving values based on keys
print(hash_table['banana'])  # Output: 20

# Deleting a key-value pair
del hash_table['cherry']


In Python, dictionaries internally use hashing to map keys to indices, allowing for average O(1) time complexity for insertions, deletions, and lookups.

Java Example

Java also provides a built-in hash table implementation through the HashMap class. Here's how you can use hashing in Java:

Java

import java.util.HashMap;

public class HashingExample {
    public static void main(String[] args) {
        // Creating a hash table (HashMap)
        HashMap<String, Integer> hashTable = new HashMap<>();

        // Inserting key-value pairs
        hashTable.put("apple", 10);
        hashTable.put("banana", 20);
        hashTable.put("cherry", 30);

        // Retrieving values based on keys
        System.out.println(hashTable.get("banana"));  // Output: 20

        // Deleting a key-value pair
        hashTable.remove("cherry");
    }
}

In Java, HashMap uses hashing to store and retrieve key-value pairs efficiently, providing average O(1) time complexity for these operations.

C++ Example

C++ does not have a built-in hash table data structure in the standard library, but you can use the unordered_map from the <unordered_map> header, which is implemented using hashing. Here's how you can use hashing in C++:

Cpp

#include <iostream>
#include <unordered_map>

int main() {
    // Creating a hash table (unordered_map)
    std::unordered_map<std::string, int> hashTable;

    // Inserting key-value pairs
    hashTable["apple"] = 10;
    hashTable["banana"] = 20;
    hashTable["cherry"] = 30;

    // Retrieving values based on keys
    std::cout << hashTable["banana"] << std::endl;  // Output: 20

    // Deleting a key-value pair
    hashTable.erase("cherry");

    return 0;
}

In C++, std::unordered_map uses hashing to store and retrieve elements efficiently, providing average constant-time complexity for insertions, deletions, and lookups.

Hash collision

A hash collision occurs when two inputs (data or keys) produce the same hash value from a hash function. In other words, when two pieces of data or keys are hashed, but the resulting hash values are identical, a collision occurs.

Causes of Hash Collisions

  • Finite Output Range: Hash functions typically have a fixed output size. For example, a standard hash function like SHA-256 produces a 256-bit hash value. With a finite number of possible hash values, it's theoretically inevitable that collisions can occur, especially as the number of possible inputs (data or keys) increases.
  • Birthday Paradox: This paradox states that in a set of randomly chosen items, the probability of at least two items having the same hash value increases as the number of items (or hash outputs) grows. Even with a well-designed hash function, the birthday paradox implies that collisions become more likely as the number of hashed items increases.

Impact of Hash Collisions

  • Data Integrity: In applications where hash functions are used to ensure data integrity (e.g., cryptographic applications, checksums), collisions can lead to the incorrect validation of data.
  • Data Structures: In hash tables or hash maps, collisions affect performance. Suppose many keys produce the same hash value (a phenomenon known as clustering). In that case, lookup times can increase significantly as the hash table must handle these collisions (often through techniques like chaining or open addressing).

Managing Hash Collisions

  • Collision Resolution: Techniques like chaining (using linked lists or other data structures to store multiple entries with the same hash value) or open addressing (probing the table for an alternative location to store the collided item) are used to manage collisions in hash tables.
  • Hash Function Design: Designing robust hash functions with good distribution properties and minimizing collisions is critical. Techniques include ensuring an even distribution of hash values across the entire hash space and avoiding predictable patterns in the input data that could lead to clustering.

In summary, a hash collision occurs when different inputs produce the same hash value from a hash function, which can affect data integrity and performance in various applications that rely on hash functions.

Collision Resolution Technique

Collision resolution in hash tables is a crucial aspect of managing data efficiently when multiple keys hash to the same slot. One common technique is chaining, where each slot in the hash table can store a linked list (or another data structure) of elements that hash to the same value. When a collision occurs, meaning two keys hash to the same slot, the new key is simply added to the existing linked list at that slot.

This method is straightforward and handles collisions effectively by allowing multiple entries to coexist in the same slot without needing to search for an alternative location within the table. On the other hand, linear probing is an example of open addressing, where collisions are resolved by placing the collided item in the next available slot in the hash table.

If another collision occurs, subsequent slots are sequentially probed until an empty slot is found. This approach aims to keep the hash table compact and contiguous, potentially improving cache performance by minimising data spread across the memory. However, it requires careful handling of clustering (multiple keys hashing to consecutive slots), which can impact lookup efficiency.

Example

Concise example demonstrating collision resolution using chaining and linear probing in a hash table with five slots:

Chaining Example:

Hash function (mod 5):

  • Key "John" -> Hash value: 3
  • Critical "Jane" -> Hash value: 1 (collision with "David")
  • Key "David" -> Hash value: 1 (collides with "Jane")
  • Critical "Alice" -> Hash value: 0

Hash Table with Chaining:

  • Initial State:
Mathematica
Copy code
Slot 0: 
Slot 1: 
Slot 2: 
Slot 3: 
Slot 4:

  • After Insertions:
Mathematica
Copy code
Slot 0: ["Alice"] 
Slot 1: ["Jane", "David"] 
Slot 2: 
Slot 3: ["John"]
Slot 4:


Linear Probing Example:

  • Initial State:
Slot 0: 
Slot 1: 
Slot 2: 
Slot 3: 
Slot 4:

  • After Insertions:
Slot 0: "Alice" 
Slot 1: "Jane" 
Slot 2: "David" 
Slot 3: "John" 
Slot 4:

  • Chaining: Collisions are resolved by creating linked lists in the hash table slots. Each slot can hold multiple elements, with collisions handled by appending to the linked list.
  • Linear Probing: Collisions are resolved by placing collided items in the next available slot. This probing continues until an empty slot is found.

These examples illustrate how different collision resolution techniques handle collisions within a hash table, ensuring efficient data storage and retrieval.

Hashing algorithms

Hashing algorithms are essential cryptographic tools that transform input data into fixed-size hash values. These algorithms are deterministic, meaning the same input always produces the same output, and they operate with a fixed output size, regardless of input size. One critical property of hashing algorithms is collision resistance, which should be highly improbable for two different inputs to produce the same hash value. Common hashing algorithms include MD5, SHA-1, SHA-256, SHA-3, and BLAKE2, each offering different levels of security and performance characteristics. 

These algorithms find applications in data integrity verification, password storage, digital signatures, and blockchain technology. Security considerations when choosing a hashing algorithm include resistance to cryptographic attacks such as collisions and pre-image attacks, alongside computational efficiency for handling large datasets or real-time processing needs.

Thus, selecting the appropriate hashing algorithm depends on specific security requirements and performance constraints of the application at hand. Hashing algorithms are fundamental components in computer science and cryptography used to transform data (often of arbitrary size) into a fixed-size hash value (hash code or digest) representing the original data. Here's an overview of hashing algorithms:

Characteristics of Hashing Algorithms

  • Deterministic: A hashing algorithm always produces the same hash value for the same input data.
  • Fixed Output Size: Hash functions produce hash values of a fixed size, regardless of the input size.
  • Fast Computation: Hash functions are designed to be computationally efficient, allowing them to generate hash values even for significant inputs quickly.
  • Pre-image Resistance: It should be computationally infeasible to reverse the hash value to obtain the original input data.
  • Collision Resistance: Two different inputs should unlikely produce the same hash value (hash collision).

Common Hashing Algorithms

1. MD5 (Message Digest Algorithm 5)

  • Produces a 128-bit (16-byte) hash value.
  • Widely used historically but is now considered weak due to vulnerabilities and collision attacks.

2. SHA-1 (Secure Hash Algorithm 1)

  • Produces a 160-bit (20-byte) hash value.
  • Also deprecated in many applications due to vulnerabilities and collision attacks.

3. SHA-256 (Secure Hash Algorithm 256-bit)

  • Part of the SHA-2 family produces a 256-bit (32-byte) hash value.
  • Widely used for digital signatures, certificate generation, and blockchain technologies.

4. SHA-3 (Secure Hash Algorithm 3)

  • Provides a set of hash functions (SHA3-224, SHA3-256, SHA3-384, SHA3-512).
  • Designed to provide better security and resistance to attacks compared to SHA-2.

5. BLAKE2 

  •  A high-speed cryptographic hash function based on ChaCha stream cypher.
  • Provides faster performance than traditional hashing algorithms like MD5 and SHA-1.

Components of Hashing Data Structure

The hashing data structure comprises several essential components that work together to enable efficient storage and retrieval of data:

1. Hash Function:

  • Purpose: A hash function takes an input (typically a key) and computes a deterministic output (hash value or hash code) of fixed size.
  • Functionality: The hash function maps data of arbitrary size to data of fixed size, usually integers. This output determines the index or location in the hash table where the data will be stored or retrieved.
  • Properties: A good hash function strives to distribute keys uniformly across the hash table to minimize collisions and ensure efficient access. It should produce hash values that are as unique as possible for each distinct input.

2. Hash Table:

  • Structure: The hash table is an array of fixed size, where each element (bucket) can store one or more key-value pairs.
  • Purpose: It provides the main storage mechanism for the data and uses the hash value generated by the hash function to determine the storage location within the array.

3. Operations:

  • Insert: Places a key-value pair into the hash table by computing its hash value and determining the appropriate bucket.
  • Search: Retrieves a value associated with a key by calculating its hash value and accessing the corresponding bucket. If collisions occur (i.e., multiple keys map to the same index), additional steps may be required to locate the correct key-value pair.
  • Delete: Removes a key-value pair from the hash table based on its key, typically by first locating the appropriate bucket using the hash value and then deleting the pair from the bucket.

4. Collision Resolution Techniques:

  • Chaining: This technique involves each bucket containing a linked list (or other data structure) of key-value pairs that hash to the same index. It allows multiple items with the same hash value to be stored and retrieved efficiently.
  • Open Addressing: In contrast to chaining, open addressing attempts to find alternative locations within the hash table itself when collisions occur. Techniques such as linear probing, quadratic probing, or double hashing are used to locate the next available slot in case of collisions.

5. Load Factor and Rehashing:

  • Load Factor: The load factor of a hash table is the ratio of the number of stored elements to the size of the table. A higher load factor increases the likelihood of collisions.
  • Rehashing: When the load factor exceeds a certain threshold, the hash table may be resized (rehashed) to maintain efficiency. This involves creating a new, larger array and rehashing all existing elements into the new array.

6. Performance Considerations:

  • Time Complexity: On average, hash table operations (insertion, deletion, search) have a constant-time complexity O(1)O(1)O(1), assuming a well-distributed hash function and efficient collision resolution.
  • Space Complexity: The space complexity depends on the number of key-value pairs stored and the implementation details of the hash table and collision resolution methods.

In conclusion, the hashing data structure combines the deterministic mapping provided by the hash function with the organized storage and efficient retrieval capabilities of the hash table. These components are essential in various applications where rapid access and manipulation of data are crucial, such as in databases, compilers, and caching mechanisms.

Applications of Hashing

Hashing has diverse applications across various fields in computer science and information security due to its ability to transform data and provide unique input representations efficiently. Here are some critical applications of hashing:

1. Data Integrity Verification

Hashing is widely used to ensure data integrity during transmission or storage. By computing a hash value (or checksum) of a file or message before and after transmission, recipients can verify if the data has been altered or corrupted. If the hash values match, the data is likely intact.

2. Password Storage

Storing passwords securely is crucial to prevent unauthorised access. Instead of storing passwords directly, systems hash passwords using cryptographic hash functions (like bcrypt, Argon2, or SHA-256) and store the hash values. During authentication, the entered password's hash is compared with the stored hash, ensuring the password remains hidden and secure.

3. Digital Signatures

Hash functions play a critical role in digital signatures, a method used to verify the authenticity and integrity of digital messages or documents. The sender hashes the message and encrypts the hash with their private key, creating a digital signature. The recipient can verify the message's integrity by decrypting the signature with the sender's public key and comparing it with the hash of the received message.

4. Data Structures

Hashing is integral to efficient data structures like hash tables and hash maps. These structures use hash functions to compute indices where data is stored or retrieved quickly. This allows for fast insertion, deletion, and lookup operations, making hash tables suitable for applications requiring rapid data access.

What is the Hash Function in DAA?

A hash function in DAA is a mathematical function that takes an input (typically a key or data item) and computes a deterministic output, known as a hash code or hash value. The key feature of a hash function is its ability to map data of arbitrary size to data of fixed size, usually integers. This hash value serves as an index or address within a hash table, enabling rapid access to stored data.

Characteristics

  • Deterministic: For a given input, a hash function always produces the same hash value.
  • Uniformity: A good hash function aims to distribute keys uniformly across the hash table to minimize collisions (when two different keys hash to the same index).
  • Efficiency: The computation of hash values should be efficient to ensure that operations such as insertion, deletion, and search in the hash table are performed in constant time on average (O(1)O(1)O(1) time complexity).

Applications in DAA

In the context of DAA, hash functions are utilized primarily in:

  • Hash Tables: Data structures that store key-value pairs, where keys are mapped to indices using a hash function. This allows for rapid retrieval and update operations based on the keys.
  • Hashing Algorithms: Techniques that leverage hash functions for various applications such as data indexing, symbol tables, cryptography, and more.
  • Performance Optimization: Hash functions play a crucial role in optimizing the performance of algorithms and data structures by facilitating efficient data access and manipulation.

Basics of Hashing in Data Structure

Hashing in data structures involves using a hash function to map data keys to fixed-size hash values, which are then used as indices in a hash table. The hash table stores key-value pairs and allows for fast insertion, deletion, and retrieval operations.

A good hash function ensures that keys are evenly distributed across the hash table to minimize collisions, where multiple keys map to the same index. Techniques like chaining (using linked lists) or open addressing (probing for alternative slots) handle collisions efficiently. 

Hash tables are widely used in databases, caching systems, symbol tables, and unique identification schemes because they provide constant-time average-case operations for data access.

Easy Problem with Hashing

Problem Statement: You are given an array of integers. Find the first non-repeating integer in the array.

Solution Approach: To solve this problem efficiently, we can use hashing to track the frequency of each integer in the array. Here's a step-by-step solution using Python:

  • Initialize a Hash Map: Use a dictionary to store the frequency of each integer encountered in the array.
  • First Pass: Iterate through the array to populate the hash map with counts of each integer.
  • Second Pass: Iterate through the array again to find the first integer with a count of 1 in the hash map.
  • Return Result: Once found, return the first non-repeating integer.

Here's the Python code that implements this solution:

def first_non_repeating(arr):
    # Step 1: Initialize frequency dictionary
    freq = {}
    
    # Step 2: Count the frequencies of each integer
    for num in arr:
        If num in freq:
            freq[num] += 1
        Else:
            freq[num] = 1
    
    # Step 3: Find the first non-repeating integer
    for num in arr:
        if freq[num] == 1:
            return num
    
    # If no non-repeating integer is found, return None or handle as needed
    return None

# Example usage:
arr = [1, 2, 3, 2, 1, 4, 5, 4]
result = first_non_repeating(arr)
print("First non-repeating integer:", result)

Explanation:

  • The array [1, 2, 3, 2, 1, 4, 5, 4] is given in this example.
  • We use a dictionary freq to count occurrences of each integer in the array.
  • After populating the dictionary, we iterate through the array again to find the first integer with a frequency of 1 (indicating it's non-repeating).
  • The output will be 3, as it's the first integer in the array that appears only once.

This problem demonstrates a practical application of hashing to efficiently solve the task of finding the first non-repeating integer in an array using simple data structures and operations.

Advantages of hashing

Hashing provides efficient data storage and retrieval mechanisms by using hash functions to map keys to indexes in data structures like hash tables. Its advantages include fast access times O(1)O(1)O(1), efficient memory usage, support for data integrity and security through cryptographic hashing, and versatility in applications such as databases, caches, and unique identifier generation.

Hashing is crucial for optimising search operations and managing large datasets with minimal memory overhead, making it indispensable for fast and reliable data management and security in modern computing environments.

1. Fast Data Retrieval: Hash tables provide average-time constant-time O(1)O(1)O(1) complexity for insertion, deletion, and lookup operations. This makes hashing ideal for scenarios where quick access to data based on keys is crucial, such as in databases, caches, and symbol tables.

2. Efficient Search Operations: Hashing allows for efficient searching of elements by their fundamental values. Instead of sequentially searching through data, a hash function calculates the location of the desired data within a table, significantly reducing the time complexity compared to linear search algorithms.

3. Data Integrity and Security: Hash functions are fundamental in ensuring data integrity and security. They generate fixed-size outputs (hash values) for arbitrary inputs, enabling verification of data integrity during storage, transmission, or retrieval. Cryptographic hash functions, like SHA-256, are critical for ensuring data authenticity and preventing tampering.

4. Optimized Memory Usage: Hash tables use memory efficiently by storing elements in predefined slots based on their hash values. This approach minimises memory overhead compared to other data structures, such as linked lists or arrays, which may require additional memory allocation.

5. Versatility in Applications: Hashing is versatile and applicable in various domains, including databases, cryptography, network protocols, and data structures. It supports tasks ranging from indexing and searching to authentication and data deduplication.

6. Collision Handling: Modern hash table implementations include collision resolution techniques, such as chaining or open addressing, to manage situations where multiple keys map to the same hash value. These techniques ensure that hash tables remain efficient and reliable even when collisions occur.

Limitations in Hashing

Hashing is a powerful technique in computer science that facilitates fast data retrieval, efficient storage, and secure data integrity checks. However, like any technology, hashing has limitations and considerations that must be understood and addressed to utilise it in various applications effectively.

These limitations include challenges such as hash collisions, where different inputs may produce the same hash value, necessitating collision resolution techniques that can impact performance. The deterministic nature of hash functions, which always produce the same output for a given input, can pose challenges in specific scenarios, particularly in cryptographic applications where unpredictability is crucial.

1. Collision Handling: Hash collisions occur when two different inputs hash to the same index in a hash table. While collision resolution techniques like chaining or open addressing mitigate this issue, excessive collisions can degrade performance and necessitate careful design of hash functions and table sizes.

2. Deterministic Output: Hash functions produce deterministic outputs for given inputs, which means the same input always results in the same hash value. While desirable for data integrity and consistency, this determinism can potentially lead to vulnerabilities in specific applications, such as hash-based data structures and cryptographic systems.

3. Hash Function Quality: The quality of a hash function is crucial. A poor hash function may produce unevenly distributed hash values, leading to data clustering in specific slots of the hash table and increasing the likelihood of collisions. Ensuring a well-designed hash function is essential for maintaining efficiency and performance.

4. Limited Range of Output: Hash functions produce outputs within a fixed range (typically the size of the hash table or hash code). This limited range can lead to potential collisions, especially as the number of unique inputs increases beyond the hash table's capacity or the hash function's output size.

Conclusion

Hashing offers significant advantages in efficient data retrieval, fast access times, and secure data integrity checks; it's essential to be mindful of its limitations and considerations. Hashing can encounter challenges such as hash collisions, which require effective collision resolution strategies to maintain performance.

While advantageous in many applications, the deterministic nature of hash functions can also pose issues in scenarios requiring randomness or unpredictability. Furthermore, the quality and distribution of hash functions are critical factors influencing their performance and effectiveness. Poorly designed hash functions may lead to uneven data distribution and increased memory usage, impacting overall system efficiency.

FAQ's

👇 Instructions

Copy and paste below code to page Head section

Hashing is a technique that converts input data (of any size) into a fixed-size value (hash code or hash value) using a hash function. This hash value is typically used to index data in hash tables or verify data integrity.

Hash functions are algorithms that take an input (or 'message') and produce a fixed-size string of bytes. The output is typically a hash code, a unique representation of the original input data. Hash functions are designed to be fast to compute and to spread their outputs uniformly across their range.

Hashing is used in various applications, including: Data Retrieval: Hash tables for efficient data storage and retrieval. Data Integrity: Verifying the integrity of data during transmission or storage. Password Security: Storing passwords securely by hashing them instead of storing them in plain text. Cryptography: Generating digital signatures, message authentication codes (MACs), and ensuring data privacy.

Collisions occur when two different inputs to a hash function produce the same hash value. Collisions are unavoidable due to the finite range of hash values compared to the potentially infinite range of inputs. Hash functions aim to minimize collisions through a uniform distribution of hash values.

There are several collision resolution techniques. Chaining: Each hash table slot contains a linked list of elements that hash to the same value. Open Addressing: Probing sequentially through the hash table to find another slot for the colliding element. Double Hashing: Using a second hash function to calculate an alternative index when collisions occur.

Hash tables handle resizing (or rehashing) by creating a new, larger array when the load factor (ratio of stored elements to table size) exceeds a predefined threshold. All existing key-value pairs are rehashed into the new array based on their updated hash values, ensuring efficient storage and retrieval operations.

Ready to Master the Skills that Drive Your Career?
Avail your free 1:1 mentorship session.
You have successfully registered for the masterclass. An email with further details has been sent to you.
Thank you for joining us!
Oops! Something went wrong while submitting the form.
Join Our Community and Get Benefits of
💥  Course offers
😎  Newsletters
⚡  Updates and future events
a purple circle with a white arrow pointing to the left
Request Callback
undefined
a phone icon with the letter c on it
We recieved your Response
Will we mail you in few days for more details
undefined
Oops! Something went wrong while submitting the form.
undefined
a green and white icon of a phone
undefined
Ready to Master the Skills that Drive Your Career?
Avail your free 1:1 mentorship session.
You have successfully registered for the masterclass. An email with further details has been sent to you.
Thank you for joining us!
Oops! Something went wrong while submitting the form.
Get a 1:1 Mentorship call with our Career Advisor
Book free session