티스토리 뷰

코드카타

 

2개 이하로 다른 비트

 

문제

 

비트는 십진수를 이진수로 변환한 값에 해당한다.

 

주어진 수 x의 비트와 비트가 1~2개 다른 수들 중에서

제일 작은 수를 찾아야 하는데 이때 그 수는 x보다 큰 값을 가진다.

 

 

입출력 예시

 

numbers의 요소는 x에 해당하며 result의 요소는 f(x)에 해당한다.

 

 

시간 복잡도를 고려하지 않은 풀이

class Solution {

    // 앞에 0을 붙여 두 2진수의 길이를 같게 만듬
    private fun makeSameLen(beat1: String, beat2: String): Pair<String, String> {
        var maxLen = maxOf(beat1.length, beat2.length)
        val padBeat1 = beat1.padStart(maxLen, '0')
        val padBeat2 = beat2.padStart(maxLen, '0')
        return Pair(padBeat1, padBeat2)
    }
	
    // 조건에 해당하는 수 반환
    private fun f(x: Long): Long {
        var n = x + 1
        while (true) {
            val (beat1, beat2) = makeSameLen(x.toString(2), n.toString(2))

            var cnt = 0
            for (i in beat1.indices) {
                if (beat1[i] != beat2[i]) cnt++
            }

            if (cnt >= 1 && cnt <= 2) return n
            else n += 1
        }
    }

    fun solution(numbers: LongArray): List<Long> {
        var res = mutableListOf<Long>()
        for (x in numbers) {
            res.add(f(x.toLong()))
        }

        return res
    }
}

number의 요소를 1씩 증가시키면서 조건에 해당하는 수를 찾는 로직이다.

 

f(x) 함수는 특정 조건을 만족할 때까지 반복문을 돌린다.

이 과정에서 n을 증가시키면서 모든 경우를 순회해야 할 수 있다.

 

numbers의 요소는 최대 10^15의 값을 가지기에

f(x)의 인자값으로 큰 값이 들어오게 되면 시간 초과가 발생한다.

 

 

알고리즘 최적화

f(x)의 인자값에 짝수와 홀수가 들어가는 경우를 나누어서 생각해봐야한다.

 

조건에 맞는 비트값을 찾기 위해 비트를 변환할 때,

두 경우에서 규칙성을 찾아내 f(x)함수를 효율적으로 설계할 수 있다.

 

2 -> 10 -> 11(3)
4 -> 100 -> 101(5)
6 -> 110 -> 111(7)

짝수를 이진법으로 변환했을 때 마지막 숫자는 0이 된다.

 

주어진 짝수의 비트값에서 마지막 자리를 1로 바꿔준 비트값은

x보다 크지만 다른 비트 갯수가 1개인 숫자의 비트값에 해당한다.

 

2진수의 마지막 자리는 1의 배수이므로 해당 숫자는 (주어진 수+1)에 해당한다.

 

1 -> 01 -> 10(2)
3 -> 011 -> 101(5)
5 -> 101 -> 110(6)
7 -> 0111 -> 1011(11)

주어진 수가 홀수 일 때, 주어진 수의 비트에서

가장 마지막(오른쪽)에 위치한 0의 인덱스를 n이라고 한다.

 

n번째 인덱스에 위치한 0을 1로, (n + 1)번째 인덱스에 위치한 1을 0으로

바꿔주면 조건에 맞는 비트값을 반환할 수 있다.

 

따라서, 초기 비트 문자열은 주어진 숫자를 이진수로 변환하고,

 그 앞에 0을 추가하여 초기화해야 한다.

 

 

풀이

class Solution {
    private fun f(x: Long): Long {
        if (x % 2 == 0L) {
            return x + 1
        } else {
            val beat = '0' + x.toString(2)
            val right = beat.lastIndexOf('0')

            val beatArray = beat.toCharArray()
            beatArray[right] = '1'
            beatArray[right + 1] = '0'

            return beatArray.joinToString("").toLong(2)
        }
    }

    fun solution(numbers: LongArray): List<Long> {
        var res = mutableListOf<Long>()
        for (x in numbers) {
            res.add(f(x))
        }

        return res
    }
}

찾아낸 비트의 규칙성을 기반으로 f(x) 함수를 설계하여 시간복잡도를 줄인다.

 

 

문자열에서 특정 인덱스에 위치한 문자 변경

fun main() {   
    val beat = "10"
    
    val beatArray = beat.toCharArray()
    beatArray[0] = '0'
    beatArray[1] = '1'
    
    print(beatArray) // "01"
}

