Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

635. Design Log Storage System - Leetcode Solution

Problem Description

The "Design Log Storage System" problem from LeetCode asks you to implement a system that can store log entries, each with a unique id and a timestamp. The timestamps are formatted as Year:Month:Day:Hour:Minute:Second (e.g., "2017:01:01:23:59:59"). The system must support two operations:

  • put(id, timestamp): Store a log entry with the given id and timestamp.
  • retrieve(start, end, granularity): Return the ids of all logs whose timestamps fall within the range from start to end (inclusive), using the specified granularity (such as "Year", "Month", "Day", etc.).

The key constraints are:

  • Each id is unique.
  • retrieve queries must respect the granularity by comparing only the relevant prefix of the timestamp.
  • All operations should be efficient, as there can be many log entries and queries.

Thought Process

The initial approach might be to store all logs in a list and, for each retrieve operation, scan through every log to check if its timestamp is in the given range. However, this brute-force method is inefficient, especially for large datasets.

To optimize, we should consider how timestamps are formatted: they are zero-padded and lexicographically sortable. This means we can compare timestamp strings directly, and the order will match chronological order. Furthermore, since granularity can vary, we only need to compare the relevant portion (prefix) of the timestamp.

For efficient retrieval, we might also consider sorting or using a data structure that supports range queries. However, since the problem does not require real-time performance and the number of logs is manageable, we can stick with a simple list and optimize the comparison logic.

Solution Approach

Let's break down the solution step by step:

  1. Data Storage:
    • Store each log as a pair (timestamp, id) in a list.
  2. Granularity Mapping:
    • Define a mapping from granularity (e.g., "Year", "Month", etc.) to the number of characters to consider in the timestamp. For example, "Year" maps to 4, "Month" to 7, and so on.
  3. Putting a Log:
    • Simply append the (timestamp, id) pair to the list.
  4. Retrieving Logs:
    • Given start, end, and granularity, determine the prefix length.
    • For each log, compare the relevant prefix of its timestamp with those of start and end.
    • If the log's prefix falls within the range, include its id in the result.
  5. Efficiency:
    • Since timestamps are compared as strings, this is efficient and straightforward.

This approach is simple, leverages the properties of the timestamp format, and is easy to implement.

Example Walkthrough

Suppose we perform the following operations:

  • put(1, "2017:01:01:23:59:59")
  • put(2, "2017:01:02:23:59:59")
  • put(3, "2016:12:31:00:00:00")
  • retrieve("2016:12:31:00:00:00", "2017:01:02:23:59:59", "Day")

Step-by-step:

  1. The granularity "Day" corresponds to the first 10 characters (YYYY:MM:DD).
  2. start_prefix = "2016:12:31", end_prefix = "2017:01:02"
  3. For each log:
    • Log 1: "2017:01:01:23:59:59" → prefix "2017:01:01" (between start and end) → include id 1
    • Log 2: "2017:01:02:23:59:59" → prefix "2017:01:02" (equal to end) → include id 2
    • Log 3: "2016:12:31:00:00:00" → prefix "2016:12:31" (equal to start) → include id 3
  4. The result is [1, 2, 3].

This demonstrates how the prefix-based comparison works for different granularities.

Time and Space Complexity

Brute-force Approach:

  • For each retrieve operation, we scan all n logs and compare prefixes. Thus, each query is O(n).
  • Storing logs takes O(1) per log, O(n) total space.
Optimized Approach:
  • If we wanted faster queries, we could sort logs or use a balanced BST, achieving O(log n) query time, but the problem constraints allow the simpler approach.

Overall, time complexity for put is O(1), for retrieve is O(n), and space complexity is O(n).

Summary

The solution to the "Design Log Storage System" problem leverages the lexicographical order of zero-padded timestamps, allowing us to compare prefixes for different granularities efficiently. By mapping granularities to prefix lengths, we can easily check if a log's timestamp falls within a specified range. The approach is simple, easy to implement, and efficient for the problem's constraints, making it an elegant solution for log storage and retrieval based on time ranges.

Code Implementation

class LogSystem:
    def __init__(self):
        self.logs = []
        self.gra_to_idx = {
            "Year": 4,
            "Month": 7,
            "Day": 10,
            "Hour": 13,
            "Minute": 16,
            "Second": 19
        }

    def put(self, id: int, timestamp: str) -> None:
        self.logs.append((timestamp, id))

    def retrieve(self, start: str, end: str, granularity: str) -> list:
        idx = self.gra_to_idx[granularity]
        start_prefix = start[:idx]
        end_prefix = end[:idx]
        res = []
        for timestamp, id in self.logs:
            if start_prefix <= timestamp[:idx] <= end_prefix:
                res.append(id)
        return res
      
class LogSystem {
    vector<pair<string, int>> logs;
    unordered_map<string, int> gra_to_idx = {
        {"Year", 4},
        {"Month", 7},
        {"Day", 10},
        {"Hour", 13},
        {"Minute", 16},
        {"Second", 19}
    };
public:
    LogSystem() {}

    void put(int id, string timestamp) {
        logs.push_back({timestamp, id});
    }

    vector<int> retrieve(string start, string end, string granularity) {
        int idx = gra_to_idx[granularity];
        string start_prefix = start.substr(0, idx);
        string end_prefix = end.substr(0, idx);
        vector<int> res;
        for (auto &p : logs) {
            string t_prefix = p.first.substr(0, idx);
            if (start_prefix <= t_prefix && t_prefix <= end_prefix) {
                res.push_back(p.second);
            }
        }
        return res;
    }
};
      
import java.util.*;

class LogSystem {
    private List<Pair> logs;
    private Map<String, Integer> graToIdx;

    public LogSystem() {
        logs = new ArrayList<>();
        graToIdx = new HashMap<>();
        graToIdx.put("Year", 4);
        graToIdx.put("Month", 7);
        graToIdx.put("Day", 10);
        graToIdx.put("Hour", 13);
        graToIdx.put("Minute", 16);
        graToIdx.put("Second", 19);
    }

    public void put(int id, String timestamp) {
        logs.add(new Pair(timestamp, id));
    }

    public List<Integer> retrieve(String start, String end, String granularity) {
        int idx = graToIdx.get(granularity);
        String startPrefix = start.substring(0, idx);
        String endPrefix = end.substring(0, idx);
        List<Integer> res = new ArrayList<>();
        for (Pair p : logs) {
            String tPrefix = p.timestamp.substring(0, idx);
            if (startPrefix.compareTo(tPrefix) <= 0 && tPrefix.compareTo(endPrefix) <= 0) {
                res.add(p.id);
            }
        }
        return res;
    }

    private class Pair {
        String timestamp;
        int id;
        Pair(String t, int i) {
            this.timestamp = t;
            this.id = i;
        }
    }
}
      
class LogSystem {
    constructor() {
        this.logs = [];
        this.graToIdx = {
            "Year": 4,
            "Month": 7,
            "Day": 10,
            "Hour": 13,
            "Minute": 16,
            "Second": 19
        };
    }

    put(id, timestamp) {
        this.logs.push([timestamp, id]);
    }

    retrieve(start, end, granularity) {
        const idx = this.graToIdx[granularity];
        const startPrefix = start.slice(0, idx);
        const endPrefix = end.slice(0, idx);
        const res = [];
        for (const [timestamp, id] of this.logs) {
            const tPrefix = timestamp.slice(0, idx);
            if (startPrefix <= tPrefix && tPrefix <= endPrefix) {
                res.push(id);
            }
        }
        return res;
    }
}