알고리즘

알아두면 좋은 알고리즘 종류 및 시간복잡도

태태개발자 2023. 7. 11. 10:19
반응형

프로그래밍 알고리즘 종류


정렬
 - 선택정렬 : O(n^2)

function selectionSort(arr) {
  const length = arr.length;
  
  for (let i = 0; i < length - 1; i++) {
    let minIndex = i;
    
    // 최솟값을 찾기 위해 배열을 순회합니다.
    for (let j = i + 1; j < length; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j;
      }
    }
    
    // 최솟값을 현재 위치와 교환합니다.
    const temp = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
  }
  
  return arr;
}

// 테스트용 배열
const arr = [64, 25, 12, 22, 11];
console.log("정렬 전:", arr);
console.log("정렬 후:", selectionSort(arr));

 - 버블정렬 : O(n^2)

function bubbleSort(arr) {
  const length = arr.length;
  
  for (let i = 0; i < length - 1; i++) {
    // 현재 루프에서 스왑이 발생하지 않으면 이미 정렬된 상태이므로 종료합니다.
    let swapped = false;
    
    for (let j = 0; j < length - i - 1; j++) {
      // 현재 요소와 다음 요소를 비교하여 순서를 바꿉니다.
      if (arr[j] > arr[j + 1]) {
        // 스왑
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        swapped = true;
      }
    }
    
    // 스왑이 발생하지 않으면 정렬이 완료된 것이므로 종료합니다.
    if (!swapped) break;
  }
  
  return arr;
}

// 테스트용 배열
const arr = [64, 34, 25, 12, 22, 11, 90];
console.log("정렬 전:", arr);
console.log("정렬 후:", bubbleSort(arr));


 - 삽입정렬 : O(n^2)

function insertionSort(arr) {
  const length = arr.length;
  
  for (let i = 1; i < length; i++) {
    const current = arr[i];
    let j = i - 1;
    
    // 현재 요소를 이미 정렬된 부분의 적절한 위치에 삽입합니다.
    while (j >= 0 && arr[j] > current) {
      arr[j + 1] = arr[j];
      j--;
    }
    
    arr[j + 1] = current;
  }
  
  return arr;
}

// 테스트용 배열
const arr = [64, 34, 25, 12, 22, 11, 90];
console.log("정렬 전:", arr);
console.log("정렬 후:", insertionSort(arr));


 - 퀵정렬 : 평균 O(n log n), 최악 O(n^2)

function quickSort(arr, left = 0, right = arr.length - 1) {
  if (arr.length > 1) {
    const index = partition(arr, left, right);
    
    if (left < index - 1) {
      quickSort(arr, left, index - 1);
    }
    
    if (index < right) {
      quickSort(arr, index, right);
    }
  }
  
  return arr;
}

function partition(arr, left, right) {
  const pivot = arr[Math.floor((left + right) / 2)];
  let i = left;
  let j = right;
  
  while (i <= j) {
    while (arr[i] < pivot) {
      i++;
    }
    
    while (arr[j] > pivot) {
      j--;
    }
    
    if (i <= j) {
      [arr[i], arr[j]] = [arr[j], arr[i]];
      i++;
      j--;
    }
  }
  
  return i;
}

// 테스트용 배열
const arr = [64, 34, 25, 12, 22, 11, 90];
console.log("정렬 전:", arr);
console.log("정렬 후:", quickSort(arr));


 - 병합정렬 : O(n log n)

function mergeSort(arr) {
  if (arr.length <= 1) {
    return arr;
  }
  
  const mid = Math.floor(arr.length / 2);
  const left = arr.slice(0, mid);
  const right = arr.slice(mid);
  
  return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right) {
  let result = [];
  let i = 0;
  let j = 0;
  
  while (i < left.length && j < right.length) {
    if (left[i] < right[j]) {
      result.push(left[i]);
      i++;
    } else {
      result.push(right[j]);
      j++;
    }
  }
  
  while (i < left.length) {
    result.push(left[i]);
    i++;
  }
  
  while (j < right.length) {
    result.push(right[j]);
    j++;
  }
  
  return result;
}

// 테스트용 배열
const arr = [64, 34, 25, 12, 22, 11, 90];
console.log("정렬 전:", arr);
console.log("정렬 후:", mergeSort(arr));

 - 힙정렬 : O(n log n)