Kotlin에서는 문자열은 불변 객체이므로 특정 인덱스에

접근해 값을 수정하는 것은 불가능하다.

 

문자열에서 특정 인덱스에 위치한 문자를 변경하려면

문자열을 문자 배열로 변환한 뒤 해당 배열의 요소를 변경해야한다.

 

 

회고

이해하는 데 상당히 오래걸렸던 문제였다.

문자열에서 특정 인덱스의 값을 변경하는 방법을 배울 수 있었다.

 


 

디테일 페이지 구현

 

개요

 

 

3번째 챕터에서는 배운 거 기반으로 기초 프로젝트를 진행했는데

해당 프로젝트에서 필수적으로 구현할 화면은 총 5가지였고 

그 중 나는 디테일 화면을 구현하기로 했다.

 

 

구현할 디테일 화면

 

메인화면에서 이미지버튼을 눌러 식사메뉴 카테고리를 선택하면

선택한 카테고리에 해당하는 음식 리스트를 출력하는 화면을 구현했다.

 

 

카테고리 문자열 넘겨주기

 

메인화면의 이미지버튼을 누르면 카테고리 문자열을 디테일 화면으로 넘겨준다. 

 

 

레이아웃 짜기

 

안배운 개념(리스트 뷰) 쓰지 마라고 해서 메뉴 리스트를 레이아웃을 하나하나 구현했다.

한 화면에 총 1개의 추천메뉴와 4개의 다른메뉴를 출력한다.

 

툴바를 제외한 위젯을 스크롤뷰로 감싸 화면 스크롤이 가능하도록 하였다.

 

 

데이터 클래스 설계

data class FoodInfo(
    val category: String,   // 카테고리 분류
    val foodId: Int,        // 음식 아이디 
    val image: Int,         // 이미지 리소스
    val name: String,       // 메뉴 이름
    val introduce: String,  // 메뉴 소개
    val tags: List<String>, // 메뉴 태그들
)

카테고리별 메뉴의 정보를 출력하는데 활용되는 데이터 클래스이다.

 

 

 

관련 변수 초기화 클래스

class FoodManager {

    private val foodList: List<FoodInfo> by lazy { initFoodData() }
    
    // 카테고리 문자열을 인자로 받아 해당하는 음식 리스트를 반환하는 함수
    fun getFoodList(category: String): List<FoodInfo> {
    	return foodList.filter { it.category == category }
    }
	
    // 카테고리에 해당하는 음식 리스트 가져오기
    var koreanFoodList = getFoodList("koreanFood")
        private set
    
    // 추천 메뉴번호 초기화 
    var koreanRandomNum = (1..5).random()

    // 추천 메뉴 데이터 클래스 가져오기
    var koreanRandomFood = koreanFoodList.find { it.foodId == koreanRandomNum }
        private set
	
    // 다른 메뉴들 데이터 클래스 가져오기
    var koreanFoodFilteredList = koreanFoodList.filter { it.foodId != koreanRandomNum }
        private set
	
    // 음식 리스트 초기화 하기
    private fun initFoodData(): MutableList<FoodInfo> {
        return mutableListOf(
            FoodInfo(
                "koreanFood",
                1,
                R.drawable.img_bibimbap,
                "비빔밥",
                "쌀밥에 고기나 나물 등과 여러 가지 양념을 넣어 비벼 먹는 음식",
                listOf("중간맛", "밥", "야채")
            ),
            // 중략
         )
     }
}

중복되는 로직은 적당히 생략해서 작성한 FoodManger클래스이다.

 

private set이라는 접근제어자를 사용해 프로퍼티 값을

외부에서 읽을 수는 있지만, 변경할 수는 없도록 설정한다.

 

 

트러블 슈팅

액티비티를 생성할 때마다 추천메뉴가 바뀌지 않는 문제가 있었는데,

이는 추천 메뉴 번호가 액티비티를 생성할 때마다 초기화 되지 않아서 발생하는 문제였다.

 

obejct내에 선언된 변수는 오브젝트 밖에서 값의 변경이 불가하므로
FoodManager를 class로 선언한 뒤 private set 접근제어자를 제거하였다.

 

 

데이터 변수 선언

private lateinit var foodList: List<FoodInfo>
private lateinit var randomFood: FoodInfo
private lateinit var filteredFoodList : List<FoodInfo>

카테고리 별로 다른 음식 데이터를 가져와야 하기에

액티비티 파일 내에서 데이터 관련 변수는 lateinit으로 선언한다.

 

 

레이아웃 초기화 함수

