Monday, October 19, 2009

赖勇浩:从一道笔试题谈算法优化

从一道笔试题谈算法优化(上)
作者:赖勇浩(http://blog.csdn.net/lanphaday)

引子
每年十一月各大IT公司都不约而同、争后恐后地到各大高校进行全国巡回招聘。与此同时,网上也开始出现大量笔试面试题;网上流传的题目往往都很精巧,既能让考查基础知识,又在平淡中隐含了广阔的天地供优秀学生驰骋。

这两天在网上淘到一道笔试题目(注1),虽然真假未知,但的确是道好题,题目如下:

从10亿个浮点数中找出最大的1万个。

这是一道似易实难的题目,一般同学最容易中的陷阱就是没有重视这个"亿"字。因为有10亿个单精度浮点数元 素的数组在32位平台上已经达到3.7GB之巨,在常见计算机平台(如Win32)上声明一个这样的数组将导致堆栈溢出。正确的解决方法是分治法,比如每 次处理100万个数,然后再综合起来。不过这不是本文要讨论的主旨,所以本文把上题的10亿改为1亿,把浮点数改为整数,这样可以直接地完成这个问题,有 利于清晰地讨论相关算法的优化(注2)。

不假思索
拿到这道题,马上就会想到的方法是建立一个数组把1亿个数装起来,然后用for循环遍历这个数组,找出最大的1万个数来。原因很简单,因为如果要找出最大的那个数,就是这样解决的;而找最大的1万个数,只是重复1万遍而已。

template< class T >
void solution_1( T BigArr[], T ResArr[] )
{
for( int i = 0; i < RES_ARR_SIZE; ++i )
{
int idx = i;
for( int j = i+1; j < BIG_ARR_SIZE; ++j )
{
if( BigArr[j] > BigArr[idx] )
idx = j;
}
ResArr[i] = BigArr[idx];
std::swap( BigArr[idx], BigArr[i] );
}
}

设BIG_ARR_SIZE = 1亿,RES_ARR_SIZE = 1万,运行以上算法已经超过40分钟(注3),远远超过我们的可接受范围。

稍作思考
从上面的代码可以看出跟SelectSort算法的核心代码是一样的。因为SelectSort是一个O(n^2)的算法(solution_1的时间复杂度为O(n*m),因为solution_1 没有将整个大数组全部排序),而我们又知道排序算法可以优化到O(nlogn),那们是否可以从这方面入手使用更快的排序算法如MergeSor、 QuickSort呢?但这些算法都不具备从大至小选择最大的N个数的功能,因此只有将1亿个数按从大到小用QuickSort排序,然后提取最前面的1 万个。

template< class T, class I >
void solution_2( T BigArr[], T ResArr[] )
{
std::sort( BigArr, BigArr + BIG_ARR_SIZE, std::greater_equal() );
memcpy( ResArr, BigArr, sizeof(T) * RES_ARR_SIZE );
}

因为STL里的sort算法使用的是QuickSort,在这里直接拿来用了,是因为不想写一个写一个众人皆知的QuickSort代码来占篇幅(而且STL的sort高度优化、速度快)。

对solution_2进行测试,运行时间是32秒,约为solution_1的1.5%的时间,已经取得了几何数量级的进展。

深入思考
压抑住兴奋回头再仔细看看solution_2,你将发现一个大问题,那就是在solution_2里所有的元素都排序了!而事实上只需找出最大的1万个即可,我们不是做了很多无用功吗?应该怎么样来消除这些无用功?

如 果你一时没有头绪,那就让我慢慢引导你。首先,发掘一个事实:如果这个大数组本身已经按从大到小有序,那么数组的前1万个元素就是结果;然后,可以假设这 个大数组已经从大到小有序,并将前1万个元素放到结果数组;再次,事实上这结果数组里放的未必是最大的一万个,因此需要将前1万个数字后续的元素跟结果数 组的最小的元素比较,如果所有后续的元素都比结果数组的最小元素还小,那结果数组就是想要的结果,如果某一后续的元素比结果数组的最小元素大,那就用它替 换结果数组里最小的数字;最后,遍历完大数组,得到的结果数组就是想要的结果了。

template< class T >
void solution_3( T BigArr[], T ResArr[] )
{
//取最前面的一万个
memcpy( ResArr, BigArr, sizeof(T) * RES_ARR_SIZE );
//标记是否发生过交换
bool bExchanged = true;
//遍历后续的元素
for( int i = RES_ARR_SIZE; i < BIG_ARR_SIZE; ++i )
{
int idx;
//如果上一轮发生过交换
if( bExchanged )
{
//找出ResArr中最小的元素
int j;
for( idx = 0, j = 1; j < RES_ARR_SIZE; ++j )
{
if( ResArr[idx] > ResArr[j] )
idx = j;
}
}
//这个后续元素比ResArr中最小的元素大,则替换。
if( BigArr[i] > ResArr[idx] )
{
bExchanged = true;
ResArr[idx] = BigArr[i];
}
else
bExchanged = false;
}
}

上 面的代码使用了一个布尔变量bExchanged标记是否发生过交换,这是一个前文没有谈到的优化手段――用以标记元素交换的状态,可以大大减少查找 ResArr中最小元素的次数。也对solution_3进行测试一下,结果用时2.0秒左右(不使用bExchanged则高达32分钟),远小于 solution_2的用时。

深思熟虑
在进入下一步优化之前,分析一下solution_3的成功之处。第一、 solution_3的算法只遍历大数组一次,即它是一个O(n)的算法,而solution_1是O(n*m)的算法,solution_2是 O(nlogn)的算法,可见它在本质上有着天然的优越性;第二、在solution_3中引入了bExchanged这一标志变量,从测试数据可见引入 bExchanged减少了约99.99%的时间,这是一个非常大的成功。

上面这段话绝非仅仅说明了solution_3的优点,更重要 的是把solution_3的主要矛盾摆上了桌面――为什么一个O(n)的算法效率会跟O(n*m)的算法差不多(不使用bExchanged)?为什么 使用了bExchanged能够减少99.99%的时间?带着这两个问题再次审视solution_3的代码,发现bExchanged的引入实际上减少 了如下代码段的执行次数:

for( idx = 0, j = 1; j < RES_ARR_SIZE; ++j )
{
if( ResArr[idx] > ResArr[j] )
idx = j;
}

上 面的代码段即是查找ResArr中最小元素的算法,分析它可知这是一个O(n)的算法,到此时就水落石出了!原来虽然solution_3是一个O(n) 的算法,但因为内部使用的查找最小元素的算法也是O(n)的算法,所以就退化为O(n*m)的算法了。难怪不使用bExchanged使用的时间跟 solution_1差不多;这也从反面证明了solution_3被上面的这一代码段导致性能退化。使用了bExchanged之后因为减少了很多查找 最小元素的代码段执行,所以能够节省99.99%的时间!

至此可知元凶就是查找最小元素的代码段,但查找最小元素是必不可少的操作,在这 个两难的情况下该怎么去优化呢?答案就是保持结果数组(即ResArr)有序,那样的话最小的元素总是最后一个,从而省去查找最小元素的时间,解决上面的 问题。但这也引入了一个新的问题:保持数组有序的插入算法的时间复杂度是O(n)的,虽然在这个问题里插入的数次比例较小,但因为基数太大(1亿),这一 开销仍然会令本方案得不偿失。

难道就没有办法了吗?记得小学解应用题时老师教导过我们如果解题没有思路,那就多读几遍题目。再次审题,注意到题目并没有要求找到的最大的1万个数要有序(注4),这意味着可以通过如下算法来解决:

1) 将BigArr的前1万个元素复制到ResArr并用QuickSort使ResArr有序,并定义变量MinElemIdx保存最小元素的索引,并定义变量ZoneBeginIdx保存可能发生交换的区域的最小索引;

