2019년 6월 12일 수요일

Java Comparator 인터페이스를 이용한 숫자 정렬






* Comparator 이용하여 숫자 정렬하기


Comparator 인터페이스, compare 메소드, Array sort, API 문서 참고: 
https://docs.oracle.com/javase/7/docs/api/index.html?overview-summary.html


문제는 아래와 같이 주어진 String 문자열들을 내림차순으로 정렬해야 하는데 000.000 같은 경우 0.0으로 출력되면 안되고, 반드시 형태를 유지해야한다.

[sample input]

9
-100
50
0
56.6
90
0.12
.12
02.34
000.000




class Solution{
    public static void main(String []args){
        //Input
        Scanner sc= new Scanner(System.in);
        int n=sc.nextInt();
        String []s=new String[n+2];
        for(int i=0;i<n;i++){
            s[i]=sc.next();
        }
        sc.close();

        Arrays.sort(s,0,n, Collections.reverseOrder(new Comparator<String>() {
            @Override
            public int compare(String a1, String a2) {
                BigDecimal a = new BigDecimal(a1);
                BigDecimal b = new BigDecimal(a2);
                return a.compareTo(b);
            }
        }));
        //Output
        for(int i=0;i<n;i++)
        {
            System.out.println(s[i]);
        }
    }
}
기존 String 배열을 재정렬하는 부분의 코드를 분석해보자.

먼저 Array.sort로 들어오는 인자를 보면, s String 배열을 0번째부터 n번째까지 Comparator의 정렬 기준으로 정렬한다. n은 포함이 아니다.
Comparator 객체를 다시한번 reverseOrder 를 하여 문제가 요구하는대로 내림차순이 되도록 하였다.

Comparator 객체를 생성하면서 동시에 compare메소드를 오버라이드(재정의) 하여 새로운 정렬 기준을 작성한다.
BigDecimal 클래스를 이용하여 String 형으로 되어있는 숫자를 변환하였고, 0의 손실이 일어나지 않도록 하였다.
또한 BigDecimal 클래스의 compareTo 메소드는 a가 b보다 앞서있는 경우에는 음수를, 동일하면 0, a가 b보다 뒤에있다면 양수를 반환한다. 
그래서 이 메소드가 반환하는 값이 Comparator클래스에서 compare 메소드의 return 값이 된다.
BigDecimal의 compareTo 메소드는 Comparator에서 구현해야하는 compare의 조건과 일치한다.
Comparator 클래스에서 오버라이드를 하는 compare 메소드에는 주의해야 할 필수 조건이 있다.

1. sgn(compare(x,y)) 가 -sgn(compare(y,x))와 같아야 한다는 것이다.

* sgn?
In the foregoing description, the notation sgn(expression) designates the mathematical signum function, which is defined to return one of -1, 0, or 1 according to whether the value of expression is negative, zero or positive.

2. ((compare(x, y)>0) && (compare(y, z)>0)) 로부터 compare(x, z)>0 을 추정할 수 있어야 한다.
3. sgn(compare(x, z))==sgn(compare(y, z)) for all z 라면, 이것으로부터 compare(x, y)==0 임을 알 수 있어야 한다.

위 조건을 반드시 만족하도록 compare 메소드를 오버라이드 시켜야 제대로 된 정렬이 일어날 것이다.

2019년 6월 4일 화요일

정규표현식 예제




1. IP 주소 패턴

IP 주소는 0-255까지 숫자 범위가 있기 때문에 각 자리수별로 올 수 있는 숫자를 생각하여 정규표현식으로 표현해야 한다.
 아이피 숫자가 한자리 혹은 두자리 수 일 경우에는 아무런 숫자가 와도 된다. 
 세자리 숫자일 경우에는 생각할 꺼리가 많아지는데, 맨 앞자리가 0,1,2 인 경우에 따라 각각의 케이스들을 | (pipeline) 으로 연결해 주면 된다.
 아이피 숫자는 맨 앞자리가 0이어도 유효하기 때문에 (0|1)일 경우에는 뒤에 두자리가 [0-9] 아무 숫자가 와도 된다. 맨 앞자리가 2일 경우에는 두 가지 분기로 나누어야만 하는데, 
