Quick Overview

Given log timestamps sorted in non-decreasing order with possible duplicates, answer many inclusive time-window queries by returning the first and last index of the entries inside each window, where a window may extend past the data's time range. It tests O(log n) per-query boundary searches on sorted data with duplicate keys.

Find Inclusive Index Ranges for Time-Window Queries on Timestamp-Sorted Logs

Company: Attentive

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Technical Screen

A service stores the timestamps of its log entries in an array sorted in non-decreasing order, so entry `i` has timestamp `timestamps[i]`. The same log is queried many times, and each query asks which entries fall inside a time window `[start, end]`. For each query, return the inclusive index range `[first, last]` of the log entries whose timestamps lie inside the window. Several entries may share the same timestamp, both ends of the window are inclusive, and the window may begin before the earliest entry or end after the latest one. ### Function Signature ```python def log_window_ranges(timestamps: list[int], queries: list[list[int]]) -> list[list[int]]: ``` Each query is a pair `[start, end]`. ### Rules - Entry `i` is in the window when `start <= timestamps[i] <= end`. - For each query, `first` is the smallest index and `last` is the largest index of an entry in the window. Because `timestamps` is sorted, every index from `first` to `last` is in the window. - If no entry falls in the window, the answer for that query is `[-1, -1]`. - `start` and `end` need not be timestamps that occur in `timestamps`, and either may lie before the first entry or after the last one. - Return one `[first, last]` pair per query, in the same order as `queries`. ### Constraints - `0 <= len(timestamps) <= 10^5` - `1 <= len(queries) <= 10^5` - `timestamps` is sorted in non-decreasing order; values may repeat. - `0 <= timestamps[i] <= 10^9` - `0 <= start <= end <= 2 * 10^9` for every query; all values fit in a 32-bit signed integer. - Answer each query in $O(\log n)$ time, where $n$ is `len(timestamps)`; scanning the log for every query is too slow at these limits. ### Examples **Example 1** ```text Input: timestamps = [1, 3, 3, 5, 8], queries = [[3, 5], [0, 100], [6, 7]] Output: [[1, 3], [0, 4], [-1, -1]] ``` For `[3, 5]`, both entries at timestamp `3` (indices `1` and `2`) and the entry at timestamp `5` (index `3`) are inside the inclusive window. `[0, 100]` begins before the first entry and ends after the last one, so it covers the whole log. No entry has a timestamp from `6` to `7`. **Example 2** ```text Input: timestamps = [2, 2, 2, 4, 4, 9], queries = [[2, 2], [3, 4], [4, 20], [0, 1], [10, 15]] Output: [[0, 2], [3, 4], [3, 5], [-1, -1], [-1, -1]] ``` `[2, 2]` returns the whole run of entries at timestamp `2`. `[3, 4]` starts at a timestamp that does not occur in the log. `[4, 20]` ends after the latest entry. `[0, 1]` lies entirely before the earliest entry and `[10, 15]` entirely after the latest, so both are empty. **Example 3** ```text Input: timestamps = [], queries = [[0, 10]] Output: [[-1, -1]] ``` An empty log has no entries in any window.

Overview: Given log timestamps sorted in non-decreasing order with possible duplicates, answer many inclusive time-window queries by returning the first and last index of the entries inside each window, where a window may extend past the data's time range. It tests O(log n) per-query boundary searches on sorted data with duplicate keys.

Read the full Attentive Software Engineer interview experience this question came from

