iOS Interview - Explaining Dictionary data structure in Swift

April 29, 20249 min read#swift, #interview

In Swift, a dictionary is a built-in data structure that allows you to store key-value pairs. It is similar to the NSDictionary class in Objective-C, but with a more modern and type-safe syntax.

Dictionary key features:

  • Declaration: You declare a dictionary using the syntax [KeyType: ValueType], where KeyType is the type of the keys and ValueType is the type of the values. For example:

    var person: [String: Any] = ["name": "John", "age": 25, "city": "New York"]
  • Type Safety: Swift dictionaries are type-safe, meaning you must specify the types of the keys and values when declaring the dictionary. This helps catch type-related errors at compile-time.

  • Accessing Values: You can access the value associated with a key using subscript syntax. For example:

let name = person["name"] as? String

Note that accessing a value returns an optional, as the key may not exist in the dictionary.

  • Modifying Values: You can add, update, or remove key-value pairs in a dictionary using subscript syntax or methods like updateValue(_:forKey:) and removeValue(forKey:).

  • Iterating: You can iterate over the key-value pairs of a dictionary using a for-in loop. For example:

for (key, value) in person {
    print("\(key): \(value)")
}

Dictionary under the hood:

The time complexity for accessing a value in a Swift dictionary using a key is on average O(1), which is considered constant time complexity. This means that the time it takes to retrieve a value from the dictionary does not depend on the size of the dictionary itself.

Dictionaries in Swift are implemented using a hash table data structure. When you insert a key-value pair into the dictionary, the key is hashed, and the resulting hash value is used as an index to store the value in an internal array. When you try to retrieve a value using the key, the key is hashed again, and the resulting hash value is used to locate the value in the internal array. The hashing algorithm in the Swift Standard library for String key is SipHash, as shown in the source code.

The hashing process and the array indexing operations take constant time, regardless of the size of the dictionary. This is what makes the average time complexity of accessing a value in a dictionary constant, O(1).

However, it’s important to note that in the worst-case scenario, when there are many hash collisions (multiple keys mapping to the same hash value), the time complexity can degrade to O(n), where n is the number of entries with the same hash value. This situation is relatively rare, and most dictionary operations in practice have an average time complexity of O(1).

Dictionary Usages

Frequency Counter Pattern:

The frequency counter pattern involves using a dictionary to count the frequency of elements in a collection.

Example:

func countCharacterFrequency(_ string: String) -> [Character: Int] {
    var frequencyDict: [Character: Int] = [:]

    for char in string {
        frequencyDict[char, default: 0] += 1
    }

    return frequencyDict
}

let string = "hello world"
let frequencyDict = countCharacterFrequency(string)
print(frequencyDict) // Output: ["h": 1, "e": 1, "l": 3, "o": 2, " ": 1, "w": 1, "r": 1, "d": 1]

Two Sum Problem:

The two sum problem involves finding a pair of numbers in an array that add up to a target sum. Using a dictionary can optimize the solution.

Example:

func twoSum(_ nums: [Int], _ target: Int) -> [Int] {
    var numDict: [Int: Int] = [:]

    for (index, num) in nums.enumerated() {
        let complement = target - num
        if let complementIndex = numDict[complement] {
            return [complementIndex, index]
        }
        numDict[num] = index
    }

    return []
}

let nums = [2, 7, 11, 15]
let target = 9
let indices = twoSum(nums, target)
print(indices) // Output: [0, 1]

Anagram Grouping:

Anagram grouping involves grouping words that are anagrams of each other. Using a dictionary with sorted word as the key can efficiently group anagrams.

Example:

func groupAnagrams(_ strs: [String]) -> [[String]] {
    var anagramDict: [String: [String]] = [:]

    for word in strs {
        let sortedWord = String(word.sorted())
        anagramDict[sortedWord, default: []].append(word)
    }

    return Array(anagramDict.values)
}

let words = ["eat", "tea", "tan", "ate", "nat", "bat"]
let anagramGroups = groupAnagrams(words)
print(anagramGroups) // Output: [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]]

Memoization:

