▸C언어/알고리즘 및 자료구조

스택구조_후위표기식_수식 계산하기(수정/기능추가) [3/3]

코데방 2019. 12. 9.
728x90

 

강의는 저랑 완전 다른 방식으로 풀어내네요. 여러가지 방면에서 다르지만 가장 큰 차이점은 괄호도 연산자와 마찬가지로 취급하여 처리한다는 점입니다. 그 외에는 함수를 어떤 방식으로 구성해서 사용했는가 정도의 차이 같습니다. 강의에는 나오지 않았지만 굳이 후위표현식으로 변환하지 않고 연산자가 나오면 바로바로 계산해도 될 것 같습니다.

강의에서 진행한 방식과 제 방식을 섞어서 다시 최종 버전을 만들어봤습니다. 그리고 다시 하는김에 조금 더 완벽하게 만들어보기 위해 이것저것 기능을 더 추가해봤습니다. 강의에 풀이가 없는 기능들이라.. 얼마나 잘 짰는지는 알 수가 없네요. 일단 기능은 다 잘 됩니다. ㅎㅎ

대략 아래와 같은 방식입니다.

 

 

[수정사항]

  • 괄호와 연산자를 하나의 스택(배열)을 사용해서 연산 순서 계산
  • 계산해야하는 연산자가 발생하는 순간 바로 계산해서 후위표기식 자체를 만들지 않음
  • 음수 계산, 실수 계산 추가
  • 제곱 계산 추가 (2^3^4 형태)
  • 제곱의 경우 음수 제곱 기능은 빠짐
  • 엑셀에서는 2^3^4 → (2^3)^4로 순서로 계산하지만, 2^(3^4)로 뒤부터 계산하도록 함

 

[ main (), void read_line() - 입력된 수식에서 공백제거 ]

  • 다시 하는김에 수식 입력받고 소숫점 자리수 선택해서 출력할 수 있도록 수정
  • 수식 입력 시 공백 제거
  • float 타입 연산이라 소숫점 끝자리 정확도 오차가 아주 약간씩 발생할 때가 있음. 2진수를 이용한 10진수 계산이라 어쩔 수 없는 문제이긴 하지만, 정확도를 맞춰주는 방법이 있을 것 같은데 아직 잘 모르겠음
  • 프로그램은 1회성 입력으로 한정. (에러처리를 return()이 아닌 exit()로 처리함)
  • 엑셀로 검증할 때 제곱은 2^(2^3)과 같이 뒷순서부터 계산하도록 해줘야함 (엑셀은 앞부터 계산)

#include "Header.h"
#define MAX 51

void read_line(FILE* fp, char* input, int len);
int main()
{
	char input[MAX];
	int n;
	printf("수식 입력 : ");
	read_line(stdin, input, MAX);
	printf("출력 소숫점 자리 입력(5자리까지 정확도 보장) : ");
	scanf_s("%d", &n);
		
	printf("%.*f", n, calcuator(input));

	return 0;
}

void read_line(FILE* fp, char* input, int len)
{
	char c;
	int i = 0;
	while ((c = fgetc(fp)) != '\n')
	{
		/* 최대 글자수 이상이면 에러 처리 */
		if (i == len - 1)
		{
			printf("최대 길이는 %d까지입니다.\n", len-1);
			exit(1);
		}
		
		/* 공백은 다 없애기 */
		if (!isspace(abs(c)))
			input[i++] = c;
	}
	input[i] = '\0';
}

 


 

[ header.h ]

  • 파일 나누기 위해 사용
  • main()함수에서 사용해야할 함수는 하나밖에 없음
#ifndef HEADER_H	
#define HEADER_H

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
float calcuator(char* str);

#endif

 


 

[ float calcuator() - 최종 결과값을 리턴함 ]

  • 연산자는 자체 문자와 우선순위 번호 값이 쌍으로 필요해서 구조체 변수 사용
  • 우선순위 번호 없이 이전 연산자와 바로 비교할 수 있지만 비효율적임 (연산 횟수 증가)
  • 상태값(status)을 이용해 각 상태 별 다르게 처리해야하는 부분 처리
  • 스택(배열)에 넣고 빼는걸 함수화할 수 있지만 2줄짜리를 굳이 함수처리해야 하나 싶어 안함
  • 연산자 및 괄호의 우선순위에 주의할 것
※ Point
실수 연산 시 항상 type에 유의해서 수식을 넣어줘야 함. 경우에 따라 되는 계산도 있지만 컴파일러에 따라 다를 수 있으므로 항상 연산자가 실수임을 알려주는 것이 필요
→ float a = 1.f + 2.f

특히 나누기 '/' 연산의 경우는 정수형태로 계산할 경우 값이 나오지 않음
→ float a = 1 / 10 (결과값 0.000000)
→ float a = 1.f / 10.f (결과값 0.100000)

