본문 바로가기
알고리즘/기초

자바 숫자의 자릿수를 판별하는 5가지 방식

by Renechoi 2023. 6. 21.

숫자의 자릿수를 판별하는 5가지 방식에 대해 살펴본다. 

 

 

 

1. 나머지 연산과 반복문을 이용한 방식 

 

public static int calculate(int number){
   int count=0;
   while (number>0){
      number /= 10;
      count++;
   }
   return count;
}

 

동작 원리 

주어진 숫자를 10으로 나눈 나머지를 계산한다. 나머지 연산을 통해 숫자의 가장 오른쪽 자릿수를 구하고, 해당 자릿수를 버린 숫자를 얻는다. 버린 숫자가 0보다 큰 경우, 자릿수를 하나 증가시키고 1단계로 돌아간다. 버린 숫자가 0인 경우, 반복문을 종료하고 자릿수를 반환한다.

 

예를 들어, 숫자 1234를 처리하는 경우 1234를 10으로 나누고 몫인 123을 얻는다. 그리고 자릿수를 하나 증가시키고, 다시 몫인 12를 얻어 자릿수를 증가시킨다. 이 과정을 반복하여 몫이 0이 되면 반복문을 종료하고 자릿수를 반환한다. 각각의 처리마다 count의 개수가 증가하며, 모든 작업을 마치고 반복문이 종료되면 count로 계수된 4를 반환한다.

 

 

 

시간 복잡도 

주어진 숫자의 자릿수만큼 반복문을 수행하므로, 숫자의 자릿수에 비례하는 시간이 소요된다. 따라서, 숫자의 자릿수를 n이라고 할 때, 시간 복잡도는 O(n)이다.

 

 

장점 

- 간단하고 이해하기 쉽다. 
- 반복문과 나머지 연산만을 사용하므로, 기본적인 연산만으로 구현 가능하다.

 

단점 

- 큰 숫자의 경우, 잦은 반복문은 부담된다. 

 

 

 

2. 배열, 반복문, 나머지 연산을 이용한 방식 

 

public static int calculate2(int number) {
   if (number == 0) {
      return 1;
   }

   int[] digits = new int[10];
   int digit =0;
   // 숫자의 각 자리수를 배열에 기록
   while (number > 0) {
      digits[digit++]++;
      number /= 10;
   }

   // 배열에서 0이 아닌 원소의 개수가 자리수를 나타냄
   int count =0;
   for (int i = 0; i < digits.length; i++) {
      if (digits[i] != 0) {
         count++;
      }
   }
   return count;
}

 

동작 원리 

 

주어진 숫자를 10으로 나누어 나머지를 배열에 저장하면서 자리수를 계산한다.

 

예를 들어, 숫자 1234를 처리하는 경우 배열 digits에는 [0, 4, 3, 2, 1, 0, 0, 0, 0, 0]가 저장된다.

 

그리고 배열에서 0이 아닌 원소의 개수를 세어 자리수를 나타낸다. 이 경우에는 count 변수에 4가 저장된다. 따라서 함수는 1234의 자리수인 4를 반환한다.

 

digits 배열을 저장하는 자리수를 계산하는 원리는 첫 번째 방식에서 사용하는 나머지 연산과 그 원리가 동일하다. 그렇다면 굳이 배열을 사용해야 하는 이유가 무엇일까? 

 

활용성 차원에서 보다 유연할 수 있기 때문이다. 예를 들어, 배열에서 0이 아닌 원소의 위치를 찾거나 각 자리수의 합을 계산하는 등의 작업을 더 쉽게 수행할 수 있다. 이러한 유연성은 특정한 자리수와 관련된 계산이 필요한 경우에 유용하다.


시간 복잡도 

입력된 숫자의 자릿수에 비례한다. 숫자를 10으로 나누어 자릿수를 계산하는 반복문에서 숫자를 한 자리씩 줄여나가기 때문에, 입력된 숫자의 자릿수가 n이라면 시간 복잡도는 O(n)다.



장점 

- 간단한 배열과 반복문을 이용하므로 이해하기 쉽다(1번과 동일).
- 숫자의 자릿수를 직접 계산하기 때문에 추가적인 메모리가 필요하지는 않으며, 1번과 비교할 때 저장된 배열을 활용할 수 있어 활용도 측면에서 유용할 수 있다. 

 