function heapSort(arr) {
  const length = arr.length;

  // 최대 힙을 구성합니다.
  for (let i = Math.floor(length / 2) - 1; i >= 0; i--) {
    heapify(arr, length, i);
  }

  // 힙 정렬을 수행합니다.
  for (let i = length - 1; i > 0; i--) {
    // 최대 힙의 루트(가장 큰 값)를 마지막 요소와 교환합니다.
    [arr[0], arr[i]] = [arr[i], arr[0]];

    // 힙의 크기를 하나씩 줄여가며 힙 속성을 유지합니다.
    heapify(arr, i, 0);
  }

  return arr;
}

function heapify(arr, heapSize, index) {
  let largest = index;
  const left = 2 * index + 1;
  const right = 2 * index + 2;

  // 왼쪽 자식 노드가 힙 범위 내에 있고 현재 값보다 크다면 largest를 업데이트합니다.
  if (left < heapSize && arr[left] > arr[largest]) {
    largest = left;
  }

  // 오른쪽 자식 노드가 힙 범위 내에 있고 현재 값보다 크다면 largest를 업데이트합니다.
  if (right < heapSize && arr[right] > arr[largest]) {
    largest = right;
  }

  // largest가 변경되었다면 현재 노드와 largest 위치의 값을 교환하고, 재귀적으로 heapify를 호출합니다.
  if (largest !== index) {
    [arr[index], arr[largest]] = [arr[largest], arr[index]];
    heapify(arr, heapSize, largest);
  }
}

// 테스트용 배열
const arr = [64, 34, 25, 12, 22, 11, 90];
console.log("정렬 전:", arr);
console.log("정렬 후:", heapSort(arr));



탐색
 - 이진탐색 : O(log n)

function binarySearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);

    if (arr[mid] === target) {
      return mid; // 타겟을 찾았을 때 인덱스 반환
    } else if (arr[mid] < target) {
      left = mid + 1; // 중간값보다 큰 범위를 탐색하기 위해 왼쪽 범위를 업데이트
    } else {
      right = mid - 1; // 중간값보다 작은 범위를 탐색하기 위해 오른쪽 범위를 업데이트
    }
  }

  return -1; // 타겟을 찾지 못한 경우 -1 반환
}

// 테스트용 배열
const arr = [11, 22, 34, 45, 56, 67, 78, 89, 90];
const target = 45;
console.log("배열:", arr);
console.log(`타겟 ${target}의 인덱스:`, binarySearch(arr, target));


 - 순차탐색 : O(n)

function sequentialSearch(arr, target) {
  const length = arr.length;
  
  for (let i = 0; i < length; i++) {
    if (arr[i] === target) {
      return i; // 타겟을 찾았을 때 인덱스 반환
    }
  }
  
  return -1; // 타겟을 찾지 못한 경우 -1 반환
}

// 테스트용 배열
const arr = [11, 22, 34, 45, 56, 67, 78, 89, 90];
const target = 45;
console.log("배열:", arr);
console.log(`타겟 ${target}의 인덱스:`, sequentialSearch(arr, target));



 그래프

   v : 정점, e : 간선
  - 너비우선탐색(BFS) : O(v+e)   

class Graph {
  constructor() {
    this.adjacencyList = new Map();
  }

  addVertex(vertex) {
    this.adjacencyList.set(vertex, []);
  }

  addEdge(v1, v2) {
    this.adjacencyList.get(v1).push(v2);
    this.adjacencyList.get(v2).push(v1);
  }

  bfs(startVertex) {
    const visited = new Set();
    const queue = [];

    visited.add(startVertex);
    queue.push(startVertex);

    while (queue.length > 0) {
      const currentVertex = queue.shift();
      console.log(currentVertex);

      const neighbors = this.adjacencyList.get(currentVertex);
      for (let neighbor of neighbors) {
        if (!visited.has(neighbor)) {
          visited.add(neighbor);
          queue.push(neighbor);
        }
      }
    }
  }
}

// 테스트
const graph = new Graph();
graph.addVertex("A");
graph.addVertex("B");
graph.addVertex("C");
graph.addVertex("D");
graph.addVertex("E");

