본문 바로가기

전공

자료구조_선형 구조

선형 구조

연결리스트의 개요

연결리스트의 정의 - 연결리스트(Linked List)는 데이터를 일렬로 저장하는 자료구조로, 각각의 노드(Node)가 데이터와 다음 노드를 가리키는 포인터(Pointer)로 구성되어 있다. 즉 하나의 노드에는 데이터를 담는 변수 하나와 다음노드를 가리키는 변수 하나가 있다.

 

배열과 연결리스트 비교

 - 배열 : 같은 타입(type)의 자료를 연속된 공간에 저장하기 위해 사용하는 선형 구조

 // 데이터 수가 자주 바뀌지 않고, 참조가 자주 일어날 때 배열을 사용하는 것이 유리하다.

 - 연결리스트 : 기억 공간에 물리적으로 흩어져 있는 자료들이 논리적인 순서를 갖도록 링크를 사용해 연결한 선형 구조

 // 데이터의 삽입과 삭제가 자주 일어나서 데이터의 수가 자주 바뀔 때 연결리스트를 사용하는 것이 유리하다.

구분 배열 연결리스트
검색 데이터에 직접 접근하므로 읽기 / 쓰기가 연결리스트에 비해 빠름 데이터에 순차 접근해야 하므로 읽기 / 쓰기가 배열에 비해 느림
데이터 / 구조 변경 데이터 추가/삭제 시 반복 이동이 필요함 노드 추가 / 삭제 용이
기억 공간 코딩 시 정해진 크기만큼 정적 할당 ( 연속 공간 ) 노드 필요 시 동적 할당하여 연결함 ( 불연속 공간 )
추가 기억 공간 추가 공간 없음 링크 필드 추가
소멸 지역 배열은 함수 종료 때
전역 배열은 프로그램 종료 때
매 노드마다 free() 함수를 사용
데이터 접근 시간 복잡도 O(1) O(n)

*  연결리스트는 데이터의 동적인 관리와 삽입/삭제 연산의 효율성을 중시할 때 유용한 자료구조이다. 하지만 임의 접근이 필요한 경우나 메모리 공간을 절약해야 하는 상황에서는 배열을 사용하는 것이 더욱 효율적이다.

 

연결리스트의 종류

단순 연결리스트 ( Singly Linked List )

 - 일반적으로 연결리스트는 단순 연결리스트를 의미하며, 각 노드에는 다음 노드를 가리키는 포인터만 있다.

 // 단순 연결리스트의 기본 구조

 p -> [ data | link ] -> [ data | link ] -> [ data | link ] -> [ data | * ^ ]

*  ^ - NULL을 의미한다.

 

원형 연결리스트 ( Circular Linked List )

 - 단순 연결리스트와 유사하지만, 마지막 노드가 첫 번째 노드를 가리키는 구조이다.

 - 시작과 끝이 연결된 순환 구조로 되어 있어서 주로 순회하며 탐색하는 알고리즘에 자주 사용된다.

 // 원형 연결리스트의 기본 구조

 head -> [ NULL | link ] -> [ data | link ] -> [ data | link ] -> [ data | link ] -> (data가 NULL인 노드를 가리킴 )

*  원형 연결리스트는 순환적인 자료구조에 유용하게 사용된다. 하지만 구현이 복잡할 수 있으며, 순회 중에 무한 루프에 빠질 수 있는 잠재적인 위험이 있다.

 

이중 연결리스트 ( Doubly Linked List )

 - 각 노드에 이전 노드와 다음 노드를 모두 가리키는 포인터가 있다.

 - 단순 연결리스트와 원형 연결리스트의 특징을 조합한 형태로, 양방향으로 순차 접근과 역방향 탐색이 가능하다.

 - 순환 구조로 되어 있어서 순환적인 작업에 유용하다.

 - 이전 노드에 대한 참조가 필요한 적업에 유용하지만, 단순 연결리스트보다 더 많은 메모리를 사용한다는 단점이 있다.

 // 이중 연결리스트의 기본 구조

 p1 -> [ ^ | data | link ] <-> [ link | data | link ]  <-> [ link | data | link ]  <-> [ link | data | ^ ] <- p2

