*본 내용은 [윤성우의 열혈 자료구조] 책과 강의를 보고 공부하면서 요점 정리한 내용입니다.
먼저 들어간 것이 나중에 나오는 자료구조
로써 초코볼이 담겨있는 통에 비유할 수 있다.LIFO(Last-in, First-out)구조
의 자료구조이다.ADT를 대상으로 '배열 기반의 스택' 또는 '연결 기반의 스택'을 구현할 수 있다.
void StackInit(Stack * pstack);
∙ 스택의 초기화를 진행한다.
∙ 스택 생성 후 제일 먼저 호출되어야 하는 함수이다.
int SIsEmpty(Stack * pstack);
∙ 스택이 빈 경우 TRUE(1)을, 그렇지 않은 경우 FALSE(0)을 반환한다.
void SPush(Stack * pstack, Data data);
∙ 스택에 데이터를 저장한다. 매개변수 data로 전달된 값을 저장한다.
Data SPop(Stack * pstack);
∙ 마지막에 저장된 요소를 삭제한다.
∙ 삭제된 데이터는 반환이 된다.
∙ 본 함수의 호출을 위해서는 데이터가 하나 이상 존재함이 보장되어야 한다.
Data Speek(Stack * pstack);
∙ 마지막에 저장된 요소를 반환하되 삭제하지 않는다.
∙ 본 함수의 호출을 위해서는 데이터가 하나 이상 존재함이 보장되어야 한다.
#define TRUE 1
#define FALSE 0
#define STACK_LEN 100
typedef int Data;
/*** 배열 기반을 고려하여 정의된 스택의 구조체 ***/
typedef struct _arrayStack
{
Data stackArr[STACK_LEN];
int topIndex;
} ArrayStack;
typedef ArrayStack Stack;
void StackInit(Stack * pstack);
int SIsEmpty(Stack * pstack);
void SPush(Stack * pstack, Data data);
Data SPop(Stack * pstack);
Data SPeek(Stack * pstack);
void StackInit(Stack * pstack)
{
pstack->topIndex = -1; // -1은 스택이 비었음을 의미
}
int SIsEmpty(Stack * pstack)
{
if(pstack->topIndex == -1)
return TRUE; // 비어있는 경우 TRUE를 반환
else
return FALSE;
}
void SPush(Stack * pstack, Data data)
{
pstack->topIndex += 1;
pstack->stackArr[pstack->topIndex] = data;
}
Data SPop(Stack * pstack)
{
int rIdx;
if(SIsEmpty(pstack))
{
printf("Stack Memory Error!");
exit(-1);
}
rIdx = pstack->topIndex; // 인덱스값 잠시 저장
pstack->topIndex -= 1;
return pstack->stackArr[rIdx];
// 삭제 후 데이터 값을 다시 0으로 만들어줄 필요 없다. top만 이동시키면 된다.
}
Data SPeek(Stack * pstack)
{
if(SIsEmpty(pstack))
{
printf("Stack Memory Error!");
exit(-1);
}
return pstack->stackArr[pstack->topIndex];
}
#include <stdio.h>
#include "ArrayBaseStack.h"
int main(void)
{
// Stack의 생성 및 초기화 ///////
Stack stack;
StackInit(&stack);
// 데이터 넣기 ///////
SPush(&stack, 1); SPush(&stack, 2);
SPush(&stack, 3); SPush(&stack, 4);
SPush(&stack, 5);
// 데이터 꺼내기 ///////
while(!SIsEmpty(&stack)) // 비었는지 확인
printf("%d ", SPop(&stack));
return 0;
}
이렇듯 메모리 구조만 보아서는 스택임이 구분되지 않는다. 헤더파일의 정의(ADT 정의)가 중요하다.
저장된 순서의 역순으로 데이터(노드)를 참조(삭제)하는 연결 리스트가 바로 연결 기반의 스택이다!
문제 06-1에서는 CLinkedList.h와 CLinkedList.c를 바꾸지 않고 단순히 활용하여 스택의 구현을 요구한다.(연결리스트->스택 구현) 꼭 풀어보자.
#define TRUE 1
#define FALSE 0
typedef int Data;
typedef struct _node
{
Data data;
struct _node * next;
} Node;
/*** 연결리스트 기반을 고려하여 정의된 스택의 구조체 ***/
typedef struct _listStack
{
Node * head; // 헤드 하나만 있으면 된다!
} ListStack;
typedef ListStack Stack;
void StackInit(Stack * pstack);
int SIsEmpty(Stack * pstack);
void SPush(Stack * pstack, Data data);
Data SPop(Stack * pstack);
Data SPeek(Stack * pstack);
void StackInit(Stack * pstack)
{
pstack->head = NULL;
}
int SIsEmpty(Stack * pstack)
{
if(pstack->head == NULL)
return TRUE;
else
return FALSE;
}
새 노드를 머리에 추가하고, 삭제 시 머리부터 삭제하는 단순 연결 리스트의 코드에 지나지 않는다.
연결 리스트 기반 스택을 직접 구현하는 것보다 의미 있는 것은 문제 06-1을 해결하는 것이다.
void SPush(Stack * pstack, Data data)
{
Node * newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = pstack->head;
pstack->head = newNode;
}
Data SPop(Stack * pstack)
{
Data rdata;
Node * rnode;
if(SIsEmpty(pstack)) {
printf("Stack Memory Error!");
exit(-1);
}
rdata = pstack->head->data;
rnode = pstack->head;
pstack->head = pstack->head->next;
free(rnode);
return rdata;
}
Data SPeek(Stack * pstack)
{
if(SIsEmpty(pstack)) {
printf("Stack Memory Error!");
exit(-1);
}
return pstack->head->data;
}
배열 기반 리스트 main함수와 완전히 동일하게 정의되었다.
#include <stdio.h>
#include "ListBaseStack.h"
int main(void)
{
// Stack의 생성 및 초기화 ///////
Stack stack;
StackInit(&stack);
// 데이터 넣기 ///////
SPush(&stack, 1); SPush(&stack, 2);
SPush(&stack, 3); SPush(&stack, 4);
SPush(&stack, 5);
// 데이터 꺼내기 ///////
while(!SIsEmpty(&stack))
printf("%d ", SPop(&stack));
return 0;
}
다음과 같은 문장의 수식을 계산할 수 있어야 한다.
( 3 + 4 ) * ( 5 / 2 ) + ( 7 + ( 9 - 5 ) )
다음과 같은 문장의 수식계산을 위해서는 다음 두 가지를 고려해야 한다.
∙ 소괄호를 파악하여 그 부분을 먼저 연산한다.
∙ 연산자의 우선순위를 근거로 연산의 순위를 결정한다.
계산기 구현에 필요한 알고리즘은 스택과는 별개의 것이다.
다만 그 알고리즘의 구현에 있어서 스택이 매우 요긴하게 활용된다.
중위 표기법(infix notation)
∙ 예) 5 + 2 / 7
∙ 수식 내에 연산의 순서에 대한 정보가 담겨 있지 않다. 그래서 소괄호와 연산자의 우선순위라는 것을 정의하여 이를 기반으로 연산의 순서를 명시한다.
전위 표기법(prefix notation)
∙ 예) + 5 / 2 7
∙ 수식 내에 연산의 순서에 대한 정보가 담겨 있다. 그래서 소괄호가 필요 없고 연산의 우선순위를 결정할 필요도 없다.
후위 표기법(postfix notation)
∙ 예) 5 2 7 / +
∙ 전위 표기법과 마찬가지로 수식 내에 연산의 순서에 대한 정보가 담겨 있다. 그래서 소괄호가 필요 없고 연산의 우선순위를 결정할 필요도 없다.
소괄호와 연산자의 우선순위를 인식하게 하여 중위 표기법의 수식을 직접 계산하게 프로그래밍하는 것보다 후위 표기법의 수식을 계산하도록 프로그래밍 하는 것이 더 쉽다!
피 연산자
를 만나면 무조건 변환된 수식이 위치할 자리로 이동시킨다.연산자
를 만나면 무조건 쟁반으로 이동한다.→ '/'연산자가 '+'연산자보다 먼저 사용되게 됨.
※ 쟁반에 기존 연산자가 있는 상황에서의 행동 방식
- 쟁반에 위치한 연산자의 우선순위가 높다면
∙ 쟁반에 위치한 연산자를 꺼내서 변환된 수식이 위치할 자리로 옮긴다.
∙ 그리고 새 연산자는 쟁반으로 옮긴다.- 쟁반에 위치한 연산자의 우선순위가 낮다면
∙ 쟁반에 위치한 연산자의 위에 새 연산자를 쌓는다.- 우선순위가 높은 연산자는 우선순위가 낮은 연산자 위에 올라서서 우선순위가 낮은 연산자가 먼저 자리를 잡지 못하게 하려는 목적!
- 형(우선순위↑)은 동생(우선순위↓)을 올라탈 수 있지만 동생은 형 위에 올라탈 수 없다고 생각하자.
피 연산자
는 그냥 옮긴다.연산자
는 쟁반
으로 옮긴다.후위 표기법의 수식에서는 먼저 연산이 이뤄져야 하는 연산자가 뒤에 연산이 이뤄지는 연산자보다 앞에 위치해야 한다. 따라서 소괄호 안에 있는 연산자들이 후위 표기법의 수식에서 앞부분에 위치해야 한다.
'(' 연산자의 우선순위는 그 어떤 사칙 연산자들보다 낮다고 간주! 그래서 ')' 연산자가 등장할 때까지 쟁반에 남아 소괄호의 경계 역할을 해야 한다.
'(' 연산자를 만나면 쟁반(Stack)의 New 바닥이 생겼다고 생각하자. ')' 연산자를 만나면 그 바닥은 사라진다.
')' 연산자는 쟁반에 올릴 필요 없다.
ConvToRPNExp 함수의 선언과 정의
∙ InfixToPostfix.h
∙ InfixToPostfix.c
스택관련 함수의 선언과 정의
∙ ListBaseStack.h
∙ ListBaseStack.c
main 함수의 정의
∙ InfixToPostfixMain.c
실행 결과
123*+
12+3*
12-3+52-*
ConvToRPNExp 함수
는 배열exp에 담긴 중위표기법 수식을 함수의 인자로 전달받은 뒤 변환된 수식을 exp에 저장한다.GetOpPrec 함수
int GetOpPrec(char op)
{
// 연산자의 우선순위에 대응하는 값을 반환한다. 값이 클수록 우선순위가 높은 것으로 정의되어 있다.
switch(op)
{
case '*':
case '/':
return 5; // 가장 높은 연산의 우선순위
case '+':
case '-':
return 3;
case '(': // '(' 연산자는 ')'연산자가 등장할 때까지 쟁반에 남아 있어야 하기 때문에 가장 낮은 우선순위를 부여!
return 1;
// ')' 연산자는 소괄호의 끝을 알리는 메시지의 역할을 한다. 따라서 쟁반으로 가지 않는다.
// 때문에 ')' 연산자에 대한 반환 값은 정의되어 있을 필요가 없다!
}
return -1; // 등록되지 않은 연산자
}
WhoPrecOp 함수
int WhoPrecOp(char op1, char op2)
{
int op1Prec = GetOpPrec(op1);
int op2Prec = GetOpPrec(op2);
if(op1Prec > op2Prec) // op1의 우선순위가 더 높다면
return 1;
else if(op1Prec < op2Prec) // op2의 우선순위가 더 높다면
return -1;
else // op1과 op2의 우선순위가 같다면
return 0;
}
ConvToRPNExp 함수
void ConvToRPNExp(char exp[])
{
Stack stack;
int expLen = strlen(exp);
char * convExp = (char*)malloc(expLen+1); // 변환된 수식을 담을 공간 마련
int i, idx=0;
char tok, popOp;
memset(convExp, 0, sizeof(char)*expLen+1); // 마련한 공간 0으로 초기화
StackInit(&stack);
/*** 일련의 변환 과정 ***/
for(i=0; i<expLen; i++) // 한 숫자당 for문을 한 번씩 돈다.
{
tok = exp[i];
if(isdigit(tok)) // tok에 저장된 문자가 피연산자라면
{
convExp[idx++] = tok;
}
else // tok에 저장된 문자가 연산자라면
{
switch(tok) // 연산자일 때의 처리 루틴
{
case '(': // 여는 소괄호라면,
SPush(&stack, tok); // 스택에 쌓는다.
break;
case ')': // 닫는 소괄호라면,
while(1) // 반복해서,
{
popOp = SPop(&stack); // 스택에서 연산자를 꺼내어,
if(popOp == '(') // 연산자 (을 만날 때 까지,
break;
convExp[idx++] = popOp; // 배열 convExp에 저장한다.
}
break;
case '+': case '-':
case '*': case '/':
/*** tok에 저장된 연산자를 스택에 저장하기 위한 과정 ***/
while(!SIsEmpty(&stack) &&
WhoPrecOp(SPeek(&stack), tok) >= 0)
// Stack 맨 위 연산자의 우선순위가 높거나 같을 때 pop 한다.
// 연산자의 우선순위가 낮으면 반복문 종료.
convExp[idx++] = SPop(&stack);
SPush(&stack, tok);
break;
}
}
}
while(!SIsEmpty(&stack)) // 스택에 남아 있는 모든 연산자를 이동시키는 반복문
convExp[idx++] = SPop(&stack);
strcpy(exp, convExp); // 변환된 수식을 반환
free(convExp);
}
#include <stdio.h>
#include "InfixToPostfix.h"
int main(void)
{
char exp1[] = "1+2*3";
char exp2[] = "(1+2)*3";
char exp3[] = "((1-2)+3)*(5-2)";
ConvToRPNExp(exp1);
ConvToRPNExp(exp2);
ConvToRPNExp(exp3);
printf("%s \n", exp1);
printf("%s \n", exp2);
printf("%s \n", exp3);
return 0;
}
EvalRPNExp 함수의 선언과 정의
∙ PostCalculator.h
∙ PostCalculator.c
스택관련 함수의 선언과 정의
∙ ListBaseStack.h
∙ ListBaseStack.c
main 함수의 정의
∙ PostCalculatorMain.c
실행 결과
42*8+ = 16
123+*4/ = 1
int EvalRPNExp(char exp[])
{
Stack stack;
int expLen = strlen(exp);
int i;
char tok, op1, op2;
StackInit(&stack);
for(i=0; i<expLen; i++)
{
tok = exp[i];
if(isdigit(tok)) // 피연산자라면
{
SPush(&stack, tok - '0'); // 숫자로 변환하여 PUSH!
}
else // 연산자라면
{
op2 = SPop(&stack); // 먼저 꺼낸 값이 두 번째 피연산자!
op1 = SPop(&stack);
switch(tok)
{ // 연산 결과를 push
case '+':
SPush(&stack, op1+op2);
break;
case '-':
SPush(&stack, op1-op2);
break;
case '*':
SPush(&stack, op1*op2);
break;
case '/':
SPush(&stack, op1/op2);
break;
}
}
}
return SPop(&stack);
}
#include <stdio.h>
#include "PostCalculator.h"
int main(void)
{
char postExp1[] = "42*8+";
char postExp2[] = "123+*4/";
printf("%s = %d \n", postExp1, EvalRPNExp(postExp1));
printf("%s = %d \n", postExp2, EvalRPNExp(postExp2));
return 0;
}
스택의 활용 (스택관련 함수의 선언과 정의)
∙ ListBaseStack.h
∙ ListBaseStack.c
후위 표기법의 수식으로 변환 (ConvToRPNExp 함수의 선언과 정의)
∙ InfixToPostfix.h
∙ InfixToPostfix.c
후위 표기법의 수식을 계산 (EvalRPNExp 함수의 선언과 정의)
∙ PostCalculator.h
∙ PostCalculator.c
중위 표기법의 수식을 계산
∙ InfixCalculator.h
∙ InfixCalculator.c
main 함수의 정의
∙ InfixCalculatorMain.c
실행 결과
1+2*3 = 7
(1+2)*3 = 9
((1-2)+3)*(5-2) = 6
InfixCalculator.h
#ifndef __INFIX_CALCULATOR__
#define __INFIX_CALCULATOR__
int EvalInfixExp(char exp[]);
#endif
InfixCalculator.c
#include <string.h>
#include <stdlib.h>
#include "InfixToPostfix.h"
#include "PostCalculator.h"
int EvalInfixExp(char exp[])
{
int len = strlen(exp);
int ret;
char * expcpy = (char*)malloc(len+1); // 문자열 저장공간 마련
strcpy(expcpy, exp); // exp를 expcpy에 복사
ConvToRPNExp(expcpy); // 후위 표기법의 수식으로 변환
ret = EvalRPNExp(expcpy); // 변환된 수식의 계산
free(expcpy); // 문자열 저장공간 해제
return ret; // 계산결과 반환
}
InfixCalculatorMain.c
#include <stdio.h>
#include "InfixCalculator.h"
int main(void)
{
char exp1[] = "1+2*3";
char exp2[] = "(1+2)*3";
char exp3[] = "((1-2)+3)*(5-2)";
printf("%s = %d \n", exp1, EvalInfixExp(exp1));
printf("%s = %d \n", exp2, EvalInfixExp(exp2));
printf("%s = %d \n", exp3, EvalInfixExp(exp3));
return 0;
}