ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Stack / Queue / Recursive function
    Algorithm | Data structure/Theory 2023. 4. 11. 02:04

    Stack / Queue / Recursive function

    탐색(Search)이란 많은 양의 데이터 중에서 원하는 데이터를 찾는 과정을 말한다.

    대표적인 그래프 탐색 알고리즘으로는 DFS와 BFS가 있다.

    DFS/BFS는 코딩 테스트에서 매우 자주 등장하는 유형!!

    DFS : https://growth-msleeffice.tistory.com/137

    BFS : 준비중

    DFS/BFS 알고리즘을 학습하기 전 반드시 알아야 할 개념으로 스택, 큐 자료구조와 재귀함수가 있다.


    스택

    먼저 들어온 데이터가 나중에 나가는 형식(선입후출)의 자료구조

    데이터를 집어 넣는 push, 데이터를 추출하는 pop, 가장 나중에 삽입된 데이터를 확인하는 peek등의 작업을 할 수 있다.

    함수 실행 콘텍스트들이 쌓이는 Call stack과 브라우저의 방문 기록이 쌓이는 History stack이 대표적이다.

    스택 자료구조를 class로 구현하는 방법에 대한 자세한 내용은 이 글에서 참고할 수 있다.


    class Stack {
      constructor() {
        this.arr = [];
      }
    
      push(item) {
        this.arr.push(item);
      }
    
      pop() {
        return this.arr.pop();
      }
    
      peek() {
        if (this.size() === 0) null;
        return this.arr[this.arr.length - 1];
      }
    
      size() {
        return this.arr.length;
      }
    
      isEmpty() {
        return this.size() === 0;
      }
    }
    
    const stack = new Stack();
    stack.push(1);
    stack.push(2);
    stack.push(3);
    stack.push(4);
    
    let lastPush = stack.peek();
    console.log('lastPush: ', lastPush); // 4
    
    const extract = stack.pop();
    console.log('extract: ', extract); // 4
    
    lastPush = stack.peek();
    console.log('lastPush: ', lastPush); // 3
    
    const size = stack.size();
    console.log('size: ', size);
    
    // 후입선출
    while (!stack.isEmpty()) {
      console.log(stack.peek()); // 3, 2, 1
      stack.pop();
    }


    먼저 들어온 데이터가 먼저 나가는 형식(선입선출)의 자료구조

    데이터를 집어 넣는 enqueue, 데이터를 추출하는 dequeue등의 작업을 할 수 있다.

    큐는 순서대로 처리해야 하는 작업을 임시로 저장해두는 버퍼(buffer)로서 많이 사용된다.

    큐의 경우 아래의 자바스크립트 배열을 사용하면 데이터의 삽입/삭제 시 인덱싱을 새로하기 때문에 데이터의 양이 많거나 시간 복잡도를 고려해야 할 경우 class로 큐 자료구조를 구현하는 것이 효과적이다.

    큐 자료구조를 class로 구현하는 방법에 대한 자세한 내용은 이 글에서 참고할 수 있다.


    class Queue {
      constructor() {
        this.arr = [];
      }
    
      enqueue(item) {
        this.arr.push(item);
      }
    
      dequeue() {
        return this.arr.shift();
      }
    
      peek() {
        return this.arr[0];
      }
    
      size() {
        return this.arr.length;
      }
    
      isEmpty() {
        return this.size() === 0;
      }
    }
    
    const queue = new Queue();
    
    queue.enqueue(1);
    queue.enqueue(2);
    queue.enqueue(3);
    queue.enqueue(4);
    queue.enqueue(5);
    const firstPush = queue.dequeue();
    console.log('firstPush: ', firstPush); // 1
    
    const size = queue.size();
    console.log('size: ', size);
    
    while (!queue.isEmpty()) {
      console.log(queue.peek()); // 2, 3, 4, 5
      queue.dequeue();
    }


    재귀 함수

    재귀 함수(Recursive Function)란 자기 자신을 다시 호출하는 함수를 의미한다.


    단순한 형태의 재귀함수 예제

    function recursiveFunction1() {
      console.log('재귀 함수를 호출합니다.');
      recursiveFunction1();
    }
    
    recursiveFunction1(); // RangeError: Maximum call stack size exceeded


    '재귀 함수를 호출합니다'라는 문자열을 무한히 출력하는 로직이다.

    이 로직을 실행하면 콘솔이 어느 정도 출력되다가 최대 콜 스택 사이즈가 초과되었다는 에러가 출력된다.

    재귀 함수를 문제 풀이에서 사용할 때는 재귀 함수의 종료조건을 반드시 명시해야 한다.

    종료 조건을 제대로 명시하지 않으면 함수가 무한히 호출될 수 있다.


    종료 조건을 포함한 재귀함수 예제

    function recursiveFunction2(counts) {
      // 5번째 호출을 했을 때 종료되도록 종료 조건 명시
      if (counts === 5) return;
      console.log(
        counts,
        '번째 재귀 함수에서',
        counts + 1,
        '번째 재귀함수를 호출합니다.',
      );
      recursiveFunction2(counts + 1);
      console.log(counts + '번째 재귀함수를 종료합니다.');
    }
    
    recursiveFunction2(1);
    // 1 번째 재귀 함수에서 2 번째 재귀함수를 호출합니다.
    // 2 번째 재귀 함수에서 3 번째 재귀함수를 호출합니다.
    // 3 번째 재귀 함수에서 4 번째 재귀함수를 호출합니다.
    // 4 번째 재귀 함수에서 5 번째 재귀함수를 호출합니다.
    // 4번째 재귀함수를 종료합니다.
    // 3번째 재귀함수를 종료합니다.
    // 2번째 재귀함수를 종료합니다.
    // 1번째 재귀함수를 종료합니다.

    재귀함수 예제 1: Factorial

    수학적으로 0!, 1!의 값은 1이다.


    function factorialIterative(n) {
      let result = 1;
      // 1부터 n까지의 수를 차례대로 곱하기
      for (let i = 1; i <= n; i++) {
        result *= i;
      }
      return result;
    }
    
    function factorialRecursive(n) {
      if (n <= 1) return 1;
      return n * factorialRecursive(n - 1);
    }
    
    console.log('반복문으로 구현 : ' + factorialRecursive(5)); // 120
    console.log('재귀함수로 구현 : ' + factorialIterative(5)); // 120


    재귀함수 예제 2: 최대공약수 계산 (유클리드 호제법)

    두 개의 자연수에 대한 최대공약수를 구하는 대표적인 알고리즘으로는 유클리드 호제법이 있다.


    유클리드 호제법

    두 자연수 A,B에 대하여 (A>B) A를 B로 나눈 나머지를 R이라고 하자.

    이때 A와 B의 최대공약수는 B와 R의 최대공약수와 같다.

    유클리드 호제법의 아이디어를 그대로 재귀 함수로 작성할 수 있다.

    예시 : GCD(192,162)

    1단계 : 192%162 = 30

    2단계 : 162%30 = 12

    3단계 : 30%12 = 6

    4단계 : 12%6 = 0

    최대 공약수 : 6


    function gcd(num1, num2) {
      const remain = num1 % num2;
      if (remain === 0) return num2;
    
      return gcd(num2, remain);
    }
    
    console.log(gcd(192, 162)); // 6


    재귀 함수 사용 시 유의 사항

    ✅ 재귀 함수를 잘 활용하면 복잡한 알고리즘을 간결하게 작성할 수 있다.

    단, 오히려 다른 사람이 이해하기 어려윤 형태의 코드가 될 수도 있으므로 신중하게 사용해야 한다.

    ✅ 모든 재귀 함수는 반복문을 이용하여 동일한 기능을 구현할 수 있다.

    ✅ 재귀 함수가 반복문 보다 유리한 경우도 있고 불리한 경우도 있다.

    ✅ 컴퓨터가 함수를 연속적으로 호출하면 컴퓨터 메모리 내부의 스택 프레임에 쌓인다.


    출처 : (이코테 2021 강의 몰아보기) 3. DFS/BFS

    'Algorithm | Data structure > Theory' 카테고리의 다른 글

    Stack  (0) 2023.04.26
    DFS(Depth-First Search)  (0) 2023.04.11
    구현 : 시뮬레이션과 완전 탐색  (0) 2023.04.07
    Greedy  (0) 2023.04.05
    [자료구조] 자료구조를 배우는 이유  (0) 2023.01.19

    댓글