Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

링크드 리스트

링크드 리스트

윤 상배

dreamyun@yahoo.co.kr

고친 과정
고침 0.82003년 3월 1일 23시
최초 문서작성

1. Linked List

1.1. 개요

Linked List(이하 링크드리스트)는 스택, 큐와 함께 가장 기본적인 자료구조(:12)가 된다. 자료구조의 이름과 같이 링크드 리스크는 자료들을 연결된 리스트화 시켜서 관리한다.

링크드 리스트는 각 데이터를 서로 연결해서 관리하며, 이때 각각의 데이터를 노드(node)라고 불리우는 Unit에 넣어서 관리한다. 스택이나 큐와 같은 경우 각 위치에 데이터자체가 저장되는 것과는 약간다르다. 이처럼 데이터를 노드에 넣어서 관리하는 이유는 데이터의 리스트를 유지하기 위해서 데이터외에도 다음 데이터의 위치를 나타낼수 있는 어떤 부가적인 정보가 필요하기 때문이다.

그림 1. 링크드 리스트의 구조

데이터의 위치를 나타내기 위해서 사용할수 있는것은 pointer(:12)가 될것이다. 즉 노드는 데이터와 함께 다음 데이터의 위치정보를 가지고 있는 "포인터"를 포함하게 된다.

링크드리스트의 경우 데이터를 노드단위로 관리하고 데이터와 더불어 포인터까지 가지고 있어야 함으로 필연적으로 자료구조의 사이즈가 좀더(약간) 커질수 밖에 없다. 포인터형의 크기가 4 byte(:12)로 (데이터수 X 4)만큼의 부가적인 공간이 더필요하기 때문이다(이중 링크드리스트라면 (데이터수 X 4 X 2)만큼). 뭐 요즘에야 메모리가격도 싸고 하니까 별문제가 없겠지만, 어쨋든 데이터 낭비가 심할수 있다는 가정하에 사용하기 바란다. 예를들어 int형 숫자를 저장하는 링크드리스트라면 단지 4byte를 저장하기 위해서 4byte의 부가적인 정보가 더 들어가야 할것이기 때문이다. 2배 이상의 메모리 낭비라고 할수 있다.

링크드리스트가 스택 혹은 큐와 비교해서 가지는 특징은 다음과 같다. 스택,큐는 배열로 칭하도록 하겠다.

  1. 배열의 경우 연속적인 공간에 위치함으로 특정위치에 있는 데이터에 접근하고자 할때 O(1)의 시간이 소모된다. 그러나 링크드리스트의 경우 O(N)의 시간이 소모된다. 처음노드 부터 순환을 해야하기 때문이다.

  2. 배열의 경우 처음과 마지막에 데이터를 삽입하는건 매우 쉽고 효율적이지만 중간에 있는 데이터를 삭제하거나, 삽입하는건 상대적으로 어렵고 비효율적이다. 링크드 리스트는 이러한 경우 매우 효율적이다.

  3. 배열의 경우 일단 만들어진 사이즈를 조정할수 없으나(물론 realloc하면되긴 하지만) 링크드리스트는 자유롭게 사이즈를 조정할수 있다.


1.2. 링크드 리스트의 종류

링크드리스트에는 3가지의 종류가 있다. 싱글링크드리스트, 이중링크드 리스트, 환형링크드 리스트인데 각각의 링크드리스트의 구현 특징에 대해서 알아보도록 하겠다.


1.2.1. 싱글링크드리스트

singly Linked List라고도 불리우는 싱글링크드리스트는 가장간단한 구조를 가지는 링크드 리스트이다.

그림 2. 싱글 링크드 리스트의 구조

각 노드는 저장하고자할 데이터와 다음 노드를 가리키는 포인터를 포함한다. 싱글링크드리스트에서 각 노드는 다음노드의 위치를 가리키기 때문에, 우리는 반드시 시작노드의 위치를 알고 있어야만 할것이다. 또한 이전노드의 위치는 알수없도록 되어 있다.


