스택의 개념

한 쪽 끝에서만 자료를 넣고 뺄 수 있는 LIFO(Last In First Out) 형식의 자료 구조.

스택의 연산

스택(Stack)은 LIFO(Last In First Out)를 따른다. 즉, 가장 최근에 스택에 추가된 항목이 가장 먼저 제거될 항목이다.

  • pop() : 스택에서 가장 위에 있는 항목을 제거한다.
  • push(item) : item 하나를 스택의 가장 윗 부분에 추가한다.
  • peek() : 스택의 가장 위에 있는 항목을 반환한다.
  • isEmpty() : 스택이 비어 있을 때에 true를 반환한다.

스택의 구현

문제의 종류에 따라 배열보다 스택에 데이터를 저장하는 것이 더 적합한 방법일 수 있다.

Array 기반 스택

  • 접근 속도가 빠르지만 변경이 용이하지 않다.
  • 배열이 생성될 때, 메모리의 연속된 공간에 데이터를 저장함 → 검색할 때는 데이터를 빠르게 찾을 수 있지만, 변경이 일어났을 때는 새로운 배열을 생성하고, 생성된 배열에 데이터를 복사해야하기 때문에 느려진다.
  • 복사해야할 데이터가 많지 않다면, 크게 상관없지만 데이터가 많아질수록 점점 느려질 것이다.

Linked List 기반 스택

  • 메모리 주소를 통해 노드가 연결되어 있어, 배열 기반과 달리 상수 시간에 i번째 항목에 접근할 수 없다. 하지만 스택에서 데이터를 추가하거나 삭제하는 연산은 상수 시간에 가능하다.
  • 배열처럼 원소들을 하나씩 옆으로 밀어 줄 필요가 없다.
  • 메모리에 연속된 공간에 존재하지 않기 때문에 검색 데이터를 찾기 위해 노드를 순회해야하므로 검색 속도는 좋지 않다.

검색이 자주 일어나고, 등록, 수정 등 변경이 자주 일어나지 않는다면, 배열 스택으로 구현하는 것이 좋다. 반대로 검색보다는 등록, 수정이 빈번히 일어나는 경우에는 연결리스트 스택을 사용하는 것이 좋다.

코드 구현(python)

stack = list()
sp = -1

def stack_pop(stack:list, sp:int):
	
	if sp < 0:
		return False
	
	return stack[:sp], stack[sp], sp-1

def stack_push(stack:list, num:int, sp:int):
	
	stack.append(num)
	sp += 1
	
	return stack, sp

def isEmpty(sp):
	return sp == -1
	

# idx 호출. 스택 초기화 추가.

메모리 스택

메모리의 스택(stack) 영역은 함수의 호출과 관계되는 지역 변수와 매개 변수가 저장되는 영역이다. 스택 영역은 호출과 함께 할당되며, 함수의 호출이 완료되면 소멸한다.

이렇게 스택 영역에 저장되는 함수의 호출 정보를 스택 프레임(stack frame)이라고 한다. 스택 영역은 push 동작으로 데이터를 저장하고, pop 동작으로 데이터를 인출한다. 이러한 스택은 후입선출(LIFO, Last-In First-Out) 방식에 따라 동작하므로, 가장 늦게 저장된 데이터가 가장 먼저 인출된다.

스택 영역은 메모리의 높은 주소에서 낮은 주소의 방향으로 할당된다.

출처 : http://www.tcpschool.com/c/c_memory_stackframe
출처 : http://www.tcpschool.com/c/c_memory_stackframe

스택 오버플로우(stack overflow)

앞서 함수의 재귀호출이 반복되면, 해당 프로그램은 스택 오버플로우(stack overflow)에 의해 종료된다.

만약 재귀 호출이 무한히 반복되면, 재귀 호출에 의한 스택 프레임이 계속해서 쌓여만 갈 것이다.

이렇게 스택의 모든 공간을 다 차지하고 난 후 더 이상의 여유 공간이 없을 때 또 다시 스택 프레임을 저장하게 되면, 해당 데이터는 스택 영역을 넘어가서 저장되게 된다.

출처 : http://www.tcpschool.com/c/c_memory_stackframe

이렇게 해당 스택 영역을 넘어가도 데이터가 저장될 수 있으면, 해당 프로그램은 오작동을 하게 되거나 보안상의 큰 취약점을 가지게 된다.

