반응형

캐시(난이도: 하) 알고리즘 문제



하편



 4. 도시 객체 배열을 처리할 수 있는 객체를 생성합니다.


입력 형식 1 : 캐시 크기(cacheSize)와 도시이름 배열(cities)을 입력받는다.
입력 형식 3 : cities는 도시 이름으로 이뤄진 문자열 배열로, 최대 도시 수는 100,000개이다.

조건 1 : 캐시 교체 알고리즘은 LRU(Least Recently Used)를 사용한다.


도시이름을 객체로 만든것처럼, 도시이름 배열을 일급 컬랙션 객체로 만들어 처리합니다.
배열로 한다면... 너무 번거로운 작업이 많아져 List로 처리하였습니다.

@Test
public void 도시_포함() {
Cities cities = new Cities(getStringArr("Jeju", "Pangyo"));
assertThat(true, is(cities.contains(new City("Jeju"))));
}

@Test
public void 도시_삭제() {
Cities cities = new Cities(getStringArr("Jeju"))
.addFirst(new City("aaa"))
.remove(new City("aaa"));

assertThat(false, is(cities.contains(new City("aaa"))));
}

@Test
public void 도시_앞쪽으로_추가() {
Cities cities = new Cities(getStringArr("Jeju"))
.addFirst(new City("aaa"));
cities.remove(cities.size()-1);

assertThat(true, is(cities.contains(new City("aaa"))));
}
package kakao.cache;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Cities {
private static final int MAX_LENGTH = 100001;
private List<City> cities;

public Cities() {
cities = new ArrayList<>();
}

public Cities(String... cities) {
if (cities == null) throw new IllegalArgumentException();
make(Arrays.asList(cities).stream()
.map(City::new)
.collect(Collectors.toList()));
}

public Cities(List<City> cities) {
if (cities == null) throw new IllegalArgumentException();
make(cities);
}

private void make(List<City> cities) {
if (cities.size() > MAX_LENGTH) throw new IllegalArgumentException();
this.cities = new ArrayList<>();
this.cities.addAll(cities);
}

public boolean contains(City city) {
return cities.stream().anyMatch(_city -> _city.equals(city));
}

public Cities addFirst(City city) {
cities.add(0, city);
return new Cities(cities);
}

public Cities remove(City city) {
cities.remove(city);
return new Cities(cities);
}

public Cities remove(int index) {
cities.remove(index);
return new Cities(cities);
}

public int size() {
return cities.size();
}

public Stream<City> stream() {
return cities.stream();
}
}


addFirst() : 캐시 교체 알고리즘은 LRU(Least Recently Used)를 충족하기 위해 사용했습니다.

가장 최근에 사용된 것이 front(0번 인덱스)에 위치하고, 최근이 아닐 수록 rear로 index가 밀리게 됩니다.


remove() : 체이닝이 되도록 다시 객체를 반환합니다.
[테스트 하기 편하기 위해서 체이닝 했을뿐, 반드시 객체를 반환할 필요는 없습니다.]


객체에 사용된 count를 넣는 방식도 있지만 불필요한 인스턴스 필드를 생성하여 관리 이슈가 생기는 것을 피하기 위해 index를 기준으로 하였습니다.


5. 캐시 크기 객체 만들기


입력 형식 1 : 캐시 크기(cacheSize)와 도시이름 배열(cities)을 입력받는다.
입력 형식 2 : cacheSize는 정수이며, 범위 0 ≦ cacheSize ≦ 30 이다.


다른 값이 들어올 수 없도록 객체로 만들면 관리 이슈가 줄어들게 됩니다.

@Test
public void cacheSize_0_일때() {
new CacheSize(0);
}
package kakao.cache;

public class CacheSize {
private static final int MAX_SIZE = 31;

private Integer size;

public CacheSize(Integer size) {
if (size < 0 || size > MAX_SIZE) throw new IllegalArgumentException();
this.size = size;
}

public int toInt() {
return size;
}

public boolean same(int number) {
return size == number;
}
}

해설에 보면 공개된 대부분의 LRU 구현 코드는 0일 때 비정상적인 작동을 한다는군요.


생성자에 넣어서 생성된다면, 입력 형식 2의 범위를 커버할 수 있습니다.



6.캐시 객체 생성


지금까지 만든 것들을 조합하면, 캐시 객체가 완성 됩니다.

@Test
public void LRU_cacheSize_0_작동() {
Cache first = new Cache(0)
.executeLRU(new City("first"));
assertThat(false, is(first.contains(new City("first"))));
}

