[자료구조 C 언어] C 프로그래밍 자료구조 - 10 : 이중 연결리스트 (Doubly Linked List)

Programming/C · 2020. 3. 10. 00:29

저번 게시물에 이어서 이중 연결 리스트 (Doubly Linked List)에 대해 함께 알아보겠습니다.

 

이중 연결리스트

 

이중 연결 리스트의 가장 큰 특징은 한 노드에서 양방향으로 이전 노드 또는 다음 노드에 접근할 수 있다는 것입니다.

 

가장 큰 예를 들면 마지막 노드에 접근하기 위해 head부터 next를 따라 tail까지 접근했던 것에 반해,

 

이중 연결 리스트는 그냥 head의 왼쪽 노드로 접근해주면 됩니다.

 

단점은 다른 노드로 접근하는 링커가 하나 더 존재함에 따라 메모리가 커지고, 구현의 복잡도가 증가합니다.

 

1) 이중 연결리스트 ADT

 

 

 ADT

 Init_List(void): 연결 리스트를 초기화합니다.

 Add_First(ListMark *ls, int data): 연결 리스트의 가장 앞쪽에 노드를 추가합니다.

 Add_Last(ListMark *ls, int data): 연결 리스트의 가장 뒷쪽에 노드를 추가합니다.

 Add_Pos(ListMark *ls, int pos, int data): 연결리스트의 원하는 위치에 노드를 추가합니다.

 Delete_First(ListMark *ls): 연결리스트의 가장 앞 노드를 삭제합니다.

 Delete_Last(ListMark *ls): 연결리스트의 가장 뒷 노드를 삭제합니다.

 Delete_Pos(ListMark *ls, int pos, int data): 연결리스트의 원하는 위치에 노드를 추가합니다.

 Return_Size(ListMark *ls): 연결 리스트에 저장된 노드의 갯수를 반환합니다.

 Reset_List(ListMark *ls): 연결리스트에 저장된 모든 노드의 메모리를 해제하고, 초기화합니다.

 

 Print_List(ListMark *ls): 연결 리스트에 저장된 모든 데이터를 출력합니다.

 

 

이중 연결 리스트를 구성하는 함수는 위와 같습니다.

 

본인이 구현하는 프로그램의 목적과 특성에 맞게 추가, 변경, 삭제할 수 있습니다.

 

 

2) 이중 연결 리스트 구현

 

이중 연결 리스트는 앞서 공부한 원형 연결리스트의 모든 노드가 자기 뒷 노드 뿐만 아니라 앞 노드도 가리킬 수 있다는 것이 특징입니다.

 

따라서, 각 노드 당 앞, 뒷 노드를 가리키는 링커가 2개 존재합니다.

 

 

2-1) 이중 연결리스트 구조체 선언

 

 

typedef struct NODE {

    struct NODE *lnext;

    struct NODE *rnext;

    

    int data;

} Node;

 

typedef struct LISTMARK {

    Node *head;

    Node *tail;

    

    int size;

 

} ListMark;

 

 

NODE 구조체에서 보이는 것과 같이 각 노드 당 앞 노드를 가리키는 *lnext와 뒷 노드를 가리키는 *rnext 두 개의 링커를 가집니다.

 

 

2-2) 이중 연결 리스트 초기화

 

 

 

ListMark *Init_List(void){

    ListMark *ls = (ListMark *)malloc(sizeof(ListMark));

    

    ls->head = NULL;

    ls->tail = NULL;

    ls->size = 0;

    

    return ls;

}

 

 이중 연결 리스트의 처음과 끝 노드를 가리키는 구조체 멤버 변수를 초기화해줍니다.

 

ls->size는 노드의 추가, 삭제 시 크기가 변화하며 연결 리스트 내의 노드 갯수를 저장합니다.

 

해당 함수는 아래와 같이 사용할 수 있습니다.

 

 

2-3) 이중 연결리스트 노드 추가

 

 