따라서 스택 오버플로우가 발생하면, 에러를 발생하고 곧바로 강제 종료시킨다.

Abstract

일반적인 방법으로 구현했을 때, 이 정렬은 안정 정렬(Stable Sort)에 속하며, 분할 정복 알고리즘의 하나입니다.

하나의 리스트를 두 개의 균등한 크기로 분할하고 분할된 부분 리스트를 정렬한 다음, 두 개의 정렬된 부분 리스트를 합하여 전체가 정렬된 리스트가 되게 하는 방법입니다.

  • 분할(Divide) : 입력 배열을 같은 크기의 2개의 부분 배열로 분할한다.
  • 정복(Conquer) : 부분 배열을 정렬한다. 부분 배열의 크기가 충분히 작지 않으면 순환 호출을 이용하여 다시 분할 정복 방법을 적용한다.
  • 결합(Combine) : 정렬된 부분 배열들을 하나의 배열에 합병한다.

Process

출처 : 위키백과

합병 정렬은 추가적인 리스트를 필요로 한다.

  1. 리스트의 길이가 0 또는 1이면 이미 정렬된 것으로 본다.
  2. 그렇지 않은 경우에는 정렬되지 않은 리스트를 절반으로 잘라 비슷한 크기의 두 부분 리스트로 나눈다.
  3. 각 부분 리스트를 재귀적으로 합병 정렬을 이용해 정렬한다.
  4. 두 부분 리스트를 다시 하나의 정렬된 리스트로 합병한다.

Python Code

def mergeSort(arr:list):

    def recursion(arr:list):

        if len(arr) == 1 or len(arr) == 0:		# 1.
            return arr

        mid = len(arr) // 2

        recursion_a = recursion(arr[:mid])		# 2.
        recursion_b = recursion(arr[mid:])
        
        idx_a = 0
        idx_b = 0
        temp = []

        while idx_a < len(recursion_a) and idx_b < len(recursion_b):		# 3.
            if recursion_a[idx_a] < recursion_b[idx_b]:
                temp.append(recursion_a[idx_a])
                idx_a += 1
            else:
                temp.append(recursion_b[idx_b])
                idx_b += 1

        if idx_a == len(recursion_a):
            temp += recursion_b[idx_b:]
        else:
            temp += recursion_a[idx_a:]

        return temp

    return recursion(arr)

 

시간 복잡도

레코드의 개수 n이 2의 거듭제곱이라고 가정($n=2^k)$했을 때, 깊이에 따라서 다음과 같다. $2^3 \rightarrow 2^2 \rightarrow 2^1 \rightarrow 2^0$ 순으로 줄어들게 된다. 이것을 일반화하게 되면 $n=2^k$가 되고 결국 $k=log_2(n)$ 라는 것을 알 수 있다.

 

합치는 연산을 계산해보면 배열의 크기가 $1 \rightarrow 2 \rightarrow 4 .... \rightarrow n$순으로 증가하여 일반화하면 최대 n번의 비교를 하게 된다.

따라서 깊이 $log(n)$과 합치는 연산 $n$을 곱하여 $nlog(n)$이 된다.

'cs 기초 공부 > 알고리즘' 카테고리의 다른 글

퀵 정렬(Quick Sort)  (0) 2021.03.28
삽입 정렬(Insertion Sort)  (0) 2021.03.26
선택 정렬 (Selection Sort)  (0) 2021.03.26
정렬 기초 공부1 (Bubble Sort)  (0) 2021.03.25

Abstract

기본적으로 $O(N^2)으로 정렬하는 알고리즘(Ex : 버블 정렬, 선택 정렬, 삽입 정렬)바꾸는 기준이 순회흘 하면서 바뀌며, 일반적으로 for문의 중첩으로 $O(N^2)의 복잡도를 가지게 된다. 하지만 퀵 정렬 알고리즘은 직관적으로 보았을 때, 비교를 반씩 줄여나가면서 정렬하는 알고리즘으로 즉, 재귀를 이용한 분할정복 알고리즘이다.

 

  • 분할(Divide) : 입력 배열을 피벗을 기준으로 비균등하게 2개의 부분 배열(피벗을 중심으로 왼쪽: 피벗보다 작은 요소들, 오른쪽 : 피벗보다 큰 요소들)로 분할한다.
  • 정복(Conquer) : 부분 배열을 정렬한다. 부분 배열의 크기가 충분히 작지 않으면 순환 호출을 이용하여 다시 분할 정복 방법을 적용한다.
  • 결합(Combine) : 정렬된 부분 배열들을 하나의 배열에 합병한다.
  • 순환 호출이 한번 진행될 때마다 최소한 하나의 원소(피벗)가 최종적으로 위치가 정해지므로, 이 알고리즘은 반드시 끝난다는 것을 보장할 수 있다.

 

