기억의 실마리
2023. 6. 28. 21:07

1. DFS, 깊이 우선 탐색 (Deep-First-Search)

깊이 우선 탐색은그래프나 트리 자료구조에서 모든 노드를 한번씩 탐색할때 쓰이는 기본적인 방법이다.

완전탐색을 할 때 가장 간단한 방법 중 한가지다.

스택(Stack) 자료구조를 사용하는 것이 일반적이다.

 

DFS 기본 동작 방식

  1. 시작 노드를 스택에 넣은 후 방문처리 한다.
  2. 스택에 마지막으로 들어온 노드에 방문하지 않은 인접노드의 존재 여부를 확인한다.
    방문하지 않은 노드가 존재한다면 노드를 스택에 넣고 방문처리한다.
    없다면 현재의 노드 즉, 스택에 마지막으로 들어온 노드를 스택에서 추출한다.
  3. 2번 과정을 반복할 수 없을때까지 반복해준다.

 

DFS 구현 특징

  • 스택 사용을 사용하거나 재귀함수를 사용한다.
  • 재귀함수는 스택과 동작원리가 동일하기 때문에 구현의 편리성이 존재한다.
  • 완전 탐색을 목적으로 하는 경우에는 BFS보다 느릴 수 있기 때문에 적재적소에 사용하는 것이 좋다.
  • 탐색속도가 느리더라도 구현의 편리함 때문에 BFS대신 DFS를 사용하는 경우도 적지않다.

 

DFS를 사용하는 경우는?

  1. 간결한 코드로 구현해야 할 떄
  2. 큐 라이브러리를 사용할 수 없을 때
  3. 트리에서 최단 거리를 탄색해야 할 때
    -> 트리에서는 두 노드를 잇는 경로가 하나만 존재하는 경우만

 

 

2. BFS, 너비 우선 탐색 (Breadth-First-Search)

깊이 우선 탐색과 마찬가지로 그래프나 트리에서 모든 노드를 한번씩 탐색할때 쓰이는 알고리즘이다

모든 간선의 길이가 동일하게 주어졌을때 최단거리를 탐색하기 위한 목적으로 사용할 수 있으며

큐(Queue) 자료구조를 사용한다.

 

BFS 기본 동작 방식

  1. 시작 노드를 큐에넣고 방문처리를 해준다.
  2. 큐에서 원소를 dequeue(삭제/꺼내기) 해주어 방문하지 않은 인접한 노드를 확인한다.
    방문하지 않은 인접노드가 있다면 해당 노드를 큐에 모두 삽입하며 방문처리한다.
  3. 2번 과정을 반복할 수 없을때까지 반복해준다.

 

BFS를 사용하는 경우는?

  1. 간선의 비용이 모두 동일하게 주어졌을때 최단거리 문제를 해결할 때
  2. 완전 탐색을 DFS로 진행했을때 메모리 혹은 시간초과되는 경우

 

2023. 6. 28. 16:26

백트래킹 (Back-Tracking)

백트래킹은 해를 찾는 도중 해가 아닌 경우 다시 되돌아가 다시 해를 탐색하는 알고리즘이다.

일반적으로 그래프나 트리의 모든 원소를 완전탐색하기 위해서 많이 사용된다.

백트래킹은 재귀함수를 이용해 구현할 수 있다. 정석적인 완전 탐색과는 다르게 조건에 따라서

더 유망한 노드로 이동한다.

 

백트래킹 알고리즘을 이용하여 풀 수 있는 문제

백트래킹을 이용한 코딩테스트 문제중에서 가장 유명한 N퀸 문제이다.

탐색을 진행하면서 유망한 경우에 대해서만 탐색을 진행해야 한다.

예를 들면 미리 table을 만들어두고 탐색을 진행한 후 문제의 조건대로 경로가 겹쳐

탐색할 필요가 없는 table의 index번째를 false에서 true로 바꾸어 미리 건너뛰어 줄 index를

지정한 후 재귀탐색을 하는 방식으로 풀이할 수 있다.

 

백준 코딩테스트 페이지: https://www.acmicpc.net/problem/9663

 

9663번: N-Queen

N-Queen 문제는 크기가 N × N인 체스판 위에 퀸 N개를 서로 공격할 수 없게 놓는 문제이다. N이 주어졌을 때, 퀸을 놓는 방법의 수를 구하는 프로그램을 작성하시오.

