본문 바로가기

Program Language and Algorithm

소프트웨어 엔지니어에게 수학이 중요한 이유 (부제 : C++로 만드는 소수 판별 알고리듬 + 에라토스테네스의 체 )

반응형

 "소프트웨어 엔지니어가 되는데 수학이 꼭 필수인 가요?"

 라는 질문을 가끔 받아왔습니다. 그럴 때마다 제 답변은 같았어요.

 "꼭 필수는 아니지만, 고생을 줄이고 싶다면, 필수가 됩니다. 그리고 지금 굳이 안 해도 나중엔 자연스럽게 수학을 하고 있을 거예요."

 였습니다.

소수 판별

 이번에 소수 판별 알고리듬을 설명하려다 보니, 자연스럽게 위의 문답이 생각나더군요. 제목으로 쓴 "소프트웨어 엔지니어에게 수학이 중요한 가?"라는 질문에 정석적인 답변을 알고 싶으시면, 이 글 보다 컴퓨터 공학이나 수학과 교수님께 문의하세요. 이 글에서는 소수 판별 알고리즘을 점진적으로 설명함으로써  상기 질문에 대한 경험적인 지식을 담으려고 합니다.

 그럼 처음에 소수 판별 프로그램을 만드는 대표적인 간단한 예시를 보죠. 

#include <iostream>
#include <chrono>

using namespace std;

bool isPrime(int in){
    if( 2 > in){
	return false;
    }

    for(int i = 2 ; in > i ; i++){
	if(0 == (in % i)){
	    return false;
	}
    }
    return true;

}

int main(int argc, char ** argv){
    int in;
    chrono::time_point<chrono::high_resolution_clock> start;
    chrono::time_point<chrono::high_resolution_clock> end;


    if(2 == argc){
	in = atoi(argv[1]);
    }else{
    	cout >> "Enter the input : ";
	cin >> in;
    }

    start = chrono::high_resolution_clock::now();
    if(isPrime(in)){
	cout << in << " is prime number." << endl;
    }else{
	cout << in << " is not prime number." << endl;
    }
    end = chrono::high_resolution_clock::now();

    cout << "Execute time : " << chrono::duration_cast<chrono::microseconds>(end - start).count() << "us" << endl;
    return 0;

}

 이 글에서는 점진적으로 조금 더 나은 소수 판별 프로그램을 만들어 갈 것입니다. (사실 다른 글들도 그런 식이지만...) 쓸데없는 분량 증가를 줄이기 위해 main함수는 동일하게 쓰고, isPrime 함수만 업데이트합니다.

 먼저 main함수를 보죠. 입력을 받기 위한 변수 in과 구동 시간을 측정하기 위한 변수 start, end를 만들어 주었습니다. 그 후 argument로 받은 입력이 있다면 할당하고, 없다면 키보드로부터 입력받습니다.

 isPrime을 호출하기 전에 start변수에 시작 시간을 저장하고, 호출 결과에 따라 소수인지 아닌지 출력합니다. 그 후 종료 시간을 저장한 후, 구동에 걸린 시간(함수 구동 시간)을 출력한 뒤 종료됩니다.

 그럼 isPrime 함수를 보시죠. 제일 간단한 형태입니다. 소수의 정의는 "1보다 크고, 1과 자기 자신만을 약수로 가지는 수" 임으로 in이 2보다 작다면, false를 반환합니다. 그 후 2부터 in-1까지의 숫자 중 약수가 있는지 확인합니다. 약수가 있다면 false를 없다면 true를 반환하죠.

 상기 프로그램이 소수 판별을 리포트로 냈을 때, 가장 많이 받는 답안입니다.

 그럼 두 번째 답안을 보죠. 대수학을 조금 적용해 보면 나오는 답안입니다. in의 약수 중 in/2 보다 큰 값이 있을까요? 답은 "없다"입니다. 그러니 2부터 in-1까지 순회하며, 약수가 있는지 확인한 기존 프로그램을 2부터 in/2까지로 변경합니다. 이때 주의 사항이 있습니다. 순회를 할 때 in/2를 꼭 포함해야 in이 4일 때도 정상 동작합니다. 결과는 아래 함수와 같이 변경되죠.