Process

출처 : 위키백과

  1. 특정한 기준을 통해 Pivot 값을 지정한다.(먼저 아래 코드와 설명에서는 가장 앞의 값을 기준으로 잡겠습니다.)
  2. Pivot을 기준으로 작으면 왼쪽으로, Pivot보다 크면 오른쪽으로 이동한다.
  3. 이동하는 방식은 
  4. Pivot 위치를 고정하고 왼쪽에 있는 값들을 다시 1번부터 실행, 오른쪽에 있는 값들을 1번부터 실행한다.
  5. 양쪽 값이 1개인 경우 멈추게 된다.

Python Code

def quickSort(arr:list):

    
    def recursion(temp:list):
    
        if len(temp) == 1 or len(temp) == 0:		# 6.
            return temp
        
        pivot_idx = 0				# 1.
        pivot = temp[pivot_idx]
        
        low = pivot_idx + 1
        high = len(temp) - 1
        low_change, high_change = False, False
        
        while high > pivot_idx and low < len(temp) and low <= high:		# 2.
        
            if temp[low] < pivot:
                low += 1
            else:
                low_change = True
            
            if temp[high] > pivot:
                high -= 1
            else:
                high_change = True
                
            if low_change and high_change:					# 3.
                temp[low], temp[high] = temp[high], temp[low]
                low_change, high_change = False, False
    
        temp[pivot_idx], temp[high] = temp[high], temp[pivot_idx]	# 4.
        return recursion(temp[:high]) + [temp[high]] + recursion(temp[high+1:])		# 5.

    return recursion(arr)

 

  1. 피벗을 선택합니다. 이 코드에서는 가장 앞에 위치한 값을 피벗으로 선택했습니다.
  2. 1번 값을 low, 마지막 값을 high로 설정하고 피벗보다 작은 경우 왼쪽으로, 피벗보다 큰 경우 오른쪽으로 이동합니다. low와 high값이 교차하는 순간 모든 값에 대한 피벗과 비교가 끝납니다.
  3. 2번에서 왼쪽에서 피벗보다 큰 경우와 오른쪽에서 피벗보다 큰 경우 서로 값을 교환(swap)합니다.
  4. 2번이 종료된 경우 high 위치의 값과 피벗을 교환(swap)합니다.
  5. 재귀적으로 왼쪽과 오른쪽을 다시 피벗을 설정하고 정렬을 시작합니다.
  6. 더이상 교체할 값이 없는 경우 종료합니다.

시간 복잡도

최선의 경우 아래와 이미지와 같아진다.

순환 호출의 깊이

레코드의 갯수가 n개라고 가정하고 n은 2의 제곱이라고 가정한다면, $2^3\rightarrow 2^2 \rightarrow 2^1 \rightarrow 2^0$식으로 전개가 된다. 이를 일반화하면 $n=2^k$가 되고 $k=log_2n$가 된다.

순환 호출 단계의 비교 연산

각 순환 호출에서는 해당 리스트의 전체와 비교해야한다. 위의 코드를 살펴보면 피벗값은 가장 처음으로 설정되고 이후 low와 high를 통해 모든 값을 비교하므로 n으로 연산할 수 있다.

 

따라서 퀵 정렬의 시간 복잡도는 $O(nlogn)$이다.

 

모든 정렬에서 이상적인 계산으로는 퀵정렬이 가장 빠르다. 하지만 퀵정렬은 최악의 경우 $O(n^2)$이 걸리는데 그 이유는 아래와 같다.

 

리스트가 계속 불균형하게 나누어지는 경우이다. 특히, 이미 정렬된 리스트에 대하여 퀵 정렬을 실행하는 경우이다.

이런 경우 리스트의 깊이가 n번이 되므로 $O(n^2)$가 된다. 이를 해결하기 위한 방법으로는 피벗의 기준을 바꾸면 해결된다.

