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.
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.