Docs
Documentation Fundamentals

Maps & Dictionaries

Introduction

Maps and dictionaries are fundamental data structures that store key-value pairs, allowing for efficient retrieval, insertion, and deletion of data based on unique keys. They form the backbone of many algorithms and applications, from database indexing to caching systems and symbol tables in compilers.

In Swift, the built-in Dictionary type provides a robust implementation of this concept, but understanding the underlying mechanisms and alternative implementations is crucial for writing efficient code and making informed architectural decisions.

Core Concepts

What is a Map?

A map (also known as an associative array or dictionary) is an abstract data type that maintains a collection of key-value pairs, where:

  • Each key is unique within the collection

  • Keys are mapped to their associated values

  • Operations include insertion, deletion, and lookup by key

Key Properties

  1. Uniqueness: Each key appears at most once in the map

  2. Association: Each key is associated with exactly one value

  3. Mutability: Values can be updated, and key-value pairs can be added or removed

  4. Efficiency: Well-designed maps provide fast lookup, insertion, and deletion

Implementation Strategies

1. Hash Tables (Hash Maps)

Hash tables use a hash function to map keys to array indices, providing average O(1) operations.

struct HashTable<Key: Hashable, Value> {
	private struct Entry {
		let key: Key
		var value: Value
		var isDeleted: Bool = false
		
		init(key: Key, value: Value) {
			self.key = key
			self.value = value
		}
	}
	
	private var buckets: [Entry?]
	private var count: Int = 0
	private let loadFactorThreshold: Double = 0.75
	
	init(capacity: Int = 16) {
		self.buckets = Array(repeating: nil, count: capacity)
	}
	
	var isEmpty: Bool {
		return count == 0
	}
	
	var size: Int {
		return count
	}
	
	// MARK: - Hash Function
	
	private func hash(_ key: Key) -> Int {
		return abs(key.hashValue) % buckets.count
	}
	
	// MARK: - Core Operations
	
	mutating func setValue(_ value: Value, forKey key: Key) {
		if shouldResize() {
			resize()
		}
		
		let index = findSlot(for: key)
		
		if let existingEntry = buckets[index], !existingEntry.isDeleted {
			// Update existing entry
			buckets[index]?.value = value
		} else {
			// Insert new entry
			buckets[index] = Entry(key: key, value: value)
			count += 1
		}
	}
	
	func getValue(forKey key: Key) -> Value? {
		let index = findSlot(for: key)
		
		if let entry = buckets[index], !entry.isDeleted, entry.key == key {
			return entry.value
		}
		
		return nil
	}
	
	mutating func removeValue(forKey key: Key) -> Value? {
		let index = findSlot(for: key)
		
		if let entry = buckets[index], !entry.isDeleted, entry.key == key {
			buckets[index]?.isDeleted = true
			count -= 1
			return entry.value
		}
		
		return nil
	}
	
	// MARK: - Helper Methods
	
	private func findSlot(for key: Key) -> Int {
		var index = hash(key)
		
		// Linear probing for collision resolution
		while let entry = buckets[index] {
			if entry.key == key {
				return index
			}
			index = (index + 1) % buckets.count
		}
		
		return index
	}
	
	private func shouldResize() -> Bool {
		return Double(count) / Double(buckets.count) >= loadFactorThreshold
	}
	
	private mutating func resize() {
		let oldBuckets = buckets
		buckets = Array(repeating: nil, count: buckets.count * 2)
		count = 0
		
		// Rehash all entries
		for entry in oldBuckets {
			if let entry = entry, !entry.isDeleted {
				setValue(entry.value, forKey: entry.key)
			}
		}
	}
}

// MARK: - Subscript Support

extension HashTable {
	subscript(key: Key) -> Value? {
		get {
			return getValue(forKey: key)
		}
		set {
			if let value = newValue {
				setValue(value, forKey: key)
			} else {
				removeValue(forKey: key)
			}
		}
	}
}

2. Separate Chaining Hash Table

An alternative approach using linked lists to handle collisions:

struct ChainedHashTable<Key: Hashable, Value> {
	private class Node {
		let key: Key
		var value: Value
		var next: Node?
		
		init(key: Key, value: Value, next: Node? = nil) {
			self.key = key
			self.value = value
			self.next = next
		}
	}
	
	private var buckets: [Node?]
	private var count: Int = 0
	
	init(capacity: Int = 16) {
		self.buckets = Array(repeating: nil, count: capacity)
	}
	
	var isEmpty: Bool {
		return count == 0
	}
	
	var size: Int {
		return count
	}
	
	private func hash(_ key: Key) -> Int {
		return abs(key.hashValue) % buckets.count
	}
	
	mutating func setValue(_ value: Value, forKey key: Key) {
		let index = hash(key)
		
		if buckets[index] == nil {
			buckets[index] = Node(key: key, value: value)
			count += 1
			return
		}
		
		var current = buckets[index]
		while let node = current {
			if node.key == key {
				node.value = value
				return
			}
			if node.next == nil {
				node.next = Node(key: key, value: value)
				count += 1
				return
			}
			current = node.next
		}
	}
	
