This project demonstrates how to efficiently ingest large CSV datasets and build an in-memory hash index for fast lookups, implemented in portable C. It loads UK Property Price data from CSV (e.g., pp-2024.csv, pp-2023.csv), constructs a hash index on the street field, and compares linear search with indexed lookups, reporting timing and load factor statistics.
- Fast CSV ingestion: Streams and parses rows without loading the entire file into memory first.
- Append-only table: Multiple CSV files can be read sequentially; rows are appended to a single global table.
- Hash index on street: Builds a chained hash table for O(1) average-time street lookups.
- Benchmarking: Compares linear search vs. hash-index search and prints wall-clock timings.
- Load factor insights: Reports unused buckets and load factor to reason about index quality.
- Memory safety: Carefully allocates and frees dynamic memory for variable-length fields.
- Data model: Records are stored in a contiguous dynamic array of
Recordstructs (table) with a globaltable_sizefor row count. Variable-lengthdistrictis heap-allocated per row. - CSV parsing: Each line is read via
fgetsand parsed with a singlesscanfformat string that handles quoted fields. The parser extracts fixed-size fields directly into struct buffers and copiesdistrictto a right-sized allocation. - Incremental growth: The table grows as lines are read via
realloc, enabling processing of very large inputs without precomputing row counts. - Hash index: The function
createIndexOnStreetbuilds anIndexEntry**array of sizeINDEX_SIZE(separate chaining). Collisions are handled by prependingIndexEntrynodes in each bucket. The hash function is a classic DJB2 variant moduloINDEX_SIZE. - Searching:
- Linear:
searchStreetLinearscans the table and comparesstreetnames. - Indexed:
searchStreethashes the target street and traverses only that bucket’s chain.
- Linear:
- Benchmarking and stats:
main.ctimes both search modes and printsunused slots,load factor, and elapsed seconds for each strategy.
- Streaming I/O: Reads one line at a time; memory usage is proportional to rows actually stored, not file size at once.
- Single-pass parsing:
sscanfunpacks fields directly into the struct layout, minimizing copies. - Amortized growth: While
reallocper row is used here for clarity, modern allocators make this surprisingly efficient; growth can be tuned (e.g., geometric) if you need even faster bulk loads. - O(1) average indexing: After build, street lookups avoid table scans, keeping query latency flat as datasets grow.
- Time complexity:
- Ingestion: O(N) to read and parse N rows.
- Index build: O(N) inserts with separate chaining.
- Search: O(N) linear vs. O(1) average with hashing (O(K) where K is bucket length).
- Space usage:
- Table: O(N) records.
- Index: O(INDEX_SIZE + N)
IndexEntrynodes and keys.
- Load factor (α = used_buckets / INDEX_SIZE):
- Reported at runtime to guide
INDEX_SIZEtuning for your dataset.
- Reported at runtime to guide
main.c: Orchestrates file loads, index creation, timings, and stats.myDSlib.h: Public types, constants, and function declarations (Record,IndexEntry,INDEX_SIZE, etc.).myDSlib.c: CSV ingestion, hashing, index build/search, memory management.pp-2023.csv,pp-2024.csv: Sample input data files.Makefile: Build automation.
makeThis produces an executable (commonly main or target defined in the Makefile).
./mainExpected console output includes total records appended after each file, creation of the hash index, timings for linear vs. hash search, and hash table stats such as unused slots and load factor. Example excerpt:
Total records appneded: 850000
Total records appneded: 1700000
Hash index on street created.
Time (Linear Search): 0.842311 seconds
Time (Hash Index) : 0.000317 seconds
Unused hash slots: 12 out of 100 (12.00% unused)
Hash table load factor: 0.880
Note: Output magnitudes depend on dataset size and INDEX_SIZE.
INDEX_SIZE: Adjust inmyDSlib.hto balance memory and collision rate. Larger values reduce chain lengths and speed up lookups at the cost of more memory.- Printing matches: Toggle
printFlagLinearSearchandprintFlagHashIndexSearchinmain.cto inspect found records. - Target field: The index is built on
street, but you can build additional indices (e.g., onpostcode) by following the same pattern.
- Every
districtstring is individually allocated and freed infree_table. - Index nodes and copied keys are freed in
free_index. - All resources are released at program exit.
- Current ingestion uses
reallocper row for simplicity; switching to capacity doubling will improve bulk load throughput. - The CSV parser assumes a consistent quoted format; if your CSV varies, adjust the
sscanfformat or use a dedicated CSV parser. - Consider normalizing
streetcase and trimming whitespace to improve match rates.