第二章_算法分析

对于算法的分析需要用到一些渐进记号(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一下,留待思考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值