일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Kernighan의 C언어 프로그래밍
- 친절한SQL튜닝
- 티스토리 쿠키 삭제
- 알파회계
- resttemplate
- 스프링 시큐리티
- ㅒ
- iterator
- 페이징
- 자바편
- 서버설정
- 자료구조와함께배우는알고리즘입문
- 코드로배우는스프링웹프로젝트
- 선형대수
- 스프링부트핵심가이드
- network configuration
- 이터레이터
- 코드로배우는스프링부트웹프로젝트
- 네트워크 설정
- 자료구조와 함께 배우는 알고리즘 입문
- 리눅스
- 처음 만나는 AI 수학 with Python
- baeldung
- 목록처리
- 처음 만나는 AI수학 with Python
- /etc/network/interfaces
- 데비안
- 구멍가게코딩단
- d
- GIT
- Today
- Total
bright jazz music
최대공약수와 최소공배수, 유클리드 호제법, 기약분수 본문
최대공약수(GCD, Greatest Common Divisor)
두 수의 공통된 약수 중 가장 큰 수를 의미한다. 예를 들어, 12와 18의 최대공약수는 6다.
- 12의 약수: 1, 2, 3, 4, 6, 12
- 18의 약수: 1, 2, 3, 6, 9, 18
- 공통된 약수: 1, 2, 3, 6 → 최대공약수는 6
최소공배수(LCM, Least Common Multiple)
두 수의 공통된 배수 중 가장 작은 수를 의미한다. 예를 들어, 12와 18의 최소공배수는 36이다.
- 12의 배수: 12, 24, 36, 48, ...
- 18의 배수: 18, 36, 54, ...
- 공통된 배수: 36, 72, ... → 최소공배수는 36
최소공배수와 최대공약수의 관계
두 수 a, b에 대하여 다음과 같은 관계가 성립한다:
LCM × GCD = a × b
유클리드 호제법
최대공약수를 구하는 가장 효율적인 알고리즘이다.
'호(互)'는 '서로'라는 뜻이고, '제(除)'는 '나누다'라는 뜻이다. 즉 '서로 나누는 방법'이라는 의미다. 두 수가 서로를 번갈아가며 나누어가는 과정이라서 이런 이름이 붙었다.
다음과 같은 원리로 동작한다:
- 두 수 a, b가 있을 때 (a > b), a를 b로 나눈 나머지를 r이라 다.
- b와 r의 최대공약수는 a와 b의 최대공약수와 같다.
- r이 0이 될 때까지 이 과정을 반복하며, 이때 나누는 수가 최대공약수가 된다.
// 최대공약수 (GCD) 구하기 - 유클리드 호제법
function getGCD(a, b) {
while (b !== 0) {
const temp = b;
b = a % b;
a = temp;
}
return a;
}
// 최소공배수 (LCM) 구하기
function getLCM(a, b) {
return (a * b) / getGCD(a, b);
}
// 사용 예시
const num1 = 12;
const num2 = 18;
const gcd = getGCD(num1, num2);
const lcm = getLCM(num1, num2);
console.log(`${num1}과 ${num2}의 최대공약수: ${gcd}`); // 6
console.log(`${num1}과 ${num2}의 최소공배수: ${lcm}`); // 36
/*
최대공약수를 구할 때, 반드시 제수(나누는 수)가 피제수(나눠지는 수)보다 작을 필요는 없다.
다만 제수가 피제수보다 큰 경우, 몫이 0이 되는 경우가 반드시 있으므로, 한 단계가 추가되어 비효율적이 된다.
[제수가 작은 경우]
36 = 24 × 1 + 12 // 1단계
24 = 12 × 2 + 0 // 2단계
-> 2단계 필요
[제수가 큰 경우]
24 = 36 × 0 + 24 // 1단계 (불필요)
36 = 24 × 1 + 12 // 2단계
24 = 12 × 2 + 0 // 3단계
-> 3단계 필요
그렇다면 최대공약수를 구하는 로직 이전에 크기를 비교하여 제수와 피제수를 상호교체하는 로직이 필요할까?
그렇지는 않을 것 같다.
/ 방법 1: swap 사용
function gcdWithSwap(a, b) {
if (a < b) {
[a, b] = [b, a]; // 메모리 공간 3개 필요 (임시 저장 공간 포함)
}
while (b !== 0) {
let r = a % b;
a = b;
b = r;
}
return a;
}
// 방법 2: 그냥 나누기
function gcdWithoutSwap(a, b) {
while (b !== 0) {
let r = a % b; // 메모리 공간 1개
a = b;
b = r;
}
return a;
}
swap 방식
장점: 불필요한 나눗셈 연산 1회 방지
단점: 추가 메모리 공간 필요, 조건문 검사 필요
그냥 나누기
장점: 코드 단순, 추가 메모리 불필요
단점: 불필요한 나눗셈 1회 발생 가능
위처럼 메모리 공간에 접근하여 값을 변경하는 비용보다 정수를 정수로 나누는 비용이 저렴하기 때문이다.
차라리 두 수가 반드시 정수임을 확인하는 로직을 추가하는 것이 나아보인다.
유클리드 호제법을 사용하지 않고 최대공약수를 구하는 방식을 생각해 보자
// 시간복잡도: O(N), N은 작은 수의 크기
function getGCD1(a, b) {
let gcd = 1;
for (let i = 1; i <= Math.min(a,b); i++) {
if (a%i===0 && b%i===0) gcd = i;
}
return gcd;
}
// 혹은 위처럼 함수를 분리하지 않고 사용한다면
for (let i = 1; i <= Math.min(a,b); i++) {
if (a%i===0 && b%i===0) n = i;
}
직관적이지만 수가 커졌을 때 순회 횟수가 증가하는 문제가 있다. 그에 비해 유클리드 호제법을 사용하면 순회 횟수를 극적으로 감소시킬 수 있다.
// 1071과 1029의 최대공약수 구하기
// 첫 번째 방법
- 1부터 1029까지 모든 수 확인 필요
- 1029번의 반복문 실행
// 유클리드 호제법
1071 = 1029 × 1 + 42
1029 = 42 × 24 + 21
42 = 21 × 2 + 0
- 단 3번의 연산으로 완료
유클리드는 어떻게 이 방법을 깨닫게 됐을까?
고대 그리스에서는 수를 선분의 길이로 표현하였다.
선분 A (길이 21)
|----------------------|
선분 B (길이 15)
|-----------------|
두 선분의 공통 측정 단위(공약수)를 찾기 위해, 긴 선분에서 짧은 선분을 반복적으로 빼보는 과정을 시도했을 것으로 추측
21에서 15를 한 번 뺌 → 6 남음
A(21): |----------------------|
B(15): |-----------------|
남은 길이(6): |------|
이제 15에서 6을 반복해서 뺌 → 3 남음
B(15): |-----------------|
6: |------|
6: |------|
남은 길이(3): |---|
6에서 3을 반복해서 뺌 → 0 남음
6: |------|
3: |---|
3: |---|
이 과정에서 마지막에 남은 길이 3이 두 선분을 정확히 측정할 수 있는 가장 큰 단위(최대공약수)가 된다는 것을 발견하였다.
21 = 3 × 7
15 = 3 × 5
이렇게 기하학적인 뺄셈 과정을 대수적인 나눗셈으로 바꾸어 표현한 것이 지금의 유클리드 호제법이 되었다. 선분의 길이를 비교하고 빼는 과정에서, 두 수의 최대공약수를 찾는 규칙성을 발견한 것으로 추측할 수 있다.
기약분수 구하기
분자와 분모의 최대공약수(GCD)를 구한다. 최대공약수는 두 수를 나눌 수 있는 최대의 값이다. 따라서 분자와 분모를 최대공약수로 나누면 분자와 분모는 서로소가 된다.
즉, 분자와 분모를 두 수의 최대공약수(GCD)로 나누면 기약분수를 만들 수 있다.
24/36을 기약분수로 만들기
1. 먼저 24와 36의 최대공약수를 구함
36 = 24 × 1 + 12
24 = 12 × 2 + 0
-> 최대공약수는 12
2. 분자와 분모를 12로 한 번 나눔
24 ÷ 12 = 2 (분자)
36 ÷ 12 = 3 (분모)
-> 2/3이 기약분수
3개 이상의 수의 최대공약수와 최소공배수를 구하는 경우
당황할 필요 없다. 먼저 임의의 두 수의 최대공약수를 구한다. 그렇게 계산된 최대공약수와 다른 수의 최대공약수를 구하면 된다.
예를 들어 [12, 18, 24]의 최대공약수를 구하는 경우, 12와 18의 최대공약수를 구하면 6이다. 그리고 다시 6과 24의 최대공약수를 구하면 된다. (이 역시 6이 된다)
GCD(12, 18, 24)의 경우:
1. GCD(12, 18) = 6
2. GCD(6, 24) = 6
LCM(12, 18, 24)의 경우:
1. LCM(12, 18) = 36
2. LCM(36, 24) = 72
// GCD:최대공약수, LCM:최소공배수
가독성을 위해 함수에 관한 코드를 아래 배치하였다.
// 두 수의 GCD
function getGCD(a, b) {
while (b !== 0) {
let r = a % b;
a = b;
b = r;
}
return a;
}
// 두 수의 LCM
function getLCM(a, b) {
return (a * b) / getGCD(a, b);
}
// 여러 수의 GCD
function getMultipleGCD(numbers) { // 배열이 매개변수로 입력됨
return numbers.reduce((acc, cur) => getGCD(acc, cur));
// 초깃값이 설정돼 있지 않으므로, 최초에는 입력 배열의 첫 번째 원소가 초깃값으로 설정됨
// 만약 [12, 18, 24]라면 최초에는 12가 초깃값으로 설정되며, cur는 18이 된다.
// 왜 cur가 12가 아니고 18이냐고?
// 자바스크립트의 reduce 함수는 초기값이 없을 때:
// acc는 자동으로 배열의 첫 번째 요소(0번 인덱스)로 설정된다.
// cur는 자동으로 배열의 두 번째 요소(1번 인덱스)부터 설정된다.
// 즉, 초깃값을 명시적을 설정했다면 cur가 0번 인덱스부터 시작했겠지만,
// 여기서는 초깃값을 설정하지 않았으므로 cur가 1번 인덱스부터 시작하는 것이다.
// 따라서 acc는 1번 원소(18)와의 연산을 통해 6으로 교체되고, 2번 원소(24)와의 연산을 통해 다시 6으로 교체된다.
// 리듀스 함수는 '누적' 또는 '교체'의 연산을 통해 '하나의 값(단일값)'을 구하는 데 사용되는데,
// 주로 누적값을 찾는데 사용되지만 여기서는 최종 교체된 값을 찾는 데 사용된 것이다.
}
// 여러 수의 LCM
function getMultipleLCM(numbers) {
return numbers.reduce((acc, cur) => getLCM(acc, cur));
}
// 사용 예시
const numbers = [12, 18, 24];
console.log(getMultipleGCD(numbers)); // 6
console.log(getMultipleLCM(numbers)); // 72
'Math > 기초수학' 카테고리의 다른 글
기초수학: 10. LaTex 수식 표기법(주피터 노트북 사용 + 무설치) (0) | 2022.07.20 |
---|---|
기초수학 : 9. 절댓값 (0) | 2022.07.19 |
기초수학 : 8. 난수 (0) | 2022.07.18 |
기초수학 : 7. 총곱 (0) | 2022.07.17 |
기초수학 : 6. 총합 (0) | 2022.07.17 |