2019년 12월 20일 금요일

Javascript 변수 선언 키워드 Let 과 Const




* let and const

let : 블럭 사이에서만 활성 되는 변수 선언 키워드. 글로벌하게 사용할 수 있는 var 키워드와는 반대이다. 스코프를 벗어나면 해당 변수를 사용할 수 없다.
또한 let 으로 선언된 변수는 동일한 이름으로 다시 let으로 선언할 수 없다. (var는 가능함)

const : 상수. 값을 레퍼런스로써 읽기만을 하기 위한 변수 선언 키워드이다.
한번 할당하면 값을 변경할 수 없다. const로 선언된 변수는 반드시초기화를 해주어야 한다.


* 문제
PI 를 상수로 선언하고 r를 입력받아, 원의 넓이와 원둘레를 구하라.


function main() {
    // Write your code here. Read input using 'readLine()' and print output using 'console.log()'.
    const PI = Math.PI;
    let r = readLine();
    // Print the area of the circle:
    console.log(r * r * PI);
    // Print the perimeter of the circle:
    console.log(2 * r * PI);
    try {    
        // Attempt to redefine the value of constant variable PI
        PI = 0;
        // Attempt to print the value of PI
        console.log(PI);
    } catch(error) {
        console.error("You correctly declared 'PI' as a constant.");
    }
}

2019년 12월 18일 수요일

Javascript 문자열에서 특정 조건 문자 한개씩 찾기






문제 : 주어진 문자열에서 모음과 자음을 찾아내어 모음을 먼저 출력한 후, 자음을 출력하시오.

주어진 문자열 : javascriptloops
출력 :
a
a
i
o
o
j
v
s
c
r
p
t
l
p
s



1. 정규표현식을 이용하여 모음과 자음을 찾아냄
function vowelsAndConsonants(s) {
    var p1 = /[aeiou]/g; //모음, g의 의미: 플래그. 문자열 내의 모든 패턴을 검색
    var p2 = /[^aeiou]/g; //자음
    var arr1 = s.match(p1); //패턴에 일치하는 결과를 배열로 만들어준다.
    var arr2 = s.match(p2);
   
   for (var i of arr1) {
       console.log(i);
   }
   for (var j of arr2) {
       console.log(j);
   }
}

2. includes 함수를 이용하여 문자 탐색.
includes는 찾을 문자를 인자로 넣어주면 된다.


function vowelsAndConsonants(s) {
    var vowel= "aeiou";
    var consonants = "";
    
   
   for (var i in s) { //문자열 그 자체를 배열처럼 사용가능하다.
       if (vowel.includes(s[i])){
           console.log(s[i]);
       }else{
           consonants+= s[i] + "\n";
       }
   }
   console.log(consonants.trim());
}

2019년 12월 16일 월요일

도수 정렬



* 도수 정렬
요소의 대소를 판단하지 않고 빠르게 정렬할 수 있는 알고리즘이다.
도수분포표를 이용하기 때문에 if 대소비교문이 필요없이 for문만으로 정렬이 가능하다.




public class fSort {
    //배열 a중 최댓값을 구하여 max로 넘겨준다.
    static void fSort(int[] a, int n, int max) {
        int[] f = new int[max + 1];
        int[] b = new int[n];

        for (int i = 0; i < n; i++) f[a[i]]++; //도수분포표 만들기
        for (int i = 1; i <= max; i++) f[i] += f[i - 1]; //누적 도수분포표 만들기
        //f 배열에 있는 값을 -- 연산자를 사용한 f 배열은 누적도수분포표가 된 상태인데
        //해당 구간의 누적개수합산을 뜻한다.
        //인덱스를 1씩 빼서 동일한 값일 경우 앞서 저장한 부분의 앞칸에 저장되게 하기 위함이다.
        for (int i = n - 1; i >= 0; i--) b[--f[a[i]]] = a[i]; 
        for (int i = 0; i < n; i++) a[i] = b[i]; //a에 b 복사하기
        
    }
}

힙(Heap) 정렬




*힙(heap) 정렬 

선택 정렬을 응용한 알고리즘인 힙 정렬은 힙의 특성을 이용하여 정렬을 수행한다.
힙은 부모의 값이 자식의 값보다 항상 크다는 조건을 만족하는 완전이진트리이다.
부모의 값이 자식보다 항상 작아도 힙이라고 한다.
힙은 형제의 대소관계가 정해져 있지 않아서 부분순서트리라고도 한다.


public class HeapSort {
    // 배열 요소 a[idx1]과 a[idx2]의 값을 바꿉니다.
    static void swap(int[] a, int idx1, int idx2) {
        int t = a[idx1];
        a[idx1] = a[idx2];
        a[idx2] = t;
    }

    //a[left] ~ a[right]를 힙으로 만듭니다.
    static void downHeap(int[] a, int left, int right) {
        int temp = a[left]; //root값
        int child; //큰 값을 가진 노드 인덱스
        int parent; // 부모 인덱스

        //자식과 부모를 비교하여 큰 것을 위로 올린다.
        for (parent = left; parent < (right + 1) / 2; parent = child) { 
            int cl = parent * 2 + 1; //왼쪽 자식 인덱스
            int cr = cl + 1; //오른쪽 자식 인덱스
            child = (cr <= right && a[cr] > a[cl]) ? cr : cl; //큰 값을 가진 노드를 자식에 대입
            if (temp >= a[child]) //root가 더 크다면 반복문 중지
                break;
            a[parent] = a[child]; //큰 값을 위로 올림
        }
        a[parent] = temp;
    }

    //힙 정렬
    static void heapSort(int[] a, int n) {
        for (int i = (n - 1) / 2; i >= 0; i--) //a[i] ~ a[n - 1]를 힙으로 만들기
            downHeap(a, i, n - 1);

        for (int i = n - 1; i > 0; i--) {
            swap(a, 0, i); //가장 큰 요소와 아직 정렬되지 않은 부분의 마지막 요소를 교환
            downHeap(a, 0, i - 1); //a[0] ~ a[i - 1]을 힙으로 만들기
        }
    }
}

2019년 12월 6일 금요일

병합(merge) 정렬





* 병합 정렬

정렬의 마친 두 배열을 병합하여 분할 정복법에 따라 정렬하는 알고리즘을 병합 정렬이라고 한다.



1. 정렬을 마친 두 배열을 병합하기


public class MergeArray {
    //정렬을 마친 배열 a, b를 병합하여 배열 c에 저장
    static void merge(int[] a, int na, int[] b, int nb, int[] c) {
        int pa = 0;
        int pb = 0;
        int pc = 0;

        while (pa < na && pb < nb) //작은 값을 c 배열에 저장한다.
            c[pc++] = (a[pa] <= b[pb]) ? a[pa++] : b[pb++];
        while (pa < na) //a에 남아있는 요소를 복사
            c[pc++] = a[pa++];
        while (pb < nb) //b에 남아있는 요소를 복사
            c[pc++] = a[pb++];

    }
    public static void main(String[] args) {
        Scanner stdIn = new Scanner(System.in);
        int[] a = {2, 4, 6, 11, 13};
        int[] b = {1, 2, 3, 4, 9, 16, 21};
        int[] c = new int[13];

        System.out.println("두 배열의 병합");

        merge(a, a.length, b, b.length, c); //배열 a와 b를 병합하여 c에 저장

        System.out.println("배열 a와 b를 병합하여 배열 c에 저장했습니다.");
        System.out.println("배열 a : ");
        for (int i = 0; i < a.length; i++) {
            System.out.print("a[" + i + "] = " + a[i]);
        }
        System.out.println("배열 b : ");
        for (int i = 0; i < b.length; i++) {
            System.out.print("b[" + i + "] = " + b[i]);
        }
        System.out.println("배열 c : ");
        for (int i = 0; i < a.length; i++) {
            System.out.print("c[" + i + "] = " + c[i]);
        }

    }
}




