자료구조 Graph, Tree, BST

김동현·2021년 4월 30일

Data Structure

목록 보기
2/5

Graph

여러개의 점들이 서로 복잡하게 연결되어 있는 관계를 표현한 자료구조
서로 다른 점들이 직접적인 관계를 가지고 있다면 바로 이어주는 선이 존재하고, 간접적인 관계를 가지고 있다면 다른 여러 점을 거쳐서 이어지는 선이 존재할 수 있다.
여기서 점은 그래프에서 정점(vertex)이라고 표현하고, 선은 간선(edge)라고 표현한다.

Graph의 실사용 예제

포털 사이트의 건색 엔진, SNS, 네이게이션 등에서 사용하는 자료구조가 바로 그래프이다.

  • 정점: 서울, 대전, 부산
  • 간선: 서울-대전, 대전-부산, 부산-서울
    가중치(연결의 강도가 얼마나 되는지)가 적혀 있지 않은 현재의 그래프는 비가중치 그래프라고 부른다.
    간단한 자바스크립트 객체를 이용, 비교하면
let isConnected = {
  seoul: {
    busan: true,
    daejeon: true
  },
  dajeon: {
    seoul: true,
    busan: true
  },
  busan: {
    seoul: true,
    daejeon: true
  }
}
console.log(isConnected.seoul.daejeon) // true
console.log(isConnected.daejeon.busan) // true

비가중치 그래프는 각 정점 간의 연결 유뮤만을 판단하는 방면, 가중치 그래프는 더 자세한 정보를 담을 수 있다.

  • 정점: 서울, 대전, 부산
  • 간선: 사울-140km-대전, 대전-200km-부산, 부산-325km-서울
    이 가중치 그래프가 확장되어 수백만개의 정점(주소)가 추가되어야 비로소 내비게이션에서 쓰는 자료구조와 유사해진다.

알아야할 용어들

  • 무향 그래프(undirected graph): 앞서 보았던 내비게이션 예제가 무향 그래프이다. 서울에서 부산으로 갈 수 있듯, 반대로 부산에서 서울로 가는 것도 가능하다.
    하지만 단방향(directed) 그래프로 구현된다면 서울에서 부산을 갈 수 있지만, 부산에서 서울로 가는 것은 불가능하다.(혹은 그 반대의 경우도)
    만약 두 지점이 일반 통행 도로로 이어져 있다면 단방향인 간선으로 표현할 수 있다.
  • 진입 차수(in-dgree) / 진출 차수(out-degree): 한 정점에 진입(들어오는 간선)하고 진출(나가는 간선)하는 간선이 몇개인지를 나타낸다.
  • 인접(adjacency): 두 정점간에 간선이 직접 이어져 있다면 이 두 정점은 인접한 정점이다.
  • 자기루프(self loop): 정점에서 진출하는 간선이 곧바로 자기자신에게 진입하는 경우 자기 루프를 가졌다 라고 표현한다. 다른 정점을 거치지 않는다는 것이 특징이다.
  • 사이클(cycle): 한 정점에서 출발하여 다시 해당 정점으로 돌아갈 수 있다면 사이클이 있다고 표현한다. 내비게이션 예시에선 서울 -> 대전 -> 부산 -> 서울 로 이동이 가능하므로 사이클이 존재하는 그래프이다.

그래프의 표현 방식: 인접 행렬 & 인접 리스트

인접 행렬


두 정점을 바로 이어주는 간선이 있다면 이 두 정점은 인접하다고 이야기한다.
인접 행렬은 정점들간의 인접함을 표시해 주는 행렬으로 2차원 배열의 모습을 가지고 있다.
만약 A라는 정점과 B라는 정점이 이어져 있다면 1, 이어져 있지 않다면 0으로 표시하는 일종의 표와 같은 모습이다.
만약 가중치 그래프라면 1 대신 관계에서 의미있는 값(위의 내비게이션 예제에선 거리)을 저장한다.
위 그래프를 인접 행렬로 표현하면

  • A의 진출차수는 1개이다: A -> C
    • [0][2] === 1
  • B의 진출차수는 2개이다: B -> A, B -> C
    • [1][0] === 1
    • [1][2] === 1
  • C의 진출차수는 1개이다: C -> A
    • [2][0] === 1