www.acmicpc.net

 

마치며...

공부하면서 이해하기 가장 어려웠던 방식의 알고리즘이었다. 특히 재귀적으로 처리한다는 방식 자체가 처음 접했을때 상당히 혼란스러웠다. 이해하기 어려운 만큼 하나씩 직접 그림을 그리면서 적용해보면서 이해할 필요가 있었다. 초반에 로직에 대한 이해 때문에 내가 애용하는 Google Keep에 하나씩 적으며 풀이해둔 것을 저장해 두었다. 혹시 시간이 지나고 재귀적인 처리작업을 불가피하게 해야 할 때 포스팅과 Google Keep을 다시 보며 복습해야겠다...

2023. 6. 28. 15:53

이진탐색 (Binary Search)

정렬이 되어있는 리스트에서 탐색 범위를 반씩 좁혀가며 데이터를 탐색하는 것이다.

각 단계마다 탐색 범위를 2로 나누어 탐색하기 때문에 시간복잡도는 O(log N)이다.

때문에 이진탐색은 매우 넓은 탐색범위(억 단위 이상)에서 최적의 해를 찾아야 하는 경우

데이터를 정려한 후 다수의 쿼리를 날려야 하는 경우에 많이 사용할 수 있다.

 

이진탐색 동작 방식

아래 리스트 표에서 값이 15인 위치를 찾고자 한다고 가정해보자.

0 5 6 7 8 9 15 16 19 20
  1. 정렬이 이미 되어 있다는 것을 가정하고 이를 통해서 [start, mid, end] 3포인트를 지정한다.
    mid는 배열의 크기를 2로 나누었을때 소수점을 버린 값이다. (10 = 5, 9 = 4)
    {start: 0, mid: 8, end: 20}

  2. 이진탐색의 방식은 결과적으로 mid가 탐색값이 되면 탐색을 멈추고 mid를 반환하는 방식이다.
    탐색값과 mid를 비교해서 탐색값이 더 크다면 start = mid를 해주어 탐색 범위를 좁히고
    변경된 start를 기준으로 mid = parseInt((start + end) / 2) 를 작성해서 mid위치를 재지정해준다.
    반대로 탐색값이 더 작다면 end = mid, mid = parseInt((start + end) / 2)를 작성하여
    end위치와 mid위치를 재지정해주면 된다.

 

마치며...

이진 탐색코드 같은 경우엔 코딩테스트에서도 분명 이점이 있지만 프로젝트를 진행할 때 특정 값을 정렬된 데이터들 내에서 탐색하는 방법을 사용할때 많이 이용하게 될 것 같다.

2023. 6. 27. 15:15

탐욕알고리즘 (Greedy Algorithm)

현재 상황에서 가장 좋아보이는 상황만을 선택하는 알고리즘이다.

흔히 그리디, 탐욕알고리즘이라고 부리기도 하며 최적의 해를 구하기위한

방법으로 사용될 때가 많다.

 

탐욕 알고리즘의 접근방법

1. 현재 상황에서 어떤 것을 선택할지 방법을 고안한다.

2. 자신이 고안한 알고리즘이 항상 최적의 해를 보장하는지 정당성을 확인해야한다.

 

탐욕 알고리즘 문제

전형적인 탐욕 알고리즘을 이용해 풀 수 있느 문제로 "거스름 돈" 문제가 있다.

거스름 돈 문제를 통해서 그리디알고리즘에 대해서 이해해보자.

문제는 아래와 같은 조건과 답을 내어야 한다.

  • 카운터에 500원 100원 50원 10원이 무수히 많다고 가정한다.
  • 손님에게 11,480원을 거슬러 주어야 할때, 동전 개수의 최소값은?

결론부터 말하자면 이 문제의 해결방법은 가장 큰 화폐단위부터 거슬러 주는 것이다.

즉 500원부터 10원까지 거슬러 줄 수 있는 만큼 거술러주면 되는 것이다.

화폐단위 500원 100원 50원 10원
현재 금액 11,480 480 80 30
남은 금액 480 80 30 0
동전 개수 22 22 + 4 22 + 4 + 1 22 + 4 + 1 + 3

