Data Structures & Algorithms

[Data Structures] 추상자료형 (Abstract Data Type) 과 리스트 자료구조

숄구-ml 2022. 5. 6. 13:34

1. 추상자료형 (Abstract Data Type)

      • 구체적인 기능의 완성과정을 언급하지 않고, 순수하게 기능이 무엇인지를 나열한 것 
      • 예시 - wallet 이라는 구조체를 정의하였다면, 이를 기반으로 ADT를 정의 할 수 있다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _wallet
{    
    int coint100Num; //100원짜리 동전의 수
    int bill5000Num; //5000원짜리 지폐의 수
} Wallet;
 
 
//Wallet의 ADT
int TakeOutMoney(Wallet * pw, int coinNum, int billNum)
    // 첫 번째 인자로 전달된 주소의 지갑에서 돈을 꺼낸다
    // 두 번째 인자로 꺼낼 동전의 수, 세 번째 인자로 꺼낼 지폐의 수를 전달
    // 꺼내고자 하는 돈의 총액이 반환, 그만큼 돈이 차감
int PutMoney(Wallet * pw, int coinNum, int billNum)
    // 첫 번째 인자로 전달된 주소의 지갑에 돈을 넣는다
    // 두 번째 인자로 넣을 동전의 수, 세 번째 인자로 넣을 지폐의 수를 전달
    // 넣은 만큼 동전과 지폐의 수가 증가
cs

 

 

2. 리스트 자료구조 - 데이터를 나란히 저장한다 / 중복된 데이터의 저장을 막지 않는다

      • 순차리스트 - 배열을 기반으로 구현된 리스트
      • 연결리스트 - 메모리의 동적 할당을 기반으로 구현된 리스트
      • 리스트 자료구조의 ADT 정의 예시
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
//리스트 자료구조의 ADT
void ListInit(List * plist);
    //초기화할 리스트의 주소 값을 인자로 전달
    //리스트 생성 후 제일 먼저 호출되어야 하는 함수
void LInsert(List * plist, LData data);
    //리스트에 데이터를 저장
    //매개변수 data에 전달된 값을 저장
void LFirst(List * plist, LData * pdata);
    //첫 번째 데이터를 pdata가 가리키는 메모리에 저장
    //데이터의 참조를 위한 초기화가 진행
    //참조 성공 시 True(1), 실패 시 False(0) qksghks
void LNext(List * plist, LData * pdata);
    //참조된 데이터의 다음 데이터가 pdata가 가리키는 메모리에 저장됨
    //순차적인 참조를 위해서 반복 호출이 가능
    //참조를 새로 시작하려면 먼저 LFirst 함수를 호출해야함
    //참조 성공 시 True(1), 실패 시 False(0) 반환
void LData(List * plist);
    //LFirst 혹은 LNext 함수의 마지막 반환 데이터를 삭제
    //삭제된 데이터는 반환됨
    //마지막 반환 데이터를 삭제하므로 연이은 반복 호출을 허용하지 않는다
void LCount(List * plist);
    //리스트에 저장되어 있는 데이터의 수를 반환
 
 
//리스트 ADT를 기반으로 정의된 main 함수 예시
#include <stdio.h>
#include "ArrayList.h"
 
