프리코스 때 미리 promise와 async/await를 공부했었는데 이머시브 코스에서 좀 더 심도 있게 알아간 부분에 대해서만 정리를 하려고 한다.

promise, async/await의 문법과 사용방법은 다른 카테고리에 정리를 해두었다.

 

 

1. 동기 vs 비동기 (Sync vs Async)

 

  어떠한 일을 수행하는데 동기적으로 처리하는 방식과 비동기적으로 처리하는 방식의 차이는 무엇일까?

  동기적으로 처리하는 방식은 주어진 일을 다 마무리하고 그다음 일을 수행하는 방식을 말한다.

  반면 비동기적으로 처리하는 방식은 병렬적으로 일을 수행하는 방식이다.

 

  실생활 예로 카페에서 커피를 주문한다고 생각해보자.

  동기적으로 처리한다면 A의 주문을 받고 A의 커피를 만들고 A에게 커피를 준 다음 B의 주문을 받을 것이다.

  비동기적으로 처리한다면 A와 B의 주문을 다 받은 다음 커피가 만들어진 순서대로 A와 B에게 커피를 줄 것이다.

 

  조금만 생각해보아도 비동기적인 방식이 훨씬 효율적임을 알 수 있다.

  마찬가지로 웹 어플리케이션에서도 클라이언트와 서버 간의 데이터를 주고받음에 있어서 비동기적인 처리를 수행한다.

  

  이처럼 비동기 처리의 장점은 동시성이라고 할 수 있다.

  하지만 단점이라고 한다면 병렬적으로 일을 수행하고 또 어떤 일이 먼저 끝날지 모르기 때문에 순서를 보장할 수 없을 것이다.

 

  때문에 비동기 처리를 하는 와중에도 순서가 지켜져야 할 필요가 있는 어떤 일을 수행하기 위해 콜백 함수가 사용될 수 있다.

  콜백 함수라는 이름의 의미에서도 알 수 있듯이 특정 일이 끝나면 나를 다시 호출해줘라는 말 자체가 순서를 가지겠다는 말이기 때문이다.

 

  아..하지만 여기서 또 문제가 발생한다.

  문제는 콜백에 콜백에 콜백이 중첩되는 콜백 지옥이 발생하고 그러한 콜백 지옥은 코드의 가독성과 디버깅을 어렵게 한다는 것이다.

 

엄청난 콜백 지옥

 

 

 

2. promise

 

  이러한 콜백 지옥의 문제를 개선하기 위해 도입된 문법이 바로 promise 문법이라고 할 수 있다.

  프로미스는 일종의 약속이다.

  자 무엇을 약속하느냐?

 

  어떤 일을 잘 수행했을 때는 나 잘 수행했어 그리고 결과 값을 넘겨줄게!

  어떤 일이 잘못되었을 때는 뭔가 문제가 생겼어 그리고 그 에러를 넘겨줄게!

 

  라고하는 일종의 약속인 것이다.

 

  2 - 1) 

    resolve를 호출하면 promise 객체의 상태가 'fullfilled' 값은 resolve에 전달해준 전달 인자가 된다.

    reject를 호출하면 promise 객체의 상태가 'rejected' 값은 resolve에 전달해준 전달인자가 된다.

    둘 중 아무것도 호출하지 않으면 상태는 'pending' 값은 'undefined'이다.

 

  2 - 2)

    .then의 리턴 값은 프로미스 객체이다! 그래서 .then chaining이 가능하다.

 

  2 - 3)

    Promise.all 은 요소 전체가 프라미스 객체인 배열을 받고 새로운 프라미스를 반환한다.

배열 안 프라미스가 모두 처리되면 새로운 프라미스가 이행되는데, 배열 안 프라미스의 결괏값을 담은 배열이 새로운 프라미스의 result가 된다.

 

 

 

3. async/await

 

  promise 문법의 syntatic sugar!

  자바스크립트 비동기 처리 패턴 중 가장 세련된 문법이다.

  왜 async & await 문법이 편할까?

  기본적으로 코드가 위에서 아래로 내려오면서 한 줄 한 줄 차근차근 읽으면서 사고하는 것이 편하기 때문!

 

  .then의 결과 값을 담으려면 await 키워드를 앞에 붙여주기만 하면 된다.

  await 키워드를 쓰려면 async 키워드가 쓰인 함수 안에서만 쓸 수 있다!

  예외 처리는 try catch 구문으로 할 수 있다!

 

  주의할 점 : await 키워드는 비동기 처리를 하는 함수나 메서드가 꼭 프로미스 객체를 반환하는 것에만 써야 의도적으로 작동한다.

 

  

  

'CodeStates' 카테고리의 다른 글

IM 25일차 (http)  (0) 2021.02.06
IM 24일차 (Basic Web Architecture, Ajax)  (0) 2021.02.05
IM 22일차 (자바스크립트 이벤트 루프)  (0) 2021.02.02
IM 18일차 (Back Tracking)  (0) 2021.01.29
IM 13일차 (Graph)  (0) 2021.01.24

