Question:
Write a program to find the n-th ugly number.
Ugly numbers are positive numbers whose prime factors only include 2, 3, 5.
For example, 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 is the sequence of the first 10 ugly numbers.
Note that 1 is typically treated as an ugly number.
简单地说就是找出第n个丑数。我们知道,1是第一个丑数,2是第二个丑数,3是第三个丑数,……,所以按照丑数的性质找出第n个丑数,第10个丑数是12。
最直接的算法就是,从1开始,对每一个数都判断一下,这个数是不是丑数,如果是的话,那么丑数的个数加1,直至第n个丑数。如何判断一个数是不是丑数,请看上一篇博客。伪代码如下:
GET-Nth-Ugly-Number(n)
1. if n <= 0
2. return -1
3. index = 0
4. num = 1
5. while true
6. isUglyNum = Is-Ugly-NUMBER(num)
7. if isUglyNum
8. index ++
9. if index == n
10. break
11. num ++
12. return num
这个算法看起来实在是太直接了,虽然能解决问题,但是时间效率不高。因为丑数远远比自然数要少,如第100个丑数是1536。如果要求第100个丑数,则while
的整个循环体需要循环1536次。当n更大时,这个循环的次数将会大大增加。
为了寻找更快的算法,我们需要另辟蹊径。不知道还记得斐波那契数列否?斐波那契数列的n个数的产生是依赖于第n-1
和n-2
个数的(n>=3)
。所以我们可以想这样的一个问题: 第n个丑数是否和之前的丑数相关?一个丑数其实就是若干个2、3和5相乘而得到的。也就说第n个丑数的因子在前面的丑数中肯定出现过。如第10个丑数12,它的因子6就是6个丑数。那么第n个丑数的产生由前哪个丑数来决定的呢?我们可以肯定的是,是由前面某的一个丑数乘以2,3或者5得到的。现在的关键问题是:我们如何得到一个排序好的丑数序列?
假设现在有一个已经排好序的丑数数组,其中最大的丑数M排在数组的最后一个位置。如果我们将这所有的丑数都乘以2,那么就会得到很多丑数,这些丑数有的比M大,有的比M小。我们假设第一个比M大的数为M2。同理,我们可以将所有的丑数乘以3和5,得到第一个比M大的数称为M3和M5。这样M2,M3和M5都比M大。取这三者之中的最小者,就是下一个丑数了。
实际上,我们每次在计算下一个丑数的时候,并不需要每次都将丑数全部乘以2,乘以3和乘以5。我们可以用L2、L3和L5来表示M2,M3和M5在丑数序列U中的位置。即U[L2]=M2, U[L3] = M3, U[L5] = M5。假设2*M2是三者中最小的,那么下一个丑数N=2*M2,此时L2 = L2 + 1,即L2向右挪了一个位置,L3和L5保持不变。那么在产生下下一个丑数的时候,由于3*M3,5*M5保持不变,依旧比N大。而L2向右挪了一个位置,我们知道,原来的丑数序列是排好序的,所以2*U[L2]比N大,所以三者的最小值依旧比N大,以此就产生了一个依次递增的丑数序列了。
基于这样的思路,我们可以写出下面的伪代码:
GET-Nth-Ugly-Number(n)
1. if n <= 0
2. return -1
3. index_two = index_three_index_five = 0
4. let UglyNum be an array of size n
5. UglyNum[0] = 1
6. for i = 1 to n -1
7. min = Min(UglyNum[index_two]*2, UglyNum[index_three]*3, UglyNum[index_five]*5)
8. UglyNum[i] = min
9. if min == UglyNum[index_two]*2
10. index_two ++
11. if min == UglyNum[index_three]*3
12. index_three ++
13. if min == UglyNum[index_five]*5
14. index_five ++
15. return UglyNum[n-1]
第1~2行是输入参数检测,如果是非正整数,则输出-1,说明这是一个无效值。第3行是初始化三个下标,分别指示M2,M3和M5
所在的位置,假设一刚开始是指向的都是第一个位置。第4行是初始化一个n维数组,这个数组用来保存这n个丑数的。6~14行是用来生成除了1之外的其他n-1个丑数。第7行是找出U2*2、U3*3
和U5*5
当中的最小值。第8行是将这个最小值最为第i个丑数。第9~14行是判断这个丑数是由之前的哪个丑数产生,并将其位置向右挪一位。以下使用Java和Swift分别实现的算法程序。
用Java语言实现这个算法:
public class Solution {
private int min(int a, int b, int c){
int min = a;
if(b < min) min = b;
if(c < min) min = c;
return min;
}
public int nthUglyNumber(int n) {
if(n <= 0) return 0;
int[] uglyArray = new int[n];
int twoIndex = 0;
int threeIndex = 0;
int fiveIndex = 0;
uglyArray[0] = 1;
for(int i = 1; i < n; i ++){
int ugly = min(uglyArray[twoIndex]*2, uglyArray[threeIndex]*3, uglyArray[fiveIndex]*5);
if (ugly == uglyArray[twoIndex]*2){
twoIndex ++;
}
if (ugly == uglyArray[threeIndex]*3){
threeIndex ++;
}
if (ugly == uglyArray[fiveIndex]*5){
fiveIndex ++;
}
uglyArray[i] = ugly;
}
return uglyArray[n-1];
}
}
用Swift语言实现这个算法
import Foundation
func min(a: Int, b: Int, c: Int) -> Int {
var min = a
if b < min {
min = b
}
if c < min {
min = c
}
return min
}
func nthUglyNumber(n: Int) -> Int {
if n <= 0 {
return -1
}
if n == 1 {
return 1
}
var twoIndex = 0;
var threeIndex = 0;
var fiveIndex = 0;
var uglyArray: [Int] = Array(count: n, repeatedValue: 0)
uglyArray[0] = 1
for i in 1...n-1 {
let arglNum = min(uglyArray[twoIndex]*2, b: uglyArray[threeIndex]*3, c: uglyArray[fiveIndex]*5)
uglyArray[i] = arglNum
if arglNum == uglyArray[twoIndex]*2 {
twoIndex += 1
}
if arglNum == uglyArray[threeIndex]*3 {
threeIndex += 1
}
if arglNum == uglyArray[fiveIndex]*5 {
fiveIndex += 1
}
}
return uglyArray[n-1]
}
题目来源:
LeetCode 算法题