본문 바로가기

개발새발 개발자/자료 구조

[Java로 배우는 자료구조] 1-1. 변수, 배열, 반복문

이 자료는 부경대학교 권오흠 교수의 'Java로 배우는 자료구조론' 수업 PPT입니다. 시간 날 때마다 틈틈히 복습하기 위해 블로그에 업로드 하는 것이며, 저작권은 권오흠 교수님께 있습니다.

 

출처: http://alg.pknu.ac.kr/t/2016-2017-java/342

 

 

강의 가이드. 나는 2강까지는 그냥 읽어보기만 하고 본격적인 영상 강의는 3강부터 들을 예정이다.

 

 

메인 함수와 출력에 대한 기본 설명

 

클래스 이름은 대문자로 구성되는 것이 관습이다. main 메소드가 왜 저렇게 생겼는지는 일단 넘어가도록 하자.

 

변수는 메서드 내부, 외부 다 가능하지만 클래스 외부에는 선언될 수 없다.

 

타입은 C와 거의 유사하다.

 

뭘 import 해야할지 모르겠다면 source > OrganizeImports를 실행해보자. ctrl+shift+O를 하면 자동으로 import를 시켜준다.

 

Scanner로 입력값을 받는다. 이때, System.in은 키보드를 의미한다. 키보드에서 정수값을 받으려면 nextInt()를 사용한다.

 

일반 String을 입력받으려면 next(), 한 줄 전체를 받고 싶다면 nextLine()을 사용한다. 위의 코드를 테스트 해보면 올바르게 입력해도 'do not match'만 뜬다.

 

왜냐하면, String은 == 연산자가 아니라 equlas 메서드로 비교해야 하기 때문이다. 이 메서드는 true나 false로 결과를 반환한다.

 

if else를 이용해 분기할 수 있다.

 

package lesson01;

public class code05 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		// 배열을 일단 선언해놓고 
		int[] grades;
		
		// 배열의 크기를 지정하면서 생성한다. 실제 배열은 이때 만들어진다.
		// int[] grades = new int[5]; 라고 한 번에 할 수도 있다.
		grades=new int[5];
		
		grades[0]=100;
		grades[1]=200;
		grades[2]=300;
		grades[3]=400;
		grades[4]=500;
		
		for(int i=0; i<grades.length; i++) {
			System.out.println(i+" "+grades[i]);
		}
		
		System.out.println();
		
		int i=0;
		while(i<grades.length) {
			System.out.println(i+" "+grades[i]);
			i++;
		}
	}
  }

배열은 두 가지 방법으로 선언할 수 있다.

 

for문을 사용하면 반복을 피할 수 있다.

 

while 또한 for문과 기능이 같은데, 일반적으로 반복 횟수가 정해져 있다면 for, 그렇지 않다면 while을 사용한다.

 

n개의 정수를 입력받아 합과 최대값을 구한다.

 

그냥 다음 칸으로 옮겨버리면 기존의 원소는 지워진다. 

import java.util.Scanner;

public class code07 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Scanner kb=new Scanner(System.in);
		
		int n=kb.nextInt();
		int[] data= new int[n];
		
		for(int i=0; i<n; i++) {
			data[i]=kb.nextInt();
		}
		kb.close();
		
		// 맨 마지막 원소만 따로 빼서 저장하면 뒤로 밀려도 지워지지 않는다.
		int tmp=data[n-1];
		// 맨 뒤부터 땡겨서 저장한다.
		for(int i=n-2; i>=0; i--) {
			data[i+1]=data[i];
			
		}
		data[0]=tmp;
		
		for(int i=0; i<n; i++) {
			System.out.println(data[i]);
		}
	}

}

이를 해결하려면 맨 마지막 숫자를 따로 저장한 뒤, 뒤에서 앞으로 index를 이동하며 원소를 뒤로 미룬다. 그럼 자연스레 0번 인덱스가 비게 되고, 여기에 임시로 저장해놓았던 맨 마지막 원소를 넣으면 된다.

 

소수를 구하는 데에는 여러 가지 방법이 있다.