Memoization is a technique used to optimize recursive algorithms by storing previously computed results in a dictionary to avoid redundant calculations.

Example (Fibonacci sequence):

var memo: [Int: Int] = [:]

func fibonacci(_ n: Int) -> Int {
    if n <= 1 {
        return n
    }

    if let result = memo[n] {
        return result
    }

    memo[n] = fibonacci(n - 1) + fibonacci(n - 2)
    return memo[n]!
}

let n = 10
let fibonacciNumber = fibonacci(n)
print(fibonacciNumber) // Output: 55

These are just a few examples of how the Dictionary data structure can be used to solve various algorithmic problems in Swift. The Dictionary provides efficient key-value lookup and can be utilized in many other patterns and algorithms as well.

Consideration when using Dictionary:

Here are some of the main disadvantages of using hash tables:

  • Collisions: Hash collisions occur when different keys map to the same hash value. Collisions can lead to reduced performance as the hash table needs to handle them using techniques like chaining or open addressing, which adds complexity and can slow down insert, delete, and lookup operations in the worst case.

  • Memory overhead: Hash tables typically require more memory compared to other data structures like arrays or linked lists. In addition to storing the key-value pairs, hash tables need to allocate memory for the underlying array and any additional data structures used to handle collisions (e.g., linked lists for chaining). This memory overhead can be significant, especially if the hash table is sparsely populated.

  • Poor cache performance: Hash tables can suffer from poor cache performance due to the random distribution of keys. Unlike arrays where elements are stored contiguously in memory, the elements in a hash table are scattered based on their hash values. This can lead to more cache misses and slower memory access times, especially for large hash tables.

  • Inefficient for small datasets: For small datasets, the overhead of creating and maintaining a hash table may outweigh the benefits of fast lookups. In such cases, simpler data structures like arrays or linked lists might be more efficient in terms of memory usage and overall performance.

  • Key ordering: Hash tables do not maintain the ordering of keys. If you need to iterate over the keys in a specific order (e.g., sorted order), you would need to use additional data structures or perform sorting separately, which adds complexity and impacts performance.

  • Difficulty in resizing: When a hash table reaches its capacity and needs to be resized, it can be an expensive operation. Resizing typically involves creating a new larger array, rehashing all the existing keys, and transferring the key-value pairs to the new array. This process can be time-consuming, especially for large hash tables.

  • Vulnerability to DoS attacks: Hash tables can be vulnerable to denial-of-service (DoS) attacks if an attacker can deliberately choose keys that lead to many collisions. This can degrade the performance of the hash table and potentially cause a denial of service. To mitigate this, techniques like randomized hash functions or secure hash algorithms can be used.

Comparision to NSDictionary

  • Syntax: In Objective-C, you create an NSDictionary using the @{} syntax or by calling the dictionaryWithObjects:forKeys: method. In Swift, you use the [KeyType: ValueType] syntax.

  • Type Safety: NSDictionary in Objective-C is not type-safe, as it stores objects of type id for both keys and values. This means that type-related errors may only be caught at runtime. In contrast, Swift dictionaries are type-safe, providing better error detection at compile-time.

  • Mutability: In Objective-C, NSDictionary is immutable, meaning you cannot modify its contents after creation. To have a mutable dictionary, you need to use NSMutableDictionary. In Swift, dictionaries are mutable by default, and you can declare them as constants using the let keyword to make them immutable.

  • Accessing Values: In Objective-C, you use the objectForKey: method to retrieve a value associated with a key, which returns nil if the key doesn’t exist. In Swift, you use subscript syntax (dictionary[key]), which returns an optional value.

  • Performance: Swift dictionaries are generally faster and more efficient than NSDictionary in Objective-C, thanks to their optimized implementation and value semantics.

Overall, dictionaries in Swift provide a more concise, type-safe, and performant alternative to NSDictionary in Objective-C, while still offering similar functionality.

Quick Drop logo

Profile picture

Personal blog by An Tran. I'm focusing on creating useful apps.
#Swift #Kotlin #Mobile #MachineLearning #Minimalist


© An Tran - 2024