바꾸는 방식은 대표적으로 피벗의 기준을 가운데 값으로 하게 되면 항상 2 부분으로 나뉘게 되며 $log(n)$이 된다.

 

'cs 기초 공부 > 알고리즘' 카테고리의 다른 글

합병 정렬(Merge Sort)  (0) 2021.03.28
삽입 정렬(Insertion Sort)  (0) 2021.03.26
선택 정렬 (Selection Sort)  (0) 2021.03.26
정렬 기초 공부1 (Bubble Sort)  (0) 2021.03.25

해당 글은 gyoogle 님 github를 참고해서 작성한 글입니다.

 

GimunLee/tech-refrigerator

🍰 기술 냉장고입니다. 🛒 기술 면접 , 전공 시험 , 지식 함양 등 분명 도움될 거예요! 🤟 - GimunLee/tech-refrigerator

github.com

 

 

삽입 정렬(Insertion Sort)

 

Abstract

손 안의 카드를 정렬하는 방법과 유사합니다.

Insertion Sort는 Selection Sort와 유사하지만, 좀 더 효율적인 정렬 알고리즘입니다.

Insertion Sort는 2번째 원소부터 시작하여 그 앞(왼쪽)의 원소들과 비교하여 삽입할 위치를 지정한 후, 원소를 뒤로 옮기고 지정된 자리에 자료를 삽입하여 정렬하는 알고리즘입니다.

최선의 경우 $O(n)$이라는 엄청나게 빠른 효율성을 가지고 있어, 다른 정렬 알고리즘의 일부로 사용될 만큼 좋은 정렬 알고리즘입니다.

 

Process

  1. 정렬은 2번째 위치(index)의 값을 temp에 저장합니다.
  2. temp와 이전에 있는 원소들과 비교하여 삽입해나갑니다.
  3. '1'번으로 돌아가 다음 위치(index)의 값을 temp에 저장하고, 반복합니다.

 

Python Code

def insertionSort(arr:list):

    for i in range(1, len(arr)):		# 1.
        prev = i - 1
        temp = arr[i]
        while prev >= 0 and arr[prev] > temp:			# 2.
            arr[prev+1] = arr[prev]
            prev -= 1
                
        arr[prev+1] = temp				# 3.

    return arr
  1. 첫 번째 원소 앞(왼쪽)에는 어떤 원소도 갖고 있지 않기 때문에, 두 번째 위치(index)부터 탐색을 시작합니다. temp에 임시로 해당 위치(index) 값을 저장하고, prev에는 해당 위치(index)의 이전 위치(index)를 저장합니다.
  2. 이전 위치(index)를 가리키는 prev가 음수가 되지 않고, 이전 위치(index)의 값이 '1'번에서 선택한 값보다 크다면, 서로 값을 교환해주고 prev를 더 이전 위치(index)를 가리키도록 합니다.
  3. '2'번에서 반복문이 끝나고 난 뒤, prev에는 현재 temp 값보다 작은 값들 중 제일 큰 값의 위치(index)를 가리키게 됩니다. 따라서, (prev+1)에 temp 값을 삽입해줍니다.

 

시간 복잡도

최악의 경우(역으로 정렬되어 있을 경우) Selection Sort와 마찬가지로, (n-1) + (n-2) + ... + 2 + 1 -> n(n-1)/2 즉, $O(n^2)$입니다.

하지만, 모두 정렬이 되어있는 경우(Optimal)한 경우, 한 번씩 밖에 비교를 안 하므로 $O(n)$의 시간 복잡도를 가지게 됩니다. 또한, 이미 정렬되어 있는 배열에 자료를 하나씩 삽입/제거하는 경우에는, 현실적으로 최고의 정렬 알고리즘이 되는데, 탐색을 제외한 오버헤드가 매우 적기 때문입니다.

 

최선의 경우는 $O(n)$의 시간복잡도를 갖고, 평균과 최악의 경우 $O(n^2)$의 시간 복잡도를 갖게 됩니다.

 

위의 코드를 자세히 살펴보면,

  1. for문 n-1번 반복
  2. prev 입력, temp 입력
  3. while문 1, 2, .... 최대 n-1번 반복
  4. prev값을 prev+1로 이동, prev - 1
  5. 적절한 위치에 temp값 입력
  6. 계산해보면 $\Sigma^{n-1}{(O(1)+O(1)+ (\Sigma^{n-1}{O(1)+O(1)}) + O(1))}$
  7. 결과적으로, $O(n^2)$

