sort

정렬 알고리즘

컴퓨터 과학과 수학에서 정렬 알고리즘(sorting algorithm)이란 원소들을 번호순이나 사전 순서와 같이 일정한 순서대로 열거하는 알고리즘이다. 효율적인 정렬은 탐색이나 병합 알고리즘처럼 (정렬된 리스트에서 바르게 동작하는) 다른 알고리즘을 최적화하는 데 중요하다. 또 정렬 알고리즘은 데이터의 정규화나 의미있는 결과물을 생성하는 데 흔히 유용하게 쓰인다. 이 알고리즘의 결과는 반드시 다음 두 조건을 만족해야 한다.

  1. 출력은 비 내림차순(각각의 원소가 전 순서 원소에 비해 이전의 원소보다 작지 않은 순서)이다.
  2. 출력은 입력을 재배열하여 만든 순열이다.

분류

  • 원소들의 크기 비교에 따른 계산 복잡도. 직렬 정렬 알고리즘의 경우 최선 동작은 O(n log n), 최선 동작 중 병렬 정렬은 O((log n)^2), 최악 동작은 O(n^2)이다.
  • 일반적인 방식 : 삽입, 교환, 선택, 병합 등. 교환 정렬에는 거품 정렬과 퀵 정렬이 있다. 선택 정렬에는 셰이커 정렬과 힙 정렬이 있다.

비교 정렬

비교 정렬은 O(n log n)보다 더 나은 성능을 낼 수 없다.

  1. 삽입 정렬 : 자료 배열의 모든 요소를 앞에서부터 차례대로 이미 정렬된 배열 부분과 비교하여, 자신의 위치를 찾아 삽입함으로써 정렬을 완성하는 알고리즘이다. 시간 복잡도는 O(n^2)이다.
1
2
3
4
5
6
7
8
9
10
11
12
void insertionSort(int[] arr)
{
for(int index = 1 ; index < arr.length ; index++){
int temp = arr[index];
int aux = index - 1;
while( (aux >= 0) && ( arr[aux] > temp ) ) {
arr[aux+1] = arr[aux];
aux--;
}
arr[aux + 1] = temp;
}
}
  1. 교환 정렬 (버블 정렬) : 두 인접한 원소를 검사하여 정렬하는 방법이다. 시간 복잡도가 O(n^2)로 상당히 느리지만, 코드가 단순하기 때문에 자주 사용된다. 원소의 이동이 거품이 수면으로 올라오는 듯한 모습을 보이기 때문에 지어진 이름이다. 양방향으로 번갈아 수행하면 칵테일 정렬이 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
void bubbleSort(int[] arr) {
int temp = 0;
for(int i = 0; i < arr.length; i++) {
for(int j= 1 ; j < arr.length-i; j++) {
if(arr[j]<arr[j-1]) {
temp = arr[j-1];
arr[j-1] = arr[j];
arr[j] = temp;
}
}
}
System.out.println(Arrays.toString(arr));
}
  1. 선택 정렬(Selection Sort) : 제자리 정렬 알고리즘의 하나로, 다음과 같은 순서로 이루어진다.

    • 주어진 리스트 중에 최솟값을 찾는다.
    • 그 값을 맨 앞에 위치한 값과 교체한다.
    • 맨 처음 위치를 뺀 나머지 리스트를 같은 방법으로 교체한다.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      void selectionSort(int[] list) {
      int indexMin, temp;

      for (int i = 0; i < list.length - 1; i++) {
      indexMin = i;
      for (int j = i + 1; j < list.length; j++) {
      if (list[j] < list[indexMin]) {
      indexMin = j;
      }
      }
      temp = list[indexMin];
      list[indexMin] = list[i];
      list[i] = temp;
      }
      }
  2. 병합 정렬 (Merge Sort) : O(n log n) 비교 기반 정렬 알고리즘이다. 일반적인 방법으로 구현했을 때 이 정렬은 안정 정렬에 속하며, 분할 정복 알고리즘의 하나이다.

    • 정렬되지 않은 리스트를 각각 하나의 원소만 포함하는 n개의 부분리스트로 분할한다.(한 원소만 든 리스트는 정렬된 것과 같으므로)
    • 부분리스트가 하나만 남을 때까지 반복해서 병합하며 정렬된 부분리스트를 생성한다. 마지막 남은 부분리스트가 정렬된 리스트이다.
    1. 리스트의 길이가 1 이하이면 이미 정렬된 것으로 본다. 그렇지 않은 경우에는
    2. 분할(divide) : 정렬되지 않은 리스트를 절반으로 잘라 비슷한 크기의 두 부분 리스트로 나눈다.
    3. 정복(conquer) : 각 부분 리스트를 재귀적으로 합병 정렬을 이용해 정렬한다.
    4. 결합(combine) : 두 부분 리스트를 다시 하나의 정렬된 리스트로 합병한다. 이때 정렬 결과가 임시배열에 저장된다.
    5. 복사(copy) : 임시 배열에 저장된 결과를 원래 배열에 복사한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// array A[] has the items to sort; array B[] is a work array