2. 1번을 응용하여 분할 정복법에 따라 병합 정렬하기
배열을 2등분하여 앞부분과 뒷부분으로 나눈다.
앞부분과 뒷부분을 각각 정렬하고 병합한다.
여기서 재귀호출을 하게 되는데 그 앞부분과 뒷부분도 또다시 병합정렬을 이용하기 때문이다.




public class MergeSort {
    static int[] buff; //작업용 배열

    //a[left] ~ a[right] 을 재귀적으로 병합 정렬한다.
    static void __mergeSort(int[] a, int left, int right) {
        if (left < right) {
            int i;
            int center = (left + right) / 2;
            int p = 0;
            int j = 0;
            int k = left;

            __mergeSort(a, left, center); //배열의 앞부분을 병합 정렬한다.
            __mergeSort(a, center + 1, right); //배열의 뒷부분을 병합정렬한다.

            for (i = left; i <= center; i++) //버퍼에다 a[left]~a[center]를 복사한다.
                buff[p++] = a[i];

            while (i <= right && j < p) //버퍼와, a배열(center 뒷부분부터 right까지)을 서로 병합정렬한다.
                a[k++] = (buff[j] <= a[i]) ? buff[j++] : a[i++];

            while (j < p) //buff의 나머지 요소를 배열 a에 복사한다.
                a[k++] = buff[j++];
        }
    }

    //병합 정렬
    static void mergeSort(int[] a, int n) {
        buff = new int[n]; //작업용 배열 생성

        __mergeSort(a, 0, n - 1); //배열 전체를 병합 정렬한다.

        buff = null; //작업용 배열 해제
    }

    public static void main(String[] args) {
        Scanner stdIn = new Scanner(System.in);

        System.out.println("병합 정렬");
        System.out.print("요솟수:");
        int nx = stdIn.nextInt();
        int[] x = new int[nx];

        for (int i = 0; i < nx; i++) {
            System.out.print("x[" + i + "] : ");
            x[i] = stdIn.nextInt();
        }

        mergeSort(x, nx);

        System.out.println("오름차순으로 정렬했습니다.");
        for (int i = 0; i < nx; i++) {
            System.out.print("x[" + i + "]= " + x[i]);
        }

    }
}



3. 자바가 제공하는 sort 사용하기
- 기본 자료형 정렬
public class ArraySortTester {
    public static void main(String[] args) {
        Scanner stdIn = new Scanner(System.in);

        System.out.print("요솟수 : ");
        int num = stdIn.nextInt();
        int[] x = new int[num];

        for (int i = 0; i < num; i++) {
            System.out.print("x[" + i + "] :");
            x[i] = stdIn.nextInt();
        }

        //sort 메소드는 기본 자료형의 배열을 정렬한다.
        //퀵 정렬 알고리즘을 개선한 것으로 안정적이지 않다.
        //즉 배열에 같은 값이 존재하는 경우 같은 값 사이의 순서가 바뀔 수 있다.

        Arrays.sort(x);

        for (int i = 0; i < num; i++) {
            System.out.print("x[" + i + "] :");
        }
    }
}



- 클래스 객체 배열의 정렬 (병합 정렬)
클래스 객체 배열의 정렬에서 사용하는 알고리즘은 병합 정렬을 개선한 것으로 안정적인 결과가 보장된다.


아래 메소드들은 기본 오름 차순 정렬이다. static void sort(Object[] a)
static void sort(Object[] a, int fromIndex, int toIndex) : fromIndex 부터 toIndex 까지만 정렬한다.



public class SortCalendar {
    public static void main(String[] args) {
        GregorianCalendar[] x = {
                new GregorianCalendar(2017, Calendar.NOVEMBER, 1),
                new GregorianCalendar(1963, Calendar.OCTOBER, 18),
                new GregorianCalendar(1985, Calendar.APRIL, 5),
        };

        Arrays.sort(x);

        for (int i = 0; i < x.length; i++)
            System.out.printf("%04d년 %02d월 %02d일\n",
                    x[i].get(Calendar.YEAR),
                    x[i].get(Calendar.MONTH) + 1,
                    x[i].get(Calendar.DATE)
            );
    }
}


결과 출력
1963년 10월 18일
1985년 04월 05일
2017년 11월 01일


만약 정렬 시에 원하는 기준이 있을 경우 그 키를 기준으로 하는 Comparator를 구현 하면 된다. 아래 메소드들은 기본 오름 차순 정렬이다.


static void sort(T[] a, Comparator c)
static void sort(T[] a, int fromIndex, int toIndex, Comparator c) : T를 부모로 갖는 클래스이어야 한다.


public class PhyscExamSort {
    static class PhyscData {
        String name;
        int height;
        double vision;

        PhyscData(String name, int height, double vision) {
            this.name = name; this.height = height; this.vision = vision;
        }
        public String toString() {
            return name + " " + height + " " + vision;
        }

        //키 오름차순용 comparator
        //Comparator의 compare 메소드를 오버라이드하여 기준을 정함.

        static final Comparator HEIGHT_ORDER = new HeightOrderComparator();

        private static class HeightOrderComparator implements Comparator {
            @Override
            public int compare(PhyscData d1, PhyscData d2) {
                return (d1.height > d2.height) ? 1 :
                        (d1.height < d2.height) ? -1 : 0;
            }
        }
    }
    public static void main(String[] args) {
        PhyscData[] x = {
                new PhyscData("이나령", 162,0.3),
                new PhyscData("전서현", 122,1.3),
                new PhyscData("홍길동", 172,0.8),
                new PhyscData("유지훈", 182,0.9),
        };
        Arrays.sort(x, PhyscData.HEIGHT_ORDER); //HEIGHT_ORDER 를 사용하여 정렬한다.
    }
}


2019년 12월 5일 목요일

퀵(quick) 정렬






* 퀵 정렬

일반적으로 많이 사용되고 있는 아주 빠른 정렬 알고리즘.
어느 한 요소를 선택하는데 이를 피벗(pivot) 이라고 한다.
그 요소보다 작은 그룹과 큰 그룹을 나누고 각 그룹에서의 피벗 설정과 그룹 나눔을 반복하여 모든 그룹이 1개의 요소가 되면 정렬을 마친다.


1. 배열을 피벗으로 나누어보는 코드

//배열 요소 a[idx1]과 a[idx2]의 값을 바꿉니다.
static void swap(int[] a, int idx1, int idx2) {
    int t = a[idx1];
    a[idx1] = a[idx2];
    a[idx2] = t;
}

//배열을 나눕니다.
static void partition(int[] a, int n) {
    int pl = 0; //point left 왼쪽 커서
    int pr = n - 1; //point right 오른쪽 커서
    int x = a[n / 2]; //피벗(가운데 위치의 값)

    do {
        while (a[pl] < x) pl++; //왼쪽커서의 값이 피벗보다 작은경우 계속 검사
        while (a[pr] > x) pr--; //오른커서의 값이 피벗보다 큰경우 계속 검사
        if (pl <= pr)
            swap(a, pl++, pr--); // 스왑한다.
    } while (pl <= pr);

    System.out.println("피벗의 값은 " + x + "입니다.");

    System.out.println("피벗 이하의 그룹");
    for (int i = 0; i <= pl - 1; i++)
        System.out.print(a[i] + " ");
    System.out.println();

    if (pl > pr + 1) {
        System.out.println("피벗과 일치하는 그룹");
        for (int i = pr + 1; i <= pl - 1; i++)
            System.out.print(a[i] + " ");
        System.out.println();
    }
    
    System.out.println("피벗 이상의 그룹");
    for (int i = pr + 1; i < n; i++)
        System.out.print(a[i] + " ");
    System.out.println();
}




2. 재귀적으로 퀵 소트를 구현 (재귀호출)

static void quickSort(int[] a, int left, int right) {
    int pl = left;
    int pr = right;
    int x = a[(pl + pr) / 2]; //피벗

    do {
        while (a[pl] < x) pl++;
        while (a[pr] > x) pr--;
        if (pl <= pr)
            swap(a, pl++, pr--);
    } while (pl <= pr);

    if (left < pr) quickSort(a, left, pr); //왼쪽끝이 오른커서보다 작으면 왼쪽그룹을 계속 나눈다.
    if (pl < right) quickSort(a, pl, right); //오른끝이 왼쪽커서보다 크면 오른그룹을 계속 나눈다.
}



