일반적인 링크드 리스트는 Node가 다른 Node를 하나만 가리키고 있지만 이중 연결리스트는 앞 뒤로 하나씩 연결을 가지고 있습니다. 

그래서 탐색을 할때 해당 노드에서 앞에 노드나 뒤에 노드로 쉽게 왔다갔다 할 수 있는 장점이 있습니다. 특정 노드를 탐색하다가 뒤로 마음대로 갈 수 있다는 의미이다. 프로그램을 작성하기전에 이러한 상황이 꼭 필요하고 자주 발생할 것 같다면 이중 연결 리스트를 고려해 보아야 합니다. 

  

아래 Node를 보면 next와 prev 라는 2개의 Node 포인터를 볼 수 있습니다. 하나는 자신의 앞에 있는 노드(next)를 가르키며 다른하나는 자신의 뒤에 있는 노드(prev)를 가르키는 역할을 합니다.

생성자를 통해 기본적으로 포인터에 NULL을 대입 합니다. 그리고 파라미터를 받는 생성자는 파마리터로 받은 ptr을 가지고 해당 노드 다음에 자신을 놓도록 합니다. 포인터를 사용하기 때문에 복잡해 보일 수 있다. 종이에 하나하나 그려가며 코드를 보면 이해가 쉽게 갈 것입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Node { 
    int data;
    Node* next, * prev; 
    Node() {
        next = prev = NULL;
        data = 0;
    }
    Node(int i, Node* ptr)//ptr 뒤에 추가
    {
        data = i;
        prev = ptr;
        next = ptr->next;
        next->prev = prev->next = this
    }  
};
cs

 


그리고 이중 연결리스트의 또 다른 장점으로는 노드 자신을 삭제하는게 쉽습니다. 그 이유는 자신의 앞에노드와 뒤에 노드의 정보를 가지고 있기 때문에 포인터를 가지고 자신을 제외한 앞뒤 포인터를 서로 연결 시켜줄 수 있어서 입니다.

아래 예시는 자기 자신의 노드를 삭제하는 함수 입니다. 함수의 첫번째 줄은 자신의 뒤에 노드에 다음을 가르키는 포인터에 자기 자신의 다음 노드의 주소값을 넘겨줍니다. 그러면 뒤에 노드가 자신의 다음 노드를 가르키게 하는 것입니다. 두번째 줄에는 자신의 다음노드의 이전을 가르키고 있는 포인터에 자신이 가르키고 있는 뒷노드의 주소값을 넘겨주는 것입니다. 그리고 마지막으로 delete로 자기자신을 삭제해서 할당받은 메모리를 해제합니다. 간결한 코드이지만 처음이해하는데는 많은 어려움이 있을 수 있습니다.


1
2
3
4
5
void selvDelete() {
     prev->next = next;
     next->prev = prev;
     delete this;
}
cs


일반적으로 간단하게 데이터를 추가하고 탐색하는 경우에는 일반 연결 리스트를 구현하면 됩니다.

연결리스트를 활용하여 구현한 Stack 예제 : http://hijuworld.tistory.com/54


하지만 데이터를 탐색할 때 뒤로 갔다 앞으로 갔다하는 경우가 많고 데이터 삭제가 빈번한 경우라면 이중 연결 리스트로 구현하는 것이 좋습니다.


아래 예제는 이중연결리스트를 구현한 예제입니다.


C++을 기본으로 작성하였으며 데이터를 삽입이나 삭제 할때 가장 앞이나 가장 뒤에서 모두 가능하게 하였습니다.

필요한 기능의 함수가 있다면 DLinkedList 구조체에 추가하여 구현하면 됩니다. 

해당 예제를 가지고 데이터를 앞뒤로 삽입하고 삭제하기 때문에 왠만한 기능을 다 할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include<iostream>
 
 
struct Node {
    int data;
    Node* next, * prev;
    Node() {
        next = prev = NULL;
        data = 0;
    }
    Node(int i, Node* ptr)//ptr 뒤에 추가한다.
    {
        data = i;
        prev = ptr;
        next = ptr->next;
        next->prev = prev->next = this
    }    
    void selvDelete() {
        prev->next = next;
        next->prev = prev;
        delete this;
    }
};
 
