Data Structure/C++ STL

[C++ STL] 1.6 std::list

lumana 2024. 6. 15. 18:32

std::list


복습

  • std::forward_list는 기본적인 형태로 구현된 연결 리스트임
    • 리스트 끝에 원소 추가, 역방향 이동, 리스트 크기 반환 등의 기능을 제공하지 않음
    • 빠른 원소 삽입이 필요한 모든 경우에 어울리는 container는 아니다.

  • std::forward의 단점을 보완하기 위해 c++에서 std::list를 제공한다
  • std::list
    • 양쪽 방향으로 연결된 리스트, 즉 이중 연결 리스트(doubly-linked-list) 구조로 되어 있다.
    • 따라서 더 많은 기능을 제공한다. (메모리는 조금 더 많이 사용한다)
// 이중 연결 리스트 노드 기본 형태
struct doubly_linked_list_node
{
    int data;
    doubly_linked_list_node* next;
    doubly_linked_list_node* prev;
};
  • doubly-lined-list의 경우 이전 노드를 가리키는 포인터가 추가로 있기 때문에 역방향으로 이동이 가능하다
  • 맨 마지막 원소와 리스트 크기를 따로 저장하여 push_back() 또는 size() 함수를 지원할 수 있다
  • std::forward_list처럼 템플릿 매개변수로 사용자 정의 할당자를 지정할 수 있다

std::list 멤버 함수

  • 전반적으로 std::forward_list와 유사하지만, 약간의 차이가 존재한다
    • forward_list의 insert_after()와 emplace_after()는 list의 insert()와 emplace()에 대응
  • 원소 이동을 역방향으로 할 수 있으므로 원소 삽입을 위해 특정 원소의 이전 원소 반복자를 제공하지 않아도 된다
    • 대신 새로운 원소가 삽입될 위치를 가리키는 반복자를 함수 인자로 전달한다

example: std::list 삽입/삭제 함수

#include <iostream>
#include <list>

int main()
{
    std::list<int> list1 = { 1, 2, 3, 4, 5 };

    // { 1, 2, 3, 4, 5, 6 }
    list1.push_back(6);

    // { 1, 0, 2, 3, 4, 5, 6 }
    list1.insert(next(list1.begin()), 0);

    // { 1, 0, 2, 3, 4, 5, 6, 7 }
    list1.insert(list1.end(), 7);

    // { 1, 0, 2, 3, 4, 5, 6 }
    list1.pop_back();

    std::cout << "삽입 & 삭제 후 리스트 : ";

    for(auto i : list1)
        std::cout << i << " ";

    return 0;
}
  • prev 포인터와 next 포인터를 모두 수정해야 하므로, std::foward_list에 비하면 두 배의 연산량이 필요하다

std::list 기타 함수

  • remove(), remove_if(), sort(), unique(), reverse() 등의 함수도 std::list에서 제공된다
    • std::foward_list와 같은 형태로 동작한다

양방향 반복자

  • std::list의 경우 역방향으로의 연산이 필요한 경우에는 역방향 반복자를 사용할 수 있다
  • random access iterator 보다는 유연하지 않다
    • 어느 방향으로 원하는 만큼 이동할 수 있지만, random access iterator 처럼 특정 위치로 한 번에 이동하는 것이 불가능함
    • 특정 위치로 이동하는 작업은 선형 시간 복잡도를 가진다
  • 어느 방향으로 이동할 수 있으므로 양방향 반복자(bidirectional iterator)라고 부른다

반복자 무효화

  • 반복자는 메모리 주소를 가리키는 포인터를 이용하여 구현되었기 때문에 컨테이너가 변경될 경우 제대로 동작하지 않을 수 있다
    • 컨테이너가 변경되어 특정 노드 또는 원소의 메모리 주소가 바뀌면 반복자가 무효화될 수 있고, 예측할 수 없는 동작이 발생할 수 있다
  • vector::push_back() 함수의 경우 capacity에 따라 새로 메모리 공간을 할당하고 새 메모리 공간에 복사는 작업을 한다
    • 이 경우 기존에 사용하던 모든 반복자, 포인터, 참조는 모두 무효화된다
  • vector::insert() 함수를 수행할 때 메모리 재할당이 필요한 경우라면 이 경우에도 기존의 반복자, 포인터, 참조는 모두 사용하면 안 된다
  • 메모리 재할당 없이 원소를 삽입하는 경우 원소 삽입 위치 이후에 있는 원소를 모두 이동해야 하므로 이 위치를 가리키는 반복자는 모두 무효화 된다
  • 연결 리스트에서는 삽입/삭제 동작에서 원소를 이동할 필요가 없으므로 반복자가 무효화되지 않는다
    • 다만 특정 원소를 삭제하는 경우, 삭제되는 원소를 가리키던 반복자는 당연히 무효화된다. 나머지는 그대로 사용 가능
std::vector<int> vec = { 1, 2, 3, 4, 5 };

// v_it[4]는 vec[4] 원소를 가리킴
auto v_it4 = vec.begin() + 4;

// v_it4 반복자는 무효화됨, v_it4를 이용하여 원소에 접근하면 예상하지 못한 에러가 발생할 수 있다
vec.insert(vec.begin() + 2, 0);
std::list<int> lst = { 1, 2, 3, 4, 5 };

auto l_it4 = next(lst.begin(), 4);

// l_it4 반복자는 유효함
lst.insert(next(lst.begin(), 2), 0);

'Data Structure > C++ STL' 카테고리의 다른 글

[C++ STL] 1.8 컨테이너 어댑터(Container Adaptor)  (0) 2024.06.15
[C++ STL] 1.7 std::dequeue  (0) 2024.06.15
[C++ STL] 1.5 Iterator(반복자)  (0) 2024.06.15
[C++ STL] 1.4 std::forward_list  (0) 2024.06.14
[C++ STL] 1.3 std::vector  (0) 2024.06.14