‘정렬되지 않은 배열이 주어졌을 때, 어떻게 오름차순으로 정렬할 수 있을까?’
정렬을 하고 난 후, 검색을 시작해야 효율성이 극대화 된다.!
매우 기본적인 정렬 알고리즘
배열 내에서 연속된 두 항목을 가리킨다. (1번째 원소, 2번째 원소)
두 항목의 순서가 뒤바뀌어 있으면 두 항목을 교환한다. (순서가 올바르다면 아무것도 하지않는다.) (*여기서 순서란 오름차순 -> 큰게 앞에 있으면 교환)
포인터를 오른쪽 한 셀씩 옮긴다.
배열의 끝까지 또는 이미 정렬된 값까지 1단계부터 3단계를 반복한다. (패스쓰루 - 첫패스쓰루시, 마지막 원소는 가장 값이 높은 값으로 배치된다. 변동 x)
이제 두 포인터를 다시 배열의 처음 두 값으로 옮겨서 1단계부터 4단계를 다시 실행함으로써, 새로운 패스스루를 실행한다. (교환이 일어나지 않는 패스스루가 생길때까지 패스스루를 반복한다. 교환할 항목이 없다는 것은 배열이 정렬됐고 문제를 해결했다는 뜻)
*두번째 패스스루시, 첫번째 패스스루를 통해 이미 올바른 위치로 간 마지막 원소와는 비교할 필요가 없다.
→ 각 패스스루마다 정렬되지 않은 값 중 가장 큰 값, “버블”이 올바른 위치로 가게된다.
패스스루란?
배열의 모든 요소를 한 번 순회하며, 연속된 두 항목을 비교하고 교환하는 단계를 의미한다. 즉, 배열의 처음부터 끝까지 또는 남은 정렬되지 않은 부분까지 한 번의 정렬 과정을 수행하는 것을 "패스스루"라고 한다.
function bubble_sort(list){
let unsorted_until_index = list.length -1;
let sorted = false; // 최적화를 위해
while(!sorted){ // 루프 한 번이 패스스루
sorted = true;
for(let i=0; i<unsorted_until_index; i++){
if(list[i]>list[i+1]){
let flag = list[i]
list[i]= list[i+1]
list[i+1] = flag
sorted = false;
}
}
unsorted_until_index -=1;
}
return list
}
구체적으로 설명하면:
sorted
는 리스트가 이미 정렬되었는지를 확인한다. 만약 리스트의 요소들이 한 번의 반복에서 모두 제자리에 있다면, sorted
를 true
로 유지하여 반복을 중단할 수 있다.sorted
가 없다면, 리스트가 정렬되었더라도 마지막까지 루프가 계속 진행된다. 이 변수를 통해 더 이상 교환이 일어나지 않으면 루프를 종료시켜 불필요한 연산을 방지할 수 있다.즉, sorted
는 최적화를 위한 중요한 역할을 한다.
sorted
의 필요성 구체적으로 이해하기정렬 상태 확인:
버블 정렬은 여러 번의 패스스루를 통해 리스트를 정렬한다.
만약 어떤 패스스루에서 교환(swap)이 한 번도 일어나지 않았다면, 리스트는 이미 정렬된 상태
sorted
변수를 사용하여 교환이 일어나지 않으면 루프를 종료할 수 있다.
불필요한 반복 방지:
리스트가 이미 정렬된 상태라면, 더 이상 패스스루를 실행할 필요가 없다.
sorted 변수가 없으면, 최대 O(n²)의 반복을 수행하게 되어 비효율적이다.
sorted가 있으면 최선의 경우 시간 복잡도가 O(n)으로 최적화!
예제 시나리오로 이해하기
리스트: [1, 2, 3, 4, 5] (이미 정렬됨)
첫 번째 패스스루:
for 루프에서 list[i] > list[i + 1] 조건을 검사.
비교는 하지만, 교환(swap)이 일어나지 않음.
sorted는 처음 true로 설정되고, 한 번도 false로 바뀌지 않음.
루프 종료:
while 루프의 조건 !sorted가 false가 되어 종료.
리스트가 이미 정렬되어 있으므로 한 번의 패스스루 후 종료.
비교 : 어느 쪽이 더 큰지 두 수를 비교한다.
교환 : 정렬하기 위해 두 수를 교환한다.
원소 N개 일 때, → N-1 + N-2 + N-3 + …. + 1 비교를 한다.
배열이 내림차순으로 정렬된 최악의 시나리오라면 비교할 때마다 교환을 해주어야한다.
즉, 10번의 비교가 있으면 10번 교환해주어야한다.
N(비교) * N(교환)
→ 버블정렬에는 값이 N개 일 때, N^2의 단계가 필요하고, 빅오 표기법으론 O(N^2) 이다.
O(N^2)은 상대적으로 느린 알고리즘으로 간주되기 때문에, 시간을 들여 더 빠른 대안은 없을지 깊이 고민해보기!
function hasDuplicateValue(array){
for(let i=0; i<array.length; i++){
for(let j=0; j<array.length; j++){
if(i !== j && array[i] === array[j]) return true;
}
}
return false;
}
최악의 시나리오 : 배열이 중복값을 포함하지 않는 경우
이 경우, 코드는 false를 반환하기 전까지 모든 루프를 수행해야하고, 가능한 모든 조합을 전부 비교한다.
결론적으로, 원소 N개가 있을 때 함수는 N^2 번의 비교를 수행한다.
시간복잡도 : O(N^2)
루프 내에 다른 루프를 중첩하는 알고리즘 → O(N^2)
function hasDuplicateValue(array){
let existingNumbers = [];
for(let i=0; i<array.length; i++){
if(existingNumbers[array[i]]===1){
return true;
}else{
existingNumbers[array[i]] = 1;
}
}
return false;
}
데이터 원소가 N개있을 때 비교를 N번한다. 단 하나의 루프에서 단지 배열에 있는 원소 수만큼 순회하기 때문
시간복잡도 : O(N)
(but, 단점: 첫번째 방식보다 메모리를 더 소비한다. ex. 만약 값이 2000이라면? 배열 index로 2000까지 생성된다.)
cf. 우테코 로또문제를 내가 이렇게 풀었다! 로또 45개 배열을 미리 만들어놓고, 당첨 번호들에만 1씩 채워서 매치하는 방식으로 당첨을 조회했다!
빅오표기법이 유일할까?
아니다!. 빅오표기법이 알고리즘을 서로 비교하고 주어진 상황에 알맞은 알고리즘을 결정하게 해주는 도구이지만, 실제로 빅오표기법에서는 한 알고리즘이 다른 알고리즘보다 훨씬 빠른 경우에도 두 경쟁 알고리즘을 정확히 똑같은 방식으로 표현하기도 한다.