언제 인접 행렬을 사용하면 좋을까?

  • 한 개의 큰 표와 같은 모습을 한 인접 행렬은 두 정점 사이에 관계가 있는지, 없는지 확인하기에 용이하다.
  • 예를들어, A에서 B로 진출하는 간선이 있는지 파악하기 위해선 0번째 줄의 1번째 열에 어떤 값이 저장되어 있는지 바로 확인할 수 있다.
  • 가장 빠른 경로(shortest path)를 찾고자 할 때 주로 사용된다.

인접 리스트

각 정점이 어떤 정점과 인접한지 리스트의 형태로 볼 수 있는 표현 방식이다.
각 정점마다 하나의 리스트를 가지고 있으며, 이 리스트에는 자신과 인접한 다른 정점들이 담겨 있다.

B는 A와 C로 이어지는 간선이 두 개가 있는데, 왜 A가 C보다 먼저일까? 순서가 중요할까?

  • 보통은 중요하지 않다 라고 답변할 수 있다.
  • 우선 순위를 다뤄야 한다면 더 적합한 자료구조(ex. queue, heap)를 사용하는 것이 합리적이다. 따라서 보통은 중요하지 않다.

언제 인접 리스트를 사용하면 좋을까?

  • 인접 행렬은 연결 가능한 모든 경우의 수를 저장한다.
    • 서로 인접하지 않다면 0을, 인접하다면 1을 저장하기 때문에 메모리를 많이 차지하게 된다.
    • 메모리를 효율적으로 사용하고 싶다면 인접 행렬 대신 인접 리스트를 사용한다.

그래프 구현

class GraphWithAdjacencyMatrix {
  constructor() {
    this.matrix = [];
  }
  addVertex() { // 그래프에 버텍스를 추가
    const currentLength = this.matrix.length;
    for(let i = 0; i < currentLength; i++) {
      this.matrix[i].push(0);
    }
    this.matrix.push(new Array(currentLength + 1).fill(0));
  }
  contains(vertex) { // 그래프에 해당 버텍스가 존재하는지 여부를 Boolean으로 반환
    if(this.matrix[vertex]) {
      return true;
    } else {
      return false;
    }
  }
  addEdge(from, to) { // fromVertex 와 toVertex 사이의 간선을 추가
    const currentLength = this.matrix.length;
    if(from === undefined || to === undefined) {
      console.log("2개의 인자가 있어야 합니다");
      return;
    }
    // 간선을 추가할 수 없는 상황에서는 추가하지 말아야한다.
    if(from + 1 > currentLength || to + 1 > currentLength || from < 0 || to < 0) {
      console.log("범위가 매트릭스 밖에 있습니다");
      return;
    }
    // 간선을 추가해야한다.
    this.matrix[from][to] = 1
  }
  hasEdge(from, to) { // 두 버텍스 사이에 간선이 있는지 확인
    if(this.matrix[from][to] === 1) {
      return true;
    } else {
      return false;
    }
  }
  removeEdge(from, to) {
    const currentLength = this.matrix.length;
    if(from === undefined || to === undefined) {
      console.log("2개의 인자가 있어야 합니다.");
      return;
    }
    // 간선을 지울 수 없는 상황에서는 지우지 말아야 한다.
    if(from + 1 > currentLength || to + 1 > currentLength || from < 0 || to < 0) {
      console.log("범위가 매트릭스 밖에 있습니다.")
      return;
    }
    // 간선을 지워야 한다.
    this.matrix[from][to] = 0;
  }
}

Tree

그래프의 여러 구조 중 읿방향 그래프의 한 구조로, 그 모습이 나무와 닮아 있다고 해서 트리 구조라는 이름이 붙여 졌다.
하나의 뿌리로 부터 가지가 사방으로 뻗은 형태를 띄우고 있기 때문이다.

마치 가계도와 흡사해 보이는 이 트리 구조를 간략하게 정의하면, 데이터가 바로 아래에 있는 하나 이상의 데이터에 단방향으로 연결되는 계층적 자료구조라고 할 수 있다.
데이터를 순차적으로 나열시킨 형태인 선형구조가 아닌, 하나의 데이터 뒤에 여러개의 데이터가 존재할 수 있는 비선형 구조로 되어 있고, 계층적으로 표현이 되며 아래로만 벋기 때문에 사이클이 없다

