버블 정렬 (Bubble Sort)
- 데이터의 인접 요소끼리 비교하고, swap 연산을 수행하며 정렬하는 방식
- 두 인접한 데이터의 크기를 비교해 정렬하는 방법
- 비교와 교환이 모두 일어날 수 있어 코드는 단순하지만 성능은 좋지 않음
- 시간 복잡도 O(n^2)
- 과정
- 비교 연산이 필요한 루프 범위 설정
- 인접한 데이터 값을 비교
- swap 조건에 부합하면 swap 연산을 수행
- 루프 범위가 끝날 때까지 2-3번 반복
- 정렬 영역을 선택 -> 다음 루프를 실행할 때는 이 영역 제외
- 비교 대상이 없을 때까지 1-5번을 반복
- 특정한 루프의 전체 영역에서 swap이 한번도 발생하지 않았다면 그 영역 뒤에 있는 데이터가 모두 정렬되었다는 뜻으로 프로세스를 종료
// 참조 https://st-lab.tistory.com/195
public class Bubble_Sort {
public static void bubble_sort(int[] a) {
bubble_sort(a, a.length);
}
private static void bubble_sort(int[] a, int size) {
// round는 배열 크기 - 1 만큼 진행됨
for(int i = 1; i < size; i++) {
boolean swapped = false;
// 각 라운드별 비교횟수는 배열 크기의 현재 라운드를 뺀 만큼 비교함
for(int j = 0; j < size - i; j++) {
/*
* 현재 원소가 다음 원소보다 클 경우
* 서로 원소의 위치를 교환하고
* 비교수행을 했다는 표시로 swapped 변수를 true로 변경한다.
*/
if(a[j] > a [j + 1]) {
swap(a, j, j + 1);
swapped = true;
}
}
/*
* 만약 swap된적이 없다면 이미 정렬되었다는 의미이므로
* 반복문을 종료한다.
*/
if(swapped == false) {
break;
}
}
}
private static void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
- 장점
- 추가적인 메모리 소비가 작음
- 구현이 매우 쉬움
- 안정 정렬이 가능
- 단점
- 다른 정렬 알고리즘에 비해 교환 과정이 많아 많은 시간을 소비
선택 정렬 (Selection Sort)
- 대상 데이터에서 최대나 최소 데이터를 데이터가 나열된 순으로 찾아가며 선택하는 방법
- 구현 방법이 복잡하고 효율적이지 않아 많이 사용하지 않음
- 최솟값 또는 최댓값을 찾고, 남은 정렬 부분의 가장 앞에 있는 데이터와 swap 하는 것이 핵심
- 시간 복잡도 O(n^2)
- 과정
- 남은 정렬 부분에서 최솟값 또는 최댓값을 찾음
- 남은 정렬 부분에서 가장 앞에 있는 데이터와 선택된 데이터를 swap함
- 가장 앞에 있는 데이터의 위치를 변경해(index++) 남은 정렬 부분의 범위를 축소
- 전체 데이터 크기만큼 index가 커질 때까지, 즉 남은 정렬 부분이 없을 때까지 반복
// 참조 https://st-lab.tistory.com/168
public class Selection_Sort {
public static void selection_sort(int[] a) {
selection_sort(a, a.length);
}
private static void selection_sort(int[] a, int size) {
for(int i = 0; i < size - 1; i++) {
int min_index = i;
// 최솟값을 갖고있는 인덱스 찾기
for(int j = i + 1; j < size; j++) {
if(a[j] < a[min_index]) {
min_index = j;
}
}
// i번째 값과 찾은 최솟값을 서로 교환
swap(a, min_index, i);
}
}
private static void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
- 장점
- 추가적인 메모리 소비가 작음
- 단점
- 안정 정렬이 아님
삽입 정렬 (Insertion Sort)
- 이미 정렬된 데이터 범위에 정렬되지 않은 데이터를 적절한 위치에 삽입시켜 정렬하는 방법
- 현재 비교하고자 하는 타겟과 그 이전의 원소들과 비교하며 자리를 swap하는 정렬 방법
- 시간 복잡도 O(n^2)
- 과정
- 현재 index에 있는 데이터 값을 선택
- 현재 선택한 데이터가 정렬된 데이터 범위에 삽입될 위치를 탐색
- 삽입 위치부터 index에 있는 위치까지 shift 연산을 수행
- 삽입 위치에 현재 선택한 데이터를 삽입하고 index++ 연산 수행
- 전체 데이터의 크기만큼 index가 커질 때까지, 즉 선택할 데이터가 없을 때까지 반복
- 적절한 삽입 위치를 탐색하는 부분에서 이진 탐색 등과 같은 탐색 알고리즘을 사용하면 시간 복잡도를 줄일 수 있음
// 참조 https://st-lab.tistory.com/179
public class Insertion_Sort {
public static void insertion_sort(int[] a) {
insertion_sort(a, a.length);
}
private static void insertion_sort(int[] a, int size) {
for(int i = 1; i < size; i++) {
// 타겟 넘버
int target = a[i];
int j = i - 1;
// 타겟이 이전 원소보다 크기 전 까지 반복
while(j >= 0 && target < a[j]) {
a[j + 1] = a[j]; // 이전 원소를 한 칸씩 뒤로 미룬다.
j--;
}
/*
* 위 반복문에서 탈출 하는 경우 앞의 원소가 타겟보다 작다는 의미이므로
* 타겟 원소는 j번째 원소 뒤에 와야한다.
* 그러므로 타겟은 j + 1 에 위치하게 된다.
*/
a[j + 1] = target;
}
}
}
- 장점
- 추가적인 메모리 소비가 적음
- 거의 정렬 된 경우 매우 효율적 ( 최선의 경우 O(n)의 시간복잡도를 가짐 )
- 안정 정렬이 가능
- 단점
- 역순에 가까울 수록 비효율적 ( 최악의 경우 O(n^2)의 시간 복잡도를 가짐 )
- 데이터의 상태에 따라서 성능 편차가 매우 큼
퀵 정렬 (Quick Sort)
- 기준 값(pivot)을 선정해 해당 값보다 작은 데이터와 큰 데이터로 분류하는 것을 반복해 정렬하는 알고리즘
- 시간 복잡도 평균 : O(nlogn) / 최악 : O(n^2)
- 과정
- 데이터를 분할하는 pivot을 설정
- pivot을 기준으로 다음 과정을 거쳐 데이터를 2개의 집합으로 분리
- start가 가리키는 데이터가 pivot이 가리키는 데이터보다 작으면 start를 오른쪽으로 1칸 이동
- end가 가리키는 데이터가 pivot이 가리키는 데이터보다 크면 end를 왼쪽으로 1칸 이동
- start가 가리키는 데이터가 pivot이 가리키는 데이터보다 크고, end가 가리키는 데이터가 pivot이 가리키는 데이터보다 작으면 start, end가 가리키는 데이터를 swap하고 start는 오른쪽, end는 왼쪽으로 1칸씩 이동
- start와 end가 만날 때 까지 반복
- start와 end가 만나면 만난 지점에서 가리키는 데이터와 pivot이 가리키는 데이터를 비교하여 pivot이 가리키는 데이터가 크면 만난 지점의 오른쪽에, 작으면 만난 지점의 왼쪽에 pivot이 가리키는 데이터를 삽입
- 분리 집합에서 각각 다시 pivot을 선정
- 분리 집합이 1개 이하가 될 때까지 과정을 반복
- 코드 참조
자바 [JAVA] - 퀵 정렬 (Quick Sort)
[정렬 알고리즘 모음] 더보기 1. 계수 정렬 (Counting Sort) 2. 선택 정렬 (Selection Sort) 3. 삽입 정렬 (Insertion Sort) 4. 거품 정렬 (Bubble Sort) 5. 셸 정렬 (Shell Sort) 6. 힙 정렬 (Heap Sort) 7. 합병(병합) 정렬 (Merge
st-lab.tistory.com
- 장점
- 특정 상태가 아닌 이상 평균 시간 복잡도는 NlongN이며, 다른 알고리즘에 비해 속도가 매우 빠름
- 병합정렬에 비해 2-3배 빠름
- 추가적인 별도의 메모리를 필요로하지 않음
- 단점
- 특정 조건하에 성능이 급격하게 떨어짐
- 재귀를 사용하기 때문에 재귀를 사용하지 못하는 환경일 경우 매우 복잡해짐
병합 정렬 (Merge Sort)
- 분할 정복 방식을 사용해 데이터를 분할하고 분할한 집합을 정렬하며 합치는 알고리즘
- 시간 복잡도 O(nlogn)
- 과정
- 주어진 리스트를 절반으로 분할하여 부분리스트로 나눔
- 해당 부분리스트의 길이가 1이 아니라면 1번 과정을 되풀이
- 인접한 부분리스트끼리 정렬하여 합침
//[Top-Down 형식]
// https://st-lab.tistory.com/233
public class Merge_Sort {
private static int[] sorted; // 합치는 과정에서 정렬하여 원소를 담을 임시배열
public static void merge_sort(int[] a) {
sorted = new int[a.length];
merge_sort(a, 0, a.length - 1);
sorted = null;
}
// Top-Down 방식 구현
private static void merge_sort(int[] a, int left, int right) {
/*
* left==right 즉, 부분리스트가 1개의 원소만 갖고있는경우
* 더이상 쪼갤 수 없으므로 return한다.
*/
if(left == right) return;
int mid = (left + right) / 2; // 절반 위치
merge_sort(a, left, mid); // 절반 중 왼쪽 부분리스트(left ~ mid)
merge_sort(a, mid + 1, right); // 절반 중 오른쪽 부분리스트(mid+1 ~ right)
merge(a, left, mid, right); // 병합작업
}
/**
* 합칠 부분리스트는 a배열의 left ~ right 까지이다.
*
* @param a 정렬할 배열
* @param left 배열의 시작점
* @param mid 배열의 중간점
* @param right 배열의 끝 점
*/
private static void merge(int[] a, int left, int mid, int right) {
int l = left; // 왼쪽 부분리스트 시작점
int r = mid + 1; // 오른쪽 부분리스트의 시작점
int idx = left; // 채워넣을 배열의 인덱스
while(l <= mid && r <= right) {
/*
* 왼쪽 부분리스트 l번째 원소가 오른쪽 부분리스트 r번째 원소보다 작거나 같을 경우
* 왼쪽의 l번째 원소를 새 배열에 넣고 l과 idx를 1 증가시킨다.
*/
if(a[l] <= a[r]) {
sorted[idx] = a[l];
idx++;
l++;
}
/*
* 오른쪽 부분리스트 r번째 원소가 왼쪽 부분리스트 l번째 원소보다 작거나 같을 경우
* 오른쪽의 r번째 원소를 새 배열에 넣고 r과 idx를 1 증가시킨다.
*/
else {
sorted[idx] = a[r];
idx++;
r++;
}
}
/*
* 왼쪽 부분리스트가 먼저 모두 새 배열에 채워졌을 경우 (l > mid)
* = 오른쪽 부분리스트 원소가 아직 남아있을 경우
* 오른쪽 부분리스트의 나머지 원소들을 새 배열에 채워준다.
*/
if(l > mid) {
while(r <= right) {
sorted[idx] = a[r];
idx++;
r++;
}
}
/*
* 오른쪽 부분리스트가 먼저 모두 새 배열에 채워졌을 경우 (r > right)
* = 왼쪽 부분리스트 원소가 아직 남아있을 경우
* 왼쪽 부분리스트의 나머지 원소들을 새 배열에 채워준다.
*/
else {
while(l <= mid) {
sorted[idx] = a[l];
idx++;
l++;
}
}
/*
* 정렬된 새 배열을 기존의 배열에 복사하여 옮겨준다.
*/
for(int i = left; i <= right; i++) {
a[i] = sorted[i];
}
}
}
// [Bottom-Up 형식]
// https://st-lab.tistory.com/233
public class Merge_Sort {
private static int[] sorted; // 합치는 과정에서 정렬하여 원소를 담을 임시배열
public static void merge_sort(int[] a) {
sorted = new int[a.length];
merge_sort(a, 0, a.length - 1);
sorted = null;
}
// Bottom-Up 방식 구현
private static void merge_sort(int[] a, int left, int right) {
/*
* 1 - 2 - 4 - 8 - ... 식으로 1부터 서브리스트를 나누는 기준을 두 배씩 늘린다.
*/
for(int size = 1; size <= right; size += size) {
/*
* 두 부분리스트을 순서대로 병합해준다.
* 예로들어 현재 부분리스트의 크기가 1(size=1)일 때
* 왼쪽 부분리스트(low ~ mid)와 오른쪽 부분리스트(mid + 1 ~ high)를 생각하면
* 왼쪽 부분리스트는 low = mid = 0 이고,
* 오른쪽 부분리스트는 mid + 1부터 low + (2 * size) - 1 = 1 이 된다.
*
* 이 때 high가 배열의 인덱스를 넘어갈 수 있으므로 right와 둘 중 작은 값이
* 병합되도록 해야한다.
*/
for(int l = 0; l <= right - size; l += (2 * size)) {
int low = l;
int mid = l + size - 1;
int high = Math.min(l + (2 * size) - 1, right);
merge(a, low, mid, high); // 병합작업
}
}
}
/**
* 합칠 부분리스트는 a배열의 left ~ right 까지이다.
*
* @param a 정렬할 배열
* @param left 배열의 시작점
* @param mid 배열의 중간점
* @param right 배열의 끝 점
*/
private static void merge(int[] a, int left, int mid, int right) {
int l = left; // 왼쪽 부분리스트 시작점
int r = mid + 1; // 오른쪽 부분리스트의 시작점
int idx = left; // 채워넣을 배열의 인덱스
while(l <= mid && r <= right) {
/*
* 왼쪽 부분리스트 l번째 원소가 오른쪽 부분리스트 r번째 원소보다 작거나 같을 경우
* 왼쪽의 l번째 원소를 새 배열에 넣고 l과 idx를 1 증가시킨다.
*/
if(a[l] <= a[r]) {
sorted[idx] = a[l];
idx++;
l++;
}
/*
* 오른쪽 부분리스트 r번째 원소가 왼쪽 부분리스트 l번째 원소보다 작거나 같을 경우
* 오른쪽의 r번째 원소를 새 배열에 넣고 r과 idx를 1 증가시킨다.
*/
else {
sorted[idx] = a[r];
idx++;
r++;
}
}
/*
* 왼쪽 부분리스트가 먼저 모두 새 배열에 채워졌을 경우 (l > mid)
* = 오른쪽 부분리스트 원소가 아직 남아있을 경우
* 오른쪽 부분리스트의 나머지 원소들을 새 배열에 채워준다.
*/
if(l > mid) {
while(r <= right) {
sorted[idx] = a[r];
idx++;
r++;
}
}
/*
* 오른쪽 부분리스트가 먼저 모두 새 배열에 채워졌을 경우 (r > right)
* = 왼쪽 부분리스트 원소가 아직 남아있을 경우
* 왼쪽 부분리스트의 나머지 원소들을 새 배열에 채워준다.
*/
else {
while(l <= mid) {
sorted[idx] = a[l];
idx++;
l++;
}
}
/*
* 정렬된 새 배열을 기존의 배열에 복사하여 옮겨준다.
*/
for(int i = left; i <= right; i++) {
a[i] = sorted[i];
}
}
}
- 장점
- 항상 두 부분리스트로 쪼개어 들어가기 때문에 최악의 경우에도 O(NlongN)으로 유지가 됨
- 안정정렬임
- 단점
- 정렬과정에서 추가적인 보조 배열 공간을 사용하기 때문에 메모리 사용량이 많음
- 보조 배열에서 원본배열로 복사하는 과정은 매우 많은 시간을 소비하기 때문에 데이터가 많을 경우 상대적으로 시간이 많이 소요됨