void Add_First(ListMark *ls, int data){ // 연결 리스트의 가장 앞 쪽에 노드를 추가합니다.

    Node *tmp = (Node *)malloc(sizeof(Node)); // 추가될 노드이므로 메모리를 할당해줍니다.

    

    tmp->data = data;

    

    if(ls->head == NULL){ // head와 tail이 NULL로 초기화됐으므로, NULL일 때 저장된 노드는 없습니다.

        ls->head = ls->tail = tmp; // 첫 노드는 head와 tail이 함께 가리킵니다.

 

        ls->head->lnext = tmp; // 어차피 ls->head와 ls->tail이 같은 주소를 가지기 때문에, 모든 노드의 링커가 다시 스스로를 가리킵니다.

        ls->head->rnext = tmp;

    }

    else{

        

        tmp->lnext = ls->head->lnext; // tmp가 새로운 head가 돼야 하므로, tmp의 lnext가 head의 이전 노드 (ls->tail)를 가리킵니다.

        tmp->rnext = ls->head; // tmp의 rnext는 head를 가리킵니다.

        

        ls->head->lnext = tmp; // head의 lnext는 tail에서 새로운 head인 tmp로 갱신됩니다.

        ls->tail->rnext = tmp; // tail의 rnext는 새로운 head인 tmp를 가리킵니다.

        

        ls->head = tmp; // tmp를 새로운 head로 갱신합니다.

    }

    

    ls->size++;

}

 

 

void Add_Last(ListMark *ls, int data){ // 연결 리스트의 가장 마지막에 노드를 추가합니다.

    Node *tmp = (Node *)malloc(sizeof(Node)); // Add_First와 동일한 논리를 가집니다.

    

    tmp->data = data;

    

    if(ls->tail == NULL){

        ls->head = ls->tail = tmp;

 

        ls->tail->lnext = tmp;

        ls->tail->rnext = tmp;

    }

    else{

        tmp->lnext = ls->tail;

        tmp->rnext = ls->tail->rnext;

        

        ls->tail->rnext = tmp;

        ls->head->lnext = tmp;

        

        ls->tail = tmp;

    }

    

    ls->size++;

}

 

 

void Add_Pos(ListMark *ls, int pos, int data){ // 연결 리스트의 원하는 위치에 노드를 추가합니다.

    Node *tmp = (Node *)malloc(sizeof(Node));

    Node *cur;

    

    last_pos = ls->size; // last_pos는 연결 리스트의 마지막 노드 위치 순서를 가리킵니다.

    

    tmp->data = data;

    

    if(pos <= ls->size){ // 원하는 위치가 연결리스트 내에 있는지 확인합니다.

        if(pos == 1){

            puts("Add_Pos -> Add_First.");

            Add_First(ls, data); // 첫 번째 위치라면 Add_First를 수행합니다.

        }

        else if(pos == last_pos){

            puts("Add_Pos -> Add_Last.");

            Add_Last(ls, data); // 마지막 위치라면 Add_Last를 수행합니다.

        }

        else{

            int n = pos - 1;

            cur = ls->head;

            

            while(--n){ // 내가 넣고 싶은 자리 바로 앞으로 cur이 설정된다.

                cur = cur->rnext;

            }

            // 넣고자 하는 자리 바로 앞 노드와 cur이 같습니다.

            tmp->lnext = cur; // cur 다음에 tmp가 들어가야 하기 때문에, tmp->lnext(이전 노드)는 cur을 가리킵니다.

            tmp->rnext = cur->rnext; // tmp는 cur->rnext가 가리키는 노드를 가리킵니다.

            

            cur->rnext = tmp; // cur->rnext는 새롭게 자신의 다음에 위치할 노드인 tmp를 가리킵니다. 

            tmp->rnext->lnext = tmp; // tmp의 다음 노드의 lnext는 tmp를 가리킵니다.

            

            ++ls->size;

        }

    }

    else{

        puts("You entered wrong position.");

    }

}

 

 

2-4) 이중 연결 리스트 노드 삭제

 

void Delete_First(ListMark *ls){ // 연결 리스트에 저장된 첫 번째 노드를 삭제합니다.

    Node *cur;

    Node *tmp;

    

    tmp = ls->head;

    

    if(tmp == NULL){

        puts("List is empty.");

    }

    else{

        cur = ls->head->rnext; // 이전 head가 삭제되고 다음 노드(==ls->head->rnext)가 새로운 head (==cur)가 됩니다.

        

        cur->lnext = ls->head->lnext; // 새로운 head인 cur은 이전 head의 링커가 가리키는 노드를 가리킵니다.

        

        ls->tail->rnext = cur; // tail의 rnext는 새로운 head인 tmp를 가리킵니다.

        

        ls->head = cur; // cur이 새로운 head로 지정됩니다.

        

        free(tmp); // tmp에 저장된 이전 head의 메모리를 해제합니다.

        

        ls->size--;

    }

}

 

 