공간 복잡도

주어진 배열 안에서 교환(swap)을 통해, 정렬이 수행되므로 $O(n)$ 입니다.

 

장점 & 단점

  1. 장점
    • 알고리즘이 단순합니다.
    • 대부분의 원소가 이미 정렬되어 있는 경우, 매우 효율적일 수 있습니다.
    • 정렬하고자 하는 배열 안에서 교환하는 방식이므로, 다른 메모리 공간을 필요로 하지 않습니다. -> 제자리 정렬(in-place sorting)
    • 안정 정렬(Stable Sort)
    • Selection Sort나 Bubble Sort와 같은 $O(n^2)$ 알고리즘에 비교하여 상대적으로 빠릅니다.
  2. 단점
    • 평균과 최악의 시간 복잡도가 $O(n^2)$으로 비효율적입니다.
    • Bubble Sort와 Selection Sort와 마찬가지로, 배열의 길이가 길어질수록 비효율적입니다.

Conclusion

Selection Sort와 Insertion Sort는 k번째 반복 이후, 첫 번째 k요소가 정렬된 순서로 온다는 점에서 유사합니다. 하지만, Selection Sort는 k+1번째 요소를 찾기 위해 나머지 모든 요소들을 탐색하지만 Insertion Sort는 k+1번째 요소를 배치하는 데 필요한 만큼의 요소만 탐색하기 때문에 훨씬 효율적으로 실행된다는 차이가 있습니다.

 

'cs 기초 공부 > 알고리즘' 카테고리의 다른 글

합병 정렬(Merge Sort)  (0) 2021.03.28
퀵 정렬(Quick Sort)  (0) 2021.03.28
선택 정렬 (Selection Sort)  (0) 2021.03.26
정렬 기초 공부1 (Bubble Sort)  (0) 2021.03.25

이 글은 gyoogle님의 github에 있는 자료를 바탕으로 작성되었습니다.

 

GimunLee/tech-refrigerator

🍰 기술 냉장고입니다. 🛒 기술 면접 , 전공 시험 , 지식 함양 등 분명 도움될 거예요! 🤟 - GimunLee/tech-refrigerator

github.com

Abstract

Selection Sort는 Bubble Sort와 유사한 알고리즘으로, 해당 순서에 원소를 넣을 위치는 이미 정해져 있고, 어떤 원소를 넣을지 선택하는 알고리즘입니다.

Selection Sort와 Insertion Sort를 헷갈려하시는 분들이 종종 있는데, Selection Sort는 배열에서 해당 자리를 선택하고 그 자리에 오는 값을 찾는 것이라고 생각하시면 편합니다.

 

Process

출처 위키백과

  1. 주어진 배열 중에 최소값을 찾습니다.
  2. 그 값을 맨 앞에 위치한 값과 교체합니다.
  3. 맨 처음 위치를 뺀 나머지 배열을 같은 방법으로 교체합니다.

 

Python Code

def selectionSort(arr:list):
	for i in range(len(arr)):			# 1.
		minimum_val, minimum_idx = 0, 0
		for j in range(i+1, len(arr)):		# 2.
        	
			if minimum_val > arr[j]:		# 3.
				minimum_val = arr[j]
				minimum_idx = j
		arr[i], arr[minimum_idx] = arr[minimum_idx], arr[i]		# 4.
    
    
    return arr
  1.  우선, 위치(idx)를 선택합니다.
  2. 선택한 위치의 값과 i+1번째 원소부터 차례대로 비교를 시작합니다.
  3. 오름차순으로 현재 선택한 자리에 있는 값보다 순회하고 있는 값이 작다면, 위치(idx)를 갱신합니다.
  4. [2]번 반복문이 끝난 뒤에 선택한 위치의 값과 기존 가장 작은 값을 서로 교환(swap)해줍니다.

시간 복잡도

데이터의 개수가 n개라고 했을 때,

  • 첫 번째 회전에서의 비교횟수 : 1~(n-1) -> n-1
  • 두 번째 회전에서의 비교횟수 : 2 ~ (n-1) -> n-2

최종적으로 $(n-1)+(n-2)+....+2+1 \rightarrow n(n-1)/2$