1. 자바스크립트는 single-thread 언어다.

 

  JavaScript는 단일 스레드이다. 한 번에 하나의 작업만 실행할 수 있다.

  일반적으로 큰 문제는 아니지만 만약 30 초가 걸리는 어떤 작업을 실행한다고 상상해보자.

  해당 작업 중에 어떠한 다른 작업도 할 수 없다. 현대 웹 애플리케이션에서 이러한 웹 사이트를 원하는 사람은 아마 없을 것이다.

 

  다행히 브라우저는 JavaScript 엔진 자체가 제공하지 않는 웹 API 기능을 제공한다.

  여기에는 DOM API, setTimeout, HTTP 요청 등이 있다. 이를 통해 비동기 동작을 수행할 수 있다.

  간단하게 setTimeout 함수로 어떻게 비동기 처리가 가능한지 알아보자!

 

 

 

  우리가 함수를 호출하면 그 함수는 Call Stack 이라는 항목에 추가된다.

  이 호출 스택은 JS 엔진의 일부이며 브라우저에 한정되지 않는다. 그리고 함수가 값을 리턴하면 스택에서 제거된다.

 

 

  respond 함수의 호출은 setTimeout 함수를 반환한다. setTimeout은 웹 API에 의해 제공된다.

  이를 통해 메인 스레드를 차단하지 않고 작업을 지연시킬 수 있다.

  setTimeout 함수에 전달한 콜백 함수인 화살표 함수 () => {return 'Hey'}가 Web API에 추가된다.

  그 동안 setTimeout 함수와 respond 함수가 스택에서 튀어나와 모두 값을 반환할 것이다!

 

 

  Web API에서 타이머는 우리가 전달한 두 번째 인수인 1000ms 동안 실행된다.

  또한 콜백 함수는 즉시 콜 스택에 추가되지 않고 대신 큐에 전달된다.

 

 

  이제 이벤트 루프가 자신의 유일한 작업을 수행 할 시간이다. 큐와 호출 스택을 연결하는 것이다!

  만약 호출 스택이 비어있는 경우 큐의 첫 번째 항목이 호출 스택에 추가된다.

 

 

  마찬가지로 콜백 함수가 콜 스택에 추가되고, 호출되고, 값을 반환하고, 스택에서 제거된다.

 

 

 

 

2. 자바스크립트 이벤트 루프 동작 원리 요약

 

www.youtube.com/watch?v=8aGhZQkoFbQ&feature=emb_title

 

위 영상을 보고 요약한 내용을 적어보았다.

 

싱글 스레드 런타임이란 하나의 싱글 스택만을 가지고 있음을 의미한다.

콜 스택은 말그대로 함수를 담는데 쓰이는 스택 자료구조이다.

함수가 호출이 되면 함수는 차례대로 스택에 쌓이고 해당 함수가 리턴이 되면 스택에서 사라진다.

 

blocking에 대한 정확한 정의는 존재하지 않는다. blocking이란 그저 느리게 동작하는 코드일 뿐이다.

통신의 과정을 거치는 네트워크 요청이나 이미지 프로세싱은 느리다.

이러한 느린 동작이 콜 스택에 남아있는 것을 보고 보통 blocking이라 한다.

 

그러면 이러한 것이 문제일까?

문제는 바로 브라우저에서 이러한 작동 방식으로 자바스크립트가 돌아간다는 점이다.

그리고 그것은 사용자에게 UX의 관점에서 매우 안 좋은 경험을 줄 수 있다는 것이다.

 

어떤 특정 동작이 30초가 걸리는 작업이 있는데 그 작업을 수행하는 동안 다른 아무것도 할 수 없는 상황을,,

아무도 그런 웹사이트는 사용하지 않을 것이다.

 

이러한 해결책으로 콜백 함수가 나왔다.

근데 도대체 콜백함수는 어떤 원리로 호출되는 것일까?

여기서 이벤트 루프의 역할과 동시성이라는 키워드가 중요하다!

 

모든 web api 작동이 완료되면 콜백 함수를 테스크 큐에 밀어넣는다.

여기서 이벤트 루프가 중요한 역할을 하는데 이벤트 루프는 스택과 테스크 큐를 지켜보다가 스택이 비어있으면 테스크 큐의 첫번째 콜백을 스택에 쌓도록하는 역할을 담당한다.

 

프론트 단에서 비동기 함수가 호출되는 방식은 이벤트 루프의 동작 방식과 같다.

동기적인 콜백은 바로 콜스택에서 작동하지만 dom api나 fetch 같은 web api 같은 경우 무조건 테스크 큐를 통해 스택이 비어있는 경우에 스택으로 들어와 실행이 되는 것이다.

 

그리고 그런 '이벤트 루프를 막지 말아라'라는 말의 의미는 '콜 스택에 필요없는 느린 코드를 쌓아서 브라우저가 할일을 못하게 만들지 말아라' 가 되겠다!

 

끝으로 마이크로 테스크 큐가 일반 테스크 큐보다 우선순위가 높기때문에 마이크로 테스크 큐에 담기는 프로미스가 우선순위가 높다!

 

  *참고 : dev.to/lydiahallie/javascript-visualized-event-loop-3dif

'CodeStates' 카테고리의 다른 글

IM 24일차 (Basic Web Architecture, Ajax)  (0) 2021.02.05
IM 22일차 (Promise & Async/Await)  (0) 2021.02.02
IM 18일차 (Back Tracking)  (0) 2021.01.29
IM 13일차 (Graph)  (0) 2021.01.24
IM 11일차 (Tree, Binary Search Tree)  (0) 2021.01.24

1. Back Tracking

 

  DFS 방식으로 자료를 탐색할 때 유망하지 않은 것에 대해선 가지치기를 수행하여 이전으로 돌아가 재탐색하는 방식을 말한다.

  여기서 유망하다는 것의 의미는 '답이 될 수 있는 경로'를 의미한다.

  또한 가지치기란 유망하지 않은 것을 '잘라내다'라고 이해하면 된다.

 

 

 