@Test
public void LRU_cacheSize_작동() {
Cache hasSecond = new Cache(1)
.executeLRU(new City("first"))
.executeLRU(new City("second"));

assertThat(false, is(hasSecond.contains(new City("first"))));
assertThat(true, is(hasSecond.contains(new City("second"))));
}
package kakao.cache;

public class Cache {

private CacheSize cacheSize;
private Cities cities;

public Cache(CacheSize cacheSize, Cities cities) {
this.cacheSize = cacheSize;
this.cities = cities;
}

public Cache(Integer cacheSize) {
this.cacheSize = new CacheSize(cacheSize);
cities = new Cities();
}

public Cache executeLRU(City city) {
if (cacheSize.same(0)) return new Cache(cacheSize.toInt());

if (contains(city)) {
remove(city);
}

if (isFull()) {
remove(lastIndex());
}

return addFirst(city);
}

public boolean contains(City city) {
return cities.contains(city);
}

private boolean isFull() {
return cities.size() >= cacheSize.toInt();
}

private int lastIndex() {
return cities.size()-1;
}

private Cache addFirst(City city) {
return new Cache(cacheSize, cities.addFirst(city));
}

private Cache remove(City city) {
return new Cache(cacheSize, cities.remove(city));
}

private Cache remove(int index) {
return new Cache(cacheSize, cities.remove(index));
}
}


LRU는 가장 최근에 사용된 것이 캐시에 남아 있어야 함으로, 사이즈가 1일 경우 가장 최근에 사용한 것만 갖고 있도록 하였습니다.

executeLRU()
1. 캐시 사이즈가 0인 예외적인 상황이 아니라면 객체를 front에 추가 합니다.
2. 해당 값을 갖고 있다면, 추가 전 삭제합니다.
3. 캐시에 없는 값이 들어왔다면, rear(오랫동안 사용하지 않은 객체)를 삭제합니다.

lastIndex() : 가독성을 위해, 한 줄 짜리 매쏘드를 궃이 사용했습니다.
(-1과 같은 숫자가 들어오는걸 피하고 싶었습니다.)

 캐시는 완성이 되었습니다.



7. 실행 객체 생성

OOP인 만큼 실행 또한 객체로 대체합니다.

@Test
public void 결과_확인() {
assertThat(50, is(new CityCache(3)
.executeTime(
getStringArr("Jeju", "Pangyo", "Seoul", "NewYork", "LA", "Jeju", "Pangyo", "Seoul", "NewYork", "LA")
)));

assertThat(21, is(new CityCache(3)
.executeTime(
getStringArr("Jeju", "Pangyo", "Seoul", "Jeju", "Pangyo", "Seoul", "Jeju", "Pangyo", "Seoul")
)));

assertThat(60, is(new CityCache(2)
.executeTime(
getStringArr("Jeju", "Pangyo", "Seoul", "NewYork", "LA", "SanFrancisco", "Seoul", "Rome","Paris", "Jeju", "NewYork", "Rome")
)));

assertThat(52, is(new CityCache(5)
.executeTime(
getStringArr("Jeju", "Pangyo", "Seoul", "NewYork", "LA", "SanFrancisco", "Seoul", "Rome","Paris", "Jeju", "NewYork", "Rome")
)));

assertThat(16, is(new CityCache(2)
.executeTime(
getStringArr("Jeju", "Pangyo", "NewYork", "newyork")
)));

assertThat(25, is(new CityCache(0)
.executeTime(
getStringArr("Jeju", "Pangyo", "Seoul", "NewYork", "LA")
)));
}
package kakao.cache;

public class CityCache {

private Cache cache;
private ExecuteResult executeResult;

public CityCache(Integer cacheSize) {
cache = new Cache(cacheSize);
executeResult = new ExecuteResult();
}

public int executeTime(String... cities) {
new Cities(cities).stream().forEachOrdered(this::execute);
return executeResult.sum();
}

private void execute(City city) {
executeResult.increase(cache.contains(city));
cache.executeLRU(city);
}
}

executeTime() : forEachOrdered로 진행되어야만 LRU가 되겠죠?


execute() : this::excute로 표현하기위해 매쏘드로 뺐습니다.


주저리 주저리...
네이밍이 너무나 어렵네요...

Cache에 LRU알고리즘이 있어야하는데, 정작 사용하는 콜렉션은 Cities를 사용하고 있으니.. 