비교하는 것이 상수 시간에 이뤄진다는 가정 아래, n개의 주어진 배열을 정렬하는데 $O(n^2)$만큼의 시간이 걸립니다. 최선, 평균, 최악의 경우 시간복잡도는 $O(n^2)$로 동일합니다.

 

위의 코드를 통해 자세히 계산해보면 아래와 같습니다.

  1. n번 반복
  2. n-1, n-2, ...., 2, 1번 반복
  3. if문 및 이하는 1번 실행
  4. swap 부분 1번 실행
  5. 식으로 표현하면 $\Sigma \Sigma (O(1)+O(1)+O(1)) + O(1)$ 입니다.
  6. 따라서 시간복잡도는 $O(n^2)$ 입니다.

공간 복잡도

주어진 배열 안에서 교환(swap)을 통해, 정렬이 수행되므로 $O(n) 입니다.

 

장점 & 단점

  1. 장점
    • Bubble sort와 마찬가지로 알고리즘이 단순하다.
    • 정렬을 위한 비교 횟수는 많지만, Bubble Sort에 비해 실제로 교환하는 횟수는 적기 때문에 많은 교환이 일어나야하는 자료 상태에서 비교적 효율적입니다.
    • Bubble Sort와 마찬가지로 정렬하고자 하는 배열 안에서 교환하는 방식이므로, 다른 메모리 공간을 필요로 하지 않습니다. -> 제자리 정렬(in-place sorting)
  2. 단점
    • 시간 복잡도가 $O(n^2)$로 비효율적입니다.
    • 불안정 정렬(Unstable Sort)입니다.

추가적인 왜?

Unstable Sort가 정확히 무엇이지?

  1. Stable Sort(안정 정렬)
    • 비교 대상의 Key가 여러개 값을 가질 때 혹은 같은 값을 가지는 데이터가 여러개가 있을 때, 정렬 후에 기존의 순서를 유지하는 정렬.
    • 즉, 정렬되지 않은 상태에서 같은 키 값을 가진 원소의 순서가 정렬 후에도 유지되느냐 이다.
    • Bubble Sort, Insertion Sort, Merge Sort, Counting Sort, Bucket Sort, Radix Sort
  2. Unstable Sort(불안정 정렬)
    • 비교 대상의 Key가 여러개 값을 가질 때 혹은 같은 값을 가지는 데이터가 여러개가 있을 떄, 정렬 후에 기존의 순서가 유지 되지 않는 정렬
    • 예를 들어서 다음 5 3 1' 9 2 1이 있다면 1 1' 2 3 5 9 와 1' 1 2 3 5 9가 나올 수 있다.
    • Selection Sort
    • Shell Sort
    • Heap Sort
    • Quick Sort

 

'cs 기초 공부 > 알고리즘' 카테고리의 다른 글

합병 정렬(Merge Sort)  (0) 2021.03.28
퀵 정렬(Quick Sort)  (0) 2021.03.28
삽입 정렬(Insertion Sort)  (0) 2021.03.26
정렬 기초 공부1 (Bubble Sort)  (0) 2021.03.25

Abstract

정렬에는 많은 종류의 정렬이 있지만 해당 포스트에서는 아래 6가지 정렬을 정리하려고 합니다.

  1. $O(n^2)$
    • Bubble Sort, 거품 정렬
    • Selection Sort, 선택 정렬
    • Insertion Sort, 삽입 정렬
  2. $O(nlogn)$
    • Quick Sort, 퀵 정렬
    • Merge Sort, 병합 정렬
    • Heap Sort, 힙 정렬

해당 글은 gyoogle 님 github의 tech-interview-for-developer를 기반으로 작성합니다.

코드는 파이썬으로 작성하려 노력합니다.

 

gyoogle/tech-interview-for-developer

👶🏻 신입 개발자 전공 지식 & 기술 면접 백과사전 📖. Contribute to gyoogle/tech-interview-for-developer development by creating an account on GitHub.

github.com

거품 정렬 (Bubble Sort)

Abstract

Bubble Sort는 Selection Sort와 유사한 알고리즘으로 서로 인접한 두 원소의 대소를 비교하고, 조건에 맞지 않다면 자리를 교환하며 정렬하는 알고리즘입니다. 아래는 위키백과에서 나오는 무작위 배열 수의 정렬 예시입니다.

출처 : https://ko.wikipedia.org/wiki/%EA%B1%B0%ED%92%88_%EC%A0%95%EB%A0%AC