public class code08 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		int n= 10111;
		boolean isPrime=true;
		
		// 소수란 1과 자신 외의 약수를 갖지 않는 수이다.
		// 2, 3, ..., n-1 사이의 정수로 나누어 떨어지면 소수가 아니다.
		// 하지만 굳이 n-1까지 갈 필요가 없다. 약수는 n/2 보다 클 수 없기 때문이다.
		for(int i=2; i<=n/2; i++) {
		if(n % i == 0) {	// not prime number
			isPrime=false;

                   // 이미 소수가 아닌 것으로 판정났다면 불필요한 반복을 피하기 위해 빠져나온다

                     break;
                }// for


		///////////////////////////////////////////////

                // 혹은 빠져나올 조건을 for문에 추가한다.
		for(int i=2; i<=n/2 & isPrime; i++) {
			if(n % i == 0) {	// not prime number
				isPrime=false;
			}
			
		}	// for
	
		////////////////////////////////////////////////


		if(isPrime) {	// 소수이면 출력 
			System.out.println(n);
		}
		
	}

}

if에 break을 넣거나 for문에 조건을 추가하면 불필요한 반복을 막을 수 있다.
 
public class code08 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		int n= 10111;
		boolean isPrime=true;


                // 루트 n까지만 계산하는 방법.
		// 루트니까 i랑 i를 곱해서 n보다 작거나 같아야 한다는 조건을 넣는다.
		for(int i=2; i*i <= n && isPrime; i++) {
			if(n % i == 0) {	// not prime number
				isPrime=false;
			}

		}	// for


		if(isPrime) {	// 소수이면 출력 
			System.out.println(n);
		}
		
	}

}

하지만 사실 n/2까지 돌 필요도 없다. 루트 n까지만 돌아도 충분하다. n의 약수 중에 루트 n보다 더 큰 게 있다면 루트 n보다 작은 약수도 존재한다는 규칙이 있다. 루트는 n과 n을 곱해야 하는데 둘 다 루트 n보다 커버리면 성립되지 않기 때문이다. 루트는 sqrt(n)으로 표현한다.

 

public class code08 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		// 1~100,000 사이의 숫자 입력
		// 1은 소수가 아니므로 굳이 하지 않고 2부터 시작한다.
        
		for(int n=2; n<=100000; n++) {
         	 boolean isPrime=true;
		
		// 루트 n까지만 계산하는 방법.
		// 루트니까 i랑 i를 곱해서 n보다 작거나 같아야 한다는 조건을 넣는다.
		for(int i=2; i*i <= n & isPrime; i++) {
			if(n % i == 0) {	// not prime number
				isPrime=false;
			}
		}	// for
		
		if(isPrime) {	// 소수이면 출력 
		System.out.println(n);
		}
	}
	}
}

이제 n이 1~100,000까지라는 조건을 넣어준다. 1은 소수가 아니므로 2부터 for문을 시작하면 된다.

 

중복된 정수 쌍이라는 것은 서로 같은 값을 가진 쌍을 의미한다. 예를 들어 (2, 2) (4, 4)가 있다.

public class code09 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		Scanner kb = new Scanner(System.in);
		int n= kb.nextInt();
		int[] data=new int [n];
		
		for(int i=0; i<n; i++) {
			data[i] = kb.nextInt();
		}
		kb.close();
		
		// n개의 데이터에 대해 모든 쌍을 비교한다. 같은 값이면 무시하도록 한다.
		// 쌍을 검사하는 것은 항상 두 개의 중첩 for문을 사용한다.
		int count =0;
		for(int i=0; i<n; i++) {	// 쌍의 첫번째 
			for(int j=0; j<n; j++) {	// 두번째
				// data[i] data[j]
				if(data[i]==data[j]) {
					count ++;
				}
			}
		}	 // for
		
		// 하지만 이렇게 하면 1, 2, 3을 넣어도 3이 출력된다.
		// i가 0일 때 j가 0 인 상황 등 꼭 겹쳐지게 되기 때문이다.
		
		System.out.println(count);

	}
}

하지만 이렇게 코드를 짤 경우 에러가 난다. i와 j가 같아지는 순간은 꼭 있기 때문이다.

 