2) 遍历BigArr其它的元素,如果某一元素比ResArr最小元素小,则将ResArr中MinElemIdx指向的元素替换,如果ZoneBeginIdx == MinElemIdx则扩展ZoneBeginIdx;

3) 重新在ZoneBeginIdx至RES_ARR_SIZE元素段中寻找最小元素,并用MinElemIdx保存其它索引;

4) 重复2)直至遍历完所有BigArr的元素。

依上算法,写代码如下:

template< class T, class I >
void solution_4( T BigArr[], T ResArr[] )
{
//取最前面的一万个
memcpy( ResArr, BigArr, sizeof(T) * RES_ARR_SIZE );
//排序
std::sort( ResArr, ResArr + RES_ARR_SIZE, std::greater_equal() );
//最小元素索引
unsigned int MinElemIdx = RES_ARR_SIZE - 1;
//可能产生交换的区域的最小索引
unsigned int ZoneBeginIdx = MinElemIdx;
//遍历后续的元素
for( unsigned int i = RES_ARR_SIZE; i < BIG_ARR_SIZE; ++i )
{
//这个后续元素比ResArr中最小的元素大,则替换。
if( BigArr[i] > ResArr[MinElemIdx] )
{
ResArr[MinElemIdx] = BigArr[i];
if( MinElemIdx == ZoneBeginIdx )
--ZoneBeginIdx;
//查找最小元素
unsigned int idx = ZoneBeginIdx;
unsigned int j = idx + 1;
for( ; j < RES_ARR_SIZE; ++j )
{
if( ResArr[idx] > ResArr[j] )
idx = j;
}
MinElemIdx = idx;
}
}
}
经过测试,同样情况下solution_4用时约1.8秒,较solution_3效率略高,总算不负一番努力。
待续……