2. 백 트래킹이 왜 필요할까?

 

  기본적으로 DFS 방식은 주어진 모든 데이터를 순회하는 완전 탐색 기법이다.

  따라서 백 트래킹 없이 탐색한다면 굳이 답이 되지 않는 경우도 탐색을 해야 돼서 시간적인 낭비라고 할 수 있다.

 

 

 

3. 대표적인 예시

 

  nQueens 문제를 DFS 방식과 Back Tracking 방식을 통해 효율적으로 풀 수 있다.

  nQueens 는 n by n의 체스판에서 n개의 queen을 놓을 수 있는 모든 경우의 수를 찾는 문제다.

  queen은 거리에 상관없이 가로, 세로, 대각선에 위치한 다른 말들을 공격할 수 있다.

  따라서 그러한 경우에 해당되지 않는 모든 경우의 수를 세야만 하는 것이다.

 

  let solutionCount = 0;
  let board = new Board({ n });
  let boardArr = board.rows();
  
  let countNQueens = function (col) {
    if (board.hasAnyQueensConflicts()) {
      return;
    } // 체스판에서 queen의 충돌여부가 있으면 더 이상 재귀 호출이 이루어지지 않음
      // 즉 유망하지 않은 길에 대한 가지치기라고 볼 수 있음.
    if (col === boardArr.length) {
      solutionCount++;
      return;
    } // 탈출 조건 
    for (let i = 0; i < boardArr.length; i++) {
      board.togglePiece(i, col); // 말을 놓을 자리에 대해 좌표를 0 -> 1
      countNQueens(col + 1); // 재귀 호출
      board.togglePiece(i, col); // 0 -> 1로 만들었던 좌표를 다시 1 -> 0 으로 만들어야 함.
    } 
  }
  countNQueens(0);
  return solutionCount;

'CodeStates' 카테고리의 다른 글

IM 22일차 (Promise & Async/Await)  (0) 2021.02.02
IM 22일차 (자바스크립트 이벤트 루프)  (0) 2021.02.02
IM 13일차 (Graph)  (0) 2021.01.24
IM 11일차 (Tree, Binary Search Tree)  (0) 2021.01.24
IM 10일차 (Linked List)  (0) 2021.01.22

1. Graph란?

 

  그래프는 정점과 간선으로 이루어진 자료구조다.

  정확히는 정점(Vertex)간의 관계를 표현하는 조직도라고 볼 수 있다.

  그런면에서 트리도 그래프라고 할 수 있다.

 

  다만 트리와는 달리 그래프는 정점마다 간선이 없을수도 있고 있을수도 있으며 루트 노드, 부모와 자식이라는 개념이 없다.

  또한 그래프는 네트워크 모델 즉, 객체와 이에 대한 관계를 나타내는 유연한 방식으로 이해할 수 있다.

 

  그래프의 실생활 예제는 다양하다.

  지도, 지하철 노선도, SNS 에서의 팔로우 및 팔로잉이 대표적인 예이다.

 

 

 

2. 용어 정리

 

  2 - 1) 정점(Vertex) : 그래프를 형성하는 노드다.

  2 - 2) 간선(Edge) : 그래프에서 노드의 연결을 의미한다.

  2 - 3) 정점 차수(Degree of Vertex) : 해당 정점(노드)에 연결된 간선의 개수를 나타낸다.

  2 - 4) 희소 그래프(sparse graph) : 정점들 간에 가능한 연결 중 일부만 존재하는 경우 해당 그래프를 희소 그래프라 한다.

  2 - 5) 밀집 그래프(dense graph) : 다양한 정점들 간에 연결이 많은 경우 해당 그래프를 밀집 그래프라고 한다.

  2 - 6) 순환 그래프(cyclical graph) : 어떤 정점에서 출발해 해당 정점으로 다시 돌아오는 경로가 존재하는 지향성 그래프를 말한다.

  2 - 7) 가중치(weight) : 간선에 대한 값으로, 문맥에 따라 다양한 것을 의미한다. 예를 들면 노드 사이의 거리가 될 수 있다.

 

 

 

3. 다양한 그래프

 

  3 - 1) 무지향성 그래프(undirected graph)

  무방향 그래프라고도 하며 한 노드가 다른 노드의 연결인 edge의 방향이 없다.

  즉 노드 간에 edge가 있으면 상호 연결을 의미한다.

 

  3 - 2) 지향성 그래프(directed graph)

 

 

 

  방향 그래프라고도 하며 edge가 방향을 가지는 그래프이다.

 

  3 - 3) 가중치 그래프(weighted graph)

 

 

 

  edge가 특정 값을 가지는 그래프이다. 값의 의미는 문맥에 따라 달라진다.

 

 

 