3. 비재귀적으로 퀵소트를 구현 (스택)

    //비재귀버전
    static void quickSort2(int[] a, int left, int right) {
        IntStack lstack = new IntStack(right - left + 1);
        IntStack rstack = new IntStack(right - left + 1);

        lstack.push(left);
        rstack.push(right);

        while (lstack.isEmpty() != true) {
            int pl = left = lstack.pop();
            int pr = right = rstack.pop();
            int x = a[(pl + pr) / 2]; //피벗

            //그룹 분할
            do {
                while (a[pl] < x) pl++;
                while (a[pr] > x) pr--;
                if (pl <= pr)
                    swap(a, pl++, pr--);
            } while (pl <= pr);

            if (left < pr) {
                // 왼쪽 그룹 범위의 인덱스를 푸시합니다.
                lstack.push(left);
                rstack.push(pr);
            }

            if (pl < right) {
                // 오른쪽 그룹 범위의 인덱스를 푸시합니다.
                lstack.push(pl);
                rstack.push(right);
            }
        }
    }




2019년 12월 4일 수요일

셸 정렬 (shell sort)





셸(shell) 정렬은 단순 삽입 정렬의 장점을 살리고 단점을 보완한 정렬 알고리즘이다.

참고: 단순삽입정렬



//기본 셸 정렬
static void shellSort(int[] a, int n) {
    //h : 요소사이의 간격을 뜻한다. h 간격의 요소를 비교하여 삽입정렬을 진행한다.
    for (int h = n / 2; h > 0; h /= 2) //h는 계속 2로 나눈 몫이 된다.  ex) h = 4, 2, 1
        for (int i = h; i < n; i++) { //i = 4, 5, 6.. 로 차례로 증가
            int j;
            int tmp = a[i]; //a[4] 값을 임시 저장한다.
            //j = 0부터 시작. a[i-h]값이 tmp보다 클때만 for문을 돈다.
            //증분은 j에서 h값을 빼준다.
            for (j = i - h; j >= 0 && a[j] > tmp; j -= h)
                a[j + h] = a[j]; //복사
            a[j + h] = tmp;
        }
}


위 코드의 문제점은 h 수열이 서로 배수가 될 경우 동일한 요소들끼리만 섞인다는 점이다.


//셸 정렬 개선 버전
//h값 수열을 서로 배수가 되지 않도록 h값을 변경
static void shellSort2(int[] a, int n) {
    int h;
    //h의 초기값을 미리 계산해놓는다.
    //초기값은 너무 크면 의미가 없으므로 배열의 요솟수 n을 9로 나눈 몫을 넘지 않도록 정한다.
    //그리고 h간격을 3배한 후 1을 더하면 요소가 충분히 섞일수 있도록 하는 초기값을 구할 수 있다.
    for (h = 1; h < n / 9; h = h * 3 + 1)
        ;
    
    //h를 3으로 나눈 몫을 가지고 간격이 점차 줄어들어
    for ( ; h > 0; h /= 3) //마지막엔 h가 1이 남게 될 것이다.
        for (int i = h; i < n; i++) {
            int j;
            int tmp = a[i];
            for (j = i - h; j >= 0 && a[j] > tmp; j -= h)
                a[j + h] = a[j];
            a[j + h] = tmp;
        }

}


2019년 12월 3일 화요일

버블/단순 선택/단순 삽입 정렬





정렬에서 사용하는 swap 메소드는 아래와 같이 구현하면 된다.

// a[idx1]와 a[idx2]의 값을 바꿉니다.
    static void swap(int[] a, int idx1, int idx2) {
        int t = a[idx1];
        a[idx1] = a[idx2];
        a[idx2] = t;
    }



1. 버블(Bubble) 정렬


    //length가 n 인 배열 a 을 버블정렬한다.
    static void bubbleSort(int[] a, int n) {
        //정렬을 마친 부분을 저장한다.
        int k = 0; // a[k]보다 앞쪽은 정렬은 마친 상태

        while (k < n - 1) {
            int last = n - 1; // 마지막으로 요소를 교환한 위치
            for (int j = n - 1; j > k; j--) //
                if (a[j - 1] > a[j]) {
                    swap(a, j - 1, j);
                    last = j; 
                }
            k = last; 
        }
    }

    // 양방향 버블 정렬(셰이커 정렬)
    static void shakerSort(int[] a, int n) {
        int left = 0;
        int right = n - 1;
        int last = right;

        while (left < right) {
            for (int j = right; j > left; j--) {
                if (a[j - 1] > a[j]) {
                    swap(a, j - 1, j);
                    last = j;
                }
            }
            left = last;

            for (int j = left; j < right; j++) {
                if (a[j] > a[j + 1]) {
                    swap(a, j, j + 1);
                    last = j;
                }
            }
            right = last;
        }
    }


2. 단순 선택 정렬
가장 작은 요소부터 선택해 알맞은 위치로 옮겨서 순서대로 정렬하는 알고리즘이다.


// 1. 가장 작은 요소의 값을 선택.
// 2. 아직 정렬하지 않은 부분의 첫번째 요소를 교환한다.
static void selectionSort(int[] a, int n) {
    for (int i = 0; i < n - 1; i++) {
        int min = i;
        for (int j = i + 1; j < n; j++)
            if (a[min] > a[j]) //최소값의 인덱스를 for 루프를 통해 찾았으면
                min = j; //해당 j(인덱스)를 min 에 저장
        swap(a, i, min); //아직 정렬되지 않은 부분과 min을 교환한다.

    }
}


3. 단순 삽입 정렬
선택한 요소를 그보다 더 앞쪽의 알맞은 위치에 삽입하는 작업을 반복하여 정렬하는 알고리즘이다.

public class InsertionSort {
    //단순 삽입 정렬
    static void insertionSort(int[] a, int n) {
        for(int i = 1; i < n; i++) {
            int j;
            int tmp = a[i]; //2번째 요소부터 선택하고 임시저장
            //tmp와 배열의 값을 하나씩 비교하여 앞이 더 크면 그 값을 뒤로 복사한다.
            for (j = i; j > 0 && a[j - 1] > tmp ; j--)
                a[j] = a[j - 1];
            //a[j - 1] <= tmp 인 경우 for 문을 탈출
            a[j] = tmp; // j인덱스 자리에 tml 값을 삽입
        }
    }

    public static void main(String[] args) {
        Scanner stdIn = new Scanner(System.in);

        System.out.println("단순 삽입 정렬");
        System.out.print("요솟수:");
        int nx = stdIn.nextInt();
        int[] x = new int[nx];

        for (int i = 0; i < nx; i++) {
            System.out.print("x[" + i + "] : ");
            x[i] = stdIn.nextInt();
        }

        insertionSort(x, nx); //배열 x를 단순 삽입 정렬 합니다.

        System.out.println("오른차순으로 정렬했습니다.");
        for (int i = 0; i < nx; i++) {
            System.out.print("x[" + i + "]= " + x[i]);
        }
    }
}

2019년 11월 30일 토요일

8퀸 문제 (재귀 호출)




8개의 퀀을 8x8판에 배치시키는데 서로가 잡힐 수 없도록 배치하는 가지수를 찾아내는 것이다.

퀸은 자신과 같은 열에 있는 다른 퀸을 공격하므로 각 열에 퀸을 1개만 배치해야 한다.
pos라는 배열은 각 배열의 인덱스가 열이 되고, 각 인덱스의 값들이 행이 된다.
즉 한 줄로 8x8의 배치 형태를 표시할 수 있다.