1.2.2. 이중링크드리스트

Double Linked List라고 불리운다. 다음노드를 가리키는 하나의 포인터만 가지고 있는 싱글링크드리스트와는 달리 이전노드를 가리키는 포인터도 가지고 있다. 전방향, 후방향어느 쪽으로든지 순환이 가능한 링크드리스트이다.

그림 3. 이중 링크드 리스트의 구조


1.2.3. 환형링크드리스트

그림 4. 환형 링크드 리스트의 구조

다른 링크드리스트와 다른점이라면 마지막 노드가 NULL이 아닌 처음노드를 가리키는것 정도이고 다른 차이점은 없다.

위의 그림은 싱글링크드리스트의 환형구조를 나타낸 그림이다. 이중링크드리스트에도 동일하게 적용될수 있다.


2. 링크드리스트의 구현

그럼 실제로 링크드리스트를 구현해보도록 하겠다.


2.1. 삽입,삭제

다른 자료구조들과 마찬가지로 링크드리스트 역시 데이터의 삽입, 삭제가 가장 기본이 되는 기능들이다. 우선 링크드리스트상에서 어떻게 데이터의 삽입 삭제가 이루어 질수 있는지에 대해서 알아보자.


2.1.1. 노드 삽입

우선 노드삽입에 대해서 알아보자. 아래는 링크드 리스트에서 노드가 삽입되는 전형적인 과정을 보여준다.

그림 5. 링크드 리스트에서의 노드삽입

위의 그림은 노드2 와 노드5 중간에 새로운 노드4가 삽입될때 어떤 방식으로 삽입이 되는지를 보여준다. 노드2가 기존에 가르키던 로드는 노드5였는데, 여기에 새로운 노드4가 삽입되었음으로 노드4를 가르키도록 포인터를 수정한다. 삽입된 노드4 역시 다음 노드로 노드5를 가르키도록 포인터를 수정한다. 이로써 노드삽입이 끝났다. 매우 간단하다.

만약 추가되는 노드4가 가장 마지막일 경우에는 가르킬 다음노드가 없음으로 NULL을 가르키게 된다.

링크드리스트가 스택이나 큐에 비해서 가지는 장점이라면, 임의의 지점에 대한 노드의 추가및 삭제가 가능하다는 점이다. 그래서 단순히 처음과 마지막에 데이터를 삭제하는 스택이나 큐에 비해서 좀더 다양한 삽입기능을 포함시킬수 있다. 아마도 다음과 같은 삽입기능을 포함시킬수 있을것이다.

표 1. 지원하는 삽입의 종류

push_back가장 뒤에 노드 추가
push_front가장 앞에 노드 추가
insert임의의 위치에 노드 추가


2.1.2. 노드 삭제

노드 삭제역시 그림으로 설명하면 쉽게 이해가능하다.

그림 6. 링크드 리스트에서의 노드삭제

위의 링크드 리스트에서 노드4를 삭제한다고 하자. 노드4가 없어지면 노드4를 가르키던 노드3이 노드5를 가르키도록 포인터를 변경하면 된다. 그걸로 끝이다.(물론 노드4는 free시켜줘야 한다.) 만약 처음노드를 삭제한다면 단지 처음노드만 free시키면 될것이다. 그러나 이때 처음노드가 어디인지 알수 없음으로 처음노드의 위치를 가르키는 다른 포인터 변수를 이용해서 위치를 저장해야 할것이다.

역시 다양한 삭제기능을 제공할것이다.

표 2. 지원하는 삭제의 종류

pop_back가장 뒤의 노드 삭제
pop_front가장 앞의 노드 삭제
erase임의의 위치에 있는 노드 삭제


2.2. 데이터 보여주기

