1. 일반적인 표현방법

int func(int n)
{
	if (n == 0)
		return 0;
	if (n == 1 || n == 2)
		return 1;
	return func(n - 1) + func(n - 2);
}

장점: 코드가 직관적이고 이해가 쉬움 

단점: 똑같은 연산을 여러번 함으로써 굉장히 비효율적이고 함수가 호출될 때 또다른 함수2개를 호출하므로 연산시간이 길어짐

깊이가 2의 제곱꼴로 깊어짐(1->2->4->8->16 . . )


시간복잡도: 연산의 횟수가 2의 거듭제곱꼴로 지속되므로 O(2^N)


2. 최적화 기법 1 : 동적프로그래밍

핵심 : 딱 한번만 계산하고 저장된 값(배열에 담음)을 호출만 한다.(메모이제이션 기법)

(위의 그림에서 f(n-5)는 7번호출되므로 메모이제이션기법으로 한번만 계산함)


시간복잡도 : 기존에 함수호출의 깊이가 깊어질수록 2의 N승씩 연산했는데, 메모이제이션으로 단 한번씩만 계산하므로 O(N)


코드

#include<iostream>
using namespace std;
long long dp[50];
long long func(long long x);
int main()
{
	cout << "50번째 피보나치 수열은" << endl;
	cout << func(50) << "입니다." << endl;

}
long long func(long long x)
{
	if (x <= 2)return 1;
	//저장된 값이라면 바로 값을 리턴!
	if (dp[x] != 0)
		return dp[x];
	dp[x] = func(x - 1) + func(x - 2);
	return dp[x];
}


결과



3. 최적화 기법 2 : 행렬의 성질 이용(분할 정복)

아이디어: 2*2크기의 행렬에 피보나치수열을 표현하여 행렬곱연산(거듭제곱 연산)으로 표현하고싶다.

핵심: 분할정복 기법으로, 일단 잘게 나누고, 나눠진 값들을 합쳐서 최종값을 구함


n번째 피보나치 수열 Fn을 행렬로표현하기


10번째 피보나치 구하기


1단계-> 10번째 피보나치 분할


2단계-> 5번째 피보나치 분할(홀수이므로 제곱연산때처럼 4번째 피보나치*첫번째 피보나치로 나눔)



3단계-> 4번째 피보나치 분할


3단계(마지막)-> 2번째 피보나치 분할


모두 나눴으니 이제 거꾸로 계산하며 합치면 됨(분할->정복)



결과


시간복잡도: n번째 피보나치가 n/2번째피보나치의 제곱->n/4번째 피보나치의 제곱 ...순으로 나눠지므로 최초의 행렬을 logN회 제곱하면 되므로 O(logN)


코드

#include<iostream>
using namespace std;
struct M
{
	long long data[2][2];
};
long long func(int x);
M divide(M a, int n);
M multiply(M a, M b);
int main()
{
	cout << "46번째 피보나치 수는" << endl;
	cout << func(46) << "입니다." << endl;
}
long long func(int x)
{
	M a;
	//F(1)설정
	a.data[0][0] = 1, a.data[0][1] = 1;
	a.data[1][0] = 1, a.data[1][1] = 0;
	//분할
	a = divide(a, x);
	//F(n)출력)
	return a.data[0][1];
}
M divide(M a, int n)
{
	if (n > 1)
	{
		//n=1이 될때 까지 분할한다.(분할)
		a = divide(a, n / 2);
		//분할된 행렬을 다시 곱함(정복)
		a = multiply(a, a);
		//n이 홀수면
		if (n % 2 == 1)
		{
			M b;
			b.data[0][0] = 1, b.data[0][1] = 1;
			b.data[1][0] = 1, b.data[1][1] = 0;
			//구한 a와 f(1)곱해줌.
			a = multiply(a, b);
		}
	}
	return a;
}
M multiply(M a, M b)
{
	M c;
	//행렬곱셈
	c.data[0][0] = a.data[0][0] * b.data[0][0] + a.data[0][1] * b.data[1][0];
	c.data[0][1] = a.data[0][0] * b.data[0][1] + a.data[0][1] * b.data[1][1];
	c.data[1][0] = a.data[1][0] * b.data[0][0] + a.data[1][1] * b.data[1][0];
	c.data[1][1] = a.data[1][0] * b.data[0][1] + a.data[1][1] * b.data[1][1];
	return c;
}

