공부하면서 문제를 풀어 본 버블정렬, 삽입정렬, 합병정렬을 제외한 다른 정렬들에 대해서 정리해보고자 한다.
정렬의 경우, 알고리즘에서 자주 나오는 개념이며 이후에도 활용하기 좋은 개념이다.
선택정렬은 이름 그대로, 배열을 순회하면서 가장 작은 수를 찾아 선택하여, 배열의 맨 앞부터 순회해서 기준 수가 더 작은 경우 그 순회하는 숫자와 자리를 바꿔준다.
선택정렬에 대한 추가 설명(참고 블로그)
해당 정렬은 버블정렬만큼이나 직관적으로 이해하기 쉽지만, 비효율적인 정렬방법이다.
하지만 버블정렬이나 선택정렬같은 경우, 요소의 수가 적을 때는 오히려 더 효율적이다.
따라서 시간복잡도는 n^2이 될 것이다.
const selectionSort = function(array) {
for (let i = 0; i < array.length - 1; i++) {
// 처음부터 훑되, 가장 마지막 인덱스 돌기전에는 자연스레 오름차순으로 정렬
//되어있을 테니까 길이의 -1 직전 까지 순회한다.
for (let j = i + 1; j < array.length; j++) {
// 최솟값의 위치를 찾음
//i+1 부터 시작하는 이유는 내가 현재 위치한 수의 이전까지는 이미 정렬되어있거나
//자기 자신이므로 크기를 비교할 필요가 없다.
// 가장 작은 수를 찾기 위한 순회 이므로, 이 경우에는 배열 끝까지 다 돌아야 한다.
if (array[i] > array[j]) {
//현재 위치한 인덱스의 수보다(지금은 첫번째 인덱스) 순회도는 인덱스의 수가 작으면
let temp = array[j]; // 최솟값을 저장
array[j] = array[i];
array[i] = temp; // 최솟값을 제일 앞으로 보내기
}
}
}
return array;
};
삽입정렬을 보완한 알고리즘이다.
블로그에 있는 삽입정렬에 대한 설명
삽입 정렬의 최대 문제점은 요소들이 삽입될 때, 이웃한 위치로만 이동하게 되는데,
만약 삽입되어야 할 위치가 현재 위치에서 상당히 멀리 떨어진 곳이라면
많은 이동을 해야만 제자리로 갈 수 있다. 즉 효율성이 떨어진다.
셸정렬은 gap을 잘못 설정할 경우, 시간복잡도가 n^2가 나온다.
function shellSort(arr) {
let gap = Math.floor(arr.length / 2);
//여기서 gap은 무조건 홀수여야 한다.
while (gap > 0) {
if(gap%2 ===0){
gap += 1
}
for (let i = 0; i < arr.length - gap; i++) {
//gap을 기준으로 나눈 부분 리스트들을 순회하는 거라서 종료는 길이에서 gap까지
let currentIndex = i;
let gapShiftedIndex = i + gap;
while(currentIndex >=0){
//해당 반복문을 넣어주고 아래에, 인덱스 교체 작업을 해주는 이유는,
//전체 배열이 gap으로 완전히 나누어 떨어지지 않는 경우에도 작업을 해주기 위해서이다.
//부분 리스트의 수가 두개가 아니라 여러개인 경우,
//첫번째와 두번째 부분리스트를 비교하고, 두번째와 세번째 부분리스트를 비교하기 위함이다.
if (arr[gapShiftedIndex] <= arr[currentIndex]) {
const temp = arr[currentIndex];
arr[currentIndex] = arr[gapShiftedIndex];
arr[gapShiftedIndex] = temp;
}
gapShiftedIndex = currentIndex
currentIndex -=gap
}
}
gap = Math.floor(gap / 2);
}
return arr;
}
매우 빠른 속도의 정렬 방법이다. 합병정렬과 비슷하게 주어진 배열을 분리하고 크기 비교를 하지만,
균등하게 쪼개지 않고 랜덤하게 하나의 요소를 선택해서 그것을 기준으로 비균등하게 쪼갠다.
주어진 배열에서 랜덤하게 한 수를 뽑고(이를 pivot(피벗)이라 부른다.),
이 수를 기준으로 작은 수는 왼쪽에 큰수는 오른쪽에 둔다.
그리고 이러한 정렬을 재귀로 계속 돌리면서 피벗을 뽑고, 그 기준으로 부분리스트를 나누어 정렬하는 것을 반복한다.
퀵정렬에 대한 추가 설명(참고 블로그)
그렇다면 실제 알고리즘을 푸는 과정은 어떻게 될까?
const partition = function(array, left, right, pivotIndex) { // 정렬하는 부분
var temp;
var pivot = array[pivotIndex];
while (left <= right) {
// 왼쪽 인덱스와 오른쪽 인덱스가 같을때까지, 즉 만날때까지 서로 순회를 돈다
while (array[left] < pivot)
//왼쪽에 위치한 수가 피벗보다 작은 경우에만 왼쪽 인덱스를 하나씩 증가하며 순회
left++;
while (array[right] > pivot)
//오른쪽에 위치한 수가 피벗보다 큰 경우에만 오른쪽 인덱스를 하나씩 감소하며 순회
right--;
if (left <= right) {
// 왼쪽이 기준보다 크고, 오른쪽이 기준보다 작으면
//위의 while문에서 벗어날 것이다. 이때 여전히 왼쪽 인덱스가 오른쪽 인덱스보다 작을때,
//왼쪽이 기분보다 크고, 오른쪽이 작다는 의미가 되므로 자리를 바꿔준다.
temp = array[left];
array[left] = array[right];
array[right] = temp; // 서로 바꿔줍니다.
left++;
right--;
}
}
temp = array[left];
array[left] = array[pivotIndex];
array[pivotIndex] = temp;
// 마지막으로 기준과 만난 수를 바꿔줍니다. 기준의 위치는 이제 left입니다.
return left;
};
const quickSort = function(array, left, right) { // 재귀하는 부분
if (!left) left = 0;
//처음시작하는 경우 왼쪽 오른쪽으로 나누지 않았으니, 임의로 왼쪽인덱스는 0,
//그리고 오른쪽 인덱스는 가장 끝 인덱스이다.
if (!right) right = array.length - 1;
var pivotIndex = right;
// 배열 가장 오른쪽의 수를 기준으로 뽑습니다.
pivotIndex = partition(array, left, right - 1, pivotIndex);
// right - 1을 하는 이유는 기준(현재 right)을 제외하고 정렬하기 위함입니다.
if (left < pivotIndex - 1)
quickSort(array, left, pivotIndex - 1); // 기준 왼쪽 부분 재귀
if (pivotIndex + 1 < right)
quickSort(array, pivotIndex + 1, right); // 기준 오른쪽 부분 재귀
return array;
};
퀵정렬의 경우, 위의 코드를 보면 알다시피, 반복문이 두번 돌아간다.
따라서 최악의 경우, 시간 복잡도는 n^2이나, 빠르게 돌면 NlogN의 시간복잡도를 가진다.