4. 그래프 구현 방법

 

  그래프를 구현하는 방법에는 인접행렬(Adjacency Materix)와 인접리스트(Adjacency List)방식이 있다.

  두개의 구현 방식은 각각의 장단점을 가지고 있는데 대부분 인접리스트 형식을 많이 사용한다.

 

  4 - 1) 인접 행렬(Adjacency Matrix)

   

  인접행렬은 그래프의 노드를 2차원 배열로 만든 것이다.

  완성된 배열의 모양은 0, 1, 2, 3, 4 의 정점을 연결하는 노드에 다른 노드들이 인접 정점이라면 1, 아니면 0을 넣어준다.

 

  인접 행렬 방식의 장점으로는 2차원 배열 안에 모든 정점들의 간선 정보를 담기 때문에 배열의 위치를 확인하면

  두 점에 대한 연결 정보를 조회할 때 O(1) 의 시간 복잡도면 가능하다.

 

  인접 행렬 방식의 단점으로는 무조건 2차원 배열이 필요하기에 노드(정점)의 개수가 많을때 공간 낭비가 있을 수 있다.

  

  4 - 2) 인접 리스트(Adjacency List)

 

  인접리스트란 그래프의 노드들을 리스트로 표현한 것이다.

  주로 정점의 리스트 배열을 만들어 관계를 설정해줌으로써 구현할 수 있다.

 

  인접리스트의 장점으로는 필요한 만큼의 공간만 사용하기때문에 공간의 낭비가 적다.

  특정 두 점이 연결되었는지 확인하려면 인접행렬에 비해 시간이 오래 걸립니다.

 

  인접 행렬은 O(1) 시간에 탐색이 가능하지만 인접 리스트 방식은 해당 노드에 접근해서 순회해야하기 때문이다.

 

 

 

5. 그래프의 탐색 방법(DFS, BFS)

 

  5 - 1) 깊이 우선 탐색(DFS)

  

    'depth first search' 을 줄인 말이다.

   

Depth First Search

    DFS는 갈 수 있는 깊이만큼 최대한 가고, 더 이상 갈 곳이 없다면 이전 정점으로 돌아가는 방식으로 그래프를 순회하는 방식이다. 

    간단히 재귀호출을 사용하여 구현하거나 스택을 사용하여 구현할 수 있다.

 

  5 - 2) 너비 우선 탐색(BFS) 

 

    'breadth first search'의 줄임말이다.

 

Breadth First Search

  BFS는 시작정점을 기준으로 인접한 모든 정점을 방문한 다음 해당 정점에 인접한 모든 정점들을 우선방문하는 방법이다.

  일반적으로 Queue(선입선출 자료구조)를 사용해서 지금 위치에서 갈 수 있는 것들을 모두 큐에 인큐하고 그 다음 항목을

  인큐하기 전에 기존에 있던 항목을 디큐하는 방법으로 구현할 수 있다.

'CodeStates' 카테고리의 다른 글

IM 22일차 (자바스크립트 이벤트 루프)  (0) 2021.02.02
IM 18일차 (Back Tracking)  (0) 2021.01.29
IM 11일차 (Tree, Binary Search Tree)  (0) 2021.01.24
IM 10일차 (Linked List)  (0) 2021.01.22
IM 8일차 (Hash Table)  (0) 2021.01.20

1. Tree

 

  일반적인 트리 자료 구조는 자식 노드를 지닌 노드들로 구성된다.

  첫 번째이자 가장 상위 노드를 루트 노드라 한다.

 

 

일반적인 트리 구조

 

 

 

2. 이진트리

 

  이진트리는 자식 노드가 왼쪽, 오른쪽 두 개뿐인 트리다.

이진 트리

class Node {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}

class BinaryTree {
  constructor() {
    this._root = null;
  }
}

 

 

 

3. 순회

 

  트리 순회를 위한 방법에는 다양한 방법이 있다.

  가장 널리 사용되는 순회 기법으로 깊이 우선 순회(선순위 순회, 중순위 순회, 후순위 순회), 단계순위 순회가 있다.

  대부분의 트리 순회는 재귀적인 방식을 통해 쉽게 순회할 수 있다.

  트리 구조 자체가 재귀적이기 때문이다.  

 

  3 - 1) 선순위 순회, 중순위 순회, 후순위 순회

 

    선순위 순회는 루트(현재 노드), 왼쪽, 오른쪽 순으로 노드를 방문한다.

    기저 조건은 현재 노드가 null일 때 종료된다. 노드가 null이 아니면 노드 값을 출력한다.

    그런 다음 왼쪽 자식에 대해 재귀 함수를 호출하고 오른쪽 자식에 대해 재귀함수를 호출한다.

 

    중순위 순회는 왼쪽, 루트(현재 노드), 오른쪽 순으로 노드를 방문한다.

    기저 조건은 선순위 순회와 같다. 기저 경우가 아닌 경우 왼쪽 자식에 대해 재귀 함수를 호출한 다음 현재 노드를 출력한다.

    마지막으로 오른쪽 자식에 대해 재귀함수를 호출한다.

 

    후순위 순회는 왼쪽, 오른쪽, 루트(현재 노드) 순으로 노드를 방문한다.

 

class BinaryTree {
  constructor() {
    this._root = null;
  }
  
  ...
  
  //선순위 순회
  traversePreOrder() {
    let currNode = this._root;
    if (!currNode) {
      return;
    } else {
      console.log(currNode.value);
      currNode.left.traversePreOrder();
      currNode.right.traversePreOrder();
    }
  }
  