	func getValue(forKey key: Key) -> Value? {
		let index = hash(key)
		var current = buckets[index]
		
		while let node = current {
			if node.key == key {
				return node.value
			}
			current = node.next
		}
		
		return nil
	}
	
	mutating func removeValue(forKey key: Key) -> Value? {
		let index = hash(key)
		
		guard let head = buckets[index] else { return nil }
		
		if head.key == key {
			buckets[index] = head.next
			count -= 1
			return head.value
		}
		
		var current = head
		while let next = current.next {
			if next.key == key {
				current.next = next.next
				count -= 1
				return next.value
			}
			current = next
		}
		
		return nil
	}
}

3. Binary Search Tree Map

A map implementation using a binary search tree for ordered key storage:

class BSTMap<Key: Comparable, Value> {
	private class Node {
		let key: Key
		var value: Value
		var left: Node?
		var right: Node?
		
		init(key: Key, value: Value) {
			self.key = key
			self.value = value
		}
	}
	
	private var root: Node?
	private var count: Int = 0
	
	var isEmpty: Bool {
		return root == nil
	}
	
	var size: Int {
		return count
	}
	
	func setValue(_ value: Value, forKey key: Key) {
		root = setValue(value, forKey: key, in: root)
	}
	
	private func setValue(_ value: Value, forKey key: Key, in node: Node?) -> Node {
		guard let node = node else {
			count += 1
			return Node(key: key, value: value)
		}
		
		if key < node.key {
			node.left = setValue(value, forKey: key, in: node.left)
		} else if key > node.key {
			node.right = setValue(value, forKey: key, in: node.right)
		} else {
			node.value = value
		}
		
		return node
	}
	
	func getValue(forKey key: Key) -> Value? {
		return getValue(forKey: key, in: root)
	}
	
	private func getValue(forKey key: Key, in node: Node?) -> Value? {
		guard let node = node else { return nil }
		
		if key < node.key {
			return getValue(forKey: key, in: node.left)
		} else if key > node.key {
			return getValue(forKey: key, in: node.right)
		} else {
			return node.value
		}
	}
	
	func removeValue(forKey key: Key) -> Value? {
		var removedValue: Value?
		root = removeValue(forKey: key, in: root, removedValue: &removedValue)
		return removedValue
	}
	
	private func removeValue(forKey key: Key, in node: Node?, removedValue: inout Value?) -> Node? {
		guard let node = node else { return nil }
		
		if key < node.key {
			node.left = removeValue(forKey: key, in: node.left, removedValue: &removedValue)
		} else if key > node.key {
			node.right = removeValue(forKey: key, in: node.right, removedValue: &removedValue)
		} else {
			removedValue = node.value
			count -= 1
			
			if node.left == nil {
				return node.right
			} else if node.right == nil {
				return node.left
			} else {
				let successor = findMin(in: node.right!)
				node.key = successor.key
				node.value = successor.value
				node.right = removeValue(forKey: successor.key, in: node.right, removedValue: &removedValue)
			}
		}
		
		return node
	}
	
	private func findMin(in node: Node) -> Node {
		var current = node
		while let left = current.left {
			current = left
		}
		return current
	}
	
	// MARK: - Ordered Operations
	
	func keys() -> [Key] {
		var result: [Key] = []
		inorderTraversal(root) { result.append($0.key) }
		return result
	}
	
	func values() -> [Value] {
		var result: [Value] = []
		inorderTraversal(root) { result.append($0.value) }
		return result
	}
	
	private func inorderTraversal(_ node: Node?, visit: (Node) -> Void) {
		guard let node = node else { return }
		inorderTraversal(node.left, visit: visit)
		visit(node)
		inorderTraversal(node.right, visit: visit)
	}
}

4. Trie (Prefix Tree) for String Keys

A specialized map implementation for string keys that supports prefix operations:

class TrieMap<Value> {
	private class Node {
		var value: Value?
		var children: [Character: Node] = [:]
		var isEndOfWord: Bool = false
	}
	
	private let root = Node()
	private var count = 0
	
	var isEmpty: Bool {
		return count == 0
	}
	
	var size: Int {
		return count
	}
	
	func setValue(_ value: Value, forKey key: String) {
		var current = root
		
		for char in key {
			if current.children[char] == nil {
				current.children[char] = Node()
			}
			current = current.children[char]!
		}
		
		if !current.isEndOfWord {
			count += 1
		}
		
		current.value = value
		current.isEndOfWord = true
	}
	
	func getValue(forKey key: String) -> Value? {
		var current = root
		
		for char in key {
			guard let node = current.children[char] else {
				return nil
			}
			current = node
		}
		
		return current.isEndOfWord ? current.value : nil
	}
	
	func removeValue(forKey key: String) -> Value? {
		return removeValue(forKey: key, from: root, index: key.startIndex)
	}
	
