✔️ 리스트(List)
데이터를 순서대로 저장한 자료구조
리스트는 배열과 연결리스트를 이용해 구현할 수 있다.
리스트 추상데이터타입
insert(list, pos, item) // pos 위치에 요소를 추가
insert_last(list, item) // 맨 끝에 요소를 추가
insert_first(list, item) // 맨 처음에 요소를 추가
delete(list, pos) // pos 위치의 요소를 제거
clear(list) // 리스트의 모든 요소를 제거
get(list, pos) // pos 위치의 요소를 반환
size() // 리스트의 길이를 반환
is_empty(list) // 리스트가 비었는지 검사
is_full(list) // 리스트가 꽉 찼는지 검사
print_list(list) // 리스트의 모든 요소를 표시
리스트는 ArrayList, Node를 활용한 List가 있다.
ArrayList는 말그대로 배열을 활용한 리스트이고
Node를 활용한 리스트는 Node 구조체를 활용한 List이다.
1. 배열로 구현
typedef struct {
int* data;
int size;
int capacity;
} ArrayList;
void init_ArrayList(ArrayList* a) {
a->capacity = 5;
a->size = 0;
a->data = (int*)malloc(a->capacity * sizeof(int)); // 동적 할당
}
int is_empty(ArrayList* a) {
return a->size == 0;
}
int is_full(ArrayList* a) {
return (a->size == a->capacity);
}
// 해당 위치의 데이터 Get
int get(ArrayList* a, int pos) {
if (pos < 0 || pos > a->capacity)
//error("위치 오류");
return;
return a->data[pos];
}
int size(ArrayList* a) {
return a->size;
}
void print_list(ArrayList* a) {
for (int i = 0; i < a->size; i++) {
printf("%d->", a->data[i]);
}
printf("\n");
}
// 마지막 위치의 인덱스에 삽입
void insert_last(ArrayList* a, int item) {
if (is_full(a)) {
a->capacity *= 2;
a->data = (int*)realloc(a->data, a->capacity * sizeof(int));
}
a->data[a->size++] = item;
}
void insert(ArrayList* a, int pos, int item) {
// 가득차지 않았고 삽입 위치가 배열범위 안이라면 삽입
if (!is_full(a) && (pos >= 0) && pos <= a->size) {
// 지정 위치에 넣기위해 기존 위치의 데이터들을 한칸씩 뒤로 미룬다.
for (int i = (a->size - 1); i >= pos; i--) {
a->data[i + 1] = a->data[i];
}
a->data[pos] = item;
a->size++;
}
// 가득찼고 pos가 범위를 안벗어난다면 용량 확장 후 삽입
else if (is_full(a) && (pos >= 0) && pos <= a->size) {
a->capacity *= 2;
a->data = (int*)realloc(a->data, a->capacity * sizeof(int));
for (int i = (a->size - 1); i >= pos; i--) {
a->data[i + 1] = a->data[i];
}
a->data[pos] = item;
a->size++;
}
}
int delete(ArrayList* a, int pos) {
if (pos < 0 || pos >= a->size)
//error("위치 오류");
return;
int item = a->data[pos];
// 특정 위치의 데이터를 삭제하기 위해 미리 데이터를 꺼내놓고,
// 그 위치를 채우기위해 뒤에서 앞으로 땡긴다.
for (int i = pos; i < (a->size - 1); i++) {
a->data[i] = a->data[i + 1];
}
a->size--;
return item;
}
void clear(ArrayList* a) {
free(a->data);
init_ArrayList(a);
}
2. 리스트로 구현
리스트의 항목들을 노드(node)에 분산하여 저장
- 노드의 구성
- 데이터 필드
- 리스트의 원소, 즉 데이터 값을 저장하는 곳
- 링크 필드
- 다른 노드의 주소값을 저장하는 장소(포인터)
- 데이터 필드
장점
- 삽입, 삭제가 용이
- 연속된 메모리 공간 필요 x
- 크기 제한 x
단점
- 구현이 어려움
- 오류가 발생하기 쉬움
연결 리스트의 종류
- 단순 연결 리스트
- 원형 연결 리스트 (마지막 Null pointer가 없음)
- 이중 연결 리스트
노드 : [데이터 | 링크]
링크또한 포인터이기 때문에 노드의 링크는 다음 노드라고 볼 수 있다.
head -> Node1 -> Node2 -> NULL
ListNode* p = head->link ;
p 는 현재 Node1을 가리키고 있다.
head->link 의미
- 노드 node1
- head가 가리키는 link값
이게 중요하다. 이 다음에 포스팅할 이중 연결리스트에서 중간 삽입이나 중간 삭제할 때 매우 헷갈린다.
이해하고 넘어가자.
typedef struct {
int data;
struct _ListNode* link;
} ListNode;
// 앞 삽입
ListNode* insert_first(ListNode* head, int value) {
ListNode* p = (ListNode*)malloc(sizeof(ListNode)); // 1
p->data = value; // 2
p->link = head; // 3
head = p; // 4
return head;
}
// preNode -> afterNode
// 이 사이에 노드를 삽입
// 새로운 노드를 만들고 afterNode쪽으로 연결
// preNode의 link를 새로운 노드에 연결
// 노드 pre 뒤에 새로운 노드 삽입
ListNode* insert(ListNode* head, ListNode* pre, int value) {
ListNode* p = (ListNode*)malloc(sizeof(ListNode));
p->data = value;
p->link = pre->link;
pre->link = p;
return head;
}
// 처음 노드 삭제
ListNode* delete_first(ListNode* head) {
ListNode* removed;
if (head == NULL) return NULL;
removed = head;
head = head->link; // 처음 노드는 삭제할 것이니 head의 다음 노드를 head 로 지정
free(removed);
return head;
}
// 노드 pre의 다음 노드를 삭제
ListNode* delete(ListNode* head, ListNode* pre) {
ListNode* removed;
removed = pre->link; // pre 다음 노드를 삭제하므로 삭제노드에 pre 다음 노드를 담는다.
pre->link = removed->link; // pre노드의 link는 삭제할 노드의 다음 노드이므로 연결
free(removed);
return head;
}
// 요소 찾기
ListNode* search_list(ListNode* head, int x) {
ListNode* p = head;
while (p != NULL) {
if (p->data == x) return p;
p = p->link;
}
return NULL; // 탐색 실패
}
// 두 연결리스트를 연결
ListNode* concat_list(ListNode* head1, ListNode* head2) {
if (head1 == NULL) return head2;
else if (head2 == NULL) return head1;
else {
ListNode* p;
p = head1;
while (p->link != NULL) {
p = p->link;
}
p->link = head2;
return head1;
}
}
// 역순환으로 head->Node -> NULL 을 NULL<- Node <- head 이렇게 변경
ListNode* reverse(ListNode* head) {
ListNode* p, * q, * r;
p = head;
q = NULL;
while (p != NULL) {
r = q;
q = p;
p = p->link;
q->link = r;
}
return q;
}
// 노드 메모리 해제 및 NULL 반환
ListNode* clear(ListNode* head) {
ListNode* p = head;
ListNode* pre;
while (p != NULL) {
pre = p;
p = p->link;
free(pre);
}
return NULL;
}
void print_list(ListNode* head) {
for (ListNode* p = head; p != NULL; p = p->link) {
if (p == NULL) {
printf("->NULL");
break;
}
printf("%d->", p->data);
}
printf("NULL\n");
}
// 사용
int main(void) {
ListNode* head = NULL;
for (int i = 0; i < 5; i++) {
head = insert_first(head, i);
print_list(head);
}
for (int i = 0; i < 5; i++) {
head = delete_first(head);
print_list(head);
}
head = insert_first(head, 1);
head = clear(head);
print_list(head);
head = insert_first(head, 1);
print_list(head);
return 0;
}
리스트의 n번째 요소를 가장 빠르게 찾을 수 있는 구현 방법은?
배열이다. 배열은 요소들이 연속된 메모리에 있기때문에 여기저기 퍼져있는 노드들을 구성한 리스트보다 더 빠르게 찾을 수 있다.
🔖 참고자료
- C로 쉽게 풀어쓴 자료구조 - 생능출판사
- 본 그림들은 참고자료를 바탕으로 작성자가 직접 그린 그림입니다.
'CS > 자료구조' 카테고리의 다른 글
큐(queue)와 덱(deque) (0) | 2024.07.02 |
---|---|
스택 (Stack) (0) | 2024.07.01 |
배열, 구조체, 포인터 (0) | 2024.06.28 |
순환 (0) | 2024.01.26 |
자료구조의 정의 (0) | 2024.01.26 |