Phase 3 · 자료구조 및 실무 프로그래밍

자료구조 기초 — 스택, 큐, 연결 리스트

데이터를 무작정 배열에 넣는 것을 넘어 목적에 맞게 데이터를 쌓고, 줄을 세우고, 기차처럼 연결하는 방법을 배웁니다. 프로그램의 실행 속도와 메모리 효율을 극대화하는 자료구조의 세계로 안내합니다.

예상 학습 시간 : 180분
난이도 : ★★★★★ 고급
사전 지식 : 구조체, 포인터, 동적 메모리
다음 단계 : 알고리즘 기초 (Step 16)
01

학습 목표

1
일반 배열의 한계(크기 고정, 중간 삭제/삽입의 어려움)를 이해한다.
2
나중에 넣은 데이터가 먼저 나오는 LIFO 구조의 스택(Stack)을 배열로 구현한다.
3
먼저 넣은 데이터가 먼저 나오는 FIFO 구조의 큐(Queue)를 배열로 구현한다.
4
포인터로 노드(Node)를 연결하여 무한 확장이 가능한 연결 리스트(Linked List)를 구현한다.
5
각 자료구조에 데이터를 삽입(Insert), 삭제(Delete), 탐색(Search)하는 로직을 이해한다.
6
큐(Queue)를 활용하여 실무적인 회원 대기열(Waiting List) 프로그램을 완성한다.
02

왜 배열만으로는 부족할까?

지금까지는 int data[100]; 과 같은 배열을 주로 사용했습니다. 배열은 인덱스(data[3])를 통해 빛의 속도로 접근(O(1))할 수 있다는 강력한 장점이 있습니다.

하지만, 만약 배열의 한가운데에 있는 데이터를 지우거나(삭제), 중간에 새 데이터를 끼워 넣으려면(삽입) 그 뒤에 있는 모든 데이터를 한 칸씩 밀거나 당겨야 하는 막대한 연산(O(n))이 발생합니다.
이러한 목적에 따라 데이터를 효율적으로 담는 그릇을 바꾸는 것이 바로 자료구조의 핵심입니다.

03

스택 (Stack) — LIFO의 마법

스택은 프링글스 통이나 접시 무더기와 같습니다. 마지막에 넣은 것(Last In)이 가장 먼저 나옵니다(First Out).

  • Push : 데이터를 꼭대기에 쌓기
  • Pop : 제일 꼭대기에 있는 데이터 꺼내기
  • Top : 현재 가장 꼭대기 위치를 가리키는 인덱스
(비어있음)
30 (Top) ← pop() 시 나감
20
10

활용처 :

  • 웹 브라우저의 '뒤로 가기' 버튼
  • 문서 편집기의 'Ctrl + Z (실행 취소)'
  • 괄호 () {} 짝 맞추기 검사
  • 함수 호출 시 생성되는 '콜 스택(Call Stack)'
⚙️ 스택 Push / Pop (배열 기반)
int stack[100];
int top = -1; // 데이터가 하나도 없을 때의 기본값

void push(int value) {
    stack[++top] = value; // top을 1 올리고 그 자리에 저장
}

int pop() {
    return stack[top--];  // 현재 top의 값을 꺼내고 top을 1 내림
}
04

큐 (Queue) — 줄서기의 미학 (FIFO)

큐는 은행 창구 대기열이나 맛집 웨이팅과 같습니다. 먼저 들어온 데이터(First In)가 먼저 나갑니다(First Out).

  • Enqueue : 줄 맨 뒤에 추가하기
  • Dequeue : 줄 맨 앞에 있는 데이터 처리 후 빼기
  • Front : 맨 앞 (출구), Rear : 맨 뒤 (입구)
↓ Front (Dequeue 시 나감) ↓ Rear (Enqueue 시 들어옴)
10
20
30
05

연결 리스트 (Linked List) — 진정한 동적 자료구조

배열의 가장 큰 단점(크기 고정, 중간 삽입/삭제의 어려움)을 포인터로 해결한 자료구조입니다. 데이터를 메모리의 흩어진 공간에 동적 할당(malloc)하고, 각 데이터가 "다음 데이터의 주소"를 들고 있게 만들어 기차처럼 연결합니다.

Head →
10
20
30
NULL

새로운 데이터를 중간에 넣고 싶다면, 기존 연결(포인터)을 끊고 새 노드의 양팔(포인터)을 엮어주기만 하면 됩니다. (밀고 당기기 연산 불필요)

⚙️ 연결 리스트의 노드 구조체
struct Node {
    int data;              // 실제 저장할 값
    struct Node *next;     // 자기 자신과 똑같이 생긴 '다음 노드'의 주소!
};

// 새로운 노드를 생성(malloc)하고 값을 넣은 뒤 연결하는 방식
⚠️ 포인터의 포인터 (**) 사용 이유

리스트의 맨 처음(Head) 자체를 바꾸는 함수 void addNode(struct Node **head)를 작성할 때는, head 포인터의 주소를 넘겨받아야 함수 안에서 원본 head를 변경할 수 있습니다. 이것이 포인터 챕터에서 배웠던 이중 포인터가 실무에서 쓰이는 대표적인 예입니다.

06

STEP 15 최종 프로젝트

🚀 Mission

큐(Queue)를 활용한 회원 대기 관리 프로그램

은행이나 맛집의 웨이팅 시스템을 구현합니다. 메뉴를 통해 대기열 맨 뒤에 회원을 추가하고, 다음 회원을 호출(처리)하면 대기열 맨 앞의 회원이 빠져나가도록 만듭니다.

  • [1] char queue[MAX][50]; 형태의 문자열 배열과 front, rear 변수를 준비합니다.
  • [2] enqueue() : queue[rear]에 이름을 복사하고 rear를 1 증가!
  • [3] dequeue() : queue[front]의 회원을 호출하고 front를 1 증가!
  • [4] 남은 대기자 목록은 front부터 rear-1까지 순회하며 출력합니다.
07

STEP 15 완료 체크리스트

데이터를 단순 저장하는 배열의 한계와, 목적에 맞게 그릇을 선택하는 자료구조의 필요성을 이해한다.
스택(Stack)의 LIFO 원리를 바탕으로 Push와 Pop 연산을 배열 기반으로 코딩할 수 있다.
큐(Queue)의 FIFO 원리를 바탕으로 Enqueue, Dequeue 시 front와 rear 포인터의 변화를 추적할 수 있다.
연결 리스트(Linked List)에서 struct Node 를 선언하고, 포인터(next)를 통해 2개 이상의 노드를 기차처럼 연결할 수 있다.
동적 할당된 연결 리스트를 while문을 통해 끝(NULL)까지 탐색하며 순차 출력할 수 있다.
C언어로 자료구조의 바닥부터 짜보기!

현대 언어(Python, JS 등)에서는 list.append() 하나면 끝나는 일들이지만, C언어에서는 우리가 직접 메모리를 뚫고 포인터 화살표를 연결해야 합니다. 이 로우 레벨(Low-Level)의 설계 감각이야말로 개발자로서 엄청난 경쟁력이 됩니다. 다음 단계에서는 이렇게 잘 정돈된 자료구조 안에서 원하는 데이터를 빛의 속도로 찾고 나열하는 알고리즘(탐색, 정렬)을 학습합니다.

이전 : Step 14 Step 16 : 알고리즘 기초