그래도 기초가 필요한 이유 - AI 답을 판별하는 눈
이 장에서 배우는 것
앞 장에서 개발 공부의 방향을 지식 목록보다 실제로 할 수 있는 일에 두었다. 그렇다고 기초 지식의 쓰임이 줄어드는 것은 아니다. AI가 만든 코드를 읽고 실행해 보았을 때, 무엇을 확인해야 하는지 알려면 기준이 필요하다. 알고리즘과 자료구조는 그 기준을 세우는 데 쓰인다. 용어를 많이 외우기 위해 배우는 것이 아니라, 답이 맞는지와 일이 불필요하게 늘어나는지를 설명하기 위해 배우는 것이다.
이 장에서는 학습 기록에서 중복된 항목을 제거하는 두 코드를 비교한다. 작은 입력에서는 두 코드가 같은 결과를 내지만, 입력이 늘어날 때 수행하는 일의 양은 다르게 증가한다. 실행 시간을 재는 대신 특정 연산의 횟수를 세어 그 차이를 눈으로 확인한다. 프로그램 속 입력 크기와 기록 번호는 설명용 가상값이다.
- AI가 준 코드에서 결과의 정확성과 처리 과정의 효율을 구분한다.
- 알고리즘·자료구조·추상화·분해가 코드의 판단 근거가 되는 이유를 설명한다.
- 리스트로 중복을 확인하는 방식과 집합을 사용하는 방식의 작업 횟수를 비교한다.
- 횟수 표가 보여 주는 사실과 보여 주지 못하는 사실을 구별한다.
문제 상황
학습 기록장을 만드는 작은 팀에서 중복 제거 기능을 맡았다고 하자. 같은 학습 항목을 여러 번 가져오면 목록에 한 번만 표시하려 한다. 기록 번호가 같으면 같은 항목으로 판단한다. 먼저 등장한 순서도 유지해야 한다. 예를 들어 번호가 3, 1, 3, 2라면 결과는 3, 1, 2여야 한다.
AI에게 다음과 같이 요청했다.
학습 기록 번호가 담긴 목록에서 중복을 제거하는 Python 코드를 작성해 달라. 같은 번호는 한 번만 남기고, 처음 등장한 순서를 유지해야 한다.
AI가 돌려준 답의 설명은 다음과 같다고 하자. 이는 실제 서비스의 대화 기록을 옮긴 것이 아니라, 이 장을 위해 만든 가상 대화다.
빈 목록을 만들고 번호를 하나씩 읽는다. 결과 목록에 없는 번호만 추가하면 중복을 제거하면서 순서를 유지할 수 있다.
설명은 타당하다. 실제로 이 방식은 요구한 결과를 만들 수 있다. 그러나 여기서 “결과가 맞다”와 “기록이 많아져도 작업량이 적절하다”는 다른 판단이다. 이미 저장한 번호를 처음부터 찾아보는 방식이라면, 새 번호가 올 때마다 확인할 대상이 늘어난다. 작은 예시가 잘 실행된다는 사실만으로 많은 기록도 적은 작업으로 처리한다고 결론 내릴 수는 없다.
반대로 AI가 “집합으로 바꾸면 빠르다”고 제안했다고 해서 바로 교체해서도 안 된다. 집합만 결과로 사용하면 처음 등장한 순서를 유지한다는 조건을 놓칠 수 있다. 기록이 번호가 아니라 다른 형태의 값이라면 집합에 넣을 수 있는지도 살펴야 한다. AI의 설명에서 판단에 필요한 조건을 찾아내는 일이 개발자의 작업에 포함된다.
이 변화는 개발자가 사라진다는 뜻보다, 개발자가 시간을 쓰는 일이 달라진다는 뜻에 가깝다. 코드를 입력하는 작업 일부를 맡길 수 있어도, 어떤 결과를 받아들일지는 사람이 설명해야 한다. 그 설명을 가능하게 하는 재료가 기초다.
맞는 결과와 적절한 작업량을 따로 본다
알고리즘(algorithm)은 문제를 해결하는 절차다. 중복 제거 문제에서는 “번호를 하나 읽고, 이미 본 번호인지 확인하고, 처음 보는 번호면 저장한다”가 절차에 해당한다. Python 문법을 모두 알지 못해도 이 절차가 요구한 일을 하는지 먼저 생각할 수 있다.
정확성을 살필 때는 구체적인 입력과 예상 결과를 연결한다. 빈 목록이라면 결과도 빈 목록이어야 한다. 모든 번호가 같다면 하나만 남아야 한다. 서로 다른 번호라면 모두 남아야 한다. 순서 유지가 조건이라면 번호가 작은 순으로 다시 정렬해서는 안 된다. 그럴듯한 설명을 이러한 작은 사례에 대입하면 빠진 조건을 찾기 쉽다.
작업량을 살필 때는 절차 안에서 반복되는 일을 찾는다. 리스트(list)는 여러 값을 순서대로 담는 자료구조다. 자료구조(data structure)는 값을 저장하고 찾아 사용하는 방식을 뜻한다. 리스트에 번호가 있는지 확인할 때는 앞에서부터 값을 살펴볼 수 있다. 찾는 번호가 맨 앞에 있으면 금방 끝나지만, 없으면 끝까지 확인해야 한다.
서로 다른 번호 네 개가 차례로 들어온다고 하자. 첫 번호는 빈 결과 목록과 비교하므로 비교가 없다. 두 번째 번호는 이미 저장한 하나와 비교한다. 세 번째 번호는 두 개, 네 번째 번호는 세 개와 비교한다. 따라서 번호끼리 같은지 묻는 횟수는 0 + 1 + 2 + 3, 즉 6회다.
입력이 n개이고 모두 다르다면 비교 횟수는 0부터 n − 1까지 더한 값이다. 이를 n × (n − 1) ÷ 2로 쓸 수 있다. 여기서 n은 입력에 들어 있는 번호의 개수다. 공식을 외우기 전에 각 번호가 앞서 저장된 번호를 모두 확인한다는 이유를 이해하는 것이 먼저다.
입력의 크기가 커질 때 작업량이 어떻게 증가하는지를 시간 복잡도(time complexity)라고 한다. 이 리스트 방식은 서로 다른 번호가 많아지는 상황에서 작업량이 대략 입력 크기의 제곱에 비례해 증가한다. 입력을 두 배로 늘리면 큰 입력에서는 비교 횟수가 대략 네 배가 되는 방향이다. 모든 입력에서 정확히 네 배라는 뜻은 아니다.
같은 번호만 반복해서 들어오면 상황이 달라진다. 결과 목록에는 번호 하나만 남고, 이후에는 그 번호와 한 번 비교하면 된다. 따라서 알고리즘을 판단할 때는 입력 개수뿐 아니라 값의 구성도 살펴야 한다. 이 장의 표는 서로 다른 번호만 들어오는 경우를 선택한다. 이 조건을 밝혀야 표의 의미를 정확히 읽을 수 있다.
자료구조를 바꾸면 확인 방법이 달라진다
집합(set)은 같은 값을 중복해서 저장하지 않는 자료구조다. Python의 집합은 값을 바탕으로 계산한 해시(hash)라는 값을 이용해 저장 위치의 후보를 찾는다. 해시는 값에서 계산하는 식별용 값이며, 서로 다른 값에 같은 해시가 나올 수도 있다. 그럴 때는 추가 확인이 필요하다.
이 덕분에 집합은 보통 처음부터 모든 원소를 훑지 않고 값이 있는지 찾는다. 일반적인 조건에서 한 번의 포함 여부 확인에 드는 평균 작업량은 집합 크기에 비례해서 늘어나지 않는 것으로 다룬다. 번호를 n개 읽으며 한 번씩 확인하면 전체 작업량은 평균적으로 n에 비례하는 방향이 된다. 다만 이것이 어떤 값과 어떤 상황에서도 내부 확인이 한 번으로 끝난다는 뜻은 아니다.
여기에는 중요한 측정상의 차이가 있다. 리스트에서는 우리가 번호끼리 비교하는 코드를 직접 써서 실제 비교 횟수를 셀 수 있다. 집합에서는 Python 내부에서 일어난 모든 비교를 이 짧은 프로그램으로 세지 않는다. 대신 코드가 집합에 포함 여부를 묻는 횟수를 센다. 두 숫자를 같은 종류의 기계 연산 횟수라고 부르면 오해가 생긴다.
| 방식 | 세는 일 | 포함하지 않는 일 |
|---|---|---|
| 리스트 | 기존 번호와 같은지 직접 비교한 횟수 | 반복 제어와 목록 추가 등의 작업 |
| 집합 | 집합에 포함 여부를 요청한 횟수 | 해시 계산과 집합 내부 탐색·비교 |
따라서 표에서 리스트가 496이고 집합이 32라고 나와도 집합이 정확히 15.5배 빠르다고 말할 수는 없다. 서로 단가가 다른 일을 센 것이기 때문이다. 표가 보여 주는 것은 리스트에서 직접 비교하는 일이 누적되는 모습과, 집합에 보내는 확인 요청이 입력마다 한 번씩이라는 구조다. 집합 방식의 평균적인 효율을 판단하려면 해시를 이용한 탐색의 성질까지 함께 알아야 한다.
순서를 유지하려면 두 저장 공간을 함께 사용한다. 집합에는 “이미 본 번호인가”를 묻고, 결과 리스트에는 “처음 등장한 번호를 어떤 순서로 보여 줄 것인가”를 맡긴다. 번호가 처음 나타났을 때 두 곳에 모두 넣는다. 확인을 잘하는 구조와 순서를 담는 구조의 역할을 나누는 것이다.
집합 방식에는 별도 집합을 유지하는 메모리 비용도 있다. 메모리는 실행 중 값을 담아 두는 공간이다. 입력이 작고 처리 횟수도 적다면 단순한 리스트 방식으로 충분할 수 있다. 기초를 배운다는 것은 집합을 무조건 선택하는 규칙을 외우는 일이 아니다. 입력 규모, 순서 조건, 값의 종류, 저장 공간을 근거로 선택을 설명하는 일이다.
추상화와 분해로 판단할 범위를 정한다
추상화(abstraction)는 현재 판단에 필요한 특징을 남기고 나머지를 잠시 접어 두는 일이다. 학습 기록에는 제목, 날짜, 메모가 있을 수 있다. 그러나 중복 여부를 기록 번호로 판단하기로 했다면, 이번 비교에서는 정수 번호만 다룬다. 덕분에 화면 구성이나 파일 저장을 생각하지 않고 중복 제거 절차에 집중할 수 있다.
추상화에는 조건이 따른다. “번호가 같으면 같은 기록이다”라는 약속이 성립해야 한다. 서로 다른 기록에 같은 번호를 붙였다면 두 코드 모두 잘못된 기준으로 기록을 없앤다. 알고리즘이 약속대로 실행되었다고 해서 약속 자체도 적절했다는 뜻은 아니다.
분해(decomposition)는 큰 일을 확인 가능한 작은 일로 나누는 것이다. 이 프로그램은 번호 만들기, 리스트 방식 실행, 집합 방식 실행, 결과 확인, 표 출력으로 나눈다. 각 부분이 맡은 일이 짧으면 AI가 만든 코드도 어디를 읽어야 하는지 찾기 쉬워진다.
특히 두 방식에 같은 입력을 전달해야 비교가 성립한다. 한쪽에는 서로 다른 번호를, 다른 쪽에는 같은 번호의 반복을 넣으면 자료구조의 차이와 입력의 차이가 섞인다. 또한 횟수가 적어도 결과가 잘못되었다면 사용할 수 없다. 그래서 완성 코드는 작은 확인 사례를 먼저 실행하고, 표를 만들 때도 두 결과가 입력과 같은지 검사한다.
이 확인은 모든 가능한 입력에 대한 증명은 아니다. 다만 코드의 설명을 믿는 데서 멈추지 않고, 읽은 절차를 실제 사례에 대입하는 출발점이다. 기초 지식은 무엇을 나누어 확인할지, 무엇을 같은 조건으로 두어야 할지를 알려 준다.
완성 코드
다음 코드를 main.py라는 이름으로 저장한다. Python 3.12 이상의 표준 기능만 사용한다. 입력을 받거나 네트워크에 연결하지 않으며 실행하면 바로 끝난다. 빈 줄을 포함해 59줄이다. 숫자는 모두 설명용 가상값이며, 시간 대신 정해진 작업의 횟수를 세므로 매번 같은 결과를 출력한다.
def remove_with_list(records):
result = []
comparisons = 0
for record in records:
found = False
for saved in result:
comparisons += 1
if record == saved:
found = True
break
if not found:
result.append(record)
return result, comparisons
def remove_with_set(records):
result = []
seen = set()
checks = 0
for record in records:
checks += 1
if record not in seen:
seen.add(record)
result.append(record)
return result, checks
def verify_examples():
cases = [
([], []),
([7, 7, 7], [7]),
([3, 1, 3, 2], [3, 1, 2]),
]
for records, expected in cases:
list_result, list_count = remove_with_list(records)
set_result, set_count = remove_with_set(records)
if list_result != expected or set_result != expected:
raise ValueError("중복 제거 결과가 예상과 다릅니다.")
def main():
verify_examples()
sizes = [4, 8, 16, 32]
print("입력 개수 | 리스트 값 비교 | 집합 포함 요청 | 결과 개수")
for size in sizes:
records = list(range(size))
list_result, list_count = remove_with_list(records)
set_result, set_count = remove_with_set(records)
if list_result != records or set_result != records:
raise ValueError("서로 다른 번호가 보존되지 않았습니다.")
print(
f"{size} | {list_count} | {set_count} | {len(set_result)}"
)
print("확인: 빈 입력, 반복 번호, 등장 순서")
print("주의: 집합 내부 비교 횟수는 세지 않습니다.")
if __name__ == "__main__":
main()
줄별 해설
아래 번호는 완성 코드의 첫 줄부터 빈 줄도 포함해 센 것이다. 빈 줄은 기능을 수행하지 않고, 함수 사이의 경계를 읽기 쉽게 만든다. 함수는 이름을 붙여 두었다가 호출해서 실행하는 코드 묶음이다.
- 1행은 리스트 방식의 함수를 정의한다. records는 함수가 전달받는 기록 번호 목록이다.
- 2행은 결과를 담을 빈 리스트를 만든다. 3행은 값 비교 횟수를 0으로 시작한다.
- 4행은 입력에서 번호를 하나씩 꺼낸다. 5행의 False는 아직 같은 번호를 찾지 못했다는 상태다.
- 6행은 지금까지 결과에 저장한 번호를 하나씩 꺼낸다. 입력 전체가 아니라 이미 남긴 번호만 확인한다.
- 7행은 비교 직전에 횟수를 하나 늘린다. += 1은 현재 값에 1을 더해 다시 저장한다는 뜻이다.
- 8행의 ==는 두 값이 같은지 묻는다. 같으면 9행에서 찾았다는 상태를 True로 바꾼다.
- 10행의 break는 가장 가까운 반복을 끝낸다. 같은 번호를 찾았으므로 결과 리스트를 더 뒤질 필요가 없다. 바깥쪽 입력 반복은 계속된다.
- 11행은 같은 번호를 찾지 못했는지 확인한다. 그렇다면 12행의 append가 번호를 결과 끝에 추가한다.
- 13행은 결과 리스트와 비교 횟수를 함께 돌려준다. return을 만나면 이번 함수 실행이 끝난다.
- 16행은 집합 방식의 함수를 정의한다. 17행은 출력 순서를 담을 리스트, 18행은 본 번호를 담을 빈 집합을 만든다.
- 19행은 포함 요청 횟수를 0으로 시작한다. 20행은 번호를 하나씩 읽고, 21행은 요청 횟수를 하나 늘린다.
- 22행의 not in은 집합에 그 번호가 없는지 묻는다. 집합 내부에서 어떤 탐색이 일어나는지는 이 코드에 드러나지 않는다.
- 23행은 처음 본 번호를 집합에 추가한다. 24행은 같은 번호를 결과 리스트 끝에도 추가해 등장 순서를 남긴다.
- 25행은 결과와 요청 횟수를 돌려준다. 리스트 방식과 반환하는 형태가 같아서 나란히 확인하기 쉽다.
- 28행은 확인 사례를 실행하는 함수다. 29~33행의 각 묶음에는 입력과 예상 결과가 들어 있다.
- 34행은 각 묶음에서 입력과 예상 결과를 나누어 받는다. 35~36행은 같은 입력을 두 방식에 전달한다. 이 확인에서는 받은 횟수를 사용하지 않는다.
- 37행의 !=는 서로 다른지 묻는다. or는 두 조건 중 하나라도 참인지 확인한다. 어느 결과든 예상과 다르면 38행에서 오류를 발생시켜 실행을 멈춘다.
- 41행은 프로그램의 전체 흐름을 묶는 main 함수를 정의한다. 42행은 표를 출력하기 전에 작은 사례를 확인한다.
- 43행은 비교할 네 입력 크기를 정한다. 44행은 표의 열 이름을 출력한다.
- 45행은 크기를 하나씩 선택한다. 46행의 range(size)는 0부터 size 직전까지의 정수를 제공하고, list는 이를 리스트로 만든다. 따라서 번호는 모두 다르다.
- 47~48행은 두 방식을 실행해 결과와 횟수를 각각 받는다. 49~50행은 모든 번호와 그 순서가 보존되었는지 확인한다.
- 51~53행은 표 한 행을 출력한다. f로 시작하는 문자열에서는 중괄호 안의 값을 글자로 넣는다. len은 결과 리스트의 원소 개수를 구한다.
- 54~55행은 확인한 사례와 측정 범위를 출력한다. 집합 요청 횟수를 내부 비교 횟수로 읽지 않도록 마지막 줄에서도 구분한다.
- 58~59행은 이 파일을 직접 실행했을 때 main을 호출한다. __name__은 Python이 정하는 이름이며, 직접 실행한 파일에서는 "__main__"이라는 값이 된다.
코드를 읽을 때는 세 가지 지점을 짚으면 된다. 횟수가 늘어나는 위치, 결과에 번호가 추가되는 위치, 잘못된 결과를 발견하는 위치다. 각각 “무엇을 세는가”, “무엇을 남기는가”, “어떻게 확인하는가”에 답한다. 문법을 처음부터 모두 외우지 않아도 이 질문을 따라가며 코드의 역할을 읽을 수 있다.
실행 결과
터미널에서 main.py를 저장한 폴더로 이동한 뒤 다음 명령을 실행한다.
python3 main.py
예상 출력은 다음과 같다.
입력 개수 | 리스트 값 비교 | 집합 포함 요청 | 결과 개수
4 | 6 | 4 | 4
8 | 28 | 8 | 8
16 | 120 | 16 | 16
32 | 496 | 32 | 32
확인: 빈 입력, 반복 번호, 등장 순서
주의: 집합 내부 비교 횟수는 세지 않습니다.
입력 개수가 8에서 16으로 늘면 리스트 값 비교는 28에서 120으로 늘어난다. 집합 포함 요청은 8에서 16으로 늘어난다. 이 표에서는 모든 번호가 서로 다르므로 결과 개수도 입력 개수와 같다. 중복 제거가 불필요한 입력을 일부러 사용한 이유는, 새 번호가 기존 번호를 끝까지 확인하는 모습을 드러내기 위해서다.
직접 실행한 뒤 6과 28을 손으로도 확인해 본다. 네 번호는 0 + 1 + 2 + 3회, 여덟 번호는 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7회 비교한다. 출력 숫자와 코드의 증가 위치, 절차의 설명이 서로 맞아야 한다. 출력만 보는 것보다 세 가지를 연결해서 보는 편이 AI 답을 판단하는 근거가 된다.
이 프로그램은 실행 속도를 측정하지 않는다. 컴퓨터 상태에 따라 바뀌는 초 단위 결과 대신, 선택한 일을 몇 번 수행했는지 보여 준다. 횟수는 증가 구조를 이해하는 데 유용하지만 실제 서비스의 응답 시간을 대신하지는 않는다. 파일 읽기나 화면 표시처럼 여기서 제외한 작업이 전체 시간에 영향을 줄 수도 있다.
실무에서 자주 틀리는 것
결과를 집합으로 바꾸면 순서도 보존된다고 생각한다
다음 코드는 중복을 제거하지만 처음 등장한 순서를 보장하는 방법이 아니다. 지금 실행에서 원하는 순서가 나왔더라도 그 관찰만으로 순서 조건을 충족했다고 판단해서는 안 된다.
records = [3, 1, 3, 2]
result = list(set(records))
print(result)
고친 코드는 포함 확인에는 집합을 쓰고, 출력 순서에는 리스트를 쓴다. 결과는 3, 1, 2다.
records = [3, 1, 3, 2]
seen = set()
result = []
for record in records:
if record not in seen:
seen.add(record)
result.append(record)
print(result)
자료구조를 바꿀 때는 빨라지는 부분과 함께 결과의 성질도 확인해야 한다. 이 사례에서는 중복 여부와 순서가 각각 확인할 조건이다.
집합 요청 횟수를 내부 비교 횟수라고 표시한다
다음 코드는 포함 여부를 한 번 요청할 때마다 숫자를 늘린다. 그런데 출력 이름은 내부에서 번호끼리 비교한 횟수처럼 되어 있다. 계산이 틀렸다기보다 숫자에 붙인 의미가 틀렸다.
seen = set()
comparisons = 0
for record in [0, 1, 2, 3]:
comparisons += 1
if record not in seen:
seen.add(record)
print("집합 내부 비교:", comparisons)
고친 코드는 실제로 관찰한 일을 이름에 담는다. 집합 내부 작업은 별도로 세지 않았다는 사실도 남긴다.
seen = set()
checks = 0
for record in [0, 1, 2, 3]:
checks += 1
if record not in seen:
seen.add(record)
print("집합 포함 요청:", checks)
print("집합 내부 비교는 측정하지 않았습니다.")
숫자가 출력되면 객관적으로 보이기 쉽다. 그러나 무엇을 한 번으로 세었는지 정의하지 않으면 숫자도 판단을 흐릴 수 있다.
두 방식이 서로 같은 답을 내면 맞다고 결론 낸다
다음 확인 코드는 두 결과가 같은지만 본다. 두 방식이 모두 같은 조건을 놓쳤다면 잘못된 답도 통과한다. 예를 들어 두 결과 모두 등장 순서를 바꾼 상황이다.
list_result = [1, 2, 3]
set_result = [1, 2, 3]
print("통과:", list_result == set_result)
고친 코드는 문제에서 정한 예상 결과와 각각 비교한다. 여기서는 확인 방법만 드러내기 위해 결과를 직접 적었다. 실제 작업에서는 실행한 함수의 결과를 넣는다.
expected = [3, 1, 2]
list_result = [1, 2, 3]
set_result = [1, 2, 3]
print("리스트 통과:", list_result == expected)
print("집합 통과:", set_result == expected)
두 구현의 일치는 유용한 단서지만, 요구한 결과와의 일치도 확인해야 한다. 그래서 완성 코드의 작은 사례에는 예상 결과를 따로 적었다.
반복이 두 겹이면 입력과 관계없이 같은 작업량이라고 생각한다
다음 코드는 중복을 제거한 뒤 결과 개수만으로 값 비교 횟수를 계산한다. 입력을 읽으며 수행한 비교가 계산에서 빠진다.
records = [7, 7, 7, 7]
result = []
for record in records:
if record not in result:
result.append(record)
count = len(result)
estimated = count * (count - 1) // 2
print("값 비교:", estimated)
위 코드는 0을 출력하지만, 뒤의 세 번호는 저장된 7과 비교해야 한다. 고친 코드는 직접 같은지 묻는 위치에서 횟수를 늘린다.
records = [7, 7, 7, 7]
result = []
comparisons = 0
for record in records:
found = False
for saved in result:
comparisons += 1
if record == saved:
found = True
break
if not found:
result.append(record)
print("값 비교:", comparisons)
고친 코드의 비교 횟수는 3이다. n × (n − 1) ÷ 2는 이번 리스트 절차에 서로 다른 번호만 들어올 때의 계산이다. 공식을 적용하기 전에 그 공식이 성립하는 입력 조건을 확인해야 한다.
한눈에 보기
| 개념 | 판단할 질문 | 이 장의 적용 |
|---|---|---|
| 알고리즘 | 어떤 순서로 문제를 해결하는가 | 읽기, 중복 확인, 처음 본 번호 저장 |
| 자료구조 | 값을 어떤 방식으로 저장하고 찾는가 | 리스트로 순서를 담고 집합으로 포함 여부 확인 |
| 시간 복잡도 | 입력이 늘면 작업량이 어떻게 증가하는가 | 서로 다른 번호에서 누적 비교가 커지는 모습 확인 |
| 추상화 | 지금 남길 특징과 그 조건은 무엇인가 | 기록 번호로 중복을 판단한다는 약속 |
| 분해 | 어떤 부분을 따로 확인할 수 있는가 | 두 방식, 결과 확인, 표 출력을 나누기 |
| 측정 범위 | 숫자가 실제로 센 것은 무엇인가 | 리스트 값 비교와 집합 포함 요청을 구분 |
기초 공부의 목표는 용어를 보고 정의를 말하는 데서 끝나지 않는다. “왜 이 코드가 요구를 충족하는가”, “어떤 입력에서 일이 많아지는가”, “이 숫자는 어디까지 설명하는가”에 근거를 붙일 수 있어야 한다. 당장 모든 내부 구조를 구현할 필요는 없다. 지금 판단하려는 코드에 필요한 개념부터 연결하면 된다.
AI가 낸 답을 받아들일 때도 같은 순서로 생각할 수 있다. 결과 조건을 읽고, 절차를 따라가고, 작은 입력으로 실행해 보고, 작업량이 늘어나는 위치를 찾는다. 다음 장에서는 이렇게 판단 근거를 갖춘 상태에서, 이해를 돕도록 AI에게 요청할 때와 결과물을 만들도록 맡길 때의 접근을 구분한다.
연습 문제
- 완성 코드의 입력 크기에 5를 추가한다고 하자. 서로 다른 번호 다섯 개에 대해 리스트 값 비교, 집합 포함 요청, 결과 개수는 각각 얼마인가. 리스트 값 비교는 덧셈으로 이유를 적는다.
- 두 함수에 [2, 2, 1, 2]를 전달하면 결과와 횟수는 어떻게 되는가. 리스트 방식에서는 번호를 하나씩 읽을 때 비교한 횟수를 적는다.
- 표의 32개 행을 보고 “집합 방식은 모든 상황에서 정확히 15.5배 빠르다”고 설명했다. 이 결론을 받아들일 수 없는 이유를 두 가지 이상 적는다.
- 중복의 기준을 기록 번호에서 “학습 제목이 같으면 같은 기록”으로 바꾸려 한다. 자료구조를 선택하기 전에 확인할 질문 두 가지를 적는다.
정답과 해설
리스트 값 비교는 10회, 집합 포함 요청은 5회, 결과 개수는 5개다. 리스트에서는 0 + 1 + 2 + 3 + 4회를 비교한다. 새 번호가 들어올 때마다 앞서 저장한 번호가 하나씩 늘어난다.
두 결과는 모두 [2, 1]이다. 리스트 값 비교는 총 3회다. 첫 2는 빈 결과에 들어가므로 0회, 두 번째 2는 저장된 2와 비교하므로 1회다. 1은 저장된 2와 비교한 뒤 추가되므로 1회다. 마지막 2는 결과의 첫 2와 비교하고 멈추므로 1회다. 집합 포함 요청은 입력마다 한 번이므로 4회다. 이 사례는 두 횟수의 숫자만 비교해 실제 속도를 판단할 수 없다는 점도 보여 준다.
리스트의 496은 직접 수행한 값 비교 횟수이고, 집합의 32는 포함 여부 요청 횟수라서 같은 비용의 연산이 아니다. 집합 내부 탐색과 해시 계산은 표에 포함되지 않는다. 또한 표는 서로 다른 정수 번호라는 특정 입력을 사용하며 실제 실행 시간을 재지 않았다. 따라서 횟수의 비율을 모든 상황의 속도 비율로 바꿀 수 없다.
예를 들어 “제목이 같지만 날짜가 다른 학습도 하나로 합칠 것인가”와 “제목 앞뒤의 공백이 달라도 같은 제목으로 볼 것인가”를 확인할 수 있다. 먼저 같은 기록의 의미를 정해야 한다. 그 뒤 그 기준을 코드가 표현하는지 살핀다. 자료구조는 정해진 기준을 효율적으로 적용하도록 도울 수 있지만, 무엇을 같은 것으로 볼지 대신 결정하지는 않는다.