-
void selectionSort(int[] arr) { int indexMin, temp; for (int i = 0; i < arr.length-1; i++) { indexMin = i; for (int j = i+1; j < arr.length; j++) { if (arr[j] < arr[indexMin]) { indexMin = j; } } temp = arr[indexMin]; arr[indexMin] = arr[i]; arr[i] = temp; } System.out.println(Arrays.toString(arr)); }
1. 선택 정렬 (Selection Sort)
- 해당 자리를 선택하고 그 자리에 오는 값을 찾는 것
1) 주어진 배열 중에서 최소값을 찾는다.
2) 그 값을 맨 앞에 위치한 값과 교체한다.
3) 맨 처음 위치를 뺀 나머지 배열을 같은 방법으로 교체한다.
- 시간 복잡도 : O (n^2)
- 공간 복잡도 : O (n)
- 장점
버블정렬과 마찬가지로 단순하다.
많은 교환이 일어나야 하는 자료상태에서는 비교적 효율적
정렬하고자 하는 배열 안에서 교환하는 방식이므로, 다른 메모리 공간을 필요로 하지 않는다.
- 단점
시간 복잡도가 O(n^2)이므로 비효율적
불안정 정렬 (Unstable Sort)이다.
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-1] > arr[j]) { temp = arr[j-1]; arr[j-1] = arr[j]; arr[j] = temp; } } } System.out.println(Arrays.toString(arr)); }
2. 버블 정렬 (Bubble Sort)
- 옆에 있는 값과 비교해서 더 작은 값을 앞으로 보낸다.
1) 1회전에 첫 번째 원소와 두 번째 원소를, 두 번째 원소와 세 번째 원소와, .... 이런식으로 (마지막-1)번째 원소와 마지막 원소를
비교하여 조건에 맞지 않는다면 서로 교환한다.
2) 1회전을 수행하고 나면 가장 큰 원소가 맨 뒤로 이동하므로 2회전에서는 맨 끝에 있는 원소를 정렬에서 제외,
2회전을 수행하고 나면 끝에서 두 번째 원소까지는 정렬에서 제외.
이렇게 정렬을 1회전 수행할 때마다 정렬에서 제외되는 데이터가 하나씩 늘어난다.
- 시간복잡도 : O (n^2)
- 공간복잡도 : O (n)
- 장점
구현이 매우 간단하고, 소스코드가 직관적
정렬하고자 하는 배열 안에서 교환하는 방식이므로, 다른 메모리 공간을 필요로 하지않는다.
안정 정렬 (stable Sort) 이다.
- 단점
시간복잡도가 최악, 최선, 평균 모두 O (n^2)이므로, 비효율적이다.
정렬 돼있지 않은 원소가 정렬 됐을때의 자리로 가기 위해서, 교환연산이 많이 일어난다.
void insertionSort(int[] arr) { for (int index = 1; index < arr.length; index++) { int temp = arr[index]; int prev = index - 1; while ( (prev >= 0) && (arr[prev] > temp) ) { arr[prev+1] = arr[prev]; prev--; } arr[prev + 1] = temp; } System.out.println(Arrays.toString(arr)); }
3. 삽입 정렬 (Insertion Sort)
- 각 숫자를 적절한 위치에 삽입한다.
- 거의 정렬된 상태라면 효율적이다.
1) 정렬은 2번째 위치의 값을 temp에 저장한다.
2) temp와 이전에 있는 원소들과 비교하며 삽입해나간다.
3) '1'번으로 돌아가 다음 위치의 값을 temp에 저장하고, 반복한다.
=> 그 회차의 인덱스의 값의 자리를 찾아간다라고 생각
- 시간복잡도 : 최선의 경우 : O (n), 최악의 경우 O (n^2)
- 공간복잡도 : O(n)
- 장점
알고리즘이 단순하다.
대부분의 원소가 이미 정렬된 상태라면, 매우 효율적이다.
정렬하고자 하는 배열 안에서 교환하는 방식이므로, 다른 메모리 공간을 필요로 하지 않는다.
안정 정렬 (Stable Sort)이다.
Selection Sort나 Bubble Sort와 같은 O (n^2) 알고리즘에 비교하여 상대적으로 빠르다.
- 단점
평균과 최악의 시간복잡도가 O (n^2)이므로 비효율적이다.
Bubble Sort, Selectino Sort와 마찬가지로, 배열의 길이가 길어질수록 비효율적
public void quickSort(int[] arr, int left, int right) { if (left >= right) return; int pi = partition(); quickSort(arr, left, pi - 1); quickSort(arr, pi + 1, right); } public int partition(int[] arr, int left, int right) { int pivot = arr[left]; int i = left; int j = right; while (i < j) { while (pivot < arr[j]) { j--; } while (i < j && pivot >= arr[i]) { i++; } swap(arr, i, j); } arr[left] = arr[i]; arr[i] = pivot; return i; }
(1) 퀵 정렬 구현 1
void quickSort(int[] arr, int start, int end) { if (start >= end) return; int pivot = start; int i = start + 1; int j = end; int temp; while (i <= j) { // 엇갈릴 때까지 while (i <= end && arr[i] <= arr[pivot] ) { // 피봇값 보다 큰 값 만날때 까지 i++; } while (j > start && arr[j] >= arr[pivot] ) { // 피봇값 보다 작은 값 만날때 까지 j--; } if (i > j) { // 엇갈린 상태면 피봇값과 교체 temp = arr[j]; arr[j] = arr[pivot]; arr[pivot] = temp; } else { // 엇갈리지 않았다면 i j 교체 temp = arr[j]; arr[j] = arr[i]; arr[i] = temp; } } quickSort(arr, start, j - 1); quickSort(arr, j + 1, end) }
(2) 퀵 정렬 구현 2
public int partition(int[] arr, int left, int right) { int mid = (left + right) / 2; swap (arr, left, mid); ... } /** * 퀵소트 O (n^2) 해결 방법 * 피벗을 선택할 때 중간요소로 선택시 해결 가능 */
(3) 퀵 정렬의 O (n^2) 해결 방법
4. 퀵 정렬 (Quick Sort)
- 대표적인 분할정복 알고리즘
- 피벗값을 잡고 왼쪽 오른쪽 분할해서 수행
1) 피벗 선택
2) 오른쪽(j) 에서 왼쪽으로 가면서 피벗보다 작은 수 찾음
3) 왼쪽(i) 에서 오른쪽으로 가면서 피벗보다 큰 수 찾음
4) 각 인덱스 i,j 에 대한 요소를 교환
5) 2,3,4 과정 반복
6) 더 이상 2,3 과정 진행이 불가능하면, 현재 피벗과 교환 : 엇갈렸을때
7) 이제 교환된 피벗 기준으로 왼쪽엔 피벗보다 작은 값, 오른쪽엔 큰 값들만 존재함
- 시간복잡도 : 평균 O (n * logN) , 최악 (O^2)
void mergeSort (int[] arr, int left, int right) { // 쪼개기 if (left < right) { int mid = (left + right) / 2; mergeSort(arr, left, mid); mergeSort(arr, mid + 1, right); merge(arr, left, mid, right); } }
void merge (int[] arr, int left, int mid, int right) { // 합치기 int[] L = Arrays.copyOfRange(arr, left, mid + 1); int[] R = Arrays.copyOfRange(arr, mid + 1, right + 1); int i = 0; int j = 0; int k = left; int ll = L.length; int rl = R.length; while (i < ll && j < rl) { if (L[i] <= R[j]) { arr[k] = L[i++]; } else { arr[k] = R[j++]; } k++; } // remain while (i < ll) { arr[k++] = L[i++]; } while (j < rl) { arr[k++] = R[j++]; } }
5. 병합 정렬 (Merge Sort)
- 합병 정렬이라고도 부르며, 분할 정복 방법을 통해 구현
- 빠른 정렬로 분류되며, 퀵 정렬와 함께 많이 언급되는 정렬 방식
* 분할정복 ?
-> 큰 문제를 작은 문제 단위로 쪼개면서 해결해나가는 방식
- 시간복잡도 : 평균,최선,최악 모두 O (n * logN)
- 퀵 정렬과의 차이점
퀵정렬 : 우선 피벗을 통해 정렬 (partition) -> 영역을 쪼갬 (quickSort)
병합정렬 : 영역을 쪼갤 수 있을만큼 쪼갬 (mergeSort) -> 정렬 (merge)
public static void main(String[] args) { int[] array = { 230, 10, 60, 550, 40, 220, 20 }; heapSort(array); for (int v : array) { System.out.println(v); } } // 힙 정렬 전체 public static void heapSort(int[] arr) { int n = arr.length; // max heap 초기화 for (int i = n/2-1; i >= 0; i--) { heapify(arr, n, i); } // extract 연산 for (int i = n-1; i > 0; i--) { swap(arr, 0, i); heapify(arr, i, 0); } } // 최대 힙을 구하는 메소드 public static void heapify(int[] arr, int n, int i) { int p = i; int l = i * 2 + 1; int r = i * 2 + 2; // 왼쪽 자식 노드 if (l < n && arr[p] < arr[l]) { // 부모 노드와 왼쪽 자식 노드 비교 p = l; } // 오른쪽 자식 노드 if (r < n && arr[p] < arr[r]) { p = r; } // 부모 노드 < 자식 노드 if (i != p) { swap(arr, p, i); heapify(arr, n, p); } } // 자리 바꾸기 public static void swap(int[] arr, int a, int b) { int temp = arr[a]; arr[a] = arr[b]; arr[b] = temp; }
6. 힙 정렬 (Heap Sort)
- 완전 이진 트리를 기본으로 하는 Heap 자료구조를 기반으로한 정렬 방식
- 순서
1) 최대 힙을 구성
2) 현재 힙 루트는 가장 큰 값이 존재함. 루트의 값을 마지막 요소와 바꾼 후, 힙의 사이즈 하나 줄임
3) 힙의 사이즈가 1보다 크면 위 과정 반복
힙 정렬을 수행하기 위해서는 힙 생성 알고리즘 (Heapify Algorithm)을 사용한다.
힙 생성 알고리즘은 특정한 '하나의 노드' 에 대해서 수행하는 것이다.
또한 해당 '하나의 노드를 제외하고는 최대 힙이 구성되어 있는 상태'라고 가정한다.
힙 생성 알고리즘 (Heapify Algorithm)은 특정한 노드의 두 자식 중에서 더 큰 자식과
자신의 위치를 바꾸는 알고리즘이다.
또한 위치를 바꾼 뒤에도 여전히 자식이 존재하는 경우 또 자식 중에서 더 큰 자식과 자신의
위치를 바꾸어야 한다. 자식이 더 이상 존재하지 않을 때 까지.
- 시간 복잡도 : 평균,최선,최악 모두 O (n * logN)
* 트리 ?
-> 말 그대로 가지를 뻗어나가는 것처럼 데이터가 서로 연결되어있다는 것
* 이진 트리 ?
-> 모든 노드의 자식 노드가 2개 이하인 트리 구조
* 완전 이진 트리 ?
-> 데이터가 루트(Root) 노드부터 시작해서 자식 노드가 왼쪽 자식 노드, 오른쪽 자식 노드로
차근차근 들어가는 구조의 이진 트리
-> 이진 트리의 노드가 중간에 비어있지 않고 빽빽히 가득 찬 구조
* 힙 ?
-> 힙은 최솟값이나 최댓값을 빠르게 찾아내기 위해 완전 이진 트리를 기반으로 하는 트리
-> 최대힙, 최소힙 존재.
최대 힙은 '부모 노드'가 '자식 노드'보다 큰 힙
- 힙 정렬이 유용할 때
-> 가장 크거나 가장 작은 값을 구할 때
최소 힙 or 최대 힙의 루트 값이기 때문에 한번의 힙 구성을 통해 구하는 것이 가능
-> 최대 k 만큼 떨어진 요소들을 정렬할 때
삽입정렬보다 더욱 개선된 결과를 얻어낼 수 있음
int[] count = new int[5]; // 최댓값이 5일때 int[] arr = { ... }; for (int i = 0; i < arr.length; i++) { count[arr[i] - 1]++; } for (int i = 0; i < 5; i++) { if (count[i] != 0) { for (int j = 0; j < count[i]; j++) { System.out.print( (i + 1) + " "); } } }
8. 계수 정렬 (Count Sort)
- 크기를 기준으로 갯수를 센다.
- '범위 조건' 이 있는 경우에 한해서 굉장히 빠르다.
- Counting이 필요 : 각 숫자가 몇 번 등장했는지 센다.
- 시간 복잡도 : O (n)
- 공간 복잡도 : O (k) -> k만큼의 배열을 만들어야 함.
- 단점
배열 사이즈 N 만큼 돌 때, 증가시켜주는 Counting 배열의 크기가 큼. (메모리 낭비가 심함)
※참조
github.com/gyoogle/tech-interview-for-developer
blog.naver.com/ndb796/221226794899
'Algorithm' 카테고리의 다른 글
트리 정리 (0) 2020.09.07 그리디&구현, DFS&BFS (0) 2020.09.01 LinkedList (0) 2020.08.31 EOF (End of File) (0) 2020.08.29 검색 정리 (0) 2020.07.13