리스트에 순차적으로 접근해야 할 때 '두 점'의 위치를 기록하면서 처리하는 알고리즘

 

문제

첫 번째 줄에 첫 번째 배열의 크기 N(1<=N<=100)이 주어집니다.

두 번째 줄에 N개의 배열 원소가 오름차순으로 주어집니다.

세 번째 줄에 두 번째 배열의 크기 M(1<=M<=100)이 주어집니다.

네 번째 줄에 M개의 배열 원소가 오름차순으로 주어집니다.

각 리스트의 원소는 int형 변수의 크기를 넘지 않습니다.

 

given

3
1 3 5
5
2 3 6 7 9

일 때 [1 2 3 3 5 6 7 9]의 배열이 나와야 한다.

 

일반적으로 배열을 합친 후 sort를 진행하거나, sorting이 되어있는 컬렉션으로 진행 시 걸리던 O(n log n)의 시간 복잡도를

 

투 포인터 알고리즘을 이용함으로써 O(n)으로 줄일 수 있다.

 

풀이는 다음과 같다.

public class Main {
	public ArrayList<Integer> solution(int n, int m, int[] arr, int[] arr2){
		ArrayList<Integer> answer = new ArrayList<>();
		int p1=0, p2=0;
		while(p1<n && p2<m) {
			if(arr[p1] < arr2[p2]) answer.add(arr[p1++]);
			else answer.add(arr2[p2++]);
		}
		while(p1<n) answer.add(arr[p1++]);
		while(p2<m) answer.add(arr2[p2++]);
		return answer;
	}

	public static void main(String[] args) {
		Main T = new Main();
		Scanner kb = new Scanner(System.in);
		int n = kb.nextInt();
		int[] arr = new int[n];
		for(int i=0; i<n; i++) {
			arr[i] = kb.nextInt();
		}
		int m = kb.nextInt();
		int[] arr2 = new int[m];
		for(int j=0; j<m; j++){
			arr2[j] = kb.nextInt();
		}
		for(int x : T.solution(n, m, arr, arr2)) {
			System.out.print(x + " ");
		}
	}
}

 

n배열과 m배열이 먼저 끝날때까지를 비교해놓고, 한쪽이 끝나면 나머지를 모두 추가하는 방식으로 구현한다.

지뢰찾기류의 문제는 생각보다 여기저기 많다.

처음 이런 델타배열류의 문제를 본 것은 프로그래머스였다.

프로그래머스의 안전지대 문제를 풀었는데, 저 때는 델타 배열의 존재를 몰라서

import java.util.*;
class Solution {
    public int solution(int[][] board) {
        int answer = 0;
        int length = board.length;
        int[][] board2 = new int[length+2][length+2];
        for (int i=0; i<board2.length;i++){
            Arrays.fill(board2[i],0);
        }
        for (int i=0;i<length;i++){
            for (int j=0;j<length;j++){
                if (board[i][j]==1){
                    for (int k=-1;k<=1;k++){
                        for(int l=-1;l<=1;l++){
                            board2[i+k+1][j+l+1] += 1;
                        }
                    }
                }
            }
        }
        for (int i=1;i<=length;i++){
            for (int j=1;j<=length;j++){
                if (board2[i][j]==0) answer++;
            }
        }
        return answer;
    }
}

이런 긴 4중 for문으로 문제를 풀었었다. 물론 문제가 좀 어렵기도 했다.

 

다음은

다음도 이와 비슷한 봉우리 찾기 문제인데, 여기는 친절하게도 한칸씩 미리 늘려서 알려주었다.

