When the Array Fills Up

개념 확인: Dynamic Arrays


용량이 찰 때의 재할당 비용을 세고 상각 비용으로 끝 추가를 설명한다.

문제 1

재할당이 일어나는 순간

문제 1과 3의 동적 배열은 원소 수 n이 용량(capacity)과 같을 때 추가하려 하면 용량을 max(1, 2n)으로 늘리고, 삭제 뒤 capacity ≥ 3n이면 용량을 max(1, 2n)으로 줄인다.

용량 1인 빈 동적 배열의 끝에 원소 9개를 차례로 추가한다.

  1. 재할당이 일어나는 추가가 몇 번째 호출인지, 그때 복사하는 원소 수와 새 용량을 모두 적는다.
  2. 9번 추가한 뒤의 용량과 원소 수를 적는다.
  3. 이어서 끝에서 원소를 하나씩 삭제할 때, 처음으로 용량이 줄어드는 삭제가 몇 번째인지와 줄어든 용량을 적는다.
  4. 줄인 뒤에도 빈 칸을 남겨 두는 이유를 적는다.
해설 펼치기
  1. 2번째 호출(원소 1개 복사, 용량 2), 3번째 호출(2개 복사, 용량 4), 5번째 호출(4개 복사, 용량 8), 9번째 호출(8개 복사, 용량 16)이다.
  2. 용량 16, 원소 수 9다.
  3. 삭제할 때마다 n이 8, 7, 6, 5로 준다. 4번째 삭제 뒤 n = 5에서 16 ≥ 3·5 = 15가 되어 용량을 max(1, 10) = 10으로 줄인다.
  4. 줄인 직후에도 여유 칸이 있어야 추가와 삭제가 번갈아 일어날 때 매번 재할당하지 않는다. 용량과 원소 수는 다르며, 원소가 절반 아래로 내려가자마자 줄이지 않는다.
문제 2

세 칸씩 늘리기와 두 배로 늘리기

전략 B에는 문제 1의 용량 증가 규칙을 쓰고, 전략 A에는 아래의 별도 증가 규칙을 쓴다.

용량 1인 빈 배열에 원소 12개를 추가한다. 늘릴 때마다 용량에 3을 더하는 전략 A와 용량을 두 배로 만드는 전략 B를 비교한다.

  1. 전략 A와 B 각각의 재할당 뒤 용량을 순서대로 적는다.
  2. 두 전략의 누적 복사 원소 수를 구한다.
  3. 추가 횟수 m이 커질 때 두 전략의 총 복사량이 각각 어떻게 증가하는지 적고, A에서 늘리는 폭을 크게 잡아도 결론이 바뀌지 않는 이유를 설명한다.
해설 펼치기
  1. A: 4, 7, 10, 13. B: 2, 4, 8, 16.
  2. A는 1 + 4 + 7 + 10 = 22개, B는 1 + 2 + 4 + 8 = 15개를 복사한다.
  3. A처럼 고정 폭 c로 늘리면 복사량의 합이 등차수열이라 약 m²/(2c), 곧 Θ(m²)이다. c를 크게 잡으면 상수만 줄 뿐 m²으로 증가하는 것은 같다. B처럼 두 배로 늘리면 등비수열의 합이 2m보다 작아 Θ(m)이다.
문제 3

상각 비용

문제 1의 동적 배열을 쓴다.

추가 한 번의 비용을 쓰기 1회와 그 호출에서 복사한 원소 수의 합으로 센다.

  1. 문제 1의 9번 추가에서 총비용과 한 번당 비용을 구한다.
  2. 추가 횟수 m에 관계없이 한 번당 상각 비용이 3보다 작은 이유를 적는다.
  3. 다음 문장이 맞는지 판단하고 고친다. “상각 비용은 입력이 고르게 들어온다고 가정하고 구한 평균 경우의 비용이다.”
  4. 끝에 추가하는 연산을 최악의 경우 Θ(n)만으로 설명하면 부족한 이유를 적는다.
