Amit K Biswas
Back to blog

Where will the crystal ball break?

2 min read

Let’s consider the collection of various distances as a boolean array. In this array, an entry is true if the crystal ball breaks at that distance, and false if it doesn’t. The false and true entries are contiguous, so the array will be like this, [false, false, false, true, true] or [false, true, true], and true entries will always come after false entries (if true entries exist at all). Why so? Because if a ball breaks at a certain distance, it’ll surely break in distances greater than the previous one. We have to find the index of the first entry where the value is true.

For linear search, we’ll need to check each entry to see if it’s true, one after another. In the worst case, only the last entry is true, and we’ll have to go through the whole array. While this works, the complexity is O(n) in the worst case.

function solution(breaks: boolean[]): number {
  for (let index = 0; index < breaks.length; index++) {
    if (breaks[index]) return index;
  }

  return -1;
}

It may look like a binary search will be considerably better, but the array being boolean has a major impact on the performance. Let’s say we have an array like this [false, false, false, true, true]. In typical Binary Search fashion, let’s halve the array and check the entry.

To measure the complexity, let’s assume a worst-case where all the values are true. For such an array of length 1000, the loops will run exactly 502 times. For 10000, it becomes 5002, and it increases proportionately. So it’s evident that the number of iterations is roughly half of the array, so O(n/2), which is equivalent to O(n) without considering the constant factor of 1/2.

function solution(breaks: boolean[]): number {
  let lo = 0;
  let resultIndex = null;

  while (lo < breaks.length - 1) {
    const mid = Math.floor(lo + (breaks.length - lo) / 2);
    if (!breaks[mid]) {
      lo = mid;
      continue;
    }

    resultIndex = mid;
    while (breaks[resultIndex]) {
      --resultIndex;
    }
    break;
  }

  return resultIndex ? resultIndex + 1 : -1;
}

Can we optimize this?

What if, unlike linear search where we go over the entries one by one, we jump a certain distance in each iteration, and then go back a bit if we already crossed the first true entry? The happy path would look like this,

We’ll start iterating from the beginning of the array and jump a certain amount of (√n) in each iteration.

For example, let’s say we have an array like [false, false, false, true, true, true]. Here, if we jump by √n each time from the 0th index, we’ll find a true entry at the 4th index in the 2nd iteration. As the entry itself isn’t the first true entry, we can iterate backwards from the 4th index to the last false index. This inner iteration will run √n times at worst-case, but we’ll find the result, guaranteed!

In terms of complexity, as we’re jumping the√n distance each time, the maximum number of iterations can be n/√n. If the jump is successful, that is we’ve found a true value, then we’re iterating a partial array with a length of √n. So the total number of iterations is (n/√n) + √n, which denotes the complexity is O(√n). This is significantly less than the linear/binary search approach.

function solution(breaks: boolean[]): number {
  const jumpAmount = Math.floor(Math.sqrt(breaks.length));

  for (
    let outerIndex = 0;
    outerIndex < breaks.length;
    outerIndex += jumpAmount
  ) {
    if (!breaks[outerIndex]) continue;
    for (
      let innerIndex = outerIndex;
      innerIndex >= outerIndex - jumpAmount;
      innerIndex--
    ) {
      if (!breaks[innerIndex]) {
        return innerIndex + 1;
      }
    }
  }

  return -1;
}

Final words

This hybrid approach balances the trade-off between speed and precision, making it ideal for problems where a critical threshold must be identified efficiently.