public class code09 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		Scanner kb = new Scanner(System.in);
		int n= kb.nextInt();
		int[] data=new int [n];
		
		for(int i=0; i<n; i++) {
			data[i] = kb.nextInt();
		}
		kb.close();
		
		int count =0;


		
		for(int i=0; i<n; i++) {	// 쌍의 첫번째 
			for(int j=0; j<n; j++) {	// 두번째 
				// data[i] data[j]

                // 따라서 인덱스가 다르더라도 원소가 같은 경우라는 조건을 넣어준다.
                if(i!=j && data[i]==data[j]) {
					count ++;
				}
			}
		}	 // for
		
		System.out.println(count);

	}

}

첫 번째 이슈는 해결이 되었다. 하지만 이 코드를 실행하면 또 다른 오류가 생긴다. 1, 1, 2를 입력하면 1이 아니라 2가 출력된다. 항상 중복 검토하게 되기 때문이다. i=1 j=2 / i=2 j=1 처럼.

 

public class code09 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		Scanner kb = new Scanner(System.in);
		int n= kb.nextInt();
		int[] data=new int [n];
		
		for(int i=0; i<n; i++) {
			data[i] = kb.nextInt();
		}
		kb.close();
		
		int count =0;


		
		// j를 i+1부터 시작하면 해결된다.

		// 순서쌍은 i보다 j가 클 경우만 검사해도 충분하기 때문이다.
		for(int i=0; i<n; i++) {	// 쌍의 첫번째 
			for(int j=i+1; j<n; j++) {	// 두번째 
				// 인덱스에 대한 조건도 없어진다.
				if(data[i]==data[j]) {
					count ++;
				}
			}
		}	 // for

		
		System.out.println(count);

	}

}

i보다 j가 클 경우, 즉 j=i+1일 때만 검사해도 충분히 순서쌍의 중복을 피할 수 있다. 

 

import java.util.Scanner;

public class code10 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		Scanner kb = new Scanner(System.in);
		// 일단 배열의 크기를 입력받는다.
		int n = kb.nextInt();
		int[] data= new int[n];
		
		// 배열에 들어갈 원소를 입력받는다.
		for(int i=0; i<n; i++) {
			data[i] = kb.nextInt();
		}
		
		kb.close();
		
		// 그냥 처음부터 끝까지 더하면 되는 것 아니냐 할 수 있지만
		// 음수가 존재하므로 그렇지 않다.
		
		/*
		 제일 먼저 생각해볼 수 있는 방법은 많은 경우의 수로 다 더해보는 것이다.
		 더할 구간 각각의 시작점과 끝점을 구해보자.
		
		 일단 최대값을 0으로 초기화 해놓는다. 0개 이상을 선택할 수 있으므로 최대값을 0으로 초기화해도 무방하다.
		 또한 어떤 값을 더해도 최대값이 0보다 작아질 수는 없기 때문이다.
		*/
         
		int max = 0; 
		
		// 시작점은 i가 된다.
		for(int i=0; i<n; i++) {	
			 // 끝점은 j가 된다. 이때 끝점은 반드시 i보다 크거나 같으므로 0이 아니라 i부터 시작한다.
			for(int j=i; j<n; j++ ) {	
				
				int sum = 0;
                
				// 위에서 시작과 끝을 설정해줬으니 이제 data[i]부터 [j]까지 더하기 위한 for문이 필요하다.
				for(int k=i; k<=j; k++) {	// i부터 j까지 반복
					sum += data[k];
				}
				
				if(sum > max) {	// 만약 지금 더한게 더 크다면 
					max=sum;	// 지금 sum이 최대값이 된다.
				}
			}
		}	// for
	
		
		System.out.println(max);
	}
}

하지만 위의 방법은 불필요한 중복을 포함하고 있다. 앞에 구했던 최대값에 다음 원소만 더해주면 되는데, 반복문을 또 수행해야 하기 때문이다.

 

import java.util.Scanner;