코드가 어려울 수 있는데, 노트에 직접 적어보면 분할 정복의 과정이 보일겁니다.


결과














일반적인 거듭제곱 표현 O(N)

int func(int x, int n)
{
	int res = 1;
	for (int i = 0; i < n; i++)
		res *= x;
	return res;
}

장점: 작성이 쉽고 직관적임

시간복잡도 : 지수(n)만큼 계산하므로 O(N)




최적화 기법(분할 정복+재귀) : O(logN)


아이디어

짝수일 때 : 2의 8승을 2의 제곱을 구한다음 그것만 3번 곱해볼까?

홀수일 때 : 2의 7승을 2의6승을 짝수처럼 계산하고 거기다가 2를 곱해볼까?


시간복잡도: 지수를 반으로 나눠가며 계산하므로 O(logN) 


코드

#include<iostream>
using namespace std;
long long func(int x, int n);
int main()
{
	//2의 50승 구하기
	int x = 2;
	int n = 50;
	cout << "2의 50승은 " << endl;
	cout<<func(x, n) << "입니다." << endl;
}
long long func(int x, int n)
{
	if (x == 1)
		return 1;
	if (n == 1)
		return x;
	//지수가 홀수 일때 
	if (n % 2 == 1)
	{
		long long res = func(x, (n - 1) / 2);
		return (res * res) * x;
	}
	//지수가 짝수 일 때
	else
	{
		long long res = func(x, n / 2);
		return res * res;
	}
}

결과




누적합은 말 그대로 구간의 누적합을 구하는 문제입니다.

일반적으로  사용되는 배열에 값을 저장하고 지정된 인덱스부터 하나씩 더해가는 방식은 최악의 경우O(n^2)의 시간복잡도를 갖기 때문에 입력의범위가 클 때 사용할 수 없습니다. 하지만 Prefix sum방식을 사용하면 O(N)으로 해결 할 수 있습니다.

누적합은 문제에서 수열이 주어지고 어떤 구간의 값의 합을 구해야 할 때 쓰일 수 있습니다. 

예를들어 크기가 5인arr배열에서 3번index와 5번index구간의 구간합을 구한다고 가정하면, 누적합은 arr[0~b까지의 누적합] - arr[0~a-1까지의누적합]으로 표현할 수 있습니다.

그림을 보면 쉽게 이해할 수 있습니다. b-a구간의 누적합을 구하기위해선 b구간까지의 합- a-1구간까지의 합을 빼주면됩니다.





[3,5]구간의 누적합은??







1. 크기가 5인 배열선언( 인덱스1~5)










2. 각 인덱스값에 누적합 저장









3. 3번구간과 5번 구간 사이의 누적합을 구하려면 겹치는 1,2구간을 제외해줘야됨

즉, 2번인덱스의 누적합을 빼줘야함.

 

위의 표에서 분홍색부분이 겹치는 부분,즉 빼줘야하는 부분이고 검은색부분이 답을 구할 범위다.



코드


#include<iostream>
using namespace std;
//인덱스1부터 가정하기위해 크기6으로생성
int arr[6] = { 0,7,6,3,2,1 };
int main()
{
	//arr에 누적합을 저장한다.
	for (int i = 1; i <=5; i++)
	{
		
		arr[i] = arr[i - 1] + arr[i];
	}
	//[3,5]구간 누적합 구하기
		cout << arr[5] - arr[3 - 1] << '\n';
}

문제풀고 복습하기


https://www.acmicpc.net/problem/11441


https://www.acmicpc.net/problem/11659

 

 

n: 자료의 개수, d: 수의 자릿수

상태: 정렬된 수 가 다른 수가 정렬되는 과정에서 위치의 변동이 생기는지 여부