graph.addEdge("A", "B");
graph.addEdge("A", "C");
graph.addEdge("B", "D");
graph.addEdge("C", "E");

graph.bfs("A");


  - 깊이우선탐색(DFS) : O(v+e)   

class Graph {
  constructor() {
    this.adjacencyList = new Map();
  }

  addVertex(vertex) {
    this.adjacencyList.set(vertex, []);
  }

  addEdge(v1, v2) {
    this.adjacencyList.get(v1).push(v2);
    this.adjacencyList.get(v2).push(v1);
  }

  dfsRecursive(startVertex) {
    const visited = new Set();

    this.dfsRecursiveHelper(startVertex, visited);
  }

  dfsRecursiveHelper(vertex, visited) {
    visited.add(vertex);
    console.log(vertex);

    const neighbors = this.adjacencyList.get(vertex);
    for (let neighbor of neighbors) {
      if (!visited.has(neighbor)) {
        this.dfsRecursiveHelper(neighbor, visited);
      }
    }
  }

  dfsIterative(startVertex) {
    const visited = new Set();
    const stack = [];

    stack.push(startVertex);

    while (stack.length > 0) {
      const currentVertex = stack.pop();

      if (!visited.has(currentVertex)) {
        visited.add(currentVertex);
        console.log(currentVertex);

        const neighbors = this.adjacencyList.get(currentVertex);
        for (let neighbor of neighbors) {
          stack.push(neighbor);
        }
      }
    }
  }
}

// 테스트
const graph = new Graph();
graph.addVertex("A");
graph.addVertex("B");
graph.addVertex("C");
graph.addVertex("D");
graph.addVertex("E");

graph.addEdge("A", "B");
graph.addEdge("A", "C");
graph.addEdge("B", "D");
graph.addEdge("C", "E");

graph.dfsRecursive("A");
// 또는
// graph.dfsIterative("A");


  - 다익스트라 알고리즘(최단경로) : O((V + E) log V)

function Graph() {
  this.adjacencyList = {};
}

Graph.prototype.addVertex = function (vertex) {
  this.adjacencyList[vertex] = {};
};

Graph.prototype.addEdge = function (v1, v2, weight) {
  this.adjacencyList[v1][v2] = weight;
  this.adjacencyList[v2][v1] = weight;
};

Graph.prototype.dijkstra = function (startVertex) {
  var distances = {};
  var visited = {};
  var queue = new PriorityQueue();

  // 모든 정점까지의 거리를 Infinity로 초기화
  for (var vertex in this.adjacencyList) {
    distances[vertex] = Infinity;
  }

  distances[startVertex] = 0;
  queue.enqueue(startVertex, 0);

  while (!queue.isEmpty()) {
    var currentVertex = queue.dequeue().element;

    if (visited[currentVertex]) {
      continue;
    }

    visited[currentVertex] = true;

    var neighbors = this.adjacencyList[currentVertex];
    for (var neighbor in neighbors) {
      var weight = neighbors[neighbor];
      var distance = distances[currentVertex] + weight;
      if (distance < distances[neighbor]) {
        distances[neighbor] = distance;
        queue.enqueue(neighbor, distance);
      }
    }
  }

  return distances;
};

function PriorityQueue() {
  this.queue = [];
}

PriorityQueue.prototype.enqueue = function (element, priority) {
  this.queue.push({ element: element, priority: priority });
  this.sort();
};

PriorityQueue.prototype.dequeue = function () {
  return this.queue.shift();
};

PriorityQueue.prototype.sort = function () {
  this.queue.sort(function (a, b) {
    return a.priority - b.priority;
  });
};

PriorityQueue.prototype.isEmpty = function () {
  return this.queue.length === 0;
};

// 테스트
var graph = new Graph();
graph.addVertex("A");
graph.addVertex("B");
graph.addVertex("C");
graph.addVertex("D");
graph.addVertex("E");

graph.addEdge("A", "B", 4);
graph.addEdge("A", "C", 2);
graph.addEdge("B", "D", 5);
graph.addEdge("C", "D", 1);
graph.addEdge("C", "E", 3);
graph.addEdge("D", "E", 7);

