JS로 배우는 자료구조와 알고리즘 1 : 언어 기초

XJB
13 min readMay 21, 2018

--

이 글은 O’Reilly의 Data Structure And Algorithms With Javascript를 정리한 글입니다.

만일, 이 글을 읽으실때 이해가 안가는 부분이 많으시다면 먼저 기초적인 자바스크립트의 변수 선언, 연산자, 조건문, 반복문 을 한번 숙지하고 오시는게 좋을것 같습니다. 또한, 개인적인 첨언이 있을 수 있으니, 잘못된 부분이 있다면 지적 바랍니다.

前선언 後 초기화.

JS의 변수는 기본적으로 전역변수(global variable; 프로그램이 시작될때 부터 종료시 까지 살아있는 변수)이고, 엄밀하게 말해서 일반적인 C언어나 Java와 같이 사용하기전에 꼭 선언을 해줘야 할 필요는 없습니다. JS에서 변수가 선언 없이 초기화 (Initialized)되면, 바로 전역 변수가 됩니다.

하지만, 이 책에서는 C++과 Java와 같은 컴파일 언어(Compiled Language)의 전통적인 관례인 전 선언 후 초기화를 하기로 되어 있습니다. 이런 전 선언 후 초기화는 전역변수의 무분별한 사용을 줄이고, 되도록이면 지역 변수를 최대한으로 사용할 수 있습니다. 이 부분은 후에 Scope 개념을 통해 짚고 나가기로 하겠습니다.

var number;
var name;
var rate = 1.2;
var greeting = "Hello, World!"
var flag = false;

JS에서 변수의 선언은 var 이라는 키워드를 통해 변수임을 명시하고, 더해서 배정문(assignment)을 사용할 수 있습니다.

산술 연산(Arithmetic)과 Math 라이브러리 함수

JS에서 기본으로 사용할 수 있는 표준 산술 연산은 다음과 같습니다. 이항연산자 이고, 두개의 변수나 숫자가 필요합니다.

  • X+Y : 더하기
  • X-Y : 빼기
  • X*Y : 곱셈
  • X/Y : 나눗셈
  • X%Y : 모듈러 연산 (일반적으로 나머지 연산이라고 한다.)

또, JS는 기본 산순연산자 외에, Math 라이브러리를 통해 삼각함수, 정수론, 로그 등의 심화 연산이 가능합니다.

고등학교 이과 수학 범위 내에서 이해가능한 함수들.

또는 Node.js 환경이라면, Math.js를 사용하면행렬 연산등 더 복잡한 연산을 할 수 있습니다.

조건문

익히 알다 시피, 조건문은 4가지가 존재합니다.

  • if(조건) {...}
  • if(조건) {...} else {...}
  • if(조건) {...} else if(조건) {...} else {...}
  • switch(변수명) { case 값1 : .... ; case ....}

그 중에서 switch문이 조건문 중에서 가장 고급의 추상화(highly Abstract)가 이루어졌다고 하는데, 이는 구문이 가장 깔끔하고 사람이 이해하기 쉽기 때문이다.

— 서울과학기술대학교 방혜자 교수님, “프로그래밍 언어론”

위의 조건문 중에서 반드시 어떤 것을 사용하라고 강제 하지는 않지만, 구현하려는 자료나 알고리즘에 따라 적절한 구문을 사용하는게 좋습니다. 보통 switch 를 쓰는 경우는 날짜, 월, 일, 키-값(key-value) 구조 일때 사용하는것이 좋습니다.

반복문

익히 알다시피, 반복문은 다음과 같은 구문으로 존재합니다.

  • while(반복이 수행 되는 조건){....}
  • do{처음에는 반드시 실행되는 구문}while(반복이 수행되는 조건);
  • for(반복에 사용되는 조건 변수 초기화; 반복 조건; 조건 변수 증감) {...}

for 반복문의 경우에는 Array의 인덱스를 통해 접근 할때, 매우 효과적입니다.

또한 반복이 수행되는 조건이 true 이거나 for(;;;) 인 경우, 구문 중간에 멈추는 부분(break point)가 없다면 무한 루프를 돌게 됩니다. 따라서 무한 루프는 주의해서 사용해야 합니다.

함수

JS에서는 두가지의 함수로 보통 나뉘어 집니다. 바로 ▲ 값을 반환하는 함수 ▲값을 반환하지 않는 함수 입니다. 값을 반환하지 않는 함수는 보통 subprocedure 또는 void function 이라고 부릅니다.

보통 컴파일 언어에서는 반환형을 다음과 같이 작성하는 경우가 많습니다.

//int 값을 반환하는 경우
int value_returning(int input){
//...
return output;
}
//반환하지 않는 경우
void subprocedure (int input) {
//...
}