void Delete_Last(ListMark *ls){ // 연결 리스트에 저장된 마지막 노드를 삭제합니다.

    Node *cur; // Delete_First와 동일한 논리를 가집니다.

    Node *tmp;

    

    tmp = ls->tail;

    

    if(tmp == NULL){

        puts("List is empty.");

    }

    else{

        cur = ls->tail->lnext;

        

        cur->rnext = ls->head;

        

        ls->head->lnext = cur;

        

        ls->tail = cur;

        

        free(tmp);

        

        ls->size--;

    }

}

 

 

void Delete_Pos(ListMark *ls, int pos, int data){ // 연결 리스트의 원하는 위치에 노드를 삽입합니다.

    Node *cur;

    Node *tmp;

    

    int last_pos = ls->size;

    

    if(pos <= ls->size){

        if(ls->head == NULL){

            puts("List is empty.");

        }

        else if(pos == 1){

            Delete_First(ls);

        }

        else if(pos == last_pos){

            Delete_Last(ls);

        }

        else{

            int n = pos;

            

            cur = ls->head;

            

            while(--n){ // 내가 지우고 싶은 자리로 cur이 설정된다.

                cur = cur->rnext;

            }

            // cur은 지우고자 하는 노드로 설정됩니다.

            tmp = cur;

            

            cur->lnext->rnext = cur->rnext; // cur의 이전 노드가 cur 다음 노드를 가리키도록 합니다.

            cur->rnext->lnext = cur->lnext; // cur의 다음 노드가 cur 이전 노드를 가리키도록 합니다.

            

            free(tmp); // tmp == cur 이므로, 지우고자 하는 노드의 메모리를 해제합니다.

            

            ls->size--;

        }

    }

    else{

        puts("You entered wrong position.");

    }

}

 

 

2-5) 이중 연결 리스트 출력, 노드 개수 반환

 

void Print_List(ListMark *ls){ // 연결 리스트에 저장된 모든 데이터를 출력합니다.

    Node *cur;

    

    cur = ls->head;

    

    if(cur == NULL){

        puts("List is empty.\n");

    }

    else{

        int n = 1;

        

        while(n <= ls->size){ // ls->size는 연결 리스트에 저장된 모든 노드의 개수와 동일합니다.

            printf("List[%d] = %d.\n", n, cur->data);

            

            cur = cur->rnext; // cur이 출력되고, cur은 자신의 다음 노드로 갱신됩니다.

            n++;

        }

    }

    

    printf("\n");

}

 

 

void Return_Size(ListMark *ls){ // 연결 리스트에 저장된 노드의 개수를 출력합니다.

    printf("\nCurrent list size is %d.\n", ls->size);

    // ls->size는 연결 리스트에 저장된 모든 노드의 갯수와 동일합니다.

}

 

 

2-6) 이중 연결리스트 노드 전체 삭제 및 초기화

 

void Reset_List(ListMark *ls){

    Node *tmp;

    Node *cur;

    

    cur = ls->head;

    

    while(cur != ls->tail){ // cur이 head부터 tail 이전까지 이동하며 메모리를 해제합니다.

        tmp = cur->rnext;   // ls->size를 사용하면 while 문 종료와 함께 메모리 해제를 완료할 수 있습니다. (tail이 남지 않아요.)

        free(cur);

        cur = tmp;

        printf("Previous list size was %d, and now list size is %d.\n", ls->size, --ls->size);

    }

    

    free(cur); // cur == ls->size 이므로, 마지막 노드인 tail의 메모리를 해제합니다.

        

    printf("Previous list size is %d, and now list size is %d.\n", ls->size, --ls->size);

    

    ls->head = NULL; // Init_List와 동일한 역할을 합니다. (초기화)

    ls->tail = NULL;

    ls->size = 0;

}

 

 

 

 

 

 

3) 이중 연결리스트 전체 코드 및 출력 결과

 

 

 

 

 

3-1) 전체 코드

 

 

 

#include <stdio.h>

#include <stdlib.h>

 

int last_pos;

 

typedef struct NODE{

    struct NODE *lnext;

    struct NODE *rnext;

    

    int data;

} Node;

 

typedef struct LISTMARK{

    Node *head;

    Node *tail;

    

    int size;

} ListMark;

 