public class code10 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		Scanner kb = new Scanner(System.in);
		// 일단 배열의 크기를 입력받는다.
		int n = kb.nextInt();
		int[] data= new int[n];
		
		// 배열에 들어갈 원소를 입력받는다.
		for(int i=0; i<n; i++) {
			data[i] = kb.nextInt();
		}
		
		kb.close();
		
		int max = 0; 
		for(int i=0; i<n; i++) {	
			
			// i가 바뀔 때 sum이 초기화 되도록 한다.
			int sum = 0;
			
			for(int j=i; j<n; j++ ) {	
				
				// j가 바뀌는 동안에는 새로 sum을 구하지 않고
				// 같은 시작점에서 계속 더하는 동안 생긴 sum에 추가만 한다.
				sum += data[j];
				
				if(sum > max) {	// 지금 sum이 최대값이 된다.
					max=sum;
				}
			}	// for
		}	// for
		
		System.out.println(max);
	}
}

하나의 시작점에서 끝점을 하나씩 추가할 때마다 max인지 구분하도록 변경했다.

 

package section01;

import java.util.Scanner;

public class Code13
{
	public static void main(String[] args)
	{
		// TODO Auto-generated method stub

		Scanner kb = new Scanner(System.in);
		int n = kb.nextInt();
		int[] data = new int[n];

		for (int i = 0; i < n; i++)
		{
			data[i] = kb.nextInt();
		}
		kb.close();

		/*
		 * 먼저 해당 구간을 붙여 정수를 만들고 그것이 소수인지 판단한 후, 
         내가 갖고있는 max보다 큰지 판단하면 된다.
		 */

		int max = 0;

		for (int i = 0; i < n; i++) // 구간의 시작
		{
			// 구간의 끝점인 j는 i보다 왼쪽에 있을 수 없으므로 i부터 시작
			for (int j = i; j < n; j++)
			{
				int val = 0;
				for (int k = i; k <= j; k++)
				{
					// convert data[i]...data[j] into an integer
					// 원래의 val를 10의 자리로 보내고 지금의 data를 더한다.
					val = val * 10 + data[k];
				}

				// test if it is a prime
				boolean isPrime = true;
				for (int k = 2; k * k <= val && isPrime; k++)
				{
					if (val % k == 0)
					{
						isPrime = false;
					}
				}

				// if yes, compare to the max
				if (isPrime && val > max)
				{
					max = val;
				}
			}
		}
		// 소수가 하나도 없어서 모든 조건을 다 통과하고 max가 0으로 나올 수도 있으므로
		if (max > 0)
		{
			// 적어도 하나의 소수를 가질 때만 출력을 해주고
			System.out.println(max);
		} else
		{
			// 없을 경우 없다고 출력한다.
			System.out.println("No prime number");
		}
	}
}

하지만 만약 아래와 같은 값을 넣으면, 결과는 1이 나온다.

하지만 1은 소수가 아니다. 따라서 이 코드에는 오류가 있다. 소수인지 구분하는 2번 코드에 문제가 있는 것이다. 즉, k=2, val=1일 때 for문을 한 번도 실행하지 않아서 isPrime이 true인 채로 끝나기 때문이다.

 

public static void main(String[] args) {
	
		Scanner kb = new Scanner(System.in);
		int n = kb.nextInt();
		int[] data = new int[n];
		
		for(int i=0; i<n; i++) {
			data[i] = kb.nextInt();
		}
		
		kb.close();
		
		int max = 0;
		
		for(int i=0; i<n; i++) {
			for(int j=i; j<n; j++) {
				
				int val=0;
				for(int k=i; k<=j; k++) {
					val = val*10 + data[k];
				}
				
				boolean isPrime = true;
				
				for(int k=2; k*k<=val && isPrime; k++) {
					if(val%k==0) {
						isPrime = false;
					}
				}
				
				// val이 1보다 크다는 조건을 추가한다.
				if(isPrime && val > max && val > 1) {	
					max=val;
				}
			}
		}
		
		if(max>0) {
			System.out.println(max);
		}
		else {
			System.out.println("No prime number.");
		}
	
	}

이렇게 val에 대한 조건을 if문에 추가해주면 된다.

 

