알고리즘 정리

Yuno·2024년 6월 28일

1. 정렬 알고리즘 (Sorting Algorithms)

버블 정렬 (Bubble Sort)

  • 기본 개념 : 인접한 두 요소를 비교하여 큰 값을 뒤로 보냄. 이 과정을 반복하여 정렬
  • 장점 : 구현이 매우 간단함
  • 단점 : 매우 비효율적. 시간 복잡도가 높아 큰 데이터셋에는 적합하지 않음
  • 시간 복잡도 : O(n^2)
  • 사용 예시 : 단순한 교육용 예제, 데이터가 매우 적을 때
public void bubbleSort(int[] arr) {
    int n = arr.length;
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                // Swap arr[j] and arr[j+1]
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

삽입 정렬 (Insertion Sort)

  • 기본 개념 : 정렬된 부분과 정렬되지 않은 부분으로 나누어, 정렬되지 않은 요소를 하나씩 꺼내어 정렬된 부분에 삽입
  • 장점 : 구현이 간단하고, 대부분의 요소가 이미 정렬된 경우 효율적
  • 단점 : 시간 복잡도가 높아 큰 데이터셋에는 적합하지 않음
  • 시간 복잡도 : O(n^2)
  • 사용 예시 : 작은 데이터셋, 대부분 정렬된 데이터
public void insertionSort(int[] arr) {
    int n = arr.length;
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j = j - 1;
        }
        arr[j + 1] = key;
    }
}

선택 정렬 (Selection Sort)

  • 기본 개념 : 가장 작은 요소를 찾아 정렬되지 않은 부분의 맨 앞에 놓음. 이 과정을 반복
  • 장점 : 구현이 간단함
  • 단점 : 시간 복잡도가 높아 큰 데이터셋에는 적합하지 않음
  • 시간 복잡도 : O(n^2)
  • 사용 예시 : 단순한 교육용 예제, 데이터가 매우 적을 때
public void selectionSort(int[] arr) {
    int n = arr.length;
    for (int i = 0; i < n - 1; i++) {
        int minIdx = i;
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIdx]) {
                minIdx = j;
            }
        }
        int temp = arr[minIdx];
        arr[minIdx] = arr[i];
        arr[i] = temp;
    }
}

병합 정렬 (Merge Sort)

  • 기본 개념 : 배열을 반으로 나누어 각각을 정렬한 후 병합. 분할 정복 기법을 사용
  • 장점 : 안정 정렬, 시간 복잡도가 항상 일정함
  • 단점 : 추가적인 메모리 공간이 필요
  • 시간 복잡도 : O(n log n)
  • 사용 예시 : 안정 정렬이 필요할 때, 대용량 데이터
public void mergeSort(int[] arr, int left, int right) {
    if (left < right) {
        int middle = (left + right) / 2;
        mergeSort(arr, left, middle);
        mergeSort(arr, middle + 1, right);
        merge(arr, left, middle, right);
    }
}

public void merge(int[] arr, int left, int middle, int right) {
    int n1 = middle - left + 1;
    int n2 = right - middle;
    int[] L = new int[n1];
    int[] R = new int[n2];
    for (int i = 0; i < n1; i++) {
        L[i] = arr[left + i];
    }
    for (int j = 0; j < n2; j++) {
        R[j] = arr[middle + 1 + j];
    }
    int i = 0, j = 0;
    int k = left;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }
    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }
    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }
}

퀵 정렬 (Quick Sort)

  • 기본 개념 : 피벗을 선택해 배열을 분활하고, 각 부분을 재귀적으로 정렬
  • 장점 : 평균적으로 매우 빠름
  • 단점 : 최악의 경우 시간 복잡도가 높음(하지만 최악의 경우는 거의 발생X)
  • 시간 복잡도 : O(n log n), 최악 O(n^2)
  • 사용 예시 : 대용량 데이터, 일반적인 정렬 작업