데이터를 삽입하거나 삭제했다면, 실제 노드정보를 보고 싶을때가 있을것이다. 이럴경우 처음노드부터 순환을 시켜야 할것이다. 순환의 방법은 간단하다. 다음노드를 가르키는 포인터가 NULL이 될때까지 계속 노드를 증가시켜 나가면 될것이다. 이외에도 편의를 위해서 처음과 마지막 데이터 정도를 얻어올수 있는 별도의 메서드를 제공하는것도 괜찮은 방법이 될것이다.


2.3. 링크드 리스트 데이터추상 구현

예제의 구현은 C++의 템플릿을 이용해서 구현하게 될것이다. 링크드리스의 구현같은경우 C를 이용한 구현을 선호할수 있는데, 특별히 C++과 다를게 없고, 아무래도 데이터 추상을 위해서는 C++을 이용하는게 효과적임으로 C++을 선택했다. C(:12)를 이용한 링크드리스트의 구현은 동적 메모리할당를 참고하기 바란다.

그리고 싱글링크드리스트를 이용한 구현을 하겠다. 나머지는 사실상 싱글링크드리스트를 약간 변형한 것이고, 싱글링크드리스트가 좀더 구현하기 편하기 때문이다. 싱글링크드리스트의 구현을 이해한다면 다른 링크드리스트의 구현도 간단할것이다.

예제는 엄격한 규칙을 따르지 않으며, 링크드리스트의 개념이해에 촛점을 맞출것이다. 예를들어 특정위치의 노드데이터를 가져오는 경우 아래의 코드는 매우 비효율적이다. 노드의 위칠를 증가시킬때마다 노드의 처음부터 순환하기 때문이다. 이것의 수정방법은 여러가지가 있다. 가장최근의 노드를 기억하게 하는방법과 이터레이터(iterator)개념을 적용하는 방법이 그것이다.

또한 싱글링크드리스트임으로 순방향으로 노드를 찾아가는건 가능하지만 역방향(뒤에서 앞으로) 노드를 찾아가는건 불가능하다. 그럼으로 가장뒤에 있는 노드를 삭제하고자 할때 어쩔수 없이 노드의 처음부터 마지막까지 훑어 나가야 한다. 역시 비효율적이다. 이런 비효율의 문제는 더블링크드리스트를 이용하면 가능할것이다.

예제 : linked.cc

#include <iostream>
#include <string>
#include <iterator>

using namespace std;

template <typename T>
class List
{
    private:
        // 노드갯수
        int         node_num;
        // NODE데이터를 저장할 구조체 
        struct Node
        {
            T    Data;
            Node *NextNode;
        };
        Node *mNode;  // 노드 할당을 위해서 사용한다. 
        Node *SNode;  // 시작노드를 포인트한다.
        Node *NNode;  // 다음노드를 포인트한다.  
        Node *ENode;  // 마지막노드를 포인트한다.

    public:
        // 생성자
        List()
        {
            node_num = 0;
        };

        // 삽입관련 메서드 ------------------------

        // 가장뒤에 노드를 추가한다. 
        // 마지막 노드 정보를 가지고 있음으로 
        // 새로운 노드를 할당하고 기존의 마지막노드가 
        // NULL대신 새로할당된 노드를 가리키게 하면 된다. 
        void push_back(T data)
        {
            mNode = new Node;
            mNode->Data     = data;
            mNode->NextNode = NULL;

            if (node_num == 0)
            {
                SNode = mNode;
                ENode = mNode;
            }
            else
            {
                ENode->NextNode = mNode;
                ENode = mNode;
            }
            node_num++;
        };

        // 가장앞에 노드를 추가한다. 
        // 만약 처음으로 노드가 추가되는거라면 
        // 이 노드는 다음노드로 NULL을 가리키게 된다. 
        // 그렇지 않을경우 기존에 가장 처음에 있던 노드를 
        // 가리키게 된다. 
        void push_front(T data)
        {
            mNode = new Node;
            mNode->Data     = data;
            mNode->NextNode = NULL;
            if (node_num == 0)
            {
                SNode =  mNode;
            }
            else
            {
                mNode->NextNode = SNode;
                SNode =  mNode;
            }
            node_num++;
        };

