Tech/Algorithm

[자료구조] 재귀함수, 정렬

claovy☘️ 2025. 3. 16. 03:18

1. 재귀함수(Recursive Function)

  • 함수 안에서 자기 자신을 다시 호출하는 기법
  • 함수 호출이 반복되다가 기저조건이 만족되면 호출을 중단하고 결과를 반한환다

 

 

재귀함수의 종료 조건

  • 재귀 함수를 풀이에 사용할 때는 재귀 함수의 종료 조건을 반드시 명시해야 한다
  • 종료조건을 명시하지 않을 시에는 함수가 무한히 호출된다
def recursive_fun():
	print('재귀함수 호출')
    recursive_fun()
    
recursive_fun()

[출력]
재귀함수 호출
재귀함수 호출
재귀함수 호출
재귀함수 호출
재귀함수 호출
재귀함수 호출
재귀함수 호출
재귀함수 호출
재귀함수 호출
...
...
...

 

재귀함수의 구성

  • 기저 조건(종료조건)
    • 재귀 호출을 중단하는 조건
    • 코드가 무한 루프에 빠지지 않도록 방지하며, 일반전으로 간단한 케이스에 해당
  • 재귀 단계
    • 함수가 자신을 호출하며 더 작은 부분으로 나누는 과정

 

팩토리얼 예제

 

팩토리얼의 정의는 n! = n x (n - 1) x (n - 2) x (n - 3) ...  x 1 입니다. 위의 식과 같이 n이 1이 아닌 5라면 5!은 5 x 4! 을 호출하게 됩니다. 문제는 컴퓨터에는 ! 의 개념이 없습니다. 따라서 4! 은 다시 4 x 3! 으로 쪼개지게 됩니다. 이러한 부분에서 팩토리얼은 자신을 호출하는 형태가 반복되기 때문에 순환 알고리즘의 특성을 띕니다.

import sys
n = int(sys.stdin.readline().strip())

def factorial(n):
    if n <= 1: # 기저 조건
        return 1
    else: # 재귀 단계
        return n * factorial(n - 1)
    
print(factorial(n))

 

피보나치 수열 예제

  • 이전 두 합으로 이루어지는 수열 
  • 종료조건 : n <= 1
  • 재귀단계 : f(n) = f(n-1) + f(n-2) 
def f(n):
	if n <= 1: # 기저 조건
    	return n
    else:
    	return f(n-1) + f(n-2) # 재귀 단계

 

 

유클리드 호제법 (Greatest Common Divisor) 예제 

  • 두 개의 자연수에 대한 최대공약수를 구하는 알고리즘
  • 종료조건 : a % b == 0
  • 재귀단계 : gcd(b, a % b)

두 자연수 A, B에 대하여 A % B의 결과를 R이라고 정의하자. 이때, A와 B의 최대공약수는 B와 R의 최대공약수와 같다 

단계 A B R
1 192 162 30
2 162 30 12
3 30 12 6
4 12 6 0

 

def gcd(a, b):
	if a % b == 0: # 기저 조건
    	return b
    else:
    	return gcd(b, a % b) # 재귀 단계
        
print(gcd(192, 162))

[출력]
6

 

 

재귀함수 문제풀이

- 백준 1914 하노이탑

https://www.acmicpc.net/problem/1914

- 백준 2947 나무조각

https://www.acmicpc.net/problem/2947

 

 

정리

이렇게 재귀 함수를 잘 활용하면 복잡한 알고리즘을 간결히 사용할 수 있습니당

하지만 모든 재귀함수가 반복문보다 유리한 것은 아니므로 필요성에 따라 알고리즘을 적절히 활용하도록 해요☺️


02. 정렬

01. 버블정렬

  • 매번 연속된 두개 인덱스를 비교하여, 정한 기준의 값을 뒤로 넘겨 정렬하는 방법 

array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]

for i in range(1, len(array)):
    for j in range(0, len(array) - i):
        if array[j] > array[j + 1]:
            array[j], array[j + 1] = array[j + 1], array[j]
print(array)

 

⌛버블정렬의 시간복잡도 / 공간복잡도

[시간복잡도]

  • n개의 배열 사이클이 n-1, n-2 .. 상태로 반복되므로 O(n²
  • 현재 리스트의 데이터가 거의 정렬된 상태라면 매우 빠르게 동작되므로 O(n)

[공간복잡도]

O(n)

 

02. 선택정렬

  • 가장 작은 요소를 위치를 변수에 저장하고 맨 앞 데이터와 swap을 반복
  • 버블에 비해 싸이클마다 1번의 swap만 일어난다는 점에서 효율적

array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]

for i in range(len(array)):
	min_index = i
    for j in range(i+1, len(array)):
    	if array[min_index] > array[j]:
        	min_index = j
    array[i], array[min_index] = array[min_index], array[i] # swap
    
 print(array)
🔎 만약 배열이 [5, 3, 1, 4, 2] 라고 치자. 연산횟수를 세어보자