알고리즘

최선

평균

최악

상태

삽입 정렬

O(n)

O(n^2)

O(n^2)

안정

선택 정렬

O(n^2)

O(n^2)

O(n^2)

불안정

버블 정렬

O(n^2)

O(n^2)

O(n^2)

정렬

셸 정렬

O(n)

O(n^1.5)

O(n^2)

불안정

퀵 정렬

O(nlogn)

O(nlogn)

O(n^2)

불안정

히프 정렬

O(nlogn)

O(nlogn)

O(nlogn)

불안정

합병 정렬

O(nlogn)

O(nlogn)

O(nlogn)

불안정

기수 정렬

O(dn)

O(dn)

O(dn)

안정

카운팅 정렬

O(n)

O(n)

O(n)

안정

평균 O(n^2)의 정렬기법 중 삽입정렬이 제일 빠릅니다. 수가 정렬되어있을 시 최대 O(n)을 자랑합니다.

(셸 정렬은 삽입정렬의 변형된 기법으로, 필요한 경우 사용할 수 있도록 연습합니다.)

카운팅 정렬은 한자리 숫자(0~9)들을 정렬하는 등의 특수한 경우에만 사용 할 수 있는 정렬입니다.

 

기수 정렬에서 d는 기수의 자릿수로 자릿수가 작을 때는 O(n)으로 봐도 무방합니다.

자릿수가 고정되어 있는 수들을 정렬 할 때만 사용할 수 있습니다

퀵정렬은 정렬기법 중 제일 빠르지만, 최악의 경우(모든 수가 정렬or역정렬되어있을 때)에는 O(n^2)입니다.

프로그래밍 문제를 풀다가 정렬기법을 써야 할 때 해결해야 할 자료의 범위가 큰 경우(1초당 약 1억번의 연산) 퀵정렬은 최선의 방법이 아닐 수 있습니다.

 

개념

에라토스테네스의 체는 소수를 판별하는 알고리즘으로, 소수를 구하는 방법중 가장 효율적인 방법이라고 할 수 있다.

아래의 그림이 에라토스테네스의 체를 설명하고있다.

그림을보면, 2부터 120까지의 숫자중, 2부터 시작하여, 2를 제외하고 모든 2의배수를 체크한다. 

그리고 3으로 넘어가서 역시3을 제외한 모든 3의배수들을 체크한다. 다음으로 4인데, 4는이미 2의배수를 체크할때 체크되어서 4는 걸러지고 

모든 4의배수들이 체크된다. 이런 순서로 모든 수를 확인한 후 최종적으로 체크가 안 된 수들이 전부 소수다.

아래에 1~120까지의 수 중 소수를 판별하는 코드를 나타냈다. 소수의 시작은 2라서, 반복문의 시작을 i=2로했다. 

1은 별도로 1로 초기화 해준다.


코드

#include<iostream>
using namespace std;

//0~120까지의 수를 담을 배열
int arr[121];
int main()
{
	//1로체크된 수는 소수가 아닌 수임

	//1은 소수가 아니므로 체크
	arr[1] = 1;
	for (int i = 2; i <= 120; i++)
	{
		//4부터걸러지므로 j=i*i
		//j의 배수만큼 걸러지므로 증감값은 j+=i
		for (int j = i * i; j <= 120; j += i)
		{
			//걸러져있으면 다음 수로 넘어간다.
			if (arr[j] == 1)continue;
			//거른다. (소수가 아니다.)
			arr[j] = 1;
		}
	}
	//확인
	for (int i = 2; i <= 120; i++)
	{
		if (arr[i] != 1)
			cout << i << " ";
	}
	cout << endl;
}


결과

에라토스테네스의 체의 시간복잡도는 O(NloglogN)이다.(이유는 모르겠다.ㅠㅠ)


저번에 공부한 코드실행시간분석 코드를 이용해 피보나치수열을

반복, dp(동적프로그래밍)으로 성능차이를 분석해보겠습니다.

재귀함수로 구현한 피보나치는 O(2^N)이고