bool isPrime(int in){
    if( 2 > in){
	return false;
    }

    int limit = in / 2;

    for(int i = 2 ; limit >= i ; i++){
	if(0 == (in % i)){
	    return false;
	}
    }
    return true;
}

 기존 for loop가 2부터 in-1 이였지만, 이번 함수에서는 limit 변수에 in/2를 넣고, 2부터 limit까지 비교하도록 변경하였습니다.

 여기까지 기본 알고리즘에 대수학을 적용했죠. 이번엔 소프트웨어 엔지니어링 기법을 추가하죠. 중복 연산 제거입니다. 약수는 항상 짝을 가집니다. 10의 약수 2는 짝으로 5를 가지고 있죠. 그럼 약수를 판별할 때 굳이 5까지 해야 할까요? 짝이 되는 약수 중 작은 쪽만 판별해도 충분합니다. 그럼 작은 쪽만 판별하려면 어떤 조건을 넣어야 할까요? 답은 sqrt(in)까지만 판별해도 충분합니다.

 예시를 들어 보죠. 만약 in이 10이라면? 위 프로그램은 2에서 약수를 찾았기 때문에 for loop는 한 번만 동작하고, 종료됩니다....? 이상하죠. 이 예시가 조기 종료 조건에 들어가서 적합한 예시가 아닙니다. 적합한 예시는 입력이 소수일 때입니다.

 in에 11을 넣어보죠. 위 프로그램은 2부터 5까지 총 4회의 loop를 진행한 뒤, in이 소수라는 출력을 내놓습니다. sqrt(11)은 3과 4 사이의 숫자죠. 따라서 조건이 변경되면, for loop는 2와 3만 진행해서 2번 동작합니다. 그리고 이 조건은 모든 합성수와 소수에 대해 정답을 내놓습니다.

bool isPrime(int in){
    if( 2 > in){
	return false;
    }

    int limit = static_cast<int>(sqrt(in));

    for(int i = 2 ; limit >= i ; i++){
	if(0 == (in % i)){
	    return false;
	}
    }
    return true;
}

 변경되는 내용은 간단합니다. in/2를 할당한 limit을 sqrt(in)으로 할당합니다. 수학 함수를 사용함으로 #include <cmath>가 추가되어야 하며, sqrt함수의 기본형은 결과가 float형임으로 캐스팅을 추가합니다.

 마지막 최적화를 하겠습니다. 소수의 주요 특성 중 하나를 적용합니다. "짝수 중 소수는 2뿐이다." 초기화 조건으로 입력이 짝수라면, 2인 경우엔 true를 반환하고, 아닌 경우에는 false를 반환합니다. 위 조건을 통과한 입력은 전부 홀수입니다. 홀수는 짝수를 약수로 가질 수 없죠. for loop도 조건이 변경됩니다. 3부터 2씩 추가해서 홀수에 대해서만 판별합니다.

bool isPrime(int in){
    if( 2 > in){
	return false;
    }

    if( 2 == in){
	return true;
    }
    if(0 == (in%2)){
	return false;
    }

    int limit = static_cast<int>(sqrt(in));

    for(int i = 3 ; limit >= i ; i+= 2){
	if(0 == (in % i)){
	    return false;
	}
    }
    return true;
}

 상기 코드가 결과적으로 얻은 효과적인 소수 판별 프로그램입니다. 그럼 성능을 알아봐야겠죠. 입력으로는 10억 이하의 가장 큰 소수 999,999,937을 이용합니다. 아래 스냅숏은 위 4가지 프로그램을 적용시킨 결과입니다.

 프로그램의 구동 시간이 점차 줄어들었죠. 수치로 분석하면, 최악의 케이스(입력이 소수)에 대해 각 프로그램의 구동 시간은 입력 N에 대해서, N, N/2, sqrt(N), sqrt(N)/2로 줄어들게 됩니다.

 위 소수 판별 사례처럼 수학에 대한 기본 지식과 소프트웨어 엔지니어링 지식은 정확한 결과를 빠르게 얻을 수 있도록 도와줍니다.

에라토스테네스의 체

 사실 소수 판별 알고리즘은 수학이 필요하다고 역설하기에 조금 부족합니다. 실제를 이를 주장하려면, 아래와 같은 문제가 적합합니다.

 "N이하의 모든 소수와 총개수를 출력하라"

 이 문제를 접하는 사람 중 대다수는 위에서 만든 소수 판별 알고리듬을 바로 적용하게 되겠죠. 이렇게 만든 프로그램은 아래와 같습니다.