	private func removeValue(forKey key: String, from node: Node, index: String.Index) -> Value? {
		if index == key.endIndex {
			guard node.isEndOfWord else { return nil }
			
			let removedValue = node.value
			node.isEndOfWord = false
			node.value = nil
			count -= 1
			return removedValue
		}
		
		let char = key[index]
		guard let childNode = node.children[char] else { return nil }
		
		let removedValue = removeValue(forKey: key, from: childNode, index: key.index(after: index))
		
		// Clean up empty nodes
		if !childNode.isEndOfWord && childNode.children.isEmpty {
			node.children[char] = nil
		}
		
		return removedValue
	}
	
	// MARK: - Prefix Operations
	
	func keysWithPrefix(_ prefix: String) -> [String] {
		var current = root
		
		for char in prefix {
			guard let node = current.children[char] else {
				return []
			}
			current = node
		}
		
		var results: [String] = []
		collectKeys(from: current, prefix: prefix, results: &results)
		return results
	}
	
	private func collectKeys(from node: Node, prefix: String, results: inout [String]) {
		if node.isEndOfWord {
			results.append(prefix)
		}
		
		for (char, childNode) in node.children {
			collectKeys(from: childNode, prefix: prefix + String(char), results: &results)
		}
	}
	
	func hasPrefix(_ prefix: String) -> Bool {
		var current = root
		
		for char in prefix {
			guard let node = current.children[char] else {
				return false
			}
			current = node
		}
		
		return true
	}
}

Swift’s Built-in Dictionary

Swift’s Dictionary type is a highly optimized hash table implementation. Let’s explore advanced usage patterns:

Advanced Dictionary Operations

extension Dictionary {
	// Merge dictionaries with custom conflict resolution
	mutating func merge<S: Sequence>(_ other: S, uniquingKeysWith combine: (Value, Value) throws -> Value) rethrows 
	where S.Element == (Key, Value) {
		for (key, value) in other {
			self[key] = try combine(self[key] ?? value, value)
		}
	}
	
	// Group elements by key
	static func grouping<S: Sequence>(_ values: S, by keyForValue: (S.Element) throws -> Key) rethrows -> [Key: [S.Element]]
	where Value == [S.Element] {
		return try Dictionary<Key, [S.Element]>(grouping: values, by: keyForValue)
	}
	
	// Transform values while preserving keys
	func mapValues<T>(_ transform: (Value) throws -> T) rethrows -> [Key: T] {
		var result: [Key: T] = [:]
		for (key, value) in self {
			result[key] = try transform(value)
		}
		return result
	}
	
	// Compact map values, removing nil results
	func compactMapValues<T>(_ transform: (Value) throws -> T?) rethrows -> [Key: T] {
		var result: [Key: T] = [:]
		for (key, value) in self {
			if let transformedValue = try transform(value) {
				result[key] = transformedValue
			}
		}
		return result
	}
}

Custom Dictionary Implementation with Swift Features

struct CustomDictionary<Key: Hashable, Value> {
	private var storage: [Key: Value] = [:]
	
	var isEmpty: Bool {
		return storage.isEmpty
	}
	
	var count: Int {
		return storage.count
	}
	
	var keys: Dictionary<Key, Value>.Keys {
		return storage.keys
	}
	
	var values: Dictionary<Key, Value>.Values {
		return storage.values
	}
	
	subscript(key: Key) -> Value? {
		get {
			return storage[key]
		}
		set {
			storage[key] = newValue
		}
	}
	
	// Default value subscript
	subscript(key: Key, default defaultValue: @autoclosure () -> Value) -> Value {
		get {
			return storage[key] ?? defaultValue()
		}
		set {
			storage[key] = newValue
		}
	}
	
	mutating func updateValue(_ value: Value, forKey key: Key) -> Value? {
		return storage.updateValue(value, forKey: key)
	}
	
	mutating func removeValue(forKey key: Key) -> Value? {
		return storage.removeValue(forKey: key)
	}
	
	mutating func merge<S: Sequence>(_ other: S, uniquingKeysWith combine: (Value, Value) throws -> Value) rethrows 
	where S.Element == (Key, Value) {
		try storage.merge(other, uniquingKeysWith: combine)
	}
}

// MARK: - Collection Conformance

extension CustomDictionary: Collection {
	typealias Element = (key: Key, value: Value)
	typealias Index = Dictionary<Key, Value>.Index
	
	var startIndex: Index {
		return storage.startIndex
	}
	
	var endIndex: Index {
		return storage.endIndex
	}
	
	subscript(index: Index) -> Element {
		let (key, value) = storage[index]
		return (key: key, value: value)
	}
	
	func index(after i: Index) -> Index {
		return storage.index(after: i)
	}
}

// MARK: - ExpressibleByDictionaryLiteral

extension CustomDictionary: ExpressibleByDictionaryLiteral {
	init(dictionaryLiteral elements: (Key, Value)...) {
		self.storage = Dictionary(uniqueKeysWithValues: elements)
	}
}

Time Complexity Analysis

Hash Table Operations

