Exit
  1. Learn
  2. Check
  3. Solve
  4. Reflect

Look in the middle first

Think of the guessing game where someone picks a number from 1 to 100 and only says "higher" or "lower". You would start at 50. Because the list is sorted, the middle number tells you which half the value must be in.

If the middle is too small, the value can only be to its right. If it is too big, only to its left. One look, and half the list is gone.

Middle too small? The answer is to the right, so move the low marker to just past the middle.
Middle too big? The answer is to the left, so move the high marker to just before the middle.
Looking for 31target = 31
3
0
8
1
12
2
19
3
23
4
31
5
40
6
52
7
67
8
lowhigh

The middle is position 4, value 23. 31 is bigger, so 23 and everything left of it are out.

Move 1 of 3

Keep a range, not a position

Two markers, low and high, hold the part of the list that can still contain the value. Start with the whole list. Each look shrinks the range, like closing in on a word by opening a dictionary to the middle again and again.

If low passes high, the range is empty and the value isn't there.

Low not past high: there is still something to check.
Low past high: nothing is left, so answer minus one.
In code
answers = []
for x in queries:
    low, high, at = 0, len(nums) - 1, -1
    while low <= high:
        mid = (low + high) // 2
        if nums[mid] == x:
            at = mid
            break
        if nums[mid] < x:
            low = mid + 1
        else:
            high = mid - 1
    answers.append(at)
return answers

Why not just use a set?

A set from Hashing can tell you "yes, it's there". But a sorted list is often what you are given, and halving needs no extra memory. It also tells you where the value sits.

That opens up the next steps: where a missing value would go, and how many copies there are, which a set cannot answer.

Sorted input: is the signal to think of halving first.