*  이중 연결리스트를 이용하면 데큐(Deque)나 양방향 큐(Doubly Ended Queue)를 쉽게 구현할 수 있다.

 

연결리스트의 운용

연결리스트 생성 / 연결리스트의 마지막에 새로운 노드 추가 / 노드 소멸

#include<stdio.h>
#include<stdlib.h>
// 노드 소멸 - 재귀 호출을 통해  연결된 역순으로 노드가 사라지게 함
void deleteLinkedList(struct node* p){
	if(p!=NULL){
    	deleteLinkedList(p->link);
        printf("%c.", p->data);
        free(p);
    }
}

int main(void){
	// 연결리스트 생성
	struct node{
    	char data;
        struct node* link;
    };
    struct node* head = (struct node*)malloc(sizeof(struct node));
    struct node* p;
    char input = ' ';
    head->data = input;
    head->link = NULL;
    p = head;
    
    printf("데이터 입력 : ");
    scanf_s("%c", &input, 1);
    getchar();
    
    while(input != '0'){
    	// p가 간접 소유한 노드 다음에 새로운 노드를 추가
    	p->link = (struct node*)malloc(sizeof(struct node));
        p = p->link;	// p는 다음 노드로 이동
        p->data = input;
        p->link - NULL;
        printf("데이터 입력 : ");
	    scanf_s("%c", &input, 1);
    	getchar();
    }
    
    printf("\n연결리스트 출력 : ");
    p = head->link;
    while(p != NULL){
    	printf("%c", p->data);
        p = p->link;
        if(p != NULL) printf(" -> ");
    }
    
    // 노드 소멸
    p = head;	// p를 맨 앞으로 이동
    deleteLinkedList(p);
    head = NULL;	// head는 NULL값을 갖게 되어 참조하는 노드 없게 됨.
    return 0;
}

*  구조체를 이용해 연결리스트를 생성하면 새로운 노드를 추가하거나 기존 노드를 삭제할 때 메모리를 효율적으로 사용할 수 있다.

*  'head'는 연결리스트의 시작을 나타내며, 노드 접근 및 첫 번째 노드를 가리키는 포인터로서 중요한 역할을 한다.

*  구조체를 이용해 동적으로 할당된 노드이므로, 삭제한 이후에는 노드의 메모리를 반드시 해제해야 메모리의 누수 현상을 방지할 수 있다.

 

노드 삽입

p 다음에 노드 삽입

// 현재 포인터 p가 가리키는 노드 다음에 새로운 노드(ins) 삽입
struct node* ins = (struct node*)malloc(sizeof(struct node));
ins->data = '0';
ins->link = p->link;	// 1. p가 현재 가리키는 link주소를 새로운 노드가 가리키도록 한다.
p->link = ins;	// 2. p의 link주소가 새로운 링크를 가리키도록 한다.

 

노드 삭제

p 다음 노드 삭제

// p가 현재 가리키는 노드 다음의 del 노드 하나 삭제
struct node* del = p->link;
p->link = del->link;	// 1. del 노드가 가리키는 주소를 p가 가리키도록 한다.
free(del);	// 2. del 노드의 메모리를 해제한다.

스택(Stack)의 개요

스택의 정의

 - 스택(Stack)은 자료를 보관할 수 있는 선형 구조(Linear Structure)이다.

 - 가장 마지막에 추가된 데이터가 가장 먼저 꺼내지는 구조인, 후입선출(LIFO, Last-In-First-Out) 방식을 따른다.

 - 데이터를 넣는(push) 동작과 데이터를 꺼내는(pop) 동작을 지원한다.

*  스택(Stack)은 먼저 들어간 데이터가 나중에 나오는 선입후출(FILO, First-In-Last-Out) 방식이기도 하다.

 

 // 스택에서 사용되는 용어

  • top : 데이터 상단 / -1로 초기화 ( underflow인지 확인하기 위해서 )
  • base : 스택의 시작 위치로 가장 아래쪽 ( 배열 인덱스 0 )
  • underflow : base를 벗어나면 발생 ( 배열 인덱스 -1 이하 )
  • limit : push 할 수 있는 한계 ( 배열의 끝 )
  • overflow : limit을 벗어나면 발생
  • push() : 스택에 데이터를 삽입하는 작업 / top++
  • pop() : 스택의 top에서 데이터를 꺼내는 작업 / top--