        // 임의의 위치에 노드를 삽입한다.  
        int insert(T data, int position)
        {
            Node *PNode;
            int i = 0;
            if (position > node_num)
            {
                return -1;
            }

            // 처음과 마지막 위치에 
            // 삽입할때는 이미 만들어진 메서드를
            // 활용한다.
            if (position == 0)
            {
                push_front(data);
                return 1;
            }
            if (position == node_num)
            {
                push_back(data);
                return 1;
            }

            mNode = new Node;
            mNode->Data     = data;
            mNode->NextNode = NULL;

            NNode = SNode;
            while(NNode)
            {
                if (i == position)
                {
                    mNode->NextNode = NNode;
                    PNode->NextNode = mNode;
                    node_num++;
                    break;
                }
                PNode = NNode;
                NNode = NNode->NextNode;
                i++;
            }
            return 1;
        }


        // 삭제 관련 메서드 -------------------------
        // 가장앞에 있는 노드를 삭제
        void pop_front()
        {
            NNode = SNode->NextNode;
            delete SNode;
            SNode = NNode;
            node_num--;
        }

        // 가장뒤에 있는 노드를 삭제
        // 싱글 링크드 리스트라서 가장뒤에 있는 노드를 
        // 지울경우 어쩔수 없이 처음부터 노드를 검색하는 수 
        // 밖에 없다. 
        // 더블링크드 리스트라면 좀더 쉽게 삭제 가능할것이다.  
        void pop_back()
        {
            int i = 0;
            Node *PNode;
            NNode = SNode;

            while(1)
            {
                if(i == node_num - 1)
                {
                    PNode->NextNode = NULL;
                    delete(NNode);
                    break;
                }
                PNode = NNode;
                NNode = NNode->NextNode;
                i++;
            }
            node_num--; 
        }


        // 값 가져오기 메서드 -----------------------

        // 가장앞의 노드에서 데이터를 가져온다.
        T front()
        {
            return SNode->Data; 
        }

        // 가장뒤의 노드에서 데이터를 가져온다.
        T back()
        {
            return ENode->Data;
        }

        // 노드를 순환하면서 모든 데이터를 출력한다.
        void show()
        {
            NNode = SNode;

            while(NNode)
            {
                cout << NNode->Data << endl;
                NNode = NNode->NextNode;
            }
        }

        // 임의의 위치에 있는 노드의 데이터를 
        // 가져온다.  
        // 이 메서드는 비효율적이고 깔끔하지 못하다. 
        // 바꿔보자. 
        T get(int num)
        {
            int i = 0;
            if (num > node_num)
            {
                return NULL;
            }
            NNode = SNode;
            while(i < num)
            {
                NNode = NNode->NextNode;
                i++;
            }
            return NNode->Data;
        }

        // 부가 메서드 ----------------------------- 
        int size()
        {
            return node_num;
        }
};

int main()
{
    List<string> list;

    // 테스트
    list.push_back("A");
    list.push_back("B");
    list.push_back("C");
    list.push_front("D");
    list.insert("100", 1);
    list.push_front("101");

    cout << "Size " << list.size() << endl;
    cout << "First Data is " << list.front() << endl;
    cout << "Last Data is " << list.back() << endl;
    list.show();

    list.pop_front();
    list.show();

    list.pop_back();
    list.show();

    cout <<"GET DATA" << list.size() << endl;
    for (int i =0; i < list.size(); i++)
    {
        cout << list.get(i) << endl;
    }
}
			


3. 결론

이상 링크드리스트와 링크드리스트 데이터 추상에 대해서 알아보았다. 이로써 기본자료구조인 스택,큐,링크드리스트에 대한 기본적인 내용을 모두 다루었다. 다음번부터는 이것들의 응용과 함께 기본적인 자료구조에 관련된 알고리즘(:12)들을 알아보게 될것이다.