문제에 접근하기 앞서 순차적으로 문제에 접근을 해보자.
이것은 단순히 하나의 열에 퀸이 있다면 그 열에는 퀸이 있을 수 없다는 규칙만을 적용하여 도출된 계산이다.
pop[0]에 0이 있다면 퀸은 최상단좌측에 한개가 위치한 것인데 0열은 또 다른 퀸이 들어갈 수 없으므로 그 다음 열에 퀸이 들어갈 경우의 수가 8가지가 된다.
이런식으로 계산을 하면 $8^8$ 가지의 퀸의 배치 경우의 수가 도출된다.
이것은 단순 가지 뻗기 방식의 퀸의 배치방법이다.

* 가지 뻗기 식의 퀸의 배치

public class QueenB {
    static int[] pos = new int[8]; //각 열의 퀸의 위치를 숫자로 저장

    //각 열의 퀸의 위치를 출력합니다.
    static void print() {
        for (int i = 0; i < 8; i++)
            System.out.printf("%2d", pos[i]);
        System.out.println();
    }

    //i열에 퀸을 놓습니다.
    static void set(int i) { //i:열
        for (int j = 0; j < 8; j++) {
            pos[i] = j; //j:행, 퀸을 j행에 배치합니다.
            if (i == 7)
                print(); //배치된 퀸들을 출력한다.
            else
                set(i + 1); //다음 열에 퀸을 배치한다.
        }
    }
    public static void main(String[] args) {
        set(0); //0열에 퀸을 배치한다.
    }
}


여기서 각 열에 퀸을 1개를 배치하는 조건 뿐만이 아니라 각 행에도 퀸을 한개씩만 배치하여야 하는 조건을 추가해 보자.


public class QueenBB {
    static boolean[] flag = new boolean[8]; //각 행에 퀸을 배치했는데 체크하는 배열
    static int[] pos = new int[8]; //각 열의 퀸의 위치

    //각 열의 퀸의 위치를 출력합니다.
    static void print() {
        for (int i = 0; i < 8; i++)
            System.out.printf("%2d", pos[i]);
        System.out.println();
    }

    static void set(int i) {
        for (int j = 0; j < 8; j++) {
            if(flag[j] == false) { //j행에 퀸을 배치 하지 않았다면
                pos[i] = j; //퀸을 j행에 배치한다.
                if(i == 7) 
                    print();
                else{
                    flag[j] = true; //j행에 배치했으니 flag를 true로 변경
                    set(i + 1); //그리고 다음 열에 퀸을 배치할 때 0행에는 배치가 되지 않는다.
                    flag[j] = false; //그리고 다시 j행은 false 로 변경한다
                }
            }
        }
    }
    public static void main(String[] args) {
        set(0); //0열에 퀸을 배치한다.
    }
}


퀸은 가로 세로 뿐만아니라 대각선으로도 이동할 수가 있다.
따라서 퀸이 배치가 되면 배치된 퀸에서 대각선으로도 퀸을 배치하지 않도록 해야한다.




public class EightQueen {
    static boolean[] flag_a = new boolean[8]; //각 행에 퀸을 배치했는지 체크
    static boolean[] flag_b = new boolean[15]; //↗대각선 방향으로 퀸을 배치했는지 체크
    static boolean[] flag_c = new boolean[15]; //↘대각선 방향으로 퀸을 배치했는지 체크
    static int[] pos = new int[8];

    //각 열의 퀸의 위치를 출력합니다.
    static void print() {
        for (int i = 0; i < 8; i++)
            System.out.printf("%2d", pos[i]);
        System.out.println();
    }

    //i열의 알맞은 위치에 퀸을 배치
    static void set(int i) {
        for (int j = 0; j < 8; j++) {
            if (flag_a[j] == false &&
                    flag_b[i + j] == false &&
                    flag_c[i - j + 7] == false) {
                pos[i] = j;
                if (i == 7)
                    print();
                else {
                    flag_a[j] = flag_b[i + j] = flag_c[i - j + 7] = true;
                    set(i + 1);
                    flag_a[j] = flag_b[i + j] = flag_c[i - j + 7] = false;
                }
            }

        }
    }
    public static void main(String[] args) {
        set(0); //0열에 퀸을 배치한다.
    }

}

대각선 방향에 퀸이 배치가 되었는지 체크하는 배열의 길이가 15개인 이유는 8x8 정사각형의 대각선 방향은 2가지 인데 각 방향마다 15개의 사선이 그려진다.
맨 처음 퀸이 위치하면 가로와 대각선방향으로는 퀸이 위치해서는 안된다. ↗대각선 방향의 사선들 중에 하나의 ↗은 ↗에 배치된 퀸의 행(j)과 열(i)의 합이 동일하다.
고로 flag_b[i + j]를 true로 만든 후 경우의 수를 제거해야한다.
이러한 방법으로 ↘대각선 방향도 규칙을 찾아내어 공식으로 만들면 된다.







2019년 11월 28일 목요일

하노이의 탑(Tower of Hanoi) 재귀 호출 구현하기




자료구조 책을 공부하다가 재귀함수파트가 너무 어려워서 이해한것을 잊어버릴까봐 글로 남긴다.
책대로 해야하는 것은 알겠는데 실제로 정말 될까를 이해하는 과정이 너무 힘들었다.
재귀 함수 안에 재귀함수가 2개이상 호출되는 경우 머리로 순서가 연산이 잘 안된다.ㅋㅋ

지금 생각해보니 몇시간동안 너무 어렵게 생각한 것 같다.
공부한 김에 정리하고 나중에 추가로 수정을 해야할 것이다.

이 코드에서 계속 잘못 생각하고 있었던 부분은 move 메소드 자체였는데,
move 메소드에서 x, y가 의미하는 바는 x 기둥에서 y기둥으로 옮긴다는 것이다.

어쨌든 이 코드에서의 큰 줄기는 가장 아래 원반을 제외한 나머지 원반그룹들을 두번째 기둥으로 옮긴 후 아래 원반을 세번째 기둥에 옮긴다.
그리고 원반 그룹들을 세번째에 옮기면 된다.

큰원반은 작은원반 위로 올라가질 못한다.
원반을 옮기려면 최소한 가장 작은 원반부터 건드려야 한다.

결국 move 메소드는 n개의 원반을 x 기둥에서 y기둥으로 옮긴다.
요약하면 아래와 같다.
기둥 번호는 차례대로 1,2,3 이다.


1. 위에서부터 n-1개의 원반들을 1번째 기둥에서 2번째 기둥으로 옮긴다. (1 -> 2)
2. 맨 아래의 원반 1개를 3번째 기둥으로 옮긴다 (1 -> 3)
3. 2번째 기둥에 있던 n-1개의 원반들을 3번째 기둥으로 옮긴다. (2 -> 3)

그런데 n-1개의 원반들을 기둥에서 기둥으로 옮길 때 move 메소드를 또다시 호출하게 된다. 다만 x, y인자를 어떤기둥에서 옮겨지는지 제대로 넣어주어야 한다.

그리고 원반 개수가 1이 될 때 실제로 옮겼다고 텍스트를 출력하게 된다.



public class Hanoi {
    /**
     * n개의 원반을 x 기둥에서 y 기둥으로 옮김
     * @param no 원반 개수
     * @param x 시작 기둥
     * @param y 목표 기둥
     */
    static void move(int no, int x, int y) {
        //가장 아래의 원반을 뺀 윗 그룹의 원반들을
        //처음 기둥에서 중간 기둥으로 이동한다.
        //x,y가 1과 3일때 6-x-y는 중간 기둥 값이다.
        //그리고 가장 마지막 기둥을 세번째 기둥으로 옮긴다.
        if (no > 1)
            move(no - 1, x, 6 - x - y);
        
        //실제 옮겼다고 출력하는 부분
        System.out.println("원반[" + no + "]을 " + x + "기둥에서 " + y + "기둥으로 옮김");

        //그룹 원반들(no-1)을 중간 기둥에서 목표 기둥으로 이동한다.
        if (no > 1)
            move(no - 1, 6 - x - y, y);
    }