Process

  1.  1회전에 첫 번째 원소와 두 번째 원소를, 두 번째 원소와 세 번째 원소를..... 이런 식으로 (마지막-1) 번째 원소와 마지막 원소를 비교하여 조건에 맞지 않는다면 서로 교환합니다.
  2.  1회전을 수행하고 나면 가장 큰 원소가 맨 뒤로 이동하므로 2회전에서는 맨 끝에 있는 원소는 정렬에서 제외도, 2회전을 수행하고 나면 끝에서 두 번째 원소까지는 정렬에서 제외됩니다. 이렇게 정렬을 1회전 수행할 때마다 정령에서 제외되는 데이터가 하나씩 늘어납니다.

Python Code(Ascending)

def bubbleSort(arr:list):
	
    for i in range(len(arr)):				# 1.
    	for j in range(len(arr)-i-1):			# 2.
        	if arr[j] > arr[j+1]:			# 3.
            	arr[j], arr[j+1] = arr[j+1], arr[j]
    
    return arr
  1.  제외될 원소의 갯수를 의미합니다. 1회전이 끝났을 때, 배열의 마지막에 가장 큰 원소가 위치하므로 1씩 증가합니다.
  2.  비교할 원소의 idx를 지정하는 for문입니다. 1회전이 끝났을 때, 마지막 값 1개가 고정, 2회전이 끝났을 때, 마지막 값 2개가 고정.... 이므로 전체 길이에서 i를 뺀 만큼 반복을 실행합니다.
  3. 현재 idx가 가르키고 있는 2개의 값을 비교합니다. 위의 코드는 오름차순 정렬이므로 현재 원소가 다음 원소보다 큰 경우 서로 자리를 교환합니다.

 

시간 복잡도

시간복잡도를 계산하면, (n-1) + (n-2) + (n-3) + ....... + 2 + 1 -> n(n-1)/2 이므로 O(n^2)입니다. 또한, Bubble Sort는 정렬이 돼있던 안돼 있던, 2개의 원소를 비교하기 때문에 최선, 평균, 최악의 경우 모두 시간 복잡도가 O(n^2)로 동일합니다.

 

코드를 활용해 더 자세하게 계산해보자.

def bubbleSort(arr:list):
	
    for i in range(len(arr)):				# 1.
    	for j in range(len(arr)-i-1):			# 2.
        	if arr[j] > arr[j+1]:			# 3.
        		arr[j], arr[j+1] = arr[j+1], arr[j]	# 4.
    
    return arr # 5.

먼저 arr의 갯수를 n으로 생각한다.

  1. for i in range(len(arr)) -> n번 반복
  2. for j in range(len(arr)-i-1) -> n번, n-1, n-2 ......, 2, 1번 반복 -> 최대 n번 반복
  3. 1번 실행하므로 O(1)
  4. 1번 실행하므로 O(1)
  5. 이를 계산해보면 $\Sigma^n \Sigma^n {(O(1) + O(1))}$
  6. 결과적으로 $O(n^2)$

 

공간 복잡도

주어진 배열 안에서 교환(swap)을 통해, 정렬이 수행되므로 O(n)입니다.

 

장점 & 단점

  1. 장점
    • 구현이 매우 간단하고, 소스코드가 직관적이다
    • 정렬하고자 하는 배열 안에서 교환하는 방식이므로, 다른 메모리 공간을 필요로 하지 않는다. -> 제자리 정렬(in-place sorting)
    • 안정 정렬(stable Sort)이다.
  2. 단점
    • 시간복잡도가 최악, 최선, 평균 모두 O(n^2)로, 굉장히 비효율적이다.
    • 정렬돼있지 않은 원소가 정렬됐을 때의 자리로 가기 위해서, 교환 연산(swap)이 많이 일어나게 된다.

 

추가적으로 생각해보는 왜?

Bubble sort와 Selection sort가 유사하다고 하는데 어떤 점에서 유사할까?

'cs 기초 공부 > 알고리즘' 카테고리의 다른 글

합병 정렬(Merge Sort)  (0) 2021.03.28
퀵 정렬(Quick Sort)  (0) 2021.03.28
삽입 정렬(Insertion Sort)  (0) 2021.03.26
선택 정렬 (Selection Sort)  (0) 2021.03.26

+ Recent posts