ListMark *Init_List(void){

    ListMark *ls = (ListMark *)malloc(sizeof(ListMark));

    

    ls->head = NULL;

    ls->tail = NULL;

    ls->size = 0;

    

    return ls;

}

 

void Add_First(ListMark *ls, int data){

    Node *tmp = (Node *)malloc(sizeof(Node));

    

    tmp->data = data;

    

    if(ls->head == NULL){ // 여기 한번 더 확인

        ls->head = ls->tail = tmp;

 

        ls->head->lnext = tmp;

        ls->head->rnext = tmp;

    }

    else{

        

        tmp->lnext = ls->head->lnext;

        tmp->rnext = ls->head;

        

        ls->head->lnext = tmp;

        ls->tail->rnext = tmp; // 이쪽을 이어주는 것

        

        ls->head = tmp;

    }

    

    ls->size++;

}

 

void Add_Last(ListMark *ls, int data){

    Node *tmp = (Node *)malloc(sizeof(Node));

    

    tmp->data = data;

    

    if(ls->tail == NULL){

        ls->head = ls->tail = tmp;

 

        ls->tail->lnext = tmp;

        ls->tail->rnext = tmp;

    }

    else{

        tmp->lnext = ls->tail;

        tmp->rnext = ls->tail->rnext;

        

        ls->tail->rnext = tmp;

        ls->head->lnext = tmp;

        

        ls->tail = tmp;

    }

    

    ls->size++;

}

 

void Add_Pos(ListMark *ls, int pos, int data){

    Node *tmp = (Node *)malloc(sizeof(Node));

    Node *cur;

    

    last_pos = ls->size;

    

    tmp->data = data;

    

    if(pos <= ls->size){

        if(pos == 1){

            puts("Add_Pos -> Add_First.");

            Add_First(ls, data);

        }

        else if(pos == last_pos){

            puts("Add_Pos -> Add_Last.");

            Add_Last(ls, data);

        }

        else{

            int n = pos - 1;

            cur = ls->head;

            

            while(--n){ // 내가 넣고 싶은 자리 바로 앞으로 cur이 설정된다.

                cur = cur->rnext;

            }

            

            tmp->lnext = cur;

            tmp->rnext = cur->rnext;

            

            cur->rnext = tmp;

            tmp->rnext->lnext = tmp;

            

            ++ls->size;

        }

    }

    else{

        puts("You entered wrong position.");

    }

}

 

void Delete_First(ListMark *ls){

    Node *cur;

    Node *tmp;

    

    tmp = ls->head;

    

    if(tmp == NULL){

        puts("List is empty.");

    }

    else{

        cur = ls->head->rnext;

        

        cur->lnext = ls->head->lnext;

        

        ls->tail->rnext = cur;

        

        ls->head = cur;

        

        free(tmp);

        

        ls->size--;

    }

}

 

void Delete_Last(ListMark *ls){

    Node *cur;

    Node *tmp;

    

    tmp = ls->tail;

    

    if(tmp == NULL){

        puts("List is empty.");

    }

    else{

        cur = ls->tail->lnext;

        

        cur->rnext = ls->head;

        

        ls->head->lnext = cur;

        

        ls->tail = cur;

        

        free(tmp);

        

        ls->size--;

    }

}

 

void Delete_Pos(ListMark *ls, int pos, int data){

    Node *cur;

    Node *tmp;

    

    int last_pos = ls->size;

    

    if(pos <= ls->size){

        if(ls->head == NULL){

            puts("List is empty.");

        }

        else if(pos == 1){

            Delete_First(ls);

        }

        else if(pos == last_pos){

            Delete_Last(ls);

        }

        else{

            int n = pos;

            

            cur = ls->head;

            

            while(--n){ // 내가 지우고 싶은 자리로 cur이 설정된다.

                cur = cur->rnext;

            }

            tmp = cur;

            

            cur->lnext->rnext = cur->rnext;

            cur->rnext->lnext = cur->lnext;

            

            free(tmp);

            

            ls->size--;

        }

    }

    else{

        puts("You entered wrong position.");

    }

 

}

 

void Print_List(ListMark *ls){

    Node *cur;

    

    cur = ls->head;

    

    if(cur == NULL){

        puts("List is empty.\n");

    }

    else{

        int n = 1;

        

        while(n <= ls->size){

            printf("List[%d] = %d.\n", n, cur->data);

            

            cur = cur->rnext;

            n++;

        }

    }

    

    printf("\n");

}

 

