Giving Up Contiguity

개념 확인: Linked-List Representation


연속 저장 대신 노드와 참조로 순서를 표현하는 방식을 추적한다.

문제 1

참조를 따라 순서 읽기

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

a = Node(7)
b = Node(3)
c = Node(9)
a.next = c
c.next = b
  1. 노드를 만든 순서와 a에서 next를 따라 읽은 값의 순서를 각각 적는다.
  2. 마지막 노드와 그 노드의 next 값을 적는다.
  3. 배열과 연결리스트에서 ‘다음 원소’를 찾는 단서가 각각 무엇인지 적는다.
해설 펼치기
  1. 만든 순서는 7, 3, 9이지만 a에서 따라 읽으면 a.next가 c, c.next가 b이므로 7, 9, 3이다.
  2. 마지막 노드는 값이 3인 b이고 b.next는 None이다.
  3. 배열은 바로 옆 칸, 곧 메모리 위치가 다음 원소를 정한다. 연결리스트는 노드의 next가 가리키는 곳이 다음 원소다.
문제 2

참조를 대입해도 노드는 하나

문제 1의 Node 클래스를 쓴다.

u = Node(5)
v = u
v.data = 8
w = Node(8)
  1. u.data, u is v, u is w, u.data == w.data의 결과를 적는다.
  2. 이 코드가 만든 노드 객체는 몇 개인지 적는다.
  3. v = u를 ‘노드를 복사한다’고 읽으면 어떤 판단이 틀리는지 적는다.
해설 펼치기
  1. u.data는 8, u is v는 True, u is w는 False, u.data == w.data는 True다.
  2. Node(...)를 두 번 불렀으므로 2개다. u와 v는 같은 노드를 가리킨다.
  3. v.data = 8이 u에는 영향을 주지 않는다고 잘못 판단한다. v = u는 같은 객체의 참조를 복사하므로 한쪽에서 바꾼 값이 다른 쪽에서도 보인다. 값이 같은 것(==)과 같은 객체인 것(is)도 구분한다.
문제 3

목록 객체의 head, tail, n

목록 객체는 head, tail, n을, 각 노드는 data, next를 저장한다.

  1. 빈 목록일 때 head, tail, n을 적는다.
  2. 원소 하나를 추가한 뒤 head와 tail의 관계와 n을 적는다.
  3. 원소가 하나인 목록에서 그 노드를 목록에서 빼며 head만 None으로 바꾸고 tail은 그대로 두었다. 어떤 문제가 남는지 적는다. 삭제 전에 saved가 그 노드를 가리켰다면, 삭제 뒤 노드 객체의 존재와 목록 소속은 각각 어떻게 되는가?
  4. tail과 n을 저장하지 않으면 끝 노드를 찾거나 원소 수를 셀 때 어떤 일이 필요한지 적는다.
해설 펼치기
  1. head와 tail은 모두 None, n은 0이다.
  2. head와 tail이 같은 새 노드를 함께 가리키고 n은 1이다.
  3. tail이 목록에서 빠진 노드를 계속 가리킨다. tail도 None으로, n도 0으로 바꾸어야 한다. saved가 남아 있다면 노드 객체는 여전히 존재하지만, head에서 도달할 수 없으므로 목록에는 속하지 않는다.
  4. tail이 없으면 head부터 next를 따라가 끝 노드를 찾아야 하고, n이 없으면 셀 때마다 모든 노드를 따라가야 한다. 둘 다 원소 수에 비례하는 작업이다.
문제 4

두 표현과 두 언어

다음 문장이 맞는지 판단하고, 틀리면 고친다.

  1. 연결리스트도 배열처럼 만들 때 용량을 정하고 칸을 한꺼번에 확보한다.
  2. 연결리스트의 노드들은 메모리에서 이어져 있어야 순서가 유지된다.
  3. Python에서 노드 참조에 1을 더하면 다음 노드로 이동할 수 있다.
  4. C의 NULL과 Python의 None은 가리키는 노드가 없음을 나타낸다.
  5. Python에서는 어떤 이름으로도 도달할 수 없게 된 노드를 실행 환경이 회수한다.
해설 펼치기
  1. 틀렸다. 연결리스트는 원소를 추가할 때마다 노드를 하나 만들므로 용량을 미리 정하지 않는다.
  2. 틀렸다. 순서는 next 참조가 정하므로 노드가 메모리에서 이어져 있을 필요가 없다.
  3. 틀렸다. Python 참조는 포인터 산술을 제공하지 않는다. 다음 노드는 next를 따라가야 한다.
  4. 맞다.
  5. 맞다. C에서는 프로그래머가 메모리 회수를 관리한다.
문제 5

같은 연산, 다른 내부 작업

원소 8개를 담은 목록을 배열과 연결리스트로 각각 표현했다.

  1. 배열에서 인덱스 5의 원소를 읽을 때와 연결리스트에서 인덱스 5의 원소를 찾을 때 각각 어떤 내부 작업을 하는지 적는다.
  2. 배열에서 인덱스 3에 새 원소를 넣으려면 어떤 추가 작업이 필요한지 적고, 옮기는 원소 수를 구한다.
  3. 배열의 빠른 인덱스 접근과 가운데 삽입의 원소 이동이 같은 성질에서 나온다는 말의 뜻을 적는다.
  4. 원소를 추가하거나 삭제할 때 배열의 칸과 연결리스트의 노드가 각각 어떻게 되는지 적는다.
  5. 두 표현 가운데 하나를 고를 때 무엇을 기준으로 하는지 적는다.
해설 펼치기
  1. 배열은 시작 주소와 인덱스로 주소를 계산해 한 칸을 바로 읽는다. 연결리스트는 head부터 next 참조를 차례로 따라가야 한다.
  2. 인덱스 3에 빈 칸을 만들려고 뒤쪽 원소를 한 칸씩 옮겨야 한다. n − i = 8 − 3 = 5개다.
  3. 원소가 연속한 칸에 있기 때문에 주소를 계산해 바로 접근할 수 있고, 같은 이유로 가운데에 빈 칸을 만들려면 뒤쪽 원소를 옮겨야 한다.
  4. 배열은 만들 때 용량만큼 칸을 확보하고 추가는 빈 칸에 쓰며 삭제해도 칸은 남는다. 연결리스트는 추가할 때 노드를 하나 만들고, 삭제하면 그 노드는 목록의 연결에서 빠진다. 그 노드를 가리키는 다른 참조가 없으면 Python 실행 환경이 회수한다.
  5. 자주 쓰는 연산에 필요한 내부 작업과 그 비용을 비교해 고른다. 같은 목록 ADT라도 표현에 따라 작업이 달라진다.