A service stores the timestamps of its log entries in an array `timestamps` sorted in non-decreasing order, so entry `i` has timestamp `timestamps[i]`. The same log is queried many times, and each query asks which entries fall inside a time window `[start, end]`. For each query, return the inclusive index range `[first, last]` of the log entries whose timestamps lie inside the window. Several entries may share the same timestamp, both ends of the window are inclusive, and the window may begin before the earliest entry or end after the latest one. Implement `log_window_ranges(timestamps, queries)`, where each query is a pair `[start, end]`, and return a list of `[first, last]` pairs. ### Rules - Entry `i` is in the window when `start <= timestamps[i] <= end`. - For each query, `first` is the smallest index and `last` is the largest index of an entry in the window. Because `timestamps` is sorted, every index from `first` to `last` is in the window. - If no entry falls in the window, the answer for that query is `[-1, -1]`. - `start` and `end` need not be timestamps that occur in `timestamps`, and either may lie before the first entry or after the last one. - Return one `[first, last]` pair per query, in the same order as `queries`. ### Constraints - `0 <= len(timestamps) <= 10^5` - `1 <= len(queries) <= 10^5` - `timestamps` is sorted in non-decreasing order; values may repeat. - `0 <= timestamps[i] <= 10^9` - `0 <= start <= end <= 2 * 10^9` for every query; all values fit in a 32-bit signed integer (none exceeds 2^31 - 1, so Java and C++ use `int`). - Answer each query in O(log n) time, where n is `len(timestamps)`; scanning the log for every query is too slow at these limits. ### Example 1 ```text Input: timestamps = [1, 3, 3, 5, 8], queries = [[3, 5], [0, 100], [6, 7]] Output: [[1, 3], [0, 4], [-1, -1]] ``` For `[3, 5]`, both entries at timestamp `3` (indices `1` and `2`) and the entry at timestamp `5` (index `3`) are inside the inclusive window. `[0, 100]` begins before the first entry and ends after the last one, so it covers the whole log. No entry has a timestamp from `6` to `7`. ### Example 2 ```text Input: timestamps = [2, 2, 2, 4, 4, 9], queries = [[2, 2], [3, 4], [4, 20], [0, 1], [10, 15]] Output: [[0, 2], [3, 4], [3, 5], [-1, -1], [-1, -1]] ``` `[2, 2]` returns the whole run of entries at timestamp `2`. `[3, 4]` starts at a timestamp that does not occur in the log. `[4, 20]` ends after the latest entry. `[0, 1]` lies entirely before the earliest entry and `[10, 15]` entirely after the latest, so both are empty. An empty log (`timestamps = []`) answers `[-1, -1]` for every query.

Constraints

  • 0 <= len(timestamps) <= 10^5
  • 1 <= len(queries) <= 10^5
  • timestamps is sorted in non-decreasing order; values may repeat.
  • 0 <= timestamps[i] <= 10^9
  • 0 <= start <= end <= 2 * 10^9 for every query; all values fit in a 32-bit signed integer.
  • Answer each query in O(log n) time, where n is len(timestamps); scanning the log for every query is too slow at these limits.

Examples

Input: ([1, 3, 3, 5, 8], [[3, 5], [0, 100], [6, 7]])

Expected Output: [[1, 3], [0, 4], [-1, -1]]

Explanation: Source Example 1: a duplicate run inside the window, a window spanning the whole log, and an empty window inside a gap.

Input: ([2, 2, 2, 4, 4, 9], [[2, 2], [3, 4], [4, 20], [0, 1], [10, 15]])

Expected Output: [[0, 2], [3, 4], [3, 5], [-1, -1], [-1, -1]]

Explanation: Source Example 2: point window on a run, start absent from the log, end past the last entry, and windows entirely before and after the log.

Hints

  1. Because timestamps is sorted, every index from first to last is in the window, so each answer depends only on where the window's block of entries begins and ends.
  2. Watch repeated timestamps at either end of the window: first must be the leftmost entry with timestamp >= start, and last the rightmost entry with timestamp <= end.
  3. If those two positions cross, no entry falls in the window and the answer is [-1, -1]. This includes an empty log and windows entirely before or after it.

Loading coding console...

Show the approach

Approach

Because timestamps is sorted, the entries inside [start, end] form one contiguous block of indices, so each answer is fixed by the block's two ends. The left end is the lower bound of start: the first index whose timestamp is >= start. The right end is one less than the upper bound of end, where the upper bound is the first index whose timestamp is > end. Each bound is a binary search over the half-open range [lo, hi) that keeps this invariant: every index below lo fails the condition and every index at or above hi satisfies it. When lo == hi, that index is the boundary. Every index in [first, last] has timestamps[i] >= start (it is at or past the lower bound) and timestamps[i] <= end (it is before the upper bound), and no index outside that range qualifies, so first and last are exactly the smallest and largest indices in the window. If first > last, no entry qualifies and the answer is [-1, -1]. Edge cases: an empty log gives both bounds 0, so last = -1 and the answer is [-1, -1]. A window entirely before the log gives last = -1, and one entirely after it gives first = n. A duplicate run at either edge is handled because the lower bound lands on the leftmost copy of start and the upper bound minus one lands on the rightmost copy of end. Searching end with a separate upper bound, rather than with the lower bound of end + 1, avoids arithmetic on input values, all of which fit in 32-bit signed integers. Answers are appended in query order.

Time complexity:
O(q log n), where n = len(timestamps) and q = len(queries)
Space complexity:
O(q) for the output; O(1) extra