동전 개수의 최소값은 30이다.

 

큰 화폐부터동전의 개수를 구하며 내려왔을때 최적의 해를 구할 수 있는 이유는

각 화폐단위가 배수 관계에 속하기 때문에 그렇다.

 

예를 들어 640원을 거슬러 주어야한다고 하고 220원, 200원, 10원 동전이 있다고 가정하면

220원 2개, 200원 1개로 총 3개가 필요할 것이다. 이 또한 배수관계에 속하기 때문에

가장 큰 수의 동전부터 나눌 수 있는 만큼 나누고 점차 작은 수의 동전으로 나누어주는

같은 방식의 탐욕알고리즘을 사용하여 문제를 해결할 수 있다.

 

마치며...

탐욕알고리즘에 대해서는 간단하게 포스팅을 해보았다. 실질적으로 탐욕알고리즘은 코딩테스트를 볼 때 중요했던 개념이었다. 나는 탐욕알고리즘을 파악할때 예시로 주어지는 입력값과 출력값(정답)을 보며 연관성을 최대한 찾아보고 답을 만들기 위한 코드설계와의 연관성과 규칙성이 있는지를 확인하면서 코드를 작성할때 어떠한 연관성이 있는지를 파악하면서 실마리를 찾아냈던 것 같다. 개인적으로 익숙해지기 어려운 접근방식이었던 기억이 있다...

2023. 6. 26. 20:48

1. 선택정렬(Selection Sort)

단계별로 정렬을 수행할때 가장 작은 원소를 선택해서 앞으로 보내는 정렬 방법이다.

앞으로 보내진 원소는 위치가 변경되지 않고 시간복잡도 O(N²)으로 비효율적인 알고리즘에 속한다.

 

동작방식

1. 단계별로 수행하면서 가장 작은 원소를 선택한다.

2. 처리되지 않은 원소중에 가장 앞의 원소와 교체한다.

 

  • 선택정렬 함수 예시
function selectionSort(arr) {
  for (let i = 0; i < arr.length; i++) {
    let minIndex = i;
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[minIndex] > arr[j]) {
        minIndex = j;
      }
    }
    // 스왑(swap)
    let temp = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
  }
}

 

 

2. 버블정렬(Bubble Sort)

인접한 두 원소를 체크하여 정렬이 안되어 있다면 위치를 서로 변경하는 정렬방식이다.

서로 인접한 두 원소를 비교하는 것이 마치 거품과 같다고 하여 붙여진 이름이다.

시간복잡도 O(N²)으로 비효율적인 정렬 알고리즘에 속한다.

 

동작방식

1. 단계별로 수행하면서 가장 큰 원소가 맨 뒤로 이동한다.

2. 다음단계에선 맨 뒤로 이동한 원소는 정렬에서 제외한다.

 

  • 버블정렬 함수 예시
function bubbleSort(arr) {
  for (let i = arr.length - 1; i > 0; i--) {
    for (let j = 0; j < i; j++) {
      if (arr[j] < arr[j + 1]) {
        let temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
      }
    }
  }
}

 

 

3. 삽입정렬(Insertion Sort)

각 원소를 적절한 위치에 삽입하는 정렬이다.

 

동작방식

1. 단계별로 수행하면서 현재 원소가 삽입될 위치를 탐색한다.

2. 지정 위치에 도달할 때까지 반복적으로 왼쪽으로 이동한다.

 

  • 삽입정렬 함수 예시
function insertionSort(arr) {
  for (let i = 1; i < arr.length; i++) {
    for (let j = i; j > 0; j--) {
      // 시작인덱스부터 1까지 1씩 감소하며 반복
      if (arr[j] < arr[j - 1]) {
        let temp = arr[j];
        arr[j] = arr[j - 1]; // 한 칸씩 왼쪽으로 이동
        arr[j - 1] = temp; // 스왑(swap)
      } else {
        break; // 자기보다 작은 데이터를 만나면 그 위치에서 멈춤
      }
    }
  }
}

 

 

4. 병합정렬(Merge Sort)

병합정렬은 분할정복(divide and conquer) 알고리즘이다.

 

분할정복이란?

  • 분할(divide): 큰 문제를 작은 부분 문제(쉬운 문제)로 분할한다.
  • 정복(conquer): 작은 부분 문제를 각각해결하도록 한다.
  • 조합(combine): 해결한 작은 부분의 답을 이용하여 큰 문제를 해결해나간다.