나누기의 경우도 두 연산자 중 하나만 실수처리해도 되긴 하지만 비쥬얼 스튜디오 외 다른 컴파일러에서는 오작동 할 수 있으므로 항상 붙여주는 것이 좋을 듯
#include "Header.h"

typedef struct {
	char a;  // 실제 연산기호 저장
	int i;  // 연산기호의 우선순위 저장
}opers;

int operator_checker(char* str, int n, float* minus, int* status);
void calc(float* result, int* r, char oper);
void error(char* str);


/* 최종 계산값 도출 */
/* parameter : 계산할 문자열 */
/* return : 정상 == 계산결과, 비정상 == 프로그램 종료*/
float calcuator(char* str)
{
	float result[10] = { 0 };  // 숫자 저장 스택
	int r = 0;  // result count
	opers oper[10];  // 연산기호, 괄호 저장 스택 (구조체)
	int op = 0;  // oper count		
	float minus = 1;  // 음수변환자
	float f_count = 10;  // 실수 자릿수 계산

	int n = -1;  // str count
	int c, status = 0;  // c : 연산자 우선순위, status = 숫자 시작과 끝 상태
	while(str[++n] != '\0')
	{
		c = operator_checker(str, n, &minus, &status);
		
		/* '-'가 연산자가 아니라 음수표기인경우 함수에서 처리만 하고 그냥 넘김 */
		if (c == -1) continue;

		/* 숫자일 경우 변환해서 바로 저장 */
		if (c == 0)
		{
			if (status != 2)
			{
				result[r] = result[r] * 10.f + ((float)(str[n] - '0') * minus);
				status = 0;
			}
			else
			{
				result[r] = result[r] + 
					((float)(str[n] - '0') / f_count)*minus;
				f_count *= 10.f;
				status = 2;
			}
			
			continue;
		} 
		
		/* 연산자 등장 시 */
		if (c > 0)
		{			
			/* 숫자가 저장된 직후 상태인 status == 0일때만 r++시켜줌 */
			if (status == 0 || status == 2)
			{
				/* 첫 문자가 괄호나 연산자로 시작할 경우 r++해주면 안됨 */
				if (n == 0 && (str[n] == '+' || str[n] == '-' || str[n] == '('));
				else
				{
					r++;
					status = 1;  // 다시 숫자 저장될 때까지는 r++ 안되게 1로 변경
					minus = 1;  // 숫자 저장이 끝나면 항상 기본값으로 바꿔줌
					f_count = 10;  // 숫자 저장이 끝마녀 기본값 셋팅 
				}
			}
						
			/* 첫 연산자일 경우 무조건 저장 */
			if (op == 0)
			{
				oper[op].a = str[n];  // 실제 연산자 저장
				oper[op++].i = c;  // 연산자 우선순위 저장
			}

			/* 두번째부터는 우선순위 비교 후 처리 */
			else if (op > 0)
			{
				/* 괄호 및 ^기호가 아닌 다른 기호가 나왔을 때 */
				if (c > 0 && c < 3)
				{
					/* 현재 연산자보다 우선순위가 높거나 같은 연산자는 계산 처리 */
					/* 이전 연산자가 열린괄호면 수행 중단 */
					while (op > 0 && oper[op - 1].i >= c && oper[op - 1].i != 4)
						calc(result, &r, oper[op-- - 1].a);
				}
				
				/* 닫힌괄호가 나왔을 때 */
				else if (c == 5)
				{
					/* 열린괄호가 나올때까지 계산 처리 */
					while (op > 1 && oper[op - 1].i != 4)
						calc(result, &r, oper[op-- - 1].a);

					/*열린 괄호 제거 */
					op--;
				}
			
				/* 닫힌 괄호가 아니라면 현재 연산자 저장 */
				if (c != 5)
				{
					oper[op].a = str[n];
					oper[op++].i = c;
				}
			}
		}		
	}

	/* 숫자로 끝난 경우, r++이 되지 않았기 때문에 마지막으로 해줌 */
	if (status == 0 || status == 2) r++;

	/* 남은 연산자 모두 계산 */
	while (op != 0)
		calc(result, &r, oper[op-- - 1].a);

	/* result에는 최종 값 하나만 남아있어야 함 */
	if (r == 1)
		return result[r - 1];
	else error("계산이 비정상 종료되었습니다.");
	return 0;	// 필요없는데 안해주면 비쥬얼 스튜디오에서 계속 warning을 띄움..
}

 


 

[ int operator_checker() - 글자 종류 확인해서 유형 맞는 처리를 해줌 ]

  • 마이너스 '-'가 연산자가 아닌 음수 처리일 경우의 수 상정
  • 직전 연산자와 비교해야하므로 직전연산자를 static으로 저장하도록 함
  • 소수점이 나왔을 때 상태값 변경