struct DLinkedList {
    Node *head;
    Node *tail;
    int count;
    DLinkedList() { //생성자
        count = 0;
        head = new Node(); //더미를 선언해서 가지고 있게한다.
        tail = new Node(); //더미를 선언해서 가지고 있게한다.
        head->next = tail; //서로연결한다.
        tail->prev = head;
    }
    ~DLinkedList() {
        while (head->next != tail)
            head->next->selvDelete();
        delete head;
        delete tail;
    }
    void firstInsert(int i) { //head 다음에 추가한다.
        new Node(i, head);
    }
    void endInsert(int i) { //tail 앞에 추가한다.
        new Node(i, tail->prev);
    }
    void firstDelete() { //head 다음 노드 삭제한다.
        if (head->next == tail)    return;
        head->next->selvDelete();
    }
    void endDelete() { //tail 앞에 제거한다.
        if (tail->prev == head) return;
        tail->prev->selvDelete();
    }
    void printAll() {
        Node* tmp = head;
        while (tmp->next != tail) {
            printf("%d\n", tmp->next->data);
            tmp = tmp->next;
        }
    }
};
 
int main() {
    DLinkedList *list = new DLinkedList();
    list->firstInsert(1); //1을 삽입한다.(가장앞)
    list->firstInsert(3); //3을 삽입한다.(1앞에)
    list->firstInsert(5); //5을 삽입한다.(3앞에)
    list->firstDelete(); //5를 삭제한다
    list->endInsert(100); //100을 삽입한다.(가장뒤에)
    list->endInsert(200); //200을 삽입한다.(100뒤에)
    list->endInsert(300); //300을 삽입한다.(200뒤에)
    list->endDelete(); //300을 삭제한다.
    list->printAll();
    delete list;
}
cs

 이중 연결리스트를 여러번 직접 종이에 그려가며 작성해 보아야 손에 익어서 언제든 작성 할 수 있습니다. 처음부터 코드를 완벽하게 작성하고 실행을 해야합니다. 대충 작성하고 디버깅을 해야지 하고 생각한다면 쉽게 에러를 찾을 수 없습니다. 포인터를 가지고 계속 왔다갔다 하기 때문에 디버깅에 많은 어려움이 있습니다. 연결 리스트 뿐만 아니라 데이터를 포인터로 연결하는 트리나 링큐 등도 디버깅하는데 많은 어려움이 있습니다. 처음부터 주의깊게 프로그래밍하는 것이 좋습니다. 


Posted by 꿈만은공돌
,

C++을 이용하여 LinkedList를 사용하여 Stack을 구현해보도록 하겠습니다.


 스택(Stack)을 설명하자면 다음과 같습니다. 가장 처음 데이터를 1을 넣으면 [1]이 됩니다. 거기에 2를 넣으면 [2, 1] 이 됩니다. 2->1을 가르키고 있는 것이죠. 그리고 4를 넣으면 [4,2,1]이 됩니다. 4->2->1 입니다. 여기에 10을 넣으면 [10, 4, 2, 1] 데이터가 됩니다. 이제 데이터를 꺼내면 10부터 나오게 됩니다. 마지막 넣은 데이터가 가장 처음 나오게 됩니다. 

 Last In First out 으로 줄여서  LIFO 라고 합니다. 

 10을 꺼내게 되면 [4, 2, 1]이 됩니다. 다시 데이터를 꺼내면 4가 나오게 됩니다. [2,1]이 됩니다. 그리고 하나더 꺼내면 2가 나오고 마지막으로 1이 나오게 됩니다.

 비유를 하자면 바닥에 이불을 하나씩 쌓는것을 생각하면 됩니다. 빨간이불을 먼저 쌓고 그다음에 검정이불을 쌓고 마지막으로 하얀이불을 쌓으면 다음에 꺼낼 때 하얀이불이 가장위에 있기때문에 하얀이불부터 꺼낼 수 있습니다.