두번째 자리가 [0-4]이면은 일의 자리는 [0-9] 아무숫자나 와도 되지만 앞자리 두개가 25일 경우 일의 자리는 0-5까지만 와야 한다. 
 아래 예시에 IP 매칭에 대한 정규표현식을 작성하였는데 두 개는 동일한 내용이다. 
 두 번째가 [0-9]를 \\d로 표현했기 때문에 더 간결해 보인다.
\\. 이 부분도 점 그 자체로 검색해야 하므로 이스케이프 문자가 붙어있다.



String pattern = "([0-9]{1,2}|(0|1)[0-9][0-9]|2[0-4][0-9]|25[0-5])(\\.([0-9]|[0-9][0-9]|(0|1)[0-9][0-9]|2[0-4][0-9]|25[0-5])){3}";

String pattern = "(\\d{1,2}|(0|1)\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d{1,2}|(0|1)\\d{2}|2[0-4]\\d|25[0-5])){3}";


2. 중복된 단어 찾기 

아래와 같은 문자열에서 여러번 중복된 단어들을 제거하고 첫번째 문자열만 남긴다. 대소문자의 구분은 없다.


test Matched matched matched matchedtest String string String matched


이걸 풀기 위해서 정규표현식의 grouping 을 이용하면 쉽게 풀린다. group() 관련 메소드는 API문서를 참고하면 된다.


연속으로 중복되는 단어들을 전부 찾아내고, 그 중에 첫번째 단어만 남겨야 한다.
패턴은 아래와 같다.


String pattern = "\\b(\\w+)(?:\\W\\1\\b)+";

하나씩 의미를 파악해보자면,

\\b(\\w+)  
\\b로 단어의 경계를 지정하고 한 글자 이상의 단어를 찾는다. 그 단어 한개가 첫번째 그룹이다. 

(?:\\W\\1\\b)+ 
(?:X) 이 문법은 이 괄호 안에 잡히는 대상은 그룹으로 잡지 않겠다는 의미이다. 또한 마지막에 + 로 되어있는데 이런 그룹이 연속으로 1개 이상이라는 의미이다.

\\W는 word가 아닌 문자열을 의미한다. 중복된 단어 사이에 공백이나 , . 등이 있는 경우도 포함하기 위함이다.
\\1 은 첫번째 그룹에서 가장 최근에 매치된 동일한 텍스트를 의미한다. 
만약 그룹이 여러개라면 \\n 이런 식으로 표현 가능하며, n번째 그룹에서 가장 최근에 매치된 텍스트를 골라낼 수 있다.
위 예제에서는 만약 단어 Matched가 매치되면 그 다음에 이 단어가 \\1자리에 위치하는 것이다.
마지막에 단어의 경계를 나타내는 \\b를 명시하지 않으면 위 예시에서 matchedtest에서 matched까지 찾아내게 된다.

이제 이 정규표현식을 통해 중복을 제거한다.
핵심은 하나의 문장을 입력받아서 하나의 문장을 Matcher가 검사하는데 하나의 문장 내에서도 while문에 Matcher로 find()를 하면서 나란히 중복되는 문자열을 찾는다. 
그리고 그 찾아낸 문자열의 m.group() 을 m.group(1) 로 replaceAll() 을 한다.

m.group()은 위 정규표현식에서 전체에 매칭되는 Full match를 의미한다.

test Matched matched matched matchedtest String string String matched

Matched matched matched 가 첫번째 m.group()이 되고 Matched는 첫번째 그룹의 값인 m.group(1)이 된다. 
String string String은 그 다음 while 루프에서 찾아지며 동일하게 첫번째 String으로 replaceAll이 될 것이다.




public class DuplicateWords {

    public static void main(String[] args) {

        String regex = "\\b(\\w+)(?:\\W\\1\\b)+";
        Pattern p = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); //대소문자 구분없이 찾는다

        Scanner in = new Scanner(System.in);
        int numSentences = Integer.parseInt(in.nextLine());
        
        while (numSentences-- > 0) {
            String input = in.nextLine();
            
            Matcher m = p.matcher(input);
            
            // Check for subsequences of input that match the compiled pattern
            while (m.find()) {
                input = input.replaceAll(m.group(), m.group(1)); 
            }
            // Prints the modified sentence.
            System.out.println(input);
        }
        
        in.close();
    }
}


3. 아이디 제한 걸기

아이디 생성 조건은 다음과 같다.
- 문자열 길이는 8자 이상~ 30자 이하.
- 첫 시작 문자는 반드시 알파벳이어야 함.
- 알파벳 대소문자, 숫자, _ 가능
- 특수문자 불가

