The most efficient way would be to use a Format Preserving Encryption algorithm; that is an algorithm that is a permutation over an arbitrary-sized set (for example, a sequence of 10 decimal digits).
Using it would be simple: you'd pick a random key and store it; you'd also keep a sequence number (in the same format as the output, for example, you might start with 0000000000). Then, when it comes time to generate the next ID, you'd increment the sequence number, and send that through the FPE algorithm; that's your next ID [1].
Because the FPE algorithm is a permutation, you will never generate the same ID twice until the sequence number wraps; hence, no collisions. You can make the ID as short as needed (current FPE algorithms have issues with truly tiny spaces; if you keep the ID to be, say, at least 6 digits, you'd be safe). And, because FPE algorithms are secure, any ID does not provide any information about any other ID (including the relative generation order).
The drawback: there are no (as far as I know) common FPE libraries available. For use, I'd suggest FF1 from this document; implementing that from scratch would be a bit of work (but would satisfy your needs).
A less efficient, but easier to implement method would be to do a variation on what you suggested: keep a list of IDs you have not yet assigned.
Here, during setup, you would initialize a list of all possible IDs in sequential order, say 000000 through 999999. In addition, you would set a value N to the highest unassigned ID (in this example, N = 999999).
Then, when it comes time to issue a new ID, you would pick a random number x between 0 and N (inclusive). Then, you would swap the IDs at indices N and x (and if x=N, then this swap operation doesn't do anything); then, you would output the value that's not in index N (and then decrement N).
That's it; this is actually the Fisher-Yates shuffle, you could do this on-demand (as I wrote it), or you could do all the shuffling at setup time (and just read things off the list when generating IDs).
This is less efficient than the FPE idea - both because the setup is more involved, and you need to keep a largish list of IDs around. On the other hand, it is a lot easier to implement than trying to implement FF1 from scratch...
[1]: Format Preserving Encryption algorithms also take a 'tweak'; you'd want to keep that a fixed value (for example, the empty string); a tweak provides a service that your specific use does not need.