console.log(graph.dijkstra("A"));


  - 벨만포드 알고리즘(최단경로) : O(V * E)

function bellmanFord(graph, startVertex) {
  const vertices = Object.keys(graph);
  const distances = {};

  // 모든 정점까지의 거리를 Infinity로 초기화
  for (let vertex of vertices) {
    distances[vertex] = Infinity;
  }

  distances[startVertex] = 0;

  // 간선 개수만큼 반복하여 최단 거리 업데이트
  for (let i = 0; i < vertices.length - 1; i++) {
    let updated = false; // 업데이트 발생 여부

    for (let vertex of vertices) {
      for (let neighbor in graph[vertex]) {
        const weight = graph[vertex][neighbor];
        const distance = distances[vertex] + weight;
        if (distance < distances[neighbor]) {
          distances[neighbor] = distance;
          updated = true; // 업데이트가 발생했음을 표시
        }
      }
    }

    // 업데이트가 발생한 경우에 한하여 음수 사이클 확인
    if (i === vertices.length - 1 && updated) {
      for (let vertex of vertices) {
        for (let neighbor in graph[vertex]) {
          const weight = graph[vertex][neighbor];
          const distance = distances[vertex] + weight;
          if (distance < distances[neighbor]) {
            console.log("음수 사이클이 존재합니다.");
            return;
          }
        }
      }
    }
  }

  return distances;
}

// 그래프 정의
const graph = {
  A: { B: -1, C: 4 },
  B: { C: 3, D: 2, E: 2 },
  C: {},
  D: { B: 1, C: 5 },
  E: { D: -3 },
};

// 테스트
const startVertex = "A";
const shortestDistances = bellmanFord(graph, startVertex);
console.log(shortestDistances);


  - 크루스칼 알고리즘(최소 비용 신장 트리) : O(E log E + E)

class DisjointSet {
  constructor() {
    this.parent = {};
    this.rank = {};
  }

  makeSet(vertex) {
    this.parent[vertex] = vertex;
    this.rank[vertex] = 0;
  }

  find(vertex) {
    if (this.parent[vertex] !== vertex) {
      this.parent[vertex] = this.find(this.parent[vertex]);
    }
    return this.parent[vertex];
  }

  union(vertex1, vertex2) {
    const root1 = this.find(vertex1);
    const root2 = this.find(vertex2);

    if (root1 !== root2) {
      if (this.rank[root1] < this.rank[root2]) {
        this.parent[root1] = root2;
      } else if (this.rank[root1] > this.rank[root2]) {
        this.parent[root2] = root1;
      } else {
        this.parent[root2] = root1;
        this.rank[root1]++;
      }
    }
  }
}

function kruskal(graph) {
  const edges = [];
  const minimumSpanningTree = [];

  // 모든 간선 추출
  for (let vertex in graph) {
    for (let neighbor in graph[vertex]) {
      edges.push([vertex, neighbor, graph[vertex][neighbor]]);
    }
  }

  // 간선 가중치를 기준으로 정렬
  edges.sort((a, b) => a[2] - b[2]);

  const disjointSet = new DisjointSet();

  // 각 정점에 대해 독립적인 집합 생성
  for (let vertex in graph) {
    disjointSet.makeSet(vertex);
  }

  // 가장 작은 가중치 간선부터 선택
  for (let edge of edges) {
    const [vertex1, vertex2, weight] = edge;

    // 사이클이 생성되지 않는 경우에만 간선 선택
    if (disjointSet.find(vertex1) !== disjointSet.find(vertex2)) {
      disjointSet.union(vertex1, vertex2);
      minimumSpanningTree.push(edge);
    }
  }

  return minimumSpanningTree;
}

// 그래프 정의
const graph = {
  A: { B: 7, D: 5 },
  B: { A: 7, D: 9, C: 8, E: 7 },
  C: { B: 8, E: 5 },
  D: { A: 5, B: 9, E: 15, F: 6 },
  E: { B: 7, C: 5, D: 15, F: 8, G: 9 },
  F: { D: 6, E: 8, G: 11 },
  G: { E: 9, F: 11 },
};

// 테스트
const minimumSpanningTree = kruskal(graph);
console.log(minimumSpanningTree);


  - 프림 알고리즘(최소 비용 신장 트리) : O(E log V + V)