  //중순위 순회
  traverseInOrder() {
    let currNode = this._root;
    if (!currNode) {
      return;
    } else {
      currNode.left.traversePreOrder();
      console.log(currNode.value);
      currNode.right.traversePreOrder();
    }
    
  //후순위 순회
  traversePostOrder() {
    let currNode = this._root;
    if (!currNode) {
      return;
    } else {
      currNode.left.traversePreOrder();
      currNode.right.traversePreOrder();
      console.log(currNode.value);
    }
  }
}

    

3 - 2) 단계 순위 순회

 

  위 순회에서는 깊이 우선 순회를 알아보았다. 단계순위 순회는 (Breadth First Search, BFS)이라고도 부른다.

  단계순위 순회 방법의 핵심은 왼쪽 혹은 오른쪽으로 깊게 들어가는 깊이우선 순회와 달리 각 노드 단계를 방문한다는 점이다.

 

class BinaryTree {
  constructor() {
    this._root = null;
  }
  
  ...
  
  traverseLevelOrder() {
    let currNode = this._root;
    let queue = [];
    
    if (!currNode) {
      return;
    } else {
      queue.push(currNode);
    }
    
    while (queue.length) {
      let temp = queue.shift();
      console.log(temp.value);
      if (temp.left) queue.push(temp.left);
      if (temp.right) queue.push(temp.right);
    }
  }
}

 

  3 - 3) 트리 순회 요약

 

    리프 노드를 방문하기 전에 루트를 조사할 필요가 있다면 선순위 순회를 선택하라.

    부모 노드를 방문하기 전에 리프 노드를 먼저 조사해야 하는 경우 후순위 순회를 선택하라.

    트리의 노드 자체에 순서가 있어서 트리를 원래 순서대로 방문하고 싶은 경우 중순위 순회를 선택하라.

    위의 순회는 모두 모든 노드를 방문해야 하기 때문에 시간 복잡도가 O(n)으로 동일하다.

  

    또한 단계 순위 순회의 경우 그래프 자료구조에서 더욱 자세하게 다룰 것이다.

 

 

 

4. 이진 탐색 트리

  

  이진 탐색 트리(Binary Search Tree) 역시 이진트리와 마찬가지로 왼쪽과 오른쪽 두 개의 자식이 있다.

  하지만 이진 탐색 트리의 경우 왼쪽 자식이 부모보다 작고 오른쪽 자식이 부모보다 크다.

  이진 탐색 트리가 이런 구조를 지닌 이유는 검색, 삽입, 특정 값을 가진 노드 제거의 시간 복잡도가 O(logN)을 지향하기 때문이다.

 

 

binary search tree

 

  4 - 1) 삽입

 

class Node {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}

class BinarySearchTree {
  constructor() {
    this._root = null;
  }
  
  ...
  insert(value) {
    let currNode = this._root;
    if (!currNode) {
      this._root = new Node(value);
    } else {
      if (value < currNode.value) {
        if (!currNode.left) {
          this.left = new BinarySearchTreeNode(value);
          return;
        } else {
          currNode.left.insert(value);
        }
      } else {
        if (!currNode.right) {
          this.right = new BinarySearchTreeNode(value);
          return;
        } else {
          currNode.right.insert(value);
        }
      }
    }
  }
}

 

  이진 탐색 트리의 삽입의 경우 크게 현재 노드의 값보다 작은 경우와 크거나 같은 경우로 분기를 나눈다.

  만약 들어온 값이 현재 노드의 값보다 작고 현재 노드의 왼쪽 자식이 null인 경우 받아온 값으로 새로운 노드를 만들고 할당한다.

  만약 왼쪽 자식이 null이 아니라면 왼쪽 자식을 새로운 현재 노드로 하여 재귀 함수를 호출한다.

 

  받아온 인자가 현재 값보다 큰 경우도 마찬가지 로직으로 구성된다.

 

  4 - 2) 검색

 

class BinarySearchTree {
  constructor() {
    this._root = null;
  }
  
  ...
  
  findNode(value) {
    let currNode = this._root;
    if (currNode.value === value) {
      return true;
    } else {
      if (value < currNode.value) {
        if (!currNode.left) return false;
        currNode.left.findNode(value);
      } else {
        if (!currNode.right) return false;
        currNode.right.findNode(value);
      }
    }
  }
}

  

  검색도 마찬 가지로 재귀를 사용하면 쉽게 원하는 값을 찾을 수 있다.

  기저 조건은 현재 노드의 값이 인자로 받아온 값과 같은 경우 true를 반환한다.

 

  4 - 3) 삭제

 

    삭제 알고리즘은 우선 삭제하고자 하는 값을 지닌 노드를 찾기 위해 트리를 순회한다.

    해당 노드를 찾은 경우 다음 세 가지 경우가 있다.

 

  • 노드에 자식이 없는 경우 -> 가장 간단한 경우다. 노드에 자식이 없다면 해당 노드에 null을 반환한다.
  • 노드에 자식이 하나 있는 경우 -> 노드에 자식이 하나 있는 경우 현재 자식을 반환한다. 해당 자식이 위 단계로 올라가서 부모를 대체
  • 노드에 자식이 두 개 있는 경우 -> 노드에 자식이 두 개 있는 경우 왼쪽 하위 트리의 최대치를 찾거나 오른쪽 하위 트리의 최소치를 찾아서 해당 노드를 대체한다.
class BinarySearchTree {
  constructor() {
    this._root = null;
  }
  
  ...
  