일반적으로 재귀함수를 사용하여 구현한다.

큰 문제를 작은 문제로 분할하는 방식이 같은 경우가 많기 때문이다.

그렇기 때문에 쪼개질 수 없는 크기가 될 때까지 계속해서 분할한다.

 

장점: 최악의 경우에도 O(N log N)을 보장할 수 있다는 점에서 효율적이다.

단점: 전반적으로 정복과정에서 임시배열이 필요한 경우가 지배적이다.

 

동작방식

1. 단계별로 수행하면서 현재 원소가 삽입될 위치를 탐색한다.

2. 지정 위치에 도달할 때까지 반복적으로 왼쪽으로 이동한다.

 

  • 병합정렬 함수 예시
function merge(left, right) {
  let sorted = [];
  while (left.length && right.length) {
    if (left[0] <= right[0]) sorted.push(left.shift());
    else sorted.push(right.shift());
  }
  return [...sorted, ...left, ...right];
}

function mergeSort(arr) {
  if (arr.length === 1) return arr;
  let half = Math.ceil(arr.length / 2);
  let left = arr.slice(0, half);
  let right = arr.slice(half);
  return merge(mergeSort(left), mergeSort(right));
}

let arr = [7, 4, 3, 2, 1, 6, 5];
const sortedArray = mergeSort(arr);

console.log(arr); //[7, 4, 3, 2, 1, 6, 5]
console.log(sortedArray); //[1, 2, 3, 4, 5, 6, 7]

function merge: 이미정렬된 배열 left와 right를 하나의 함수로 합쳐주는 순수 함수

function mergeSort: 배열을 반으로 나누어 merge함수에게 left와 right로 전달한다.

 

 

마치며...

오늘 포스팅한 정렬알고리즘은 사실, 알고리즘을 따로 공부를하기전까진 단순하게 정렬하는거지! 라고 생각했지만 정말 다양하고 어떤방식으로 정렬을 하느냐에 따라 시간복잡도가 제곱이상의 차이가 나기도 한다는 것이 새롭게 다가왔던 것 같다. 앞으로도 코드의 퀄리티를 위해서 프로젝트에서 정렬할때 잘 생각해보고 적용시켜야겠다.

2023. 6. 26. 13:44

1. 배열(Array)

가장 기본적인 자료구조이며 여러개의 변수를 담는 공간으로 이해할 수 있다.

배열은 인덱스(Index)가 존재하고 인덱스는 0부터 시작한다.

특정한 인덱스에 직접적으로 접근이 가능하고 수행시간은 O(1)을 가진다.

 

장점: 캐시(cache) 히트 적중률이 높고 조회가 빠르다.

단점: 배열의 크기를 미리 지정해야 하는 것이 일반적이며 데이터의 추가 및 삭제에 한계가 있다.

 

캐시히트 적중률이란?

CPU(클라이언트)에서 요청한 데이터가 캐시에 존재하여 이를 이행할 수 있을때 캐시 히트(Hit)라고 한다. 

Memory address 0x0000 0x0004 0x0008
Index 0 1 2
Value 10 30 70

 

2. 연결리스트(Linked List)

연결 리스트는 메인 메모리상에서 주소가 연속적이지 않으며 배열과 다르게

크기가 정해져있지 않고 동적으로 변경이 가능한 것이 특징이다.

 

장점: 포인터가 다음 데이터의 위치를 가지고 있기 때문에 삽입과 삭제가 간편하다.

단점: 특정 인덱스의 원소를 검색할때 앞에서부터 원소를 찾기 때문에 검색속도가 느리다.

3. 스택(Stack)

먼저 들어온 데이터가 나중에 추출되는 자료구조이며 박스가 쌓인 형태를 스택이라고 볼 수 있다.

새로운 원소를 삽입할때는 마지막위치에 삽입하고 삭제할때 마찬가지로 마지막 위치에서 삭제된다.

 

 

4. 큐(Queue)

큐는 먼저 삽입된 데이터가 먼저 추출되는 자료구조이다.

예시로 게임 대기 큐의 경우 먼저 대기한 사람이 먼저 게임에 매칭되는 것과 동일하다.