public void quickSort(int[] arr, int low, int high) {
    if (low < high) {
        int pi = partition(arr, low, high);
        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}

public int partition(int[] arr, int low, int high) {
    int pivot = arr[high];
    int i = (low - 1);
    for (int j = low; j < high; j++) {
        if (arr[j] < pivot) {
            i++;
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    int temp = arr[i + 1];
    arr[i + 1] = arr[high];
    arr[high] = temp;
    return i + 1;
}

힙 정렬 (Heap Sort)

  • 기본 개념 : 힙 자료구조를 이용해 정렬. 이진 힙을 사용해 최대 힙이나 최소 힙을 구성
  • 장점 : 추가 메모리 사용이 적음
  • 단점 : 비교적 느릴 수 있음
  • 시간 복잡도 : O(n log n)
  • 사용 예시 : 메모리 사용이 중요한 경우
public void heapSort(int[] arr) {
    int n = arr.length;
    for (int i = n / 2 - 1; i >= 0; i--) {
        heapify(arr, n, i);
    }
    for (int i = n - 1; i >= 0; i--) {
        int temp = arr[0];
        arr[0] = arr[i];
        arr[i] = temp;
        heapify(arr, i, 0);
    }
}

public void heapify(int[] arr, int n, int i) {
    int largest = i;
    int left = 2 * i + 1;
    int right = 2 * i + 2;
    if (left < n && arr[left] > arr[largest]) {
        largest = left;
    }
    if (right < n && arr[right] > arr[largest]) {
        largest = right;
    }
    if (largest != i) {
        int swap = arr[i];
        arr[i] = arr[largest];
        arr[largest] = swap;
        heapify(arr, n, largest);
    }
}

2. 탐색 알고리즘 (Searching Algorithms)

  • 기본 개념 : 배열의 첫 요소부터 순차적으로 탐색
  • 장점 : 구현이 간단하고, 데이터가 정렬될 필요가 없음
  • 단점 : 시간 복잡도가 높음
  • 시간 복잡도 : O(n)
  • 사용 예시 : 데이터가 적거나, 정렬되지 않은 배열
public int linearSearch(int[] arr, int x) {
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] == x) {
            return i;
        }
    }
    return -1;
}
  • 기본 개념 : 정렬된 배열에서 중간 요소를 기준으로 반씩 나누어 탐색
  • 장점 : 매우 빠름
  • 단점 : 배열이 정렬되어 있어야 함
  • 시간 복잡도 : O(log n)
  • 사용 예시 : 정렬된 배열에서의 탐색
public int binarySearch(int[] arr, int x) {
    int left = 0, right = arr.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] == x) {
            return mid;
        }
        if (arr[mid] < x) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return -1;
}

3. 그래프 알고리즘 (Graph Algorithms)

  • 기본 개념 : 한 정점에서 시작해 갈 수 있는 한 깊이 들어가며 탐색
  • 장점 : 구현이 간단하고, 경로를 쉽게 찾을 수 있음
  • 단점 : 스택 오버플로우의 위험이 있음
  • 시간 복잡도 : O(V + E) (V : 정점의 수, E : 간선의 수)
  • 사용 예시 : 경로 찾기, 미로탐색
public void DFS(int v, boolean[] visited, List<List<Integer>> adjList) {
    visited[v] = true;
    System.out.print(v + " ");
    for (int i : adjList.get(v)) {
        if (!visited[i]) {
            DFS(i, visited, adjList);
        }
    }
}
  • 기본 개념 : 한 정점에서 시작해 인접한 모든 정점을 탐색한 후, 다음 레벨로 넘어감
  • 장점 : 최단 경로를 찾을수 있음
  • 단점 : 큐가 필요하며, 메모리 사용이 많을 수 있음
  • 시간 복잡도 : O(V + E) (V : 정점의 수, E : 간선의 수)
  • 사용 예시 : 최단 경로 찾기