#include <iostream>
#include <cmath>
#include <chrono>
#include <vector>

using namespace std;

bool isPrime(int in){
    if( 2 > in){
	return false;
    }

    if( 2 == in){
	return true;
    }
    if(0 == (in%2)){
	return false;
    }

    int limit = static_cast<int>(sqrt(in));

    for(int i = 3 ; limit >= i ; i+= 2){
	if(0 == (in % i)){
	    return false;
	}
    }
    return true;
}

int printPrime(vector<bool> &pri){
    int count = 0;

    for(int i = 2 ; pri.size() > i ; i++){
	if(pri[i]){
	    cout << i << " ";
	    count++;
	}
	if(0 == (i % 100)){
	    cout << endl;
	}
    }
    cout << endl << " number of primes : " << count << endl;

    return 0;
}

int main(int argc, char ** argv){
    int in;
    chrono::time_point<chrono::high_resolution_clock> start;
    chrono::time_point<chrono::high_resolution_clock> end;


    if(2 == argc){
	in = atoi(argv[1]);
    }else{
	cin >> in;
    }

    vector<bool> pri(in+1, false);

    start = chrono::high_resolution_clock::now();
    for(int i = 2 ; in >= i ; i++){
	if(isPrime(i)){
	    pri[i] = true;
	}
    }
    end = chrono::high_resolution_clock::now();

    printPrime(pri);

    cout << endl << "Execute time : " << chrono::duration_cast<chrono::microseconds>(end - start).count() << "us" << endl;
    return 0;
}

 기존 코드를 활용해서 isPrime을 2부터 in까지 구동해 주면 되겠죠. 그런데 이 구조에서는 함수를 1회 구동할 때마다 출력을 해야 하고, 출력은 많은 리소스를 요구함으로 알고리듬 비교에 적합하지 않습니다. vector를 이용해 소수인지 합성수 인지만 저장하게 한 후, 한 번에 출력하는 구조로 만들었습니다. 출력 함수에서는 100 단위로 개행해 줍니다.

 위 프로그램을 구동한 스냅숏은 아래와 같습니다. 이전과 같이 10억과 같은 큰 수를 입력하면 구동 시간이 너무 오래 소요되기 때문에 10,000,000(천만)을 입력으로 사용했습니다.

 천만 이하의 소수의 개수는 664,579개 이고, 구동 시간은 약 1.8초가 소요되었네요. 아마도 이 프로그램이 "에라토스테네스의 체"에 관한 수학적 지식이 없는 소프트웨어 엔지니어가 만들 수 있는 프로그램 일 것입니다. ( 물론 해당 지식이 없어도, 메모이제이션 등과 같은 소프트웨어 엔지니어링 기술을 적용한다면, 자연스럽게 에라토스테네스의 체를 만들게 되겠죠. )

 N 이하의 모든 소수를 찾는 문제에 대해서 개발자가 에라토스테네스의 체를 이미 알고 있다면, 당연히 에라토스테네스의 체를 만들어 이 문제를 해결할 것입니다. 에라토스테네스의 체에 대한 배경 지식은 링크로 대체합니다. 위키피디아

 

에라토스테네스의 체 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 수학에서 에라토스테네스의 체는 소수를 찾는 방법이다. 고대 그리스 수학자 에라토스테네스가 발견하였다. 알고리즘[편집] 2부터 소수를 구하고자 하는 구간

ko.wikipedia.org

 위 링크에도 소프트웨어 코드가 있으나, 제가 만든 간단한 코드를 소개하겠습니다.

#include <iostream>
#include <vector>
#include <chrono>

using namespace std;

int makeEratos(vector<bool> &eratos, int in){
    eratos = vector<bool>(in, true);

    eratos[0] = false;
    eratos[1] = false;

    for(int i = 2 ; in > i ; i++){
	if(eratos[i]){
	    for(int j = i * 2 ; in > j ; j += i){
		if(eratos[j]){
		    eratos[j] = false;
		}
	    }
	}
    }
    return 0;
}

int printEratos(vector<bool> &eratos){
    int count = 0;

    for(int i = 2 ; eratos.size() > i ; i++){
	if(eratos[i]){
	    cout << i << " ";
	    count++;
	}
	if(0 == (i % 100)){
	    cout << endl;
	}
    }
    cout << endl << " number of primes : " << count << endl;

    return 0;
}