int main(void)
{
    //ArrayList의 생성 및 초기화
    List list;
    int data;
    ListInit(&list);
 
    //5개의 데이터 저장
    LInsert(&list, 11); LInsert(&list, 11);
    LInsert(&list, 22); LInsert(&list, 22);
    LInsert(&list, 33); 
 
    //저장된 데이터의 전체 출력
    printf("현재 데이터의 수: %d \n", LCount(&list));
    if (LFirst(&list, &data)
    { 
        printf("%d ", data);
        while(LNext(&list, &data))
            printf("%d ", data);
    }
    printf("\n\n")
 
    if(LFirst(&list, &data))
    {
        if(data == 22)
            LRemove(&list);
        while(LNext(&list, &data))
        {
            if(data == 22)
                LRemove(&list);
        }
    }
 
    return 0;
}
 
cs

 

 

 

3. 순차 리스트 - 배열 기반의 리스트 자료구조

    • 배열 기반 리스트 구현을 위한 헤더파일
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
//ArrayList.h
 
#ifndef __ARRAY_LIST_H__
#define __ARRAY_LIST_H__
 
#define TRUE 1
#define FALSE 0
#define LIST_LEN 100
typedef int LData;
 
typedef struct __ArrayList  // 배열 기반 리스트를 정의한 구조체
{
    LData arr[LIST_LEN];    // 리스트의 저장소인 배열
    int numOfData;          // 저장된 데이터의 수
    int curPosition;        // 데이터 참조위치를 기록
} ArrayList;
 
typedef ArrayList List;
 
void ListInit(List * plist);
void LInsert(List * plist, LData data);
int LFirst(List * plist, LData * pdata);
int LNext(List * plist, LData * pdata);
LData LRemove(List * plist);
int LCount(List * plist);
 
#endif
 
cs
  • 배열 기반 리스트의 단점: 1. 배열의 길이가 초기에 결정되어야 하고 변경이 불가능 2. 삭제의 과정에서 데이터의 이동이 매우 빈번히 일어난다
  • 배열 기반 리스트의 장점: 1. 데이터의 참조가 쉽다. 인덱스 값을 기준으로 어디든 한 번에 참조가 가능 

 

 

4. 연결 리스트 - 메모리의 동적 할당을 기반으로 구현된 리스트 자료구조

    • 노드를 표현한 구조체의 정의
1
2
3
4
5
6
7
8
typedef int LData // 상황에 따라 변경
 
typedef struct _node
{
    LData data;          // 데이터를 담을 공간
    struct _node * next; // 연결의 도구
} Node;
 
cs

 

  • 단순 연결 리스트 - 연결의 형태가 한쪽 방향으로 전개되고 시작과 끝이 분명히 존재
    1. 머리에 새로운 노드를 추가하는 경우
      • 장점: 포인터 변수 tail이 불필요
      • 단점: 저장된 순서를 유지하지 않는다
    2. 꼬리에 새로운 노드를 추가하는 경우
      • 장점: 저장된 순서가 유지된다
      • 단점: 포인터 변수 tail이 필요
    3. 머리에 새로운 노드를 추가하되 맨 앞에 더미 노드가 존재하는 구조
      • 장점: 노드를 추가, 삭제, 조회하는 방법에 있어서 첫 번째 노드와 두 번째 이후의 노드에 차이가 없어진다 - 일관된 형태로 구성할 수 있다

    --> 포인터 변수 tail을 유지하기 위해 넣어야 할 부가적인 코드가 번거로울 뿐만아니라 리스트 자료구조는 저장된 순서를 유지해야 하는 자료구조가 아니므로 1번의 방식을 많이 사용한다. 또한, 더미 노드를 추         가하면 장점이 더 커지므로 3번을 구현해 보겠음

 

  • 더미 노드 기반의 단순 연결 리스트 (3번) 구조 헤더파일 정의 
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
#ifndef __D_LINKED_LIST_H__
#define __D_LINKED_LIST_H__
#define TRUE 1
#define FALSE 0 
 
typedef int LData;
typedef struct _node
{
    LData data;
    struct _node * next;
} Node;
 
typedef struct __Linkedlist
{
    Node * head;
    Node * cur;
    Node * before;
    int numofData;
    int (*comp)(LData d1, LData d2);
} LinkedList;
 
typedef LinkedList List;
 
void ListInit(List * plist);
void LInsert(List * plist, LData data);
int LFirst(List * plist, LData * pdata);
int LNext(List * plist, LData * pdata);
LData LRemove(List * plist);
int LCount(List * plist);
 
void SetSortRule(List * plist, int (*comp)(LData d1, LData d2)); // 데이터 정렬 순서를 정하기 위한 함수가 추가됨
 
#endif
cs
출처:&nbsp;https://velog.io/@mmindoong/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8-2Linked-List

 

    • 더미 노드 기반의 단순 연결 리스트 (3번) 구조 구현
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
#include <stdio.h>
#include <stdlib.h>
#include "DLinkedList.h"
 
void ListInit(List * plist)
{
    plist->head = (Node*)malloc(sizeof(Node));   // 더미 노드의 생성
    plist->head->next = NULL;
    plist->comp = NULL;
    plist->numOfData = 0;
}
 
void LInsert(List * plist, LData data)
{
    Node * newNode = (Node*)malloc(sizeof(Node)); // 새로운 노드의 생성
    newNode->data = data;
    
    newNode->next = plist->head->next;
    plist->head->next = newNode;
    (plist->numOfData)++;
}
 
int LFirst(List * plist, LData * pdata)
{
    if (plist->head->next == NULL)     // 더미 노드가 NULL을 가리킨다면, 반환할 데이터가 없다
        return FALSE;
 
    plist->before = plist->head;     // before는 더미 노드를 가리키게 함
    plist->cur = plist->head->next;    // cur은 첫 번째 노드를 가리키게 함
 
    *pdata = plist->cur->data;         // 첫 번째 노드의 데이터를 전달
    return TRUE;                     // 데이터 반환 성공
}
 
int LNext(List * plist, LData * pdata)
{
    if (plist->cur->next == NULL)     // cur이 NULL을 가리킨다면
        return FALSE;                 // 반환할 데이터가 없다
 
    plist->before = plist->cur;       // cur이 가리키던 것을 before가 가리키게 함
    plist->cur = plist->cur->next;    // cur은 그 다음 노드를 가리킴
    *pdata = plist->cur->data;        // cur이 가리키는 노드의 데이터 전달
    return TRUE;                     // 데이터 반환 성공
}
 
LData LRemove(List * plist)
{
    // cur의 위치 재조정에 주목, 삭제 노드 이전 노드를 가리켜야 LNext 함수를 이용했을 시
    // 그 다음 노드를 참조할 수 있다
    
    Node * delNode = plist->cur;     // 소멸 대상의 주소 값을 delNode에 저장
    LData removeData = delNode->data; // 소멸 대상의 데이터를 removeData에 저장
 
    plist->before->next = plist->cur->next; // 소멸 대상을 리스트에서 제거
    plist->cur = plist->before; // cur이 가리키는 위치를 재조정
    
    free(delNode); // 리스트에서 제거된 노드 소멸
    (plist->numOfData)--; // 저장된 데이터의 수 하나 감소
    return removeData; // 제거된 노드의 데이터 반환
}
 
int LCount(List * plist)
{
    return plist->numOfData;
}
cs
 
 
                                    LRemove 함수 관련 이미지
출처:&nbsp;https://velog.io/@mmindoong/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8-2Linked-List

 

  • 연결 리스트에서 정렬기준 설정과 구현
    1. 연결 리스트의 정렬기준이 되는 함수를 등록하는 SetSortRule 함수
    2. SetSortRule 함수를 통해서 전달된 함수정보를 저장하기 위한 LinkedList의 멤버 comp
    3. comp에 등록된 정렬기준을 근거로 데이터를 저장하는 LInsert 함수
        • SetSortRule 함수가 호출되면서 정렬의 기준이 리스트의 멤버 comp에 등록되면, LInsert 함수 내에서는 comp에 등록된 정렬의 기준을 근거로 데이터를 정렬하여 저장한다
      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
      //어떤 함수건 SetSortRule의 인자가 될 수 있다 
      int WhoIsPrecede(int d1, int d2)
      {
          if(d1 < d2)
              return 0;    //d1이 정렬 순서상 앞선다
          else
              return 1;     //d2가 정렬 순서상 앞서거나 같다
      }
       
      void SetSortRule(List * plist, int (*comp)(LData d1, LData d2)
      {
          plist->comp = comp;
      }
       
      //정렬 기준 추가 전
      void LInsert(List * plist, LData data)
      {
          Node * newNode = (Node*)malloc(sizeof(Node)); // 새로운 노드의 생성
          newNode->data = data;
          
          newNode->next = plist->head->next;
          plist->head->next = newNode;
          (plist->numOfData)++;
      }
       
      //정렬 기준 추가 후
      void LInsert(List * plist, LData data)
      {
          Node * newNode = (Node*)malloc(sizeof(Node));
          newNode->data = data;         // 새 노드에 데이터 저장
          Node * pred = plist->head;  // pred는 더미 노드를 가리킴
       
          while(pred->next != NULL && plist->comp(data, pred->next->data) !=0)
          {
              pred = pred->next;         // 다음 노드로 이동
          }
          
          newNode->next = pred->next;    // 새 노드의 오른쪽을 연결
          pred->next = newNode;         // 새 노드의 왼쪽을 연결
       
          (plist->numOfData)++;         // 저장된 데이터의 수 하나         
      }
      cs

       

  • 원형 연결 리스트 (Circular Linked List)
    1. 단순 연결 리스트의 마지막 노드는 NULL을 가리켰다. 이 마지막 노드가 첫 번째 노드를 가리키게 하면 이것이 바로 '원형 연결 리스트'가 된다
    2. 단순 연결 리스트처럼 머리와 꼬리를 가리키는 포인터 변수를 각각 두지 않아도, 하나의 포인터 변수만 있어도 머리 또는 꼬리에 노드를 간단히 추가할 수 있다 
    3. 기본 형태에 대한 그림
     
출처:&nbsp;https://velog.io/@mmindoong/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%9B%90%ED%98%95-%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8-Circular-Linked-List
  • 변형 원형 연결 리스트
    1. 위의 그림을 보면 head에 포인트가 있는 상황에서 꼬리에 새로운 노드를 추가하는 방식은 head를 시작으로 리스트의 끝을 찾아가는 과정을 거쳐야 한다.
    2. 따라서, 이 포인터 변수를 머리가 아닌 꼬리를 가리키게 하면 이야기는 쉬워진다
      • 꼬리를 가리키는 포인터 변수는? tail
      • 머리를 가리키는 포인터 변수는? tail->next
       

출처: https://velog.io/@mmindoong/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%9B%90%ED%98%95-%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8-Circular-Linked-List

       3. 원형 연결 리스트의 자료구조 헤더파일 정의

 

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
#ifndef __C_LINKED_LIST_H__
#define __C_LINKED_LIST_H__
#define TRUE 1
#define FALSE 0
 
typedef int Data;
typedef struct _node
{
    Data data;
    struct _node * next;
}Node;
 
typedef struct _CLL
{
    Node * tail;
    Node * cur;
    Node * before;
    int numOfData;
}CList;
 
typedef CList List;
 
void ListInit(List * plist);
void LInsert(List * plist, Data data); // 꼬리에 노드 추가
void LInsertFront(List * plist, Data data);  // 머리에 노드 추가
 
int LFirst(List * plist, Data * pdata);
int LNext(List * plist, Data * pdata);
Data LRemove(List * plist);
int LCount(List * plist);
 
#endif
cs

 

 

  • 양방향 연결 리스트 (doubly Linked List) 또는 이중 연결 리스트
    1. 하나의 노드가 자신의 왼쪽과 오른쪽 노드를 동시에 가리키는 구조
    2. 양방향으로 조회가 가능하기 때문에, 한쪽 방향으로만 이동할 때 필요한 포인터 변수 before가 불필요 해진다.
    3. 양방향 연결리스트 타입 예시
출처:&nbsp;https://velog.io/@mmindoong/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%9D%B4%EC%A4%91-%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8Doubly-Linked-List

   

 

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
78
79
80
81
82
83
84
85
86
87
//그림 5-17 기본적인 양방향 연결리스트 헤더 파일 및 구현 예시

#ifndef
 __DB_LINKED_LIST_H__
#define __DB_LIKNED_LIST_H__
#define TRUE 1
#define FALSE 0
typedef int Data;
 
typedef struct _node
{
    Data data;                
    struct _node * next;    //오른쪽 노드를 가리키는 포인터 변수
    struct _node * prev;    //왼쪽 노드를 가리키는 포인터 변수
}Node;
 
typedef struct _DLinkedList
{
    Node * head;
    Node * cur;
    int numOfData;
} DBLinkedList;
 
typedef DBLinkedList List;
void ListInit(List * plist);
void LInsert(List * plist, Data data);
int LFirst(List * plist, Data * pdata);
int LNext(List * plist, Data * pdata);
int LPrevious(List * plist, Data * pdata);  //왼쪽 노드로 이동하며 조회할 수 있는 함수
int LCount(List * plist);
 
#endif
 
 
void ListInit(List * plist)
{
    plist->head = NULL;
    plist->numOfData = 0;
}
 
void LInsert(List * plist, Data data)
{
    //더미노드가 존재하지 않기 때문에, 
    //첫 번째 노드를 추가하는 과정과, 두 번째 노드 이후를 추가하는 과정을
    //나누어서 생각해야 한다
    
    Node * newNode = (Node *)malloc(sizeof(Node));
    newNode->data = data;
 
    newNode->next = plist->head;    //새 노드의 next를 NULL로 초기화
    newNode->prev = NULL;            //새 노드의 prev를 NULL로 초기화
    
    if (plist->head != NULL)    
    {
        plist->head->prev = newNode;
    }
 
    plist->head = newNode;            //포인터 변수 head가 새 노드를 가리키게 함
    (plist->numOfData)++;
}
 
int LFirst(List * plist, Data * pdata)
{
    if(plist->head == NULL)
        return FALSE;
 
    plist->cur = plist->head;
    *pdata = plist->cur->data;
    return TRUE;
}
 
int LNext(List * plist, Data * pdata)
{
    if(plist->cur->next == NULL)
        return FALSE;
 
    plist->cur=plist->cur->next;
    *pdata = plist->cur->data;
    return TRUE;
}
 
int LPrevious(List * plist, Data * pdata)
{
    if(plist->cur->prev == NULL)
        return FALSE;
    
    plist->cur=plist->cur->prev;
    *pdata = plist->cur->data;
    return TRUE;
}
cs

 

 

 

728x90