단점 

- 입력된 숫자의 범위에 따라 배열의 크기가 커질 수 있습니다. 이는 메모리 사용량을 증가시킬 수 있으며, 큰 숫자에 대한 처리 속도를 느리게 할 수 있다. 현재 코드에서는 임시로 10으로 할당했다. 
- 숫자의 범위가 매우 큰 경우 많은 반복문은 부담된다.

 

 

 

 

 

 

3. 수학적인 원리를 이용한 방식 

 

public static int calculate3(int number) {
   if (number == 0) {
      return 1;
   } else {
      return (int) Math.log10(Math.abs(number)) + 1;
      // return (int) (Math.log10(number) + 1); // 절대값을 빼고 구해도 된다.
   }
}

 

동작 원리 

주어진 숫자가 0인 경우 1을 반환하고, 그렇지 않은 경우에는 주어진 숫자의 절댓값에 대한 로그 값을 계산한 후 10을 밑으로 하는 로그 값을 구한다. 이 값에 1을 더하여 자리수를 계산한다.

 

수학적인 원리는 다음과 같다.

 

주어진 숫자의 절댓값에 대한 로그 값을 계산하면, 해당 숫자를 10의 몇 승으로 표현할 수 있는지 알 수 있다. 예를 들어, log10(1000)은 3이며 이는 10의 3승이 1000이라는 것을 의미합니다. 따라서, log10 값에 1을 더해주면 숫자의 자릿수를 구할 수 있다.

 

이를 적용하여 예를 들자면, 숫자 1234를 처리하는 경우 Math.abs(1234)의 로그 값인 3.0913을 계산하고, 1을 더하여 자리수 4를 반환한다.


시간 복잡도 

 

입력된 숫자의 자릿수에 비례한다. Math.log10 함수는 입력된 숫자에 대한 로그 값을 계산하므로, 숫자의 자릿수에 비례하는 시간이 소요된다. 따라서, 숫자의 자릿수를 n이라고 할 때, 시간 복잡도는 O(n)이다.



장점 

- 수학적인 원리를 이용하여 자리수를 계산하므로 간단하고 정확하다.
- 단순한 수학 함수를 사용하기 때문에 구현이 간단하다(단, 수학 원리를 알고 있을 때)



단점 

- Math.log10 함수는 부동소수점 연산을 수행한다. 숫자의 범위가 큰 경우에는 부동소수점 연산의 컴퓨터 연산 정밀도 한계로 인해 정확한 결과가 나오지 않을 수 있다.

 

 

 

 

4. String으로 만들어 길이를 계산하는 방식

 

public static int calculate4(int number){
   return String.valueOf(number).length();
}

 


동작 원리 

주어진 숫자를 String으로 변환한 후, 변환된 문자열의 길이를 계산하여 자리수를 반환한다. 예를 들어, 숫자 1234를 처리하는 경우 "1234"로 변환된 문자열의 길이인 4를 반환한다.




시간 복잡도 

String 변환과 문자열 길이 계산에 소요되는 시간에 의존한다. String.valueOf() 메서드는 주어진 숫자를 문자열로 변환하는 과정에서 입력된 숫자의 자릿수에 비례하여 실행 시간이 증가하기 때문에 O(n)의 시간 복잡도를 갖는다. n은 입력된 숫자의 자릿수이다.

변환된 문자열의 길이를 계산하는 length() 메서드는 O(1)의 시간이 소요되며, 따라서, 시간 복잡도는 O(n)이다.



장점 

 

- 간단한 메서드 호출로 구현이 간편하고 쉽다. 
- 자바 내장 함수인 String.valueOf()와 length()를 사용하므로 추가적인 로직을 구현할 필요가 없다.
-> 그냥 갖다 쓰면 된다. 


단점 

- 입력된 숫자를 문자열로 변환하는 과정은 비용이 크며 추가적인 객체를 생성하므로 많은 메모리를 사용할 수 있다. 

 

 

5. Stream API를 이용한 방식 

 

public static long calculate5(int number) {
   return Stream.iterate(number, n -> n / 10)
      .takeWhile(n -> n > 0)
      .count();
}

 


동작 원리 

 