    public static void main(String[] args) {
        Scanner s = new Scanner(System.in);

        System.out.println("하노이의 탑");
        System.out.println("원반 개수:");
        int n = s.nextInt();

        //기둥 번호를 순서대로 1,2,3 칭함
        //하노이탑의 목표: n개의 원반을 1번 기둥에서 3번 기둥으로 옮김
        move(n, 1, 3);
    }
}

유클리드 호제법으로 최대공약수 구하기




* 유클리드 호제법 개념 참고

재귀 호출을 이용하여 유클리드 호제법 알고리즘을 구현하였다.
두 수를 서로 나눈 나머지와 두 수중의 하나의 수는 서로 최대 공약수인 점을 이용하여,
나머지와 하나의 수를 계속 나누어 가다보면 나머지가 0이 되는 시점에서 나누어지는 수를 리턴한다.

public class EuclidGCD {
    //유클리드 호제법 알고리즘을 이용합니다.
    //정수 x,y의 최대 공약수를 구하여 반환합니다.
    static int gcd(int x, int y) {
        if(y == 0)
            return x;
        else
            return gcd(y, x % y);
    }
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int x = sc.nextInt();
        int y = sc.nextInt();

        System.out.println("최대 공약수는 " + gcd(x, y) + "입니다.");
    }
}

2019년 11월 27일 수요일

Java Queue 구현하기



자료구조와 함께 배우는 알고리즘 실습 내용 (157P)
링버퍼로 큐를 구현한 예제

배열로 구현하되 링버퍼로 구현한 큐는 배열의 끝에 도달할 때 다시 처음으로 돌아가게 되는 구조이다. 따라서 디큐를 할 때 앞요소를 가리키는 포인터를 +1 만 해주면 된다.
데이터들을 전부 이동시키거나 할 필요가 없어 링으로 구현한 큐의 복잡도는 $O(1)$이 된다.




/**
 * Description: 링버퍼로 큐를 구현
 */
public class IntQueue {
    private int max; //큐의 용량
    private int front; //앞 요소
    private int rear; //마지막 요소
    private int num; //현재 데이터 수
    private int[] que; //큐 본체

    //실행 시 예외 : 큐가 비어있음
    public class EmptyIntQueueException extends RuntimeException{
        public EmptyIntQueueException() { }
    }
    //실행 시 예외 : 큐가 가득 참
    public class OverflowIntQueueException extends RuntimeException{
        public OverflowIntQueueException() { }
    }
    //생성자
    public IntQueue(int capacity){
        num = front = rear = 0;
        max = capacity;
        try{
            que = new int[max];
        }catch (OutOfMemoryError e){
            max = 0;
        }
    }
    //큐에 데이터를 인큐
    public int enque(int x) throws OverflowIntQueueException {
        if (num >= max)
            throw new OverflowIntQueueException();
        que[rear++] = x;
        num++;
        if(rear == max)
            rear = 0;
        return x; //인큐한 데이터를 리턴
    }
    //큐에 데이터를 디큐
    public int deque() throws EmptyIntQueueException {
        if(num <= 0)
            throw new EmptyIntQueueException();
        int x = que[front++];
        num--;
        if (front == max)
            front = 0;
        return x;
    }
    //큐에서 데이터를 피크(프런트 데이터를 들여다봄)
    public int peek() throws EmptyIntQueueException {
        if(num <= 0)
            throw new EmptyIntQueueException();
        return que[front];
    }
    //큐에서 x를 검색하여 인덱스를 반환, 찾지 못하면 -1
    public int indexOf(int x) {
        //i는 num의 수만큼만 반복하면 된다.
        for(int i = 0; i < num; i++) { 
            /** 
             * 실제 시작지점에서부터의 인덱스를 구하기 위함.
             * 링형태의 인덱스값을 구해야 하므로 
             * max로 나누어 떨어질때 0부터 시작할 수 있도록 한다.
             */
            int idx = (i + front) % max;
            if(que[idx] == x)
                return idx;
        }
        return -1;
    }
    //큐를 비움
    public void clear() {
        num = front = rear = 0;
    }
    //큐의 용량의 반환
    public int capacity() {
        return max;
    }
    //큐에 쌓여있는 데이터 수를 반환
    public int size() {
        return num;
    }
    //큐가 비어있나요?
    public boolean isEmpty() {
        return num <= 0;
    }
    //큐가 가득 찾나요?
    public boolean isFull() {
        return num >= max;
    }
    //큐 안의 모든 데이터를 프런트 -> 리어 순으로 출력
    public void dump() {
        if (num <= 0)
            System.out.println("큐가 비어 있습니다.");
        else{
            for (int i = 0; i < num ; i++)
                System.out.print(que[(i + front) % max] + " ");
            System.out.println();
        }
    }
}

2019년 10월 23일 수요일

안드로이드 화면 전환하기






1. Layout Inflation (레이아웃 인플레이션)

앱이 실행될 때 XML 레이아웃의 내용이 메모리에 객체화되고 객체화된 XML 레이아웃을 소스 파일에서 사용한다.
XML 레이아웃의 내용이 메모리에 객체화 되는 과정을 Inflation(인플레이션)이라고 한다.



* setContentView() : 레이아웃을 메모리에 객체화 하는 메소드

- 인자로 R.layout.activity_main 과 같이 받아서, 해당 레이아웃 XML 파일을 메모리에 객체화한다.
- 화면 전체(메인 화면)에 나타낼 뷰를 지정할 수 있다.
- 화면의 일부분을 차지하는 부분 레이아웃을 객체화할 수는 없다.
- 부분 레이아웃을 메모리에 객체화 하려면 인플레이터를 사용해야 한다.





2. LayoutInflater (부분 화면) 클래스 사용하기

안드로이드는 부분 화면을 객체화 하여 화면에 띄우기 위해 시스템 서비스로 LayoutInflater 라는 클래스를 제공한다.
getSystemService() 메서드를 사용하여 LayoutInflater 객체를 참조할 수 있다.
cf) 시스템 서비스는 단말이 시작되면서 항상 실행되는 서비스이다.
혹은 LayoutInflater.from() 를 이용하여 LayoutInflater 객체를 참조할 수도 있다.



cf) 프로젝트에서 액티비티 추가하기
[app 우클릭] -> [New] -> [Activity] -> [Empty Activity] 



Activity Name을 MenuActivity 라고 입력하고 Finish 클릭


MenuActivity로 실습을 할것이므로 Manifest 파일에 첫 실행 액티비티를 위처럼 수정한다.






public class MenuActivity extends AppCompatActivity {
    
    LinearLayout container;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_menu);
        
        container = findViewById(R.id.container);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener(){
            /**
             * 버튼을 클릭했을 때 부분화면을 전체 레이아웃의 리니어 레이아웃에 추가한다.
             * @param v
             */
            @Override
            public void onClick(View v){
                LayoutInflater inflater = (LayoutInflater) getSystemService(
                        Context.LAYOUT_INFLATER_SERVICE);
                // 부분 레이아웃(sub1.xml)을 메모리에 객체화
                // container에 부분 레이아웃을 붙인다.
                inflater.inflate(R.layout.sub1, container, true);
                // 그 후 container에서 부분 레이아웃의 요소를 참조할 수 있게 된다.
                CheckBox checkBox = container.findViewById(R.id.checkBox);
                checkBox.setText("로딩되었어요.");
            }
        });
    }
}





sub1.xml




activity_menu.xml




추가하기 버튼을 클릭하면 부분화면이 아래에 추가된다.





3. 여러 화면 만들고 화면간 전환하기


프로젝트에 MenuActivity를 새로 추가하면 Manifest 파일에 자동으로 추가된다.
label 속성은 화면의 제목, theme 는 대화상자 형태로 액티비티를 설정한다.



activity_menu.xml
단순하게 버튼 한개만 추가한 형태이다.
이 화면은 대화상자 형태로 표시 될 것이다.


activity_main.xml
메인 액티비티 역시 버튼 하나로 구성되었다.

메인 액티비티에서 버튼을 클릭하면, 메뉴 액티비티를 띄울 것이고
메뉴 액티비티에는 돌아가기 버튼이 있어 클릭 시 메인 액티비티로 돌아가게 된다.