또 다른 문제는 입력값이 많을 경우다. val은 정수이고 받을 수 있는 데에 한계가 있으므로, n=100처럼 늘어나면 overflow가 생긴다. long으로 바꾼다면 더 큰 정수로 표현할 수 있지만 그것이 근본적인 해결책은 아니다. 일단 참고만 해두자.

 

이번에 구현할 것은 버블 소트 알고리즘이다.

 

버블 소트는 가장 큰 값을 찾아서 맨 뒤로 보낸다. 그 자리에 있던 2는 다른 곳으로 보낸다. 중요한 키 포인트는 가장 큰 값을 맨 뒤로 보낸다는 사실이다. 옮기고 나면 13을 제외한 나머지 숫자에 대해 똑같이 반복한다.

 

그 마지막 자리에 넣기 위해서 맨 처음부터 하나씩 숫자를 살펴본다. 해당 숫자와 다음 숫자를 비교하고 지금 숫자가 더 크면 둘의 자리를 바꾼다. 더 큰 숫자가 있으면 멈춘다. 그 숫자를 다시 다음 숫자와 비교한다.

 

예를 들어, 맨 앞의 8을 계속 비교하다가 11을 만나면 멈춘다. 11은 다시 뒤의 숫자와 비교한다. 13이 더 크므로 13으로 넘어가서 13과 다음 숫자를 비교한다. 13보다 작은 수이므로 13을 뒤로 보낸다. 결국, 다른 값이 어떻게 되든 가장 큰 값이 뒤로 가게 된다.

 

한 스텝, 한 스텝 넘어갈 수록 처리할 데이터가 하나씩 줄어든다. 그림을 보면 맨 마지막 데이터를 가리키는 인덱스를 i라고 표시했다. 그리고 n-1, n-2...해서 n=1이 될 때까지 앞으로 넘어간다. 따라서 코드로 표현하면 다음과 같다.

 

import java.util.Scanner;

public class code12 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		Scanner kb = new Scanner(System.in);
		// 일단 배열의 크기를 입력받는다.
		int n = kb.nextInt();
		int[] data= new int[n];
		
		// 배열에 들어갈 원소를 입력받는다.
		for(int i=0; i<n; i++) {
			data[i] = kb.nextInt();
		}
		kb.close();
		
		// bubble sort
		// 맨 뒤에서 i=1까지만 i--를 반복한다.
		// for문 한 번이 그림에서의 한 스텝이다.
		for (int i=n-1; i>0; i--) {
			// data[0]에서 data[i] 사이에서 제일 큰 값을 data[i]로 몰아간다.
			// 0에서 i-1까지 쭉 스캔하면서 최대값을 찾아낸다.
			for(int j=0; j<i; j++) {
				if(data[j] > data[j+1]) {
					// swap data[j] and data[j+1]
					// 일단 data[j]값을 다른 곳에 옮겨두고 
					int tmp = data[j];
					// 이제 data[j]에는 마음대로 값을 쓸 수 있으니 data[j+1]을 넣고 
					data[j] = data[j+1];
					// 임시로 저장해뒀던 값을 data[j+1]에 넣으면 된다.
					data[j+1] = tmp;
				}
			}
		}
		
		// 정렬된 값을 출력한다.
		System.out.println("Sorted data: ");
		for(int i=0; i<n; i++) {
			System.out.println(data[i]);
		}
	}
}

 

입력받을 때마다 버블 소트 알고리즘으로 정렬해도 되지만 비효율적이다. 이미 버블 소트를 한 상태에서 입력된 하나의 값 때문에 모든 값을 다시 정렬해야 하기 때문이다. 

 

그럼 입력받은 값을 어떻게 특정 자리에 집어넣을 수 있을까? 두 가지 방법이 있다.

1. 앞에서부터 저장된 수를 쭉 비교한 다음 처음으로 입력값보다 크거나 같은 수가 나오면 바로 그 앞에 넣는다.

2. 1번의 방법을 뒤에서 부터 수행하며 입력값보다 작은 값을 찾고 그 뒤에 넣는다.

 

어떻게 보면 둘은 같아보인다. 하지만 조금 다르다. 만약 값이 5라면 1번 방법의 경우 처음부터 쭉 보다가 4와 6 사이로 가기 위해 6부터 뒤의 값을 다 뒤로 미뤄야 한다.

 