void BottomUpMergeSort(A[], B[], n)
{
for (width = 1; width < n; width = 2 * width)
{
for (i = 0; i < n; i = i + 2 * width)
{
// Merge two runs: A[i:i+width-1] and A[i+width:i+2*width-1] to B[]
// or copy A[i:n-1] to B[] ( if(i+width >= n) )
BottomUpMerge(A, i, min(i+width, n), min(i+2*width, n), B);
}
CopyArray(B, A, n);
}
}

// Left run is A[iLeft :iRight-1].
// Right run is A[iRight:iEnd-1 ].
void BottomUpMerge(A[], iLeft, iRight, iEnd, B[])
{
i = iLeft, j = iRight;
// While there are elements in the left or right runs...
for (k = iLeft; k < iEnd; k++) {
// If left run head exists and is <= existing right run head.
if (i < iRight && (j >= iEnd || A[i] <= A[j])) {
B[k] = A[i];
i = i + 1;
} else {
B[k] = A[j];
j = j + 1;
}
}
}

void CopyArray(B[], A[], n)
{
for(i = 0; i < n; i++)
A[i] = B[i];
}
  1. 퀵 정렬(quick sort) : 분할 정복 방법을 통해 리스트를 정렬한다. 퀵 정렬은 n개의 데이터를 정렬할 때, 최악의 경우에는 O(n2)번의 비교를 수행하고, 평균적으로 O(n log n)번의 비교를 수행한다.
    • 리스트 가운데서 하나의 원소를 고른다. 이렇게 고른 원소를 피벗이라고 한다.
    • 피벗 앞에는 피벗보다 값이 작은 모든 원소들이 오고, 피벗 뒤에는 피벗보다 값이 큰 모든 원소들이 오도록 피벗을 기준으로 리스트를 둘로 나눈다. 이렇게 리스트를 둘로 나누는 것을 분할이라고 한다. 분할을 마친 뒤에 피벗은 더 이상 움직이지 않는다.
    • 분할된 두 개의 작은 리스트에 대해 재귀적으로 이 과정을 반복한다. 재귀는 리스트의 크기가 0이나 1이 될 때까지 반복된다.

참조“위키백과”


1. K번째 수


  • 문제 설명
    배열 array의 i번째 숫자부터 j번째 숫자까지 자르고 정렬했을 때, k번째에 있는 수를 구하려 합니다.

예를 들어 array가 [1, 5, 2, 6, 3, 7, 4], i = 2, j = 5, k = 3이라면

array의 2번째부터 5번째까지 자르면 [5, 2, 6, 3]입니다.
1에서 나온 배열을 정렬하면 [2, 3, 5, 6]입니다.
2에서 나온 배열의 3번째 숫자는 5입니다.
배열 array, [i, j, k]를 원소로 가진 2차원 배열 commands가 매개변수로 주어질 때, commands의 모든 원소에 대해 앞서 설명한 연산을 적용했을 때 나온 결과를 배열에 담아 return 하도록 solution 함수를 작성해주세요.

  • 제한사항
    array의 길이는 1 이상 100 이하입니다.
    array의 각 원소는 1 이상 100 이하입니다.
    commands의 길이는 1 이상 50 이하입니다.
    commands의 각 원소는 길이가 3입니다.
  • 입출력 예
    array commands return
    [1, 5, 2, 6, 3, 7, 4] [[2, 5, 3], [4, 4, 1], [1, 7, 3]] [5, 6, 3]
    입출력 예 설명
    [1, 5, 2, 6, 3, 7, 4]를 2번째부터 5번째까지 자른 후 정렬합니다. [2, 3, 5, 6]의 세 번째 숫자는 5입니다.
    [1, 5, 2, 6, 3, 7, 4]를 4번째부터 4번째까지 자른 후 정렬합니다. [6]의 첫 번째 숫자는 6입니다.
    [1, 5, 2, 6, 3, 7, 4]를 1번째부터 7번째까지 자릅니다. [1, 2, 3, 4, 5, 6, 7]의 세 번째 숫자는 3입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.*;