Operation

Average Case

Worst Case

Best Case

Insert

O(1)

O(n)

O(1)

Search

O(1)

O(n)

O(1)

Delete

O(1)

O(n)

O(1)

Binary Search Tree Map Operations

Operation

Average Case

Worst Case

Best Case

Insert

O(log n)

O(n)

O(log n)

Search

O(log n)

O(n)

O(log n)

Delete

O(log n)

O(n)

O(log n)

Trie Operations

Operation

Time Complexity

Space Complexity

Insert

O(m)

O(m)

Search

O(m)

O(1)

Delete

O(m)

O(1)

Prefix

O(p + k)

O(k)

Where m = key length, p = prefix length, k = number of keys with prefix

Real-World Applications

1. LRU Cache Implementation

class LRUCache<Key: Hashable, Value> {
	private class Node {
		let key: Key
		var value: Value
		var prev: Node?
		var next: Node?
		
		init(key: Key, value: Value) {
			self.key = key
			self.value = value
		}
	}
	
	private let capacity: Int
	private var cache: [Key: Node] = [:]
	private let head = Node(key: "" as! Key, value: "" as! Value) // Dummy head
	private let tail = Node(key: "" as! Key, value: "" as! Value) // Dummy tail
	
	init(capacity: Int) {
		self.capacity = capacity
		head.next = tail
		tail.prev = head
	}
	
	func get(_ key: Key) -> Value? {
		guard let node = cache[key] else { return nil }
		
		// Move to front
		moveToFront(node)
		return node.value
	}
	
	func set(_ key: Key, _ value: Value) {
		if let existingNode = cache[key] {
			existingNode.value = value
			moveToFront(existingNode)
		} else {
			let newNode = Node(key: key, value: value)
			cache[key] = newNode
			addToFront(newNode)
			
			if cache.count > capacity {
				removeLeastUsed()
			}
		}
	}
	
	private func moveToFront(_ node: Node) {
		removeFromList(node)
		addToFront(node)
	}
	
	private func addToFront(_ node: Node) {
		node.next = head.next
		node.prev = head
		head.next?.prev = node
		head.next = node
	}
	
	private func removeFromList(_ node: Node) {
		node.prev?.next = node.next
		node.next?.prev = node.prev
	}
	
	private func removeLeastUsed() {
		if let lastNode = tail.prev, lastNode !== head {
			cache[lastNode.key] = nil
			removeFromList(lastNode)
		}
	}
}

2. Word Frequency Counter

class WordFrequencyCounter {
	private var frequencies: [String: Int] = [:]
	
	func addText(_ text: String) {
		let words = text.lowercased()
			.components(separatedBy: .whitespacesAndNewlines)
			.compactMap { word in
				let cleaned = word.trimmingCharacters(in: .punctuationCharacters)
				return cleaned.isEmpty ? nil : cleaned
			}
		
		for word in words {
			frequencies[word, default: 0] += 1
		}
	}
	
	func frequency(of word: String) -> Int {
		return frequencies[word.lowercased()] ?? 0
	}
	
	func mostFrequent(count: Int) -> [(word: String, frequency: Int)] {
		return frequencies
			.sorted { $0.value > $1.value }
			.prefix(count)
			.map { (word: $0.key, frequency: $0.value) }
	}
	
	func wordsWithFrequency(_ frequency: Int) -> [String] {
		return frequencies
			.compactMap { $0.value == frequency ? $0.key : nil }
			.sorted()
	}
}

3. Configuration Manager

class ConfigurationManager {
	private var configurations: [String: Any] = [:]
	private let queue = DispatchQueue(label: "config.queue", attributes: .concurrent)
	
	func setValue<T>(_ value: T, forKey key: String) {
		queue.async(flags: .barrier) {
			self.configurations[key] = value
		}
	}
	
	func getValue<T>(forKey key: String, type: T.Type) -> T? {
		return queue.sync {
			return configurations[key] as? T
		}
	}
	
	func getValue<T>(forKey key: String, default defaultValue: T) -> T {
		return queue.sync {
			return configurations[key] as? T ?? defaultValue
		}
	}
	
	func removeValue(forKey key: String) {
		queue.async(flags: .barrier) {
			self.configurations[key] = nil
		}
	}
	
	func allKeys() -> [String] {
		return queue.sync {
			return Array(configurations.keys)
		}
	}
	
	func clear() {
		queue.async(flags: .barrier) {
			self.configurations.removeAll()
		}
	}
}

4. Database Index Simulation

struct DatabaseIndex<Key: Hashable, RecordID: Hashable> {
	private var index: [Key: Set<RecordID>] = [:]
	
	mutating func addRecord(id: RecordID, forKey key: Key) {
		index[key, default: Set<RecordID>()].insert(id)
	}
	
	mutating func removeRecord(id: RecordID, forKey key: Key) {
		index[key]?.remove(id)
		if index[key]?.isEmpty == true {
			index[key] = nil
		}
	}
	