  remove(value) {
    
    return deleteRecursively(this._root, value);
    
    function deleteRecursively(root, value) {
      if (!root) {
        return null;
      } else if (value < root.value) {
        root.left = deleteRecursively(root.left, value);
      } else if (value > root.value) {
        root.right = deleteRecursively(root.right, value);
      } else {
        //찾은 경우
        if (!root.left && !root.right) {
          //자식이 없는 경우
          return null;
        } else if (!root.left) {
          //오른쪽 자식만 있는 경우
          root = root.right;
          return root;
        } else if (!root.right) {
          //왼쪽 자식만 있는 경우
          root = root.left;
          return root;
        } else {
          let temp = findMin(root.right);
          root.value = temp.value;
          root.right = deleteRecursively(this.right, temp.value);
          return root;
        }
      }
      return root;
    }
    
    function findMin(root) {
      while (root.left) {
        root = root.left;
      }
      return root;
    }
  }
}

 

  이진 탐색 트리의 삽입, 검색, 삭제에 대해 알아보았다.

  만약 이진 탐색 트리가 균형 트리라면 세 가지 연산 모두 시간 복잡도가 O(logN) 일 것이다.

 

  하지만 불균형 트리(자식이 오른쪽이나 왼쪽으로 치우친 경우)는 세 가지 연산 모두 시간 복잡도가 O(n)이 된다.

  즉 이진 탐색 트리의 장점을 전혀 살리지 못하는 것이다.

 

  위의 해결 방법으로는 데이터가 삽입이 될 때 트리가 스스로 균형을 잡도록 하는 AVL 트리가 있다.

  하지만 나는 아직 AVL 트리를 다루기엔 벅차다. 일주일 만에 많은 자료 구조를 공부하면서 머리가 터지기 일보직전이다.

  AVL 트리에 대해선 추후에 포스팅을 해야 할 것 같다.

'CodeStates' 카테고리의 다른 글

IM 18일차 (Back Tracking)  (0) 2021.01.29
IM 13일차 (Graph)  (0) 2021.01.24
IM 10일차 (Linked List)  (0) 2021.01.22
IM 8일차 (Hash Table)  (0) 2021.01.20
IM 4일차 (Class)  (0) 2021.01.19

1. Linked List란?

 

  연결 리스트는 각 노드가 다른 노드를 가리키는 자료 구조다.

  고정된 크기를 갖는 배열과 달리(물론 자바스크립트에서 배열은 동적 자료 구조이지만 일반적으로 다른 언어에서의 배열을 기준으로 함.)

  연결 리스트는 실행 시간에 메모리를 할당하거나 해제할 수 있는 동적 자료 구조다.

 

  연결 리스트에는 단일 연결 리스트와 이중 연결 리스트가 있다.

 

 

 

2. Singly Linked List

 

  단일 연결 리스트는 각 노드가 다음 노드에 대한 참조를 갖는 자료 구조다.

  단일 연결 리스트의 노드에는 data와 next라는 속성이 있고 data에는 노드에 들어갈 값을 저장하고

  next에는 다른 노드에 대한 주소 값을 할당한다. 즉 next를 통해 노드 간의 연결이 이루어진다고 할 수 있다.

 

class Node {
  constructor(data) {
    this.data = data;
    this.next = null;
  }
}

class SinglyLinkedList {
  constructor() {
    this.head = null;
    this.size = 0;
  }
}

// 연결 리스트의 시작을 head라고 부른다.
// 헤드의 기본적인 디폴트 값은 null이다;

 

  2 - 1) 데이터의 삽입

 

    연결 리스트의 헤드가 비어 있다면 헤드가 신규 노드가 된다.

    그렇지 않다면 예전 헤드가 temp에 저장되고 새로운 헤드는 신규로 추가된 노드가 된다.

    마지막으로 새로운 헤드의 next는 예전 헤드를 가리킨다.

 

class SinglyLinkedList() {
  constructor() {
    this.head = null;
    this.size = 0;
  }
  
  insertNode(value) {
    if (this.head === null) {
      this.head = new Node(value);
    } else {
      let temp = this.head;
      this.head = new Node(value);
      this.head.next = temp;
    }
    this.size++;
  }
}

 

  2 - 2) 삭제

 

    노드를 삭제하는 것은 해당 노드의 참조를 제거함으로써 구현할 수 있다.

    삭제는 크게 세 부분으로 나뉜다고 할 수 있다.

    

    헤드에 있는 노드를 삭제, 중간 노드를 삭제, 마지막 노드를 삭제

    헤드 노드를 삭제하려면 삭제할 항목의 다음 항목이 새로운 헤드가 되면 된다.

    중간 노드를 삭제하려는 경우 해당 노드 이전의 노드가 삭제하려는 노드의 다음 노드를 가리키도록 하면 된다.

    마지막 노드를 삭제하려는 경우 해당 노드의 이전 노드의 next 값을 null로 만든다.

 

class SinglyLinkedList {



   ...
   
  remove(value) {
    let currNode = this.head;
    if (currNode.data === value) {
      this.head = currNode.next;
      this.size--;
      return;
    } else {
      let prevNode = currNode;
      while (currNode.next) {
        if (currNode.data === value) {
          prevNode.next = currNode.next;
          this.size--;
          return;
        }
        prevNode = currNode;
        currNode = currNode.next;
      }
      prevNode.next = null;
    }
    this.size--;
  }
}

 

  2 - 3) 검색

  

    head를 시작으로 반복 순회하면 된다.

 

class SinglyLinkedList {
  constructor() {
    this.head = null;
    this.size = 0;
  }
  
  
  
  ...
  
  find(value) {
    let currNode = this.head;
    while (currNode.next) {
      if (currNode.data === value) {
        return true;
      }
      currNode = currNode.next;
    }
    return false;
  }
}

 

 

 