Thursday, October 15, 2009

FFT in Matlab, from WIKI

fft - Discrete Fourier transform

Syntax

Y = fft(X)
Y = fft(X,n)
Y = fft(X,[],dim)
Y = fft(X,n,dim)

Definition

The functions Y=fft(x) and y=ifft(X) implement the transform and inverse transform pair given for vectors of length by:

where

is an th root of unity.

Description

Y = fft(X) returns the discrete Fourier transform (DFT) of vector X, computed with a fast Fourier transform (FFT) algorithm.

If X is a matrix, fft returns the Fourier transform of each column of the matrix.

If X is a multidimensional array, fft operates on the first nonsingleton dimension.

Y = fft(X,n) returns the n-point DFT. If the length of X is less than n, X is padded with trailing zeros to length n. If the length of X is greater than n, the sequence X is truncated. When X is a matrix, the length of the columns are adjusted in the same manner.

Y = fft(X,[],dim) and Y = fft(X,n,dim) applies the FFT operation across the dimension dim.

Examples

A common use of Fourier transforms is to find the frequency components of a signal buried in a noisy time domain signal. Consider data sampled at 1000 Hz. Form a signal containing a 50 Hz sinusoid of amplitude 0.7 and 120 Hz sinusoid of amplitude 1 and corrupt it with some zero-mean random noise:

Fs = 1000;                    % Sampling frequency T = 1/Fs;                     % Sample time L = 1000;                     % Length of signal t = (0:L-1)*T;                % Time vector % Sum of a 50 Hz sinusoid and a 120 Hz sinusoid x = 0.7*sin(2*pi*50*t) + sin(2*pi*120*t);  y = x + 2*randn(size(t));     % Sinusoids plus noise plot(Fs*t(1:50),y(1:50)) title('Signal Corrupted with Zero-Mean Random Noise') xlabel('time (milliseconds)')

It is difficult to identify the frequency components by looking at the original signal. Converting to the frequency domain, the discrete Fourier transform of the noisy signal y is found by taking the fast Fourier transform (FFT):

NFFT = 2^nextpow2(L); % Next power of 2 from length of y Y = fft(y,NFFT)/L; f = Fs/2*linspace(0,1,NFFT/2+1);  % Plot single-sided amplitude spectrum. plot(f,2*abs(Y(1:NFFT/2+1)))  title('Single-Sided Amplitude Spectrum of y(t)') xlabel('Frequency (Hz)') ylabel('|Y(f)|')

The main reason the amplitudes are not exactly at 0.7 and 1 is because of the noise. Several executions of this code (including recomputation of y) will produce different approximations to 0.7 and 1. The other reason is that you have a finite length signal. Increasing L from 1000 to 10000 in the example above will produce much better approximations on average.

Algorithm