위 조건은 아래와 같이 심플하게 정규표현식으로 가능하다.


String pattern = "\\p{Alpha}[\\w]{7,29}";

7,29로 범위를 지정한 이유는 앞에 \\p{Alpha} 가 한자리를 차지하기 때문에 1씩 빼주었다. 



4. Html Tag 안의 텍스트 내용 추출하기

아래는 Tag의 내용을 추출하는 코드이다.
다만 태그의 열고 닫는 문법이 정확해야 그 내용을 추출해야한다. 만약 <tag>내용...<tag>이라면 올바른 태그 형식이 아니다.


public class Solution{
 public static void main(String[] args){
  
 Scanner in = new Scanner(System.in);
 int testCases = Integer.parseInt(in.nextLine());
 while(testCases>0){
     String line = in.nextLine();
   
            String pattern = "<(.+)>([^<>]+)</\\1>";
            Pattern p = Pattern.compile(pattern);
            Matcher m = p.matcher(line);
            boolean flag = false;
   while(m.find()){
                flag = true;
                System.out.println(m.group(2));
            }
            if(!flag)
                System.out.println("None");
   testCases--;
  }
 }
}

String pattern = "<(.+)>([^<>]+)</\\1>";

먼저 여는 태그 부분의 꺽쇠 안의 내용 그룹 한개와, content에 해당하는 부분을 2개의 그룹으로 만들고, 닫는 태그는 \\1을 이용하여 첫번째 그룹의 캡쳐된 문자열을 넣도록 하여 태그 문법이 맞도록 하였다.
content에는 < 혹은 >를 제외한 모든 문자가 들어가도록 되어있다. 
<h1><a>contents</a>invalid</h1> 이런 식의 형태에서 추출 해야 할 때 <a>태그 안에 있는 contents 문구는 유효하지만, invalid 문구는 유효하지 않도록 하는 것이 문제의 규칙이다.


2019년 6월 3일 월요일

java String split 메소드, 문자열 쪼개기




* String.split 메소드 정규표현식 이용하여 자르기



 Java에서 String을 특정 구분자로 잘라야 할 때 보통 split 메소드를 많이 쓰는데 그에 대한 예제이다.

 문자열 중에서 공백을 포함한 특수문자가 들어간 문자열은 모두 쪼개는 코드이다.
처음 문제를 풀 땐 어떻게 해서든 답을 맞춰야 하기 때문에,
아래와 같이 정규표현식에 특수문자를 일일이 지정해주고 쪼갠 후에 비어있는 배열의 값을 체크하여 전체 배열의 길이에서 빼는 임시 방편으로 땜빵(?)하는 모습이다.
빈 문자열 객체나 공백만 있는 문자열은 카운팅하지 않는 것이 문제의 조건이었다.


public class Solution {

    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        String s = scan.nextLine();
        // Write your code here.
        String[] arr = s.split("[\\s!,?._'@]");
        int n = 0;
         for(String t : arr){
            if(t.isEmpty())
                n++;
                continue; 
        }
        System.out.println(arr.length-n);

        for(String t : arr){
            if(t.isEmpty())
                continue;
            
            System.out.println(t);
        }
        scan.close();
    }
}


동일한 내용의 코드를 개선시킨 버전이다.

public class Solution {

    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        String s = scan.nextLine().trim();
        // Write your code here.

        String[] arr = s.split("[^\\p{Alpha}]+");
        int size = s.isEmpty() ? 0 : arr.length;

        System.out.println(size);        
        for(String t : arr){
            if(t.isEmpty())
                continue;
            System.out.println(t);
        }

        scan.close();
    }
}


정규표현식 부분을 아래와 같이 바꾸는 것이 가독성에 더 좋을 것이다.
문자가 아닌 것은 특수문자와 공백이다.


String[] arr = s.split("[^\\p{Alpha}]+");



또한 삼항 연산자를 이용하여 본래 문자열이 그냥 "" 라면 0을 대입한다.
 s = ""  (빈문자열) 을 위의 조건으로 split을 하게 되면 s는 그 어떤 것으로도 쪼개지지가 않기 때문에 그 자체가 arr에 들어가게 된다. 
 arr 배열은 {""} 가 되고, length가 1이 되므로 빈 문자열은 계산에서 빼기 위하여 삼항연산자로 s가 빈문자열 일 때의 처리를 해준 것이다.