public class Main {
    public int solution(int[][] arr) {
        int cnt = 0;
        for (int i=1; i<arr.length-1; i++){
            for (int j=1; j<arr.length-1;j++){
                if ((arr[i-1][j] < arr[i][j])
                        &&(arr[i+1][j]<arr[i][j])
                        &&(arr[i][j+1]<arr[i][j])
                        &&(arr[i][j-1]<arr[i][j])) cnt++;
            }
        }
        return cnt;
    }

문제가 그렇게 복잡하지는 않았는데, 만약 이게 전후좌우, 또는 전후좌우대각선, 또는 

이거 조건식 걸다간 죽어버릴거야

이런 류로 탐색을 해야 한다면 어떨까?

for문을 하루종일 돌리거나, if조건문에만 엄청나게 식을 많이 써서 돌릴 것이다.

이 때 필요한 것이 델타 배열이다.

델타 배열은 간단하다.

일단 먼저 상하좌우 4방향을 탐색해야한다고 생각해보자. 이 때,

int[] dx = {-1,0,0,1};
int[] dy = {0,-1,1,0};

를 만들어 i+dx = nx, i+dy = ny로 놓고 푼다면 내가 있는 좌표의 위치를 찾아볼 수 있다.

더 나아가서 대각선까지 포함하는 8방을 탐색해야한다고 생각해보자.

int[] dx = {-1, -1, -1, 0, 0, 1, 1, 1};
int[] dy = {-1, 1, 0, -1, 1, -1, 1, 0};

이 때 for문을 이용해 8번 돌리면 내 주위의 방향을 다 파악할 수 있다.

public char[][] solution(int n, char[][] arr){
        char[][] answer = new char[n][n];
        int[] dx = {-1, -1, -1, 0, 0, 1, 1, 1};
        int[] dy = {-1, 1, 0, -1, 1, -1, 1, 0};
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr.length; j++) {
                if (Character.isDigit(arr[i][j])) {
                    answer[i][j] = '*';
                } else {
                    int tmp = 0;
                    for (int k = 0; k < 8; k++) {
                        int nx = i + dx[k];
                        int ny = j + dy[k];
                        if (nx >= 0 && nx < arr.length && ny >= 0 && ny < arr.length) {
                            if (Character.isDigit(arr[nx][ny])) tmp += arr[nx][ny] - '0';
                        }
                    }
                    if (tmp > 10) answer[i][j] = 'M';
                    else answer[i][j] = (char) (tmp + '0');
                }
            }
        }
        return answer;
    }

이렇게 델타 배열을 이용하면, 조건문을 훨씬 편하게 짤 수도 있고,

또 혹시나 위에 그림처럼 8방이 아닌 많은 방향에서도 정확하게 탐색을 할 수 있다.

 

좋아하는 말 중에 E=MC^2라는 말이 있다.

Error = More Code라는 말인데, 델타 배열을 쓰지 않으면 너무 많은 코드로 더 많은 에러를 야기시킬 수 있다.

무엇이 틀린지 한참 고민해야하는것은 덤이다.

 

추가 팁)

1. 다른조건보다 늘 nx, ny의 유효성검사를 해주어야 한다. 그렇지 않으면 ArrayOutOfBoundException 에러가 난다.

2. 가장 안쪽에 for문을 돌 때 break를 걸어주면 더 편하다. 예를 들어 봉우리 문제인 경우, 내 주위 8방향보다 내가 값이 더 커야 하는데, 다른 방향에서의 value가 더 클 때 break를 걸어주면 최대 7번의 for문이 도는것을 절약할 수 있다.

많은 답이 있을 것 같은 소수 찾기 문제. 

내가 생각해본 나의 풀이 :

isPrime으로 각 소수를 찾는다( i=2일때부터 root n까지 탐색)하고 n%i == 0이면 false를 반환한다.

이 isPrime으로 n까지 수들 돌면서 소수를 뽑는다.

그러나 훨씬 빠른 방법으로 소수를 찾을 수 있다.

그냥 이렇게 써 놓으면 무슨 말인지 모를 수가 있다.

어떤 수부터 n이라는 수까지 탐색을 하면서

배열을 지워 나가는 것이다

1) i=2일 때

i번째 인덱스를 가지고 있는 친구가 처음으로 내용물이 0인 상태로 발견이 되었다.

그러면 answer를 하나 증가시켜주고, 2의 배수인 친구들을 전부 내용물을 1로 바꿔준다.

이건 for문안의 j=j+i에 설정되어있다.

2) i=4일 때

아까 i=2로 인해 내용물이 1로 변했으므로 지나간다.

 

2중 포문을 돌기 때문에 굉장히 느릴것이라고 생각했지만, 내가 짰던 코드보다 훨씬 빠르다.

특히나 for문 안에 있는 j=j+i 스킬이 굉장히 유용할 것 같다.

하나의 포문 안에 변수를 2개 돌리는 스킬도 좋으니 찾아보자

중복 문자를 제거하는 방법 또한 수없이 많다. 

Stream 클래스의 distinct, LinkedHashSet를 만들어 add를 하고 출력 등 갖가지 방법이 있으나

오늘은 indexOf를 이용한 풀이를 진행한다.

처음 문제를 봤을때는 String class의 replace를 이용한 풀이법을 생각했으나 쉽지 않았다.

그래서 푼 방법이 LinkedHashSet를 이용해 진행하였다.

중복 제거와 순서 저장 모두를 사용해야 하기 때문이다.

