링크드리스트(LinkedList) 구현 (C언어) : Part.6 - 수정 및 회고
지금까지 링크드 리스트를 구현했다. 하지만 내가 처음 구현할 때 미처 생각지 못하고 넘어갔던 내용이 있다. 모든 함수를 Node를 이용해 호출하고 Node를 인자로 담고 있었다. 그렇다 보어 어떤 함수는 이중 포인터를 사용하는 함수가 되어 버렸다. 이 것을 수정하려고 한다.
1. 구현 목표
- 구조체 ll (linkedlist) 를 선언하고 모든 코드를 ll을 사용하도록 수정한다.
typedef struct linkedlist {
Node* head;
int size;
} ll;
지금까지 구현했던 코드의 가장 앞 단에 위와 같은 코드를 추가할 것이다. 이 ll 구조체는 head라는 노드 타입의 포인터 변수이고 추가로 size라는 int 타입 변수를 가진다.
Node** head와 같은 방식에서 ll 구조체(linkedlist)를 따로 선언해서 사용하는 방식으로 바꾸면, 코드 구조에 여러 가지 실질적인 이점이 생긴다. 단순히 보기 좋은 수준을 넘어서 설계·테스트·유지보수에 확실한 이점이 있다.
- 먼저 리스트에 메타 데이터를 추가하기 편하다.
typedef struct linkedlist {
Node* head;
int size; // 리스트 길이
Node* tail; // 마지막 노드 (O(1) 삽입 가능)
char* name; // 리스트 이름
} ll;
만약 위와 같이 링크드리스트를 정의하면 링크드 리스트는 길이와 tail이라는 노드 정보 name 이라는 리스트 이름도 가질 수 있다. 만약 기존에 Node** 방식을 사용한다면 위 내용을 따로 변수로 지정하고 관리해야 할 것이다.
기타 여러 장점이 있지만 일단 다 소개하지는 않겠다.
2. 기능 구현
일단 단순히 구조체만 추가한다고 되는 것이 아니다. 기존의 코드들의 수정 사항이 많이 발생한다.
#include <stdio.h>
#include <stdlib.h>
#include "minunit.h"
// ---[구현할 기능 정의]--- //
typedef struct Node {
int data;
struct Node* next;
} Node;
typedef struct linkedlist {
Node* head;
int size;
} ll;
Node* create_node(int data);
void insert_end(ll* list, int data);
void print_list(ll* list);
void free_list(ll* list);
void insert_front(ll* list, int data);
void delete_node(ll* list, int data);
Node* find_node(ll* list, int data);
void insert_at(ll* list, int index, int data);
void reverse_list(ll* list);
void sort_list(ll* list);
// --- [링크드 리스트 구현] --- //
Node* create_node(int data) {
Node *newNode = (Node *)malloc(sizeof(Node));
if (newNode == NULL) {
return NULL;
}
newNode->data = data;
newNode->next = NULL;
return newNode;
}
void insert_end(ll* list, int data) {
Node* new_node = create_node(data);
if (new_node == NULL) {
return;
}
if (list->head == NULL) {
list->head = new_node;
return;
}
Node* cur = list->head;
while (cur->next) {
cur = cur->next;
}
cur->next = new_node;
}
void free_list(ll* list) {
if (list == NULL ||list->head == NULL) {
return;
}
Node* temp = NULL;
Node* cur = list->head;
while (cur) {
temp = cur;
cur = cur->next;
free(temp);
}
list->head = NULL;
}
void print_list(ll* list) {
Node* cur = list->head;
while (cur) {
printf("%d ", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
void insert_front(ll* list, int data) {
Node* new_node = create_node(data);
if (new_node == NULL) {
return;
}
if (list == NULL) {
list->head = new_node;
return;
}
new_node->next = list->head;
list->head = new_node;
}
Node* find_node(ll* list, int data) {
if (list == NULL) return NULL;
Node* cur = list->head;
while (cur != NULL) {
if (cur->data == data) {
return cur;
}
cur = cur->next;
}
return NULL;
}
void insert_at(ll* list, int index, int data) {
Node* newNode = create_node(data);
if (newNode == NULL) {
return;
}
if (index == 0){
newNode->next = list->head;
list->head = newNode;
return;
}
int count = 0;
Node* cur = list->head;
while (cur != NULL && count < index - 1) {
cur = cur->next;
count++;
}
// index가 리스트 길이보다 클 경우 처리
if (cur == NULL) {
printf("[insert_at] Invalid index %d: out of bounds.\n", index);
free(newNode); // 사용 안 하므로 메모리 해제
return;
}
newNode->next = cur->next;
cur->next = newNode;
}
void reverse_list(ll* list) {
if (list == NULL) return;
Node *prev = NULL;
Node *cur = list->head;
Node *next = NULL;
while (cur != NULL) {
next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
list->head = prev;
}
void delete_node(ll* list, int data) {
if (list == NULL) return;
Node* cur = list->head;
Node* prev = NULL;
while (cur != NULL) {
if (cur->data == data) {
if (prev == NULL) {
list->head = cur->next;
}else {
prev->next = cur->next;
}
Node* temp = cur;
cur->next = NULL;
free(temp);
return;
}
prev = cur;
cur = cur->next;
}
}
Node* merge(Node *left, Node *right) {
Node dummy;
dummy.next = NULL;
Node* tail = &dummy;
while (left && right) {
if (left->data < right->data) {
tail->next = left;
left = left->next;
}else {
tail->next = right;
right = right->next;
}
tail = tail->next;
}
if (left != NULL) tail->next = left;
if (right != NULL) tail->next = right;
return dummy.next;
}
void sort_list(ll* list) {
if (list == NULL || list->head == NULL || list->head->next == NULL) {
return;
}
Node* slow = list->head;
Node* fast = list->head;
while (fast->next && fast->next->next) {
slow = slow->next;
fast = fast->next->next;
}
Node* left = list->head;
Node* right = slow->next;
slow->next = NULL;
ll left_list = { .head = left };
ll right_list = { .head = right };
sort_list(&left_list);
sort_list(&right_list);
list->head = merge(left_list.head, right_list.head);
}
// --- [메인 & 테스트 코드] --- //
static char * test_insert_end() {
ll list = { .head = NULL, .size = 0 };
insert_end(&list, 10);
insert_end(&list, 20);
insert_end(&list, 30);
print_list(&list);
mu_assert("error: head->data != 10", list.head->data == 10);
mu_assert("error: head->next->data != 20", list.head->next->data == 20);
mu_assert("error: head->next->next->data != 30", list.head->next->next->data == 30);
free_list(&list);
return 0;
}
static char * test_insert_front() {
ll list = { .head = NULL, .size = 0 };
insert_front(&list, 10);
insert_front(&list, 20);
insert_front(&list, 30);
// 리스트: 30 -> 20 -> 10
print_list(&list);
mu_assert("insert_front failed at head", list.head->data == 30);
mu_assert("insert_front failed at 2nd", list.head->next->data == 20);
mu_assert("insert_front failed at 3rd", list.head->next->next->data == 10);
free_list(&list);
return 0;
}
static char * test_delete_node() {
ll list = { .head = NULL, .size = 0 };
insert_end(&list, 10);
insert_end(&list, 20);
insert_end(&list, 30);
delete_node(&list, 20);
// 리스트: 10 -> 30
mu_assert("delete_node failed at head", list.head->data == 10);
mu_assert("delete_node failed at 2nd", list.head->next->data == 30);
mu_assert("delete_node failed: list too long", list.head->next->next == NULL);
free_list(&list);
return 0;
}
static char * test_delete_from_empty_list() {
ll list = { .head = NULL, .size = 0 };
delete_node(&list, 10); // 아무 일도 없어야 함
mu_assert("delete_node on empty list should keep head NULL", list.head == NULL);
return 0;
}
static char * test_find_node() {
ll list = { .head = NULL, .size = 0 };
insert_end(&list, 5);
insert_end(&list, 10);
insert_end(&list, 15);
Node* found = find_node(&list, 10);
mu_assert("find_node failed to find 10", found != NULL && found->data == 10);
Node* not_found = find_node(&list, 99);
mu_assert("find_node should return NULL for 99", not_found == NULL);
free_list(&list);
return 0;
}
static char * test_find_in_empty_list() {
ll list = { .head = NULL, .size = 0 };
Node* found = find_node(&list, 10);
mu_assert("find_node on empty list should return NULL", found == NULL);
return 0;
}
static char * test_insert_at() {
ll list = { .head = NULL, .size = 0 };
insert_end(&list, 1);
insert_end(&list, 3);
insert_at(&list, 1, 2); // 1 -> 2 -> 3
mu_assert("insert_at failed at 1", list.head->data == 1);
mu_assert("insert_at failed at 2", list.head->next->data == 2);
mu_assert("insert_at failed at 3", list.head->next->next->data == 3);
free_list(&list);
return 0;
}
static char * test_insert_at_out_of_bounds() {
ll list = { .head = NULL, .size = 0 };
insert_at(&list, 3, 100); // 인덱스 3은 잘못된 위치, 아무 일도 없어야 함
mu_assert("insert_at out of bounds should not modify list", list.head == NULL);
return 0;
}
static char * test_reverse_list() {
ll list = { .head = NULL, .size = 0 };
insert_end(&list, 1);
insert_end(&list, 2);
insert_end(&list, 3);
reverse_list(&list); // 3 -> 2 -> 1
mu_assert("reverse_list failed at 1", list.head->data == 3);
mu_assert("reverse_list failed at 2", list.head->next->data == 2);
mu_assert("reverse_list failed at 3", list.head->next->next->data == 1);
free_list(&list);
return 0;
}
static char * test_reverse_single_node() {
ll list = { .head = NULL, .size = 0 };
insert_end(&list, 42);
reverse_list(&list);
mu_assert("reverse single node failed", list.head != NULL && list.head->data == 42 && list.head->next == NULL);
free_list(&list);
return 0;
}
static char * test_reverse_empty_list() {
ll list = { .head = NULL, .size = 0 };
reverse_list(&list); // 아무 문제 없어야 함
mu_assert("reverse_list on empty list should keep head NULL", list.head == NULL);
return 0;
}
static char * test_sort_list() {
ll list = { .head = NULL, .size = 0 };
insert_end(&list, 30);
insert_end(&list, 10);
insert_end(&list, 20);
sort_list(&list); // 10 -> 20 -> 30
mu_assert("sort_list failed at 1", list.head->data == 10);
mu_assert("sort_list failed at 2", list.head->next->data == 20);
mu_assert("sort_list failed at 3", list.head->next->next->data == 30);
mu_assert("sort_list failed: list too long", list.head->next->next->next == NULL);
free_list(&list);
return 0;
}
static char * test_sort_empty_list() {
ll list = { .head = NULL, .size = 0 };
sort_list(&list); // 아무 문제 없어야 함
mu_assert("sort_list on empty list should keep head NULL", list.head == NULL);
return 0;
}
static char * all_tests() {
mu_run_test(test_insert_end);
mu_run_test(test_insert_front);
mu_run_test(test_delete_node);
mu_run_test(test_find_node);
mu_run_test(test_insert_at);
mu_run_test(test_reverse_list);
mu_run_test(test_sort_list);
mu_run_test(test_delete_from_empty_list);
mu_run_test(test_find_in_empty_list);
mu_run_test(test_insert_at_out_of_bounds);
mu_run_test(test_reverse_single_node);
mu_run_test(test_reverse_empty_list);
mu_run_test(test_sort_empty_list);
if (tests_failed > 0) {
return "Some tests failed.";
}
return 0;
}
int tests_run = 0;
int tests_failed = 0;
int main(void) {
char *result = all_tests();
printf("========================================\n");
printf("Tests run: %d\n", tests_run);
printf("Tests passed: %d\n", tests_run - tests_failed);
printf("Tests failed: %d\n", tests_failed);
if (result != 0) {
printf("❌ TESTS FAILED\n");
return 1;
}
printf("✅ ALL TESTS PASSED\n");
return 0;
}
혹시 따라하다 실패할 수 있으니 전체 코드를 첨부한다. 더 보기를 통해 확인할 수 있다.
Node* create_node(int data);
void insert_end(Node** head, int data);
void print_list(Node* head);
void free_list(Node* head);
void insert_front(Node** head, int data);
void delete_node(Node** head, int data);
Node* find_node(Node* head, int data);
void insert_at(Node** head, int index, int data);
void reverse_list(Node** head);
void sort_list(Node** head);
Node* create_node(int data);
void insert_end(ll* list, int data);
void print_list(ll* list);
void free_list(ll* list);
void insert_front(ll* list, int data);
void delete_node(ll* list, int data);
Node* find_node(ll* list, int data);
void insert_at(ll* list, int index, int data);
void reverse_list(ll* list);
void sort_list(ll* list);
먼저 기존의 함수 선언 부를 전부 변경해야 한다.
void insert_end(ll* list, int data) {
Node* new_node = create_node(data);
if (new_node == NULL) {
return;
}
if (list->head == NULL) {
list->head = new_node;
return;
}
Node* cur = list->head;
while (cur->next) {
cur = cur->next;
}
cur->next = new_node;
}
위와 같이 실제 구현 함수의 내용들도 변경해야한다.
마지막으로 테스트 코드도 변경해야 한다.
static char * test_insert_end() {
ll list = { .head = NULL, .size = 0 };
insert_end(&list, 10);
insert_end(&list, 20);
insert_end(&list, 30);
print_list(&list);
mu_assert("error: head->data != 10", list.head->data == 10);
mu_assert("error: head->next->data != 20", list.head->next->data == 20);
mu_assert("error: head->next->next->data != 30", list.head->next->next->data == 30);
free_list(&list);
return 0;
}
3. 회고 및 느낀 점
이래서 처음부터 설계를 잘해야 하는 것이다... 만약 이 것보다 더 큰 코드에서 놓치고 구현을 했다면 수정하는 것 보다 새로 만드는 것이 빠를 수도 있을지 모른다.
C로 무엇인가를 구현한다는 것이 아직 많이 낯설기 때문에 많은 것을 배우고 느낄 수 있었다. 계속해서 스택, 큐 등 여러 자료 구조를 직접 구현해 보며 이해할 것이다.