Javascript Queue

Javascript에서는 큐 라이브러리를 기본적으로 제공하고있지 않기 때문에

간단하게 Queue 라이브러리를 직접 작성하여 사용할 수 있다.

 

필자 역시 Javascript를 주로 사용하기 때문에 Queue라이브러리를 만들어 두었다.

class Queue {
  constructor() {
    this.items = {};
    this.headId = null;
    this.tailId = null;
    this.length = 0;
  }
  size() { return this.length; }
  isEmpty() { return this.length === 0; }
  enq(item) {
    if (!this.headId) this.headId = 0;
    if (this.headId == 0) {
      this.items[this.length] = item;
      this.length++;
      this.tailId = this.length - 1;
    }
    else  {
      this.tailId++;
      this.items[this.tailId] = item;
      this.length++;
    }
  }
  deq() {
    if (this.isEmpty()) throw new Error('Queue is empty');
    if (this.length == 0) return
    this.length--;
    const item = this.items[this.headId];
    delete this.items[this.headId];
    this.headId++;
    if (this.length == 1) this.tailId = this.headId;
    if (this.length == 0) {
      this.headId = null;
      this.tailId = null;
    }
    return item;
  }
  head() {
    if (this.isEmpty()) throw new Error('Queue is empty');
    return this.items[this.headId];
  }
  tail() {
    if (this.isEmpty()) throw new Error('Queue is empty');
    return this.items[this.tailId - 1];
  }
}

혹시나 블로그를 방문하시는분들께서 javasciprt Queue가 필요하셨다면 사용하셔도 무방합니다.

 

5. 트리(Tree)

트리는 계층적인 구조를 표현할 때 사용할 수 있는 자료구조다.

나무를 뒤집은 것 같은 형태이다.

  • 루트 노드(root node): 부모가 없는 최상위 노드
  • 단말 노드(leaf node): 자식이 없는 노드

서로 같은 부모를 가지는 노드간의 관계를 형제관계라고 하며 루트노드에서의 길이를

깊이(depth)라고 한다. 이는 즉 출발노드에서 목적지 노드까지 거쳐야 하는 간선의 수를 의미한다.

트리의 높이(height)는 루트 노드에서 가장 깊은 노드까지의 길이를 의미한다.

 

트리의 종류

  • 이진 트리(Binary Tree): 최대 2개의 자식을 가질 수 있는 구조의 트리를 말한다.

 

  • 포화 이진 트리(Full Binary Tree): 리프노드를 제외한 모든 노드가 두 자식을 가지고 있는 트리다.

 

  • 완전 이진 트리(Complete Binary Tree): 모든 노드가 왼쪽 자식부터 차례로 채워진 트리다.

 

  • 높이 균형 트리(Height Balanced Tree): 왼쪽과 오른쪽 자식 트리의 높이가 1이상 차이가 나지 않는 트리다.

 

6. 힙(Heap)

힙은 원소중 최댓값 또는 최솟값을 빠르게 찾아내는 자료구조이다.

원소 삽입과 삭제할때 O(logN)의 수행시간을 요구하며 N개의 데이터를 힙에 넣었다

모두 꺼내는 작업은 정렬과 동일하여 수행시간 O(N log N)을 요구한다.

 

힙은 완전 이진트리 자료구조를 따르며 우선순위가 가장 높은 노드가 루트에 위치한다.

 

1. 최대 힙(max heap)

  • 부모 노드의 키 값이 자식 노드의 키 값보다 항상 크다.
  • 루트 노드가 가장 크며, 값이 큰 데이터가 우선순위를 가진다.

2. 최소 힙(min heap)

  • 부모 노드의 키 값이 자식 노드의 키 값보다 항상 작다.
  • 루트 노드가 가장 작으며, 값이 작은 데이터가 우선순위를 가진다.

 

7. 우선순위 큐(Priority Queue)

우선순위에 따라서 데이터를 추출할 수 있는 자료구조다.

우선순위 큐는 일반적으로 힙(heap)을 이용해 구현하며 배열 자료형으로도 구현가능하다.

우선선순위 큐 구현방식 삽입 시간 삭제 시간
배열 자료형 O(1) O(N)
힙(Heap) O(logN) O(logN)