// 스택의 장점과 단점

스택의 장점 스택의 단점
 - 구현이 간단하고, 기본적인 연산이 빠르게 수행된다.
 - 재귀 알고리즘에 사용된다.
 - 자료를 역순으로 처리할  때 유용하게 사용된다.
 - 메모리 할당과 해제가 쉽다.
 - 가장 먼저 추가된 데이터르 먼저 처리할 때는 비효율적이다.
 - 스택은 고정된 크기를 가지기 때문에 미리 크기를 예측해서 메모리를 할당해야 한다.
 - 중간에 있는 데이터의 삭제가 어렵다.

*  웹 브라우저의 뒤로 가기 기능도 스택을 이용한다. / 사용자가 방문한 페이지의 기록을 스택에 저장하고, 이후 스택에서 페이지를 꺼내어 뒤로 가기를 구현한다.

 

스택의 특징 - 스택은 데이터를 저장하고 관리하는 자료구조 중 하나이다.

  • 후입선출(LIFO)
     - 가장 최근에 삽입된 데이터가 가장 먼저 삭제되는 구조이고, 이는 데이터의 입출력 순서가 역순으로 이루어지는 것을 의미한다.
     // 재귀 알고리즘 - 최근에 호출한 함수부터 종료됨
  • 함수 호출과 재귀 알고리즘
     - 함수 호출 시 호출된 함수의 정보를 스택에 저장하며, 함수 실행이 종료되면 스택에서 해당 정보를 꺼내어 이전 상태로 돌아간다. 같은 원리로 재귀 알고리즘에 스택이 사용된다.
  • 제한된 접근
     - 스택은 상단(top)에 대한 접근만 가능하며, 다른 요소에는 직접적으로 접근할 수 없다.
     // top으로 pop(), push() 접근
     - 스택에는 오직 상단의 요소만 확인 및 삭제 작업이 가능 -> 스택을 간단하고 직관적인 자료구조로 사용할 수 있다.
  • 메모리의 한정된 사용
    = 크기가 정해진 배열일 때만 해당 / 연결리스트는 해당 xx
     - 스택은 메모리 공간을 한정적으로 사용하므로, 스택에 저장할 수 있는 데이터의 요소도 제한되어 있다.
     - 스택이 가득 찬 상태에서 삽입 연산은 스택 * 오버플로우(Stack Overflow)로 이루어질 수 있다.
  • 삽입과 삭제가 상수 시간에 이루어짐
     - 스택은 가장 상단(top)에 있는 요소만 접근하여 데이터를 삭제할 수 있다.
     - 삽입과 삭제 연산의 시간 복잡도는 상수 시간 O(1)이다.

*  오버플로 - 주어진 공간 또는 범위를 넘어서 데이터가 저장되거나 처리되는 현상

// 미로 찾기에 활용된다.

 

배열과 연결리스트의 비교 - 스택은 후입선출(LIFO, Last-In-First-Out)의 기본 규칙만 따른다면 배열(Array)과 연결리스트(Linked List) 두 가지 방법으로 모두 구현할 수 있다.

배열로 스택 구현 연결리스트로 스택 구현
 - 구현이 간단하고 빠르게 접근이 가능
 - 배열은 크기를 미리 지정해야 하므로, 스택의 크기가 유동적일 때 메모리의 낭비가 발생
 - 스택이 꽉 찬 상황에서는 새로운 요소를 추가할 수 없음 (push() 불가능)
 - 크기에 대한 제약이 없어 메모리의 유연한 사용이 가능
 - 새로운 요소를 추가할 때마다 동적으로 메모리를 할당하므로, 효율적인 메모리 사용이 가능
 // 메모리의 크기는 정해져 있기 때문에 무한하지 않아서 동적할당이 유연해도 무한하게 사용X
 - 배열에 비해 접근 속도가 느리고 구현이 복잡

 