또한 이전에 포스팅했듯, String에 적재하는 방식은 스트링 풀에 계속 새로운 스트링 객체를 생성하기 때문에

메모리적으로도 좋지 않을것이라고 생각했다.

LinkedHashSet을 이용해 charAt를 저장하고, 나중에 한꺼번에 join으로 합쳤다.

조금 더 생각해보면, 훨씬 간편하면서도 나이브하게 문제를 풀 수 있다.

바로 indexOf를 사용하는 것이다.

예를 들어 apple이라는 글자가 있을때 i번째 인덱스와 charAt(i)의 인덱스는 다음과 같다

  a p p l e
i 0 1 2 3 4
indexOf(charAt(i)) 0 1 1 3 4

indexOf는 가장 처음의 인덱스만 반환하기때문에 apple의 두번째 p부터는 p의 인덱스로 1을 가져가고,

이를 이용해 중복을 제거할 수 있다.

속도 또한 밑의 방식이 조금 더 빠르게 나온다.

문자열을 뒤집는 방법에는 다음과 같은 방법이 있다.

  • 전부 뒤집기
  • 부분 뒤집기

String은 immutable(불변)하다는 특징이 있다. 스트링을 더하거나 바꿔주거나 할 때는 무조건 새로운 String이 생성된다.

a+b를 콘솔에 찍어보면 Hello World!가 나온다고 해서 단순히 '아, Hello라는 문자열에 World를 더해주었구나' 라고 생각해서는 안된다.

찍어본 HashCode

위의 결과에서 알 수 있듯, a, b, a+b의 hashcode는 다르다. a에다가 b를 붙여준 것이 아니라, "Hello World"라는 스트링을 생성해준 것이다. String을 리터럴로(""의 형태로) 한번 생성하면 Heap 영역에 있는 Constant String Pool에서 같은 String이 있는지 검사한 후, 같은 스트링이 없다면 스트링 풀에 문자열 객체를 생성하고 주소값을 반환한다. 이와 같은 맥락에서 String에서의 equals와 ==의 차이 문제가 발생하기도 한다. 자세한 내용은 나중에 따로 설명하겠다.

 

그렇다면 스트링을 이용하고 싶은데 메모리를 덜 먹는 방법은 없을까?

바로 StringBuilder 클래스를 이용하는 것이다. StringBuffer 클래스도 있지만 이것은 동기화 차이이므로 Builder 클래스가 낫다.

for (String x : str){
	StringBuilder sb = new StringBuilder();
	answer.add(String.valueOf(sb.append(x).reverse()));
}

스트링빌더는 한번 생성한 개체로 필드의 가변 인자들을 받아 append등의 연산을 빠르고 가볍게 할 수 있다.

조금 더 풀이를 깎아서,

for (String x : str){
	String tmp = new StringBuilder(x).reverse();
	answer.add(tmp)
}

로도 나타낼 수 있겠다. 조금 더 명확하다.

 

toCharArray를 이용해서 문자열을 부분적으로 뒤집는 방법도 있다.

for (String x : str){
	char[] arr = x.toCharArray();
	int lt = 0;
	int rt = arr.length-1;
	while (lt<rt){
		char tmp = arr[lt];
		arr[lt] = arr[rt];
		arr[rt] = tmp;
        lt++;
        rt--;
        }
        answer.add(String.valueOf(arr));
}

기술 자체는 어려운게 아니지만, lt와 rt를 설정하는 방법이 핵심이다.

 

lt는 한 칸씩 증가, rt는 한 칸씩 감소하면서 char배열을 읽어낼 수 있다.

마지막에 String.valueOf로 값을 뽑아준다.

 

 

 

 

https://hyeran-story.tistory.com/123#recentEntries

 

[Java] equals()과 == 차이점, String Constant Pool(상수 풀)

자바에서 String의 값을 비교할때 equals()를 쓰시나요 ==을 쓰시나요? 보통 산술연산자에서 값을 비교할때는 ==을 하는데요 인텔리제이에서 String의 값을 비교할때 ==을 쓰면 아래와 같은 메세지를

hyeran-story.tistory.com

 

'알고리즘 > 기타' 카테고리의 다른 글

Two pointers  (0) 2023.04.25
[JAVA]지뢰 찾기 - 델타 배열을 이용한 방법  (0) 2023.02.02
소수 찾기 - 외부 메서드 없이 탐색  (0) 2023.02.02
문자열 중복제거 - indexOf()  (0) 2023.01.29

+ Recent posts