매일 업데이트
2023-09-15 12:15 13 min

코딩 인터뷰를 위한 튜토리얼

데이터 정렬의 중요성과 JavaScript 배열 정렬 알고리즘

데이터 목록을 정렬하는 것은 애플리케이션 개발에서 매우 중요한 부분입니다. 이는 정보 표시 및 검색 효율성을 향상시키는 데 필수적입니다. 따라서 훌륭한 소프트웨어 엔지니어라면 배열을 정렬하는 다양한 방법을 숙지하고 있어야 합니다. 이 글에서는 JavaScript에서 배열을 정렬하는 데 사용되는 몇 가지 주요 알고리즘을 살펴보겠습니다.

정렬이란 무엇이며, 왜 중요할까요?

출처: 언스플래시

정렬이란, 주어진 기준에 따라 값을 체계적으로 배열하는 과정입니다. 이 순서는 오름차순이나 내림차순일 수 있으며, JavaScript에서 배열을 정렬하는 것은 데이터의 의미를 명확하게 만들어 줍니다. 예를 들어, 최신 파일부터 정렬된 파일 목록을 보거나, 가격별로 정렬된 제품 목록을 확인할 수 있습니다. 또한, 정렬된 데이터에서만 효율적으로 작동하는 이진 검색과 같은 알고리즘을 사용할 때도 정렬은 필수적입니다.

JavaScript에는 데이터를 쉽게 정렬할 수 있는 내장 함수와 라이브러리가 존재하지만, 코딩 인터뷰나 하위 수준 코드를 작성해야 할 때는 정렬 알고리즘이 내부적으로 어떻게 작동하는지 이해하는 것이 중요합니다.

JavaScript 배열 정렬 알고리즘

버블 정렬

버블 정렬은 이해하고 구현하기 가장 쉬운 알고리즘 중 하나입니다. 이 알고리즘은 배열을 여러 번 반복하면서, 각 반복마다 인접한 두 요소를 비교하여 순서가 잘못된 경우 서로 교환합니다. 배열의 요소 수만큼 반복하며, 각 반복이 끝날 때마다 배열의 끝부분부터 정렬됩니다. 오름차순으로 숫자를 정렬하기 위한 의사 코드는 다음과 같습니다.

1. 배열의 요소 수를 n이라고 합니다.
2. i를 사용하여 반복 횟수를 추적하면서 n번 반복합니다.(각 반복에서 다음 작업을 수행합니다.)
   a. 배열의 두 번째 요소부터 (n-i)번째 요소까지 반복합니다.
   b. 이전 요소가 현재 요소보다 크면 두 요소를 교환합니다.

이를 JavaScript 코드로 표현하면 다음과 같습니다.

function sort(arr) {
    const n = arr.length;

    for (let i = 0; i < n; i++) {
        for (let j = 1; j < n - i; j++) {
            if (arr[j - 1] > arr[j]) {
                const temp = arr[j - 1];
                arr[j - 1] = arr[j];
                arr[j] = temp;
            }
        }
    }
    
    return arr;
}

이 알고리즘의 동작 방식을 더 명확하게 이해하기 위해 각 패스에서 배열이 어떻게 변경되는지 console.log를 사용하여 확인하는 것이 좋습니다. 아래 코드는 각 패스마다 배열의 변화를 출력하도록 수정된 버블 정렬 함수와, 이 함수를 사용하여 정렬된 작은 배열의 예시를 보여줍니다.

function sort(arr) {
    const n = arr.length;

    for (let i = 0; i < n; i++) {
	console.log(`Pass: ${i}`);

        for (let j = 1; j < n - i; j++) {
            if (arr[j - 1] > arr[j]) {
                const temp = arr[j - 1];
                arr[j - 1] = arr[j];
                arr[j] = temp;
            }
	
	    console.log(arr);
        }
    }
    
    return arr;
}

const array = [9, 2, 7, 4, 1];
sort(array);

console.log(array);

위 코드를 실행한 결과는 다음과 같습니다.

버블 정렬의 시간 복잡도는 O(n^2)으로, 각 패스마다 배열을 반복하는 n번의 패스를 수행하기 때문입니다. 따라서 데이터 크기가 커질수록 효율성이 떨어집니다. 하지만, 추가적인 메모리를 거의 사용하지 않고 배열 요소를 제자리에서 수정하므로 공간 복잡도는 O(1)입니다.

삽입 정렬

삽입 정렬은 또 다른 인기 있는 JavaScript 배열 정렬 알고리즘입니다. 이 알고리즘은 배열의 두 번째 요소부터 시작하여 각 요소를 이미 정렬된 부분에 올바른 위치에 삽입하는 방식으로 작동합니다. 배열의 각 요소를 선택하여 num이라고 하면, num 왼쪽에 있는 모든 숫자가 num보다 작아질 때까지 num을 왼쪽으로 이동시킵니다. 이 과정을 배열의 두 번째 요소부터 마지막 요소까지 반복하면 모든 숫자가 정렬됩니다. 다음은 의사 코드로 표현한 설명입니다.