스택(stack)의 구현

배열로 구현 - 배열의 끝에서부터 데이터를 삽입(push)하고 삭제(pop)하면서 스택을 구현한다. 이때 스택에 데이터를 삽입할 위치를 가리키고 있는 변수 top의 초기값은 -1로 설정한다.

*  배열 - 대부분의 프로그래밍 언어에서 기본적으로 제공하는 자료구조이기 때문에, 스택을 배열로 구현하면 다른 프로젝트나 코드에서 재사용하기 용이하다.

#include<stdio.h>
#define SIZE 6
int stack[SIZE] = {0, };
int top = -1;

int push(int data){
	if(top == SIZE-1){
    	printf("stack overflow!\n");
        return -1;
    }
    stack[++top] = data;
    return 0;
}

int pop(void){
	if(top == -1){
    	printf("stack underflow!\n");
        return -1;
    }
    return stack[top--];
}

int main(void){
	// 정상적으로 push()됨
    push(10); push(20); push(30); push(40); push(50); push(60);
    // overflow 발생으로 push()되지 않고, 'stack overflow!'를 출력
    push(70);
    
    // 정상적으로 pop()됨
    printf("1st pop: %d\n", pop());
    printf("2nd pop: %d\n", pop());
    printf("3rd pop: %d\n", pop());
    
    // 정상적으로 push()됨
    push(80);
    
    // 정상적으로 pop()됨
    printf("4th pop: %d\n", pop());
    printf("5th pop: %d\n", pop());
    printf("6th pop: %d\n", pop());
    printf("7th pop: %d\n", pop());
    
    // top은 -1이므로 underflow 발생.
    // 'stack underflow!'를 출력하고 리턴받은 -1도 출력
    printf("8th pop: %d\n", pop());
    return 0;
}

 

연결리스트로 구현 - 연결리스트의 노드(Node)는 데이터 필드와 다음 노드를 가리키는 포인터로 구성되어 있다. 따라서 연결리스트의 맨 앞에 새로운 노드를 삽입하고, 삭제할 때는 맨 앞의 노드를 제거한다.

*  연결리스트 - 동적인 크기 조정과 유연한 메모리 관리가 필요한 상황에서 편리하다. 하지만 포인터를 사용하기 때문에 메모리 사용량이 더 많아지고 오버해드가 발생할 수 있다.

#include <stdio.h>
#include <stdlib.h>
#define SIZE 10

struct node{
	int data;
    struct node*link;
};
struct node* top = NULL;
int cnt=0;

int push(int data){
	struct node* addNode;
    if(cnt >= SIZE){
    	printf("Stack OVerflow!\n");
        return -1;
    }
    addNode = (struct node*)malloc(sizeof(struct node));
    addNode->data = data;
    addNode->link = top;
    top = addNode;
    cnt++;
    return 0;
}

int pop(vode){
	int data;
    struct node* delNode = top;
    if(top == NULL){
    	printf("Stack Underflow!\n");
        return -1;
    }
    data = top->data;
    top = top->link;
    free(delNode);
    cnt--;
    return data;
}

int main(void){
	struct node* freeNode;
    push(10); push(20); push(30);
    printf("POP: %d\n", pop());	// POP: 30
    push(40); push(50);
    printf("스택 유효 데이터(top에서 base까지) : \n");
    while(top!=NULL){	// 50, 40, 20, 10
    	printf("%d, ", top->data);
        freeNode = top;
        top = top->link;
        free(freeNode);
    }
    return 0;
}

큐(Queue)의 개요

큐의 정의

 - 큐는 자료구조의 일종으로, 먼저 들어온 데이터가 먼저 나가는 선입선출(FIFO, First-In-First-Out) 방식으로 작동한다.

 - 일반적으로 버퍼(Buffer)로 사용되며, 네트워크 운영체제, 멀티프로세스 환경 등에 많이 사용된다.

*  큐는 데이터의 순차적인 처리와 작업의 조정을 위한 효율적인 자료구조로, 다양한 상황에서 유용하게 활용되고 있다.