private fun setLayout() {
    val foodManager = FoodManager() // FoodManager 클래스 인스턴스 초기화
        
    val category = intent.getStringExtra("food") // 카테고리 문자열 가져오기
    val tvTitleList = findViewById<TextView>(R.id.tv_title_list)
    
    // 카테고리 별로 데이터 초기화
    when (category) {
        "koreanFood" -> {
            tvTitleList.text = getString(R.string.detail_menu_list_title, getString(R.string.main_koreanfood))
            foodList = foodManager.koreanFoodList
            randomFood = foodManager.koreanRandomFood!!
            filteredFoodList = foodManager.koreanFoodFilteredList
        }
        else -> {
            foodList = emptyList()
        }
    }
    
    // 데이터 문제없이 초기화했으면 레이아웃 초기화
    if (foodList.isNotEmpty()) {
        setRecommendFoodInfo()
        setFoodListInfo()
    }
}

타이틀 리스트의 텍스트는 "%s 리스트"에 해당하고 만약 선택한 카테고리가

한식이라면 타이틀 텍스트는 "한식 리스트"로 초기화 된다.

 

해당 함수를 onCreate()내에서 호출시켜 레이아웃을 초기화 한다.

 

 

음식 메뉴 레이아웃 초기화

private fun setRecommendFoodInfo() {
        val recommendImg = findViewById<ImageView>(R.id.iv_recommend_food)
        val tvRecommendMenuName = findViewById<TextView>(R.id.tv_recommend_menu_name)
        val tvRecommendMenuIntroduce = findViewById<TextView>(R.id.tv_recommend_menu_introduce)
        val tvDetailTag1 = findViewById<TextView>(R.id.tv_detail_tag_1)
        val tvDetailTag2 = findViewById<TextView>(R.id.tv_detail_tag_2)
        val tvDetailTag3 = findViewById<TextView>(R.id.tv_detail_tag_3)

        recommendImg.setImageResource(randomFood.image) // 음식 이미지 리소스 초기화
        tvRecommendMenuName.text = randomFood.name // 메뉴이름 텍스트 초기화
        tvRecommendMenuIntroduce.text = randomFood.introduce // 메뉴소개 텍스트 초기화
		
        // 태그 개수에 따라 태그 초기화
        when (randomFood.tags.size) {
            1 -> setTag(tvDetailTag1, randomFood, 0)
            2 -> {
                setTag(tvDetailTag1, randomFood, 0)
                setTag(tvDetailTag2, randomFood, 1)
            }
            3 -> {
                setTag(tvDetailTag1, randomFood, 0)
                setTag(tvDetailTag2, randomFood, 1)
                setTag(tvDetailTag3, randomFood, 2)
            }
            else -> return
        }
    }

추천 메뉴와 다른 메뉴 레이아웃 초기화는 로직이 동일하다.

 

 

태그 색상 정의

 

태그 색상들을 values/colors.xml에 정의해준다.

 

 

태그 설정 함수

// 태그 정보 설정
private fun setTag(textView: TextView, food: FoodInfo, index: Int) {
    textView.apply {
        visibility = View.VISIBLE // 출력 여부
        text = food.tags[index] // 태그 글자
        
        // 글자색 & 배경색
        setTextColor(ContextCompat.getColor(context, applyTextColor(food.tags[index])))
        setBackgroundResource(applyBackgroundByTag(food.tags[index]))
    }
}

// 태그 글자색 초기화 (생략)
private fun applyTextColor(tag: String): Int {
    return when (tag) {
        "고기" -> R.color.white
        else -> R.color.white
    }
}

// 태그 배경색 초기화 (생략)
private fun applyBackgroundByTag(tag: String): Int {
    return when (tag) {
        "고기" -> R.drawable.bg_tag_dark_brown
        else -> R.drawable.bg_tag_white
    }
}

메뉴정보를 기반으로 태그 글자에 따라 태그의 글자색과 배경색을 초기화한다.

 

 

 

실행 결과

 

개인적으로 UI가 아주 마음에 든다.

 

 

회고

금요일 날에 해당 페이지를 구현하려고 했는데 이미 팀원분이 만들어놓으셔서
나는 로직분석, 자잘한 버그 수정, 메뉴 데이터 추가 이 정도만 했다.

 

분류 기준이 애매해서 음식마다 태그지정하는게 힘들었다.

 

로직을 분석하면서 객체지향 프로그래밍에서 어떤식으로 클래스를 설계하고
활용하는지에 대한 감을 어느정도 잡을 수 있었다.

 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함