CitiCache가 적합할 것 같고... 여러가지 고민을 하던중 외부에서 사용할 때, Cache보단 CityCache가 하위처럼 보이기 때문에 CityCache로 하였습니다.


원래는 Cache가 Abstract class로 있고, CityCache와 CityCacheVO로 구현하면 확장하기 좋지만 일급 콜렉션에 대한 공부가 부족하다는 것을 느끼며 구현에 실패하게 되었습니다.


LRU도 객체로 빼야하나 고민도 하다가 알고리즘인 만큼 매쏘드로 하였습니다.


반응형
반응형

캐시(난이도: 하) 알고리즘 문제

상편



요구 사항

입력 형식

  1. 캐시 크기(cacheSize)와 도시이름 배열(cities)을 입력받는다.
  2. cacheSize는 정수이며, 범위는 0 ≦ cacheSize ≦ 30 이다.
  3. cities는 도시 이름으로 이뤄진 문자열 배열로, 최대 도시 수는 100,000개이다.
  4. 각 도시 이름은 공백, 숫자, 특수문자 등이 없는 영문자로 구성되며, 대소문자 구분을 하지 않는다. 도시 이름은 최대 20자로 이루어져 있다.


출력 형식
  1. 입력된 도시이름 배열을 순서대로 처리할 때, “총 실행시간”을 출력한다.


조건

  1. 캐시 교체 알고리즘은 LRU(Least Recently Used)를 사용한다.
  2. cache hit일 경우 실행시간은 1이다.
  3. cache miss일 경우 실행시간은 5이다.





1. 실행 시간의 시간 객체 생성

표현으로는 시간이지만, 테스트에서 제시한 시간은 초,분,시 가 아닌 단순한 숫자입니다.

조건 2 : cache hit일 경우 실행시간은 1이다.
조건 3 : cache miss일 경우 실행시간은 5이다.



1.1. Number interface 생성.


공통적으로 실행시간의 증가와 결과 값을 가져오는 기능이 필요합니다.


package kakao.cache;

public interface Number {
public <T> T increase();
public int toInt();
}


1.2. cache hit 객체 생성

package kakao.cache;

public class Hit implements Number {

private Integer count;

public Hit(Integer count) {
this.count = count;
}

@Override
public Hit increase() {
return new Hit(++count);
}

@Override
public int toInt() {
return count;
}
}

숫자는 1씩 증가하게 됩니다.

1.2. cache miss 객체 생성

package kakao.cache;

public class Miss implements Number {

private Integer count;

public Miss(Integer count) {
this.count = count;
}

@Override
public Miss increase() {
return new Miss(count + 5);
}

@Override
public int toInt() {
return count;
}
}

숫자는 5씩 증가하게 됩니다.


2. 출력 객체 생성 (방금 만든 객체를 조합합니다.)

출력 형식 : 입력된 도시이름 배열을 순서대로 처리할 때, “총 실행시간”을 출력한다.

@Test
public void 응답시간_포함_1() {
boolean cacheHit = true;
assertThat(1, is(new ExecuteResult().increase(cacheHit).sum()));
}

@Test
public void 응답시간_미포함_5() {
boolean cacheMiss = false;
assertThat(5, is(new ExecuteResult().increase(cacheMiss).sum()));
}
package kakao.cache;

public class ExecuteResult {

private Hit hit;
private Miss miss;

public ExecuteResult() {
this.hit = new Hit(0);
this.miss = new Miss(0);
}

public ExecuteResult(Hit hit, Miss miss) {
this.hit = hit;
this.miss = miss;
}

public ExecuteResult increase(boolean isHit) {
if (isHit) {
hit = hit.increase();
} else {
miss = miss.increase();
}
return new ExecuteResult(hit, miss);
}

public int sum() {
return hit.toInt() + miss.toInt();
}
}

sum() : 2개 이상의 값의 더하기 연산을 암시합니다. [유지보수할 사람을 위해...]



3. 도시 객체 만들기 ("도시이름"을 도시라는 객체로 관리합니다.)

입력 형식 4 : 각 도시 이름은 공백, 숫자, 특수문자 등이 없는 영문자로 구성되며, 대소문자 구분을 하지 않는다. 도시 이름은 최대 20자로 이루어져 있다.

@Test (expected = IllegalArgumentException.class)
public void 도시명_글자만() {
new City(" s d d ");
}

@Test
public void 도시_만들기() {
new City("seoul");
}