하지만 JS에서는 반환형을 명시하지 않고, 보통 funtion 이라는 키워드만 사용합니다. 아래는 n! = n*(n-1)*(n-2)*….*1 을 구현한 예 입니다.

function factorial(n) {
var product = 1;
for(var i = n; i >= 1; --i)
product *= i;
return product;
}

JS는 또한 C 또는 C++에서 보통 사용하는 포인터를 사용할 필요 없이 Call-by-reference로 바로 매개변수를 전달 할 수 있습니다. 아래는 과목 4개 점수의 평균을 구하는 함수입니다.

var grade = [77,83,64,90];
function average(arr) {
var sum = 0;
for(var i = 0; i < arr.length; i++)
sum += arr[i];
return sum / arr.length;
}
console.log(average(grade)); //78.5

또한 반환도 Array나 객체 형태로 바로 가능합니다. 아래는 10의 약수 집합을구하는 프로그램 입니다.

var num = 10;
function divisor(n) {
var result = [];
for(var i = 1; i <= n; i++)
if(n % i == 0) result.push(i);
return result;
}
console.log(divisor(num)); //[1,2,5,10]

변수의 스코프(Scope)

스코프는 사전적으로 “영역”을 의미합니다. 보통 JS에서 스코프는 변수가 엑세스 할 수 있는 범위 를 의미합니다.

그리고 보통 이런 스코프의 개념은 함수에서 드러나기 때문에 함수 스코프(Function Scope) 라고 합니다. 함수 스코프의 경우에는 변수의 값은 함수 안에서 변수가 선언되고 정의 될 때, 그 안에서만 값이 보입니다.

또, 전역 스코프(Global Scope) 가 있습니다. 전역 스코프의 예시는 아래에서 확인해 볼 수 있습니다.

function showScope() {
return scope;
}
var scope = "global";
console.log(scope); // "global"
console.log(showScope()); // "global"

위의 예시에서 보면 첫번째 scope 변수는 함수 안에서 scope가 선언도 되지 않고, 사용되고 있지만 showScope() 의 값이 “global” 가 됩니다. 그 이유는 var scope = "global" 이 함수 바깥에서 선언되어 전역변수의 기능을 하기 때문입니다. 따라서 함수는 이 전역변수의 값을 출력합니다.

Global frame에서 함수와 변수가 정의된다.
전역 변수가 초기화 된다. 그후 전역 변수의 값이 출력된다.
showScope 함수가 실행된다.
반환 값이 자동적으로 전역변수를 따라간다.

프로그램 실행 방식을 한눈에 볼 수 있는 사이트를 이용하면 쉽게 이해할 수 있습니다.

참고 : http://www.pythontutor.com/visualize.html

위와 같이 함수 내에서 선언되지 않은 변수는 자동적으로 전역 스코프를 따라 갑니다. 만약에 함수 안에서 동일한 이름의 변수가 선언되면 어떻게 될까요.

function showScope() {
var scope = "local"
return scope;
}
var scope = "global";
console.log(scope); // "global"
console.log(showScope()); // "local"

이와 같은 경우 함수 안에서 선언된 변수의 경우 그 함수 안에서 만 유효한 것을 볼 수 있습니다.

함수 밖의 scope 변수와 함수 안의 scope 변수는 다르다.

하지만, 헷갈리게도 다음 예제에서는 또 다른 결과가 나옵니다.

function showScope() {
scope = "local"
return scope;
}
var scope = "global";
console.log(scope); // "global"
console.log(showScope()); // "local"
console.log(scope); // "local"

왜 갑자기 전역 변수 scope의 값이 “local”로 수정 되었을 까요? 위 코드의 차이점은 바로 showScope() 함수 안의 scope 변수 앞에 var 키워드가 빠진 건데요?

이는 아주 큰 맹점입니다. 왜냐하면, var 키워드는 선언과 동시에, 지역변수임을 알려주기 때문입니다. 이 키워드가 빠지면 바로 전역 변수 scope 를 가리키게 됩니다.

showScope 함수가 실행 된 후에…
전역변수가 수정됩니다.

위 코드에서 함수안에 var 키워드를 넣으면 다음과 같이 정상적으로 실행됩니다.

따라서, 이와 같은 혼동을 막기 위해서 var 키워드의 사용에 주의 해야 합니다. 또한 전역변수를 과도하게 사용하지 않아야 코드 작성시 오류를 예방할 수 있는 주요 전략이 되겠지요.

재귀함수(Recursion)

재귀 함수란 보통 함수 안에 똑같은 함수라고 생각하시면 됩니다. 보통 C보다 오래 전에 나온 프로그래밍 언어의 경우 이런 재귀함수를 지원하지 않는 경우가 많지만, JS에서는 비교적 젊은 언어라서 재귀함수를 사용할 수 있습니다.