MainActivity를 작성하기 앞서 부모 클래스들에게서
상속받은 메서드들 중 하나인 onActivityResult()를 Override 하여 구현한다.
새로 띄운 액티비티로부터 응답을 처리 하기 위한 메소드 이다. 


MainActivity.java


public class MainActivity extends AppCompatActivity {
    // 새 액티비티를 띄울 때 보낼 코드
    public static final int REQUEST_CODE_MENU = 101;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            /**
             * 버튼을 클릭했을 때 새로운 액티비티(MenuActivity)를 띄운다.
             * @param v
             */
            @Override
            public void onClick(View v) {
                // 액티비티를 띄우기 위해 인텐트 객체를 사용한다.
                // 또한 액티비티간에 데이터를 전달할 수 있다.
                // 이벤트 처리 메서드 안에서 this 변수로 MainActivity 객체를 참조할 수 없으므로
                // getApplicationContext() 를 사용하였다.
                // Intent 생성자 인자로 MenuActivity.class를 넣음으로써 이와 연결된다.
                Intent intent = new Intent(getApplicationContext(), MenuActivity.class);
                // startActivityForResult 는 새 액티비티로부터 응답을 받을 수 있다.
                startActivityForResult(intent, REQUEST_CODE_MENU);
            }
        });
    }

    /**
     * 새로 띄었던 액티비티가 응답을 보내오면 그 응답을 처리한다.
     * @param requestCode : 액티비티를 띄울 때 전달했던 요청 코드
     * @param resultCode : 새 액티비티로부터 전달된 응답코드 (새 액티비티에서 처리한 결과가 정상인지 여부 판단)
     * @param data : 새 액티비티로부터 받은 인텐트
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if(requestCode == REQUEST_CODE_MENU) {
            Toast.makeText(getApplicationContext(),
                    "onActivityResult 호출됨. 요청 코드 : " + requestCode +
                            ", 결과 코드 : " + resultCode, Toast.LENGTH_LONG ).show();
            if(resultCode == RESULT_OK){
                //인텐트로부터 데이터 꺼내오기 
                String name = data.getStringExtra("name");
                Toast.makeText(getApplicationContext(), "응답으로 전달된 name : " + name, Toast.LENGTH_LONG).show();
            }
        }
    }
}


* 새로 띄울 액티비티(MenuActivity.java)


public class MenuActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_menu);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v){
                // 인텐트 객체 생성
                Intent intent = new Intent();
                // name 값을 부가 데이터로 넣기
                intent.putExtra("name", "mike~");
                // 새로 띄운 액티비티에서 이전 액티비티로 인텐트를 전달하기
                setResult(RESULT_OK, intent);
                // 현재 액티비티 화면에서 없애기
                finish();
            }
        });
    }
}


버튼을 클릭하면 다이얼로그 방식의 MenuActivity가 보여진다.


메인 액티비티로 돌아가게 되면 응답을 처리하는 메서드가 호출되며
요청, 결과 코드를 확인할 수 있다.
Toast가 두개여서 나중것이 가려졌는데 Toast 한개를 주석처리하면 확인 할 수 있다.





4. Intent (인텐트) 사용하기

인텐트는 앱 구성 요소 간에 작업 수행을 위한 정보를 전달하는 역할을 한다.


* 인텐트의 기본 구성 요소 : 액션(Action) , 데이터(Data)

액션은 수행할 기능이며 데이터는 수행될 대상의 데이터를 의미한다.

액션과 데이터를 이용해 인텐트 객체를 생성하고 필요한 액티비티를 띄어줄 수 있다.

* 인텐트 종류 : 명시적 인텐트, 암시적 인텐트
명시적 인텐트는 클래스 객체나 컴포넌트 이름을 지정하여 호출할 대상을 확실히 아는 경우를 말한다.
암시적 인텐트는 액션과 데이터를 지정하였으나 호출할 대상이 달라질 수 있는 경우를 말한다.

- 암시적 인텐트의 여러가지 속성


  • 범주(Category) : 액션이 실행되는데 필요한 추가적 정보
  • 타입(Type) : 인텐트에 들어가는 데이터의 MIME 타입을 명시적으로 지정
  • 컴포넌트(Component) : 인텐트에 사용될 컴포넌트 클래스 명을 명시
  • 부가 데이터(Extra Data) : 추가적인 정보가 들어있는 번들 데이터를 전달


아래 코드는 인텐트에 액션과 데이터를 넣어 다른 앱의 액티비티를 띄우는 코드이다.


단순히 입력상자와 버튼을 만들었다.
입력상자에는 전화번호 포맷 형식의 데이터를 입력한다.



public class MainActivity extends AppCompatActivity {

    EditText editText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        editText = findViewById(R.id.editText);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //입력 상자에 입력된 전화번호 가져오기
                String data = editText.getText().toString();
                //전화 걸기 화면을 보여줄 인텐트 객체 생성
                Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(data));
                //액티비티 띄우기 (액티비티 메모리에 올리기)
                startActivity(intent);
            }
        });
    }
}


전화걸기 버튼을 클릭했을 때,
자동으로 전화번호 앱이 실행되는 것을 확인할 수 있다.


아래 두번째 코드는 컴포넌트 이름을 이용해 새로운 액티비티를 띄우는 경우이다.
위 화면 레이아웃에서 id가 button2인 버튼을 하나더 추가하고, 새로운 액티비티(MenuActivity)를 하나 생성한다.


Button button2 = findViewById(R.id.button2);
        button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                //컴포넌트 이름을 지정할 수 있는 객체 생성
                //ComponentName 생성자의 인자로 순서대로 패키지명과, 클래스 명이 된다.
                ComponentName name = new ComponentName("org.techtown.samplecallintent",
                        "org.techtown.samplecallintent.MenuActivity");
                // 인텐트 객체에 컴포넌트를 지정한다.
                intent.setComponent(name);
                startActivityForResult(intent,101);
            }
        });


컴포넌트이름을 지정할 때 클래스 명은 앞에 패키지 경로까지 전부다 적어주어야 한다.
인텐트를 이용해 다른 화면을 띄울 수가 있는데 직접 만든 화면이나, 다른 사람이 만든 앱의 화면을 띄울 수도 있다.





5. 플래그와 부가 데이터 사용하기

startActivity 또는 startActivityForResult 메서드를 여러 번 호출 하게 될 경우 메모리에 여러 개가 중복되어 만들어질 것이다.
따라서 중복된 액티비티는 띄우지 않게 하기 위해 플래그를 사용한다.

액티비티가 만들어질 때마다 액티비티 스택이라는 곳에 차곡차곡 쌓이게 되는데, startActivity 또는 startActivityForResult를 호출하게 되면 이전의 액티비티는 액티비티 스택에 저장되고 새로 만들어진 액티비티가 화면에 보이게 된다.
 즉 가장 상위에 쌓인 것부터 화면에 보여지면서 back 시스템 버튼을 눌렀을 때 그 아래의 액티비티가 보여지게 된다.

동일한 액티비티들을 여러 번 실행한다면 동일한 액티비티가 스택에 여러 개 들어가게 되는 문제를 해결해주는 것이 플래그이다.

* 대표적인 플래그들
  • FLAG_ACTIVITY_SINGLE_TOP : 이미 생성된 액티비티가 있다면 그 액티비티를 그대로 사용하라는 플래그. 기존에 사용하는 액티비티에서 인텐트 객체만 전달받으려면 onNewIntent()를 오버라이드하면 된다.
  • FLAG_ACTIVITY_NO_HISTORY : 처음 이후에 실행된 액티비티는 액티비티 스택에 쌓이지 않는다.
  • FLAG_ACTIVITY_CLEAR_TOP : 이것이 설정되어 있는 액티비티 위에 있는 다른 액티비티를 모두 종료하게 된다. 홈 화면과 같이 항상 우선하는 액티비티를 만들때 유용함.



* 인텐트에 객체를 직렬화하여 데이터 보내기

객체를 직렬화 하기 위해서는 데이터를 담고 있는 클래스는 Parcelable 인터페이스를 구현해야한다.
자바의 Serializable 직렬화와 동일한 개념이다.

* Parcelable 직렬화 필수 구현
  1. CREATOR 상수 정의
  2. describeContents() 구현
  3. writeToParcel(Parcel dest, int flags) 구현



import android.os.Parcel;
import android.os.Parcelable;

/*
    안드로이드에서의 직렬화는 Parcelable 인터페이스를 구현함으로써 사용할 수 있다.
 */
