I believe I have come up with something that should work generally and efficiently if you are guaranteed not to have duplicates* (however, it should be extensible to any number of holes and any range of integers).
The idea behind this method is like quicksort, in that we find a pivot and partition around it, then recurse on the side(s) with a hole. To see which sides have the hole, we find the lowest and highest numbers, and compare them with the pivot and number of values on that side. Say the pivot is 17 and the minimum number is 11. If there are no holes, there should be 6 numbers (11, 12, 13, 14, 15, 16, 17). If there are 5, we know there is a hole on that side and we can recurse on just that side to find it. I'm having trouble explaining it more clearly than that, so let's take an example.
15 21 10 13 18 16 22 23 24 20 17 11 25 12 14
Pivot:
10 13 11 12 14 |15| 21 18 16 22 23 24 20 17 25
15 is the pivot, indicated by pipes (||
). There are 5 numbers on the left side of the pivot, as there should be (15 - 10), and 9 on the right, where there should be 10 (25 - 15). So we recurse on the right side; we'll note that the previous bound was 15 in case the hole is adjacent to it (16).
[15] 18 16 17 20 |21| 22 23 24 25
Now there are 4 numbers on the left side but there should be 5 (21 - 16). So we recurse there, and again we'll note the previous bound (in brackets).
[15] 16 17 |18| 20 [21]
The left side has the correct 2 numbers (18 - 16), but the right has 1 instead of 2 (20 - 18). Depending on our ending conditions, we could compare the 1 number to the two sides (18, 20) and see that 19 is missing or recurse one more time:
[18] |20| [21]
The left side has a size of zero, with a gap between the pivot (20) and previous bound (18), so 19 is the hole.
*: If there are duplicates, you could probably use a hash set to remove them in O(N) time, keeping the overall method O(N), but that might take more time than using some other method.