Post

[Day14] Recursion, Two-pointer

[Day14] Recursion, Two-pointer

재귀 (Recursion)

  • 함수가 자기 자신을 호출하는 방식으로 문제를 해결하는 기법
  • 얼마나 반복되는지 모를 때 사용
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    public class Test {
      public static void main(String[] args) {
          int n=10;
          printNos(n);
      }
    	
      public static void printNos(int n) {
          if(n > 0) {
              printNos(n - 1);
              System.out.print(n + " ");
          }
      }
    }
    

    재귀를 사용하는 사례

    분할 정복 (Divide and Conquer)

  • 문제를 더 작은 하위 문제로 나누어 해결하는 방식이 필요할 때.
  • (ex) 퀵 정렬 (Quick Sort) 및 병합 정렬 (Merge Sort): 배열을 여러 부분으로 나누고 각각을 정렬한 후 다시 합치는 방식입니다.

트리 및 그래프 탐색

  • 트리나 그래프 구조에서 깊이 우선 탐색(DFS)과 같은 탐색 알고리즘이 필요할 때.
  • (ex) 이진 트리의 순회 (전위, 중위, 후위 순회)

순열, 조합, 부분집합 생성

  • 주어진 집합에서 가능한 모든 조합이나 순열을 생성해야 할 때.
  • (ex) 특정 숫자의 조합을 구하는 문제

백트래킹 (Backtracking)

  • 모든 가능한 경우를 탐색해야 하며, 특정 조건을 만족할 때 해결책을 찾는 경우
  • (ex) N-Queens 문제, 미로 찾기, 스도쿠 해결 등.

피보나치 수열

  • 앞의 두 수의 합이 바로 뒤의 수가 되는 수의 배열
  • (ex) F(n) = F(n-1) + F(n-2)로 정의되는 피보나치 수열.

동적 프로그래밍

  • 재귀적 구조가 필요한 문제에서 메모이제이션 기법을 사용할 경우
  • (ex) 특정 문제를 재귀적으로 해결하면서 이전에 계산한 결과를 저장하여 성능을 개선하는 경우

factiorial

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package recur;

public class FactorialTest {
	
	public static void main(String[] args) {
		int n=5;
		int result=factorial(n);
		System.out.println(result);
	}
	
	public static int factorial(int n) {
		if(n==0) return 1;
		return n * factorial(n-1);
	}
}

재귀의 장/단점

  • 장점
    • 코드가 간결하고 이해하기 쉬움
    • 복잡한 문제를 쉽게 해결 가능
  • 단점
    • 스택 메모리 사용 → 스택 오버플로우 발생 가능
    • 반복문보다 비효율적(성능 이슈)

투포인터

  • 주로 배열이나 리스트와 같은 선형 자료 구조에서 사용되는 알고리즘
  • 두 개의 포인터(left, right) 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
Q. 정렬된 배열 nums와 정수 target이 주어질 때, 두 수의 합이 target이 되는 두 수의 인덱스를 반환하세요
*/
public class TwoSum {

	public static void main(String[] args) {
		int []nums = {1,2,3,4,6};
		int target=6;
		
		int left=0;
		int right = nums.length-1;
		
		while(left<right) {
			int sum=nums[left]+nums[right];
			if(sum == target) {
				System.out.println(left+":"+right);
				break;
			}else if(sum < target) {
				left++;
			}else {
				right--;
			}
		}
	}

}

적용 사례

합을 찾는 문제

  • 두 포인터를 사용하여 특정 합을 찾는 경우, 한 포인터는 시작 지점에서, 다른 포인터는 끝 지점에서 시작하여 두 포인터를 이동시키며 조건을 만족하는 경우를 찾음.

중복 제거

  • 정렬된 배열에서 중복된 요소를 제거할 때, 하나의 포인터는 현재 위치를 가리키고 다른 포인터는 중복 여부를 확인하는 데 사용.

슬라이딩 윈도우

  • 배열이나 리스트와 같은 선형 자료 구조에서 연속적인 부분 배열이나 부분 문자열을 처리하는 데 유용한 알고리즘
  • 이 기법은 특정 범위를 효율적으로 탐색하고, 계산할 수 있음.

슬라이딩 윈도우는 두 개의 포인터 또는 인덱스를 사용하여 데이터의 일부를 선택하고, 이 선택된 범위를 “윈도우”라고 부름. 이 윈도우는 데이터의 특정 부분을 나타내며, 필요에 따라 크기나 위치를 조정할 수 있음.

