알고리즘/이론공부

연결 리스트 - 반복자(Iterator)

멍멍댕댕구 2024. 11. 5. 11:00

연결 리스트에서 같은 위치에 여러번 삽입/삭제가 일어나는 경우 해당 위치를 기억하고 싶을 때는 어떻게 해야 할까요? 배열에서의 index와 같은 역할을 하는 것이 있으면 편하게 사용할 수 있지 않을까요? 이러한 역할을 해주는 것이 바로 반복자(Iterator) 입니다. 오늘은 연결리스트에서 사용하는 반복자(Iterator)에 대해서 알아보도록 하겠습니다.

 

🔎 반복자란?

반복자는 특정 노드의 위치를 지정해줄 수 있습니다. 연결 리스트 형태에서 삽입/삭제를 하려면 해당 노드의 위치까지 탐색 과정을 먼저 거쳐야 합니다. 이는 $O(n)$의 시간복잡도가 필요한데요, 동일한 위치에 삽입/삭제가 반복적으로 일어나는 경우 반복자를 이용하여 해당 위치를 지정해두면 탐색 없이 삽입/삭제 연산이 가능하게 됩니다.

 

 

🧮 반복자와 삽입/삭제 연산

🔷 Iterator 없이 삽입하는 경우

노드가 2개일 때 맨 뒤에 새로운 노드를 넣는 상황을 가정해 보도록 하겠습니다.

 

마지막 노드를 찾기 위해 Tail까지 탐색을 한 후, 맨 뒤에 노드를 넣어줍니다. 이 때의 시간복잡도는 탐색 $O(n)$, 삽입 $O(1)$ 입니다. 이후에 3번째 노드 앞에 새로운 노드를 넣으려면 어떻게 될까요?

 

3번째 노드인 Tail을 찾기 위해 다시 한번 탐색을 한 후, Tail 앞에 새로운 노드를 생성해야 합니다. 이 때의 시간복잡도 또한 $O(n)$, 삽입 $O(1)$ 입니다.

 

따라서 삽입할 때 마다 탐색으로 $O(n)$의 시간복잡도가 되는 것을 확인할 수 있습니다.

 

🔷 Iterator가 있을 때 삽입하는 경우

그러면 Iterator가 있는 경우 어떻게 달라지게 되는지 알아보겠습니다.

첫 삽입에서는 똑같이 두번째까지 탐색을 실행하여 Tail 뒤에 노드를 넣어주게 됩니다. Iterator는 Head 노드부터 한칸씩 뒤로 이동하여 두번째 노드를 가리키고 있게 됩니다. 이 때의 시간복잡도는 탐색 $O(n)$, 삽입 $O(1)$ 입니다.

이후 3번째 데이터 앞에 새로운 노드를 넣어야 합니다. 이 때, Iterator가 이미 두 번째 노드를 가리키고 있기 때문에 바로 다음 노드가 3번째 노드가 됩니다.

따라서 3번째 노드는 Iterator.next로 구할 수 있습니다. 이 경우 시간복잡도는 탐색(이동) $O(1)$, 삽입 $O(1)$ 이 됩니다.

 

이와 같이 이전 위치와 연관된 곳에 삽입/삭제가 일어나는 경우 Iterator를 이용하면 탐색 시간이 감소합니다.

 

✏️ Iterator의 구현

Iterator의 구현은 복잡한 것이 아니고 변수를 하나 만들어 Head 노드를 저장하면 그것이 Iterator가 됩니다.

/*
  DoublyLinkedList라는 이중 연결 리스트 클래스가 있다고 가정
  pushBack() : 리스트의 가장 뒤에 노드를 삽입하는 함수
  begin() : 리스트의 head를 리턴하는 함수
  end() : 리스트의 tail을 리턴하는 함수
*/

const dll = new DoublyLinkedList(); //이중 연결 리스트 선언

dll.pushBack('test1');
dll.pushBack('test2');
dll.pushBack('test3');

iterator = dll.begin(); //반복자 설정
while(iterator !== dll.begin()) {
    console.log(iterator.data);
    iterator = iterator.next;
}

 

위의 코드를 보면, iterator라는 변수에 리스트의 head 노드를 넣어주었습니다. iterator 변수는 while문을 돌면서 다음 위치의 노드들을 가리킵니다. 노드 자체를 넣어준 것이기 때문에 다른 노드들과 같이 prev, next 를 통해 앞 뒤의 노드로 이동하는것이 가능하며, data 를 통해 저장된 데이터를 반환할 수도 있습니다.