1. 배열의 요소 수를 n이라고 합니다.
2. i를 1부터 n-1까지 반복합니다.(두 번째 요소부터 시작)
   a. currentElement를 array[i]로 설정합니다.
   b. j를 i - 1로 설정합니다.
   c. j가 0보다 크거나 같고 array[j]가 currentElement보다 크면 다음을 반복합니다.
      i. array[j]를 array[j+1]로 이동합니다.
      ii. j를 1 감소시킵니다.
   d. array[j+1]를 currentElement로 설정합니다.

다음은 위 의사 코드를 JavaScript로 구현한 코드입니다.

function insertionSort(array) {
  const n = array.length;

  for (let i = 1; i < n; i++) {
    const currentElement = array[i];
    let j = i - 1;

    while (j >= 0 && array[j] > currentElement) {
      array[j + 1] = array[j];
      j -= 1;
    }

    array[j + 1] = currentElement;
  }

  return array;
}

버블 정렬과 마찬가지로, console.log를 추가하여 알고리즘의 작동 방식을 시각화하는 것이 도움이 됩니다. 아래 코드는 삽입 정렬이 어떻게 작동하는지 보여줍니다.

function sort(array) {
    const n = array.length;

    for (let i = 1; i < n; i++) {
        const currentElement = array[i];
        let j = i - 1;
        console.log("Placing element:", currentElement);

        while (j >= 0 && array[j] > currentElement) {
            array[j + 1] = array[j];
            j -= 1;
        }

        array[j + 1] = currentElement;
        console.log("Placed it at position:", j + 1);
        console.log(array);
    }

    return array;
}

const array = [4, 1, 2, 5, 3];
sort(array);

위 코드 조각을 실행하면 다음과 같은 결과가 출력됩니다.

병합 정렬

삽입 정렬과 버블 정렬은 O(n^2)의 시간 복잡도를 가지는 반면, 병합 정렬은 O(n * log(n))의 시간 복잡도를 갖는 준선형 알고리즘입니다. 병합 정렬은 분할 정복 방식을 사용합니다. 먼저 배열을 요소가 하나만 남을 때까지 재귀적으로 더 작은 배열로 나눕니다. 분할된 배열은 이후 다시 병합되는데, 이 때 하위 배열들이 정렬된 상태로 합쳐집니다.

병합 과정은 두 개의 정렬된 배열을 합치는 것과 동일한 방식으로 진행됩니다. 병합 정렬 알고리즘의 의사 코드는 다음과 같습니다.

1. 배열의 길이가 1 이하이면 배열을 반환합니다.(기본 케이스)
2. 중간 인덱스를 찾습니다.
   a. mid를 (배열 길이 / 2)의 내림 값으로 설정합니다.
3. 배열을 두 개의 하위 배열로 나눕니다.
   a. leftArray를 만들고 배열의 첫 번째 절반을 복사합니다.(인덱스 0부터 mid까지)
   b. rightArray를 만들고 배열의 두 번째 절반을 복사합니다.(인덱스 mid+1부터 끝까지)
4. leftArray에 대해 MergeSort를 재귀적으로 호출합니다.
5. rightArray에 대해 MergeSort를 재귀적으로 호출합니다.
6. 정렬된 두 개의 하위 배열을 병합합니다.
   a. 빈 resultArray를 만듭니다.
   b. leftArray와 rightArray가 모두 비어 있지 않은 동안 다음을 반복합니다.
      i. leftArray의 첫 번째 요소가 rightArray의 첫 번째 요소보다 작거나 같으면 해당 요소를 resultArray에 추가합니다.
      ii. 그렇지 않으면 rightArray의 첫 번째 요소를 resultArray에 추가합니다.
   c. leftArray에 남아 있는 모든 요소를 resultArray에 추가합니다.(있는 경우)
   d. rightArray에 남아 있는 모든 요소를 resultArray에 추가합니다.(있는 경우)
7. resultArray를 반환합니다.

이를 JavaScript로 구현한 결과는 다음과 같습니다.

function sort(array) {

	// Base case in which we stop subdividing the array
	if (array.length == 1) {
		return array;
	}

	// Finding the middle point of the array
	const m = Math.round(array.length / 2);

	// Divide the array into two halves
	const leftUnsorted = array.slice(0, m);
	const rightUnsorted = array.slice(m);

	// Recursively call merge sort
	const leftSorted = sort(leftUnsorted);
	const rightSorted = sort(rightUnsorted);

	// Return a merged sorted array
	return merge(leftSorted, rightSorted);
}

function merge(left, right) {
	// Merge two sorted lists
	let result = [];
	let leftIndex = 0;
	let rightIndex = 0;

	while (leftIndex < left.length && rightIndex < right.length) {
		if (left[leftIndex] < right[rightIndex]) {
			result.push(left[leftIndex]);
			leftIndex += 1;
		} else {
			result.push(right[rightIndex]);
			rightIndex += 1;
		}
	}

	return result.concat(left.slice(leftIndex), right.slice(rightIndex));
}

예시 배열을 사용하여 코드를 실행하면 정상적으로 작동하는 것을 확인할 수 있습니다.

퀵 정렬