일반적으로 우선순위 큐는 이진트리 구조를 사용한다.

 

javascript는 기본적으로 우선순위 큐 라이브러리를 제공하지 않기 때문에 필요한 경우

별도의 라이브러리를 사용해야 한다.

 

참고할 수 있는 깃허브: https://github.com/ndb796/priorityqueuejs

 

8. 그래프(Graph)

그래프는 특정 노드의 정점과 노드와 노드의 간선을 나타내기 위한 자료구조이다.

그래프는 두가지 방식으로 구현할 수 있다.

 

  • 인접행렬: 2차원 배열을 사용하는 방식

각 인덱스에 각 노드가 자리해 있으며 위 이미지의 표를 배열로 표현한다면 아래와 같다.

[0, 3, 10]

[3, 0, infinity]

[10, infinity, 0]

 

각 행은 각 노드의 인덱스번호부터 출발했을때 얻을 수 있는 비용이다.

즉 0, 1, 2 의 인덱스를 가지고 있기 때문에 행에 3의 길이여야 하고

각 인덱스에서 출발한다고 가정하면 1행에서 인덱스 0번자리는 자기 자신이기 때문에

비용이 발생하지 않아 0이다. 다음 인덱스 1번자리는  0번노드 -> 1번노드로 이동하는

비용이 3이다. 그리고 0번노드 -> 2번노드는 비용이 10이기 때문에 결과적으로

1번째 행은 [0. 3, 10]이 되는 것이다. 나머지 값들도 모두 동일하게 적용되며

간선이 없어 갈 수 없는 경우는 무한(infinity)으로 표기한다.

 

  • 인접리스트: 연결 리스트를 이용하는 방식

인접 리스트의 경우 노드의 각 인덱스를 각 열로 나열하고 연결가능한 노드와 간선비용을 저장한다.

0번노드의 경우 1번노드와 2번노드를 연결 할 수 있고 1번노드와 연결하면 비용은 3,

2번노드와 연결하면 비용은 10이기 때문에 최상단 행이(index 0번 노드) [1, 3], [2, 10]를 가지게 된다.

나머지도 마찬가지로 연결이 불가능한 경우는 모두 제외하고 연결이 되는 경우만

연결되는 노드번호와 간선의 비용만 저장한다.

 

그래프의 시간 복잡도

  • 인접행렬: 모든 정점들의 연결 여부를 저장해 O(V²)의 공간을 요구한다.
    공간효율성이 좋지 않지만 두 노드의 연결 여부를 O(1)의 시간만을 요구한다.

  • 인접리스트: 연결된 간선의 정보만을 저장하여 O(V + E)의 공간을 요구한다.
    공간효율성이 좋은편이지만 두 노드의 연결여부를 확인하는데 O(V)의 시간이 요구된다.

 

마치며...

지난 알고리즘 학습을 복습하면서 포스팅을 진행하고 있는데 초기에 학습했던 내용들이 분명 학습당시엔 명료했던 이해도가 시간이 지나니 흐리게 남아있는 경우가 많은 것 같다. 정말 기록하는 것은 중요하고 어떠한 키워드를 가지고 생각하며 기록해야하는지를 다시한번 하게 되었다.

2023. 6. 25. 16:46

알고리즘 이란?

어떠한 문제를 해결하기 위한 절차나 방법을 뜻한다.

공식화한 형태로 표현한 것을 의미하며 유한성을 가지고 언젠가는 끝나야 하는 속성을 가지고 있다.

 

알고리즘의 조건

알고리즘은 아래 5가지 조건에 만족해야 한다.

  • 입력: 외부에서 제공되는 자료가 0개 이상 존재해야 한다.
  • 출력: 적어도 2개 이상의 서로 다른 결과를 도출해내야 한다.
  • 명확성: 수행과정은 무엇을 하기위한 것인지 정의가 명확해야 한다.
  • 유한성: 알고리즘의 명령어대로 수행하여 처리가 되었을때 종료되어야 한다.
  • 효율성: 모든 과정이 명백히 실행가능해야 하며 시간적, 공간적 효율성을 가져야 한다.

 

1. 선형 자료구조

하나의 데이터 뒤에 다른 데이터가 하나 존재하는 자료구조이다.
데이터가 일렬로 연속적으로(순차적) 연결되어있다.
배열, 연결리스트, 스택, 큐, 덱 등이 있다.

 