class Solution {
public int[] solution(int[] array, int[][] commands) {
int[] answer = new int [commands.length];
int[] temp = {};
int cnt=0;
for(int [] command : commands){
temp = Arrays.copyOfRange(array, command[0]-1, command[1]);
Arrays.sort(temp);
answer[cnt++] = temp[command[2]-1];
}

return answer;
}
}
  • 후기 : 풀고보니 이전에도 한번 풀었던 문제였다. 어떻게 똑같은 메소드를 사용해서 푼걸 보면 신기하다.

2. 가장 큰 수


3. H-Index


Data Structure

자료구조


  • 상황과 문맥에 맞게 데이터를 담을 수 있는 적절한 구조를 말하며, 데이터에 편리하게 접근하고 조작하기 위한 방법이다.
  • 자료 구조는 크게 단순 구조와 비단순 구조로 나뉜다. 단순구조는 프로그래밍에서 사용되는 기본 데이터 타입을 의미하며, 비단순 구조는 단순한 데이터를 저장하는 구조가 아니라 여러 데이터를 목적에 맞게 효과적으로 저장하는 자료구조 이며, 선형구조와 비선형 구조로 나뉜다.
    1
    2
    * 선형구조 : 저장되는 자료의 전후 관계가 1대1(List, Stack, Queue)
    * 비선형구조 : 데이터 항목 사이의 관계가 1:n 도는 n:m(Graph, Tree)

    Array & LinkedList


  • Array
    자료구조의 기본이라고 할 수 있는 Array에 대해서 보자. Array는 논리적 순서와 물리적 순서가 일치한다. 따라서 index값을 통한 원소 접근이 용이하며, 구현이 쉽다.

    단점은 삽입, 삭제등에 대한 연산에 Cost가 높다는 것이다. 삭제의 경우 순서를 맞추기 위해, 뒤의 원소들을 모두 앞으로 Shift연산을 해줘야 하며, 삽입의 경우도 삽입한 인덱스 포함. 그 뒤의 인덱스들에 Shift 연산을 해줘야 한다.

  • Linked List
    Array의 삽입/삭제 연산에 대한 비효율성을 극복하고자 등장한 것이 Linked List이다. LinkedList는 논리적으로 순서대로 되어있으나, 물리적으로 순서대로 되어있지 않다. 대신 LinkedList는 각 원소가 다음 index 위치에 해당하는 물리적 주소를 가지고 있다. 그렇기에 삽입/삭제시에는 데이터를 shift할 필요 없이, 해당되는 원소의 물리적 주소만 변경해주면 된다. 하지만 이 같은 특징 때문에 원하는 index를 참조하려면, 1번 index부터 차례대로 접근해야 한다는 비효율성이 있다.

Stack & Queue


  • Stack은 선형 자료구조의 일종으로, FILO(First In Last Out)의 대표적인 예시이다. 원소가 먼저 들어간 것이 나중에 나오고, 가장 나중에 들어간 원소가 가장 먼저 나오게 된다. 미로찾기, 괄호 유효성 체크 등에 활용된다.

  • Queue : 선형 자료구조이며, Stack과는 반대로 FIFO(First In First Out)구조이다. 줄을 선다는 뜻과 같게, 먼저 들어간 원소가 가장 먼저 나온다. 작업 우선순위, Heap 구현 등에 사용된다.

Tree