void Return_Size(ListMark *ls){

    printf("\nCurrent list size is %d.\n", ls->size);

    

//    return size;

}

 

void Reset_List(ListMark *ls){

    Node *tmp;

    Node *cur;

    

    cur = ls->head;

    

    while(cur != ls->tail){

        tmp = cur->rnext;

        free(cur);

        cur = tmp;

        printf("Previous list size was %d, and now list size is %d.\n", ls->size, --ls->size);

    }

    

    free(cur);

        

    printf("Previous list size is %d, and now list size is %d.\n", ls->size, --ls->size);

    

    ls->head = NULL;

    ls->tail = NULL;

    ls->size = 0;

}

 

void Check_List(ListMark *ls){

    printf("ls->head->data = %d.\n", ls->head->data);

    printf("ls->tail->data = %d.\n", ls->tail->data);

    

    printf("ls->head->rnext->data = %d.\n", ls->head->rnext->data);

    printf("ls->tail->lnext->data = %d.\n", ls->tail->lnext->data);

    

    printf("ls->head->lnext->lnext->data = %d.\n", ls->head->lnext->lnext->data);

    printf("ls->tail->rnext->rnext->data = %d.\n", ls->tail->rnext->rnext->data);

    

    printf("ls->head->rnext->rnext->data = %d.\n", ls->head->rnext->rnext->data);

    printf("ls->tail->lnext->lnext->data = %d.\n\n", ls->tail->lnext->lnext->data);

}

 

int main(void) {

    ListMark *ls = Init_List();

    

    Add_First(ls, 11);

    Add_First(ls, 12);

    Add_First(ls, 13);

    Add_First(ls, 14);

    Return_Size(ls);

    Print_List(ls);

    

    

    Add_Last(ls, 21);

    Add_Last(ls, 22);

    Add_Last(ls, 23);

    Add_Last(ls, 24);

    Return_Size(ls);

    Print_List(ls);

    

//    Check_List(ls);// 여긴 잘 된다.

    

    

    Reset_List(ls);

    Return_Size(ls);

    Print_List(ls);

    

    

    Add_First(ls, 11);

    Add_First(ls, 12);

    Add_First(ls, 13);

    Add_First(ls, 14);

    Return_Size(ls);

    Print_List(ls);

    

//    Check_List(ls);

    

    Add_Last(ls, 21);

    Add_Last(ls, 22);

    Add_Last(ls, 23);

    Add_Last(ls, 24);

    Return_Size(ls);

    Print_List(ls);

 

//    Check_List(ls);

    

    Delete_First(ls);

    Delete_First(ls);

    Delete_First(ls);

    Return_Size(ls);

    Print_List(ls);

    

//    Check_List(ls);

    

     Delete_Last(ls);

     Delete_Last(ls);

     Delete_Last(ls);

     Return_Size(ls);

     Print_List(ls);

    

//     Check_List(ls);

    

    Add_Pos(ls, 1, 31);

    Add_Pos(ls, 1, 32);

    Print_List(ls);

    

//    Check_List(ls);

    

    Add_Pos(ls, 2, 33);

    Add_Pos(ls, 3, 34);

    Print_List(ls);

 

//    Check_List(ls);

 

    Delete_Pos(ls, 1, 31);

    Delete_Pos(ls, 1, 32);

    Print_List(ls);

    

//    Check_List(ls);

    

    Delete_Pos(ls, 2, 33);

    Delete_Pos(ls, 3, 34);

    Print_List(ls);

 

//    Check_List(ls);

    return 0;

}

 

 

 

 

 

3-2) 출력 결과

 

아래 출력 결과는 main 함수를 그대로 출력한 것입니다.

 

이중 연결 리스트의 특성을 확인하기 위해선 main 함수에서 주석 처리된 Check_List() 함수를 실행하시면 됩니다.

 

아래 출력 결과 이후에 Check_List() 함수를 실행시킨 결과를 함께 확인해보겠습니다.

 

 

 

 

 

 

 

 

Check_List() 함수를 실행시킨 결과를 확인해보겠습니다.

 

 

 

 

head와 tail에서 좌측 (이전 노드), 우측 (다음 노드)으로 자유롭게 이동할 수 있는 것을 볼 수 있습니다.

 

 

 

다음 게시물에선 트리놔 이진 트리에 관한 내용을 설명드리겠습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형