# 개요

학원 다니면서 개인 프로젝트로 플래너 개발을 해보기로 했다.

처음에는 타이머로 시작했다가 내가 생각했던 방향이 아닌거 같아서 플래너로 바꿨다.

처음에 타이머로 생각한 이유는 단순히 뽀모도로 타이머(https://chromewebstore.google.com/detail/focus-to-do-%EB%BD%80%EB%AA%A8%EB%8F%84%EB%A1%9C-%ED%83%80%EC%9D%B4%EB%A8%B8-+-%EC%97%85%EB%AC%B4/ngceodoilcgpmkijopinlkmohnfifjfb?pli=1)를 개발하려고 했기 때문인데.. 코드 작성하다보니 타이머가 돌아가는 로직은 프론트에서 간단하게 처리 할 수 있다고 생각했기 때문이다.

원래는 프로젝트 개발진행하면서 블로그도 같이 쓰려고했는데, 개발 중간에서야 작성하게 됐다 ㅎㅎ;

데이터베이스(Mysql)을 추가하고 프로젝트와 연결하는 과정

 


 

# EC2에 데이터 베이스(Mysql) 설치

데이터베이스는 학원에서 오라클을 배웠지만, Mysql을 사용하기로 했다. Mysql을 선택한 이유는

  • 내 로컬(노트북)에는 데이터베이스 프로그램이 설치되어 있지 않은데 굳이 설치하고 싶지 않았다. 그렇다면 어떤 원격의 데이터베이스를 이용해야 한다.
    1. 학원 컴퓨터를 항상 켜 놓은 상태로 두고 원격 데스크톱 연결을 통해 노트북에서 학원 컴퓨터로 접속해서 개발을 진행 → 학원 공유기를 포트 포워딩 해야하는데 공유기 비밀번호를 모르기 때문에... 패스
    2. 내장 데이터베이스인 H2를 사용 → 기왕 사용하는거 데이터베이스 설치해서 사용하자라는 생각으로.. 패스
    3. 옛날에 AWS를 사용해봤으니까 복습한다는 생각으로 EC2 인스턴스에 데이터베이스를 설치하여 사용하는 것으로 결정!
  • 오라클을 설치하는 과정이 꽤나 복잡해서 Mysql을 선택했다. (근데 Mysql도 생각보다 복잡하더라..)
    • RDS라는 서비스도 있긴 했는데 사용해본적이 없어서 우선 EC2에 설치해서 사용해보다가 RDS도 사용 해보기로..

EC2에 Mysql 설치하는 과정

 

[AWS] EC2에 DB설치

# 개요 생성한 EC2에 Database(Mysql)을 설치해보자 학원에서는 Oracle을 사용하긴 했는데, 찾아보니 Linux에 Oracle설치하는게 꽤 복잡해서 그냥 Mysql로 진행하기로 했다. 근데 AWS RDS라는 데이터베이스 서

jino-dev-diary.tistory.com

 


 

# 내 프로젝트와 연결, CRUD 기능 구현

Mysql jdbc인 Mysql Connector를 통해 EC2에 설치된 Mysql과 연결했다.

그 과정에서 기존에 Controller의 List에 저장했던 Plan 객체들을 Database에 저장하도록 수정했다.

CRUD 기능에 대한 메서드를 구현했는데 Connection을 생성하는 부분이 중복되어서 JdbcTemplate에 데이터베이스와 연결하는 코드를 분리하여 작성하였다.

Connection 이후 PreparedStatement를 이용해서 Query에 객체 필드들을 매핑해주고 executeUpdate()(CUD) / executeQuery() (R)을 통해 쿼리를 실행했다.

이후 필요에 따라 데이터베이스 row를 자바 Object로 매핑 후 반환했다.

 

Add Feature: connect to Mysql Database · JinHoooooou/MiniTimer@8f53d07

JinHoooooou committed Feb 7, 2024

github.com

 


 

# 간단 회고

저 커밋 하나에 내용이 너무 많아서 여전히 읽기 힘들다 ㅜㅜ 커밋 작게하는 습관을 들여야하는데 생각보다 쉽지않다

database 연결할 때 접속 계정과 패스워드가 필요한데, Github에 올릴 때는 그걸 빼고 올려야 하는데 같이 올려버렸다... 이번 경우에는 EC2 인스턴스를 중지 후 재시작 함으로써 hostname이 변경 되었기 때문에 크게 상관 없겠지만 이런 정보는 별도의 파일로 만들어서 .gitignore에 해당 파일을 추가하거나 환경 변수로 구성하는 방법으로 보완해야겠다는 생각이 들었다.

# 개요

항상 Gradle을 쓸 때 IDE를 이용해서 빌드 및 실행을 했었는데 CMD로는 어떻게 할까?

그리고 IDE에서는 실행 할 때 알아서 빌드까지 진행해 주었는데 구분 동작은 어떻게 되는지 알아보자.

Gradle에 대한 세세한 정보는 여기서 다루지 않는다. 단순히 Gradle로 빌드하고 실행하는 것만 다룬다.

 


# Gradle Project Structure

  • settings.gradle: 프로젝트의 구성을 정의하는 설정 파일
  • build.gradle: 프로젝트 빌드 스크립트가 포함된 파일, Groovy나 Kotlin DSL을 사용
  • src/main: 메인 소스 코드 및 리소스가 있는 디렉토리
  • src/test: 테스트 소스 코드 및 리소스가 있는 디렉토리
  • build/: 필드 프로세스에서 생성되는 출력파일이 있는 디렉토리. 컴파일 된 class파일 빌드된 jar파일

  


# Gradle Wrapper

원래는 Gradle 빌드를 하려면 Gradle 설치를 따로 해야한다고 한다. 근데 옛날에 스프링부트로 개발할 때는 설치 없이도 잘 됐는데??

알고보니 Gradle Wrapper라는 것이 있다고 한다.

Gradle Wrapper는 미리 지정된 버전의 Gradle을 호출하도록 하는 스크립트다. Gradle 빌드를 사용하기 위해서는 알맞은 버전이 설치되어 있는데 매번 버전 업데이트를 하는 것이 비효율적이기 때문에 Gradle에서 프로젝트 생성시 내장 Gradle를 넣어주도록 했다.

Gradle 프로젝트 생성하면 기본적으로 생성되어 있는 gradlew이나 gradlew.bat 파일을 이용한다.

  • gradle-wrapper.properties: gradle wrapper 설정 파일. gradle 버전 및 다운로드 링크 기록
  • gradle-wrapper.jar: gradle wrapper 실행에 필요한 jar파일. gradle 래퍼 스크립트인 gradlew파일이 gradle을 다운로드하고 실행하는데 사용
  • gradlew/gradlew.bat: Unix / Windows 시스템에서의 Gradle Wrapper 스크립트. 프로젝트를 빌드하고 관리하는데 필요한 Gradle 실행 파일을 다운로드하고 실행

 


# Gradle 명령어를 이용하여 빌드

cmd로 빌드할 때는 gradlew / gardlew.bat 파일이 있는 root directory로 이동

gradle clean

IDE(IntelliJ)

CMD

 


gradle build

IDE(IntelliJ)

CMD

 


# .jar파일 실행

CMD에서 java -jar {projectName}.jar 파일을 실행하기 위해서는 build.gradle 파일에 다음과 같이 작성해야한다.

jar {
    manifest {
        attributes 'Main-Class': 'com.kh.Main'
    }
    from {
        configurations.runtimeClasspath.collect {
            it.isDirectory() ? it : zipTree(it)
        }
    }
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
  • manifest 부분: JAR파일 내의 manifest 파일에 대한 속성을 지정, 버전, 주요 클래스, 클래스 경로 등의 정보가 포함됨
  • from 부분: Dependency가 필요할 때 추가함
  • DuplicateStrategey 부분: 중복 파일이 발견되면 하나만 JAR에 포함하고 나머지는 제외 

gradle 빌드 이후에 다음 명령어를 통해 Jar파일을 실행한다.

java -jar {jar파일_경로}

 


# Reference

Gradle 빌드 관련

https://jammdev.tistory.com/210

 

[Gradle] Gradle 사용법 - 설치, 초기화 및

지난 글에서는 Gradle 이 무엇인지 알아보았다. 이번에는 Gradle 을 설치하고 사용해보는 튜토리얼을 진행해보려 한다. 해당 내용은 Gradle 의 Getting Started 내용을 읽고 작성하였다. 1. Gradle 설치 & Gradl

jammdev.tistory.com

Chat GPT

https://chat.openai.com/share/0a93ceee-548e-4da2-81cd-93a2b612486e

 

Jar 배포 관련

https://www.inflearn.com/questions/923059/%EC%95%B1-%EB%B0%B0%ED%8F%AC%EC%8B%9C-jar-%EC%A7%88%EB%AC%B8

 

앱 배포시 jar 질문 - 인프런

앱을 배포하는 도중 계속 에러가 나 질문 올립니다.plugins { id 'java' id 'org.springframework.boot' version '3.0.4' id 'io.spring.dependency-management' version '1.1.0'...

www.inflearn.com

https://ink1234.tistory.com/12

 

[Gradle] dependency 가 포함된 jar 만들기

프로젝트에 Dependency를 추가 할때 Gradle의 dependencies를 사용해서 외부에서 라이브러리들 다운로드해서 임포트를 하는 경우가 있습니다. 12345678910dependencies { compile group: 'org.apache.httpcomponents', name: 'h

ink1234.tistory.com

https://stackoverflow.com/questions/21721119/creating-runnable-jar-with-gradle

 

Creating runnable JAR with Gradle

Until now I created runnable JAR files via the Eclipse "Export..." functionallity but now I switched to IntelliJ IDEA and Gradle for build automation. Some articles here suggest the "application" ...

stackoverflow.com

 

# 개요

학원 다니면서 개인 프로젝트로 플래너 개발을 해보기로 했다.

처음에는 타이머로 시작했다가 내가 생각했던 방향이 아닌거 같아서 플래너로 바꿨다.

처음에 타이머로 생각한 이유는 단순히 뽀모도로 타이머(https://chromewebstore.google.com/detail/focus-to-do-%EB%BD%80%EB%AA%A8%EB%8F%84%EB%A1%9C-%ED%83%80%EC%9D%B4%EB%A8%B8-+-%EC%97%85%EB%AC%B4/ngceodoilcgpmkijopinlkmohnfifjfb?pli=1)를 개발하려고 했기 때문인데.. 코드 작성하다보니 타이머가 돌아가는 로직은 프론트에서 간단하게 처리 할 수 있다고 생각했기 때문이다.

원래는 프로젝트 개발진행하면서 블로그도 같이 쓰려고했는데, 개발 중간에서야 작성하게 됐다 ㅎㅎ;

 


# Gradle 빌드 툴 사용

학원에서 현재까지 진행된 프로젝트를 각자 발표한다고 했다. 발표하는 것은 문제가 아닌데, 개발 환경이 달라서 조금 문제가 됐다. 내 로컬 개발 IDE는 IntelliJ (Community)인데, 학원에서 진행하는 IDE는 Eclipse를 사용했다. 내 기억으로는 IntelliJ에서 생성한 프로젝트랑 Eclipse랑 호환이 안돼서 추가적인 세팅이 필요한 것으로 알고있다.

그래서 구글링하면서 세팅을 만져봤는데.. 내가 잘 못 한건지 계속 안되더라 ㅜㅜ 그래서 그냥 IntelliJ에서 작성한 코드들 다 복사해서 Eclipse에 새로 프로젝트 생성해서 붙여넣기 할까 생각했었는데, Git 설정도 문제가 될 것 같아서 그 방법은 관뒀다. Eclipse와 내 Remote Git을 연결해서 clone할까 생각도 했는데 그것도 내가 잘 못해서 그러는지 계속 안되더라 ㅜㅜ

그래서 "빌드 툴인 Gradle을 사용해서 jar파일로 만들고 발표할 로컬에서 Gradle을 이용해서 실행하면 되지 않을까?" 라는 생각으로 Gradle을 사용해보았다.

간단히 build.gradle파일과 settings.gradle파일 추가하면 IntelliJ가 자동으로 인식해서 Gradle Project Load할거냐고 물어본다. (역시 갓텔리제이... 똥클립스..) 

https://github.com/JinHoooooou/MiniTimer/commit/855a1daa1fa7fb098133687ff4c05e62f0008e45

 

Gradle 빌드 툴 사용관련 글

 

[Gradle] Gradle 빌드, 실행하기

# 개요 항상 Gradle을 쓸 때 IDE를 이용해서 빌드 및 실행을 했었는데 CMD로는 어떻게 할까? 그리고 IDE에서는 실행 할 때 알아서 빌드까지 진행해 주었는데 구분 동작은 어떻게 되는지 알아보자. Gradl

jino-dev-diary.tistory.com


# 자바 Linter와 Code Style 적용

학원 로컬과 노트북 설정이 달라서 그런가 계속 자동 정렬할 때마다 Git Change 요소가 발생해서 이 참에 Code Style을 적용해보기로 했다.

다른 블로그들 보면 네이버 캠퍼스 핵데이 코딩 컨벤션을 많이 맞췄던데.. 혼자하는 프로젝트기도 하고 Google Code Style이 더 범용적(?)으로 사용할테니 Google Code Style을 적용했다.

https://jino-dev-diary.tistory.com/entry/IntelliJ-LinterCheckStyle%EC%99%80-FormatterCode-Style-%EC%A0%81%EC%9A%A9

 

[IntelliJ] Linter(CheckStyle)와 Formatter(Code Style) 적용

개요 코드 스타일 통일을 위해 IntelliJ에 Linter와 Formatter를 적용해보았다 옛날에 파이썬 쓸 때, black, flake8같은 도구들이 매우 좋았는데 자바에는 없나 싶어서 찾아보니 있었다! CheckStyle 설치 Intelli

jino-dev-diary.tistory.com

 


# Conver Timer into Planner

CRUD까지 추가하고 든 생각이 Timer 자체는 View에서 처리하는 작업인데, 객체로 구현할 필요가 있을까? 라는 점이었다. 구글링을 해서 여느 플래너 어플리케이션을 봐도 서버쪽에서 Timer를 처리하지는 않았다.

그래서 Timer 객체를 Plan 객체로 수정하고 들어갈 필드들을 다시 생각해보았다. 예시 어플리케이션으로 선택한 뽀모도로 타이머를 보니 제목, 뽀모도로 수, 마감일, 프로젝트, 미리알림, 반복, 하위작업, 노트가 있었다.

여기서 하위 작업을 추가하는 건 뭔가 어려워 보이고.. 당장 쉽게 구현할 수 있는 것은 제목, 뽀모도로 수, 노트, 완료 여부 정도라고 생각하여 Timer → Plan 객체로 수정하고 hour, minute, second 필드를 삭제하고 timerCount와 clear라는 필드를 추가했다.

https://github.com/JinHoooooou/MiniTimer/commit/4ce3ffbf113a911bdef0ae360442d98f09e333d1

 


# 간단 회고

이 지점의 커밋들을 보니 학원에서 작업하다가 집에서 이어서 작성하려고 완전한 상태가 아닌데도 커밋하고 푸시한 커밋들이 있었다. 혼자 작업하다보니 이런 커밋들이 구별도 가고 크게 상관없겠지만, 협업하는 경우는 어떻게 해야하는지 궁금해졌다.

회사에서 작업하다가 완전하지 않은 상태인데 나머지는 집가서 마저 작업하려고 한다면(퇴근하고 집에가서 일을 더 한다는게 이상하게 느껴지긴 하는데;) 그 완전하지 않은 상태로 푸시하고 집에서 풀 이후에 작업하는게 맞는걸까? 아니면 다른 방법이 있을까?? 궁금해졌다.

이전에 자바로 웹 어플리케이션을 개발할 때는 바로 스프링 부트를 사용했는데, 지금 프로젝트 방향은 기본 프로젝트 Gradle 프로젝트로 바꾸게 됐다. 그러면서 프로젝트 구조에 대한 이해도 늘었고 이전에는 막연하게 IntelliJ에서 Gradle 알아서 사용해준 느낌이었는데, Gradle 명령어나 CMD에서 사용하는 법도 공부 할 수 있어서 좋았다.

# 개요

학원 다니면서 개인 프로젝트로 플래너 개발을 해보기로 했다.

처음에는 타이머로 시작했다가 내가 생각했던 방향이 아닌거 같아서 플래너로 바꿨다.

처음에 타이머로 생각한 이유는 단순히 뽀모도로 타이머(https://chromewebstore.google.com/detail/focus-to-do-%EB%BD%80%EB%AA%A8%EB%8F%84%EB%A1%9C-%ED%83%80%EC%9D%B4%EB%A8%B8-+-%EC%97%85%EB%AC%B4/ngceodoilcgpmkijopinlkmohnfifjfb?pli=1)를 개발하려고 했기 때문인데.. 코드 작성하다보니 타이머가 돌아가는 로직은 프론트에서 간단하게 처리 할 수 있다고 생각했기 때문이다.

원래는 프로젝트 개발진행하면서 블로그도 같이 쓰려고했는데, 개발 중간에서야 작성하게 됐다 ㅎㅎ;

 


# CRUD의 R(Read) 기능 추가

Read one / Read All에 대한 View 클래스 추가와 Controller에 메서드를 추가했다

Read One기능은 TimerController의 필드인 List의 Index 기반으로 했다. 나중에 데이터베이스 추가되면 Primary Key인 Id로 조회하거나 Front에 해당 객체 선택할 수 있도록 구현할 수 있겠지?

리팩토링을 위해 Create와 마찬가지로 Read기능에 대한 View와 Controller 테스트 코드를 작성했다. 

https://github.com/JinHoooooou/MiniTimer/commit/8b7c49d965d0a6cf870733550104a31537bb6650

 

TimerController의 필드인 List에 대한 캡슐화가 제대로 이루어진것 같지 않아서 isEmpty(), size()메서드를 따로 작성했다.

https://github.com/JinHoooooou/MiniTimer/commit/b104fe410aa618d69b0ed644c7780ebafd2f7aa2

 


# CRUD의 U(Update) 기능 추가

Update에 대한 View 클래스와 Controller에 메서드를 추가했다.

https://github.com/JinHoooooou/MiniTimer/commit/a03e217edf16138eedf27ccdded28b94a82d1293

 


# CRUD의 D(Delete) 기능 추가

Delete에 대한 View 클래스와 Controller에 메서드를 추가했다.

https://github.com/JinHoooooou/MiniTimer/commit/d37bd9a292c2eee7898cd19eac4b18228e77399d

https://github.com/JinHoooooou/MiniTimer/commit/e63dcb2ab82b5ef598241e522f4617fb258b5bd3

 


# 간단 회고

기능을 추가할 때 마다 테스트 코드를 작성했다. TDD로 구현한 것 까지는 아니지만, 테스트가 있어야 공격적인 리팩토링이 가능하기 때문에 작성했다.

근데 작성할 수록 View에 대한 테스트 코드를 작성하는것에 대해 회의감을 느꼈다. '이렇게 까지 View 테스트 코드를 작성해야 하나?" 라는 생각이 들었다. 그도 그럴 것이, View 부분은 비즈니스 로직 없이 단순히 콘솔에 보여주는 화면만 구성하는데 이것을 위한 테스트 코드까지 작성하는 것은 시간 낭비라는 생각이 계속 나를 지배했다. 그럼에도 불구하고 '테스트 코드 작성 연습하고 공부하자'라는 생각으로 작성했다.

Controller에서 getList()메서드로 List필드를 가져오고, 그 List에대한 메서드를 호출하는 것 보다. Controller에게 물어보는 메서드를 작성하는것이 더 올바른 캡슐화라고 생각해서 size(), isEmpty() 메서드를 작성했다. 근데 지금와서 보니 이 메서드들은 테스트 코드에서만 사용하는 메서드가 돼버려서 잘 못 작성한것 같다.

# 개요

학원 다니면서 개인 프로젝트로 플래너 개발을 해보기로 했다.

처음에는 타이머로 시작했다가 내가 생각했던 방향이 아닌거 같아서 플래너로 바꿨다.

처음에 타이머로 생각한 이유는 단순히 뽀모도로 타이머(https://chromewebstore.google.com/detail/focus-to-do-%EB%BD%80%EB%AA%A8%EB%8F%84%EB%A1%9C-%ED%83%80%EC%9D%B4%EB%A8%B8-+-%EC%97%85%EB%AC%B4/ngceodoilcgpmkijopinlkmohnfifjfb?pli=1)를 개발하려고 했기 때문인데.. 코드 작성하다보니 타이머가 돌아가는 로직은 프론트에서 간단하게 처리 할 수 있다고 생각했기 때문이다.

원래는 프로젝트 개발진행하면서 블로그도 같이 쓰려고했는데, 개발 중간에서야 작성하게 됐다 ㅎㅎ;

 


# 타이머 개발

기본적으로 학원 수업 진행 방향을 따라서 개발을 진행했고 거기에 추가적으로 내가 공부하고싶은 부분을 적용시켰다.

객체(Timer)에 대한 필드 설계를 하고 콘솔창에 View를 출력하고 사용자 입력 (Scanner)을 통해 실행되는 어플리케이션을 만드는 것으로 시작했다.

뽀모도로 타이머 어플을 보고 처음 설계한 것은 Timer 객체에 Hour, Minute, Second가 있고 Title과 Memo가 있어서 해당 요소들을 필드로 추가했다.

MainView에서 Timer 생성, 조회, 수정, 삭제 (CRUD)에 대한 View들을 사용자 입력으로 받고 그 기능들은 TimerController의 메서드로 구현했다.

처음에는 command 입력에 대한 기능들을 분기처리로 구현했다가 다형성을 이용하여 클래스를 분리하여 구현했다.

https://github.com/JinHoooooou/MiniTimer/commit/f3135fcb067d836ef2b534a3909c484dd39582e4

 

각 View에 대한 출력을 어플리케이션 실행이 아닌 단위 테스트 실행으로 확인할 수 있게 테스트 코드를 사용했다.

https://github.com/JinHoooooou/MiniTimer/commit/75d45a5b7524028afc3dc15f5a21e326c4b281ba 

 


# CRUD의 C(Create) 기능 추가

그리고 Create Timer에 대한 테스트 코드, View, Controller를 구현했다. 각 객체에 대한 저장은 아직 데이터베이스 연결을 하지 않았기 때문에 Controller에 있는 List에 저장하도록 구현했다.

https://github.com/JinHoooooou/MiniTimer/commit/eb696ee727a3b18035f14e2b03c6dbd7dfee3234

https://github.com/JinHoooooou/MiniTimer/commit/554c813326b17bd83210f36429371bdf8533ecca

 


# 간단 회고

지금 블로그에 작성하는건 커밋한지 약 1~2개월 후에 작성하는 것인데... 커밋 메시지 내용이 너무 개판이고 커밋 파일들도 중구난방이라 확인하는 것이 너무 힘들다..

아무리 혼자 하는 개발이라고 하지만 커밋 정리를 잘 하자.. 앞으로 한동안 커밋 개판인거 블로그에 옮기려니 벌써 어지럽다... ㅜㅜ

1. 문제

https://www.acmicpc.net/problem/1738

 

1738번: 골목길

첫째 줄에 골목길들이 교차하는 지점의 개수 n (2 ≤ n ≤ 100)과 골목길의 개수 m (1 ≤ m ≤ 20,000) 이 차례로 주어진다. 이어지는 m개의 행에 각각의 골목길을 나타내는 세 정수 u, v, w가 차례로

www.acmicpc.net

문제 설명

민승이는 놀러가기 위해 집을 나섰다. 민승이네 집에서 코레스코 콘도까지 가기 위해서는 복잡하게 얽혀있는 골목길들을 통과해야 한다.

그런데, 어떤 길에는 깡패가 서식하고 있어, 그 길을 지나게 되면 깡패에게 일정한 양의 금품을 갈취당하게 된다. 그런가하면, 어떤 길에는 지나가던 행인들이 흘리고 간 금품들이 떨어져 있어, 그 길을 지나게 되면 일정한 양의 금품을 획득하게 된다. 한 번 지나간 길을 다시 방문하더라도 금품을 갈취당하거나 획득한다.

골목길의 연결 상태와, 각 골목길을 지날 때 갈취당하거나 획득하게 되는 금품의 양이 주어졌을 때, 민승이가 최대한 유리한 경로를 따라 집에서 코레스코 콘도까지 가기 위해서는 어떻게 해야 하는지 출력하는 프로그램을 작성하시오. 

보유 중인 금품의 양이 음수가 될 수 있다. 최대한 유리한 경로 또는 최적의 경로는 민승이네 집에서 출발하여 코레스코 콘도에 도착하는 경로 중 금품의 양이 최대가 되는 경로이다. 

입력

첫째 줄에 골목길들이 교차하는 지점의 개수 n (2 ≤ n ≤ 100)과 골목길의 개수 m (1 ≤ m ≤ 20,000) 이 차례로 주어진다. 이어지는 m개의 행에 각각의 골목길을 나타내는 세 정수 u, v, w가 차례로 주어진다. 이는 u번 교차점에서 v번 교차점으로 이동할 수 있는 골목길이 나있다는 의미이다. 즉, 주어지는 골목길들은 기본적으로 모두 일방통행로이다. w (0 ≤ |w| ≤ 1,000)는 이 길을 지날 때 갈취당하거나 획득하게 되는 금품의 양이다. 음수는 갈취, 양수는 획득을 뜻한다.

골목길의 교차점 번호는 1이상 n이하의 정수이다. 민승이네 집은 1번 교차점에 있고, 이곳 코레스코 콘도는 n번 교차점에 있다.

출력

최적의 경로를 구할 수 있다면 민승이네 집부터 코레스코 콘도까지 가는 동안 거치게 되는 교차점들의 번호를 공백 하나를 사이에 두고 차례로 출력하면 된다. 그런데, 경우에 따라서는 최적의 경로라는 것이 존재하지 않는 상황이 발생한다. 어떠한 경우에 그런 상황이 발생하는지 생각해 보자. 그러한 경우에는 -1을 출력하도록 한다.

최적의 경로가 여러 개 존재할 때는 아무거나 출력해도 된다.

 

2. 어떻게 풀까

  • 최단 경로 알고리즘을 이용해서 해결 할 수 있다.
  • 이 문제의 경우 최솟값을 구하는게 아니라 최댓값을 구하는 것이므로 음수 사이클을 찾는 것이 아니라 양수 사이클을 찾아야 한다.
  • 사이클이 형성되는지 확인 해야하므로 벨만 포드 알고리즘을 이용하여 해결했다.
    • 사이클이 존재하더라도 사이클이 목적지에 영향을 주지 않는다면 상관없다.
      • 이 점을 간과해서 틀렸다. ㅜ

 

3. 코드 (Java)

package baekjoon.shortest_path.골목길_20220318;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.Queue;
import java.util.StringTokenizer;

public class Main {

  private static final BufferedReader READER = new BufferedReader(new InputStreamReader(System.in));
  private static StringTokenizer tokenizer;

  private static int vertexCount;
  private static int edgeCount;
  private static int[][] edges;
  private static int[] distanceFromSource;
  private static int[] predecessor;

  public static void main(String[] args) throws IOException {
    tokenizer = new StringTokenizer(READER.readLine());
    vertexCount = Integer.parseInt(tokenizer.nextToken());
    edgeCount = Integer.parseInt(tokenizer.nextToken());

    init();
    bellmanFord();

    if (distanceFromSource[vertexCount] == Integer.MIN_VALUE) {
      System.out.println(-1);
      return;
    }

    for (int[] edge : edges) {
      int from = edge[0];
      int to = edge[1];
      int weight = edge[2];
      if (distanceFromSource[from] == Integer.MIN_VALUE) {
        continue;
      }

      if (isCycle(from, to, weight) && isCycleOnPath(to)) {
        System.out.println(-1);
        return;
      }
    }

    printShortestPath();
  }

  private static void init() throws IOException {
    edges = new int[edgeCount][];
    for (int i = 0; i < edgeCount; i++) {
      tokenizer = new StringTokenizer(READER.readLine());
      int from = Integer.parseInt(tokenizer.nextToken());
      int to = Integer.parseInt(tokenizer.nextToken());
      int weight = Integer.parseInt(tokenizer.nextToken());

      edges[i] = new int[]{from, to, weight};
    }

    distanceFromSource = new int[vertexCount + 1];
    predecessor = new int[vertexCount + 1];
    for (int i = 1; i <= vertexCount; i++) {
      distanceFromSource[i] = Integer.MIN_VALUE;
      predecessor[i] = -1;
    }
    distanceFromSource[1] = 0;
  }

  private static void bellmanFord() {
    for (int i = 0; i < vertexCount - 1; i++) {
      for (int[] edge : edges) {
        int from = edge[0];
        int to = edge[1];
        int weight = edge[2];
        if (distanceFromSource[from] == Integer.MIN_VALUE) {
          continue;
        }
        if (distanceFromSource[to] < distanceFromSource[from] + weight) {
          distanceFromSource[to] = distanceFromSource[from] + weight;
          predecessor[to] = from;
        }
      }
    }
  }

  private static boolean isCycle(int from, int to, int weight) {
    return distanceFromSource[to] < distanceFromSource[from] + weight;
  }

  private static boolean isCycleOnPath(int targetVertex) {
    boolean[] visit = new boolean[vertexCount + 1];
    Queue<Integer> queue = new LinkedList<>();
    queue.add(targetVertex);

    while (!queue.isEmpty()) {
      int currentVertex = queue.poll();
      if (visit[currentVertex]) {
        continue;
      }
      visit[currentVertex] = true;

      for (int[] edge : edges) {
        int from = edge[0];
        int to = edge[1];

        if (from == currentVertex && !visit[to]) {
          queue.add(to);
        }
      }
    }

    return visit[vertexCount];
  }

  private static void printShortestPath() {
    StringBuilder sb = new StringBuilder();
    int index = vertexCount;
    sb.append(vertexCount);
    while (predecessor[index] != -1) {
      sb.insert(0, predecessor[index] + " ");
      index = predecessor[index];
    }
    System.out.print(sb.toString());
  }
}
  • bellmanFord() 메소드를 통해 출발 노드부터 목적지 노드까지의 최단 경로를 구했다.
  • 목적지 노드까지의 경로가 없거나, 경로상에 양수 사이클이 존재할 경우 -1를 출력하도록 했다.
  • 경로 상에 양수 사이클이 존재하는지 검사했다.
    • 사이클이 존재하는 지 검증
      • 벨만 포드 알고리즘 이후에도 distanceFromSource가 갱신이 되는지 확인했다.
    • 사이클이 존재하고, 그 사이클이 경로상에 존재하는지 검증
      • 사이클이 존재하는 노드에서 목적지 노드까지의 경로가 있는지 BFS를 이용하여 확인했다.
      • 이 부분을 간과해서 틀렸다.
  • predecessor과 backtracking을 이용해서 최단 경로를 출력했다.
    • 처음에는 StringBuilder에 append()로 하나씩 추가한 후 reverse()로 마무리를 했었는데 계속 실패가 나와서 insert()로 바꾸었다.

 

벨만 포드 알고리즘을 알고 있다면 쉽게 풀 수 있긴 한데, 음수(문제는 양수)사이클이 출발 노드에서 목적지 노드까지의 경로에 포함되는지 검사하는 로직이 반드시 필요하다는것을 간과했다. 

'Coding Test > 백준' 카테고리의 다른 글

[백준] 1944. 복제 로봇 (Java)  (0) 2022.01.20
백준 - 1238 - 파티 (java)  (0) 2021.05.26
백준 - 16398 - 행성 연결 (java)  (0) 2021.05.25
백준 - 14502번 - 연구소 (java)  (0) 2021.05.25

1. 문제

https://programmers.co.kr/learn/courses/30/lessons/43163

 

코딩테스트 연습 - 단어 변환

두 개의 단어 begin, target과 단어의 집합 words가 있습니다. 아래와 같은 규칙을 이용하여 begin에서 target으로 변환하는 가장 짧은 변환 과정을 찾으려고 합니다. 1. 한 번에 한 개의 알파벳만 바꿀 수

programmers.co.kr

문제 설명

제한 사항

입출력 예


2. 어떻게 풀까?

  • 인자로 주어지는 begin에서 시작해서 words에 주어진 단어들과 비교하면서 알파벳 하나만 바꿔서 그 단어가 될 수 있는지 확인한다.
  • words내에 방문한 단어들을 방문 표시하고, 모든 단어들을 방문하거나 target과 같은 단어를 방문하고 난 뒤에는 다시 방문 표시를 지워주어야 한다. 이게 무슨소리냐.. 말로 설명하니 어려우니까 그림으로 대체

visit가 다음과 같이 이루어진다고 가정하자. 방문한 word에 visit을 표시
cog까지 도착했을 때 distance는 6이다. 이제 dog로 돌아갈 때 visit 표시를 한 cog를 다시 unvisit로 표시하지 않으면 → cog로 갈 수가 없다
dog → log → cog로 갈 때보다, → cog로 가는 경로가 더 빠르다. 가장 빠른 길을 찾고 있기 때문에 갱신을 위해서 visit을 다시 unvisit로 바꿔주는 작업이 반드시 필요하다.

  • 아니 그럼 visit을 굳이 둘 필요 없지않냐? 라고 생각할 수도 있는데.... 그러면 무한루프에 hot → dot → hot → dot ... 이런식으로 무한루프에 빠지니까 visit또한 반드시 필요하다.

 

 

 

  • hit에서 hot으로, hot에서 dog로 알파벳 하나를 바꿔서 만들 수 있는 단어라는 것을 우리는 직관적으로 알 수 있지만 이것을 코드로는 어떻게 구현할까?
  • 단어와 단어 한 글자씩 비교하면서 다른 글자라면 count를 증가시키고, 그 count가 1이라면 알파벳 하나를 바꿔 만들 수 있는 단어가 된다고 판단하면 되겠다.

3. 코드 (Java)

package programmers.level3.단어변환_20220125;

import java.util.HashMap;
import java.util.Map;

public class Solution {

  public int result = Integer.MAX_VALUE;
  public Map<String, Boolean> visit;

  public int solution(String begin, String target, String[] words) {
    visit = new HashMap<>();
    for (String word : words) {
      visit.put(word, false);
    }
    dfs(begin, target, words, 0);
    return result == Integer.MAX_VALUE ? 0 : result;
  }

  private void dfs(String currentWord, String targetWord, String[] words, int distance) {

    if (currentWord.equals(targetWord)) {
      result = Math.min(result, distance);
      return;
    }

    for (String word : words) {
      if (!visit.get(word) && getCharDifferenceCount(currentWord, word) == 1) {
        visit.replace(word, true);
        dfs(word, targetWord, words, distance + 1);
        visit.replace(word, false);
      }
    }
  }

  private int getCharDifferenceCount(String a, String b) {
    if (a.length() != b.length()) {
      return -1;
    }

    int result = 0;
    for (int i = 0; i < a.length(); i++) {
      if (a.charAt(i) != b.charAt(i)) {
        result++;
      }
    }
    return result;
  }
}

 

 

 

 

 

 

사실 틀렸었다.

애매하게 테스트3만 틀려서 더 멘붕옴;

이유를 생각해봤는데.... 알파벳을 한 번 바꿔서 일치하는지 체크하는 부분에서 실수를 저질렀다.

    for (String word : words) {
      String erase = currentWord.replaceAll("[" + word + "]", "");
      if (!visit.get(word) && erase.length() == 1) {
        visit.replace(word, true);
        dfs(word, targetWord, words, distance + 1);
        visit.replace(word, false);
      }
    }

내 생각은 이랬다. "hot"에서 정규표현식을 이용하여 "[hit]"에 매치되는 부분을 지우면 "o"만 남게되고 그러면 알파벳 차이가 1개이니까 조건에 부합하겠구나! 나 천잰듯?

천재가 아니라 멍청이었다.

만약 단어가 "hto"였다면 어떨까? "hto" → "hit"가 되려면 알파벳을 2개 바꿔야한다. (t → i, o → t) 근데 저 코드대로라면 erase는 역시 "o"만 남게되고 조건에 맞으니까 if문을 통과하게 된다.

아니.. 개 허술한 코드였는데.. 다 틀려야지 하나만 틀리니까 더 찾기 힘들잖아.....................

그래서 정공법으로 코드 수정한 결과가 getCharDifferenceCount() 메소드를 이용한 것이었다.

그나마 다행인 것은 구글링해서 뭐가 틀렸는지 확인한 것이 아니라 스스로 틀린 부분을 생각해낸 부분이라는점..?

1. 문제

https://www.acmicpc.net/problem/1944

 

1944번: 복제 로봇

첫째 줄에 미로의 크기 N(4 ≤ N ≤ 50)과 열쇠의 개수 M(1 ≤ M ≤ 250) 이 공백을 사이에 두고 주어진다. 그리고 둘째 줄부터 N+1째 줄까지 미로의 정보가 주어진다. 미로는 1과 0, 그리고 S와 K로 주어

www.acmicpc.net

문제 설명

세준이는 어느 날 획기적인 로봇을 한 개 개발하였다. 그 로봇은 복제 장치를 이용하면 자기 자신을 똑같은 로봇으로 원하는 개수만큼 복제시킬 수 있다. 세준이는 어느 날 이 로봇을 테스트하기 위하여 어떤 미로에 이 로봇을 풀어 놓았다. 이 로봇의 임무는 미로에 흩어진 열쇠들을 모두 찾는 것이다. 그리고 열쇠가 있는 곳들과 로봇이 출발하는 위치에 로봇이 복제할 수 있는 장치를 장착해 두었다.

N*N의 정사각형 미로와 M개의 흩어진 열쇠의 위치, 그리고 이 로봇의 시작 위치가 주어져 있을 때, 모든 열쇠를 찾으면서 로봇이 움직이는 횟수의 합을 최소로 하는 프로그램을 작성하여라. 로봇은 상하좌우 네 방향으로 움직이며, 로봇이 열쇠가 있는 위치에 도달했을 때 열쇠를 찾은 것으로 한다. (복제된 로봇이어도 상관없다) 하나의 칸에 동시에 여러 개의 로봇이 위치할 수 있으며, 로봇이 한 번 지나간 자리라도 다른 로봇 또는 자기 자신이 다시 지나갈 수 있다. 복제에는 시간이 들지 않으며, 로봇이 움직이는 횟수의 합은 분열된 로봇 각각이 움직인 횟수의 총 합을 말한다. 복제된 로봇이 열쇠를 모두 찾은 후 같은 위치로 모일 필요는 없다.

입력

첫째 줄에 미로의 크기 N(4 ≤ N ≤ 50)과 열쇠의 개수 M(1 ≤ M ≤ 250) 이 공백을 사이에 두고 주어진다. 그리고 둘째 줄부터 N+1째 줄까지 미로의 정보가 주어진다. 미로는 1과 0, 그리고 S와 K로 주어진다. 1은 미로의 벽을 의미하고, 0은 지나다닐 수 있는 길, S는 로봇이 출발하는 위치, K는 열쇠의 위치가 주어진다. S는 1개, K는 M개가 주어진다. S와 K에서만 복제를 할 수 있음에 유의한다. 미로는 벽으로 둘러쌓여 있는 형태이다. 즉, 모든 테두리는 벽이다.

출력

첫째 줄에 모든 로봇이 움직인 횟수의 총 합을 출력한다. 모든 열쇠를 찾는 것이 불가능한 경우 횟수 대신 첫째 줄에 -1을 출력하면 된다.

 

 

 

 

2. 어떻게 풀까

  • 각 Key와 Start 지점에서 로봇이 복제 될 수 있다는 것은 A지점에서 출발해서 B지점으로 가는 거리가 Start 지점에서 B지점으로 가는 거리가 더 가까울 수 있다는 뜻이다.
  • 그렇다는 것은..? 각 지점을 정점이라고 가정하고 각 정점끼리 간선이 이어져 있고 이 간선들을 이용해서 최소 신장 트리를 만들라는 얘기가 될 것이다.
  • 최소 신장 트리를 만들 수 없는 경우에는 -1을 출력하면 될 것이고, 최소 신장 트리가 만들어질 경우에는 그 가중치의 최솟값을 출력하면 되겠다.
  • 그렇다면 정점은 입력으로 주어진 map의 좌표를 이용해서 저장하면 될 것이고... 각 정점간의 간선을 어떻게 만들것인가.. map에서 상하좌우로만 움직일 수 있고, 각 좌표간 거리는 1이라고 했으니까 BFS를 이용해서 각 정점간의 거리를 구하고 그것을 이용해서 간선을 구현하면 되겠다.

드가보자

 

 

 

 

3. 코드 (자바, 통과한 코드)

package baekjoon.mst.복제로봇_20220120;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

public class Main {

  private static BufferedReader reader;
  private static int[] pointY = {0, 0, 1, -1};
  private static int[] pointX = {1, -1, 0, 0};
  private static List<int[]> edges;
  private static Vertex[] vertices;
  private static int[] parents;

  public static void main(String[] args) throws IOException {
    reader = new BufferedReader(new InputStreamReader(System.in));
    edges = new ArrayList<>();

    String[] parts = reader.readLine().split(" ");
    int mapSize = Integer.parseInt(parts[0]);
    int vertexCount = Integer.parseInt(parts[1]) + 1;
    vertices = new Vertex[vertexCount];
    char[][] maps = buildMapsAndAddVertex(mapSize);

    for (Vertex vertex : vertices) {
      getEdgesUsingBFS(maps, vertex);
    }

    parents = makeSet();
    int result = kruskal();

    System.out.println(isMST() ? result : -1);
  }

  private static char[][] buildMapsAndAddVertex(int mapSize) throws IOException {
    char[][] maps = new char[mapSize][mapSize];
    int vertexIndex = 0;

    for (int i = 0; i < mapSize; i++) {
      String line = reader.readLine();
      for (int j = 0; j < mapSize; j++) {
        maps[i][j] = line.charAt(j);
        if (isSourceOrKey(maps[i][j])) {
          vertices[vertexIndex] = new Vertex(i, j, vertexIndex++);
        }
      }
    }
    return maps;
  }

  private static boolean isSourceOrKey(char c) {
    return c == 'S' || c == 'K';
  }

  private static void getEdgesUsingBFS(char[][] maps, Vertex sourceVertex) {
    Queue<int[]> queue = new LinkedList<>();
    boolean[][] visit = new boolean[maps.length][maps.length];
    visit[sourceVertex.y][sourceVertex.x] = true;
    queue.add(new int[]{sourceVertex.y, sourceVertex.x, 0});

    while (!queue.isEmpty()) {
      int[] current = queue.poll();
      int currentY = current[0];
      int currentX = current[1];
      int distanceFromSourceVertex = current[2];

      for (int i = 0; i < 4; i++) {
        int nextY = currentY + pointY[i];
        int nextX = currentX + pointX[i];

        if (!visit[nextY][nextX] && isValidPoint(maps, nextY, nextX)) {
          visit[nextY][nextX] = true;
          queue.add(new int[]{nextY, nextX, distanceFromSourceVertex + 1});

          if (isSourceOrKey(maps[nextY][nextX])) {
            for (Vertex destinationVertex : vertices) {
              if (destinationVertex.y == nextY && destinationVertex.x == nextX) {
                edges.add(new int[]{sourceVertex.id, destinationVertex.id, distanceFromSourceVertex + 1});
                break;
              }
            }
          }
        }
      }
    }
  }

  private static boolean isValidPoint(char[][] maps, int nextY, int nextX) {
    return (nextY >= 0 && nextY < maps.length)
        && (nextX >= 0 && nextX < maps.length)
        && (maps[nextY][nextX] != '1');
  }

  private static int kruskal() {
    edges.sort(Comparator.comparingInt(x -> x[2]));
    int result = 0;
    for (int[] edge : edges) {
      int from = edge[0];
      int to = edge[1];
      int weight = edge[2];

      if (find(parents, from) != find(parents, to)) {
        union(parents, from, to);
        result += weight;
      }
    }
    return result;
  }

  private static int[] makeSet() {
    int[] parents = new int[vertices.length];
    for (int i = 0; i < parents.length; i++) {
      parents[i] = i;
    }
    return parents;
  }

  private static int find(int[] parents, int x) {
    if (parents[x] == x) {
      return x;
    }
    return parents[x] = find(parents, parents[x]);
  }

  private static void union(int[] parents, int x, int y) {
    x = find(parents, x);
    y = find(parents, y);

    if (x == y) {
      return;
    }

    if (x < y) {
      parents[y] = x;
    } else {
      parents[x] = y;
    }
  }

  private static boolean isMST() {
    for (int i = 0; i < vertices.length; i++) {
      if (find(parents, i) != 0) {
        return false;
      }
    }
    return true;
  }

  private static class Vertex {

    int y;
    int x;
    int id;

    public Vertex(int y, int x, int id) {
      this.y = y;
      this.x = x;
      this.id = id;
    }
  }
}

부가 설명 없이 코드만 보고 이해할 수 있는 코드...라고 믿고 부가 설명은 달지 않는다. (이해가 안되신다면 댓글 감사하겠습니다..)

 

근데 틀렸었다.

무수한 틀렸습니다...

틀린 이유를 코드를 보면서 골똘히 생각해봤는데 두 부분에서 버그가 있었다.

          if (isSourceOrKey(maps[nextY][nextX])) {
            for (Vertex destinationVertex : vertices) {
              int destinationVertexId = -1;
              if (destinationVertex.y == nextY && destinationVertex.x == nextX) {
                destinationVertexId = destinationVertex.id;
                break;
              }
              edges.add(
                  new int[]{sourceVertex.id, destinationVertex.id, distanceFromSourceVertex + 1});
            }
          }

BFS를 이용해서 각 Vertex간의 거리를 구하고 탐색하는 좌표가 Vertex에 해당하면 Edge를 만들어 저장하는 부분이다. 여기서 edges.add() 부분이 if (destinationVertex.y == nextY && destinationVertex.x == nextX) 안에 있어야 했다. 그렇지 않으면 index가 -1인 상태로 edges에 추가되어서 버그가 발생할 수 있다.

 

    parents = makeSet();
    int result = kruskal();

    System.out.println(!edges.isEmpty() ? result : -1);

 

모든 Key에 닿을 수 있는 경우 가중치의 최솟값을 출력하고 그렇지 않은 경우 -1을 리턴하는 부분이다. 여기서 edges가 비어있는 경우를 모든 Key에 닿을 수 없는 경우로 생각하고 작성했는데, 생각해보니 edges 있어도 모든 Key에 닿을 수 있는 경우가 있을 수 있다. 그래서 find() 메소드로 모든 vertex들이 같은 집합인지 체크하는 isMST() 메소드를 작성하여 수정했다. 

'Coding Test > 백준' 카테고리의 다른 글

[백준] 1738. 골목길 (Java)  (0) 2022.03.21
백준 - 1238 - 파티 (java)  (0) 2021.05.26
백준 - 16398 - 행성 연결 (java)  (0) 2021.05.25
백준 - 14502번 - 연구소 (java)  (0) 2021.05.25

문제

https://www.codewars.com/kata/5a8d2bf60025e9163c0000bc

 

Codewars: Achieve mastery through coding challenge

Codewars is a coding practice site for all programmers where you can learn various programming languages. Join the community and improve your skills in many languages!

www.codewars.com

 

문제 설명

In this Kata, you will sort elements in an array by decreasing frequency of elements. If two elements have the same frequency, sort them by increasing value.

Solution.sortByFrequency(new int[]{2, 3, 5, 3, 7, 9, 5, 3, 7});

// Returns {3, 3, 3, 5, 5, 7, 7, 2, 9}
// We sort by highest frequency to lowest frequency.
// If two elements have same frequency, we sort by increasing value.

More examples in test cases.

Good luck!

 

배열에 integer 값들 count순서대로 정렬하고, count가 같다면 오름차순 정렬하라는 뜻

 

 

 

 

어떻게 풀까?

  • 정렬 문제라서 무지성으로 sort() 메소드를 써야겠다라는 생각을 함
  • 근데 counting순서로 정렬을 할 수 있나..?? sort() 메소드 쓰기전에 배열의 값들 counting을 해야할 것 같음
  • 일단 Map에 (int, count)으로 데이터를 모으고 얘네를 count순으로 정렬하고 배열에 담는 식으로 하면 될 것 같음
  • 드가자

 

 

 

 

코드

자바 코드

package codewars._6kyu.Simple_frequency_sort_20220117;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;

public class Solution {

  public static int[] sortByFrequency(int[] array) {
    Map<Integer, Integer> countingMap = getCountingMap(array);
    List<Entry<Integer, Integer>> entryList = getSortedEntryList(countingMap);
    List<Integer> result = getListSortedByFrequency(entryList);

    return result.stream().mapToInt(x -> x).toArray();
  }

  private static Map<Integer, Integer> getCountingMap(int[] array) {
    Map<Integer, Integer> map = new HashMap<>();
    for (int target : array) {
      map.put(target, map.getOrDefault(target, 0) + 1);
    }
    return map;
  }

  private static List<Entry<Integer, Integer>> getSortedEntryList(
      Map<Integer, Integer> countingMap) {
    List<Entry<Integer, Integer>> entryList = new ArrayList<>(countingMap.entrySet());
    entryList.sort((o1, o2) -> {
      if (o1.getValue().equals(o2.getValue())) {
        return o1.getKey() - o2.getKey();
      }
      return o2.getValue() - o1.getValue();
    });
    return entryList;
  }

  private static List<Integer> getListSortedByFrequency(List<Entry<Integer, Integer>> entryList) {
    List<Integer> result = new ArrayList<>();
    for (Entry<Integer, Integer> entry : entryList) {
      int key = entry.getKey();
      int value = entry.getValue();
      for (int i = 0; i < value; i++) {
        result.add(key);
      }
    }
    return result;
  }

  public int[] sortByFrequencyBestPracticeSolution(int[] array) {
    Map<Integer, Long> integerCountMap =
        Arrays.stream(array)
            .boxed()
            .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
    return Arrays
        .stream(array)
        .boxed()
        .sorted(Comparator.<Integer, Long>comparing(integerCountMap::get)
            .reversed()
            .thenComparing(Comparator.naturalOrder()))
        .mapToInt(Integer::intValue)
        .toArray();
  }
}

Map으로 (int, count) 데이터를 만들고, EntrySet list를 통해 value(count) 순으로 정렬 (count가 같다면 key 오름차순), 그리고 result list에 하나씩 담아 배열로 변환하여 리턴

근데 Best Practice를 보니까 Stream으로 신박하게 해결했다. 참고하면 좋을듯?

Collectors.groupingBy(), Function.identity(), Collectors.counting(), Comparator.<Integer, long>comparing() 메소드들은 처음 보는 메소드들인데 알아두면 Stream에서 요긴하게 쓸 수 있을듯

 

 

 

파이썬 코드

def solve(arr):
    int_count_map = {}
    for integer in arr:
        int_count_map.setdefault(integer, 0)
        int_count_map[integer] = int_count_map.get(integer) + 1

    sorted_by_value_dict = sorted(int_count_map.items(), key=lambda x: x[1], reverse=True)

    result = []
    for items in sorted_by_value_dict:
        key = items[0]
        value = items[1]
        for i in range(value):
            result.append(key)

    return result


def solve_best_practice(arr):
    return sorted(sorted(arr), key=lambda n: arr.count(n), reverse=True)

파이썬으로도 풀어보자 해서 억지로 풀긴했다. 근데 BestPractice보니까 헛웃음이 나더라

자바에서 푼 것 처럼 dict에 (int, count)로 담고, count 내림차순으로 정렬했다.

근데 BestPractice보니까 그냥 개 쉽게 풀어버림.. 당황스럽네

 

그래프 순회

  • 그래프에서 임의의 한 정점에서 출발하여 모든 정점을 한번씩 방문하는 작업
  • 탐색하는 동안 이미 방문한 정점이 있을 수 있으므로 방문했다는 표시를 하여 중복방문을 피한다.
  • 대표적으로 DFS, BFS가 있다.

BFS (Breath-First-Search): 너비 우선 탐색

  • 출발노드에서 인접한 노드(형제노드)를 먼저 탐색하는 방식
  • 가중치가 없는 그래프에서 최단경로를 찾을 때 사용한다.
  • 일반적으로 Queue를 이용하여 구현한다.

동작방식

  1. 시작정점을 방문하여 큐에 삽입한다.
  2. 큐에서 정점을 꺼내 정점에서 인접한 정점중, 방문하지 않은 정점들을 방문하여 큐에 삽입한다.
  3. 모든 정점을 반복할 때까지 반복한다.

Visualize

E노드에서 더이상 간선이 없으므로 그냥 큐에서 꺼내는것으로 끝이다

G노드에서 더이상 간선이 없으므로 그냥 큐에서 꺼내는것으로 끝이다. H노드와 I노드도 마찬가지이다.

 

코드

의사코드

BFS(graph, start_node) {
  Queue q;
  boolean[] visit;
  
  q.add(start_node);
  visit[start_node] = true;
  
  while(!q.isEmpty()) {
    current_node = q.pop();
    for(adjacent_node to current_node) {
      if(!visit[adjacent_node]) {
        visit[adjacent_node] = true;
        q.add(adjacent_node);
      }
    }
  }
}

코드 - Queue (Java)

class BFS_Queue {
  private int[][] graph;
  private boolean[] visit;
  
  public BFS_Queue() {
    this.graph = {
      {0, 1, 1, 1, 0, 0, 0, 0, 0},
      {1, 0, 0, 0, 1, 0, 0, 0, 0},
      {1, 0, 0, 0, 0, 1, 1, 0, 0},
      {1, 0, 0, 0, 0, 0, 0, 1, 0},
      {0, 1, 0, 0, 0, 0, 0, 0, 0},
      {0, 0, 1, 0, 0, 0, 0, 0, 1},
      {0, 0, 1, 0, 0, 0, 0, 0, 0},
      {0, 0, 0, 1, 0, 0, 0, 0, 0},
      {0, 0, 0, 0, 0, 1, 0, 0, 0},
    };
    this.visit = new boolean[graph.length];
  }
  
  public void bfs(startNode) {
    Queue<Integer> queue = new LinkedList();
    
    queue.add(startNode);
    visit[startNode] = true;
    
    while(!queue.isEmpty()) {
      int currentNode = queue.poll();
      
      for(int i = 0; i < graph[currentNode].length; i++) {
        if(isAdjacentNode(currentNode, i) && !visit[i]) {
          visit[i] = true;
          queue.add(i);
        }
      }
    }
  }
  
  public boolean isAdjacentNode(int currentNode, int targetNode) {
    return graph[currentNode][targetNode] == 1;
  }
}

시간 복잡도

  • 인접 리스트: \(O(N+E)\) (\(N\): 정점 개수, \(E\): 간선 개수)
  • 인접 행렬: \(O(N^2)\)

참고

2021.04.14 - [Algorithm] - [Algorithm] 그래프 (Graph)

 

[Algorithm] 그래프 (Graph)

참고1 : 유튜브 (권오흠 교수, 2015 봄학기 알고리즘) 참고2 : (다양한 예제로 학습하는) 데이터 구조와 알고리즘 for java 1. 그래프 : G = (V, E) 1. 정의 V : 정점(Vertex) 또는 노드(Node)들의 집합 E : 간선,..

jino-dev-diary.tistory.com

2022.01.08 - [Algorithm] - [Algorithm] DFS (Depth-First-Search, 깊이 우선 탐색)

 

[Algorithm] DFS (Depth-First-Search, 깊이 우선 탐색)

그래프 순회 그래프에서 임의의 한 정점에서 출발하여 모든 정점을 한번씩 방문하는 작업 탐색하는 동안 이미 방문한 정점이 있을 수 있으므로 방문했다는 표시를하여 중복방문을 피한다. 대표

jino-dev-diary.tistory.com

 

+ Recent posts