int main(int argc, char ** argv){
    int in;

    chrono::time_point<chrono::high_resolution_clock> start;
    chrono::time_point<chrono::high_resolution_clock> end;

    if(2 == argc){
	in = atoi(argv[1]);
    }else{
	cout << "Enter the number : ";
	cin >> in;
    }

    if(0 > in){
	return 0;
    }

    in++;

    vector<bool> eratos;

    start = chrono::high_resolution_clock::now();
    makeEratos(eratos, in);
    end = chrono::high_resolution_clock::now();

    printEratos(eratos);

    cout << "Execute time : " << chrono::duration_cast<chrono::microseconds>(end - start).count() << "us" << endl;

    return 0;
}

 에라토스테네스의 체 구현 코드에 대한 설명에서도 중복되는 main함수와 printEratos함수는 상기 코드를 제외하고, 생략하겠습니다.

 먼저 main함수는 소수 판별 때와 같은 구조입니다. 입력을 받고, 함수를 호출하면서, 구동 시간을 측정합니다.

 printEratos함수는 넘겨받은 vector를 순회하며, 소수로 판별된 수를 출력하고, 총개수를 계산해 출력합니다.

 그럼 중요한 makeEratos함수 차례죠. 입력 변수로는 bool 형태의 vector eratos와 입력 숫자 in을 받습니다. 먼저 eratos를 초기화해줍니다. 크기는 in이며,  판별 전에 모든 숫자는 소수로 예측함으로 true로 설정합니다. 벡터의 인덱스는 해당 숫자에 매칭 합니다. eratos[5]가 true이면, 5는 소수, false 이면 소수가 아닌 것이죠.

 당연히 0과 1은 소수가 아니기 때문에 false로 설정합니다. 그 후 2부터 in-1까지 순회하며, 해당 숫자가 i가 소수(eratos[i] == true)이면, 모든 ni(n은 2 이상)를 false로 설정합니다.

 위 프로그램의 구동 결과는 아래 스냅숏입니다.

 결과는 당연히 이전 프로그램과 동일하지만, 구동 시간이 현저히 줄었습니다. 1.8초나 걸리던 프로그램이 0.060초 소요되었네요. 이게 바로 수학을 배워야 하는 이유입니다. 사람에 따라 필요한 지식은 다르겠지만, 에라토스테네스의 체와 같은 간단한 알고리듬과 모듈러 연산이나, 기본적인 확률론은 숙지하는 게 이롭습니다.

 제 결론은 소프트웨어 엔지니어에게 수학은 필수가 아닐 수 있습니다. 하지만 수학을 적용한 프로그램과 그렇지 못한 프로그램의 효율성을 고려한다면, 필수가 됩니다.

 이상 논설은 줄이고, 상기 프로그램도 최적화해 봐야겠죠. 제가 가장 먼저 떠오른 것은 i가 소수일 때 ni를 순회하며 eratos를 업데이트하는 방식입니다. 위 코드에서는 eratos[j]가 true인 경우에만 false로 업데이트했죠. 하지만 조건문은 시간을 많이 씁니다. 대신 true or false에 관계없이 전부 false로 설정해도 상관없죠. 사실 이런 고민은 전자 공학 백그라운드의 소프트웨어 엔지니어들이 주로 하는 고민입니다. 컴퓨팅 시스템에 따라 조건문이 현저히 느리기도 하고, 값의 할당이 현저히 느리기도 합니다. 이 의문은 컴퓨팅 시스템에 따라 답이 달라질 수 있습니다. 그래도 일단 최신 랩탑에 탑재한 리눅스에서의 효과를 보죠.

 코드는 아래와 같습니다.

