주어진 배열에서 최솟값을 찾아 배열의 맨 앞에 위치시키는 방식으로 동작한다. 이러한 과정을 반복하면서 배열이 정렬 된다.
SelectionSort
package com.example.datastructure.Sort;
public class SelectionSort implements ISort {
@Override
public void sort(int[] arr) {
for(int i = 0; i < arr.length - 1 ; i++){
int minIndex = i;
for(int j = i + 1; j < arr.length; j++){
if(arr[minIndex] > arr[j]){
minIndex = j;
}
}
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
데이터의 개수가 n개라고 했을 때
첫 번째 회전에서의 비교에서 첫번째 값 ~ n번째 값까지 비교하므로 n-1번
두 번째 회전에서의 비교횟수 두번째 값 ~ n번째 값까지 비교하므로 n-2번
...
(n-1) + (n-2) + .... + 2 + 1 → n(n-1)/2
즉 주어진 n개의 배열을 정렬하는데 필요한 시간복잡도는 O(n²)입니다.
최선, 평균, 최악의 경우 모두 동일한 시간복잡도를 가집니다.
인접한 두 개의 원소를 비교하면서 큰 값을 오른쪽으로 이동시키거나 작은 값을 왼쪽으로 이동시켜 배열을 정렬한다.
BubbleSort
package com.example.datastructure.Sort;
public class BubbleSort implements ISort{
@Override
public void sort(int[] arr) {
// 안정 정렬
// 인플레이스 정렬
for(int i = 0; i < arr.length - 1; i++){
for(int j = 0; j < arr.length - 1 - i; j++){
if(arr[j] > arr[j+1]){
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
}
데이터의 개수가 n개라고 했을 때
첫 번째 회전에서
첫번째 값과 두번째 값, 두번째 값과 세번째 값... n-1번째값과 n번째 값을 비교하여 총 n-1번 비교
두 번째 회전에서
첫번째 값과 두번째 값, 두번째 값과 세번째 값... n-2번째값과 n-1번째 값을 비교하여 총 n-2번 비교
...
(n-1) + (n-2) + .... + 2 + 1 → n(n-1)/2
즉 주어진 n개의 배열을 정렬하는데 필요한 시간복잡도는 O(n²)입니다.
최선, 평균, 최악의 경우 모두 동일한 시간복잡도를 가집니다.
주어진 배열을 정렬된 부분과 정렬되지 않은 부분으로 나누고, 정렬되지 않은 부분의 원소를 정렬된 부분의 올바른 위치에 삽입하여 배열을 정렬한다.
InsertionSort
package com.example.datastructure.Sort;
public class InsertionSort implements ISort{
@Override
public void sort(int[] arr) {
for(int i = 1; i< arr.length; 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;
}
}
}
데이터의 개수가 n개라고 했을 때
최선의 경우 : O(n)
비교 횟수 : (n-1)번
앞의 값들이 모두 정렬되어 있으므로 바로 앞의 원소와 동일하게 1번의 비교만 이루어집니다.
최악의 경우(자료가 역순인 경우) : O(n²)
비교 횟수
첫번째 타겟과(두 번째에서 시작) 그 앞인 첫 번째 값을 비교하여 1번
두번째 타겟과 첫 번째, 두 번째 값과 비교하여 2번
...
마지막 타겟과 그 앞의 값들을 비교하여 n-1번
따라서 1 + 2 + .... + (n-2) + (n-1) → n(n-1)/2 입니다.
분할 정복(Divide and Conquer) 방법을 사용하여 배열을 정렬한다. 배열을 절반으로 분할한 뒤 각각의 부분 배열을 재귀적으로 정렬한 후, 정렬된 부분 배열을 병합하여 최종적으로 정렬된 배열을 얻는 방식이다.
package com.example.datastructure.Sort;
public class RecursiveSum {
public static int recursiveSum(int n){
if(n <= 0) { // 기저 조건
return 0;
}else{
return n + recursiveSum(n - 1); // 자기 자신을 호출하면서 n-1에 대한 합을 구함
}
}
public static void main(String[] args) {
int result = recursiveSum(5);
System.out.println(result); // 출력 결과 15
}
}
함수 호출과정
n이 0이므로 if 블록으로 이동하여 0 반환
역순으로 함수 호출이 해제되며 결과가 계산됩니다:
MergeSort
package com.example.datastructure.Sort;
import java.util.Arrays;
public class MergeSort implements ISort{
@Override
public void sort(int[] arr) {
// in-place sort
mergeSort(arr, 0, arr.length - 1);
}
private void mergeSort(int[] arr, int low, int high) {
if (low >= high) {
return; // base case
}
int mid = low + ((high - low) / 2);
System.out.println("mergeSort(arr, " + low + ", " + high + ")");
mergeSort(arr, low, mid);
mergeSort(arr, mid + 1, high);
merge(arr, low, mid, high);
}
private void merge(int[] arr, int low, int mid, int high){
int[] temp = new int[high - low + 1];
int idx = 0;
int left = low;
int right = mid + 1;
while(left <= mid && right <= high){
if(arr[left] <= arr[right]){
temp[idx] = arr[left];
left++;
}else {
temp[idx] = arr[right];
right++;
}
idx++;
}
while(left <= mid){
temp[idx] = arr[left];
idx++;
left++;
}
while(right <= high){
temp[idx] = arr[right];
idx++;
right++;
}
for(int i = low; i <= high; i++){
arr[i] = temp[i - low];
}
}
}
Main
package com.example.datastructure.Sort;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] arr = {38, 27, 43, 3, 9, 82, 10};
MergeSort mergeSort = new MergeSort();
System.out.println("Original Array: " + Arrays.toString(arr));
mergeSort.sort(arr);
System.out.println("Sorted Array: " + Arrays.toString(arr));
}
}
결과
재귀 함수 호출 과정
이런 방식으로 왼쪽 부분 배열과 오른쪽 부분 배열의 원솔르 비교해서 더 작은 값이 복사되고 L, K를 하나씩 증가한다.
만약 숫자가 남아 있다면 이미 정렬된 상태이기 때문에 남은 값을 그대로 복사해서 넣어 준다.
정렬 과정에서 n개의 요소를 계속해서 반으로 나누는데, 이러한 분할은 log n
번 이루어집니다. 그리고 각 단계에서 병합 작업을 수행하는 데에는 최대 O(n)의 시간이 소요된다.
따라서 병합 정렬의 시간 복잡도는 O(n log n)으로 나타낼 수 있습니다.
퀵 정렬은 평균적으로 매우 빠른 속도를 자랑하는 정렬 방법이다. 퀵 정렬(Quick Sort)은 분할 정복(Divide and Conquer) 방식의 정렬 알고리즘으로 기준점을 기준으로 배열을 두 개의 부분으로 분할하고, 각각을 재귀적으로 정렬하여 최종적으로 정렬된 배열을 얻는다.
하나의 리스트를 피벗(pivot)을 기준으로 두 개의 비균등한 크기로 분할하고 분할된 부분 리스트를 정렬한 다음, 두 개의 정렬된 부분 리스트를 합하여 전체가 정렬된 리스트가 되게 하는 방법이다.
퀵 정렬은 다음의 단계들로 이루어진다.
순환 호출이 한번 진행될 때마다 최소한 하나의 원소(피벗)는 최종적으로 위치가 정해지므로, 이 알고리즘은 반드시 끝난다는 것을 보장할 수 있다.
QuickSort
package com.example.datastructure.Sort;
public class QuickSort implements ISort{
@Override
public void sort(int[] arr) {
quickSort(arr, 0, arr.length - 1);
}
private void quickSort(int[] arr, int low, int high){
if(low >= high){
return;
}
int pivot = low + ((high - low) / 2);
int pivotValue = arr[pivot];
int left = low;
int right = high;
while(left <= right){
while(arr[left] < pivotValue){
left++;
}
while(arr[right] > pivotValue){
right--;
}
if(left <= right){
int tmp = arr[right];
arr[right] = arr[left];
arr[left] = tmp;
left++;
right--;
}
}
quickSort(arr, low, right);
quickSort(arr, low, high);
}
}
분할 과정은 배열을 평균적으로 반으로 나누기 때문에 log n
번 수행됩니다. 그리고 각 분할 단계에서 기준점을 기준으로 왼쪽과 오른쪽 부분 배열에 요소를 배치하는 작업은 평균적으로 O(n)의 시간이 소요됩니다.
따라서 평균 시간 복잡도는 O(n log n)이 되는 것입니다. 이러한 시간 복잡도 때문에 퀵 정렬은 대부분의 경우에 매우 빠른 속도로 정렬을 수행합니다.
하지만 최악의 경우에는 시간 복잡도가 O(n^2)이 될 수 있습니다. 최악의 경우는 기준점(pivot)이 항상 최솟값 또는 최댓값으로 선택되는 경우로, 이미 정렬되어 있는 배열이나 거의 정렬된 배열에 대해 발생할 수 있습니다.
최악의 경우