// Level Order Traversal에 활용된다.


 // 큐에서 사용되는 용어

  • rear : 가장 최근에 추가한 데이터를 지시하는 포인터 또는 인덱스
  • front : 사용할 데이터를 지시하는 포인터 또는 인덱스
  • overflow : 끝까지 add()된 경우에 add()를 시도하면 발생 ( rear == SIZE-1 )
  • underflow : 더 이상 꺼낼 수 없는 경우에 delete()를 시도하면 발생 ( front < rear )
  • add() : 데이터를 추가하는 작업
  • delete() : 데이터를 꺼내 사용하는 작업

 

큐의 종류

 - 선형 큐(Linear Queue) : 가장 기본적인 큐 종류 중 하나로, 큐의 머리(front)와 꼬리(rear)를 가리키는 포인터 변수를 사용하여 데이터를 추가하고 제거한다.

*  선형 큐 - 가장 기본적이고 간단한 형태이지만, 일반적으로 배열을 사용해 구현해서 메모리 낭비 문제가 발생할 수 있다.

 

 - 환형 큐(Circular Queue) : 머리와 꼬리가 원형으로 연결되어 있으며, 큐의 가장 끝에 도달했을 때 배열의 처음으로 다시 돌아가 데이터를 저장할 수 있다.

*  환형 큐 - 메모리를 효율적으로 사용하면서도 크기 제한이 없어 더 유연한 구현이 가능하다.

 // 변수 rear, front가 반복적으로 계속 사용되어 int형의 최대 범위에 도달하게 되면 초기화하는 작업이 필요함.

 

 - 데큐(DeQueue / Double-ended Queue) : 큐의 앞과 뒤에서 모두 삽입과 삭제가 가능하다. 즉, 큐의 특징인 FIFO(First-In-First-Out)와 스택의 특징인 LIFO(Last-In-First-Out)를 모두 갖고 있다.

*  데큐 - 양방향으로 데이터를 처리할 수 있어, 스크롤링, 검색 등 양쪽 방향으로 데이터에 접근해야 하는 상황에 유용하다. 

 

- 우선순위 큐(Priority Queue)

 : 우선순위가 다른 다수의 큐를 동시에 운영하며, 데이터를 저장할 때는 데이터의 우선순위를 고려하여 해당 큐에 저장하고, 꺼낼 때는 우선순위가 가장 높은 데이터를 먼저 꺼내는 자료구조이다.

 : 긴급한 데이터일수록 우선순위가 높은 큐에 저장되며, 우선순위가 높은 큐부터 삭제 작업이 이루어진다.

 : 운영체제의 프로세스 작업 스케줄링 및 네트워크 패킷 스케줄링 등에 사용된다.

 // 비선점 스케줄링 - 이미 할당된 CPU를 다른 프로세스가 강제로 빼앗아 사용할 수 없는 스케줄링 기법

 // 선점 스케줄링 - 하나의 프로세스가 CPU를 할당받아 실행하고 있을 때, 우선순위가 높은 다른 프로세스가 CPU를 강제로 빼앗아 사용할 수 있는 스케줄링 기법

*  우선순위 큐 - 데이터에 우선순위를 할당하여 처리하는 큐이다. 각 데이터는 우선순위와 함께 큐에 추가되며, 우선선위가 높은 데이터가 먼저 처리된다.

*  우선순위가 낮은 큐에 있는 데이터는 무한정 대기해야 할 가능성이 있으므로 일정 시간이 경과하면 우선순위가 높은 큐로 이동시켜야 한다.

 

큐의 구현

배열로 구현 - 포인터를 사용해 큐에 데이터를 삽입(rear)하고 삭제(front)할 수 있다. 즉 포인터를 이용해 처음과 끝 인덱스를 추적하고, 추가 및 제거 작업은 인덱스를 조정하여 처리한다.

*  배열은 큐를 구현하는 데 있어서 편리하고 효율적인 자료구조이다. 하지만 크기가 고정되어 있기 때문에 크기를 동적으로 조정해야 하는 경우에는 별도로 배열 재할당과 데이터 이동 과정이 필요하다.