int makeEratos(vector<bool> &eratos, int in){
    eratos = vector<bool>(in, true);

    eratos[0] = false;
    eratos[1] = false;

    for(int i = 2 ; in > i ; i++){
	if(eratos[i]){
	    for(int j = i * 2 ; in > j ; j += i){
		eratos[j] = false;
	    }
	}
    }
    return 0;
}

 if(eratos[j]) 구문이 사라졌습니다. 이 프로그램은 지정할 변수에 true or false를 보지 않고, 무조건 false로 설정하죠. 그 결과는 어떻게 될까요?

 어차피 구동 결과는 같음으로 printEratos함수는 주석처리하였습니다. 위는 조건문을 쓴 결과이고, 아래는 무조건 설정을 한 결과입니다. 10회씩 구동해본 결과 무조건 설정을 한 경우가 빠릅니다.

 사실 위 결론은 성급합니다. 이유는 아래 스냅숏을 확인해 보시죠. 

 인풋 변수를 천만에서 10억으로 변경하면, 조건을 확인한 경우가 더 빨라지게 됩니다.  이렇게 상황에 따라 결과가 달라지기도 하니, 엔지니어는 항상 다양한 변수를 숙지하고, 최적의 방향으로 개발해야 하죠.

 이 글에서는 입력이 천만 일 때 걸리는 시간은 너무 작아서 문제가 안됨으로 업데이트 시 조건문을 사용하겠습니다. 이번엔 수학적인 지식을 적용합니다. 결과는 아래 코드입니다.

int makeEratos(vector<bool> &eratos, int in){
    eratos = vector<bool>(in, true);

    eratos[0] = false;
    eratos[1] = false;

    int limit = static_cast<int>(sqrt(in));

    for(int i = 2 ; limit >= i ; i++){
	if(eratos[i]){
	    for(int j = i * i ; in >= j ; j += i){
		if(eratos[j]){
		    eratos[j] = false;
		}
	    }
	}
    }
    return 0;
}

  소수 판별 알고리듬에서 언급했듯이 약수가 있는지 확인할 때는 sqrt(in) 이하까지만 보면 충분합니다. in보다 작은 수에 대해서는 in에 대한 판별을 할 때 당연히 판별됨으로 검색 조건을 2부터 sqrt(in)으로 지정합니다. 다음으로 ni들을 설정할 때 i보다 작은 n은 이미 계산되었습니다. ( ex : n이 2이고 i가 5인 경우는 n이 5이고 i가 2인 경우에 이미 계산되었음.) 따라서 int j = 2 * i 가 j = i * i로 변경되었습니다.

 아래 스냅숏은 상기 코드들의 구동 결과입니다.

  수학적인 최적화가 적용된 era3가 가장 빠른 결과를 보여줍니다. 여기에 추가로 "2를 초과하는 짝수는 모두 합성수이다"라는 것을 적용시키면 더 최적화가 가능합니다.

소수 판별의 함정

 소수 판별에 대해 고민한 많은 소프트웨어 엔지니어들은 에라토스테네스의 체를 변형한 다른 최적 알고리듬을 생각합니다. 에라토스테네스의 체는 n이하의 소수들이 자신의 배수들을 합성 수로 지정해서 소수를 판별합니다. n이 n보다 작은 소수의 배수가 아니라면, n은 소수라는 정의입니다. 그럼 반대로 자신보다 작은 소수들이 자신의 약수인지 판별하는 것은 어떨까요? 일종의 dp 기법이라고도 볼 수 있고 메모이제이션 기술도 적용되는 기법이죠. 

 위 기법의 장점은 메모리와 결과물의 활용에 있습니다. 에라토스테네스의 체는 n보다 작은 소수의 개수와 상관없이 n크기의 메모리 공간이 필요합니다. 하지만 n보다 작은 소수의 개수는 n이 커질수록 증가량이 감소합니다. 그러니 소수의 리스트를 저장하면, 메모리를 아낄 수 있습니다. 또한 소수를 순회한다고 할 때, 순회의 횟수와 효율성을 향상할 수 있습니다.

 코드를 보며 설명하죠.

#include <iostream>
#include <vector>
#include <chrono>
#include <cmath>

using namespace std;

int makePriList(vector<int> &pri, int in){
    pri.clear();
    pri.reserve(in/5);

    for(int i = 3 ; in > i ; i+=2){

	bool flag = true;

	int sq = static_cast<int>(sqrt(i));

	for(auto j : pri){
	    if(sq < j){
		break;
	    }
	    if(0 == (i % j)){
		flag = false;
		break;
	    }
	}

	if(flag){
	    pri.push_back(i);
	}
    }

    pri.insert(pri.begin(), 2);
    return 0;
}

int printPriList(vector<int> &pri){

    for(int i = 0 ; pri.size() > i ; i++){
	cout << pri[i] << " ";
	if(0 == (i % 10)){
	    cout << endl;
	}
    }

    cout << endl << " number of primes : " << pri.size() << endl;


    return 0;
}