	func findRecords(forKey key: Key) -> Set<RecordID> {
		return index[key] ?? Set<RecordID>()
	}
	
	func findRecords(forKeys keys: [Key]) -> Set<RecordID> {
		return keys.reduce(into: Set<RecordID>()) { result, key in
			result.formUnion(findRecords(forKey: key))
		}
	}
	
	func findRecords(matchingAll keys: [Key]) -> Set<RecordID> {
		guard let firstKey = keys.first else { return Set<RecordID>() }
		
		return keys.dropFirst().reduce(findRecords(forKey: firstKey)) { result, key in
			result.intersection(findRecords(forKey: key))
		}
	}
}

Performance Optimization Techniques

1. Memory-Efficient String Interning

class StringInterner {
	private var internedStrings: [String: String] = [:]
	
	func intern(_ string: String) -> String {
		if let existingString = internedStrings[string] {
			return existingString
		} else {
			internedStrings[string] = string
			return string
		}
	}
	
	func clear() {
		internedStrings.removeAll()
	}
	
	var count: Int {
		return internedStrings.count
	}
}

2. Batch Operations for Better Performance

extension Dictionary {
	mutating func setBatch<S: Sequence>(_ keyValuePairs: S) where S.Element == (Key, Value) {
		reserveCapacity(count + keyValuePairs.underestimatedCount)
		
		for (key, value) in keyValuePairs {
			self[key] = value
		}
	}
	
	func getBatch<S: Sequence>(_ keys: S) -> [Key: Value] where S.Element == Key {
		var result: [Key: Value] = [:]
		result.reserveCapacity(keys.underestimatedCount)
		
		for key in keys {
			if let value = self[key] {
				result[key] = value
			}
		}
		
		return result
	}
}

Best Practices and Design Patterns

1. Type-Safe Configuration Keys

protocol ConfigurationKey {
	associatedtype Value
	static var defaultValue: Value { get }
}

struct ServerURLKey: ConfigurationKey {
	typealias Value = URL
	static let defaultValue = URL(string: "https://api.example.com")!
}

struct MaxRetryCountKey: ConfigurationKey {
	typealias Value = Int
	static let defaultValue = 3
}

class TypeSafeConfiguration {
	private var storage: [String: Any] = [:]
	
	func setValue<Key: ConfigurationKey>(_ value: Key.Value, for key: Key.Type) {
		storage[String(describing: key)] = value
	}
	
	func getValue<Key: ConfigurationKey>(for key: Key.Type) -> Key.Value {
		return storage[String(describing: key)] as? Key.Value ?? Key.defaultValue
	}
}

2. Lazy Loading Dictionary

class LazyDictionary<Key: Hashable, Value> {
	private var storage: [Key: Value] = [:]
	private let valueProvider: (Key) -> Value?
	
	init(valueProvider: @escaping (Key) -> Value?) {
		self.valueProvider = valueProvider
	}
	
	subscript(key: Key) -> Value? {
		if let existingValue = storage[key] {
			return existingValue
		}
		
		if let computedValue = valueProvider(key) {
			storage[key] = computedValue
			return computedValue
		}
		
		return nil
	}
	
	func preload(keys: [Key]) {
		for key in keys {
			_ = self[key] // Trigger lazy loading
		}
	}
}

3. Multi-Level Cache

class MultiLevelCache<Key: Hashable, Value> {
	private var l1Cache: [Key: Value] = [:]
	private var l2Cache: [Key: Value] = [:]
	private let l1Capacity: Int
	private let l2Capacity: Int
	private let valueProvider: (Key) -> Value?
	
	init(l1Capacity: Int, l2Capacity: Int, valueProvider: @escaping (Key) -> Value?) {
		self.l1Capacity = l1Capacity
		self.l2Capacity = l2Capacity
		self.valueProvider = valueProvider
	}
	
	func getValue(for key: Key) -> Value? {
		// Check L1 cache first
		if let value = l1Cache[key] {
			return value
		}
		
		// Check L2 cache
		if let value = l2Cache[key] {
			promoteToL1(key: key, value: value)
			return value
		}
		
		// Load from source
		if let value = valueProvider(key) {
			setValue(value, for: key)
			return value
		}
		
		return nil
	}
	
	private func setValue(_ value: Value, for key: Key) {
		if l1Cache.count >= l1Capacity {
			evictFromL1()
		}
		
		l1Cache[key] = value
	}
	
	private func promoteToL1(key: Key, value: Value) {
		l2Cache[key] = nil
		setValue(value, for: key)
	}
	
	private func evictFromL1() {
		if let (key, value) = l1Cache.randomElement() {
			l1Cache[key] = nil
			
			if l2Cache.count >= l2Capacity {
				l2Cache.removeValue(forKey: l2Cache.randomElement()!.key)
			}
			
			l2Cache[key] = value
		}
	}
}

Testing and Debugging

Unit Testing Dictionary Implementations

import XCTest

class HashTableTests: XCTestCase {
	var hashTable: HashTable<String, Int>!
	