병합 정렬과 마찬가지로 퀵 정렬도 분할 정복 전략을 사용합니다. 이 알고리즘은 배열에서 피벗 요소를 선택한 다음, 피벗보다 작은 요소는 왼쪽으로, 큰 요소는 오른쪽으로 이동시키는 방식으로 작동합니다. 이 과정이 완료되면 피벗은 배열 내에서 올바른 위치에 있게 됩니다.

피벗 주변의 요소를 재배치하기 위해, 퀵 정렬은 우선 피벗을 배열의 끝으로 이동시킵니다. 그 후, 배열의 왼쪽부터 시작하여 피벗보다 큰 첫 번째 숫자를 찾고, 동시에 배열의 오른쪽부터 시작하여 피벗보다 작은 첫 번째 숫자를 찾습니다. 두 숫자를 찾으면 서로 교환합니다. 이 과정을 왼쪽 포인터가 오른쪽 포인터보다 커질 때까지 반복합니다.

이후, 마지막으로 피벗과 교환한 두 숫자 중 큰 숫자를 피벗과 교환합니다. 이 시점에서 피벗은 올바른 위치에 있게 되며, 피벗보다 작은 숫자는 왼쪽, 큰 숫자는 오른쪽에 위치합니다. 이 과정은 하위 배열에 요소가 하나만 남을 때까지 피벗의 왼쪽과 오른쪽 하위 배열에 대해 재귀적으로 반복됩니다.

퀵 정렬의 의사 코드는 다음과 같습니다.

1. lessThanPointer가 greaterThanPointer보다 작으면 다음을 수행합니다.
   a. 배열에서 피벗 요소를 선택합니다.
   b. 작은 요소는 왼쪽으로, 큰 요소는 오른쪽으로 이동하도록 요소를 재배치합니다.
   c. 왼쪽 하위 배열에 대해 Quicksort를 재귀적으로 호출합니다.
   d. 오른쪽 하위 배열에 대해 Quicksort를 재귀적으로 호출합니다.

다음은 위 의사 코드를 JavaScript로 구현한 것입니다.

function sort(array, low, high) {
    if (low < high) {
        // Choose the pivot index and partition the array
        const pivotIndex = move(array, low, high);

        // Recursively sort the subarrays to the left and right of the pivot
        sort(array, low, pivotIndex - 1);
        sort(array, pivotIndex + 1, high);
    }
}

function move(array, low, high) {
    // Select the pivot element (in this case, the last element)
    const pivotElement = array[high];

    // Initialize the index for the smaller element
    let i = low - 1;

    for (let j = low; j < high; j++) {
        // If the current element is less than or equal to the pivot, swap it with the element at index i+1
        if (array[j] <= pivotElement) {
            i += 1;
            const temp = array[i];
            array[i] = array[j];
            array[j] = temp;
        }
    }

    // Swap the pivot element into its correct position
    const temp = array[i];
    array[i] = array[j];
    array[j] = temp;

    // Return the index of the pivot element
    return i + 1;
}

Node.js 환경에서 퀵 정렬을 사용하여 예시 배열을 정렬하면 다음과 같은 결과를 얻을 수 있습니다.

최선의 경우 퀵 정렬은 준선형 시간 복잡도로 실행됩니다. 또한, 퀵 정렬의 공간 사용량은 로그 함수에 비례하여 증가하므로 다른 JavaScript 배열 정렬 알고리즘에 비해 비교적 효율적입니다.

코딩 인터뷰를 위한 팁

❇️ 연습은 필수입니다. 다양한 알고리즘을 학습하는 것도 중요하지만, 문제 해결 능력과 컴퓨팅 사고력을 키우는 것이 더욱 중요합니다. 리트코드알고 엑스퍼트와 같은 플랫폼을 이용하여 꾸준히 연습하는 것이 좋습니다.

❇️ 문제에 바로 접근하기보다 먼저 스스로 해결하려고 노력하십시오. 이 과정을 통해 문제 해결 능력을 향상시킬 수 있습니다.

❇️ 만약 문제를 너무 오래 붙잡고 있다면, 해결 방법을 참조하십시오. 솔루션 분석을 통해 문제 해결 방법을 배울 수 있습니다. 많은 학습 플랫폼에서 솔루션을 제공하며, ChatGPT나 Google Bard와 같은 도구도 개념 이해에 도움이 될 수 있습니다.

❇️ 코드를 바로 작성하기보다는 먼저 화이트보드에 해결 방법을 설계하고 충분히 생각해보는 것이 좋습니다. 의사 코드를 사용하여 아이디어를 정리하는 것도 유용한 방법입니다.

결론

이 글에서는 몇 가지 널리 사용되는 정렬 알고리즘을 살펴보았습니다. 이러한 알고리즘을 모두 처음부터 배우는 것은 어려울 수 있으므로, 하나의 정보 출처에만 의존하기보다는 다양한 자료를 함께 참고하는 것이 좋습니다. 즐거운 코딩 학습 되시길 바랍니다!

다음으로는 Python의 정렬 함수를 이해하는 시간을 가져보시기 바랍니다.

저자
Korea

기술 트렌드와 실용적인 팁을 전하는 लेखक입니다.