예를 들면 경우의 수를 계산 할때 많이 사용하는 팩토리얼(!)을 수학적으로 정의하면 다음과 같습니다.

반복적인 구현

이를 JS로 구현하면 다음과 같습니다.

function factorial(n) {
var product = 1;
for(var i = n; i >= 1; --i)
product *= i;
return product;
}

이를 반복적인 구현(Iteration) 이라고 합니다. 하지만 위 수학식을 달리 표현하면 다음과 같습니다.

재귀함수

함수로 생각하면 다음은 위 표현과 동일 합니다.

f(n)은 n! 과 동일

이를 JS로 프로그래밍 하면 다음과 같습니다.

function factorial(n) {
if(n==0) return 1;
else return n * factorial(n-1);
}

반복적인 구현에 비해 매우 간결하고 단순 합니다. factorial(5)를 실행 하면 다음과 같이 실행됩니다.

재귀함수의 실행

정상적으로 실행하는것을 볼 수 있습니다.

이런 재귀함수는 매우 단순하고 편하게 사용할 수 있고, 어떤 경우에는 매우 효율적입니다. 예를 들면 단순한 거듭제곱을 구현한다고 해봅시다.

이와 같이 처음에는 반복적인 구현, 그리고 두번째는 재귀적인 구현을 하였습니다. 반복적인 구현의 경우에는 for문을 사용하겠지요.

반복 적인 구현의 경우 한 구문이 r 번 실행 됩니다.

재귀를 사용하는 경우

하지만 재귀함수를 사용하는 경우, 같은 함수는 겨우 log r 번만 수행 됩니다. 하나의 구문을 실행하는데 일정한 시간이 소요된다고 하면, r이 매우 큰 값이라면, 굉장히 큰 시간 차이입니다.

n 과 log n 의 현격한 차이…

이렇게, 재귀함수가 효율적일 때가 있습니다. 하지만, 반대로 재귀함수를 사용하면 오히려 비효율적인 때가 있습니다. 예를 들면 피보나치 수열의 경우 입니다. 따라서, 재귀함수를 사용할때는 잘 생각해봐야 할겁니다.

객체와 객체지향 프로그래밍(Object-Oriented Programming)

이 책에서 자료구조를 논할 때에는 보통 객체를 사용해서 논할 겁니다. 객체에 대한 설명은 이곳을 참고하십시오.

JS는 객체를 만드는데 많은 방법이 있습니다. 이 책에서는 대표적으로 생성자를 통해서 객체를 만드는 방법을 소개하고 있습니다.

function Checking(amount) {   
this.balance = amount; // 속성
this.deposit = deposit; // 메소드(함수)
this.withdraw = withdraw; // 메소드(함수)
this.toString = toString; // 메소드(함수)
}

여기서 Checking (예금 계좌) 이라는 객체를 만들었고, 생성자를 통해서 this 키워드를 통해 balance 라는 객체의 속성에 값을 할당 받을 수 있습니다. 그리고 Checking 계좌에는 deposit() , withdraw() , toString() 이라는 메소드를 통해 계좌를 관리하거나, 계좌 정보를 출력할 수 있습니다.

각각의 메소드의 경우 생성자 바깥에서 다시 구현 하면 됩니다.

function deposit(amount) { //입금
this.balance += amount;
}
function withdraw(amount) { //출금
if (amount <= this.balance) { //잔액 여유 시
this.balance -= amount;
}
if (amount > this.balance) { //잔액 부족 시
print("잔액 부족");
}
}
function toString() { //잔고 표시
return "잔고: " + this.balance;
}

이와 같이 메소드를 선언하면 Checking 이라는 객체에 this라는 키워드를 통해 속성에 접근 할 수 있습니다.

실제 사용시에는 다음과 같이 사용합니다.

var account = new Checking(500); 
account.deposit(1000);
console.log(account.toString()); //"잔고: 1500"
account.withdraw(750);
print(account.toString()); // "잔고: 750"
account.withdraw(800); // "잔액 부족"
print(account.toString()); // 잔고: 750

즉, new 키워드를 통해 객체의 인스턴스를 생성 할 수 있습니다. 인스턴스는 흔히 붕어빵 틀에서 구운 붕어빵 같은 거라고 이해하시면 될겁니다.

마침

앞으로 자료구조와 알고리즘에 사용할 JS 언어에 대한 설명은 여기 까지 입니다. 이외에도 다양한 JS의 언어적인 특성이 있지만, 이 책에서는 상당부분 생략되어 있습니다.

언어적인 부분에서 모르는 부분이 있으면 그때마다 레퍼런스를 보면서 이해하는게 좋을것 같습니다.

2018.5.22.

--

--

XJB

웹 개발자가 되려는 대학생 입니다.