	override func setUp() {
		super.setUp()
		hashTable = HashTable<String, Int>()
	}
	
	func testInsertAndRetrieve() {
		hashTable["key1"] = 100
		hashTable["key2"] = 200
		
		XCTAssertEqual(hashTable["key1"], 100)
		XCTAssertEqual(hashTable["key2"], 200)
		XCTAssertEqual(hashTable.size, 2)
	}
	
	func testUpdateExistingKey() {
		hashTable["key1"] = 100
		hashTable["key1"] = 150
		
		XCTAssertEqual(hashTable["key1"], 150)
		XCTAssertEqual(hashTable.size, 1)
	}
	
	func testRemoveKey() {
		hashTable["key1"] = 100
		let removedValue = hashTable.removeValue(forKey: "key1")
		
		XCTAssertEqual(removedValue, 100)
		XCTAssertNil(hashTable["key1"])
		XCTAssertEqual(hashTable.size, 0)
	}
	
	func testCollisionHandling() {
		// Insert many items to force collisions
		for i in 0..<100 {
			hashTable["key\(i)"] = i
		}
		
		// Verify all items are retrievable
		for i in 0..<100 {
			XCTAssertEqual(hashTable["key\(i)"], i)
		}
		
		XCTAssertEqual(hashTable.size, 100)
	}
	
	func testResizing() {
		let initialCapacity = 16
		let itemsToInsert = 50 // This should trigger resize
		
		for i in 0..<itemsToInsert {
			hashTable["key\(i)"] = i
		}
		
		// All items should still be accessible after resize
		for i in 0..<itemsToInsert {
			XCTAssertEqual(hashTable["key\(i)"], i)
		}
	}
	
	func testPerformance() {
		measure {
			for i in 0..<10000 {
				hashTable["key\(i)"] = i
			}
		}
	}
}

class BSTMapTests: XCTestCase {
	var bstMap: BSTMap<String, Int>!
	
	override func setUp() {
		super.setUp()
		bstMap = BSTMap<String, Int>()
	}
	
	func testOrderedTraversal() {
		let keys = ["dog", "cat", "elephant", "ant", "bear"]
		for (index, key) in keys.enumerated() {
			bstMap.setValue(index, forKey: key)
		}
		
		let sortedKeys = bstMap.keys()
		XCTAssertEqual(sortedKeys, ["ant", "bear", "cat", "dog", "elephant"])
	}
	
	func testBalanceIssues() {
		// Insert in sorted order (worst case for basic BST)
		for i in 1...10 {
			bstMap.setValue(i, forKey: "key\(String(format: "%02d", i))")
		}
		
		// Should still work, but performance will be degraded
		for i in 1...10 {
			XCTAssertEqual(bstMap.getValue(forKey: "key\(String(format: "%02d", i))"), i)
		}
	}
}

class TrieMapTests: XCTestCase {
	var trie: TrieMap<Int>!
	
	override func setUp() {
		super.setUp()
		trie = TrieMap<Int>()
	}
	
	func testPrefixOperations() {
		trie.setValue(1, forKey: "cat")
		trie.setValue(2, forKey: "car")
		trie.setValue(3, forKey: "card")
		trie.setValue(4, forKey: "care")
		trie.setValue(5, forKey: "dog")
		
		let carPrefixKeys = trie.keysWithPrefix("car")
		XCTAssertEqual(Set(carPrefixKeys), Set(["car", "card", "care"]))
		
		XCTAssertTrue(trie.hasPrefix("ca"))
		XCTAssertFalse(trie.hasPrefix("bat"))
	}
	
	func testEmptyStringHandling() {
		trie.setValue(42, forKey: "")
		XCTAssertEqual(trie.getValue(forKey: ""), 42)
		
		let emptyPrefixKeys = trie.keysWithPrefix("")
		XCTAssertTrue(emptyPrefixKeys.contains(""))
	}
}

Performance Profiling Tools

class PerformanceProfiler {
	static func measureTime<T>(operation: () throws -> T) rethrows -> (result: T, time: TimeInterval) {
		let startTime = CFAbsoluteTimeGetCurrent()
		let result = try operation()
		let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
		return (result, timeElapsed)
	}
	
	static func benchmarkDictionaryOperations<Key: Hashable, Value>(
		dictionary: inout [Key: Value],
		keys: [Key],
		values: [Value]
	) -> [String: TimeInterval] {
		precondition(keys.count == values.count)
		
		var results: [String: TimeInterval] = [:]
		
		// Benchmark insertions
		let (_, insertTime) = measureTime {
			for (key, value) in zip(keys, values) {
				dictionary[key] = value
			}
		}
		results["insert"] = insertTime
		
		// Benchmark lookups
		let (_, lookupTime) = measureTime {
			for key in keys {
				_ = dictionary[key]
			}
		}
		results["lookup"] = lookupTime
		
		// Benchmark updates
		let (_, updateTime) = measureTime {
			for (key, value) in zip(keys, values) {
				dictionary[key] = value
			}
		}
		results["update"] = updateTime
		
		// Benchmark deletions
		let (_, deleteTime) = measureTime {
			for key in keys {
				dictionary[key] = nil
			}
		}
		results["delete"] = deleteTime
		
		return results
	}
}