Tree는 Stack, Queue와는 다르게 비선형 자료구조로, 계층적 구조를 표현하는 자료구조이다. 실제 데이터를 삽입하고 삭제한다는 생각 이전에, 표현에 집중하자. 트리의 구성 요소는 다음과 같다.

  • Node(노드) : 트리를 구성하고 있는 원소 그 자체를 말한다.
  • Edge(간선) : 노드와 노드사이를 연결하고 있는 선을 말한다.
  • Root(루트노드) : 트리에서 최상위 노드를 말한다.
  • Leaf(리프노드) : 트리에서 최하위 노드를 말한다.
  • Internal(가지노드) : 트리에서 최하위 노드를 제외한 모든 노드를 말한다.
  • Level : 노드로 이루어진 각 층
  • Height : 트리에서 Level의 수

Binary Tree (이진트리)


Binary Tree는 Root노드를 포함, Leaf노드를 제외한 모든 노드의 자식이 두 개인 것을 말한다. 공집합 역시 노드로 인정한다.

이진트리에는 모든 Level이 가득 찬 이진트리인 Full Binary Tree(포화 이진 트리)와 위에서 아래로, 왼족에서 오른쪽으로 순서대로 채워진 트리인 Complete Binary Tree(완전 이진 트리)가 있다.(두 트리의 차이점을 알아두면 좋을 것 같다) 배열로 포화 이진트리와 완전 이진트리를 구현했을 때, 노드의 개수 n에 대해서 i번째 노드에 parent(i) = i/2, left_child=2i, right_child = 2i+1의 인덱스 값을 갖는다.

BST, Binary Search Tree (이진 탐색 트리)


자료구조에서 효율적인 탐색 방법만을 고민할 것이 아니라, 효율적인 저장방법도 고민해야 한다. Binary Search Tree(이진 탐색 트리)는 이진 트리이며, 데이터를 저장하는 특별한 규칙이 있다. 그 규칙으로 찾고자 하는 데이터를 찾을 수 있다.

  1. 이진 탐색 노드에 저장된 값은 유일한 값이다.
  2. 루트 노드의 값은 왼쪽에 있는 모든 노드의 값보다 크다.
  3. 루트 노드의 값은 오른쪽에 있는 모든 노드의 값보다 작다.
  4. 각 서브 트리별로 2, 3번 규칙을 만족한다.

저장할 때 위의 규칙대로 잘 저장하기만 하면, 루트 노드로부터 원하는 값을 찾아나가는 것은 어렵지 않으나, 값이 추가되고 삭제됨에 따라, 한쪽에만 치우친 Skewed Tree(편향 트리)가 될 가능성이 있다. 이를 해결하기 위해 Rebalancing이라는 기법을 사용하여 트리를 재조정하게 된다.

평균 탐색시간은 O(logN), 최악 탐색시간은 O(N)

Red Black Tree


RBT(Red-Black Tree)는 위에서 설명한 Rebalancing 기법의 하나로, 기존 이진탐색트리의 삽입, 삭제, 탐색의 비효율성을 개선한 방법이다. RBT는 다음과 같은 규칙을 따른다.

  1. 각 노드는 Red 혹은 Black의 색을 갖는다.
  2. 루트 노드는 Black이다.
  3. 각 말단 노드는 Black이다.
  4. 어떤 노드의 색이 Red라면, 두 자식 노드의 색은 모두 Black이다.
  5. 어느 한 노드로부터 리프노드(NIL)까지의 Black의 수는 리프노드를 제외하면 모두 같다. (Black-Height)

RBT의 특징

  1. Binary Search Tree이므로, BST의 특징을 모두 갖고 있다.
  2. 루트로부터 말단 노드까지의 최소 경로는 최대 경로의 두 배보다 크지 않다. 이를 Balanced한 상태라 한다.
  3. 노드의 Child가 없을 경우, Child를 가리키는 포인터에 NIL(혹은 NULL)값을 저장한다. 이러한 NIL노드들을 말단 노드로 간주한다. 말단 노드이기 때문에, 이 노드들의 색은 Black이다.

