For each direction of a conversation, TCP keeps track of the number of bytes sent, and the number of bytes received. This is done using a Sequence number (to track bytes sent) and an Acknowledgement number (to track bytes received).
For simplicity, I'll proceed as if the SEQ# and the ACK# are a count of packets sent (in reality, it is a count of bytes sent). In addition, we will only keep track of the conversation in one direction, we won't bother ourselves with the conversation in the other direction.
Bob Alice
Sends packets #1, #2, #3 --->
Bob sends to Alice packets with SEQ# 1, 2, and 3.... There are three possibilities of how Alice can respond:
If Alice receives each packet, she will respond with ACK#4, saying she has received everything before #4, and is ready for #4 next.
If Alice receives only #1, and nothing else, she will respond with ACK#2, indicating she is ready for #2, and has received everything before. This will prompt Bob to sent #2 and #3 again.
If Alice receives only #1 and #3, and somehow #2 gets lost in transit. Alice still has no choice but to respond with ACK#2. Remember, Alice can only respond with an Acknowledgement number that confirms the packets she has received. If she responded with #4, it would communicate to Bob that #1, #2, and #3 were received, which isn't the case.
So in both cases prior, Bob receives ACK#2, and Bob has no way of knowing whether just #2 or #2 and #3 were lost -- so to play it safe, Bob will resend #2 and #3.
That said, in the 3rd case above, when Bob re-sends #2 and #3, but Alice already had #3.. whether she replaces the bytes within packet #3 in her buffer with the newly received #3, or whether she simply ignores the duplicate received #3 is entirely up to the implementation -- and largely the difference will be unnoticeable to the user. The end effect is the same.
Of course, this is simply a factor of there just being two numbers being tracked, A SEQ# stating bytes sent, and an ACK# tracking bytes received. As the OP pointed out, there was a feature added to TCP called Selective Acknowledgement, or TCP SACK.
TCP SACK allows the receiver to not just send an ACK# (bytes received), but to also specify a "left edge" and "right edge" of the bytes that were received. Which then indicates to the sender specifically which bytes need to be resent.