Memory Usage Analysis

class MemoryProfiler {
	static func measureMemoryUsage<T>(operation: () -> T) -> (result: T, memoryUsed: Int) {
		let before = mach_task_basic_info()
		let result = operation()
		let after = mach_task_basic_info()
		
		let memoryUsed = Int(after.resident_size - before.resident_size)
		return (result, memoryUsed)
	}
	
	private static func mach_task_basic_info() -> mach_task_basic_info {
		var info = mach_task_basic_info()
		var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
		
		let result = withUnsafeMutablePointer(to: &info) {
			$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
				task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
			}
		}
		
		guard result == KERN_SUCCESS else {
			fatalError("Failed to get memory info")
		}
		
		return info
	}
}

Common Pitfalls and Solutions

1. Hash Function Quality

// Poor hash function - causes many collisions
struct PoorHashable: Hashable {
	let value: String
	
	func hash(into hasher: inout Hasher) {
		hasher.combine(value.count) // Only uses string length!
	}
}

// Better hash function - distributes values well
struct BetterHashable: Hashable {
	let value: String
	
	func hash(into hasher: inout Hasher) {
		hasher.combine(value) // Uses entire string content
	}
}

// Custom hash function for specific needs
struct CustomHashable: Hashable {
	let id: Int
	let category: String
	
	func hash(into hasher: inout Hasher) {
		hasher.combine(id)
		hasher.combine(category)
	}
	
	static func == (lhs: CustomHashable, rhs: CustomHashable) -> Bool {
		return lhs.id == rhs.id && lhs.category == rhs.category
	}
}

2. Memory Leaks in Reference-Based Maps

// Potential memory leak - strong reference cycles
class Node {
	let id: String
	var connections: [String: Node] = [:]
	
	init(id: String) {
		self.id = id
	}
}

// Solution: Use weak references where appropriate
class SafeNode {
	let id: String
	private var _connections: [String: WeakRef<SafeNode>] = [:]
	
	init(id: String) {
		self.id = id
	}
	
	func addConnection(_ node: SafeNode) {
		_connections[node.id] = WeakRef(node)
	}
	
	func getConnection(_ id: String) -> SafeNode? {
		return _connections[id]?.value
	}
	
	func removeDeadConnections() {
		_connections = _connections.compactMapValues { ref in
			ref.value != nil ? ref : nil
		}
	}
}

class WeakRef<T: AnyObject> {
	weak var value: T?
	
	init(_ value: T) {
		self.value = value
	}
}

3. Thread Safety Considerations

class ThreadSafeDictionary<Key: Hashable, Value> {
	private var dictionary: [Key: Value] = [:]
	private let queue = DispatchQueue(label: "dictionary.queue", attributes: .concurrent)
	
	subscript(key: Key) -> Value? {
		get {
			return queue.sync {
				return dictionary[key]
			}
		}
		set {
			queue.async(flags: .barrier) {
				self.dictionary[key] = newValue
			}
		}
	}
	
	func getValue(forKey key: Key) -> Value? {
		return queue.sync {
			return dictionary[key]
		}
	}
	
	func setValue(_ value: Value, forKey key: Key) {
		queue.async(flags: .barrier) {
			self.dictionary[key] = value
		}
	}
	
	func removeValue(forKey key: Key) -> Value? {
		return queue.sync(flags: .barrier) {
			return self.dictionary.removeValue(forKey: key)
		}
	}
	
	var count: Int {
		return queue.sync {
			return dictionary.count
		}
	}
}

Advanced Topics

1. Consistent Hashing for Distributed Systems

class ConsistentHash<Node: Hashable> {
	private var ring: [UInt32: Node] = [:]
	private let virtualNodes: Int
	
	init(virtualNodes: Int = 100) {
		self.virtualNodes = virtualNodes
	}
	
	func addNode(_ node: Node) {
		for i in 0..<virtualNodes {
			let hash = computeHash("\(node):\(i)")
			ring[hash] = node
		}
	}
	
	func removeNode(_ node: Node) {
		for i in 0..<virtualNodes {
			let hash = computeHash("\(node):\(i)")
			ring[hash] = nil
		}
	}
	
	func getNode(forKey key: String) -> Node? {
		guard !ring.isEmpty else { return nil }
		
		let hash = computeHash(key)
		let sortedHashes = ring.keys.sorted()
		
		// Find the first node clockwise from the hash
		for nodeHash in sortedHashes {
			if nodeHash >= hash {
				return ring[nodeHash]
			}
		}
		
		// Wrap around to the first node
		return ring[sortedHashes.first!]
	}
	
	private func computeHash(_ input: String) -> UInt32 {
		return UInt32(input.hashValue) & 0xFFFFFFFF
	}
}

2. Bloom Filters for Membership Testing