2. 비선형 자료구조

하나의 데이터뒤에 다른 데이터가 여러개 올 수 있는 자료구조이다.
데이터가 일직선상으로 연결되어있지 않아도 되며 트리, 그래프 같은 것이 있다.

 

자료구조와 알고리즘의 관계

효율적인 자료구조 설계를 위해 알고리즘 지식이 요구된다.
효율적인 알고리즘을 작성하기 위해서는 문제상황에 맞는 적절한 자료구조가 사용되어야 하며
프로그램을 작성할 때 자료구조와 알고리즘 모두 고려해야 한다.

 

프로그램의 성능 측정 방법

  • 시간복잡도(time complexity): 알고리즘에 사용되는 연산 횟수를 측정한다.
  • 공간복잡도(space complexity): 알고리즘에 사용되는 메모리의 양을 측정한다.

주로 공간을 많이 사용하는 대신 시간을 단축하는 방법을 선택한다.

 

Big-O 표기법

복잡도를 표현할 때는 Big-O 표기법을 사용한다.

특정한 알고리즘이 얼마나 효율적인지 수치적으로 표현할 수 있으며 가장 빠르게 증가하는

항만을 고려하는 표기법이다.

 

  • 다음 알고리즘은 O(n)의 시간 복잡도를 가진다.
let n =10;
let sum = 0;
for (let i = 0; i < n; i++) {
  sum += i;
}
console.log(sum); // 45

 

  • 다음의 알고리즘은 O(n²)의 시간 복잡도를 가진다.
let n = 9;

for(let i = 1; i <= n; i++) {
  for(let j = 1; j <= n; j++) {
    console.log(`${i} X ${j} = ${i * j}`);
  }
}
/*
실행결과:
1 X 1 = 1
1 X 2 = 2
...
9X 8 = 72
9 X 9 = 81
*/

 

연산횟수에 대한 소요시간

일반적으로 연산횟수가 10억을 넘어가면 1초이상의 시간이 소요된다.
[예시] n = 1000

  • O(log n) : 약 10번 연산
  • O(n) : 약 1,000번 연산
  • O(n log n) : 약 10,000번 연산
  • O(n²) : 약 1,000,000번 연산
  • O(n³) : 약 1,000,000,000번 연산

Big-O표기법으로 시간 복잡도를 표기할 때는 가장 큰 항만을 표시한다.
가장 큰 항에 붙어 있는 계수는 제거한다.
O(3n² + n) = O(n²)

 

 

Big-O 표기법을 사용한 시간복잡도의 예시

  • O(1) : 스택에서 Push, Pop
  • O(log n) : 이진트리
  • O(n) : for 문
  • O(n log n) : 퀵 정렬, 병합정렬, 힙 정렬
  • O(n²): 이중 for 문, 삽입정렬, 거품정렬, 선택정렬
  • O(2ⁿ) : 피보나치 수열

 

마치며...

마무리 글에 갑자기 알고리즘에 대해서 공부를 하게 된 이유에 대해 적을까 한다. 메모서비스 개발을 진행하고 있다가 마무리되어 갈 즈음에 문득 스스로 문제해결능력에 대한 불만와 방식에 대해서 의구심을 품게 되었다. "과연 지금까지의 방식이 효율적이고 가장 빠른 길일까?" 그러다 알고리즘에 대해서 문득 떠올리게 되었고 하나씩 알아보게 되었는데 지금까지 나에게 느꼈던 부족함을 해결해 줄 수 있을 것이라 생각이 들었다. 그렇게 개발에 1개월의 공백을 두게 되었지만 다양한 알고리즘을 학습하고 코드를 직접 만들어보기도 하면서 지금까지 나에게 부족했던 부분들이 많이 채워지는 것을 느꼈다. 문제를 해결하기 위해 사고하는 방식이 달라졌고 시간복잡도에 대한 개념이 생기니 효율적으로 문제해결에 다가설 수 있게 된 것 같았다. 완벽하다고 할 수 없겠지만 나에게 있어 개발자로서 필요한 역량을 향상시키기 위한 없어선 안될 1개월이었던 것 같다.

 

2023. 5. 24. 17:31

git filter-branch란?