public class SimpleData implements Parcelable {

    int number;
    String message;

    public SimpleData(int num, String msg){
        number = num;
        message = msg;
    }

    /**
     * SimpleData의 생성자의 인자로 Parcel 객체를 받아들이는데
     * Parcel 객체에서 데이터를 읽어들이는 역할을 한다.
     * @param src
     */
    public SimpleData(Parcel src){
        number = src.readInt();
        message = src.readString();
    }

    /**
     * CREATOR 상수를 정의해야한다.
     * Parcel 객체를 읽어들이고, 객체를 생성하는 역할
     * 반드시 static final 로 선언해야 한다.
     */
    public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {

        public SimpleData createFromParcel(Parcel in){
            return new SimpleData(in);
        }

        public SimpleData[] newArray(int size){
            return new SimpleData[size];
        }

    };

    /**
     * 필수 구현
     * @return
     */
    public int describeContents(){
        return 0;
    }

    /**
     * 필수 구현
     * SimpleData 객체에 있는 데이터를 Parcel 객체로 만드는 역할을 한다.
     * @param dest
     * @param flags
     */
    public void writeToParcel(Parcel dest, int flags){
        dest.writeInt(number);
        dest.writeString(message);
    }

}



public class MainActivity extends AppCompatActivity {

    public static final int REQUEST_CODE_MENU = 101;
    public static final String KEY_SIMPLE_DATA = "data";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(getApplicationContext(), MenuActivity.class);
                // 객체를 생성
                SimpleData data = new SimpleData(100, "Hello Android!!");
                // 인텐트에 객체를 보낸다.
                intent.putExtra(KEY_SIMPLE_DATA, data);
                startActivityForResult(intent, REQUEST_CODE_MENU);
            }
        });
    }
}



public class MenuActivity extends AppCompatActivity {

    TextView textView;

    public static final String KEY_SIMPLE_DATA = "data";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_menu);

        textView = findViewById(R.id.textView);
        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                intent.putExtra("name", "mike");
                setResult(RESULT_OK, intent);

                finish();

            }
        });
        /**
         * 메인 액티비티가 전달한 인텐트를 참조하기 위해 onCreate()안에서 getIntent()를 호출
         */
        Intent intent = getIntent();
        processIntent(intent);

    }

    public void processIntent(Intent intent){
        if(intent != null){
            //Bundle 객체를 반환한다.
            Bundle bundle = intent.getExtras();
            //Bundle 객체에서 키를 이용하여
            //getParcelable메서드를 호출하면 SimpleData 객체을 얻어낼 수 있다.
            SimpleData data = bundle.getParcelable(KEY_SIMPLE_DATA);

            textView.setText("전달 받은 데이터\nNumber: "+data.number +
                    "\nMessage:"+data.message);

        }
    }
}





6. 태스크(Task) 관리 이해하기

태스크는 앱이 어떻게 동작할지 결정하는데 사용된다.
태스크를 이용하면 프로세스처럼 독립적인 실행 단위와 상관없이 어떤 화면들이 같이 동작해야 하는지 흐름을 관리할 수 있다.

프로세스끼리는 정보를 공유할 수가 없다.
따라서 하나의 프로세스에서 다른 프로세스의 화면을 띄우려면 시스템의 도움이 필요하다. 
시스템에서 이런 액티비티의 각종 정보를 저장해두기 위해 태스크라는 것을 만든다.

시스템에서는 액티비티들의 정보를 저장해두기 위해 태스트라는 것을 만들고 알아서 관리하지만 직접 제어할 수도 있다.

AndroidManifest.xml 에 액티비티를 등록할 때 태스크도 함께 설정할 수 있다.


public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                //나 자신을 띄운다.
                Intent intent = new Intent(getApplicationContext(), MainActivity.class);
                startActivity(intent);

            }
        });
    }
}

위 코드는 버튼을 클릭할 때마다 자기자신의 액티비티를 스택에 넣는다. 이것은 AndroidManifest.xml 파일에서 activity 태그의 속성에서 android:launchMode="standard" 을 넣어주는 것과 같다.

만약 android:launchMode="singleTop" 으로 설정한다면 태스크의 가장 위쪽에 있는 액티비티는 더 이상 새로이 만들지 않게된다.
이는 5번의 플래그의 FLAG_ACTIVITY_SINGLE_TOP 와 같은 효과이다.
따라서 중복되어 액티비티가 생성되지 않으므로 (onCreate메서드가 실행되지 않음)
인텐트는 onNewIntent()메서드로 전달받으면 된다.

singleTask : 액티비티가 실행되는 시점에 새로운 태스크를 만들게 된다.
singleInstance: 액티비티가 실행되는 시점에 새로운 태스크를 만들면서, 그 이후에 실행되는 액티비티들은 이 태스크를 공유하지 않는다.





7. 액티비티의 수명주기와 SharedPreferences 이해하기


onCreate -> onStart -> onResume -> onPause ->onStop -> onDestroy


앱이 갑자기 중지되어 앱 데이터의 저장과 복원이 필요할 때가 있다.
간단한 데이터를 저장하거나 복원시에는 SharedPreferences 를 사용할 수 있다.


아래 코드는 앱을 실행했을 때 입력상자에 사람 이름을 입력한 상태에서, 앱을 종료한 후 다시 실행했을 때 사람 이름이 그대로 보이도록 만드는 내용이다.

앱이 갑자기 중지 될때 항상 호출되는 메서드는 onPause이다.
앱이 시작될 때 항상 호출되는 메서드는 onResume이다.
즉 중지 시에 데이터를 저장하고 시작할 때 데이터를 복원하면 된다. 




public class MainActivity extends AppCompatActivity {

    EditText nameInput;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(getApplicationContext(), MenuActivity.class);
                startActivity(intent);
            }
        });
        Toast.makeText(this, "onCreate 호출됨.",Toast.LENGTH_LONG).show();
        nameInput = findViewById(R.id.nameInput);
        
    }

    @Override
    protected void onStart() {
        super.onStart();
        println("onStart 호출됨.");
    }

    @Override
    protected void onStop() {
        super.onStop();
        println("onStop 호출됨.");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        println("onDestroy 호출됨.");
    }

    public void println(String data){
        Toast.makeText(this, data, Toast.LENGTH_LONG).show();
        //Logcat에 로그 출력하기
        Log.d("Main", data);
    }

    @Override
    protected void onPause() {
        super.onPause();
        Toast.makeText(this, "onPause 호출됨", Toast.LENGTH_LONG).show();
        //현재 입력상자에 입력된 데이터를 저장한다.
        saveState();
    }

    @Override
    protected void onResume() {
        super.onResume();
        Toast.makeText(this, "onPause 호출됨", Toast.LENGTH_LONG).show();
        //설정 정보에 저장된 데이터를 복원한다.
        restoreState();
    }

    protected  void restoreState(){
        SharedPreferences pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
        if( (pref != null) && (pref.contains("name"))){
            String name = pref.getString("name","");
            nameInput.setText(name);
        }
    }

    protected void saveState(){
        SharedPreferences pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
        SharedPreferences.Editor editor = pref.edit();
        editor.putString("name", nameInput.getText().toString());
        editor.commit(); //커밋을 해야 데이터가 저장이 된다.
    }

    protected void clearState(){
        SharedPreferences pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
        SharedPreferences.Editor editor = pref.edit();
        editor.clear();
        editor.commit();
    }
}













2019년 10월 22일 화요일

안드로이드 이벤트 처리





1. 터치 이벤트, 제스처 이벤트