int main(int argc, char ** argv){
    int in;

    chrono::time_point<chrono::high_resolution_clock> start;
    chrono::time_point<chrono::high_resolution_clock> end;

    if(2 == argc){
	in = atoi(argv[1]);
    }else{
	cout << "Enter the number : ";
	cin >> in;
    }

    if(0 > in){
	return 0;
    }

    in++;

    vector<int> pri;

    start = chrono::high_resolution_clock::now();
    makePriList(pri, in);
    end = chrono::high_resolution_clock::now();

    //printPriList(pri);

    cout << "Execute time : " << chrono::duration_cast<chrono::microseconds>(end - start).count() << "us" << endl;

    return 0;
}

 함수의 호출 형태가 변경되었습니다. vector형 변수 pri의 형식이 bool에서 int로 변경되었죠. 변경된 벡터는 소수를 직접 저장합니다. for loop는 3부터 2씩 증가해서 in이하의 숫자들 중 소수인 것을 찾게 됩니다. pri에는 항상 i보다 작은 소수들이 저장되어 있으니, pri를 순회합니다. 이때 sqrt(i) 보다 큰 값은 할 필요가 없으니 중지시킵니다. 나누어 떨어지는 소수가 없으면 pri에 i를 추가합니다. 마지막으로 pri의 가장 앞에 2를 추가해 줍니다.

 결과를 보기 전에 간단히 생각해 보시죠. 이 알고리듬은 에라토스테네스의 체보다 빠른지, 에라토스테네스의 체에서 어떤 점을 보완하는지 살펴보죠. 에라토스테네스의 체는 합성수 n에 대해서 n의 약수들이 n이 합성수 임을 업데이트합니다. 100의 경우 약수 2가 이미 합성수 임을 업데이트해줬지만, 다른 약수인 5도 100이 합성 수라고 업데이트합니다. 그에 비해 제안한 알고리듬은 2가 100의 약수라고 했을 때 , 구동을 종료하고 다음 숫자로 넘어가죠. 제안한 알고리듬이 이용한 sqrt(n) 보다 작은 모든 소수가 약수인지 비교하는 방법은 합성수 n이 합성수 임을 확인하는 가장 빠른 방법입니다. 이런 장점을 생각하면 제안 알고리듬이 느리지 않을 것 같습니다.

 아래 스냅숏은 제안한 알고리듬과 에라토스테네스의 체의 구동 결과입니다.

 구동 결과는 에라토스테네스의 체가 월등히 빠릅니다. 이유는 아래와 같습니다.

 에라토스테네스의 체의 경우 워스트 케이스는 합성수 n의 모든 약수가 합성수 임을 업데이트하는 것입니다. n이 크다면 합성수의 개수도 커지겠죠. 그런데 이 알고리듬은 구조상 n의 약수 중 소수만 업데이트를 합니다. 또한 sqrt(n) 이상의 약수도 업데이트하지 않습니다. 즉 합성수에 대해 제안 알고리듬의 성능이 높을 수 있지만, 에라토스테네스의 체가 많이 느리진 않습니다. (확인 결과 10억 이하의 숫자 중 중복 연산이 가장 많은 횟수는 9회입니다.)

 하지만 n이 소수라면, 에라토스 테네스의 체는 n에 접근했을 때 이미, 조건(2와 sqrt(n) 사이의 소수)의 숫자들이 약수가 아님을 증명해서 n이 소수라는 것을 알고 있습니다. 그런데 제안 알고리듬은 그 사실을 모르기 때문에 조건에 맞는 숫자들 전체가 약수가 아닌지 확인해야 하죠. 만약 n이 999,999,937이라면, 3,401개의 소수들을 순회하며,  약수가 아님을 확인해야 합니다.

 많은 합성수 들에 대해서 9회 이하의 중복 연산을 하는 게, 적은 수의 소수에서 천 번이 넘는 연산을 해주는 것보다 효율적인 것이죠. 

 예제 전엔 제안 알고리듬이 효율적이라고 생각한 분들도 많을 것입니다. (저 포함) 하지만 실제로는 그렇지 않죠. 수학적인 지식들을 이용해 검증해 보니, 왜 느린지도 알 수 있었습니다. 적절한 수학적 지식들은 이런 바보짓을 방지하게 해 주거나, 이유를 설명해 줍니다.

 추가 논설은 이미 위에서 조금 했으니 이만 마치겠습니다.

 

 긴 글 봐주셔서 감사합니다.

반응형