Dev/SW Engineering

5. Data Structure - Graph, Tree, BST

HJChung 2020. 5. 24. 16:08

Graph

1)개념

: 노드(정점,Node, vertex) 와 간선(edge)로 구성된 자료구조

용어들
차수(Degree): 정점에 몇 개의 간선이 연결되어 있는가
사이클(Cycle): 자기 자신으로 다시 돌아올 수 있는 경로

2) 구현

그래프 구현 방식1: 인접 행렬

    • 인접 행렬: 정점의 연결 관계를 2차원 배열에 0(연결x), 1(연결 o)로 표현하는 것
    • 인접 행렬의 장점: 연결 여부를 O(1)만에 알 수 있다.
    • 인접 행렬의 단점: 1. 인접한 정점을 찾는데 O(n)걸린다. 2. 실제 간선의 개수와 관계없이 무조건 n**2개의 칸을 써야 한다.

그래프 구현 방식2: 인접 리스트

  • 인접 리스트: 각각의 정점에 대하여 인접한 정점 번호를 저장
  • 인접 리스트의 장점: 1. 인접한 정점을 찾는데 해당 정점의 딱 필요한 개수만 확인하면 된다. 2. 필요한 만큼만 공간을 활용한다.
  • 인접 리스트의 단점: 연결 여부를 확인하기 위해선 인접한 모든 node를 전부 확인해야 한다. 그래서 O(v의 차수) = O(deg(v)) 만큼 걸린다.

 

 

  • 인접 리스트를 이용한 그래프의 구현
/* Undirected Graph */
// 무방향그래프를 인접 리스트로 구현
class Graph {
  constructor() {
    this.nodes = {};
  }

  //addNode(node) - 그래프에 노드를 추가합니다.
  //그래프의 인접 리스트 구조
  //this.node = { 1: [2, 3, 4], 
  //              2: [1, 3], 
  //              3: [1, 2], 
  //              4: [1]
  //             }

  addNode(node) {
    this.nodes[node] = this.nodes[node] || {
      edges: [],
    };
    
    //원래는 
    // this.nodes[node] = this.nodes[node] || {
    //   edges: {},
    // }; 
    //였는데 굳이 왜 edges를 객체로 해야하는지 이해가 안가서 (위의 '그래프의 인접 리스트 구조'처럼) 배열로 만들어서 구현했음
  }

  //contains(node) - 그래프에 해당 노드가 존재하는지 여부를 반환합니다.
  contains(node) {
    return (this.nodes[node] ? true : false);
  }

  //removeNode(node) - 그래프에서 노드를 삭제합니다.
  //1. 각 노드의 edge에 node 번호 있으면 다 삭제
  //2. 그리고 최종적으로 해당 node 객체도 삭제
  removeNode(node) {
    for(let currNode in this.nodes){
      let Nodeindex = this.nodes[currNode]['edges'].indexOf(node)

      if(Nodeindex !== -1){
        this.nodes[currNode].edges.splice(Nodeindex, 1);
      }
    }
    delete this.nodes[node];
  }

  //hasEdge(fromNode, toNode) - fromNode와 toNode 사이의 간선 존재 여부를 반환합니다.
  //무방향이니까 하나만 확인해주면 된다. 나는 fromNode의 edges에 toNode가 있는지만 확인ㄴ
  //1. fromNode의 edges에 toNode가 있는지 확인
  //2-1. 있으면 true반환
  //2-2. 없으면 false반환
  hasEdge(fromNode, toNode) {
    if(this.nodes[fromNode].edges.includes(toNode)){
      return true;
    }
    return false;
  }

  //addEdge(fromNode, toNode) - fromNode와 toNode 사이의 간선을 추가합니다.
  //방향이 없으니까 fromNode, toNode에 모두 상대 Node를 추가해 주어야한다. 
  addEdge(fromNode, toNode) {
    this.nodes[fromNode].edges.push(toNode);
    this.nodes[toNode].edges.push(fromNode);
  }

