*본 내용은 [윤성우의 열혈 자료구조] 책과 강의를 보고 공부하면서 요점 정리한 내용입니다.
구체적인 기능의 완성과정을 언급하지 않고, 순수하게 기능
이 무엇인지를 나열한 것
지갑의 추상 자료형 (지갑의 기능)
∙ 카드의 삽입
∙ 카드의 추출(카드를 빼냄)
∙ 동전의 삽입
∙ 동전의 추출(동전을 빼냄)
∙ 지폐의 삽입
∙ 지폐의 추출(지폐를 빼냄)
지갑을 의미하는 구조체 Wallet의 정의
기능의 명세를 가리켜 왜 자료형이라 하는 것일까?
자료형의 정의는 연산에 의미가 있다. (자료형 int를 생각해보자.)
완전한 자료형의 정의로 인식되기 위해서는 해당 자료형과 관련이 있는 연산(함수)이 함께 정의되어야 한다.
+) 프로그래밍의 기본이 되는 순서는 구조체와 함수
를 더불어 정의하고 추가적인 함수
를 정의해야 한다.
//자료형 Wallet의 정의
typedef struct _wallet
{
int coin100Num; //100원짜리 동전의 수
int bill5000Num; //5000원짜리 지폐의 수
}Wallet;
//자료형 Wallet의 정의의 일부
int TakeOutMoney(Wallet* pw, int coinNum, int billNum); //돈 꺼내는 연산
void PutMoney(Wallet * pw, int coinNum, int billNum); //돈 넣는 연산
리스트의 구분
∙ 순차 리스트
: 배열을 기반으로 구현된 리스트
∙ 연결 리스트
: 메모리의 동적 할당을 기반으로 구현된 리스트
(이는 구현 방법을 기준으로 한 구분으로 순차 리스트, 연결리스트 두 리스트의 ADT(기능)는 동일하다.)
리스트의 특징
∙ 저장 형태: 데이터를 나란히(하나의 열로) 저장한다.
∙ 저장 특성: 중복이 되는 데이터의 저장을 허용한다.
위와 같은 특징을 나타내기 위해 배열과 연결리스트를 이용한다.
※ LData는 저장 대상의 자료형을 결정할 수 있도록 typedef로 선언된 자료형의 이름이다.
void ListInit(List * plist);
∙ 초기화할 리스트의 주소 값을 인자로 전달한다.
∙ 리스트 생성 후 제일 먼저 호출되어야 하는 함수이다.
void LInsert(List * plist, LData data);
∙ 리스트에 데이터를 저장한다. 매개변수 data에 전달된 값을 저장한다.
void LFirst(List * plist, LData * pdata);
∙ 첫 번째 데이터가 pdata가 가리키는 메모리에 저장된다.
∙ 데이터의 참조를 위한 초기화가 진행된다.
∙ 참조 성공 시 TRUE(1), 실패 시 FALSE(0) 반환
int LNext(List * plist, LData * pdata);
∙ 참조된 데이터의 다음 데이터가 pdata가 가리키는 메모리에 저장된다.
∙ 순차적인 참조를 위해서 반복 호출이 가능하다.
∙ 참조를 새로 시작하려면 먼저 LFirst 함수를 호출해야 한다.
∙ 참조 성공 시 TRUE(1), 실패 시 FALSE(0) 반환
LData LRemove(List * plist);
∙ LFirst 또는 LNext 함수의 마지막 반환 데이터를 삭제한다.
∙ 삭제된 데이터는 반환된다.
∙ 마지막 반환 데이터를 삭제하므로 연이은 반복 호출을 허용하지 않는다.
int LCount(List * plist);
∙ 리스트에 저장되어 있는 데이터의 수를 반환한다.
int main(void)
{
List list; //리스트의 생성
....
ListInit(&list); //리스트의 초기화
....
}
int main(void)
{
....
LInsert(&list, 11); //리스트에 11을 저장
LInsert(&list, 22); //리스트에 22을 저장
LInsert(&list, 33); //리스트에 33을 저장
...
}
리스트 처음부터 끝까지 데이터 출력
int main(void)
{
....
if(LFirst(&list, &data)) //첫 번째 데이터 변수 data에 저장
{
printf("%d", data);
while(LNext(&list, &data)) //두 번째 이후 데이터 변수 data에 저장
printf("%d ",data);
}
....
}
int main(void)
{
....
if(LFirst(&list, &data)
{
if(data == 22)
LRemove(&list); //위의 LFirst 함수를 통해 참조한 데이터 삭제
while(LNext(&list, &data)) //LNext가 False 반환할 때까지(끝까지)
{
if(data == 22)
LRemove(&list); //위의 LNext 함수를 통해 참조한 데이터 삭제
}
}
}
※주의) LRemove 함수는 연이은 호출을 허용하지 않는다!
ArrayList.h
#ifndef __ARRAY_LIST_H__ // 헤더 파일의 중복 포함을 막기 위한 매크로 선언
#define __ARRAY_LIST_H__ // ,,
#define TRUE 1 // '참'을 표현하기 위한 매크로 정의
#define FALSE 0 // '거짓'을 표현하기 위한 매크로 정의
/*** ArrayList의 정의 ****/
#define LIST_LEN 100 // 배열의 길이
typedef int LData; // 저장할 대상의 자료형 변경을 위한 typedef 선언,
// 저장 대상을 바꾸고 싶으면 int를 수정하면 됨
typedef struct __ArrayList // 배열기반 리스트를 정의한 구조체
{
LData arr[LIST_LEN]; // 리스트의 저장소인 배열
int numOfData; // 리스트에 저장된 데이터의 수
int curPosition; // 데이터 참조위치를 기록(마지막 참조위치)
} ArrayList;
/*** ArrayList와 관련된 연산들 ****/
typedef ArrayList List; // 리스트의 변경을 용이하게 하기 위한 typedef 선언
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
void ListInit(List * plist)
{
(plist->numOfData) = 0; // 아무것도 저장되어 있지 않음
(plist->curPosition) = -1; // -1은 아무런 위치도 참조하지 않았음을 의미
}
void LInsert(List * plist, LData data)
{
/*** 예외 처리 ***/
if(plist->numOfData > LIST_LEN) // 더 이상 저장할 공간이 없다면
{
puts("저장이 불가능합니다.");
return;
}
plist->arr[plist->numOfData] = data; // 데이터 저장
(plist->numOfData)++; // 저장된 데이터의 수 증가
}
int LFirst(List * plist, LData * pdata)
{
if(plist->numOfData == 0) // 저장된 데이터가 하나도 없다면
return FALSE;
(plist->curPosition) = 0; // 참조 위치 초기화! 첫 번째 데이터의 참조를 의미!
*pdata = plist->arr[0]; // pdata가 가리키는 공간에 데이터 저장
return TRUE;
}
int LNext(List * plist, LData * pdata)
{
if(plist->curPosition >= (plist->numOfData)-1 // 더 이상 참조할 데이터가 없다면
return FALSE;
(plist->curPostion)++;
*pdata = plist->arr[plist->curPosition];
return TRUE;
}
curPosition 위치의 데이터 삭제
LData LRemove(List * plist)
{
int rpos = plist->curPosition; // 삭제할 데이터의 인덱스 값 참조
int num = plist->numOfData;
int i;
LData rdata = plist->arr[rpos]; // 삭제할 데이터를 임시로 저장
// 삭제를 위한 데이터의 이동을 진행하는 반복문
for(i=rpos; i<num-1; i++)
plist->arr[i] = plist->arr[i+1];
(plist->numOfData)--; // 데이터의 수 감소
(plist->curPosition)--; // 참조위치를 하나 되돌린다.
return rdata; // 삭제된 데이터의 반환
}
리스트의 저장 대상을 int형 변수가 아닌 구조체 변수로 바꾸어보자.
※ 헤더파일(ArrayList.h)은 변경될 수 있지만 소스파일(ArrayList.c)은 기능 개선되거나 버그 잡을 때 아니면 변경하면 안 됨
Point.h
typedef struct _point
{
int xpos;
int ypos;
} Point;
// Point 변수의 xpos, ypos 값 설정
void SetPointPos(Point * ppos, int xpos, int ypos);
// Point 변수의 xpos, ypos 정보 출력
void ShowPointPos(Point * ppos);
// 두 Point 변수의 비교
int PointComp(Point * pos1, Point * pos2);
Point.c
void SetPointPos(Point * ppos, int xpos, int ypos) // 초기화
{
ppos->xpos = xpos;
ppos->ypos = ypos;
}
void ShowPointPos(Point * ppos) // 출력
{
printf("[%d, %d] \n", ppos->xpos, ppos->ypos);
}
int PointComp(Point * pos1, Point * pos2) // 비교
{
if(pos1->xpos == pos2->xpos && pos1->ypos == pos2->ypos)
return 0;
else if(pos1->xpos == pos2->xpos)
return 1;
else if(pos1->ypos == pos2->ypos)
return 2;
else
return -1;
}
※ typedef 선언에서 보이듯이 실제로 저장을 하는 것은 Point 구조체 변수의 주소 값이다.
int main(void)
{
List list;
Point compPos;
Point * ppos;
ListInit(&list);
/*** 4개의 데이터 저장 ***/
// 다음을 4번 반복
ppos = (Point*)malloc(sizeof(Point));
SetPointPos(ppos, 2, 1);
LInsert(&list, ppos); // 포인터 구조체의 주소값을 저장
......
}
int main(void)
{
......
/*** 저장된 데이터의 출력 ***/
printf("현재 데이터의 수: %d \n", LCount(&list));
if(LFirst(&list, &ppos))
{
ShowPointPos(ppos);
while(LNext(&list, &ppos))
ShowPointPos(ppos);
}
printf("\n");
......
}
int main(void)
{
......
/*** xpos가 2인 모든 데이터 삭제 ***/
// 임의의 변수 선언
compPos.xpos=2;
compPos.ypos=0;
if(LFirst(&list, &ppos))
{
if(PointComp(ppos, &compPos)==1)
{
ppos=LRemove(&list);
free(ppos);
}
while(LNext(&list, &ppos))
{
if(PointComp(ppos, &compPos)==1)
{
ppos=LRemove(&list);
free(ppos);
}
}
}
......
}
LRemove()가 삭제 값을 반환해 주어야 하는 이유
LRemove()가 삭제 값을 반환해 주기 때문에 그 반환된 값으로 free()가 가능해진다.
동적할당된 메모리까지 소멸되나? NO!
리스트 자료구조의 책임은 삭제까지 담당하는 것이 아님.
주소값이 동적할당된 경우도 있지만 아닌 경우도 있음. 이걸 리스트 자료구조가 판단하는 것은 무리이므로 우리가 free() 해주어야 함.
삭제는 단지 주소가 가리키는 값과 주소 사이의 연결 관계를 끊는 것일 뿐이다.
배열 기반 리스트의 단점
∙ 배열의 길이가 초기에 결정되어야 한다. 변경이 불가능하다.
∙ 삭제의 과정에서 데이터의 이동(복사)가 매우 빈번히 일어난다.
배열 기반 리스트의 장점
∙ 데이터 참조가 쉽다. 인텍스 값 기준으로 어디든 한 번에 참조 가능!