gd. 이번 주는 구현이 주가 되는 과제 수행이 진행된다. 연결 리스트를 C언어로 직접 구현해보는 시간이다.
말은 이렇게 했지만 큼직큼직한 부분은 모두 잘 마련되어있다. 그래서 상대적으로 응용적인 부분에 대한 작성이 필요하다.
Insert, Delete, Find
기본 연산에 있어 세 가지 함수는 필수이다.
ListNode *findNode(LinkedList *ll, int index){
// 노드를 찾는 함수이다.
ListNode *temp;
if (ll == NULL || index < 0 || index >= ll->size)
return NULL;
// 뭔가 이상한 상태를 차단한다.
temp = ll->head;
// 시작점부터 가리키기
if (temp == NULL || index < 0)
return NULL;
// 혹시 모르니 여기서 이상한 상태를 한번 더 확인하기
while (index > 0){
temp = temp->next;
if (temp == NULL)
return NULL;
index--;
}
// index가 0이면 적절한 위치를 찾았으니 반환
return temp;
}
int insertNode(LinkedList *ll, int index, int value){
ListNode *pre, *cur;
if (ll == NULL || index < 0 || index > ll->size + 1)
return -1;
// 완전 첫 삽입이거나, 아니면 첫 번째 위치에 삽입을 임의로 시도하는 경우
if (ll->head == NULL || index == 0){
// 첫 시작점을 일단 가리킨다. 아무것도 없다면 NULL 일 것이고, 값이 있다면 넣은 값 next로 배정한다.
cur = ll->head;
// 공간을 할당한다.
ll->head = malloc(sizeof(ListNode));
// 값을 할당한다.
ll->head->item = value;
// next = cur
ll->head->next = cur;
// 이 연결 리스트의 사이즈를 1 키운다.
ll->size++;
return 0;
}
// 위에 해당이 없는 경우 여기서 삽입을 진행하게 된다. 정확하게는 index 위치에 시도한다.
if ((pre = findNode(ll, index - 1)) != NULL){
// pre는 index에 - 1 위치에 있다. 기존 내용을 빼두기
cur = pre->next;
// 그리고 실제 위치에 새로운 값을 배정한다.
pre->next = malloc(sizeof(ListNode));
// 실제 값을 부여한다.
pre->next->item = value;
// 이제 기존에 있던 값을 다시 입힌다.
pre->next->next = cur;
// 전체 사이즈를 1 늘린다.
ll->size++;
return 0;
}
return -1;
}
int removeNode(LinkedList *ll, int index){
// index 번째의 내용을 지운다
ListNode *pre, *cur;
// 상태가 안좋은 애들 거르기
if (ll == NULL || index < 0 || index >= ll->size)
return -1;
// 첫째 노드를 밀어버리면 수정이 반드시 필요하다
if (index == 0){
// 지울 노드의 다음 노드를 일단 기억해야한다
cur = ll->head->next;
// 지울 노드는 그냥 밀어버린다.
free(ll->head);
// 다음 노드가 새로운 첫 노드가 된다.
ll->head = cur;
// 사이즈를 1 줄인다.
ll->size--;
return 0;
}
// 지울 노드의 직전 노드까지 찾는다.
if ((pre = findNode(ll, index - 1)) != NULL){
if (pre->next == NULL)
return -1;
// 근데 막상 지울 노드가 없는 경우를 구분한다.
// 지울 노드를 픽한다.
cur = pre->next;
// 지울 노드의 다음 주소를 살아 있는 노드에 부여한다.
pre->next = cur->next;
// 밀어 버리기
free(cur);
// 사이즈 1 줄이기
ll->size--;
return 0;
}
return -1;
}
번외
void printList(LinkedList *ll){
// 전체 인쇄하기. 그냥 순회의 일반적인 형태를 볼 수 있다.
ListNode *cur;
if (ll == NULL)
return;
cur = ll->head;
if (cur == NULL)
printf("Empty");
while (cur != NULL)
{
printf("%d ", cur->item);
cur = cur->next;
}
printf("\n");
}
void removeAllItems(LinkedList *ll)
{
// 순회하먀 전부 밀어버리는 코드. 순회 하며 존재하는 경우 모두 free한다.
ListNode *cur = ll->head;
ListNode *tmp;
while (cur != NULL){
tmp = cur->next;
free(cur);
cur = tmp;
}
ll->head = NULL;
ll->size = 0;
}
Quest #1 : 정렬된 리스트에 순서 지켜 삽입하기
중복 값은 허용되지 않으며, 기본 정렬은 오름차순이다.
즉, 순회를 하다가 적절하 위치를 찾으면 추가하게끔 했다. 여기서는 numCnt로 몇 번째를 해야하냐? 를 논해야 했다.
int insertSortedLL(LinkedList *ll, int item)
{
ListNode *cur;
int numCnt = 0;
if(ll->head == NULL)
{
cur = ll->head;
ll->head = malloc(sizeof(ListNode));
ll->head->item = item;
ll->head->next = cur;
ll->size++;
return 0;
// 첫 번째 아이템 삽입 위치는 반드시 0이니 당연하게 0을 반환하도록 한다.
}
else
{
int isInsertable = 0;
ListNode *exists;
exists = ll->head;
if(exists->item == item)
return -1;
// cur가 제대로 준비 안된 상태에서 cur->item 사용 시도해서 SegFault
if(exists->item > item)
{
cur = ll->head;
ll->head = malloc(sizeof(ListNode));
ll->head->item = item;
ll->head->next = exists;
ll->size++;
return 0;
}
while (isInsertable == 0)
{
if(exists->next == NULL)
{
cur = exists->next;
exists->next = malloc(sizeof(ListNode));
exists->next->item = item;
exists->next->next = NULL;
ll->size++;
isInsertable = 1;
return ll->size - 1;
// 맨 끝 도달시 이렇게 반환하도록 한다.
}
if(exists->next->item == item) // is this already exists?
return -1;
if(exists->next->item < item)
{
exists = exists->next;
numCnt++;
}
else // 아이템을 배치 해야하는 상태
{
ListNode *tmp = exists->next;
cur = exists->next;
exists->next = malloc(sizeof(ListNode));
exists->next->item = item;
exists->next->next = tmp;
ll->size++;
isInsertable = 1;
return numCnt + 1;
}
}
}
}
Quest #2 : 두 리스트에 대한 내용 합치기
List1, List2가 주어지면, List1.1st | List2.1st | List1.2nd | List2.2nd .... 를 둘 중에 한 리스트의 내용이 없어질 때까지 한다.
정확히는, List1이 전부 소모되면 List2도 더 이용 할 수 없다. 그러나 List2가 전부 소모되면, List1은 전부 이용한다.
그니까 이게.. List1 사이사이에 List2의 아이템을 끼는 개념인데, List2가 아이템이 더 없고 List1이 남았다면 더 끼워넣지 않는 채로 진행된다는 것. 따라서 이것은 List1이 아이템이 얼마나 있냐를 우선적인 진행 조건으로써 생각해야 한다.
void alternateMergeLinkedList(LinkedList *ll1, LinkedList *ll2)
{
ListNode *target = ll1->head;
while(target != NULL)
{
ListNode *nxtTarget = target->next;
if(ll2->head != NULL)
{
ListNode *insertTarget = ll2->head;
ll2->head = ll2->head->next;
insertTarget->next = target->next;
target->next = insertTarget;
}
target = nxtTarget;
}
}
Quest #3 : 홀수 아이템 뒤로 보내기
도움되라고 주석을 많이 쓰다가 주석에 모든 내용을 담았다.
void moveOddItemsToBack(LinkedList *ll)
{
LinkedList* tmpll = malloc(sizeof(LinkedList));
tmpll->head = NULL;
tmpll->size = 0;
// 해당 되는 수 (여기서는 홀수)를 저장 할 연결리스트 신규 선언
ListNode *cur;
// 모든 아이템 순회에 필요한 커서 선언
ListNode *BackupNext;
// 홀수 처리 발생시, 다음 목적지 누락 방지를 위한 백업
ListNode *latestEven = NULL;
// 가장 마지막에 있는 짝수 노드에 최종 완성된 홀수 리스트를 이어 줄 예정.
if (ll == NULL)
return;
// 유효하지 않은 연결 리스트에 대한 작동 차단
cur = ll->head;
int visitedCount = 0;
// 사전 정의 함수 사용을 위해서는 n번째 위치 파악이 필요하여 선언.
while (cur != NULL)
{
BackupNext = cur->next;
// 목적지 누락 방지
if((cur->item % 2) == 1)
{
insertNode(tmpll, tmpll->size, cur->item);
int a = removeNode(ll, visitedCount);
// 홀수에 대한 처리로, 별도의 연결 리스트에 추가하며 기존 연결 리스트에서 제거
visitedCount--;
// 기존 리스트에서 한 개의 아이템이 빠졌으니 길이도 달라짐, 이 visitedCount로 해당 내용에 맞춰 조정되어야함.
}
else
{
latestEven = cur;
// 짝수의 경우 붙여줘야하는 위치를 현재 위치로 조정하기
}
visitedCount++;
cur = BackupNext;
// cur 갱신 후 쭉 진행
}
// 전체 내용 마무리 된 경우, 마지막에 ll에 매핑시켜서 끝내기
if(latestEven != NULL)
latestEven->next = tmpll->head;
else
ll->head = tmpll->head;
free(tmpll);
// is free
}
Quest #4 : 짝수 뒤로 보내기
#3과 한 글자 다르다. 다만 여긴 주석이 없고 아주 조금은 다르지만, 핀트는 완전히 같다.
void moveEvenItemsToBack(LinkedList *ll)
{
LinkedList tmpll;
tmpll.head = NULL;
tmpll.size = 0;
ListNode *cur;
ListNode *BackupNext;
ListNode *latestOdd = NULL;
if (ll == NULL)
return;
cur = ll->head;
int visitedCount = 0;
while (cur != NULL)
{
BackupNext = cur->next;
if((cur->item % 2) == 0)
{
insertNode(&tmpll, tmpll.size, cur->item);
int a = removeNode(ll, visitedCount);
visitedCount--;
// delete this node.
}
else
{
latestOdd = cur;
}
visitedCount++;
cur = BackupNext;
}
if(latestOdd != NULL)
latestOdd->next = tmpll.head;
else
ll->head = tmpll.head;
//
}
Quest #5 : 주어진 리스트 2등분 하기
단, 홀수인경우 첫번째 리스트가 1 더 길게 배치한다.
void frontBackSplitLinkedList(LinkedList *ll, LinkedList *resultFrontList, LinkedList *resultBackList)
{
int pivotCount = ll->size;
if(pivotCount % 2 == 1)
{
pivotCount /= 2;
pivotCount += 1;
}
else
{
pivotCount /= 2;
}
// pivotCount가 리스트 아이템 갯수를 찢는 기준이 된다.
ListNode *target;
target = ll->head;
int cnt = 0;
resultFrontList->head = ll->head;
while (target != NULL)
{
cnt++;
if(cnt == pivotCount)
{
// cnt가 설정한 값에 도달하면
// 해당 시점부터의 리스트를 찢는다
ListNode *rightHead;
rightHead = target->next;
target->next = NULL;
resultBackList->head = rightHead;
}
target = target->next;
}
}
Quest #6 : 최대값을 첫 위치로 보내기
어쨌든 최대값을 구하려면 전체 순회를 하며 해당 위치를 알아야한다. 그리고 해당 위치와 관련되어 조정을 하려고 하겠다면 해당 위치 그 자체가 아니라 해당 위치 한 칸 이전에서 조정이 필요하다. 최대값을 갱신하면서 왜 최대값을 가리키는 노드를 해당 노드 전으로 했는지에 대해 이해하는데 도움이 될 것이다.
마지막에 도달하면 찍어뒀던 노드에 대해 직접적인 위치 조정 후 마무리된다.
int moveMaxToFront(ListNode **ptrHead)
{
// 값을 직접 조정하기 위해선 해당 노드를 알아보긴 하되, 해당 노드 이전에서 조작이 일어나야한다.
ListNode *prev = NULL, *cur = NULL;
if( (*ptrHead) == NULL)
return -1;
prev = (*ptrHead);
cur = prev->next;
int maxItemValue = 0;
ListNode *maxItemPos, *maxItemPosPrev;
while(cur != NULL)
{
int prevMax = maxItemValue;
maxItemValue = max(maxItemValue, cur->item);
if(prevMax != maxItemValue)
{
// 최대 수가 수정되면 최대 기준점, 그리고 최대의 위치 전부 수정 되게끔 해야한다.
maxItemPos = cur;
maxItemPosPrev = prev;
}
// 꼭 해당이 아니더라도 cur와 prev는 계속 돌게끔 해야한다.
prev = cur;
cur = cur->next;
}
// 리스트에 끝에 도달한경우 우리가 얻은 데이터를 실 적용 해야한다.
maxItemPosPrev->next = maxItemPos->next;
ListNode *backUp = (*ptrHead);
(*ptrHead) = maxItemPos;
(*ptrHead)->next = backUp;
// 맨 앞으로 값을 끌고오는 과정
}
Quest #7 : 재귀적으로 리스트 내용 뒤집기
void RecursiveReverse(ListNode **ptrHead)
{
// 이중 포인터를 사용한다. 해당 포인터는 리스트의 첫 시작점을 가리키는 주소의 위치를 말한다.
ListNode *first = NULL, *rest = NULL;
first = (*ptrHead);
rest = first->next;
// 맨 좌측 노드를 뺀 상태로 새롭게 재귀를 시도한다.
// 한 칸씩 뒤집어야하는 만큼, base case를 찍어야한다.
if(rest == NULL)
return;
RecursiveReverse(&rest);
first->next->next = first;
// 5 4 3 2 1 이 기준이고 지금 2 1을 논하고 있다고 하자.
// 1의 다음을 2로 배정하면, 2 -> 1 -> 저 왼쪽에 있던 2를 가리키게 된다.
first->next = NULL;
// 기존 좌측에 위치하는 2의 연결을 끊는다. 1 -> 2가 된다.
*ptrHead = rest;
// 다음 진행이 가능하게끔 다시 넘겨준다.
}
수시로 수정하며 수습해야 할 것만 같다.
'문제풀이' 카테고리의 다른 글
C언어로 이진 트리 논하기 (0) | 2025.04.16 |
---|---|
C언어로 이분 탐색 트리 구현하기 (0) | 2025.04.14 |
[PY] 9084 : 동전 (0) | 2025.04.11 |
[PY] 2573 : 빙산 (0) | 2025.04.10 |
[PY] 14916 : 거스름돈 (0) | 2025.04.10 |