버블 정렬은 기본적으로 배열의 두 수(a, b)를 선택한 뒤, 만약 그 두 수가 정렬되었다면 놔두고 아니라면 두 수를 바꾸는 방식으로 진행된다
다음과 같은 정렬되지 않은 배열이 있다고 가정한다. 프로그램은 이 배열을 오름차순으로 정렬해야 한다.
[7, 2, 0, 1, 5, 6, 4]
배열의 첫 두 숫자(7, 2)를 비교한다.
7 > 2이므로 두 수를 바꾼다.
[2, 7, 0, 1, 5, 6, 4]
배열의 처음부터 끝까지 작업하면
[2, 0, 1, 5, 6, 4, 7] - 1회 : 가장 큰 수인 7이 정렬되었다.
계속 반복하면
[0, 1, 2, 5, 4, 6, 7] - 2회
[0, 1, 2, 4, 5, 6, 7] - 3회
[0, 1, 2, 4, 5, 6, 7] - 4회 :3회와 결과가 같으므로 모두 정렬된 것으로 판단.
public static void bubbleSort(int[] arr) {
for(int i = 1; i < arr.length - 1; i++) {
for(int j= 0 ; j < arr.length-i; j++) {
if(arr[j]<arr[j+1]) {
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
열의 모든 요소를 앞에서부터 차례대로 이미 정렬된 배열 부분과 비교하여, 자신의 위치를 찾아 삽입함으로써 정렬을 완성

public static void insertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
for (int j = i; j > 0; j--) {
if (arr[j] < arr[j - 1]) {
int tmp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = tmp;
} else {
break;
}
}
}
}
제자리 정렬 알고리즘의 하나로 다음의 순서로 이루어진다.
1. 주어진 리스트 중에 최소값을 찾는다.
2. 그 값을 맨 앞에 위치한 값과 교체한다(패스(pass)).
3. 맨 처음 위치를 뺀 나머지 리스트를 같은 방법으로 교체한다.
private static void selectionSort(int[] arr) {
// 1. 최소 값을 찾아 앞 쪽부터 교환하는 방식
for (int i = 0; i < arr.length - 1; i++) {
int min = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min])
min = j;
}
int tmp = arr[i];
arr[i] = arr[min];
arr[min] = tmp;
}
// 2. 최대 값을 찾아 뒤 쪽부터 교환하는 방식
for (int i = arr.length - 1; i > 0; i--) {
int max = i;
for (int j = i - 1; j >= 0; j--) {
if (arr[j] > arr[max])
max = j;
}
int tmp = arr[i];
arr[i] = arr[max];
arr[max] = tmp;
}
}
1. 정렬되지 않은 리스트를 각각 하나의 원소만 포함하는 n개의 부분리스트로 분할한다. (한 원소만 든 리스트는 정렬된 것과 같으므로)
2. 부분리스트가 하나만 남을 때까지 반복해서 병합하며 정렬된 부분리스트를 생성한다. 마지막 남은 부분리스트가 정렬된 리스트이다.
1. 리스트의 길이가 1 이하이면 이미 정렬된 것으로 본다. 그렇지 않은 경우에는
2. 분할(divide): 정렬되지 않은 리스트를 절반으로 잘라 비슷한 크기의 두 부분 리스트로 나눈다.
3. 정복(conquer): 각 부분 리스트를 재귀적으로 합병 정렬을 이용해 정렬한다.
4. 결합(combine): 두 부분 리스트를 다시 하나의 정렬된 리스트로 합병한다.
이때 정렬 결과가 임시배열에 저장된다.
5. 복사(copy): 임시 배열에 저장된 결과를 원래 배열에 복사한다.
public static void mergeSort(int[] arr, int[] tmp, int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
mergeSort(arr, tmp, left, mid);
mergeSort(arr, tmp, mid + 1, right);
merge(arr, tmp, left, right, mid);
}
}
public static void merge(int[] arr, int[] tmp, int left, int right, int mid) {
int p = left;
int q = mid + 1;
int idx = p;
// p, q 둘 중 하나는 해당 배열 범위 안에 있는 동안
while (p <= mid || q <= right) {
if (p <= mid && q <= right) { // 둘 다 범위 안인 경우
if (arr[p] <= arr[q]) { // 값 비교하여 정렬
tmp[idx++] = arr[p++];
} else {
tmp[idx++] = arr[q++];
}
} else if (p <= mid && q > right) { // 왼쪽만 남은 경우 이어주기
tmp[idx++] = arr[p++];
} else { // 오른쪽만 남은 경우 이어주기
tmp[idx++] = arr[q++];
}
}
for (int i = left; i <= right; i++) { // 정렬된 부분 데이터 arr 쪽으로
arr[i] = tmp[i];
}
}
최대 힙 트리나 최소 힙 트리를 구성해 정렬을 하는 방법으로서, 내림차순 정렬을 위해서는 최소 힙을 구성하고 오름차순 정렬을 위해서는 최대 힙을 구성하면 된다.
public static void heapSort(int[] arr) {
// 힙으로 재구성 (maxHeap 기준, 마지막 부모 노드부터)
for(int i = arr.length / 2 - 1; i >= 0; i--) {
heapify(arr, i, arr.length);
}
// maxHeap 기준 root 쪽을 끝 쪽으로 보내면서 재정렬 -> 오름차순
for(int i = arr.length - 1; i > 0; i--) {
swap(arr, 0, i);
heapify(arr, 0, i);
}
}
public static void heapify(int[] arr, int parentIdx, int size) {
// 자식 노드 위치
int leftIdx = 2 * parentIdx + 1;
int rightIdx = 2 * parentIdx + 2;
int maxIdx = parentIdx;
// 왼쪽 자식 노드가 크면 maxIdx 를 해당 인덱스로 교체
if(leftIdx < size && arr[maxIdx] < arr[leftIdx]) {
maxIdx = leftIdx;
}
// 오른쪽 자식 노드가 크면 maxIdx 를 해당 인덱스로 교체
if(rightIdx < size && arr[maxIdx] < arr[rightIdx]) {
maxIdx = rightIdx;
}
// maxIdx 가 바뀐 경우, 부모 노드가 교체되는 것을 의미
// 교체하고 서브 트리 검사 하도록 재귀 호출
if(parentIdx != maxIdx) {
swap(arr, maxIdx, parentIdx);
heapify(arr, maxIdx, size);
}
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
퀵 정렬은 분할 정복(divide and conquer) 방법을 통해 리스트를 정렬한다.
1. 리스트 가운데서 하나의 원소를 고른다. 이렇게 고른 원소를 피벗이라고 한다.
2. 피벗 앞에는 피벗보다 값이 작은 모든 원소들이 오고, 피벗 뒤에는 피벗보다 값이 큰 모든 원소들이 오도록 피벗을 기준으로 리스트를 둘로 나눈다. 이렇게 리스트를 둘로 나누는 것을 분할이라고 한다. 분할을 마친 뒤에 피벗은 더 이상 움직이지 않는다.
3. 분할된 두 개의 작은 리스트에 대해 재귀(Recursion)적으로 이 과정을 반복한다. 재귀는 리스트의 크기가 0이나 1이 될 때까지 반복된다.
재귀 호출이 한번 진행될 때마다 최소한 하나의 원소는 최종적으로 위치가 정해지므로, 이 알고리즘은 반드시 끝난다는 것을 보장할 수 있다.

public static void quickSort(int[] arr, int left, int right) {
if(left >= right) {
return;
}
// 분할
int pivot = partition(arr, left, right);
// 기준값 중심으로 좌우 재귀 호출
quickSort(arr, left, pivot - 1);
quickSort(arr, pivot + 1, right);
}
public static int partition(int[] arr, int left, int right) {
// 가장 좌측 값을 기준값으로 설정
// 이외에 기준 값은 배열에서 중간에 있는 값을 고르거나, 임의의 수를 3개 선정 후 중앙 값을 고르는 등의 방법이 있음
int pivot = arr[left];
int i = left;
int j = right;
while(i < j) {
while (arr[j] > pivot && i < j) {
j--;
}
while(arr[i] <= pivot && i < j) {
i++;
}
swap(arr, i, j);
}
swap(arr, left, i);
return i;
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
삽입정렬의 아래 성질을 보완한 정렬이다.
삽입 정렬은 입력되는 초기리스트가 "거의 정렬"되어 있을 경우 효율적이다.
삽입 정렬은 한 번에 한 요소의 위치만 결정되기 때문에 비효율적이다.
셸 정렬은 주어진 자료 리스트를 특정 매개변수 값의 길이를 갖는 부파일(subfile)로 쪼개서, 각 부파일에서 정렬을 수행한다. 즉, 매개변수 값에 따라 부파일(Subfile)이 발생하며, 매개변수값을 줄이며 이 과정을 반복하고 결국 매개변수 값이 1이면 정렬은 완성된다.
셸 정렬은 다음과 같은 과정으로 나눈다.
1. 데이터를 십수 개 정도 듬성듬성 나누어서 삽입 정렬한다.
2. 데이터를 다시 잘게 나누어서 삽입 정렬한다.
3. 이렇게 계속 하여 마침내 정렬이 된다.
셸 정렬에서 데이터를 나누는 값(이하 N)은 보통 전체에서 2로 나누는 값으로 진행한다. 그러나 3을 나누고 1을 더하는 경우가 더 빠르다고 알려져 있다. 즉 N/2 보다는 N/3+1이 더 빠르다.
public static void shellSort(int[] arr) {
// 초기 간격
int gap = arr.length / 2;
// 초기 간격부터 간격 반씩 줄여가면서 진행
for (int g = gap; g > 0; g /= 2) {
for (int i = g; i < arr.length; i++) {
int tmp = arr[i];
int j = 0;
for (j = i - g; j >= 0; j -= g) {
if (arr[j] > tmp) {
arr[j + g] = arr[j];
} else {
break;
}
}
arr[j + g] = tmp;
}
}
}