  //removeEdge(fromNode, toNode) - fromNode와 toNode 사이의 간선을 삭제합니다.
  //1. 둘 사이에 간선이 있는지 부터 확인
  //2-1. 있으면 간선 삭제(fromNode의 edge에서도 toNode 삭제해야하고, toNode의 edge에서도 fromNode 삭제해야 한다.)
  //2-2. 없으면 그냥 끝
  removeEdge(fromNode, toNode) {
    if(this.hasEdge(fromNode, toNode)){
      let Nodeindex = this.nodes[fromNode].edges.indexOf(toNode);
      this.nodes[fromNode].edges.splice(Nodeindex, 1);

      Nodeindex = this.nodes[toNode].edges.indexOf(fromNode);
      this.nodes[toNode].edges.splice(Nodeindex, 1);
    }
  }
}

 

Tree

1) 개념

: 노드로 구성된 계층적 자료구조 / 사이클이 없는 그래프

용어들

  • 노드(node, 정점): 트리의 구성요소
  • 간선(edge): 노드와 노드를 잇는 선
  • root: 트리 구조에서 최상위에 존재하는 노드들
  • 부모 노드(parent)
  • 자식 노드(child)
  • depth(level): 루트를 기준으로 다른 노드로 접근하기 위한 거리
  • height:
  • sibling: 같은 depth에 존재하는 노드들을 부르는 관계
  • leaf: 자식이 없는 node

2) 구현

class TreeNode {
  constructor(value) {
    this.value = value;
    this.children = [];
  }
  //Q. children left, right 신경쓰지 않아도 되는 이유: binary tree가 아니기때문
  //트리의 구성
  //        1
  //        /\
  //       2  3
  //      /\
  //     4  5   이면
  // this.children = [1]
  // node 번호             , [children node들]
  // 1 (=thiss.chilren[0])          [2, 3]
                                  // 2 (=thiss.chilren[1].children[0])          [4, 5]
                                  // 3 (=thiss.chilren[2].children[1])           []
                                                                              // 4 (=thiss.chilren[1].children[0].children[0])           []
                                                                              // 5 (=thiss.chilren[1].children[0].children[1])           []


  //insertNode(value) - 트리에 노드를 추가합니다.
  //Q. 문제가 '트리에 노드를 추가한다'고 너무 단순하다고 모호하게 적혀있어서 tree.test.js확인
  // -> 정말 노드만 추가하면 되고, 기존 트리 노드와의 연결성은 신경써주지 않아도 되는 것으로 보임
  // -> 또한 tree.test.js의 31:37 문제를 보았을 때, this.children은 해당 트리에 있는 모든 node들을 모아둔 배열이고, 
  // 여기서 index를 통해 접근하여 그 TreeNode의 class에 자식 Node를 insertNode하는 방식으로 진행됨. 
  // 1. 새로운 노드 선언하고
  // 2. this.children에 새로운 노드 추가히주기
  insertNode(value) {
    let newTreeNode = new TreeNode(value);
    this.children.push(newTreeNode);
  }


  //contains(value) - 트리에 해당 노드가 존재하는지 여부를 반환합니다.
  //1. this.children의 인덱스(각각의 node) 가서
  //2. 그것의 this.children도 확인 - 이때 어디까지 깊이 들어갈지 모르니까 재귀
  // 2-1. 기저조건: this.children이 비어있으면 나오기
  // 2-2. 아니면 재귀: this.children.contains(value)
  contains(value) {
    for(let i=0; i<this.children.length; i++){
      let currNode = this.children[i];
      if(currNode.value === value){
        return true;
      }
      else{
        if(currNode.contains(value)){
          return true; //tree.test.js의 39:47번 문제를 보면 return true를 왜 해줘야 하는지 이해간다.
          // 여기서 return true를 안해주면, 재귀로 깊이 들어갔을 때 value에 해당하는 node를 찾아도, return 이 아무것도 안되기때문에
          // 결국 다음 재귀가 들어가게되면 false가 되기 때문이다. 
        }
      }
    }
    return false;
    
  }

}

 

 

Binary Search Tree

1) 개념

최대 2개의 자식만 가지는 트리로, 이진 탐색 트리에서는 노드의 값에 따라 정렬 방법 및 노드의 순서가 존재한다.

예를 들어,

 

이 트리에서는 노드의 왼쪽 서브트리에는 노드의 값보다 작은 값이, 오른쪽 서브트리에는 노드의 값보다 같거나 큰 값이 존재합니다.

트리의 성질:

트리는 그 안에 또 트리가 존재한다.