dp로 구현한 피보나치는O(N)으로, 재귀로구현한것보다 훨씬!!빠른 효율적인 알고리즘입니다.

반복문으로구현한 피보나치또한 O(N)입니다.

따라서 범위가 작을때는 어느방법을 사용해도상관없으나 범위가 커질수록  효율적인 방법을 써야합니다.

dp와 반복문으로로구한 피보나치수열의 시간차이가 상당함을 알 수 있습니다.

(상당한 차이입니다.)


빅오표기법으로 표기한 4가지 시간복잡도의 빠른순서(왼쪽부터)

O(N) <O(NlogN) <O(N^2) < O(2^N)


#include<iostream>
#include<algorithm>
#include<ctime>
using namespace std;
int fibo_recursive(int x);
int fibo_dp(int x);
int  fibo_repeat(int x);
int  dp[100];
int main()
{
	clock_t start, finish;
	double duration;
	int sum = 0;
	start = clock();
	int x = fibo_recursive(40);
	finish = clock();
	duration = (double)(finish - start) / CLOCKS_PER_SEC;
	cout << "재귀구현피보나치값:" << x << "시간: " << duration << "초\n";

	start = clock();
	int y = fibo_dp(40);
	finish = clock();
	duration = (double)(finish - start) / CLOCKS_PER_SEC;
	cout << "dp구현피보나치값:" << y << "시간: " << duration << "초\n";

	start = clock();
	int z = fibo_repeat(40);
	finish = clock();
	cout << "반복문피보나치값:" << z << "시간: " << duration << "초\n";
}
int fibo_recursive(int x)
{

	if (x == 1)return 1;
	if (x == 2)return 1;
	return fibo_recursive(x - 1) + fibo_recursive(x - 2);
}
int fibo_dp(int x)
{
	if (x == 1)return 1;
	if (x == 2)return 1;
	if (dp[x] != 0)return dp[x];
	return dp[x] = fibo_dp(x - 1) + fibo_dp(x - 2);
}
int  fibo_repeat(int x)
{
	if (x == 1)return 1;
	if (x == 2)return 1;
	int temp, current = 1, last = 0;
	for (int i = 2; i <= x; i++)
	{
		temp = current;
		current += last;
		last = temp;
	}
	return current;
}


'알고리즘 > 실행시간 측정' 카테고리의 다른 글

프로그램 실행시간 측정하기  (0) 2019.11.30

프로그램의  코드실행시간을 측정하기위한 방법이다.

주로 시간복잡도의 큰요인으로 작용되는 함수,반복문등의 연산식 전후로 다음과같이 선언하여

실제 코드의 실행시간을 알 수 있다.

clock_t자료형을 사용하기위해 헤더파일 time을 include한다.(여기선c++이기떄문에 ctime으로선언)

CLOCKS_PER_SEC-> 수행시간을 초단위로 계산한다고 생각하면된다.

clock_t 자료형: 실제로는 typedef long clock_t 로 설정되어있어서 clock_t를 long으로써도된다.

duration:함수가 끝날때시간과 시작할때 시간을 빼준값을 실수형으로바꿔서 소수점단위까지의 값을 구한다.

결과값은 컴퓨터의성능에 따라 다르다.(노트북 성능이 쓰레기라 다른 컴퓨터에서 돌렸을때 값보다 많이느리다..)


#include<iostream>
#include<ctime>
using namespace std;

{
	clock_t start, finish;
	double duration;
	int sum = 0;

	start = clock();
	for (int i = 0; i < 100000000; i++)

		sum += i;

	finish = clock();

	//duration=실제 코드수행시간
	duration = (double)(finish - start) / CLOCKS_PER_SEC;
	cout << duration << endl;


}


**여기서구하는건 실제 함수 동작시간이지 시간복잡도o(n)을 계산한게아니다.

시간복잡도: O(N)==O(100000000)==약1초


'알고리즘 > 실행시간 측정' 카테고리의 다른 글

피보나치수열,정렬의 실행시간분석  (2) 2019.11.30

+ Recent posts