-
리스트(List)란?Data Structure 2024. 5. 14. 00:28728x90
리스트(list), 선형 리스트(Linear list)란?
▶ 순서를 가진 항목들의 모임
▶ 집합: 항목간의 순서의 개념이 없음
리스트의 예
▶ 요일: (일요일, 월요일, ..., 토요일)
▶ 한글 자음의 모임: (ㄱ, ㄴ, ..., ㅎ)
▶ 카드: (Ace, 2, 3, ..., King)
▶ 핸드폰의 문자 메시지 리스트
리스트 리스트의 연산은 무엇이 있을까?
▶ 새로운 항목을 리스트의 끝, 처음, 중간에 추가한다.
▶ 기존의 항목을 리스트의 임의의 위치에서 삭제한다.
▶ 모든 항목을 삭제한다.
▶ 기존의 항목을 대치한다.
▶ 리스트가 특정한 항목을 가지고 있는지를 살핀다.
▶ 리스트의 특정 위치 항목을 반환한다.
▶ 리스트 안의 항목 개수를 센다.
▶ 리스트가 비었는지, 꽉 찼는지를 체크한다.
▶ 리스트 안의 모든 항목을 표시한다.
등 등..
그럼 위의 내용을 바탕으로 리스트의 ADT를 생성한다면?
리스트의 ADT 리스트 ADT의 사용 예시를 그림과 코드로 살펴보자.
리스트 ADT 예: 그림 리스트 ADT 예: 코드 리스트를 구현하는 방법
▶ 배열을 이용하는 방법
구현은 간단하지만, 삽입, 삭제 시에 오버헤드가 발생하고, 항목의 개수가 제한된다.
▶ 연결리스트를 이용하는 방법
구현은 복잡하지만, 삽입, 삭제가 효율적이며, 크기가 제한되지 않는다.
배열과 연결리스트를 이용한 리스트 구현 우선, 배열로 리스트를 구현해보자.
방법은 1차원 배열에 항목들을 순서대로 저장한다.
삽입 연산: 삽입 위치 다음의 항목들을 이동하여야 함.
1차원 배열 삽입 연산 삭제 연산: 삭제 위치 다음의 항목들을 이동하여야 함.
1차원 배열 삭제 연산 ArrayListType의 구현
▶ 항목들의 타입은 element로 정의
▶ list라는 1차원 배열에 항목들을 차례대로 저장
▶ length에 항목의 개수 저장
▶ is_empty 연산과 is_full 연산의 구현
ArrayListType의 삽입 연산
▶ add 함수는 먼저 배열이 포화상태인지를 검사하고 삽입 위치가 적합한 범위에 있는지를 검사한다.
▶ 삽입 위치 다음에 있는 자료들을 한 칸씩 뒤로 이동한다..
ArrayListType의 삽입 연산 ArrayListType의 삭제 연산
▶ 삭제 위치를 검사한다. (is_empty 기능을 포함)
▶ 삭제 위치부터 마지막 자료를 한 칸씩 앞으로 옮긴다.
ArrayListType의 삭제 연산 그 다음, 연결 리스트의 방법이다.
아까, 리스트를 표현하는 방법에는 2가지가 있다고 했다.
순차 표현: 배열을 이용한 리스트 표현
연결된 표현: 연결 리스트를 사용한 리스트 표현, 하나의 노드가 데이터와 링크로 구성되어 있고, 링크가 노드들을 연결한다.
연결된 표현
▶ 리스트의 항목들을 노드(node)라고 하는 곳에 분산하여 저장
▶ 다음 항목을 가리키는 주소도 같이 저장
▶ 노드 (node) : <항목, 주소> 쌍
▶ 노드는 데이터 필드와 링크 필드로 구성
데이터 필드: 리스트의 원소, 즉 데이터 값을 저장하는 곳
링크 필드: 다른 노드의 주소값을 저장하는 장소 (포인터)
▶ 메모리 안에서 노드의 물리적 순서가 리스트의 논리적 순서와 일치할 필요 없음
연결된 표현의 장단점
장점
▶ 삽입, 삭제가 보다 용이하다.
▶ 연속된 메모리 공간이 필요 없다.
▶ 크기 제한이 없다.
단점
▶ 구현이 어렵다.
▶ 오류가 발생하기 쉽다.
연결 리스트의 구조
▶ 노드 = 데이터 필드 + 링크 필드
노드 ▶ 헤드 포인터(head pointer): 리스트의 첫 번째 노드를 가리키는 변수
헤드 포인터 ▶ 노드의 생성: 필요할 때마다 동적 메모리 생성 이용하여 노드를 생성
노드의 생성 연결 리스트의 종류
연결 리스트의 종류 ▶ 단순 연결 리스트
하나의 링크 필드를 이용하여 연결한다.
마지막 노드의 링크 값은 NULL
단순 연결 리스트 단순 연결 리스트의 삽입 연산 (1)
단순 연결 리스트의 삭제 연산 (1)
단순 연결 리스트의 구현
▶ 데이터 필드: 구조체로 정의
▶ 링크 필드: 포인터 사용
▶ 노드의 생성: 동적 메모리 생성 라이브러리 malloc 함수 이용
▶ 데이터 필드와 링크 필드 설정
▶ 두 번째 노드 생성과 첫 번째 노드와의 연결
▶ 헤드 포인터(head pointer)
연결 리스트의 맨 처음 노드를 가리키는 포인터
단순 연결 리스트의 삽입 연산 (2)
▶ 삽입 함수의 프로토타입
헤드포인터가 함수 안에서 변경되므로 헤드포인터의 포인터 필요 ▶ 삽입의 3가지 경우
1) head가 NULL인 경우: 공백 리스트에 삽입
2) p가 NULL인 경우: 리스트의 맨 처음에 삽입
3) 일반적인 경우: 리스트의 중간에 삽입
▶ head가 NULL인 경우
head가 NULL이라면 현재 삽입하려는 노드가 첫 번째 노드가 된다. 따라서 head의 값만 변경하면 된다.
head가 NULL인 경우 ▶ p가 NULL인 경우
p가 NULL이라면 새로운 노드를 리스트의 맨 앞에 삽입한다.
p가 NULL인 경우 ▶ head와 p가 NULL이 아닌 경우
가장 일반적인 경우이다.new_node의 link에 p->link값을 복사한 다음, p->link가 new_node를 가리키도록 한다.
head와 p가 NULL이 아닌 경우 방문 연산 코드(display)
▶ 방문 연산: 리스트 상의 노드를 순차적으로 방문
▶ 반복(iteration)과 순환(recursion) 기법을 모두 사용 가능
▶ 반복 버전(iteration)
▶ 순환 버전(recursion)
단순 연결 리스트의 삭제 연산(2)
▶ 삭제 함수의 프로토타입
▶ 삭제의 2가지 경우
1) p가 NULL인 경우: 맨 앞의 노드를 삭제
2) p가 NULL이 아닌 경우: 중간 노드를 삭제
▶ p가 NULL인 경우
연결 리스트의 첫 번째 노드를 삭제한다. 헤드 포인터 변경▶ p가 NULL이 아닌 경우
removed 앞의 노드인 p의 링크가 removed 다음 노드를 가리키도록 변경
합병 연산 코드(concat)
▶ 합병 연산: 2개의 리스트를 합하는 연산
역순 연산 코드(reverse)
▶ 역순 연산: 리스트의 노드들을 역순으로 만드는 연산
탐색 연산 코드(search)
▶ 탐색 연산: 특정한 데이터 값을 갖는 노드를 찾는 연산
이를 바탕으로 simpleLinkedList를 전체 구현해본 결과는 다음과 같다.
#include <stdio.h> #include <stdlib.h> typedef struct ListNode { int data; // 데이터를 저장하는 변수 struct ListNode *link; // 다음 노드를 가리키는 포인터 } ListNode; typedef int element; // 노드 생성 코드 ListNode *create_node(element data, ListNode *link) { ListNode *new_node; new_node = (ListNode *)malloc(sizeof(ListNode)); if(new_node == NULL) printf("메모리 할당 에러\n"); new_node->data = data; new_node->link = link; return new_node; } // 리스트 삽입 연산 // phead: 리스트의 헤드 포인터의 포인터 // p: 선행 노드 // new_node: 삽입될 노드 void insert_node(ListNode **phead, ListNode *p, ListNode *new_node) { if( *phead == NULL ) { // 공백 리스트인 경우 *phead = new_node; } else if( p == NULL ) { // p가 NULL이면 첫 번째 노드로 삽입 new_node->link = *phead; *phead = new_node; } else { // p 다음에 삽입 new_node->link = p->link; p->link = new_node; } } // 리스트 방문 연산 // iteration의 방법 void display(ListNode *head) { ListNode *p = head; while( p != NULL ) { printf("%d->", p->data); p = p->link; } printf("\n"); } // recursion의 방법 void display_recur(ListNode *head) { ListNode *p = head; if( p != NULL ) { printf("%d->", p->data); display_recur(p->link); } } // 리스트 삭제 연산 // phead: 헤드 포인터에 대한 포인터 // p: 삭제될 노드의 선행 노드 // removed: 삭제될 노드 void remove_node(ListNode **phead, ListNode *p, ListNode *removed) { if( p != NULL ) { *phead = removed->link; // *phead = (*phead)->link; } else { p->link = removed->link; } free(removed); } // 리스트 합병 연산 ListNode *concat(ListNode *head1, ListNode *head2) { ListNode *p; if( head1 == NULL ) { return head2; } else if( head2 == NULL ) { return head1; } else { p = head1; while( p->link != NULL ) p = p->link; p->link = head2; return head1; } } // 리스트 역순 연산 ListNode *reverse(ListNode *head) { // 순회 포인터로 lead, middle, trail을 사용 ListNode *lead, *middle, *trail; lead = head; // lead는 역순으로 만들 리스트 middle = NULL; // middle는 역순으로 만들 노드 while (lead != NULL) { // trail은 역순으로 된 리스트. trail은 middle을, middle은 lead를 차례로 따라간다. trail = middle; middle = lead; lead = lead->link; middle->link = trail; // middle의 링크 방향을 바꾼다. } return middle; // middle은 역순으로 된 리스트의 헤드 포인터 } // 리스트 탐색 연산 ListNode *search(ListNode *head, int x) { ListNode *p; // ListNode *p = head; p = head; while( p != NULL ) { if( p->data == x ) return p; // 탐색 성공 p = p->link; } return p; // 탐색 실패일 경우 NULL 반환 } // main 연결 리스트 실행문 int main() { ListNode *list1 = NULL, *list2 = NULL; ListNode *p; // list1 = 30 -> 20 -> 10 insert_node(&list1, NULL, create_node(10, NULL)); insert_node(&list1, NULL, create_node(20, NULL)); insert_node(&list1, NULL, create_node(30, NULL)); display(list1); // list1 = 20 -> 10 remove_node(&list1, NULL, list1); display(list1); // list2 = 80 -> 70 -> 60 insert_node(&list2, NULL, create_node(60, NULL)); insert_node(&list2, NULL, create_node(70, NULL)); insert_node(&list2, NULL, create_node(80, NULL)); display(list2); // list1 = list1 + list2 list1 = concat(list1, list2); display(list1); list1 = reverse(list1); display(list1); p = search(list1, 20); if(p != NULL) printf("검색 결과: %d \n", p->data); return 0; }
게시글이 너무 길어져서 원형 연결 리스트와 이중 연결 리스트, 연결 리스트의 응용은 다음 게시글에서 쓰도록 하겠다.
다음에 계속...
728x90'Data Structure' 카테고리의 다른 글
트리(Tree)란? (0) 2024.05.26 CircularLinkedList, DoublyLinkedList, etc... (0) 2024.05.14 큐(Queue)란? (4) 2024.04.28 스택(Stack)이란? (0) 2024.04.27 포인터(Pointer)와 동적 메모리 할당 (0) 2024.04.27