对于算法的分析需要用到一些渐进记号(O,Ω,Θ,o)T(N) = O(f(N))表示T(N)的增长率小于等于f(N)的增长率;T(N) = Ω(f(N))表示T(N)的增长率大于等于f(N)的增长率;T(N) = Θ(f(N))表示T(N)的增长率等于f(N)的增长率;T(N) = o(f(N))表示T(N)的增长率小于f(N)的增长率。
书上有一个法则3我觉得应该记一下:
对于任意常数k,logkN = O(N),它告诉我们对数增长的非常缓慢。
为了比较两个函数的增长率,我们可以通过做比值取极限的方法,当两个都趋于无穷大时,还可以应用洛必达法则。
由于书中对递归的算法时间分析有点粗糙,或者说有点复杂。我这里借鉴了《算法导论》中“主方法”的方法,摘要如下:
设a>=1和b>1为常数,设f(n)为一函数,T(n)由递归式T(n) = aT(n/b) + f(n)对非负整数定义,其中n/b指n/b向下或者向上取整,那么T(n)可能有如下渐进界:
1)若对于某常数ε>0,有f(n) = O(nlogba-ε),则T(n) =Θ(nlogba)
2)若f(n) =Θ(nlogba),则T(n) =Θ(nlogbalgn)
3)若对某常数ε>0,有f(n) =Ω(nlogba+ε),且对常数c<1与所有足够大的n,有af(n/b)<=cf(n),则T(n) =Θ(f(n))
上面三种情况中,都把函数f(n)和nlogba进行比较,直觉上是解由两个函数中较大的那个决定。若两个一样大,则乘以对数因子lgn。
这一章中分析了几个问题:
最大子序列和的问题:有四种方法来解决,O(n^3),O(n^2),O(nlogn)和O(n)的方法,解法也可以参见《编程珠玑》第八章和《编程之美》的2.14节。个人觉得后两种方法真的非常巧妙,特别是使用递归的方法。
int MaxSubSequenceSum1(int arr[], int N)
{
int sum = 0, max = 0;
for (int i=0; i<=N-1; ++i) {
for (int j=i; j<=N-1; ++j) {
for (int k=i; k<=j; ++k) {
sum += arr[k];
}
if (sum >max)
max = sum;
sum = 0;
}
}
return max;
}
int MaxSubSequenceSum2(int arr[], int N)
{
int sum = 0, max = 0;
for (int i=0; i<=N-1; ++i) {
for (int j=i; j<=N-1; ++j) {
sum += arr[j];
if (sum>max)
max = sum;
}
sum = 0;
}
return max;
}
int MaxSubSequenceSum3(int arr[], int left, int right)
{
if (left==right) {
if (arr[left]>0)
return arr[left];
else
return 0;
}
int center = left + ((right - left)>>1);
int maxLeft = MaxSubSequenceSum3(arr, left, center);
int maxRight = MaxSubSequenceSum3(arr, center+1, right);
int maxLeftBoardSum = 0, leftBoard = 0;
for (int i=center; i>=left; --i) {
leftBoard += arr[i];
if (leftBoard>maxLeftBoardSum)
maxLeftBoardSum = leftBoard;
}
int maxRightBoardSum = 0, rightBoard = 0;
for (int i=center+1; i<=right; ++i) {
rightBoard += arr[i];
if (rightBoard>maxRightBoardSum)
maxRightBoardSum = rightBoard;
}
int maxCenter = maxLeftBoardSum + maxRightBoardSum;
int temp = maxLeft>maxRight?maxLeft:maxRight;
return temp>maxCenter?temp:maxCenter;
}
int MaxSubSequenceSum4(int arr[], int N)
{
int max = 0, tempSum = 0;
for (int i=0; i<N; ++i) {
tempSum += arr[i];
if (tempSum>max)
max = tempSum;
else if (tempSum<0)
tempSum = 0;
}
return max;
}
对二分查找问题:有非递归和递归两种写法,用递归写法对计算时间复杂度比较直观(O(lgn))。
int BinarySearch1(int arr[], int N, int x)
{
int low = 0, high = N-1, mid;
while (low<=high) {
mid = low + ((high-low)>>1);
if (arr[mid]==x)
return mid;
else if (arr[mid]>x)
high = mid - 1;
else
low = mid +1;
}
return -1;
}
int BinarySearch2(int arr[], int low, int high, int x)
{
if (low<=high) {
int mid = low + ((high-low)>>1);
if (arr[mid]==x)
return mid;
else if (arr[mid]>x)
return BinarySearch2(arr, low, mid-1, x);
else
return BinarySearch2(arr,mid+1, high, x);
} else
return -1;
}
GCD问题:在《算法导论》31.2节有详细的说明,用GCD递归定理:GCD(a,b) = GCD(b,a%b)就可以编写精巧的程序。时间复杂度O(lgb)
int GCD(int m, int n)
{
if (n==0)
return m;
else
return GCD(n, m%n);
}
幂运算问题:又是对递归的经典运用。
long int Pow(int x, int N)
{
if (N==0)
return 1;
if (N==1)
return x;
if ((N&1)==1)
return Pow(x*x, N/2)*x;
else
return Pow(x*x, N/2);
}
对于幂运算特别注意Pow(x*x, N/2)不能写成Pow(x, N/2)* Pow(x, N/2),那样就违反了递归的合成效益法则的原则了。
下面给出课后习题的部分解答
2.7有关生成前N个自然数的一个随机置换
书上给出了三种方法,但有用的是第三种
1.为了填入A[i],不断生成随机数直到它不同于已经生成的A[0…i-1]
int RandInt(int i, int j)
{
int result = rand()%(j-i+1)+i;
return result;
}
void RandGen1(int arr[], int N)
{
int i = 1, j;
bool flag = true;
int temp = RandInt(1, N);
arr[0] = temp;
while (i<N) {
flag = true;
while (flag) {
temp = RandInt(1, N);
for (j=0; j<=i-1; ++j)
if (arr[j]==temp)
break;
if (j==i) {
flag = false;
arr[i] = temp;
}
}
++i;
}
}
2.使用一个used数组标记这个随机数否被生产过,若产生过则重新生成
void RandGen2(int arr[], int N)
{
int *used = new int[N+1];
memset(used, 0, sizeof(int)*(N+1));
int i=0;
while (i<N) {
int temp = RandInt(1, N);
if (used[temp]==0) {
arr[i] = temp;
used[temp] = 1;
++i;
}
}
delete[] used;
}
3.先生成一个顺序的排列,然后通过交换打乱排列
void RandGen3(int arr[], int N)
{
for (int i=0; i<N; ++i)
arr[i] = i + 1;
for (int i=0; i<N; ++i)
swap(arr[i], arr[RandInt(i,N-1)]);
}
这种方法在《算法导论》60页上有证明,证明它这样生成的一定是随机数。
2.11给出一个有效的算法来确定在整数A1<A2<…<AN的数组中是否存在整数i,使得Ai=i。
由于已经排好序了,可以用二分法来降低复杂度。如果Amid = mid,那正好存在这个整数,
如果Amid < mid,说明这个整数要存在的话只可能在Amid之后的部分(因为A[1…N]均不相等),如果Amid > mid,说明这个整数要存在的话只可能在Amid之前的部分。
#include <iostream>
using namespace std;
#define N 5
int BinarySearch(int arr[], int low, int high)
{
if (low<=high) {
int mid = low + ((high-low)>>1);
if (arr[mid]==mid+1)
return mid+1;
if (arr[mid]>mid+1)
return BinarySearch(arr, low, mid-1);
else
return BinarySearch(arr, mid+1, high);
} else
return -1;
}
int main(int argc, char **argv)
{
int arr[N] = {-1,1,3,5,6};
cout<<BinarySearch(arr, 0, N-1)<<endl;
system("pause");
return 0;
}
2.12
求最小子序列的和
这个应该和求最大子序列的和一样的。
int MinSubSeqSum(int arr[], int N)
{
int min = 0, sum = 0;
for (int i=0; i<N; ++i) {
sum += arr[i];
if (sum<min)
min = sum;
else if (sum>0)
sum = 0;
}
return min;
}
求最小正子序列的和
许多人把这个题与上一问相混淆了。正子序列指子序列的和是正的,不是指子序列中每个数都是正的。要求最小正子序列可以用前缀数组的思想:
首先先求一下从第一位开始的到第i位的累加,注意第一个元素要置零
eg:1,-3,3,-4,5,7 -> 0,1,-2,1,-3,2,9,0,1,-2,1,-3,2,9对应的序号分别是0,1,2,3,4,5,6
然后对累加后的数组进行排序,若有两个相同的数,不用管它排序即可,它们的序号跟着一起排序。排序后的结果是-3,-2,0,1,1,2,9,序号分别是4,2,0,1,3,5,6。
最后扫描排序后的数组元素,两两一组,后一个的序号如果大于前一个的话则后一个的值减去前一个的值,最后找出正的最小值即可。如上正的最小值为1(1-0=1),是原数组的第0个元素,还有就是1(2-1=1),是原数组的第3,4两个元素的和。
有人会问为什么是两两相邻的相减而不是隔几个再减?因为隔几个再减也是符合的,但是因为若A与C相连,B与C相连,那A与B一定相连,所以不必隔几个再减了。
typedef struct _Elem {
int value;
int index;
}Elem;
int cmp(const Elem& e1,const Elem& e2)
{
return e1.value < e2.value;
}
int MinPositiveSubSeqSum(int arr[], int N)
{
Elem *temp = new Elem[N+1];
temp[0].value = 0;
temp[0].index = 0;
for (int i=1; i<=N; ++i) {
temp[i].value = temp[i-1].value + arr[i-1];
temp[i].index = i;
}
sort(temp, temp+N+1, cmp);
int sum = MAX;
for (int i=0; i<N; ++i) {
if (temp[i].index<temp[i+1].index) {
int diff = temp[i+1].value-temp[i].value;
if (diff>0 && diff<sum)
sum = diff;
}
}
delete[] temp;
return sum;
}
求最大子序列的乘积
可以用分治法。将数组分为前半段,后半段和中间段。最大子序列的乘积就是由前半段最大值,后半段最大值和中间段最大值中的最大的那个。需要注意的是,在计算中间段最大值时,需要分左右两个部分,它也是有两种可能的,1.左半部分的最小值(负数)和右半部分的最小值(负数)相乘,2.左半部分的最大值(正数)和右半部分的最大值(正数)相乘。
int max4(int a, int b, int c, int d)
{
int temp1 = a>b?a:b;
int temp2 = c>d?c:d;
return temp1>temp2?temp1:temp2;
}
int MaxSubSeqMulti(int arr[], int low, int high)
{
if (low==high)
return arr[low];
int center = low + ((high-low)>>1);
int leftmax = MaxSubSeqMulti(arr, low, center);
int rightmax = MaxSubSeqMulti(arr, center+1, high);
int leftmaxpositive = 0,leftminnegtive = 0;
int multi = 1;
for (int i=center; i>=low; --i) {
multi *= arr[i];
if (multi>0 && multi>leftmaxpositive)
leftmaxpositive = multi;
if (multi<0 && multi<leftminnegtive)
leftminnegtive = multi;
}
multi = 1;
int rightmaxpositive = 0,rightminnegtive = 0;
for (int i=center+1; i<=high; ++i) {
multi *= arr[i];
if (multi>0 && multi>rightmaxpositive)
rightmaxpositive = multi;
if (multi<0 && multi<rightminnegtive)
rightminnegtive = multi;
}
int maxpositive = leftmaxpositive * rightmaxpositive;
int maxnegtive = leftminnegtive * rightminnegtive;
return max4(leftmax,rightmax,maxpositive,maxnegtive);
}
2.13编写一个程序来确定正整数是否是素数
这个题目很常规,素数的判断是看N是否是一个奇数(2除外)并且不被3,5,7…sqrt(N)整除的数。
#include <iostream>
#include <cmath>
using namespace std;
bool IsPrimeNum(unsigned int N)
{
if (N==2)
return true;
if ((N&1)==0)
return false;
for (int i=3; i<=sqrt(double(N)); i+=2) {
if (N%i==0)
return false;
}
return true;
}
int main(int argc, char **argv)
{
if (IsPrimeNum(66))
cout<<"is a prime num"<<endl;
else
cout<<"is not a prime num"<<endl;
system("pause");
return 0;
}
2.19题找出数组中出现次数超过N/2的元素,没有的话需要指出。
这个题目乍一看很像《编程之美》上的“发帖水王”,但是其实不是,发帖水王说的是一定有这么一个出现次数超过N/2的元素存在,但这里不一定。看了书上给出的算法还是很疑惑。主要是N是奇数的时候会有问题。如3,2,3,2,1,按照答案书上的算法,得出主元是1。MARK一下,留待思考。