[Date Structure] 포인터와 연결리스트
Linked list : 물리적으로 흩어져 있는 자료들을 서로 연결하여 하나로 묶는 방법
Linked list는 데이터를 저장하는 data field와 다른 노드의 주소를 저장하는 포인터 변수를 가지고 있는link field가 있다.
1. 포인터
- Pointer variable(or pointer) : 메모리의 주소를 저장하기 위한 변수
포인터의 선언
포인터 변수의 타입은 가리키는 변수의 자료형과 일치해야 한다.
float *pf; // float 형 변수를 가리킬 목적의 포인터 pf 선언
int* pi; // int 형 변수를 가리킬 목적의 포인터 pi 선언 - 이 방법이 자료형과 변수가 구분되어 우수하다.
char* p, q, r; // char* 변수 p와 char 변수 q, r을 선언
char *p, *q, *r // char* 변수 p, q, r 선언 - 한 문장으로 여러 개의 포인터 변수를 선언할 수 있다.
포인터는 사용하기 전에 반드시 초기화해야 한다.
int i = 10;
int* p;
p = &i;
포인터의 활용
- 역참조 연산자(*) : 포인터가 가리키는 메모리의 내용을 추출하거나 변경할 때 사용한다.
*p = 10
i = 10 // *p와 i는 완전히 동일하며, 두 문장은 동일하다.
어떤 포인터가 다른 포인터를 가리킬 수 있다.
2. 동적 메모리 할당
Static memory allocation(정적 메모리 할당) : 프로그램이 컴파일될 때 변수나 객체가 사용할 메모리를 미리 할당하는 방식
int x; // int 형 변수 i를 정적으로 할당
int* p; // 포인터 변수 p를 정적으로 할당
int A[10]; // 길이가 10인 배열을 정적으로 할당
예를 들어 위와 같은 문장이 실행되면 int 형의 크기(4byte)의 메모리가 자동으로 만들어진다.
x = 10;
p = &x;
이후 위와 같은 문장을 이용하여 이 변수를 사용할 수 있다. 정적 메모리 할당은 다음과 같은 특징이 있다.
- 필요한 메모리의 크기가 프로그램이 컴파일 될 때 결정되고 프로그램의 실행 중에 크기를 변경할 수 없다.
- 변수를 선언만 하면 자동으로 메모리가 할당되고 해당 프로그램 블록이 끝나면 자동으로 제거된다.(개발자는 메모리의 제거에 신경 쓸 필요가 없다.)
하지만 입력의 크기를 미리 알 수 없는 경우에 고정된 크기의 메모리를 미리 할당해야 하기 때문에 배열을 정적으로 할당하는 것은 경우에 따라 비효율적일 수 있다. 만약 할당된 크기보다 더 큰 입력이 들어온다면 처리하지 못 할 것이며, 더 작은 입력이 들어온다면 남은 메모리 공간이 낭비될 것이다. 이 문제를 해결하기 위해 dynamic memory allocation(동적 메모리 할당) 방법이 제시된다.
동적 메모리 할당은 다음과 같은 특징이 있다.
- 동적 메모리 할당 라이브러리 함수를 사용해 메모리를 생성하고, 필요 없으면 해제해야 한다.(개발자가 메모리의 생성과 해제를 직접 관리해야 하는 번거로움이 있다.)
- 프로그램 실행 중에 필요한 메모리의 크기를 결정하고 시스템으로부터 할당받아서 사용할 수 있다.(필요한 만큼만 할당하고 반납하므로 메모리를 매우 효율적으로 사용할 수 있다.)
동적 메모리 할당 라이브러리 함수
동적으로 메모리를 할당하여 주소를 반환하는 함수는 다음과 같다.
void* malloc(int size);
void* calloc(int num, int size);
malloc() 함수 : size 크기의 메모리 블록을 할당하고, 이 메모리 주소의 시작 주소를 byte 단위로 반환한다.
calloc() 함수 : 배열 요소의 크기는 size 바이트이고 개수는 num인 배열 형식의 메모리를 할당하고, 이 메모리의 시작 주소를 byte 단위로 반환한다.
char* pc = (char*)malloc(5); // 문자 5개를 저장할 공간 할당
int* pi = (int*)malloc(sizeof(int)); // int를 저장할 공간 할당
malloc() 함수와 calloc() 함수가 반환하는 void* 형이므로 적절한 자료형으로 형변환해서 사용해야 한다.
메모리를 동적으로 해제하는 함수는 다음과 같다.
void free(void* ptr);
free(pc);
free(pi);
free() 함수 : 동적으로 할당되었던 메모리 블록을 시스템에 반납한다.
ptr은 할당되었던 메모리 블록을 가리키는 주소값이다. 할당과 달리 해제시에는 자료형을 명시할 필요가 없다.
포인터와 동적 메모리 할당
int* pi = (int*)malloc(sizeof(int)*10);
Polynomial* pa = (Polynomial*)malloc(sizeof(Polynomial));
pi[3] = 10; // 동적 할당된 메모리를 배열처럼 사용
pa -> degree = 5; // 동적 할당된 다항식 구조체의 멤버 변경
free(pi);
free(pa);
구조체 배열도 동일한 방법으로 동적으로 할당하고 해제할 수 있다. 다항식 구조체 Polynomial의 배열을 동적으로 할당하여 다항식을 10개를 저장하는 예를 보자.
Polynomial* list = (Polynomial*) malloc(sizeof(Polynomial)*10);
list[0].degree = 0; // 0번째 다항식의 degree를 0으로 초기화
list[9].coef[2] = 3.1; // 9번째 다항식의 2번째 계수를 3.1로 설정
...
free(list); // Poly 배열을 동적으로 해제
여기서 list는 구조체 배열이고, list[k]는 하나의 다항식 구조체이다.
3. 연결 리스트
- 연결 리스트 : 물리적으로 흩어져 있는 자료들을 서로 연결하여 하나로 묶는 방법
- 연결된 표현 : 데이터와 링크로 구성되어 있으며, 링크가 노드들을 연결한다. 줄(링크, 포인터)로 연결된 상자(데이터)라고 생각할 수 있다.
- 연결된 표현 스택, 큐, 리스트, 덱, 트리, 그래프 등 여러 가지의 자료구조를 구현하는 데 널리 사용된다.
- 데이터들이 메인 메모리상에서 한군데 모아져 있는 것이 아니라 어디에나 흩어져서 존재한다.
- 각각의 데이터는 다음 데이터를 가리키는 줄을 가지고 있어 순서를 유지한다.
- 첫 데이터에서부터 순서대로 줄을 따라가면 모든 데이터를 방문할 수 있다.
배열에 대한 연결 리스트의 장점
Array | Linked list | |
---|---|---|
처음에 선언한 크기로 고정된다. | 메모리 | 메모리를 할당할 수 있는 한 자료의 제한이 없다. |
자료를 추가할 때 그 위치 이후의 모든 자료들을 뒤로 하나씩 복사한 후 그 자리에 자료를 넣어야 한다. | 삽입 | 데이터 B와 C 사이에 데이터 N을 삽입하고자 하면, B에서 N을 가리키도록 하고 N이 C를 가리키도록 링크만 수정하면 된다. |
데이터를 옮겨야 한다. | 삭제 | 데이터들을 연결하는 줄만 수정하면 된다. |
한꺼번에 많은 공간을 할당해야 한다.(정적 메모리 할당) | 메모리 할당 | 데이터 저장을 위한 메모리 공간이 필요할 때마다 동적으로 만들어 쉽게 추가할 수 있다. |
쉽다. | 구현 | 어렵다. 오류가 발생하기 쉽다. |
연결 리스트 구조
연결 리스트 용어 | 정의 |
---|---|
연결 리스트 | 서로 연결된 노드들의 집합 |
노드 | 데이터 필드와 링크 필드로 구성된다. |
데이터 필드 | 정수나 구조체의 자료형의 데이터가 들어간다. |
링크 필드 | 다음 노드의 주소를 저장하는 포인터 변수를 담고 있어 다음 노드를 가리킨다. |
헤드 포인터 | 연결 리스트에서 첫 번째 노드의 주소를 저장하는 포인터 |
연결 리스트는 첫 번째 노드를 알아야 링크로 매달려 있는 모든 노드에 순차적으로 접근할 수 있기 때문에 첫 번째 노드의 주소를 반드시 저장해야 한다. 마지막 노드는 링크 필드의 값을 NULL로 설정하여 이 노드가 마지막임을 알린다.
연결 리스트의 종류
- 단순 연결 리스트
- 원형 연결 리스트
- 이중 연결 리스트