Walking the References

개념 확인: Linked-List Operations


참조를 따라가는 비용과 연결을 고치는 순서를 추적한다.

문제 1

순회와 i번째 접근

문제 1~4는 다음 단일 연결리스트에서 각각 따로 시작한다.

head → 12 → 5 → 17 → 8 → 30 → None 
def size(head):
    count = 0
    current = head
    while current is not None:
        count += 1
        current = current.next
    return count
  1. size(head)의 while 반복이 몇 번 실행되고, 반복이 끝나는 조건이 무엇인지 적는다.
  2. 인덱스 3인 노드에 가려면 head에서 next를 몇 번 따라가야 하고, 그 노드의 값은 무엇인지 적는다.
  3. 배열과 연결리스트에서 전체 순회가 끝나는 조건을 각각 적는다.
  4. 목록의 각 원소마다 목록 전체를 다시 훑어 같은 값의 개수를 세는 작업의 비용을 적는다.
해설 펼치기
  1. 노드 5개를 하나씩 방문하므로 5번 실행하고, current가 None이 되면 끝난다.
  2. next를 3번 따라가 값 8인 노드에 도착한다. i번째에 가는 비용은 Θ(1+i)다.
  3. 배열은 인덱스가 원소 수 n에 도달하면, 연결리스트는 current가 None이 되면 끝난다. 배열은 경계를 수로 비교하고 연결리스트는 끝을 나타내는 참조를 확인한다.
  4. 순회 안에서 다시 순회하므로 n·n, 곧 Θ(n²)이다. 순회를 두 번 연속하는 것은 Θ(n)이지만 중첩하면 증가율이 바뀐다.
문제 2

탐색의 비교 횟수

문제 1의 단일 연결리스트에서 새로 시작한다.

search(head, target)은 앞에서부터 값을 비교하고 같으면 True를, None에 이르면 False를 반환한다.

  1. target이 12, 17, 30, 99일 때 각각의 비교 횟수와 반환값을 적는다.
  2. 원소가 n개일 때 최선·최악의 비교 횟수와, 찾는 값이 목록에 있고 위치가 고르게 분포할 때의 평균 비교 횟수를 적는다.
  3. 값이 목록에 없음을 확인하려면 모든 노드를 비교해야 하는 이유를 적는다.
해설 펼치기
  1. 12는 1번 비교해 True, 17은 3번 비교해 True, 30은 5번 비교해 True, 99는 5번 비교한 뒤 None에 이르러 False다.
  2. 최선은 첫 노드가 답인 1번, 최악은 마지막이거나 없을 때 n번, 평균은 (n + 1)/2번이다.
  3. 정렬되지 않은 목록에서는 아직 비교하지 않은 노드에 찾는 값이 있을 수 있으므로, 없다고 결론 내리려면 None까지 모두 확인해야 한다.
문제 3

연결을 고치는 순서

문제 1의 단일 연결리스트에서 새로 시작한다.

  1. 값 5인 노드(previous)와 값 17인 노드(current) 사이에 새 노드 31을 넣는다. 고쳐야 할 참조 두 개의 올바른 실행 순서를 한 가지 적는다.
  2. 한 구현이 previous.next = new를 먼저 실행한 뒤 new.next = previous.next를 실행했다. 결과가 어떻게 되는지 적는다.
  3. 맨 앞에 새 노드 40을 넣을 때 head = new를 먼저 실행하면 무엇을 잃는지 적고, 올바른 순서를 적는다.
  4. 인덱스 i에 삽입하는 비용을 자리 찾기와 연결 고치기로 나누어 적는다.
해설 펼치기
  1. new.next = current로 31이 17을, previous.next = new로 5가 31을 가리키게 한다. current가 17을 계속 가리키므로 두 대입의 순서를 바꾸어도 같은 연결이 된다.
  2. previous.next가 이미 새 노드이므로 new.next가 자기 자신을 가리키는 순환이 생기고, 17 이후의 노드에 도달할 수 없게 된다.
  3. 기존 첫 노드 12를 가리키는 참조가 하나도 남지 않아 목록 전체를 잃는다. new.next = head를 먼저 실행한 뒤 head = new로 옮긴다.
  4. 자리 찾기가 순회라 Θ(1+i), 연결 고치기는 참조 두 개라 Θ(1)이므로 전체 Θ(1+i)다.
문제 4

삭제와 첫 노드

문제 1의 단일 연결리스트에서 새로 시작한다.

  1. 값 17인 노드를 삭제할 때 previous와 current가 가리키는 노드와, 바꿀 참조 하나를 적는다.
  2. 삭제 뒤 목록을 head부터 적고, 노드 17이 목록에서 어떤 상태가 되는지 적는다. 이어 마지막 노드 30을 지울 때 previous.next를 무엇으로 바꾸는지 적는다. 목록 객체가 tail도 저장한다면 tail은 어디로 옮기는가?
  3. 첫 노드 12를 삭제할 때 previous가 무엇이고 대신 무엇을 고쳐야 하는지 적는다.
해설 펼치기
  1. previous는 5, current는 17을 가리키고 previous.next = current.next로 5가 8을 가리키게 한다.
  2. 17을 지운 뒤에는 head → 12 → 5 → 8 → 30 → None이다. 노드 17은 목록에서 더 이상 도달할 수 없다. 이어 30을 지울 때는 previous가 8을 가리키므로 previous.next = None으로 바꾸고, tail을 저장한다면 8로 옮긴다.
  3. 첫 노드에는 앞 노드가 없어 previous가 None이다. head = head.next로 head를 고쳐야 한다. 이 경우를 따로 처리하지 않으면 첫 노드를 삭제할 수 없다.
문제 5

연산별 비용

head만 저장한 단일 연결리스트에 원소가 1,000개 있다.

  1. 다음 연산마다 비용을 Θ 표기로 적고, 비용을 결정하는 작업을 적는다. 맨 앞에 삽입, 인덱스 700에 삽입, 인덱스 700에서 삭제, 목록에 없는 값 탐색.
  2. 인덱스 700에 삽입할 때 연결을 고치는 일은 참조 두 개인데도 전체 비용이 Θ(1+i)인 이유를 적는다.
  3. 합을 구하는 순회와 최댓값을 구하는 순회를 따로 한 번씩 하는 작업과, 각 원소마다 목록 전체를 다시 훑는 작업의 비용을 비교한다.
해설 펼치기
  1. 맨 앞에 삽입은 참조 두 개만 고치므로 Θ(1)이다. 인덱스 700에 삽입과 삭제는 자리를 찾는 순회가 비용을 정해 Θ(1+i)다. 없는 값 탐색은 항상 모든 노드와 비교하므로 1,000번 비교해 Θ(n)이다.
  2. 비용은 가장 오래 걸리는 단계가 정한다. 인덱스 700의 앞 노드까지 head부터 next를 따라가는 순회가 i에 비례하고, 연결 고치기 Θ(1)은 그보다 작다.
  3. 순회를 연속해서 두 번 하면 2n이라 Θ(n)이다. 각 원소마다 다시 훑으면 순회 안에 순회가 있어 n·n, 곧 Θ(n²)이다. 상수 배는 증가율을 바꾸지 않지만 중첩은 바꾼다.