class Graph {
  constructor() {
    this.adjacencyList = new Map();
  }

  addVertex(vertex) {
    this.adjacencyList.set(vertex, new Map());
  }

  addEdge(v1, v2, weight) {
    this.adjacencyList.get(v1).set(v2, weight);
    this.adjacencyList.get(v2).set(v1, weight);
  }

  prim(startVertex) {
    const minimumSpanningTree = new Map();
    const visited = new Set();
    const priorityQueue = new PriorityQueue();

    // 시작 정점을 방문 처리
    visited.add(startVertex);

    // 시작 정점과 연결된 간선들을 우선순위 큐에 삽입
    const edges = this.adjacencyList.get(startVertex);
    for (let [vertex, weight] of edges.entries()) {
      priorityQueue.enqueue({ vertex, weight });
    }

    while (!priorityQueue.isEmpty()) {
      const { vertex, weight } = priorityQueue.dequeue();

      // 이미 방문한 정점이면 건너뛰기
      if (visited.has(vertex)) {
        continue;
      }

      // 정점을 방문 처리하고, 최소 비용 간선을 minimumSpanningTree에 추가
      visited.add(vertex);
      minimumSpanningTree.set(vertex, weight);

      // 현재 정점과 연결된 간선들을 우선순위 큐에 삽입
      const edges = this.adjacencyList.get(vertex);
      for (let [v, w] of edges.entries()) {
        if (!visited.has(v)) {
          priorityQueue.enqueue({ vertex: v, weight: w });
        }
      }
    }

    return minimumSpanningTree;
  }
}

// 우선순위 큐(Priority Queue) 구현
class PriorityQueue {
  constructor() {
    this.queue = [];
  }

  enqueue({ vertex, weight }) {
    this.queue.push({ vertex, weight });
    this.sort();
  }

  dequeue() {
    return this.queue.shift();
  }

  sort() {
    this.queue.sort((a, b) => a.weight - b.weight);
  }

  isEmpty() {
    return this.queue.length === 0;
  }
}

// 그래프 생성
const graph = new Graph();
graph.addVertex("A");
graph.addVertex("B");
graph.addVertex("C");
graph.addVertex("D");
graph.addVertex("E");

graph.addEdge("A", "B", 4);
graph.addEdge("A", "C", 2);
graph.addEdge("B", "D", 5);
graph.addEdge("C", "D", 1);
graph.addEdge("C", "E", 3);
graph.addEdge("D", "E", 7);

// 테스트
const minimumSpanningTree = graph.prim("A");
console.log(minimumSpanningTree);


  - 개선된 프림 알고리즘(최소 비용 신장 트리) : O(E log E + V) 

          * 기존 우선순위 큐를 힙 자료구조를 사용하여 가장 작은 가중치의 간선을 선택하여 속도를 개선

 

class Graph {
  constructor() {
    this.adjacencyList = new Map();
  }

  addVertex(vertex) {
    this.adjacencyList.set(vertex, new Map());
  }

  addEdge(v1, v2, weight) {
    this.adjacencyList.get(v1).set(v2, weight);
    this.adjacencyList.get(v2).set(v1, weight);
  }

  prim(startVertex) {
    const minimumSpanningTree = new Map();
    const visited = new Set();
    const heap = new Heap();

    // 시작 정점을 방문 처리
    visited.add(startVertex);

    // 시작 정점과 연결된 간선들을 힙에 삽입
    const edges = this.adjacencyList.get(startVertex);
    for (let [vertex, weight] of edges.entries()) {
      heap.insert({ vertex, weight });
    }

    while (!heap.isEmpty()) {
      const { vertex, weight } = heap.deleteMin();

      // 이미 방문한 정점이면 건너뛰기
      if (visited.has(vertex)) {
        continue;
      }

      // 정점을 방문 처리하고, 최소 비용 간선을 minimumSpanningTree에 추가
      visited.add(vertex);
      minimumSpanningTree.set(vertex, weight);

      // 현재 정점과 연결된 간선들을 힙에 삽입
      const edges = this.adjacencyList.get(vertex);
      for (let [v, w] of edges.entries()) {
        if (!visited.has(v)) {
          heap.insert({ vertex: v, weight: w });
        }
      }
    }

    return minimumSpanningTree;
  }
}