public void BFS(int start, int V, List<List<Integer>> adjList) {
    boolean[] visited = new boolean[V];
    LinkedList<Integer> queue = new LinkedList<>();
    visited[start] = true;
    queue.add(start);
    while (queue.size() != 0) {
        start = queue.poll();
        System.out.print(start + " ");
        for (int i : adjList.get(start)) {
            if (!visited[i]) {
                visited[i] = true;
                queue.add(i);
            }
        }
    }
}

다익스트라 알고리즘 (Dijkstra`s Algorithm)

  • 기본 개념 : 시작 정점에서 다른 모든 정점까지의 최단 경로를 찾음
  • 장점 : 음의 가중치가 없는 그래프에서 최적
  • 단점 : 음의 가중치를 처리할 수 없음
  • 시간 복잡도 : O(V^2) 또는 O(E log V) (우선순의 큐 사용시)
  • 사용 예시 : 네트워크 라우팅
public int[] dijkstra(int V, List<List<Node>> adjList, int start) {
    int[] dist = new int[V];
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[start] = 0;
    PriorityQueue<Node> pq = new PriorityQueue<>(V, new Node());
    pq.add(new Node(start, 0));
    while (!pq.isEmpty()) {
        int u = pq.poll().node;
        for (Node neighbor : adjList.get(u)) {
            int v = neighbor.node;
            int weight = neighbor.cost;
            if (dist[u] + weight < dist[v]) {
                dist[v] = dist[u] + weight;
                pq.add(new Node(v, dist[v]));
            }
        }
    }
    return dist;
}

class Node implements Comparator<Node> {
    public int node, cost;
    public Node() {}
    public Node(int node, int cost) {
        this.node = node;
        this.cost = cost;
    }
    @Override
    public int compare(Node n1, Node n2) {
        return Integer.compare(n1.cost, n2.cost);
    }
}

벨만 - 포드 알고리즘 (Bellman - Ford Algorithm)

  • 기본 개념 : 시작 정점에서 다른 모든 정점까지의 최단 경로를 찾음
  • 장점 : 음의 가중치를 처리할 수 있음
  • 단점 : 시간 복잡도가 높음
  • 시간 복잡도 : O(VE)
  • 사용 예시 : 음의 가중치가 있는 그래프에서의 최단 경로
public void bellmanFord(int V, int E, int[][] edges, int start) {
    int[] dist = new int[V];
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[start] = 0;
    for (int i = 0; i < V - 1; i++) {
        for (int j = 0; j < E; j++) {
            int u = edges[j][0];
            int v = edges[j][1];
            int weight = edges[j][2];
            if (dist[u] != Integer.MAX_VALUE && dist[u] + weight < dist[v]) {
                dist[v] = dist[u] + weight;
            }
        }
    }
    for (int j = 0; j < E; j++) {
        int u = edges[j][0];
        int v = edges[j][1];
        int weight = edges[j][2];
        if (dist[u] != Integer.MAX_VALUE && dist[u] + weight < dist[v]) {
            System.out.println("Graph contains negative weight cycle");
        }
    }
}

플로이드 - 워셜 알고리즘 (Floyd - Warshall Algorithm)

  • 기본 개념 : 모든 정점 간의 최단 경로를 찾음
  • 장점 : 음의 가중치도 처리할 수 있음
  • 단점 : 시간 복잡도가 높음
  • 시간 복잡도 : O(V^3)
  • 사용 예시 : 모든 정점 간의 최단 경로 필요 시
public void floydWarshall(int V, int[][] graph) {
    int[][] dist = new int[V][V];
    for (int i = 0; i < V; i++) {
        for (int j = 0; j < V; j++) {
            dist[i][j] = graph[i][j];
        }
    }
    for (int k = 0; k < V; k++) {
        for (int i = 0; i < V; i++) {
            for (int j = 0; j < V; j++) {
                if (dist[i][k] + dist[k][j] < dist[i][j]) {
                    dist[i][j] = dist[i][k] + dist[k][j];
                }
            }
        }
    }
    for (int i = 0; i < V; i++) {
        for (int j = 0; j < V; j++) {
            if (dist[i][j] == Integer.MAX_VALUE) {
                System.out.print("INF ");
            } else {
                System.out.print(dist[i][j] + " ");
            }
        }
        System.out.println();
    }
}

4. 분할 정복 (Divide and Conquer)

병합 정렬 (Merge Sort)

  • 기본 개념 : 배열을 반으로 나누어 각각을 정렬한 후 병합
  • 장점 : 안정 정렬, 시간 복잡도가 일정
  • 단점 : 추가 메모리가 필요
  • 시간 복잡도 : O(n log n)
  • 사용 예시 : 안정 정렬이 필요할 때
    ex) 코드는 <1. 정렬 알고리즘> 참조

퀵 정렬 (Quick Sort)

  • 기본 개념 : 피벗을 선택해 배열을 분활하고, 각 부분을 재귀적으로 정렬
  • 장점 : 평균적으로 매우 빠름
  • 단점 : 최악의 경우 시간 복잡도가 높음(하지만 최악의 경우는 거의 발생X)
  • 시간 복잡도 : O(n log n), 최악 O(n^2)
  • 사용 예시 : 대용량 데이터, 일반적인 정렬 작업
    ex) 코드는 <1. 정렬 알고리즘> 참조

최대 부분 배열 문제 (Maximum Subarray Problem)

  • 기본 개념 : 분할 정복 기법을 사용하여 배열 내 최대 합을 갖는 부분 배열을 찾음
  • 장점 : 효율적임
  • 단점 : 구현이 복잡할 수 있음
  • 시간 복잡도 : O(n log n)
  • 사용 예시 : 주식 가격 변동 분석
public int maxSubArraySum(int[] arr, int left, int right) {
    if (left == right) {
        return arr[left];
    }
    int mid = (left + right) / 2;
    return Math.max(Math.max(maxSubArraySum(arr, left, mid),
                             maxSubArraySum(arr, mid + 1, right)),
                    maxCrossingSum(arr, left, mid, right));
}

public int maxCrossingSum(int[] arr, int left, int mid, int right) {
    int sum = 0;
    int leftSum = Integer.MIN_VALUE;
    for (int i = mid; i >= left; i--) {
        sum += arr[i];
        if (sum > leftSum) {
            leftSum = sum;
        }
    }
    sum = 0;
    int rightSum = Integer.MIN_VALUE;
    for (int i = mid + 1; i <= right; i++) {
        sum += arr[i];
        if (sum > rightSum) {
            rightSum = sum;
        }
    }
    return leftSum + rightSum;
}

분할 정복 알고리즘의 장단점

  1. 장점
  • 문제를 작은 부분으로 나누어 해결하기 때문에 복잡한 문제를 쉽게 해결할 수 있음.
  • 재귀적 접근으로 코드가 간결해짐
  • 특정 문제에 대해 시간 복잡도가 감소
  1. 단점
  • 재귀 호출이 많아 스택 오버플로우가 발생할 수 있음
  • 추가적인 메모리 공간이 필요할 수 있음

5. 그리디 알고리즘 (Greedy Algorithms)

그리디 알고리즘 개념

  • 기본 개념 : 현재 상황에서 가장 좋아 보이는 선택을 함
  • 장점 : 구현이 간단하고, 특정 문제에 대해 빠르게 최적해를 찾을 수 있음
  • 단점 : 항상 최적해를 보장하지 않음
  • 사용 예시 : 최소 스패닝 트리, 최단 경로 문제 (특정 경우), 활동 선택 문제

활동 선택 문제 (Activity Selection Problem)

  • 기본 개념 : 시작 시간과 종료 시간이 주어진 활동 중 가장 많은 활동을 선택
  • 시간 복잡도 : O(n log n) (정렬 포함)
  • 사용 예시 : 스케줄링 문제
public void activitySelection(int[] start, int[] finish, int n) {
    Arrays.sort(finish);
    int i = 0;
    System.out.print("Selected activities: " + i + " ");
    for (int j = 1; j < n; j++) {
        if (start[j] >= finish[i]) {
            System.out.print(j + " ");
            i = j;
        }
    }
}

최소 스패닝 트리 (Minimum Spanning Tree)

크루스칼 알고리즘 (Kruskal`s Algorithm)

  • 기본 개념 : 간선을 오름차순으로 정렬한 후, 최소 비용으로 연결
  • 시간 복잡도 : O(E log E)
  • 사용 예시 : 네트워크 연결 문제