질문

깊이 우선 탐색은 무엇인가요?

자료구조의 node들을 돌아다니며, 그 안에 무슨 값이 있는지를 탐색 해보고 싶을 때, 순회(탐색)방법에는 깊이 우선탐색과 너비 우선탐색이 있습니다.

여기서 깊이 우선탐색은, 스택을 이용해서 최대한 깊숙하게, 많이 가보는 방식으로 갈 수 있을 만큼 (발자취-스택 을 남기면서)가보고 갈 수 없으면 남긴 발자취를 따라 이전 node로 back하는 방식입니다.


이진 탐색 트리가 주어졌을 때, 다음과 같은 방법으로 탐색할 수 있나요?

 

전위 순회(Preorder Traversal): 부모 → 좌 → 우

중위 순회(Inorder Traversal): 좌 → 부모 → 우

후위 순회(Postorder Traversal): 좌 → 우 → 부모

이진 탐색 트리의 종류에도 여러가지가 있습니다. 다음 이진 트리는 각각 어떤 특징을 가지나요?

  • 정 이진 트리(Full Binary Tree)
  • 완전 이진 트리(Complete Binary Tree)
  • 포화 이진 트리(Perfect Binary Tree)

2) 구현

class BinarySearchTreeNode {
  constructor(value) {
    this.value = value;
    this.left = null; //this.value보다 작은 값들로 이루어진 subtree
    this.right = null; //this.value보다 큰 값들로 이루어진 subtree
  }
  //this: 전체 binary search tree의 root..?

  //insert(value) - 이진 탐색 트리에 노드를 추가합니다.
  //1. 일단 맨 끝에 넣고, 
  //2. 우선순위 비교해가면서 알맞은 자리 찾아감. 
  //2-1. 위(this.value)에서부터 비교해가며 내려온다. (이때, 무조건 전체 tree의 우선순위가 맞아야 한다고 생각했는데, 그냥 해당 node, 그 node의 left, 그 node의 right만 맞으면 된다.)
  //2-2. this.value > value일 때
  // 3-1. 왼쪽이 비어있으면 왼쪽에 넣고 끝, 3-2. 비어있지 않으면, 그 밑으로 내려가서 같은 과정 반복
  //2-3. this.value <= value일 때
  // 3-1. 오른쪽이 비어있으면 오른쪽에 넣고 끝, 3-2. 비어있지 않으면 그 밑으로 내려가서 같은 과정 반복
  insert(value) {
    let newNode = new BinarySearchTreeNode(value);

   function insertRecursion(topNode){
      if(topNode.value > newNode.value){
        if(topNode.left === null){
          topNode.left = newNode;
        }
        else{
          insertRecursion(topNode.left);
        }
      }
      else if(topNode.value <= newNode.value){
        if(topNode.right === null){
          topNode.right = newNode;
        }
        else{
          insertRecursion(topNode.right)
        }
      }
    }

    insertRecursion(this);

  }

  //contains(value) - 이진 탐색 트리에 해당 노드가 존재하는지 여부를 반환합니다.
  //1. 위에서부터 확인하면서 내려온다. 
  //2. 위에서 값 확인
  //3-1. 맞으면 true 반환하고 끝
  //3-2. 아니면 값이 작으면 왼쪽 으로 반복
  //3-3. 아니면 값이 크거나 같으면 오른쪽으로 반복
  contains(value) {
    var flag = false;

    function containsRecursion(topNode){
      if(topNode.value === value){
        flag = true;
        return;
      } 
      else if(topNode.value > value){
        if(topNode.left ===  null){
          return;
        }
        else{
          containsRecursion(topNode.left);
        }
      }
      else{
        if(topNode.right === null){
          return;
        }
        else{
          containsRecursion(topNode.right);
        }
      }
    }

    containsRecursion(this);

    return flag;
  }
  
  //inorder(callback) - 이진 탐색 트리를 중위순회 합니다.
  //기저조건: 좌, 우가 모두 없을 때
  //아니면, Left-> Root-> Right(좌 → 부모 → 우)
  inorder(callback) {

    function findValue(currNode)
    {
      if(currNode!==null)
      {
        findValue(currNode.left);
        callback(currNode.value);
        findValue(currNode.right);
      }
    }
    findValue(this);


  }
}