RBT에서의 삽입 과정은 다음과 같다. 우선 새로 삽입한 노드를 BST 특성을 유지하며 삽입한 후, 색을 Red로 칠한다. 이는 Black-Height의 수를 최대한 유지하기 위해서이다. 삽입 결과 RBT 특성이 위배된다면, 노드의 색을 다시 칠한다. 만일 Black-Height 특성, 즉 위의 5번 규칙이 위배되었다면, Rotation을 통해 조정한다.

삭제 과정 역시 마찬가지로 우선 BST 특성을 유지하며 노드를 삭제한다. 삭제될 노드의 Child와 색깔로 Rotation 방법이 정해진다 (후에 삭제 과정을 자세히 조사하겠다.)

Binary Heap

Binary Heap은 배열에 기반한 완전 이진탐색트리이며, Max-Heap과 Min-Heap이 있다. Max-Heap은 상위 노드의 값이 하위 각 노드의 값보다 크며, Min-Heap은 반대로 상위 노드의 값이 하위 각 노드의 값보다 작다(형제 노드끼리는 상관없다). 이 성질을 이용하면 최대, 최솟값을 찾아내는 것이 훨씬 용이하다.

HashTable

HashTable은 내부적으로 배열을 사용하며, 평균적으로 빠른 탐색속도 O(1)를 갖는다. 평균적으로라는 의미는 충돌을 고려하지 않았을 때이다. Key 값을 해시함수를 통하여 인덱스로 변한 후에, 그 인덱스에 집어 넣는다. 만약 다른 Key값을 해시함수를 통과시켰는데, 같은 인덱스가 나온다면, 그걸 충돌이라고 한다. 충돌 해소법(Resolve Conflict) 방법에는 기본적인 두 가지 방법이 있다.

  1. Open Address 방식 (개방 주소법)
    충돌 발생시, 다른 인덱스를 찾는다.
    Linear Probing : 순차적으로 탐색하여 다음 인덱스를 찾는다.
    Quadratic Probing : 2차 함수를 이용해 탐색할 위치를 찾는다.
    Double Hashing Probing : 충돌 발생시 새로운 해시함수를 활용하여 주소를 찾는다.
  2. Separate Chaining 방식 (분리 연결법)
    충돌 발생 시 다른 인덱스를 찾는 대신, 그 인덱스에다가 연결하는 방법.
    연결 리스트를 이용하여 연결하는 방법과, Tree(RBT)를 이용하여 연결하는 방법이 있다. 두 방식 모드 Worst Case가 O(N)이다. 데이터의 크기가 크다면 Separate Chaining, 아니라면 Open Address 방식이 더 낫다.

Hashmap의 Resize : 일정 개수이상 크기가 커지면, 해시 버킷의 크기를 두 배로 늘림.

Graph

정점과 간선의 집합이며, 일종의 Tree이다.
Undirected와 Directed Graph가 있는데, 방향성 유무로 결정된다.

Degree란 Undirected Graph에서 정점에 연결된 간선의 개수이다. Directed Graph에서의 Degree는 방향성이 있기 때문에 둘로 나뉘는데, 나가는 간선의 개수는 Outdegree, 들어오는 간선의 개수를 Indegree라 한다.

가중치 그래프란 간선에 가중치를 둔 그래프, 부분 그래프 란 한 그래프의 일부 정점 및 간선으로 이루어진 그래프.

그래프의 구현 방법 :

  1. 인접 행렬 : 정방 행렬을 사용하여 구현. 연결 관계를 O(1)로 파악 가능. 공간복잡도는 O(2V)
  2. 인접 리스트 : 리스트를 사용하여 구현. 정점간 연결 여부 파악에 오래 걸림. 공간복잡도는 O(E + V)

탐색 방법에는 깊이 우선 탐색(DFS, Depth First Search)와 너비 우선 탐색(BFS, Breadth First Search)이 있다.

깊이 우선 탐색은 깊숙히 들어가서 탐색하고 나오는 것이며 Stack을 통해 구현한다.
너비 우선 탐색은 임의이 한 정점에 대해 인접한 정점을 queue에 넣고, dequeue연산에서 나온 하나의 점점으로 드러아것 그 정점의 인접한 정점을 다시 Queue에 넣어서 탐색하는 방식이다. BFS로 찾은 경로는 최단 경로이다.

참조“zueyul 자료 구조 정리”
참조“hygoogi 자료구조 정리”