특징

  • 연속성: 윈도우 내의 요소들은 항상 연속적.
  • 효율성: 윈도우의 크기를 변경하면서 반복적으로 계산할 수 있어, 중복 계산을 피하고 시간 복잡도를 줄임.
  • 상태 유지: 각 윈도우의 상태를 유지하면서 필요한 계산을 수행할 수 있음.
  • 시간 복잡도 감소: O(n)
  • 간결한 코드: 복잡한 중첩 루프를 줄이고, 코드의 가독성을 높임.

사용 예시

  • 최대/최소 합의 부분 배열 찾기: 연속된 K 개의 요소의 최대 합을 찾는 문제.
  • 문자열 문제: 주어진 문자열에서 특정 조건을 만족하는 부분 문자열의 길이나 개수를 찾는 문제.
  • 중복 제거: 특정 조건을 만족하는 부분 배열에서 중복 요소를 제거하는 문제.

슬라이딩 윈도우의 유형

  • 고정 크기 윈도우: 윈도우의 크기가 고정되어 있는 경우 (예: K 개의 요소).
  • 가변 크기 윈도우: 윈도우의 크기가 동적으로 조정되는 경우 (예: 조건에 따라 요소를 추가하거나 제거).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
Q. 최대합이 되는 연속된 k개의 요소 찾기
*/
public class SlidingWindow {
	public static void main(String[] args) {
		int[] nums= {1,2,3,4,5};
		int k=3; //연속된 요소의 개수
		int result=maxSum(nums,k);
		System.out.println(result);
	}
	
	public static int maxSum(int[] nums, int k) {
		int n=nums.length;	
		if(n<k) {
			throw new IllegalArgumentException("배열의 길이가 k보다 커야합니다");
		}
		int maxSum=0;
		for (int i = 0; i < k; i++) {
			maxSum += nums[i];
		}
		int windowSum=maxSum;
		for (int i = k; i < n; i++) {
			windowSum += nums[i]-nums[i-k];
			maxSum=Math.max(maxSum, windowSum);
		}
		return maxSum;	
	}
}

Exception

예외 처리 방법에는 try-catch와 throws 두 가지가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Test {
	public static void main(String[] args)  {
		int x = 100;
		int y = 0;
			
		y = Integer.parseInt(args[0]);
		int result;
		// try-catch 사용
		try {
			result = Divider.divide(x, y);
			System.out.println(result);
		} catch (MyException e) {
			System.out.println(e.getMessage());
		}
		System.out.println("더 중요한 일...");
	}
}
class Divider{
	public static int divide(int x, int y) throws MyException{
		if(y==0) {
			throw new MyException("0을 입력하지 마세요");
		}
		return x/y;
	}
}
class MyException extends Exception{
	public MyException(String message) {
		super(message);
		// throws MyException을 사용하여 직접 예외를 정의하고 던질 수 있음
		// new MyException("0을 입력하지 마세요") 형태로 예외 발생 가능
	}
}

2차원 배열 다루기

행 우선 순회 (왼쪽 -> 오른쪽)

1
2
3
4
5
6
for (int i = 0; i < rows; i++) {
	int cols=arr[i].length;
    for (int j = 0; j < cols; j++) {
        System.out.print(arr[i][j] + " ");
    }
}

출력

1 2 3
4 5 6
7 8 9

열 우선 순회 (위 -> 아래)

1
2
3
4
5
6
7
int rows=arr.length;
int cols=arr[0].length;
for (int i = 0; i < rows; i++) {
    for (int j = 0; j < cols; j++) {
        System.out.print(arr[i][j] + " ");
    }
}

출력

1 4 7
2 5 8
3 6 9

지그재그 순회

1
2
3
4
5
6
7
8
9
10
11
12
for (int i = 0; i < rows; i++) {
    if (i % 2 == 0) { // 짝수 행: 왼쪽 → 오른쪽
        for (int j = 0; j < cols; j++) {
            System.out.print(arr[i][j] + " ");
        }
    } else { // 홀수 행: 오른쪽 → 왼쪽
        for (int j = cols - 1; j >= 0; j--) {
            System.out.print(arr[i][j] + " ");
        }
    }
    System.out.println();
}

출력

1 2 3
6 5 4
7 8 9


END

This post is licensed under CC BY 4.0 by the author.