하지만 만약 2번처럼 뒤에서 부터 한다면, 5가 어디로 갈 지는 모르지만 어쨌든 9는 5보다 크므로 한 칸씩 물러난다. 7은 9가 이미 뒤로 가서 칸이 비어있으므로 쉽게 뒤로 가면 된다. 4를 만나면 비어있는 칸에 5가 들어가면 되므로 어떤 숫자가 지워질 위험이 없다. 또한, 3부터는 볼 필요가 전혀 없어진다. 물론 최악의 경우에는 끝까지 탐색해야 한다.

 

이러한 방법을 Ordered List에 새로운 값을 insert 한다고 한다. 두 번째 방법, 즉 뒤에서 스캔하는 방법으로 코드를 짜보자.

 

public class code13 {

	public static void main(String[] args) {
		
		Scanner kb = new Scanner(System.in);
		// 일단 배열의 크기를 입력받는다.
		int n = kb.nextInt();
		int[] data= new int[n];
		
		for(int i=0; i<n; i++) {
			// 배열에 바로 넣지 않고 임시로 갖고 있다가 비교한다.
			int tmp = kb.nextInt();
			
			// 배열 데이터가 i개라면 인덱스는 i-1이므로 
			int j=i-1;
			
			// tmp보다 작거나 같은 애가 나타날 때까지
			while(data[j] > tmp) {
				// data j를 뒤로 보낸다.
				data[j+1] = data[j];
				
				// i는 현재까지 입력된 데이터의 개수이다.
				// 입력된 값이 6개면 i=6이다.
				// 그럼 인덱스는 5부터 쭉 감소해간다.
				j--;
			}
			
			// data[j]가 tmp보다 작아지면 while을 빠져나온다.
			// 비어있는 j+1에 tmp를 넣는다.
			data[j+1] = tmp;
			
			// 출력하는 코드
			// i개에서 1개를 더 추가했으므로 데이터의 개수는 i+1이다.
			// 따라서 인덱스 i까지 반복한다.
			for(int k=0; k<=i ;k++) {
				System.out.print(data[k] + " ");
			}
			System.out.println();
			
		}
		
		kb.close();

	}

}

이렇게 하면 코드가 잘 돌아갈 것 같지만 사실 문제가 하나 있다. tmp가 만약 -1이라면 배열에 있는 1보다도 작으므로 제일 앞으로 가게 될 것이다. 그럼 더 이상 비교할 값이 없으므로 while 문을 멈춰야 한다.

 

따라서 아래와 같이 while문을 수정한다.

 

public class code13 {

	public static void main(String[] args) {
		
		Scanner kb = new Scanner(System.in);
		// 일단 배열의 크기를 입력받는다.
		int n = kb.nextInt();
		int[] data= new int[n];
		
		for(int i=0; i<n; i++) {
			// 배열에 바로 넣지 않고 임시로 갖고 있다가 비교한다.
			int tmp = kb.nextInt();
			
			// 배열 데이터가 i개라면 인덱스는 i-1이므로 
			int j=i-1;
			
			// tmp보다 작거나 같은 애가 나타날 때까지
			while(j>=0 && data[j] > tmp) {
				// data j를 뒤로 보낸다.
				data[j+1] = data[j];
				
				// i는 현재까지 입력된 데이터의 개수이다.
				// 입력된 값이 6개면 i=6이다.
				// 그럼 인덱스는 5부터 쭉 감소해간다.
				j--;
			}
			
			// data[j]가 tmp보다 작아지면 while을 빠져나온다.
			// 비어있는 j+1에 tmp를 넣는다.
			data[j+1] = tmp;
			
			// 출력하는 코드
			// i개에서 1개를 더 추가했으므로 데이터의 개수는 i+1이다.
			// 따라서 인덱스 i까지 반복한다.
			for(int k=0; k<=i ;k++) {
				System.out.print(data[k] + " ");
			}
			System.out.println();
			
		}
		
		kb.close();

	}

}

 

과제는 홈페이지에 게시된다고 한다. 일단 혼자 풀어보고 확인해봐야겠다.