브랜치를 재작성할 수 있는 기능이며 간략하게 소개하자면 필터를 제공해서

필터에 적용된 파일만 가지고 히스토리를 재구축하는 기능이다. 

이번에 다룰 내용은 특정 파일에 대한 기록을 제외하고 재구축 하는 것에

대한 내용이다.

 

어떤 경우에 쓰면 좋을까?

개발을 진행하면서 깃허브 레포지토리에 push할때 간혹 실수를 하게 될 때가 있다.

예를 들면 절대로 공유되어선 안될 민감한 정보들이 담겨있는 env파일이

푸쉬되어버린 경우에는 아무리 이후에 gitignore파일을 수정하고 깃허브에서 푸쉬된

env파일을 삭제해도 해당 레포지토리의 공개범위가 public인 경우라면 히스토리를

확인하면 env파일의 내용을 누구나 볼 수가있다. 그렇기 때문에 Git의 히스토리 자체를

필터링하여 특정 히스토리를 삭제하고 깃허브에 적용해주어야 할 경우에 많이 사용한다.

 

filter-branch를 사용하게 된 계기

과거 개발 입문당시에 env파일을 실수로 push했던 일이 있었고 레포지토리에서 삭제하면

나에게만 보여지고 다른 유저는 볼 수 없을 것이라고 생각했었던 것 같다. 그렇게 둘러보다

2가지 프로젝트에서  env파일을 삭제했던 기록이 남아있었고 삭제처리가 필요했다. 때문에

git에서 제공하는 filter-branch라는 기능을 알게되었고 이 기능을 사용하여 env에 대한

기록을 모두 제거하고 재구축했다.

 

사용예시

1. 기능사용 환경

내가 filter-branch를 사용한 환경은 백엔드와 프론트엔드를 동시 개발을 진행하고 있었기 때문에

project라는 디렉토리 안에 fontend, backend 두가지 디렉토리로 나누어 개발했다. project디렉토리

내부에 존재하는 frontend와 backend디렉토리를 스테이징 시켜 버전을 관리하고 있었기 때문에

깃허브 레포지토리에는 fontend, backend 두가지 디렉토리가 존재하고 있는 환경이였다.

 

2. 기능사용이 가능한 위치

반드시 해당 기능은 최상위 트리에서만 실행해야 한다. 나의 경우는 fontend디렉토리가 아닌

backend디렉토리에서 env파일에 대한 기록을 삭제해야하기 때문에 처음에는 backend디렉토리 에서

파워쉘을 열어 filter-branch 명령어를 사용했었다. 하지만 실행되지 않았다.

그 이유는 최상위 디렉토리는 backend디렉토리가 아닌 project디렉토리이기 때문이다.

반드시 최상위 트리인 project디렉토리에서 명령어를 실행시켜야 하며 명령어를 작성할때

경로만 backend/... 이렇게 지정해주면 되는 것이다.

 

 

3.  예시코드

# 코드의 두번째 줄에서 backend/.env 는 필터링해줄 경로+파일
git filter-branch -f --index-filter 'git rm --cached --ignore-unmatch 
backend/.env' --prune-empty -- --all

# 이 후에 강제 push 해준다.
git push origin main --force

위에 명령어는 지정된 경로에서 지정된 파일과 일치하지 않는 히스토리만을 남긴채로

재구축하는 명령어다. " Ref 'backend/.env' was rewritten " 명령어가 나오면 재구축이 완료된 것이다.

이후에 강제푸쉬를 해주고 깃허브 레포지토리를 확인해보면 원하는 히스토리가 사라져 있는 것을

볼 수 있다.

 

마치며...

나의 경우는 특별히 문제없이 기능이 잘 수행되었고 문제를 해결할 수 있었지만 타 기술블로그를 찾아 보니 불가피하게 문제해결을 의도하지 못한 방향으로 해결해야하는 상황도 종종있는 것 같았다. 아무리 문제를 해결할 수 있는 좋은 기능들이 있다고 하여도 너무 맹신하는 것 보다는 미리 문제를 방지할 수 있는 기반을 구축한 후에 개발을 진행하는 것이 가장 중요하다고 생각했다.

'Git > Github' 카테고리의 다른 글

[ GitHub ] 깃허브 사용해보기  (0) 2023.03.19