// 배열로 선형 큐 구현
#include <stdio.h>
#define SIZE 6
int queue[SIZE] = {0, };
int rear = -1, front = 0;

int add(int data){
	if(rear == SIZE -1){
    	printf("Queue is full!\n");
        return -1;
    }
    queue[++rear] = data;
    return 0;
}

int delete(void){
	if(front < rear){
    	printf("Queue is empty!\n");
        return -1;
    }
    return queue[front++];
}

int main(void){
	add(10); add(20);
    printf("%d\n", delete());	// 10
    printf("%d\n", delete());	// 20
    printf("%d\n", delete());	// Queue is empty!	-1
    add(30); add(40); add(50); add(60);
    add(70);	// Queue is full!
	printf("%d\n", delete());	// 30
    printf("%d\n", delete());	/ 40
    add(80);	// Queue is full!
    return 0;
}
// 배열로 환형 큐 구현
#include <stdio.h>
#define SIZE 6
int cqueue[SIZE];
int rear = -1, front = 0, cnt = 0;

int add(int data){
	if(cnt == SIZE){
    	printf("Circular Queue is Full!\n");
        return -1;
    }
    cnt++;
    cqueue[++rear % SIZE] = data;
    return 0;
}

int delete(void){
	if(cnt == 0){
    	printf("Circular Queue is Empty!\n");
        return -1;
    }
    cnt--;
    // delete()호출문 종료 직후 front 1 증가
    return cqueue[front++ % SIZE];
}

int main(void){
	add(10); add(20); add(30);
    printf("%d\n", delete());	// 10
    printf("%d\n", delete());	// 20
    printf("%d\n", delete());	// 30
    printf("%d\n", delete());	// Queue is Empty!	-1
    add(40); add(50); add(60); add(70); add(80); add(90);
    add(100);	// Queue is Full!
    return 0;
}

 

연결리스트로 구현 - front와 rear를 가리키는 두 개의 포인터를 이용해 연결리스트 상에서의 삽입과 삭제 연산이 발생할 때 위치값을 조정하며 처리한다.

*  큐를 연결리스트로 구현하면, 큐의 앞이나 뒤에 요소를 추가하거나 삭제하는 경우 연결리스트가 해당 노드의 연결을 조정함으로써 삽입 및 삭제 작업을 상수 시간 O(1)에 수행할 수 있다.

// 연결리스트를 사용해 선형 큐를 구현
#include <stdio.h>
#include <stdlib.h>
#define SIZE 6

struct node{
	int data;
    struct node * link;
};
struct node* rear - NULL, *front - NULL;
int cnt=0;

int add(int data){
	if(cnt -- SIZe){
    	printf("!ueue is Full!\n");
        return -1;
    }
    if(rear == NULL){
    	rear = (struct node*)malloc(sizeof(struct node));
        front = rear;
    }
    else{
    	rear->link =(strct node*)malloc(sizeof(struct node));
        rear = rear->link;
    }
    rear->data = data;
    cnt++;
    return 0;
}

int delete(void){
	int value;
    struct node* del = front;
    if(cnt==0){
    	prinf("Qudue is Empty!\n");
        return -1;
    }
    value = front->data;
    front = front->link;
    if(front == NULL) rear = NULL;
    free(del);
    cnt--;
    return value;
}

int main(void){
	add(10); add(20); add(30);
    printf("%d\n", delete());	// 10
    printf("%d\n", delete());	// 20
    printf("%d\n", delete());	// 30
    printf("%d\n", delete());	// Queue is Empty!	-1
    add(40); add(50); add(60); add(70); add(80); add(90);
    add(100);	// Queue is Full!
    return 0;
}

 

'전공' 카테고리의 다른 글

Java_예외 처리, 스레드  (0) 2024.10.08
Java_컬렉션 제네릭  (7) 2024.10.08
파이썬_변수, 자료형 / 제어문, 반복문 / 함수  (0) 2024.07.10
자료구조_비선형 구조  (0) 2024.07.09
자료구조_자료구조와 알고리즘  (0) 2024.07.09