3. Doubly Linked List

 

  이중 연결 리스트는 양방향 단일 연결 리스트이다.

  즉 단일 연결 리스트는 다음 노드만을 참조하지만 이중 연결 리스트는 이전 노드도 참조를 하게된다.

 

class Node {
  constructor(data) {
    this.data = data;
    this.next = null;
    this.prev = null;
  }
}

class DoublyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.size = 0;
  }
}

 

  이중 연결 리스트에서의 데이터의 삽입, 삭제, 검색을 구현하는 것은 단일 연결 리스트와 크게 다르지 않다.

  하지만 삽입과 삭제의 경우 next 속성과 prev 속성 둘 다 갱신돼야 한다.

 

  3 - 1) 헤드에 항목 삽입하기

 

class DoublyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.size = 0;
  }
  
  ...
  
  addAtFront(value) {
    if (this.head === null) {
      this.head = new DoublyLinkedList(value);
      this.tail = this.head;
    } else {
      let temp = new DoublyLinkedList(value);
      temp.next = this.head;
      this.head.prev = temp;
      this.head = temp;
    }
    this.size++;
  }
}

 

  3 - 2) 테일에 항목 삽입하기

 

class DoublyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.size = 0;
  }
  
  ...
  
  addAtTail(value) {
    if (this.tail === null) {
      this.tail = new Node(value);
      this.head = this.tail;
    } else {
      let temp = new Node(value);
      temp.prev = this.tail;
      this.tail.next = temp;
      this.tail = temp;
    }
    this.size++;
  }
}

 

  3 - 3) 헤드의 항목 삭제

 

class DoublyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.size = 0;
  }
  
  ...
  
  deleteAtHead() {
    let toReturn = null;
    if (this.head !== null) {
      toReturn = this.head.data;
      if (this.head === this.tail) {
        this.head = null;
        this.tail = null;
      } else {
        this.head = this.head.next;
        this.head.prev = null;
      }
    }
    this.size--;
    return toReturn;
  }
}

 

 

  3 - 4) 테일의 항목 삭제

   

class DoublyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.size = 0;
  }
  
  ...
  
  deleteAtTail() {
    let toReturn = null;
    if (this.tail !== null) {
      toReturn = this.tail.data;
      if (this.tail === this.head) {
        this.head = null;
        this.tail = null;
      } else {
        this.tail = this.tail.prev;
        this.tail.next = null;
      }
    }
    this.size--;
    return toReturn;
  }
}

 

  3 - 5) 검색

 

class DoublyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.size = 0;
  }
  
  ...
  
  findAtHead(value) {
    let currNode = this.head;
    while (currNode.next) {
      if (currNode.data === value) {
        return true;
      }
      currNode = currNode.next;
    }
    return false;
  }
  
  findAtTail(value) {
    let currNode = this.tail;
    while (currNode.prev) {
      if (currNode.data === value) {
        return true;
      }
      currNode = currNode.prev;
    }
    return false;
  }
}
  
  

 

 

 

4. 요약

 

  단일 연결 리스트와 이중 연결 리스트 모두 삽입, 삭제 연산은 O(1) 상수 시간 복잡도를 지닌다.

  하지만 검색은 모두 O(n) 시간이 걸린다.

 

 

 

*참고 : 자바스크립트로 하는 자료 구조와 알고리즘, 지은이: 배세민

'CodeStates' 카테고리의 다른 글

IM 13일차 (Graph)  (0) 2021.01.24
IM 11일차 (Tree, Binary Search Tree)  (0) 2021.01.24
IM 8일차 (Hash Table)  (0) 2021.01.20
IM 4일차 (Class)  (0) 2021.01.19
IM 7일차 (Stack, Queue)  (0) 2021.01.19

1. 해시 테이블

 

  key와 value로 이루어진 데이터에서 key를 해시 함수를 통해 해싱된 index로 하고

  그 index에 value 값이 저장되는 자료구조를 해시 테이블이라고 한다.

 

  해시 테이블의 장점은 데이터의 삽입, 삭제, 조회가 빠르다는 것에 있다.

  물론 좋은 해시 함수를 통해 해싱을 해야 한다는 것과 충돌이 났을 때 충돌을 잘 해결하는 구현이 필요하다.

 

  다만 현재까지 개발된 거의 모든 해시함수는 해시충돌을 일으키는 것으로 확인됐다고 한다.

  물론 해시충돌 자체를 줄이는 것도 의미가 있겠습니다만, 중요한 것은 해시충돌이 해시값 전체에 걸쳐 균등하게 발생하게끔 하는 것이다.

 

 

 

2. chaining

 

  해시충돌 문제를 해결하기 위한 아이디어 가운데 하나는 한 버킷당 들어갈 수 있는 엔트리의 수에 제한을 두지 않음으로써 모든 자료를 해시테이블에 담는 것이다.

 

  해당 버킷에 데이터가 이미 있다면 마치 연결리스트처럼 노드가 노드를 가리키도록 하기 때문에 체인이라는 말이 붙은 것 같다.

  유연하다는 장점을 가지나 메모리 문제를 야기할 수 있다.

 

  또한 그럴 일은 잘 없겠지만 최악의 경우 한 버킷에 모든 데이터가 들어있어 조회와 삭제의 경우 시간복잡도가 O(n)이 될 수 있다.

 

 

 