// 힙(Heap) 구현
class Heap {
  constructor() {
    this.heap = [];
  }

  insert(element) {
    this.heap.push(element);
    this.heapifyUp(this.heap.length - 1);
  }

  deleteMin() {
    const min = this.heap[0];
    const last = this.heap.pop();

    if (this.heap.length > 0) {
      this.heap[0] = last;
      this.heapifyDown(0);
    }

    return min;
  }

  heapifyUp(index) {
    const parentIndex = Math.floor((index - 1) / 2);

    if (parentIndex >= 0 && this.heap[parentIndex].weight > this.heap[index].weight) {
      [this.heap[parentIndex], this.heap[index]] = [this.heap[index], this.heap[parentIndex]];
      this.heapifyUp(parentIndex);
    }
  }

  heapifyDown(index) {
    const leftChildIndex = 2 * index + 1;
    const rightChildIndex = 2 * index + 2;
    let smallestIndex = index;

    if (leftChildIndex < this.heap.length && this.heap[leftChildIndex].weight < this.heap[smallestIndex].weight) {
      smallestIndex = leftChildIndex;
    }

    if (rightChildIndex < this.heap.length && this.heap[rightChildIndex].weight < this.heap[smallestIndex].weight) {
      smallestIndex = rightChildIndex;
    }

    if (smallestIndex !== index) {
      [this.heap[index], this.heap[smallestIndex]] = [this.heap[smallestIndex], this.heap[index]];
      this.heapifyDown(smallestIndex);
    }
  }

  isEmpty() {
    return this.heap.length === 0;
  }
}

// 그래프 생성
const graph = new Graph();
graph.addVertex("A");
graph.addVertex("B");
graph.addVertex("C");
graph.addVertex("D");
graph.addVertex("E");

graph.addEdge("A", "B", 4);
graph.addEdge("A", "C", 2);
graph.addEdge("B", "D", 5);
graph.addEdge("C", "D", 1);
graph.addEdge("C", "E", 3);
graph.addEdge("D", "E", 7);

// 테스트
const minimumSpanningTree = graph.prim("A");
console.log(minimumSpanningTree);


 

 문제 해결전략
  - 재귀호출 : O(k^n) :  n은 재귀 호출의 횟수이고, k는 각 재귀 호출 당 수행되는 연산의 수

  • 함수가 자기 자신을 호출하여 문제를 해결하는 방법입니다.
  • 주어진 문제를 더 작은 하위 문제로 분할하고, 재귀적으로 해결합니다.
  • 종료 조건을 설정하여 재귀 호출을 멈추게 합니다.


  - 동적계획법 : O(n * m), 여기서 n은 하위 문제의 개수이고 m은 각 하위 문제의 연산 시간 복잡도

  • 큰 문제를 작은 하위 문제로 나누어 해결하고, 중복되는 계산을 피하는 방법입니다.
  • 작은 하위 문제의 해를 저장하고, 이를 활용하여 큰 문제를 해결합니다.
  • Memoization(메모이제이션) 기법을 사용하여 중복 계산을 피합니다.

 

- 분할정복

  • 문제를 더 작은 하위 문제로 분할하고, 각 하위 문제를 독립적으로 해결합니다.
  • 작은 문제의 해를 결합하여 원래 문제의 해를 얻습니다.
  • 재귀 호출을 사용하여 문제를 분할하고 해결합니다.

 

- 탐욕 알고리즘

  • 각 단계에서 지금 당장 가장 좋은 선택을 하는 알고리즘입니다.
  • 최적해를 구하는 것이 목표이지만, 항상 최적해를 보장하지는 않습니다.
  • 현재 상황에서 가장 유리한 선택을 반복하여 전체적인 최적해를 찾습니다.

 

- 백트래킹

  • 해를 찾기 위해 모든 가능한 후보군을 탐색하고, 불필요한 후보군은 배제합니다.
  • 한 단계씩 진행하다가 현재 상태에서 해를 찾지 못하면 이전 단계로 되돌아가 다른 후보를 탐색합니다.
  • DFS(Depth-First Search)와 재귀 호출을 활용합니다.


반응형