트리 구조는 루트(Root)라는 하나의 꼭짓점 데이터를 시작으로 여러개의 데이터를 간선(edge)으로 연결한다.
이 데이터들을 노드(node)라고 하며, 상위 노드와 하위 노드가 연결이 되면 부모/자식 관계를 가지게 된다.
A는 B와 C의 부모 노드(Parent Node)가 되는 것이고,
B와 C는 A의 자식 노드(Child Node)가 되는 것이다.
자식이 없는 노드는 나무의 잎과 같다고 하여 리프 노드(Leaf Node)라고 부른다.

이 자료구조는 특이하게도 높이와 깊이를 잴 수 있다.
노드와 노드의 간격(거리)를 레벨(Level)이라고 부르며, 첫 번째 노드인 루트를 Level1로 설정한다.
루트부터 가장 안쪽에 있는 노드까지의 레벨을 트리의 높이(Height)라고 하고, 반대로 특정 노드부터 시작하여 루트까지의 레벨을 노드의 깊이(Depth)라고 표현한다.
같은 레벨에 나란히 있는 노드들은 형제 노드(Sibling Node)로 표현하고 있다.
root에서 뻗어나오는 큰 트리의 내부에 트리의 모양새를 갖춘 작은 트리를 서브 트리라고 부른다.(D-H-I, B-D-E, C-F-G-J)

트리의 실사용 예제

가장 대표적으로, 컴퓨터의 디렉토리 구조를 생각할 수 있다.
모두 하나의 폴더에서 시작되어, 가지처럼 뻗어나가는 형태를 보이고 있다.
사용자들이 사용하기 편하게 하기 위한 파일 시스템과 같은 구현은 모두 트리를 이용해서 만든다.

트리 구현

class Tree { 
  constructor(value) { // constructor로 만든 객체는 트리의 Node가 된다.
    this.value = value;
    this.children = [];
  }
  insertNode(value) { // 트리의 삽입 메서드
    // 값이 어떤 이름으로 만들어지고 어느 위치에 붙는지 떠올리는 것이 중요하다.
    const childNode = new Tree(value);
    this.children.push(childNode);
  }
  contains(value) { // 값이 포함되어 있다면 true를 반환
    if(this.value === value) {
      return true;
    }
    // 값을 찾을 때까지 children 배열을 순회하며 childNode를 탐색
    for(let i = 0; i < this.children.length; i++) {
      if(this.children[i].contains(value)) {
        return true;
      }
    }
    return false;
  }
}

BST(Binary Search Tree)

트리는 편리한 구조를 전시하는 것 외에 효율적인 탐색을 위해 사용하기도 한다.
가장 간단하고 많이 사용하는 트리의 모습은 이진 트리(binary tree)와 이진 탐색 트리(binary search tree)이다.
자식 노드가 최대 두 개인 노드들로 구성된 트리를 이진 트리라고 정의한다.
이 두 개의 노드는 왼쪽 자식과 오른쪽 자식으로 분류한다.
이진 트리는 자료의 삽입, 삭제 방법에 따라 정 이진 트리(Full Bianry Tree), 완전 이진 트리(Complete Binary Tree), 포화 이진 트리(Perfect Binary Tree)로 나뉜다.

  • 완전 이진 트리: 마지막 레벨을 제외한 모든 노드가 가득 차 있어야 하고, 마지막 레벨의 노드는 전부 차 있지 않아도 되지만 왼쪽이 채워져야 한다.
  • 정 이진 트리: 각 노드가 0개 혹은 2개의 자식 노드를 갖는다.
  • 포화 이진 트리: 정 이진 트리이면서 완전 이진 트리인 경우이다. 모든 리프 노드의 레벨이 동일이고, 모든 레벨이 가득 채워져 있는 트리이다.

모든 왼쪽 자식들은 루트나 부모보다 작은 값이고, 모든 오른쪽 자식들은 루트나 부모보다 큰 값인 특징을 가지고 있는 이진 트리를 이진 탐색 트리라고 정의한다.
이진 탐색 트리는 균형 잡힌 트리가 아닐 때, 입력되는 값으 순서에 따라 한 쪽으로 노드들이 몰리게 될 가능성이 있다.
이러한 문제를 해결하기 위해 삽입과 삭제마다 트리의 구조를 재조정하는 과정을 거치는 알고리즘을 도입할 수 있다.

profile
개발자로서의 첫걸음

0개의 댓글