3. open addressing

 

  open addressing은 chaining과 달리 한 버킷당 들어갈 수 있는 엔트리가 하나뿐인 해시테이블이다. 

  그럼 이 경우엔 충돌을 어떻게 해결하느냐?

 

  3 - 1) 선형 탐사

 

    선형 탐사는 한 번에 한 인덱스를 증가시킴으로써 사용 가능한 인덱스를 찾는다.

    예를 들어 'a' 라는 key 와 'b' 라는 key가 동일한 해시값 3을 가진다고 하면 'a'의 value는 3에 들어갈 것이다.

   

    반면 'b'의 value는 3의 자리에 이미 데이터가 존재하기때문에 선형탐사 방식을 이용해 그 다음 빈자리인 4에 들어갈 것이다.

    이러한 방법을 선형 탐사라 한다.

  

    선형 탐사의 단점은 동일한 해시 값을 가지는 키가 많이 존재할 경우 데이터를 조회할 때 순회가 필요하다는 것이다.

    데이터를 조회할 때도 해시 함수를 통해 얻어진 해시 값으로 인덱스를 찾아서 데이터를 조회해야하는데

    만약 그 인덱스에 원하는 데이터가 없는 경우 해당 인덱스부터 시작해서 인덱스를 1씩 증가시켜 반복을 통해 확인해야 한다는 것이다.

 

  3 - 2) 이차 탐사

 

    이차 탐사는 이러한 선형 탐사의 문제를 보완하기위해 도입된 기법이다.

    이차 탐사는 충돌이 일어나는 경우 매번 1씩 증가시키는 선형탐사와 달리 완전 제곱을 사용한다.

    물론 선형 탐사보다는 덜 하지만 이차 탐사 또한 규칙적으로 인덱스를 증가시켜 데이터를 저장하기 때문에 마찬가지로 순회는 필요하다.

 

  3 - 3) 이중 해싱

 

    이중해싱(double hashing)은 탐사할 해시값의 규칙성을 없애버려서 clustering을 방지하는 기법이다.

    앞서 선형 탐사와 이차 탐사에서의 문제점이 clustering이라고 할 수 있다.

 

    2개의 해시함수를 통해 하나는 최초의 해시값을 얻을 때, 또 다른 하나는 해시충돌이 일어났을 때 탐사 이동폭을 얻기 위해 사용한다.

    이렇게 되면 최초 해시값이 같더라도 탐사 이동폭이 달라지고, 탐사 이동폭이 같더라도 최초 해시값이 달라져 clustering을 모두 완화할 수 있다.

'CodeStates' 카테고리의 다른 글

IM 11일차 (Tree, Binary Search Tree)  (0) 2021.01.24
IM 10일차 (Linked List)  (0) 2021.01.22
IM 4일차 (Class)  (0) 2021.01.19
IM 7일차 (Stack, Queue)  (0) 2021.01.19
IM 4일차 (Pesudoclassical)  (0) 2021.01.15

이전에 함수를 가지고 객체 지향을 구현하는 방법에 관해 블로깅을 했었다.

이번에 블로킹할 class keyword는 자바스크립트에서 객체 지향을 좀 더 편하게 사용할 수 있도록 도와준다.

 

그렇다고 프로토타입 체이닝이라는 동작 원리가 달라지는 것은 아니다.

즉 class 키워드는 syntatic sugar라고 생각해도 좋을 듯하다.

 

기본적인 클래스 문법과 상속, 메서드나 프로퍼티를 오버 라이딩하는 것은 익숙해졌기 때문에

잘 모르는 부분만 간단하게 짚고 넘어가려고 한다.

 

1. 메서드나 프로퍼티에 static 키워드를 붙이면 정적 프로퍼티나 정적 메서드가 된다.

 

  이러한 정적 프로퍼티나 정적 메서드는 클래스 자체에 해당 프로퍼티나 메서드를 구현함으로써

  특정 클래스 인스턴스가 아닌 클래스 '전체’에 필요한 기능을 만들 때 사용할 수 있다.

  인스턴스끼리 비교해주는 메서드나 팩토리 메서드를 만들 때 정적 메서드가 쓰이기도 한다.

 

  정적 프로퍼티나 메서드는 개별 인스턴스에 묶이지 않는다.  

  즉 인스턴스에서 접근할 수가 없다는 의미다.

 

 

 

2. private, protected keyword는 내부 인터페이스를 숨기기 위해 사용된다.

 

  내부 인터페이스와 외부 인터페이스 간의 정확한 구별은 중요하다.

  이는 객체 지향을 구현하는 이유이기도 하다.

  이러한 특성은 캡슐화, 추상화와 관련이 있다.

 

  자바스크립트에서 private keyword는 '#'이고 private keyword는 '_'로 사용할 수 있다.

 

 

 

3. instanceOf 연산자

 

  instanceof 연산자를 사용하면 객체가 특정 클래스에 속하는지 아닌지를 확인할 수 있습니다. instanceof는 상속 관계도 확인해준다.

 

 

 

*참고 : ko.javascript.info/classes

 

 

'CodeStates' 카테고리의 다른 글

IM 10일차 (Linked List)  (0) 2021.01.22
IM 8일차 (Hash Table)  (0) 2021.01.20
IM 7일차 (Stack, Queue)  (0) 2021.01.19
IM 4일차 (Pesudoclassical)  (0) 2021.01.15
IM 4일차 (자바스크립트에서 객체의 작동 원리)  (0) 2021.01.14

+ Recent posts