1 cycle  배열 전체를 검사하며 최소값 1 찾기, 5와 교환 (연산횟수 4번)
> [1, 3, 5, 4, 2

2 cycle  최소값 2찾기, 3과 교환 (연산횟수 3번)
> [1 | 2, 5, 4, 3

3 cycle 최소값 3찾기, 5와 교환 (연산횟수 2번)
> [1, 2 | 3, 4, 5

4 cycle 최소값 4찾기, 비교 후 정렬 완료 (연산횟수 1번)
> [1, 2, 3 | 4, 5

 

⌛선택정렬의 시간복잡도 / 공간복잡도

[시간복잡도]

  • 선택정렬은 N번 만큼 가장 작은 수를 찾아서 앞으로 보냄 
  • N + (N - 1) + (N - 2) + ... + 2 + 1     =>         => O(n²)

[공간복잡도]

  • 하나의 배열에서만 진행되므로 O(n)이다.

03. 삽입정렬

  • 현재 위치에서 그 이하의 배열들을 비교하여 자신의 위치를 찾아 삽입하는 정렬 알고리즘 
  • 선택 정렬에 비해 구현 난이도가 높지만, 효율적임

array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]

for i in range(1, len(array)):
	for j in range(i, 0, -1):
    	if array[j] < array[j - 1]:
        	array[j], array[j - 1] = array[j - 1], array[j]
        else:
        	break
            
print(array)

 

⌛삽입정렬의 시간복잡도 / 공간복잡도

[시간복잡도]

  • 선택 정렬과 마찬가지고 반복문이 두번 중첩되어 사용되므로 O(n²)
  • 현재 리스트의 데이터가 거의 정렬된 상태라면 매우 빠르게 동작되므로 O(n)

[공간복잡도]

O(n)

 

04. 합병 정렬

  • 함수가 호출될 때마다 재귀적으로 함수를 호출하고 정렬된 부분을 두 개씩 병합하면서 정렬하는 방법
  • 큰 데이터셋을 정렬할 때 유용 
# task 1. 절반씩 나눈다
>before :         array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]
>after  :         Left [7, 5, 9, 0, 3]   Right [1, 6, 2, 4, 8]


# task 2. 원소가 하나 남을때까지 쪼갠다
> [7] [5] [9] [0] [3]        [1] [6] [2] [4] [8]

# task 3. 나눈 부분을 차례대로 각각 정렬한다
# 임시공간에 리스트를 비교 후 병합
>before     [7] [5] [9] [0] [3]      [1] [6] [2] [4] [8]
            ㅁㅁ
            [5 7] [0 9] [3] 
            ㅁㅁㅁㅁ
            [0 5 7 9] [3]
            ㅁㅁㅁㅁㅁ
            [0 3 5 7 9]				[1 2 4 6 8]
            
>after  	[0 1 2 3 4 5 6 7 8 9]
def merge_sort(arr):
    # 배열의 길이가 1 이하이면 이미 정렬된 상태이므로 바로 반환
    if len(arr) <= 1:
        return arr
    
    # 배열을 두 부분으로 나눈다
    mid = len(arr) // 2
    left_half = arr[:mid]
    right_half = arr[mid:]

    # 재귀적으로 왼쪽과 오른쪽을 정렬
    left_sorted = merge_sort(left_half)
    right_sorted = merge_sort(right_half)

    # 두 정렬된 배열을 병합
    return merge(left_sorted, right_sorted)

def merge(left, right):
    sorted_arr = []
    i = j = 0

    # 두 배열을 비교하여 작은 값을 차례대로 sorted_arr에 넣음
    while i < len(left) and j < len(right):
        if left[i] < right[j]:
            sorted_arr.append(left[i])
            i += 1
        else:
            sorted_arr.append(right[j])
            j += 1

    # 나머지 요소들을 그대로 추가
    sorted_arr.extend(left[i:])
    sorted_arr.extend(right[j:])

    return sorted_arr

array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]
sorted_array = merge_sort(array)
print(sorted_array)

 

05. 퀵 정렬

  • 피벗(기준 데이터)를 설정하고, 피벗보다 큰 데이터와 작은 데이터의 위치를 바꾸는 방법
  • 병합 정렬과 더불어 대부분의 프로그래밍 언어의 정렬 라이브러리의 근간이 되는 알고리즘

# 동작과정

1. 리스트의 한 요소 피벗을 선택(처음 or 중간 or 마지막)

2. 피벗을 기준으로 작은 요소는 왼쪽, 큰 요소는 오른쪽

3. 피벗을 제외한 왼쪽리스트와 오른쪽 리스트는 1,2번을 반복 

array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]

def quick_sort(arr):
    # 배열이 비었거나 길이가 1이면 이미 정렬된 상태
    if len(arr) <= 1:
        return arr
    
    # 피벗 선택 (여기서는 배열의 첫번째 원소를 피벗으로 선택)
    pivot = arr[0]
    another = arr[1:]
    
    # 피벗을 기준으로 왼쪽에는 피벗보다 작은 값, 오른쪽에는 큰 값들이 오도록 배열을 나눈다
    left = [x for x in another if x <= pivot]
    right = [x for x in another if x > pivot]
    
    # 재귀적으로 왼쪽과 오른쪽을 정렬하고, 피벗을 중간에 추가
    return quick_sort(left) + [pivot] + quick_sort(right)

sorted_array = quick_sort(array)
print(sorted_array)

 

정리

[정렬 종류별 장단점]

[정렬종류 별 시간, 공간 복잡도]

정렬 종류 평균 Best Worst 공간 복잡도
선택 정렬 O(n²) O(n²) O(n²) O(n)
버블 정렬 O(n²) O(n) O(n²) O(1)
삽입 정렬 O(n²) O(n) O(n²) O(1)
합병 정렬 O(n log n) O(n log n) O(n log n) O(n)
퀵 정렬 O(n log n) O(n log n) O(n²) O(log n)

 

'Tech > Algorithm' 카테고리의 다른 글

백준 1149 rgb 거리 [python]  (0) 2025.04.07
백준 1914 하노이 탑 [python]  (0) 2025.03.23
백준 2947번 나무조각 [python]  (0) 2025.03.21