The FFT functions (fft, fft2, fftn, ifft, ifft2, ifftn) are based on a library called FFTW [3],[4]. To compute an -point DFT when is composite (that is, when ), the FFTW library decomposes the problem using the Cooley-Tukey algorithm [1], which first computes transforms of size , and then computes transforms of size . The decomposition is applied recursively to both the - and -point DFTs until the problem can be solved using one of several machine-generated fixed-size "codelets." The codelets in turn use several algorithms in combination, including a variation of Cooley-Tukey [5], a prime factor algorithm [6], and a split-radix algorithm [2]. The particular factorization of is chosen heuristically.

When is a prime number, the FFTW library first decomposes an -point problem into three ( )-point problems using Rader's algorithm [7]. It then uses the Cooley-Tukey decomposition described above to compute the ( )-point DFTs.

For most , real-input DFTs require roughly half the computation time of complex-input DFTs. However, when has large prime factors, there is little or no speed difference.

The execution time for fft depends on the length of the transform. It is fastest for powers of two. It is almost as fast for lengths that have only small prime factors. It is typically several times slower for lengths that are prime or which have large prime factors.

Friday, September 25, 2009

g++ extra qualification

很多比较旧的代码会有如写法
class Foo
{

int Foo::Foo(void);

}
在g++ 4.1以后会报错 extra qualification
直接改为
class Foo
{

int Foo(void);

}
即可