/* 숫자 및 연산자 확인 */
/* parameter : 문자열, 해당 문자열 순서, minus변수 포인터, status변수 포인터 */
/* return : 숫자 : 0, 연산자 : 연산자 우선순위, 음수, 소수기호 : -1 */
int operator_checker(char* str, int n, float* minus, int* status)
{
	static char operators[] = "+-*/^()";
	static int operator_order[] = { 1,1,2,2,3,4,5 };
	static int pre_str = 0;	// 한칸 전 값을 저장하고 있음
	/* 첫 문자가 +, -, ( 가 아니라면 에러처리 */
	if (n == 0 && (str[n] == '*' || str[n] == '/' || 
							str[n] == '^' || str[n] == ')'))
		error("첫 문자가 잘못되었습니다.");

	/* 해당 문자열이 숫자라면 0값 리턴 */
	if (str[n] >= '0' && str[n] <= '9')
	{
		pre_str = 0;
		return 0;
	}
	
	/* 소숫점이라면 status 상태값 바꾸고 -1값 리턴 */
	if (str[n] == '.')
	{
		*status = 2;
		return -1;
	}

	/* 연산기호라면 연산자 우선순위 번호 리턴 */
	for (int w = 0; w < 7; ++w)	
		if (str[n] == operators[w])
		{
			/* 연산기호가 아니라 음수기호일 경우 */
			/* 첫번째에 '-'기호일 때 음수 처리 */
			/* str[i] == '-'이면서 str[i-1]이 연산자, 열린괄호일 경우 음수 처리 */
			if (str[n] == '-' && ((n == 0) || 
					(n > 0 && pre_str > 0 && pre_str < 5)))
			{
				*minus = -1;
				pre_str = operator_order[w];
				return -1;
			}
			/* 괄호나 '-'가 아닌데 앞이 기호라면 기호 중복이므로 에러 처리 */
			else if (n > 0 && str[n] != '-' && str[n] != '(' &&
						str[n] != ')' && pre_str > 0 && pre_str < 5)
				error("기호가 중복으로 입력되었습니다.");

			/* 음수기호가 아닌 그냥 연산자일 경우 */
			pre_str = operator_order[w];
			return operator_order[w];
		}
	
	/* 숫자, 연산자, 괄호 외 문자가 입력돼있으면 프로그램 종료 */
	error("수식에 잘못된 문자가 포함되었습니다.");
	return 0;  // 여기선 굳이 필요없는데 안해주면 비쥬얼 스튜디오에서 계속 warning을 띄움..
}

 


[ void calc() - 연산자가 제공되면 유형에 맞게 두 숫자를 계산해서 저장]

  • 제곱(^)연산자는 연속으로 나올 경우 뒤에서부터 계산함 (2^2^3 == 2^8)

 

※ Point
포인터변수의 값을 -- 해줄 때 괄호를 꼭 해줘야함
→ *r-- (비정상)
→ (*r)-- (정상)

'--' 표기는 해당 값(*r)을 -1해준다는 의미이므로, 해당값을 표시하는 *r 전체를 괄호로 묶어줘야함
/* 연산자 계산 */
/* parameter : 숫자 저장된 배열, 배열 현재 갯수, 연산자 */
/* return : void */
void calc(float* result, int* r, char oper)
{
	/* 연산자가 있는데 result에 값이 하나라면 비정상 처리 */
	if (*r < 2)
		error("연산자가 잘 못 입력되었습니다.");

	else if (oper == '+')
	{
		result[*r - 2] += result[*r - 1];
		result[(*r)-- - 1] = 0;
	}
	else if (oper == '-')
	{
		result[*r - 2] -= result[*r - 1];
		result[(*r)-- - 1] = 0;
	}
	else if (oper == '*')
	{
		result[(*r) - 2] *= result[*r - 1];
		result[(*r)-- - 1] = 0;
	}
	else if (oper == '/')
	{
		result[(*r) - 2] /= result[*r - 1];
		result[(*r)-- - 1] = 0;
	}
	else if (oper == '^')
	{
		float temp = 1;
		for (int i = 0; i < result[*r - 1]; ++i)
			temp *= result[*r - 2];
		result[*r - 1] = 0;
		result[(*r)-- - 2] = temp;		
	}
}

 


 

[ error() - 에러메세지 출력 후 프로그램 종료 ]

  • 만약 오류메세지 출력 후 계속 재사용한다면 해당 함수 불필요
  • return값을 통한 에러 처리 시, 여러 함수를 거칠 때 최종적으로 main()함수까지 돌아갈 수 있도록 잘 구성해줘야함
void error(char* str)
{
	printf("%s\n", str);
	exit(1);
}

 


 

실수 계산 정확도가 생각보다 좀 떨어져서 찝찝하긴 하지만.. 어쨌든 끝났습니다. ㅎㅎ

728x90

댓글

💲 추천 글