class Edge implements Comparable<Edge> {
    int src, dest, weight;
    public int compareTo(Edge compareEdge) {
        return this.weight - compareEdge.weight;
    }
}

class Graph {
    int V, E;
    Edge[] edge;
    Graph(int V, int E) {
        this.V = V;
        this.E = E;
        edge = new Edge[E];
        for (int i = 0; i < E; i++) {
            edge[i] = new Edge();
        }
    }

    int find(int[] parent, int i) {
        if (parent[i] == i) {
            return i;
        }
        return find(parent, parent[i]);
    }

    void union(int[] parent, int[] rank, int x, int y) {
        int xroot = find(parent, x);
        int yroot = find(parent, y);
        if (rank[xroot] < rank[yroot]) {
            parent[xroot] = yroot;
        } else if (rank[xroot] > rank[yroot]) {
            parent[yroot] = xroot;
        } else {
            parent[yroot] = xroot;
            rank[xroot]++;
        }
    }

    void KruskalMST() {
        Edge[] result = new Edge[V];
        int e = 0;
        int i = 0;
        for (i = 0; i < V; i++) {
            result[i] = new Edge();
        }
        Arrays.sort(edge);
        int[] parent = new int[V];
        int[] rank = new int[V];
        for (int v = 0; v < V; v++) {
            parent[v] = v;
            rank[v] = 0;
        }
        i = 0;
        while (e < V - 1) {
            Edge next_edge = edge[i++];
            int x = find(parent, next_edge.src);
            int y = find(parent, next_edge.dest);
            if (x != y) {
                result[e++] = next_edge;
                union(parent, rank, x, y);
            }
        }
        for (i = 0; i < e; i++) {
            System.out.println(result[i].src + " -- " + result[i].dest + " == " + result[i].weight);
        }
    }
}