@Test
public void 도시_이름_비교() {
assertThat(true, is(new City("seoul").equals(new City("Seoul"))));
}
package kakao.cache;

public class City {
private static final int MAX_LENGTH = 21;

private String city;

public City(String city) {
if (city == null || city.isEmpty() || city.length() > MAX_LENGTH) throw new IllegalArgumentException();
if (!city.matches("^[a-zA-Z]*$")) throw new IllegalArgumentException();

this.city = city.trim().toLowerCase();
}

@Override
public String toString() {
return city;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
return this.city.toString().equals(o.toString());
}
}

"공백, 숫자, 특수문자 등이 없는 영문자"의 조건을 충족하기 위해
생성자에 있는 "^[a-zA-Z]*$"가 제일 중요하죠.

그리고 "대소문자 구분을 하지 않는다"는 조건을 충족하기 위해 city를 만들고 toLowerCase()를 사용합니다.

equals() : 객체를 비교하는 것이 아닌 객체의 String값을 비교합니다. (list에서 remove시 필수)

하편에서 이어집니다...

반응형
반응형

다트 게임(난이도: 하) 알고리즘 문제


하편


 4. 옵션 객체 생성


스타상과 아차상이 존재하죠!? 

알고리즘 규칙 4. 옵션으로 스타상(*) , 아차상(#)이 존재하며 스타상(*) 당첨 시 해당 점수와 바로 전에 얻은 점수를 각 2배로 만든다. 아차상(#) 당첨 시 해당 점수는 마이너스된다.

    @Test
    public void 옵션_계산() {
        assertThat(4l, is(Option.STAR.calculate(2)));
        assertThat(-2l, is(Option.ACHA.calculate(2)));

        assertThat(4l, is(new Options(Option.STAR).calculate(2)));
        assertThat(-4l, is(new Options(Option.STAR).add(Option.ACHA).calculate(2)));
        assertThat(-4l, is(new Options(Option.ACHA).add(Option.STAR).calculate(2)));
    }
package kakao.dart;

import java.util.Arrays;
import java.util.function.Function;

public enum Option {
    STAR("*", value -> value * 2),
    ACHA("#", value -> -value);

    private String shortName;
    private Function<Long, Long> expression;

    Option(String shortName, Function<Long, Long> expression) {
        this.shortName = shortName;
        this.expression = expression;
    }

    public long calculate(long value) {
        return expression.apply(value);
    }

    public static boolean hasOption(String text) {
        return Arrays.asList(Option.values()).stream()
                .map(option -> option.shortName)
                .anyMatch(shortName -> text.indexOf(shortName) > -1);
    }

    public static Option getByShortName(String shortName) {
        return Arrays.asList(Option.values()).stream()
                .filter(option -> option.shortName.equals(shortName))
                .findFirst()
                .orElseThrow(IllegalArgumentException::new);
    }
}

Bouns와 같은 방식으로 구현하였습니다.

calculate() : 가 핵심로직으로 되죠. 상편을 참고하세요~

그런데... 옵션 객체는 좀 특수한 룰을 갖고 있네요.


5. 옵션 일급 콜렉션 생성

알고리즘 규칙 6. 스타상(*)의 효과는 아차상(#)의 효과와 중첩될 수 있다. 이 경우 중첩된 아차상(#)의 점수는 -2배가 된다. (예제 5번 참고)

옵션이 2개 존재 할 수 있게 되네요.

package kakao.dart;

import java.util.ArrayList;
import java.util.List;

public class Options {
    private List<Option> options;

    public Options(List<Option> options) {
        if (options == null) throw new IllegalArgumentException();
        this.options = options;
    }

    public Options(Option option) {
        options = new ArrayList<>();
        options.add(option);
    }

    public Options add(Option option) {
        options.add(option);
        return new Options(options);
    }

    public Option unique() {
        if (options.size() != 1) throw new IllegalStateException();
        return options.get(0);
    }

    public long calculate(long value) {
        for (int i = 0; i < options.size(); i++) {
            value = options.get(i).calculate(value);
        }
        return value;
    }
}

다른 멤버 변수가 없는 일급 콜렉션으로 객체를 생성하였습니다.

일급 콜렉션을 사용함으로, 불필요한 ArrayList의 상속이나, 상태값 변경에 대한 리스크를 줄일기 편합니다.

add() : 해당 로직에서 자기 자신을 반환하는 것에 주의해주세요.
getter, setter 를 사용하지 않고 객체다운 운용이 가능해 집니다.


unique() : 2개가 존재하는 값은 이전 에 기록된 적이 있다는 것을 의미합니다.
의도적으로 잘못된 사용을 막기위한 매쏘드 입니다. [코드가 불안하네요 ㅠㅠ]


이제 어느정도 모양을 잡아가고 있네요!!


6. 다트 포인트 객체 생성 (조합)


알고리즘 규책 9. 스타상(*), 아차상(#)은 점수마다 둘 중 하나만 존재할 수 있으며, 존재하지 않을 수도 있다.
이전에 만든 2개의 객체를 1개의 객체로 만듭니다.

    @Test
    public void 던지기() {
        assertThat(2L ,is(new Point("2S").calculate()));
        assertThat(-2L ,is(new Point("2S#").calculate()));
        assertThat(8L ,is(new Point("2D*").calculate()));

        assertThat(4L,is(new Point(new BasicPoint("2S"), new Options(Option.STAR)).calculate()));
        assertThat(-2L,is(new Point(new BasicPoint("2S"), new Options(Option.ACHA)).calculate()));
        assertThat(-4L,is(new Point(new BasicPoint("2S"), new Options(Option.STAR).add(Option.ACHA)).calculate()));
    }
package kakao.dart;

import java.util.ArrayList;

public class Point {

    private BasicPoint basicPoint;
    private Options options;

    public Point(String text) {
        if (Option.hasOption(text)) {
            String option = text.substring(text.length()-1);
            options = new Options(Option.getByShortName(option));

            text = text.substring(0, text.indexOf(option));
        }
        basicPoint = new BasicPoint(text);
    }

    public Point(BasicPoint basicPoint, Options options) {
        this.basicPoint = basicPoint;
        this.options = options;
    }

    public Point addOption(Point point) {
        if (options == null) {
            options = new Options(new ArrayList<>());
        }
        return new Point(basicPoint, options.add(point.options.unique()));
    }

    public boolean has(Option option) {
        return options != null && option == options.unique();
    }

    public long calculate() {
        if (options == null) return basicPoint.calculate();
        return options.calculate(basicPoint.calculate());
    }
}

addOption() : 스타상(*)을 위해서 존재하는 매쏘드 입니다.
이전의 값에도 옵션을 주기 위한 매쏘드입니다.


has() : getter를 사용해 값을 비교하는 것이 아닌, 객체에게 물어보는 행위를 하게 됩니다.


7. 점수에 옵션 적용하기.


알고리즘 규칙 4. 옵션으로 스타상(*) , 아차상(#)이 존재하며 스타상(*) 당첨 시 해당 점수와 바로 전에 얻은 점수를 각 2배로 만든다. 아차상(#) 당첨 시 해당 점수는 마이너스된다.

점수를 여러개 가지고 있어야 하기 때문에 컬렉션의 형태가 관리하기 편하며, 갯수도 임의로 변경 가능하다.

    @Test
    public void 점수() {
        assertThat( 37l, is(new Score("1S2D*3T").calculate()));
        assertThat( 9l, is(new Score("1D2S#10S").calculate()));
        assertThat( 3l, is(new Score("1D2S0T").calculate()));
        assertThat( 23l, is(new Score("1S*2T*3S").calculate()));
        assertThat( 5l, is(new Score("1D#2S*3S").calculate()));
        assertThat( -4l, is(new Score("1T2D3D#").calculate()));
        assertThat( 59l, is(new Score("1D2S3T*").calculate()));
    }

package kakao.dart;

import java.util.ArrayList;
import java.util.List;

public class Score {
    List score;

    public Score(String text) {
        if (text == null || text.isEmpty()) throw new IllegalArgumentException();
        score = new ArrayList<>();
        fillScore(text);
    }

    private void fillScore(String text) {
        String[] tokens = text.split("[^\\d]{1,2}");

        for (int index = 1; index < tokens.length; index++) {
            String point = text.substring(0, text.indexOf(tokens[index]));
            add(new Point(point));
            text = text.substring(point.length());
        }

        add(new Point(text));
    }

    private void add(Point point) {
        if (!score.isEmpty() && point.has(Option.STAR)) {
            int prevIndex = score.size() -1;
            Point previous = score.get(prevIndex);

            score.set(prevIndex, previous.addOption(point));
        }
        score.add(point);
    }

    public long calculate() {
        return score.stream().mapToLong(Point::calculate).sum();
    }
}

fillScore() : 드디어 핵심 로직인 토큰이 나오게 되네요. BasicPoint와 같은 match를 사용하죠.
해당 토큰별로 구분하여, 각각의 점수로 스코어를 만들게 됩니다.

fill로 하지 않음으로, 기존의 같은 값을 넣는 매쏘드가 아님을 밝힙니다.
while()문을 매쏘드로 빼려고 했지만, 문맥상 읽기 힘들어져서 그냥 두었습니다.


add() : 첫번째를 제외한 스타상(*)은 바로 전 값에 영향을 주기위해,
score.size가 0인 상태에선 진행할 수 없습니다.

생성자에서 새로운 값을 set하기 때문에, 안전하게 사용할 수 있습니다.

최대한 객체를 객체답게 사용하기위해

규칙 1: 한 메서드에 오직 한 단계의 들여쓰기만 한다.
규칙 2: else 예약어를 쓰지 않는다.
규칙 3: 모든 원시값과 문자열을 포장한다.
규칙 6: 모든 엔티티를 작게 유지한다.
규칙 7: 2개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
규칙 8: 일급 콜렉션을 쓴다.
규칙 9: 게터/세터/프로퍼티를 쓰지 않는다.

출처 : 효과적으로 TDD, 리팩토링, OOP를 연습하는 방법


kakao_dart.zip


반응형

'프로그래밍 > 알고리즘' 카테고리의 다른 글

캐시(난이도: 하) [하편]  (0) 2018.02.23
캐시(난이도: 하) [상편]  (0) 2018.02.22
다트 게임(난이도: 하) [상편]  (0) 2018.02.14
비밀 지도(난이도: 하) JAVA  (0) 2018.02.14
반응형

카카오 신입 공채 1차 코딩 테스트에 있는 알고리즘 문제를 풀어봅니다.

다트 게임(난이도: 하) [상편 - 필수 문자열에 해당하는 객체 생성]


상편


“점수|보너스|[옵션]”으로 이루어진 문자열 3세트. 이므로,
"1S","1D","1T"와 같은 "점수|보너스"는 반드시 존재합니다.

점수객체, 보너스객체, 점수객체+보너스객체(Object Compoistion[조합 객체])를 만들어 역할을 담당하게 합니다.

1. 다트 포인트 객체 생성 ("2S","2D","2T") - 앞쪽 숫자

알고리즘 규칙 2. 각 기회마다 얻을 수 있는 점수는 0점에서 10점까지이다.

먼저 테스트 코드를 만들어봅니다. (10점이지만 20점 까지로 해보았습니다.)

package kakao.dart;

import org.junit.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;

public class BasicPointTest {

    @Test
    public void 포인트_제한_0에서_20() {
        for (int i = 0; i < 20; i++) {
            new PointNumber(String.valueOf(i));
        }
    }

    @Test (expected = IllegalArgumentException.class)
    public void 포인트_최대값_초과_21() {
        new PointNumber("21");
    }
}

해당 조건에 부합하는 객체를 만듭니다.

package kakao.dart;

public class PointNumber {

    private static final Long MIN_NUMBER = 0l;
    private static final Long MAX_NUMBER = 20l;

    private Long number;

    public PointNumber(String numeric) {
        if (numeric == null) throw new IllegalArgumentException();
        Long number = Long.valueOf(numeric);
        if (outOfNumber(number)) throw new IllegalArgumentException();

        this.number = number;
    }

    private boolean outOfNumber(Long number) {
        return number < MIN_NUMBER || MAX_NUMBER < number;
    }

    public long toLong() {
        return number;
    }
}

기본 점수 객체를 만들어 놓으면, 이후에 객체 사용시 validation check(null, invalid value)에 대한 부담을 줄일 수 있습니다.

다트 게임은 0~20까지의 점수를 기준으로 했습니다. (bull (Single bull, Double bull)은 제외했습니다.)

outOfNumber() : 생성시 핵심 로직으로 작동합니다.

2. 다트 보너스 객체 생성 ("2S","2D","2T") - 뒷쪽 문자

알고리즘 규칙 3. 점수와 함께 Single(S), Double(D), Triple(T) 영역이 존재하고 각 영역 당첨 시 점수에서 1제곱, 2제곱, 3제곱 (점수^1 , 점수^2 , 점수^3 )으로 계산된다.

@Test
    public void 보너스_계산() {
        assertThat(2l, is(Bonus.SINGLE.calculate(2)));
        assertThat(4l, is(Bonus.DOUBLE.calculate(2)));
        assertThat(8l, is(Bonus.TRIPLE.calculate(2)));
    }

계산을 했을경우 왼쪽의 값과 같도록 테스트코드를 만들었습니다.

package kakao.dart;

import java.util.Arrays;
import java.util.function.Function;

public enum Bonus {
    SINGLE("S", value -> value),
    DOUBLE("D", value -> (long) Math.pow(value, 2)),
    TRIPLE("T", value -> (long) Math.pow(value, 3));

    private String shortName;
    private Function<Long, Long> expression;

    Bonus(String shortName, Function<Long, Long> expression) {
        this.shortName = shortName;
        this.expression = expression;
    }

    public long calculate(long value) {return expression.apply(value);}

    public static Bonus getByShortName(String shortName) {
        return Arrays.asList(Bonus.values()).stream()
                .filter(bonus -> bonus.shortName.equals(shortName))
                .findFirst()
                .orElseThrow(IllegalArgumentException::new);
    }
}

java8에서 개발했기 때문에 Function<Long, Long>이 가능케 되는데요.
자세한 내용은 우아한 형제의 이동욱님께서 작성한 Java Enum 활용기를 참고하세요~

java 7에서 하는 방법도 나와있답니다 :)

calculate() : 핵심로직으로 제곱 연산을 담당합니다.

3. 조합 객체 만들기

점수|보너스|[옵션]”으로 이루어진 문자열

3세트.알고리즘 규칙 8. Single(S), Double(D), Triple(T)은 점수마다 하나씩 존재한다.
다트 포인트와 보너스는 항상 붙어 다니므로, 1개의 객체로 취급합니다.

    @Test (expected = IllegalArgumentException.class)
    public void 포인트_보너스_부재() {
        new BasicPoint("1");
    }

    @Test
    public void 포인트_계산() {
        assertThat(2L, is(new BasicPoint("2S").calculate()));
        assertThat(4L, is(new BasicPoint("2D").calculate()));
        assertThat(8L, is(new BasicPoint("2T").calculate()));
    }
package kakao.dart;

import java.util.Optional;

public class BasicPoint {

    private static final String POINT_MATCH = "[^\\d]{1,2}";

    private PointNumber pointNumber;
    private Bonus bonus;

    public BasicPoint(String text) {
        if (text == null || text.isEmpty()) throw new IllegalStateException();
        pointNumber = matchPoint(text);
        bonus = matchArea(text);
    }

    private PointNumber matchPoint(String text) {
        return new PointNumber(Optional.of(text.split(POINT_MATCH)).orElseThrow(IllegalArgumentException::new)[0]);
    }

    private Bonus matchArea(String text) {
        return Bonus.getByShortName(text.substring(text.length()-1));
    }

    public Long calculate() {
        return bonus.calculate(pointNumber.toLong());
    }
}

 optional.of()는 단순히 if문을 줄이기 위해 사용하였습니다.

matchPoint, matchArea 매쏘드 에서 의도한 값이 들어오지 않으면 IllegalStateException을 던지도록 하였습니다. 

이렇게 해서 다트 게임의 필수 값을 구현했습니다.

객체를 객체답게 사용하기위해

규칙 1: 한 메서드에 오직 한 단계의 들여쓰기만 한다.
규칙 2: else 예약어를 쓰지 않는다.
규칙 3: 모든 원시값과 문자열을 포장한다.
규칙 6: 모든 엔티티를 작게 유지한다.
규칙 7: 2개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
규칙 8: 일급 콜렉션을 쓴다.
규칙 9: 게터/세터/프로퍼티를 쓰지 않는다.

출처 : 효과적으로 TDD, 리팩토링, OOP를 연습하는 방법


반응형

'프로그래밍 > 알고리즘' 카테고리의 다른 글

캐시(난이도: 하) [하편]  (0) 2018.02.23
캐시(난이도: 하) [상편]  (0) 2018.02.22
다트 게임(난이도: 하) [하편]  (0) 2018.02.14
비밀 지도(난이도: 하) JAVA  (0) 2018.02.14
반응형

카카오 신입 공채 알고리즘 풀이 with JAVA


카카오 신입 공채 1차 코딩 테스트에 있는 알고리즘 문제를 풀어봅니다.


비밀 지도(난이도: 하) 알고리즘 문제


1. 2진수 변환 객채를 만듭니다.

package kakao.secret_map;

public class BinaryString {

    private String binaryValue;

    private final static String BLINK_STRING = " ";
    private final static String WALL_STRING = "#";

    public BinaryString(int decimal) {
        this.binaryValue = Integer.toBinaryString(decimal)
                .replaceAll("0", BLINK_STRING)
                .replaceAll("1", WALL_STRING);
    }

    @Override
    public String toString() {
        return binaryValue;
    }
}

1. 10진수를 2진로 변환.
2. 2진수를 원하는 글자로 변환.

객체 지향인 만큼 최대한 객체에게 일을 위임합니다.


2. 비밀 지도의 숫자가 들어있는 Number 객체를 만듭니다.

package kakao.secret_map;

public class Number {

    private Integer decimal;

    public Number(Integer decimal) {
        if (decimal == null) throw new IllegalArgumentException();
        this.decimal = decimal;
    }

    public BinaryString toBinary() {
        return new BinaryString(decimal);
    }

    public Number orWise(Number number) {
        return new Number(decimal | number.decimal);
    }

    @Override
    public String toString() {
        return toBinary().toString();
    }
}

Integer의 toBinaryString() 메쏘드와 비트 연산자인 |을 활용하는 문제였네요.

1. 2진수 변환
2. 비트연산

두 가지만 알면 되니 너무 쉽죠!?
하지만 저런 기능을 모르셨던 분들은 for문 돌리고, reverseOrder하고, 문자 비교하는등 
여러가지 복잡한 연산을 하셨던 분들도 계셨을 거에요!

실은 저도 그렇게 가려다가 ㅠㅠ Integer에 이런기능이 없을까 하고 클래스를 찾아보니 있더군요!

역시 사람은 조오금은 게으름을 피우는게... ㅎㅎㅎ

여기서 제일 핵심 비지니스 로직은 orWise() 입니다.
상태값이 변경된 객체가 아닌 새로운 객체를 반환하는것이죠.

꼭!!! 다른값이 아닌 새로운 객체로 반환한 이유를 고민해보세요~


3. 이제 여러개의 객체를 담을 수 있는 일급 클래스를 만듭니다.

package kakao.secret_map;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class Numbers {
    private List<Number> numbers;

    public Numbers(Integer... numberArr) {
        numbers = Arrays.asList(Optional.of(numberArr).orElseThrow(IllegalArgumentException::new))
                .stream()
                .map(Number::new)
                .collect(Collectors.toList());
    }

    public Numbers(List<Number> numbers) {
        this.numbers = numbers;
    }

    public Numbers orWise(Numbers numbers) {
        if (this.numbers.size() != numbers.numbers.size()) throw new IllegalArgumentException();
        List<Number> _temp = new ArrayList();
        
        for (int i = 0; i < this.numbers.size(); i++) {
            _temp.add(this.numbers.get(i).orWise(numbers.numbers.get(i)));
        }
        return new Numbers(_temp);
    }

    @Override
    public String toString() {
        return numbers.toString();
    }
}

핵심 비지니스 로직이였던 orWise를 다시 사용하는 것이 전부 입니다.


실제로 작동하는지 확인해 볼까요?

package kakao.secret_map;

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class NumberTest {

    @Test
    public void 진수_변환() {

        assertEquals("#  #", new BinaryString(9).toString());

        assertEquals("#### ", new Number(30).toString());

        assertEquals("#####", new Number(30).orWise(new Number(9)).toString());
    }

    @Test
    public void 여러개의_진수_변환() {
        Numbers numbers = new Numbers(10, 20, 30, 40, 50);
        System.out.println(numbers);

        System.out.println(new Numbers(9, 20, 28, 18, 11).orWise(new Numbers(30, 1, 21, 17, 28)));
    }
}

테스트 코드로 작성하였습니다.


객체를 객체답게 사용하기위해


규칙 2: else 예약어를 쓰지 않는다.
규칙 3: 모든 원시값과 문자열을 포장한다.
규칙 7: 2개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
규칙 8: 일급 콜렉션을 쓴다.
규칙 9: 게터/세터/프로퍼티를 쓰지 않는다.

출처 : 효과적으로 TDD, 리팩토링, OOP를 연습하는 방법


kakao_secret_map.zip


반응형

'프로그래밍 > 알고리즘' 카테고리의 다른 글

캐시(난이도: 하) [하편]  (0) 2018.02.23
캐시(난이도: 하) [상편]  (0) 2018.02.22
다트 게임(난이도: 하) [하편]  (0) 2018.02.14
다트 게임(난이도: 하) [상편]  (0) 2018.02.14

+ Recent posts