정렬 알고리즘이란 ? 데이터 요소를 특정 순서로 나열하는 알고리즘.
정렬 알고리즘은 데이터를 특정 순서로 나열하는 데 사용되며, 위와같이 다양한 측면에서 유용하다. 정렬 알고리즘의 종류를 알아보기 전에 두 가지 주요한 정렬 특성인 stable정렬
과 in-place 정렬
에 대해 알아보자.
// stable 정렬 예시
let numbers = [1,4(첫번째),5,2,3,4(두번째)]
// 문자열을 기준으로 정렬
numbers.sort();
console.log(numbers)
// 결과
[ 1, 2, 3, 4(첫번째), 4(두번째), 5 ]
옆에 있는 데이터와 비교하여 더 작은 값을 앞으로 보내는 정렬. 즉, 가장 큰 값이 맨 뒤로 간다. 정렬 방식이 마치 물속에서 올라오는 물방울과 같다고하여 버블 정렬이라고 한다.
function bubbleSort(arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = 1; j < arr.length; j++) {
if (arr[j - 1] > arr[j]) {
[arr[j - 1], arr[j]] = [arr[j], arr[j - 1]];
}
}
}
return arr;
}
console.log(bubbleSort([1, 4, 3, 6, 5])) // [ 1, 3, 4, 5, 6 ]
In-place
정렬이다.O(n^2)
이다.선택 정렬은 배열을 처음부터 순회하여 가장 작은 수를 제일 앞에 둔다. 그리고 맨 앞의 값을 제외한 배열로 다시 반복하는 정렬.
function selectionSort(arr) {
let indexMin;
for (let i = 0; i < arr.length - 1; i++) {
indexMin = i;
for (let j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[indexMin]) {
indexMin = j;
}
}
[arr[i], arr[indexMin]] = [arr[indexMin], arr[i]];
}
return arr;
}
console.log(selectionSort([5,7,1,4,3])) // [ 1, 3, 4, 5, 7 ]
O(n^2)
. 비효율적이다.삽입 정렬은 i번째 원소를 정렬된 상태의 앞부분과 비교하여 적절한 위치에 삽입하는 방식.
배열의 마지막 원소까지는 모두 정렬된 상태여야 하며, 마지막부터 첫번째까지의 원소와 i번째 원소를 각각 비교한다.
i번째 원소보다 작은 값이 탐색되면 그 위치에 i번째 요소를 삽입한다.
function insertionSort(arr) {
for (let i = 1; i < arr.length; i++) {
let value = arr[i];
let prev = i - 1;
while (prev >= 0 && arr[prev] > value) {
arr[prev + 1] = arr[prev];
prev--;
}
arr[prev + 1] = value;
}
return arr;
}
console.log(insertionSort([5, 6, 1, 2, 4, 3]))
i + 1
번째 원소를 배치하는데 필요한 만큼의 원소만 탐색하기 때문에 비교적 효율적이다.O(n)
의 시간복잡도를 가진다.O(n^2)
분할 정복 방법(문제를 작은 2개의 문제로 분리하고 각각을 해결한 다음, 결과를 모아서 원래의 문제를 해결하는 전략)을 통한 정렬로, 하나의 pivot을 정해서 이 pivot보다 작은 값은 왼쪽에 큰값은 오른쪽에 위치시키는 방법.
최악의 경우를 제외하고 가장 빠른 정렬이다. 실제로 많이 사용되는 정렬 알고리즘.
퀵 정렬은 다음과 같은 단계로 이루어진다.
function partition(arr, left, right) {
// 피벗을 기준으로 작은 값은 왼쪽, 큰 값은 오른쪽으로 이동
const pivot = arr[right];
let i = left - 1;
for (let j = left; j < right; j++) {
if (arr[j] <= pivot) {
i++;
// 값 교환
const temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 피벗 위치 교환
const temp = arr[i + 1];
arr[i + 1] = arr[right];
arr[right] = temp;
return i + 1;
}
function quickSort(arr, left, right) {
if (left < right) {
// 피벗을 기준으로 배열을 분할
const partitionIndex = partition(arr, left, right);
// 분할된 부분을 정렬
quickSort(arr, left, partitionIndex - 1);
quickSort(arr, partitionIndex + 1, right);
}
}
console.log(quickSort([4,1,7,6,3,8,2,5])) // [1,2,3,4,5,6,7,8]
O(nlogn)
의 시간복잡도를 가지는 다른 정렬 알고리즘과 비교했을 때 가장 빠르다.O(n^2)
의 시간복잡도를 가진다.분할 정복 알고리즘 중 하나로, 배열을 반으로 나눈 후 각 부분을 재귀적으로 정렬하고, 정렬된 부분을 병합하는 방식.
O(n log n)
이기 때문에 성능이 준수하여 자주 사용된다.// 두 수 비교
function merge(left, right) {
let result = [];
let leftIndex = 0;
let rightIndex = 0;
while (leftIndex < left.length && rightIndex < right.length) {
result.push(
left[leftIndex] < right[rightIndex]
? left[leftIndex++]
: right[rightIndex++]
);
}
return result.concat(left.slice(leftIndex), right.slice(rightIndex));
}
// 재귀
function mergeSort(arr) {
if (arr.length <= 1) {
return arr;
}
const middle = Math.floor(arr.length / 2);
const left = arr.slice(0, middle);
const right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}
console.log(mergeSort([5, 2, 4, 7, 6, 1, 3, 8])) // [1, 2, 3, 4, 5, 6, 7, 8]
O(n log n)
의 시간복잡도를 가진다.최대힙 트리나 최소힙 트리를 구성하여 정렬하는 방식
힙은 일종의 트리로 수의 집합에서 가장 작은 수(키)나 가장 큰 수만을 자주 꺼내올때 유용한 자료구조
최대힙 : 완전 트리이면서 Root가 모든 경우에 자식들보다 커야한다.
최소힙 : 루트 값이 모든 경우에 자식들보다 작은 값.
랜덤한 순서의 숫자들을 힙(배열)으로 만들어 넣고, 힙에서 하나씩 제거하면 된다. 가장 큰 값 또는 가장 작은 값 순서로 빠지기 때문에 알아서 정렬된다.
function heapSort(arr) {
// 최대 힙 구성
buildMaxHeap(arr);
// 힙에서 요소 하나씩 추출하면서 정렬
for (let i = arr.length - 1; i > 0; i--) {
// 최대값(힙의 루트)을 배열의 끝으로 이동
swap(arr, 0, i);
// 힙 크기를 줄여 재구성
heapify(arr, 0, i);
}
return arr;
}
function buildMaxHeap(arr) {
const len = arr.length;
// 배열의 중간부터 시작하여 최대 힙 구성
for (let i = Math.floor(len / 2); i >= 0; i--) {
heapify(arr, i, len);
}
}
function heapify(arr, index, heapSize) {
const left = 2 * index + 1;
const right = 2 * index + 2;
let largest = index;
// 왼쪽 자식이 부모보다 크면 largest를 왼쪽 자식으로 설정
if (left < heapSize && arr[left] > arr[largest]) {
largest = left;
}
// 오른쪽 자식이 부모 또는 왼쪽 자식보다 크면 largest를 오른쪽 자식으로 설정
if (right < heapSize && arr[right] > arr[largest]) {
largest = right;
}
// largest가 변경되었을 경우 위치 교환 후 재귀적으로 heapify
if (largest !== index) {
swap(arr, index, largest);
heapify(arr, largest, heapSize);
}
}
function swap(arr, i, j) {
const temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
console.log(heapSort([5,3,1,4,7])) // [1,3,4,5,7]
O(nlogn)
의 시간복잡도를 가진다 = 성능이 준수하다각 항목을 세어 저장해두고, 이를 앞 순서대로 정렬하는 방식. 정수 혹은 정수로 표현할 수 있는 데이터에 대한 정렬 알고리즘
// EX
let arr = [3,1,6,3,4,1,5]
// 각 숫자가 몇 개 나오는지 세어보고
1: 2개,
3: 2개,
4: 1개,
5: 1개,
6: 1개
// 정렬하면
[1,1,3,3,4,5,6]
이처럼 개수를 센 뒤, 1부터 6까지 그대로 갯수대로 나열하는 방식이며, 요소값들을 비교하지 않고 정렬하는 방식이다.
function countingSort(arr) {
const max = Math.max(...arr);
const count = Array(max + 1).fill(0);
for (let i = 0; i < arr.length; i++) {
count[arr[i]]++;
}
const sortedArray = [];
for (let num = 0; num < count.length; num++) {
for (let i = 0; i < count[num]; i++) {
sortedArray.push(num);
}
}
return sortedArray;
}
console.log(countingSort([4, 2, 2, 8, 3, 3, 1])) // [1, 2, 2, 3, 3, 4, 8]
O(n)
의 시간복잡도를 가진다.각 자릿수를 낮은 자리수에서부터 가장 큰 자리수까지 올라가면서 정렬을 수행하는 방식
기수 정렬은 비교연산을 수행하지 않아 조건이 맞는 상황에서 빠른 정렬 속도를 보장한다.
입력 데이터의 최대값에 따른 계수 정렬의 비효율성을 개선하기 위해 기수 정렬을 사용할 수 있다. 자릿수의 값 별로 정렬을 하므로, 나올 수 있는 값의 최대 사이즈는 9이기 때문
bucket
배열을 하나 만들어, 1의 자리가 같은 원소들을 생성한 배열에 모은다.// 이 코드는 양의 정수 배열에 대해서만 동작한다.
// 음의 정수나 소수 같은 경우에는 추가적인 처리가 필요하다
function getMax(arr, size) {
let max = arr[0];
for (let i = 1; i < size; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
function countSort(arr, size, exp) {
const output = new Array(size).fill(0);
const count = new Array(10).fill(0);
for (let i = 0; i < size; i++) {
count[Math.floor(arr[i] / exp) % 10]++;
}
for (let i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
for (let i = size - 1; i >= 0; i--) {
output[count[Math.floor(arr[i] / exp) % 10] - 1] = arr[i];
count[Math.floor(arr[i] / exp) % 10]--;
}
for (let i = 0; i < size; i++) {
arr[i] = output[i];
}
}
function radixSort(arr, size) {
const max = getMax(arr, size);
for (let exp = 1; Math.floor(max / exp) > 0; exp *= 10) {
countSort(arr, size, exp);
}
}
console.log(radixSort([170, 45, 75, 90, 802, 24, 2, 66]))
// [2, 24, 45, 66, 75, 90, 170, 802]
O(n)
의 시간복잡도를 가진다