Stack은이렇듯  LIFO 으로 마지막에 들어온 데이터가 가장 처음에 나오는 방식 입니다.


 연결리스트를 구현할때 Node들을 만들어서 서로 연결해야 합니다. Node는 기본적으로 데이터를 보관할 변수하나와 다른 노드를 가르킬 포인터 변수를 가집니다. 아래 예제에서는 data변수가 데이터를 저장할 변수가 됩니다. 그리고 next가 다른 노드를 가르킬 포인터가 됩니다. 

 그리고 추가로 생성자를 추가시켰습니다. 구조체에도 c++에서는 생성자나 함수등을 포함시킬 수 있습니다. class와의 차이는 모든 변수와 함수가 기본적으로 public입니다. 첫번째 생성자는 기본생성자로 초기화만 시킵니다. 다음생성자는 데이터와 다음 노드를 받습니다. 파라미터로 들어온 ptr 다음 노드로 자신을 가리키게 합니다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Node {
    int data;
    Node* next;
    Node() {
        next = NULL;
        data = 0;
    }
 
    Node(int i, Node* ptr) //ptr 뒤에 추가
    {
        data = i;
        next = ptr->next;
        ptr->next = this
    }    
};
cs


연결 리스트(LinkedList)를 가지고 구현한 스택(stack) 코드입니다. 기본적으로 .cpp로 C++ 컴파일을 해야한다.

Stack에 생성자를 보면 new Node로 더미노드를 만들어서 head에 넣는 것을 볼 수 있습니다. 이는 pop과 push등을 할때 더미노드가 있어야 코드가 더 간결해집니다.

소멸자도 반드기 구현해줘야 합니다. 그래야 사용하지 않는 메모리를 heap에서 계속 점유하고 있기 때문에 프로그램이 계속 돌다가 메모리가 부족한 현상이 발생 할 수도 있습니다.

push 함수는 head 다음에 새로운 노드를 추가시킵니다. pop은 head 다음 노드를 꺼내는 역할을 합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include<stdio.h>
 
struct Node {
    int data;
    Node* next;
    Node() {
        next = NULL;
        data = 0;
    }
 
    Node(int i, Node* ptr) //ptr 뒤에 추가
    {
        data = i;
        next = ptr->next;
        ptr->next = this
    }    
};
 
struct Stack {
    Node *head;
    int count;
    Stack() { //생성자
        head = new Node(); //더미노드
        count = 0;
    }
 
    ~Stack() { //소멸자
        while (count!=0) {
            pop();
        }
        delete head;
    }
 
    void pop() { //데이터 제거
        Node *tmp = head;
        head = head->next; 
        delete tmp; 
        count--;
    }
 
    void push(int i) { //데이터 삽입
        new Node(i, head);
        count++;
    }
 
    void printAll() { //모두 출력
        Node *cur = head;
        while (cur->next) {
            cur = cur->next;
            printf("%d\n", cur->data);
        }
    }
};
 
int main() {
    Stack *stack = new Stack();
    stack->push(1);
    stack->push(3);
    stack->push(5);
    stack->push(7);
    stack->printAll();
    delete stack;
}
cs

이제까지 단일 연결리스트에 대해서 알아 보았습니다. 

단일 연결리스트 vs 이중 연결 리스트


단일 연결 리스트는 Node가 다른 Node를 하나만 가리키고 있지만 이중 연결리스트는 앞 뒤로 하나씩 연결을 가지고 있습니다. 


그런데 이중연결리스트는 탐색을 할때 해당 노드에서 앞에 노드나 뒤에 노드로 쉽게 왔다갔다 할 수 있는 장점이 있습니다. 특정 노드를 탐색하다가 뒤로 마음대로 갈 수 있다는 의미이다. 프로그램을 작성하기전에 이러한 상황이 꼭 필요하고 자주 발생할 것 같다면 이중 연결 리스트를 고려해 보아야 합니다. 


이중 연결리스트 : http://hijuworld.tistory.com/55




Posted by 꿈만은공돌
,