해설 펼치기
  1. 쓰기 9회와 복사 15개를 더해 총비용 24, 한 번당 24/9 ≈ 2.67이다.
  2. m번 추가하면 쓰기는 m회이고 복사량의 합 1 + 2 + 4 + ⋯는 2m보다 작다. 총비용이 3m보다 작으므로 한 번당 3보다 작다.
  3. 틀렸다. 상각 비용은 확률 분포를 가정하지 않고, 어떤 순서로 m번 호출해도 총비용이 m×c를 넘지 않게 하는 한 번당 비용의 상한 c다. 입력 분포를 가정하는 것은 평균 경우다.
  4. 재할당하는 한 번은 Θ(n)이지만 그런 호출은 드물다. 한 번의 최악값만 보면 연속한 추가의 총비용 Θ(m), 곧 상각 Θ(1)을 설명하지 못한다.
문제 4

Python list 측정 읽기

한 컴퓨터에서 빈 목록에 append를 반복하며 sys.getsizeof를 기록했더니 원소 수 5~8에서 모두 120, 9~16에서 모두 184였다. 같은 컴퓨터에서 원소 200,000개를 append로 만드는 전체 시간은 9.6 ms였다.

  1. 원소 수가 5에서 8로 늘 동안 메모리 크기가 그대로인 구간은 무엇을 뜻하는지 적는다.
  2. append 한 번당 시간을 나노초(ns)로 구한다.
  3. 이 두 측정만으로 CPython의 증가 배수가 정확히 두 배라고 단정할 수 없는 이유를 적는다.
  4. insert(0, x)를 같은 횟수만큼 반복하면 append와 달리 비용이 어떻게 되는지 적는다.
해설 펼치기
  1. 이미 확보해 둔 여유 칸에 원소를 넣어 재할당이 일어나지 않은 구간이다.
  2. 9.6 ms/200,000 = 0.000048 ms = 48 ns다.
  3. 메모리 크기에는 목록 객체의 기본 크기도 들어 있고, 수치는 실행 환경에 따라 달라질 수 있다. 몇 번의 증가 구간만으로 구현의 정확한 증가 규칙을 알 수 없다.
  4. insert(0, x)는 매번 기존 원소를 한 칸씩 밀어야 하므로 한 번에 Θ(n)이다. 재할당과 관계없이 이동 비용이 남는다.
문제 5

원형 배열

용량 8인 원형 배열에서 첫 원소의 칸이 j = 6, 원소 수가 n = 4다. i번째 원소는 a[(j + i) mod 8]에 있다. 3과 4는 처음 상태에서 각각 따로 생각한다.

  1. 네 원소가 들어 있는 실제 칸 번호를 순서대로 적는다.
  2. get(2)가 읽는 실제 칸을 적는다.
  3. 끝에 원소를 하나 추가하면 새 원소가 들어가는 실제 칸을 적는다.
  4. 앞에서 원소를 하나 삭제하면 원소를 옮기는지 적고, 삭제 뒤의 j와 n을 적는다.
  5. 원소 수가 용량 8에 도달해 재할당하면 새 배열에서 첫 원소의 칸과 j가 어떻게 되는지 적는다.
해설 펼치기
  1. (6 + 0) mod 8 = 6, 7, (6 + 2) mod 8 = 0, 1이므로 칸 6, 7, 0, 1이다.
  2. (6 + 2) mod 8 = 0이므로 칸 0이다.
  3. (6 + 4) mod 8 = 2이므로 칸 2이고, 추가 뒤 n = 5다.
  4. 원소를 옮기지 않는다. 칸 6의 원소를 돌려주고 j = (6 + 1) mod 8 = 7, n = 3이 된다.
  5. j부터 읽은 순서대로 새 배열의 0번 칸부터 복사하므로 첫 원소가 0번 칸에 오고 j = 0이 된다. 원형 배열이어도 용량이 차면 Θ(n) 재할당이 필요하다.