[자료구조] 연결 리스트 (Linked List)
1) 연결 리스트(Linked List)
- 자료의 논리적인 순서와 물리적인 순서가 일치하지 않는 자료구조
- 각 원소에 저장되어 있는 다음 원소의 주소에 의해 순서가 연결되는 방식
- 물리적인 순서를 맞추기 위한 오버 헤드가 발생하지 않음
- 여러 개의 작은 공간을 연결하여 하나의 전체 자료구조를 표현
- 크기 변경이 유연하고, 더 효율적으로 메모리를 사용
typedef struct
{
int data; // 노드 배부의 실제 데이터 또는 레코드
Node* next; // Next가 가리키는 것은 노드 타입
}Node;
void main()
{
Node* p;
p = (Node*)malloc(sizeof(Node));
p->data = 33;
p->next = (Node*)malloc(sizeof(Node));
p->next->data = 22;
p->next->next = NULL;
}
자유공간 리스트(Free Space List)
- 사용하기 전의 메모리나 사용이 끝난 메모리를 관리의 용이성을 위해 노드로 구성하여 연결한 리스트
- 큐(선입 선출)와 유사함
데이터 출력
temp = head;
while(temp != null)
{
printf("%d ", temp->data);
temp = temp->next;
}
데이터 삽입
- 삽입할 노드를 자유 공간 리스트에서 할당 받음
- 새 노드의 데이터 필드에 data를 저장
- 새 노드 p가 리스트의 첫 번째 노드(temp->next)를 가리키게 한다.
- 첫 번째 노드(temp->next)가 p로 갱신해준다.
p = (Node*)malloc(sizeof(Node)); // 1.
p->data = 8; // 2.
p->next = temp->next; // 3.
temp->next = p; // 4.
데이터 삭제
- 노드 temp의 다음 노드의 주소(temp->next)를 포인터 p에 저장하여, 포인터 p가 다음 노드를 가리키게 함
- 만약 노드 temp가 리스트의 마지막 노드였다면 temp->next의 값은 null이므로 포인터 p의 값은 null이 됨. 결국 노드 temp 다음에 삭제할 노드가 없다는 의미이므로 삭제연산을 수행하지 못하고 반환(return)
- 삭제할 노드 p의 다음 노드(p->next)를 노드 temp의 다음 노드(temp->next)로 연결
- 삭제한 노드 p를 자유 공간리스트에 반환
if (temp == NULL) return;
else
{
p = temp->next; // 1.
if(p == NULL) return; // 2.
temp->next = p->next; // 3.
free p; // 4.
}
2) 원형 연결 리스트(Circular Linked List)
- 단순 연결 리스트에서 마지막 노드가 리스트의 첫 번째 노드를 가리키게 하여 리스트의 구조를 원형으로 만든 연결 리스트
- 단순 연결 리스트의 마지막 노드의 링크 필드에 첫 번째 노드의 주소를 저장하여 구성
- 링크를 따라 계속 순회하면 이전 노드에 접근 가능
데이터 삽입
- 마지막 노드의 링크를 첫 번째 노드로 연결하는 부분만 제외하고는 단순 연결 리스트에서의 삽입 연산과 같다.
- 첫 번째 노드로 삽입하기
InsertFistNode(head, x)
{
Node* new = getNode();
new.data = x;
if(head == NULL) // 공백 리스트일 경우
{
head = new; // 포인터 CL이 new를 가리키게 한다.
new.link = new; // 첫 노드가 자신을 가리키게 한다.
}
else
{
Node* temp = head; // temp가 첫 노드를 가리키기
while(temp.link != head) // 마지막 노드까지 이동하기
temp = temp.link;
new.link = temp.link; // 새로운 노드가 현재의 첫 노드를 가리킨다.
temp.link = new; // 마지막 노드의 다음 노드는 첫번째 노드
head = new; // 헤드가 새로운 노드를 가리킨다.
}
}
- 중간 노드로 삽입하기
InsertMiddleNode(head, pre, x)
{
Node* new = getNode();
new.data = x;
if(head == NULL) // 공백 리스트일 경우
{
head = new; // 포인터 CL이 new를 가리키게 한다.
new.link = new; // 첫 노드가 자신을 가리키게 한다.
}
else
{
new.link = pre.link;
pre.link = new;
}
}
데이터 삭제
- 원형 연결 리스트 head에서 포인터 pre가 가리키는 노드의 다음 노드를 삭제하고 삭제한 노드는 자유공간 리스트에 반환하는 연산
DeleteNode(head, pre)
{
if(head == NULL) return;
else
{
Node* old = pre.link; // pre.link를 삭제할 노드 old로 지정
pre.link = old.link;
if(old == head) // 삭제할 노드가 head인 경우
head = old.link; // 두 번째 노드가 첫 번째 노드가 되도록 조정
free(old); // 메모리 해제
}
}
3) 이중 연결 리스트(Doubly Linked List)
- 양쪽 방향으로 순회할 수 있도록 노드를 연결한 리스트
- 두 개의 Link 필드와 Data 필드로 구성
- Left Link
- Right Link
typedef struct
{
Dnode* lLink;
char data[5];
Dnode* rLink;
}Dnode;
데이터 삽입
InsertNode(head, pre, x)
{
Node* new = getNode();
new.data = x;
new.rLink = pre.rLink;
pre.rLink = new;
new.lLink = pre;
new.rLink.lLink = new;
}
데이터 삭제
DeleteNode(head, old)
{
old.lLink.rLink = old.rLink;
old.rLink.lLink = old.lLink;
free(old);
}
4) 원형 이중 연결 리스트(Circular Linked List)
- 이중 연결 리스트를 원형으로 구성
- 장점 : 단순 연결, 이중 연결, 원형 리스트의 장점을 모두 가지고 있다.
- 단점 : 비용이 많이 든다.
5) 배열과 연결리스트 비교
- 공간
- 배열은 정적이므로 최대크기를 미리 예상해야 함
- 만들어 놓은 배열 공간이 실제로 쓰이지 않으면 낭비
- 연결 리스트는 실행 시 필요에 따라 새로운 노드 생성
- 연결 리스트는 포인터 변수공간을 요함
- 검색 시간
- 배열은 단번에 찾아감(묵시적 어드레싱: 인덱스 연산)
- 연결 리스트는 헤드부터 포인터를 따라감(현시적 어드레싱: 포인터 읽음)
- 삽입, 삭제 시간
- 배열의 삽입 : 오른쪽 밀림(Shift Right)
- 배열의 삭제 : 왼쪽 밀림(Shift Left)
- 연결 리스트 : 인접 노드의 포인터만 변경
- 어떤 자료구조를 선택할 것인가?
- 검색 위주이면 배열이 유리
- 삽입 삭제 위주라면 연결 리스트가 유리
- 최대 데이터 수가 예측 불가라면 연결 리스트
6) 다항식의 연결 자료구조 표현
- 노드 구조
- 각 항에 대해서 계수와 지수를 저장
- 계수를 저장하는 coef와 지수를 저장하는 expo의 두 개의 필드로 구성
- 링크 필드 : 다음 항을 연결하는 포인터로 구성
typedef struct
{
float coef;
int expo;
Node* link;
}Node;
다항식의 덧셈 알고리즘
- *p : 다항식의 A(x)에서 비교할 항을 지시
- *q : 다항식의 B(x)에서 비교할 항을 지시
- *r : 덧셈 연산 결과 만들어지는 다항식 C(x)의 항을 지시
addPoly(A, B)
{
Node* p = A;
Node* q = B;
Node* C = NULL;
Node* r = NULL;
float sum = 0;
while (p != NULL & q != NULL) // p와 q가 null이 아닌 동안 반복
{
if(p.expo == q.expo) // p 차수가 q 차수와 같은 경우
{
sum = p.coef + q.coef // 계수의 합을 구함
if (sum != 0) // C 다항식에 지수와 합을 계수로 추가
appendTerm(C, sum, p.expo, r);
p = p.link; // p 다항식의 다음 차수를 p가 가리킴
q = q.link; // q 다항식의 다음 차수를 q가 가리킴
}
else if(p.expo < q.expo) // p 차수가 q 차수보다 작은 경우
{
appendTerm(C, q.coef, q.expo, r);
q = q.link;
}
else // p 차수가 q 차수보다 큰 경우
{
appendTerm(C, p.coef, p.expo, r);
p = p.link;
}
}
while (p != null) // p의 나머지 항 복사
{
appendTerm(C, p.coef, p.expo, r);
p = p.link;
}
while (q != null) // q의 나머지 항 복사
{
appendTerm(C, q.coef, q.expo, r);
q = q.link;
}
return C;
}