The author`s blog: http://hi.baidu.com/zjugator/blog/item/77bb6cec02bee22163d09f7d.html

Sunday, September 13, 2009

GCC configureation

gcc App.o -o App -L/usr/local/lib -L/usr/lib -L/usr/lib64 -DNO_SDL_MAIN -lz -lzip -lzipd -ldl -lX11 -lXext -lpthread -lGLU -lGL -ljpeg -lpng -lavformat -lavcodec -lavutil -lavdevice -lSDL -lG3D -lGLG3D -lG3Dd -lGLG3Dd -lm

这是GCC编译G3D例子starter的输入!看上去确实吓人,在刚接触GCC的时候,这个东西几乎让我疯狂了!在抓狂了一周后,今天凌晨终于解决了编译和链接的问题!现在谈谈这两天的感觉。希望抛砖引玉!
1. Linux下的library 和header都是搜索式的,比如有两个路径,/usr/local/lib和/lib,里面分别有一个叫G3D的header,当你使用linux下的编译器时,比如gcc,他会先搜索一个路径,在搜索另一个路径,先到先的,这个顺序是取决于你的输入顺序或设置的,一些新安装的库可能需要你对输入做一定的排序才能正确使用。G3D的安装过程中,我就犯了一个这样的错误,/usr/local/lib首先搜索,其下的libzipd.a没有更新,所以不管我怎么修改配置,重新编译,更换版本都无济于事,最后的解决办法很简单,要么删除/usr/local/lib下的库或者将新编译的库文件设置到较高的搜索优先级。Windows下就相对简单了,他们是登记式的,或者是更细化的搜索,你只要在注册表,环境变量或编译器中登记要使用的类库,虽然这个过程linux也有,但windows是没有固定的/lib和/include目录的,就是没有默认搜索,所以windows这个过程更直观一些。
2.“Undefined reference to .......” in link step. 这个问题通常因为library没有找到,比如你安装library的路径没有被编译器搜索,而include却被搜索到了,所以就出现了编译成功而链接出错!这里更需要注意的是gcc与window的错误信息是有区别的,在windows上,undefind refernce 肯定出现在编译阶段,链接阶段会说undefind symbol...如果你习惯了vc的错误信息,很容易把gcc link的错误当作编译的错误,所以不管你怎么改,都是不会有效的......
3.gcc的库是搜索式的,所以你不需要定义库的位置,只需要定义库的名字,如果你的安装路径不是默认的,你还需要设定一个搜索路径,gcc会一个接一个的搜索,当然,这里也会有谁先谁后的问题!library的命名方式是lib开头,然后是库的名字,最后的后缀是lib。gcc输入这是-l开头,紧跟着是lib的名字。比如,我向gcc传入 -lG3D, 在../lib中,他应该叫做libG3D.lib。最后用-lm表示library输入结束,而且要在最后!-L说明搜索的路径,FIFS!

Tuesday, September 1, 2009

Least Squares Approximation

Suppose you are in a science class and you receive these instructions:

Find the temperature of the water (in degrees Celsius) at the times 1, 2, 3, 4, and 5 seconds after you have applied heat to the container. Conduct your experiment carefully. Graph each data point with time on the x-axis and temperature on the y-axis. Your data should follow a straight line. Find the equation of this line.

The data from the experiment looks like this when charted and graphed:

graph


Notice that our data points don't fall exactly on a straight line as they were supposed to, so how are we going to find the slope and intercept of the line?

This is a common problem with experimental sciences because the data points that we measure seldom fall on a straight line. Therefore, scientists try to find an approximation. In this case, they would try to find the line that best fits the data in some sense. The first problem is to define "best fit." It is convenient to define an error as a distance from the actual value of y for x (the value that was measured in the experiment) to the predicted value of y for x. Therefore, it seems reasonable that the "best fit" line would somehow minimize the errors, but how? You could minimize the sum of the absolute values of the errors; this is called the l1 fit. It would also be reasonable to find the biggest error for each line and choose the line that minimizes this quantity; this is called the l infinity fit. However, the fit that is used most often is the l2 fit which is called the least squares fit. This method is called the least squares fit because it finds the line that minimizes the sum of the squares of the errors. Gauss developed this method to solve a problem when he was a young man (about the age of a high-school senior) to help his friend solve a chemistry problem. This is the fit that is most often used because it is the only one that can be found by solving a system of linear equations.

You have just read a lot of new information, so let's illustrate the concepts with our example. We have the graph of the data above. Now we need to guess which line best fits our data. If we assume that the first two points are correct and choose the line that goes through them, we get the line y = 1 + x. If we substitute our points into this equation, we get the following chart. The points and line are graphed below.

chart


Therefore, the sum of the squares of the errors is 27. Do you think that we can do better than this?

If we choose the line that goes through the points when x = 3 and 4, we get the line y = 4 + x. Will we get a better fit? Let's look at it.

chart

The sum of the squares of the error is 18. That is a better fit, but can we do even better?

Let's try the line that is half way between these two lines. The equation would be y = 2.5 + x. It looks like this:

chart


The sum of the squares of the error is 11.25 with this line, so this is the best line yet. Can we do better? It doesn't seem very scientific or efficient to keep guessing at which line would give the best fit. Surely there is a methodical way to determine the best fit line. Let's think about what we want.

A line in slope-intercept form looks like c0 + c1x = y where c0 is the y -intercept and c1 is the slope. We want to find c0 and c1 such that c0 + c1xi = yi is true for all our data points:

c0 + 1c1   =   2
c0 + 2c1   =   3
c0 + 3c1   =   7
c0 + 4c1   =   8
c0 + 5c1   =   9


We know that there may not exist c0 and c1 that fit all these equations, so we try to find the best fit. We can write these equations in the form Xc = y (these are just new letters for our familiar equation Ax = b ) where

matrices

In general, we cannot solve this system because the system is usually inconsistent because it is overdetermined. In other words, we have more equations than unknowns (the unknowns are the two variables, c0 and c1, for which we are trying to solve). There is a system of equations called the normal equations that can be used to find least squares solution to systems with more equations than unknowns.

Theorem 8.1 Let X be an m by n matrix such that XTX is invertible, then the soltution to the normal equations, XTXc = XTy, is the least squares approximation to c in Xc = y.

Remark 25 It is important to remember that the solution to the normal equations is only an approximation to c for Xc = y. It is not equal to c because Xc = y is inconsistent, so it has no solution. In other words, there does not exist a vector, c, that makes Xc = y a true statement. Therefore, we use the normal equations to approximate c.

Remark 26 For now, you don't need to check to see if XTX is invertible because most of the systems that we encounter will meet this requirement. However, if you cannot find a solution to the normal equations, you should check to see if XTX is invertible.

The normal equations will give us the "best fit" line (or curve) every time according to the way we defined "best fit." The proof of this is at the end of this chapter. Let's try applying the normal equations to our system. First, we multiply so that we have a system that we can solve.

matrices


Now we can work with the augmented matrix and use Gauss-Jordan elimination to find the solution of the normal equations. This solution will be the coefficients of the line which give the best fit in the least squares sense.

matrices


When we graph and chart the line y = 0.1 + 1.9x, we get:

chart

The sum of the squares of the error is 2.7. This is a great improvement over our guesses and we know that we cannot do any better. In general, if we have n data points, we solve XTXc = XTy with

matrices, matrices and matrix
The ellipse marks (written as dots or dots tell you to continue in the same pattern.

What if we are told that our data is not supposed to fit a straight line, but instead falls in the shape of a parabola? Consider the following data from another experiment:

chart

We can find the curve that best fits our data in a similar manner. The general equation for a parabola is c0 + c1x + c2x2 = y. Therefore, we want to find the values of the coefficients, c1 c2, and c3, so that the curve we find best fits these equations:

c0 - 1c1 + 1c2   =   3
c0 + 0c1 + 0c2   =   1
c0 + 1c1 + 1c2   =   -1
c0 + 2c1 + 4c2   =   1
c0 + 3c1 + 9c2   =   3


Let us use the normal equations with matrix matrix and column vector

matrices


Now we can augment the matrix and solve using Gaussian elimination.

matrices


Back-substitution yields the coefficients

coefficients


These coefficients indicate that the curve we want is equation Let's graph this curve and fill in our chart:

chart


errors

We find that the sum of the squared errors is 32/35 Using our definition of least squares "best fit," you will not be able to find a parabola that fits the data better than this one. In general, to find the parabola that best fits the data, you use the normal equations XTXc = XTy with

matrices

Notice that the normal equations used to find the best fit line and the best fit parabola have the same form. Do you think that we could expand this to higher degree polynomials? Yes, we can. In general, we use the normal equations XTXc = XTy with

matrices

where m represents the degree of the polynomial curve that you wish to fit and n represents the number of data points. The least squares "best fit" curve for these equations is c0 + c1x +c2x2 + … + cm - 1xm - 1 + cmxm. Remember that the degree is the highest power of the variable in your equation. A line is a first degree polynomial and a parabola is a second degree polynomial.

If we can find the best fit curve for any degree polynomial, why don't we always use a higher degree polynomial and fit the data better? After all, if we have n data points and fit them to a polynomial of degree n - 1, we will have a perfect fit every time because our systems would not be inconsistent. However, our goal is not just to find a curve that fits the data closely. Usually, we want the curve to predict what would happen between our data points. If we choose a curve that exactly fits all our data points, we are incorporating the error in our measurements into our model unless the model fits the data exactly (which occurs only rarely). Unfortunately, there is no set rule for deciding what degree polynomial should be used to fit the data. However, first and second degree polynomials provide the simplest models and should fit most of your data until you start modeling more complicated systems.

If you notice, we said that we usually fit a curve so that we can predict what would happen between our data points. Predicting an outcome between data points is called interpolation. Why didn't we say anything about predicting the behavior beyond our data points? Predicting an outcome beyond the data is called extrapolation. It is usually dangerous to extrapolate much beyond the data because we have no indication that the data will continue to follow the same curve since our curve was only fit to the data. For example, we measured the height of a teenage boy every year for a few years and charted his growth. The growth appeared linear, so we fit a line to the data and got y = 32 + 2.25x. We have graphed the data with age on the x-axis and height on the y-axis.

data

If we extrapolate back several years, this young man was over two and a half feet tall when he was born. According to this model, he will never stop growing, so he will be 8 feet 4 inches tall by the time he is 30 and almost 14 feet tall by the time he is 60. Do you think that this is an accurate prediction?

If the temperature at the airport on the 4th of July was in the 90's for two years in a row, would it be reasonable to predict that the temperature in January between those years was also in the 90's? No, it would not. We have two problems with this model. One problem is that we only have 2 data points. You can always find a line that fits the two points, but there is no reason to believe that the relationship between the day of the year and the temperature is a linear relationship. Also, we didn't take into account other factors that could affect our model such as the pattern of the seasons. These are problems that can arise when you model a situation. When we start modeling situations and using least squares to make predictions, we are entering the world of statistics. That means that we must think about what the data represents rather than just apply the normal equations. There are many interesting applications of statistics that you can explore in another course. However, using matrices, you already know one way to find a "best fit" curve for your data.