터치 이벤트는 화면을 손가락으로 누를 때 발생하는 이벤트들을 처리할 수 있다.
제스처 이벤트는 터치 이벤트 중에서 일정한 패턴, 스크롤과 같은 이벤트를 처리할 수가 있다.
제스처 이벤트는 터치 이벤트를 받은 후에 추가적인 확인을 거쳐 만들어진다.




package org.techtown.sampleevent;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    TextView textView;
    GestureDetector detector;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = findViewById(R.id.textView);

        View view = findViewById(R.id.view);
        /**
         * 뷰에 OnTouchListener 를 등록한다.
         */
        view.setOnTouchListener(new View.OnTouchListener(){
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent){
                // 액션의 상태 값을 가져오는 메소드
                int action = motionEvent.getAction();
                // 현재 X, Y 좌표의 위치를 가져옴
                float curX = motionEvent.getX();
                float curY = motionEvent.getY();

                if(action == MotionEvent.ACTION_DOWN) {
                    println("손가락 눌림 : "+ curX +" , " + curY);
                }else if( action == MotionEvent.ACTION_MOVE) {
                    println("손가락 움직임 : " + curX + " , " +  curY);
                }else if(action == MotionEvent.ACTION_UP) {
                    println("손가락 뗌 : " + curX + " , " + curY);
                }
                return true;
            }
        });

        //제스쳐 이벤트를 생성
        detector = new GestureDetector(this, new GestureDetector.OnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) {
                println("onDown() 호출됨.");
                return true;
            }

            @Override
            public void onShowPress(MotionEvent e) {
                println("onShowPress() 호출됨.");
            }

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                println("onSingleTapUp() 호출됨.");
                return true;
            }

            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                println("onScroll() 호출됨 : "+ distanceX + " , " + distanceY);
                return true;
            }

            @Override
            public void onLongPress(MotionEvent e) {
                println("onLongPress() 호출됨.");
            }

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                println("onFling() 호출됨 : "+ velocityX + ", " + velocityY);
                return true;
            }
        });
        /**
         * 두번째 뷰에다 제스쳐이벤트를 설정
         */
        View view2 = findViewById(R.id.view2);
        view2.setOnTouchListener(new View.OnTouchListener(){
                @Override
                public boolean onTouch(View view, MotionEvent motionEvent ){
                    // 뷰를 터치했을때 발생하는 터치이벤트를 제스터 디렉터로 전달한다.
                    detector.onTouchEvent(motionEvent);
                    return true;
                }
        });
    }

    public void println(String data) {
        textView.append(data + "\n");
    }

}



스크롤뷰 -> 리니어레이아웃 -> 텍스트뷰에서 이벤트 정보를 찍어보기로 한다.


파란 부분이 첫번째 뷰, 주황 부분이 두번째 뷰이다.
이벤트가 발생할때마다 아래의 스크롤뷰에 수직으로 데이터들이 쌓인다.





2. 키 이벤트 처리하기

키 입력은 시스템 버튼이 눌렸거나, 문자열 키가 입력된 경우에 관하여 이벤트 처리를 할 수 있다.


- 시스템 [back] 버튼 눌렸을 때 토스트 메시지 생성하기
시스템 버튼인 back은 onKeyDown 메서드를 재정의 하여 간단히 이벤트 처리할 수 있다.


MainActivity.java 에서 우클릭 한 후 [Generate] 선택
[Override Methods] 선택


[onKeyDown] 메소드를 선택하고 [OK] 하면 메소드가 자동으로 생성된다.





@Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        
        if(keyCode == KeyEvent.KEYCODE_BACK){
            Toast.makeText(this, "시스템 [back] 버튼이 눌렸습니다.",
                    Toast.LENGTH_LONG).show();
            return true;
        }
        return false;
    }





3. 단말 방향 전환 시 이벤트 처리

단말의 방향 전환 시에는 앱화면에 대하여 가로, 세로 각각 XML 레이아웃을 만들 필요가 있다.
단말의 방향이 바뀌었을 때는 액티비티는 메모리에서 없어졌다가 다시 만들어지게 된다.


/app/res/layout/ : 단말 세로 방향
/app/res/layout-land/ : 단말 가로 방향


[res] 폴더에서 우클릭하여 [New] -> [Android Resource Directory] 클릭


Directory name 을 layout-land 로 입력하고 OK.


Project Files 구조로 볼때만 layout-land 폴더가 보이게 된다.
생성된 것을 확인.


layout 폴더와 layout-land 는 같은 역할을 하지만 단말이 가로방향으로 보일 때는 layout-land 폴더 안에 있는 XML 레이아웃 파일이 사용된다.
layout-land 폴더가 없다면 어떤 방향이든 layout 폴더의 XML 파일을 디폴트로 사용하게 된다.


단말의 방향이 바뀌었을 때는 액티비티는 메모리에서 없어졌다가 다시 만들어지게 된다면, 이전에 선언해 두었던 변수 값이 사라지므로 이를 유지하도록 해야한다.

이를 위해 onSaveInstanceState 콜백 메서드가 제공된다.
이 메서드는 액티비티가 종료되기 전의 상태를 저장한다.
이때 저장한 상태는 onCreate() 메서드가 호출될 때 전달되는 번들 객체로 복원할 수 있다.




public class MainActivity extends AppCompatActivity {
    String name;

    EditText editText;

    /**
     * onCreate의 파라미터를 보면 Bundle 객체인 savedInstanceState를 인자로 받게 되어 있다.
     * 이 객체에서 데이터를 가져와서 name 변수에 다시 할당하게 되어 복구되는 것이다.
     * @param savedInstanceState
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        showToast("onCreate 호출됨.");

        editText = findViewById(R.id.editText);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view){
                // 버튼을 클릭했을 때 사용자가 입력한 값을 name 변수에 저장
                name = editText.getText().toString();
                showToast("입력된 값을 변수에 저장하였습니다 : "+ name);
            }
        });
        // 이 화면이 초기화 될 때 name 변수값 복원
        // ex) 단말 방향 전환시 액티비티가 메모리에서 제거되는 상황
        if(savedInstanceState != null){
            name = savedInstanceState.getString("name");
            showToast("값을 복원하였습니다 : "+name);
        }

    }
    /**
     * onSaveInstanceState 메소드는
     * 액티비티가 종료되기 전의 상태를 저장한다.
     * @param outState
     */
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString("name", name);
    }

    @Override
    protected void onStart() {
        super.onStart();
        showToast("onStart 호출됨.");
    }

    @Override
    protected void onStop() {
        super.onStop();
        showToast("onStop 호출됨.");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        showToast("onDestroy 호출됨.");
    }

    public void showToast(String data){
        Toast.makeText(this, data, Toast.LENGTH_LONG).show();
    }

}





4. 단말 방향이 바뀔 때 액티비티 유지하는 방법

- /app/manifests/AndroidManifest.xml 수정

아래와 같이 빨간네모상자의 속성을 설정하면, 시스템은 단말의 방향이 바뀌는 시점에 액티비티에게 상태 변화를 알려줄 수 있게 된다. 
keyboardHidden 값은 키패드가 자동으로 나타나지 않도록 하는 설정이다.


activity 태그가 액티비티를 등록할 때 사용하는 태그이다.
configChanges 속성을 위와 같이 추가한다.





public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    /**
     * 액티비티에서 방향 전환을 인식할 수 있게 되어,
     * 단말의 방향이 바뀌는 시점에 자동으로 이 메서드가 호출된다.
     * @param newConfig
     */
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        if(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
            showToast("가로 방향: ORIENTATION_LANDSCAPE");
        }else if(newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
            showToast("세로 방향: ORIENTATION_PORTRAIT");
        }
    }
    public void showToast(String data){
        Toast.makeText(this, data, Toast.LENGTH_LONG).show();
    }

}




5. 단말 방향을 고정시키기

- /app/manifests/AndroidManifest.xml 수정


screenOrientation 속성에서 landscape로 설정하면 화면이 가로로 고정된다.