struct BloomFilter {
	private var bitArray: [Bool]
	private let hashFunctions: [(String) -> Int]
	
	init(size: Int, hashFunctionCount: Int) {
		self.bitArray = Array(repeating: false, count: size)
		self.hashFunctions = (0..<hashFunctionCount).map { seed in
			return { string in
				var hasher = Hasher()
				hasher.combine(string)
				hasher.combine(seed)
				return abs(hasher.finalize()) % size
			}
		}
	}
	
	mutating func insert(_ item: String) {
		for hashFunction in hashFunctions {
			let index = hashFunction(item)
			bitArray[index] = true
		}
	}
	
	func mightContain(_ item: String) -> Bool {
		for hashFunction in hashFunctions {
			let index = hashFunction(item)
			if !bitArray[index] {
				return false
			}
		}
		return true
	}
	
	func definitelyDoesNotContain(_ item: String) -> Bool {
		return !mightContain(item)
	}
}

3. Cuckoo Hashing Implementation

struct CuckooHash<Key: Hashable, Value> {
	private var table1: [Entry?]
	private var table2: [Entry?]
	private let maxIterations = 8
	
	private struct Entry {
		let key: Key
		let value: Value
	}
	
	init(capacity: Int = 16) {
		self.table1 = Array(repeating: nil, count: capacity)
		self.table2 = Array(repeating: nil, count: capacity)
	}
	
	private func hash1(_ key: Key) -> Int {
		return abs(key.hashValue) % table1.count
	}
	
	private func hash2(_ key: Key) -> Int {
		return abs(key.hashValue.multipliedReportingOverflow(by: 31).partialValue) % table2.count
	}
	
	mutating func setValue(_ value: Value, forKey key: Key) {
		let newEntry = Entry(key: key, value: value)
		
		if insert(newEntry) {
			return
		}
		
		// Rehash if insertion failed
		rehash()
		_ = insert(newEntry)
	}
	
	private mutating func insert(_ entry: Entry) -> Bool {
		var currentEntry = entry
		
		for _ in 0..<maxIterations {
			let index1 = hash1(currentEntry.key)
			if table1[index1] == nil {
				table1[index1] = currentEntry
				return true
			}
			
			// Kick out existing entry and place it in table2
			let evicted = table1[index1]!
			table1[index1] = currentEntry
			
			let index2 = hash2(evicted.key)
			if table2[index2] == nil {
				table2[index2] = evicted
				return true
			}
			
			// Kick out from table2 and continue cycle
			currentEntry = table2[index2]!
			table2[index2] = evicted
		}
		
		return false // Cycle detected, need to rehash
	}
	
	private mutating func rehash() {
		let oldTable1 = table1
		let oldTable2 = table2
		
		table1 = Array(repeating: nil, count: table1.count * 2)
		table2 = Array(repeating: nil, count: table2.count * 2)
		
		for entry in oldTable1.compactMap({ $0 }) {
			_ = insert(entry)
		}
		
		for entry in oldTable2.compactMap({ $0 }) {
			_ = insert(entry)
		}
	}
	
	func getValue(forKey key: Key) -> Value? {
		let index1 = hash1(key)
		if let entry = table1[index1], entry.key == key {
			return entry.value
		}
		
		let index2 = hash2(key)
		if let entry = table2[index2], entry.key == key {
			return entry.value
		}
		
		return nil
	}
}

Exercises

Exercise 1: LFU Cache

Implement a Least Frequently Used (LFU) cache that evicts the least frequently accessed items when capacity is exceeded.

Exercise 2: Multi-Map Implementation

Create a data structure that allows multiple values for a single key, supporting operations like add(key, value), remove(key, value), and getAll(key).

Exercise 3: Ordered Dictionary

Implement a dictionary that maintains insertion order while providing O(1) access times.

Exercise 4: Radix Tree (Patricia Trie)

Build a compressed trie that minimizes space usage for string keys with common prefixes.

Exercise 5: Robin Hood Hashing

Implement a hash table using Robin Hood hashing for better worst-case performance.

Summary

Maps and dictionaries are versatile data structures that form the foundation of many algorithms and applications. Understanding their various implementations helps you choose the right tool for specific requirements:

Key Takeaways:

  • Hash tables provide average O(1) operations but can degrade to O(n) in worst cases

  • Binary search trees offer O(log n) operations with ordered traversal capabilities

  • Tries excel at prefix-based operations and string key scenarios

  • Swift’s Dictionary is highly optimized and suitable for most use cases

  • Thread safety requires careful consideration in concurrent environments

  • Memory efficiency and cache locality significantly impact real-world performance

When to Choose Each Implementation:

  • Use hash tables for general-purpose key-value storage with fast access

  • Choose BST maps when you need ordered keys or range queries

  • Select tries for prefix matching and string-heavy applications

  • Consider specialized implementations for specific performance requirements

Understanding these fundamentals enables you to build efficient, scalable applications and make informed decisions about data structure selection in your Swift projects.

Docs
Copyright © Mitch Lang. All rights reserved.