주어진 숫자를 초기값으로 설정하여 Stream을 생성한다. 각 단계에서 현재 숫자를 10으로 나누어 다음 단계에 전달한다. iteration을 수행하며 자릿수를 하나씩 줄여가는 역할을 한다.

 

takeWhile 메서드를 사용하여 숫자가 0보다 큰 경우에만 Stream에 유지되며, 즉, 자릿수가 있는 동안만 Stream이 유지된다. 

 

최종적으로 Stream의 개수를 세어 자릿수를 반환한다.



시간 복잡도 

입력된 숫자의 자릿수에 따라 Stream의 요소 수가 결정되며, takeWhile을 통해 n이 0보다 같거나 작게 되는 시점까지 반복하므로 다른 반복문을 이용한 방식과 동일하게 입력된 숫자의 자릿수에 비례하여 시간이 증가한다. 따라서 이 방식의 시간 복잡도는 O(n)이다.


장점 

- Stream API를 활용하여 간단하고 가독성 좋은 코드를 작성할 수 있다. 
- 추가적인 객체 생성 없이 입력된 숫자를 변환하지 않고 자릿수를 계산할 수 있다.



단점 

- 다른 방식과 마찬가지로 숫자가 커질 때 반복문에 대한 부담이다.

- Stream 객체를 생성하고 유지하기 위한 메모리 공간이 필요하므로, 큰 입력에 대해서는 메모리 사용량이 더 많아질 수 있다.

 

 

 

전체 코드 

package basic.math;