6. 동적 계획법 (Dynamic Programming)

동적 계획법 개념

  • 기본 개념 : 문제를 작은 부분 문제로 나누어, 그 결과를 저장하고 재활용
  • 장점 : 중복 계산을 피할 수 있음.
  • 단점 : 추가적인 메모리 공간이 필요
  • 사용 예시 : 피보나치 수열, 배낭 문제, 최장 공통 부분 수열 (LCS)

피보나치 수열 (Fibonacci Sequence)

  • 시간 복잡도 : O(n)
  • 사용 예시 : 중복 계산을 피하기 위해
public int fibonacci(int n) {
    int[] fib = new int[n + 1];
    fib[0] = 0;
    fib[1] = 1;
    for (int i = 2; i <= n; i++) {
        fib[i] = fib[i - 1] + fib[i - 2];
    }
    return fib[n];
}

배낭 문제 (Knapsack Priblem)

  • 기본 개념 : 주어진 무게 제한 내에서 최대 가치를 갖는 물건을 선택
  • 시간 복잡도 : O(nW) (n : 물건의 수, W : 무게 한도)
  • 사용 예시 : 자원 할당 문제
public int knapSack(int W, int[] wt, int[] val, int n) {
    int[][] K = new int[n + 1][W + 1];
    for (int i = 0; i <= n; i++) {
        for (int w = 0; w <= W; w++) {
            if (i == 0 || w == 0) {
                K[i][w] = 0;
            } else if (wt[i - 1] <= w) {
                K[i][w] = Math.max(val[i - 1] + K[i - 1][w - wt[i - 1]], K[i - 1][w]);
            } else {
                K[i][w] = K[i - 1][w];
            }
        }
    }
    return K[n][W];
}
profile
Hello World

0개의 댓글