import java.util.stream.Stream;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class Digit {

   @Test
   public static void test(){
      Assertions.assertEquals(1, calculate(1));
      Assertions.assertEquals(1, calculate2(1));
      Assertions.assertEquals(1, calculate3(1));
      Assertions.assertEquals(1, calculate4(1));
      Assertions.assertEquals(1, calculate5(1));

      Assertions.assertEquals(1, calculate(9));
      Assertions.assertEquals(1, calculate2(9));
      Assertions.assertEquals(1, calculate3(9));
      Assertions.assertEquals(1, calculate4(9));
      Assertions.assertEquals(1, calculate5(9));

      Assertions.assertEquals(2, calculate(10));
      Assertions.assertEquals(2, calculate2(10));
      Assertions.assertEquals(2, calculate3(10));
      Assertions.assertEquals(2, calculate4(10));
      Assertions.assertEquals(2, calculate5(10));

      Assertions.assertEquals(2, calculate(11));
      Assertions.assertEquals(2, calculate2(11));
      Assertions.assertEquals(2, calculate3(11));
      Assertions.assertEquals(2, calculate4(11));
      Assertions.assertEquals(2, calculate5(11));

      Assertions.assertEquals(2, calculate(99));
      Assertions.assertEquals(2, calculate2(99));
      Assertions.assertEquals(2, calculate3(99));
      Assertions.assertEquals(2, calculate4(99));
      Assertions.assertEquals(2, calculate5(99));

      Assertions.assertEquals(3, calculate(100));
      Assertions.assertEquals(3, calculate2(100));
      Assertions.assertEquals(3, calculate3(100));
      Assertions.assertEquals(3, calculate4(100));
      Assertions.assertEquals(3, calculate5(100));

      Assertions.assertEquals(3, calculate(101));
      Assertions.assertEquals(3, calculate2(101));
      Assertions.assertEquals(3, calculate3(101));
      Assertions.assertEquals(3, calculate4(101));
      Assertions.assertEquals(3, calculate5(101));

      Assertions.assertEquals(4, calculate(1000));
      Assertions.assertEquals(4, calculate2(1000));
      Assertions.assertEquals(4, calculate3(1000));
      Assertions.assertEquals(4, calculate4(1000));
      Assertions.assertEquals(4, calculate5(1000));

      Assertions.assertEquals(4, calculate(1223));
      Assertions.assertEquals(4, calculate2(1223));
      Assertions.assertEquals(4, calculate3(1223));
      Assertions.assertEquals(4, calculate4(1223));
      Assertions.assertEquals(4, calculate5(1223));
   }
   public static void main(String[] args) {
      test();
   }

   /**
    * 입력된 숫자의 자리수에 비례하는 반복문이 수행되며 입력된 숫자의 자리수를 k라고 할 때, 시간 복잡도는 O(k).
    *
    * 이 방식은 반복문을 사용하여 간단하고 직관적이다. 정수를 직접 연산하여 자리수를 계산하므로 추가적인 객체 생성이 필요하지 않다.
    * 따라서 성능면에서 효율이 좋다.
    * @param number
    * @return
    */
   public static int calculate(int number){
      int count=0;
      while (number>0){
         number /= 10;
         count++;
      }
      return count;
   }

   /**
    * 순수 배열을 이용해 풀이하는 방식.
    * 입력된 숫자를 순회하면서 각 자리수를 배열 digits에 기록한다.
    * 그리고 배열에서 0이 아닌 원소의 개수를 세어 자리수를 구한다.
    * 입력된 숫자의 자리수에 비례하는 시간 복잡도를 가지므로 시간 복잡도는 O(k)로 동일하다.
    *
    * 원리적으로 나머지를 이용한 방식과 동일하다. 그러나 굳이 배열을 사용하는 이유는 활용성 차원에서 보다 유연할 수 있기 때문이다.
    * 예를 들어, 배열에서 0이 아닌 원소의 위치를 찾거나 각 자리수의 합을 계산하는 등의 작업을 더 쉽게 수행할 수 있다.
    * 이러한 유연성은 특정한 자리수와 관련된 계산이 필요한 경우에 유용하다.
    * @param number
    * @return
    */
   public static int calculate2(int number) {
      if (number == 0) {
         return 1;
      }

      int[] digits = new int[10];
      int digit =0;
      // 숫자의 각 자리수를 배열에 기록
      while (number > 0) {
         digits[digit++]++;
         number /= 10;
      }

      // 배열에서 0이 아닌 원소의 개수가 자리수를 나타냄
      int count =0;
      for (int i = 0; i < digits.length; i++) {
         if (digits[i] != 0) {
            count++;
         }
      }
      return count;
   }


   /**
    * 입력된 숫자의 자리수에 따라 log10 값을 계산한 후 1을 더하여 자리수를 구한다.
    * 음수일 경우에는 절댓값을 취하여 계산한다.
    * 입력된 숫자의 자리수에 비례하는 시간 복잡도를 가지므로 시간 복잡도는 O(k).
    *
    * 추가적인 객체 생성이 필요하지 않다는 면에서 장점이 있다.
    * 다른 방식과 동일한 성능과 효율을 가지면서
    * 수학적인 계산을 활용하여 자리수를 판별하므로 보다 간결하고 직관적일 수 있다.
    * @param number
    * @return
    */
   public static int calculate3(int number) {
      if (number == 0) {
         return 1;
      } else {
         return (int) Math.log10(Math.abs(number)) + 1;
         // return (int) (Math.log10(number) + 1); // 절대값을 빼고 구해도 된다.
      }
   }


   /**
    * 숫자를 문자열로 변환하는 작업(String.valueOf(number))은 입력된 숫자의 자리수에 비례하며
    * 문자열의 길이를 계산하는 작업(length())은 상수 시간이 소요된다.
    * 따라서 입력된 숫자의 자리수를 k라고 할 때, 시간 복잡도는 O(k).
    *
    * 문자열로 변환하는 작업은 비용이 크고 추가적인 객체 생성을 필요로 한다.
    * 또한, 숫자를 문자열로 변환하고 길이를 계산하는 과정에서 많은 메모리를 사용할 수 있다.
    * 따라서 성능과 효율면에서는 나머지를 이용한 수학적인 방식보다 떨어진다.
    * @param number
    * @return
    */
   public static int calculate4(int number){
      return String.valueOf(number).length();
   }

   /**
    * Stream을 사용하여 입력된 숫자를 10으로 나누는 작업을 반복하며
    * 입력된 숫자의 자리수에 비례하여 반복하므로
    * 따라서 입력된 숫자의 자리수를 k라고 할 때, 시간 복잡도는 O(k).
    *
    * Stream을 사용하여 코드가 더욱 간결하고 함수형적인 스타일로 작성할 수 있다는 장점이 있다.
    * @param number
    * @return
    */
   public static long calculate5(int number) {
      return Stream.iterate(number, n -> n / 10)
         .takeWhile(n -> n > 0)
         .count();
   }

}
반응형