基础知识
1 数学知识
1.1 指数
X A X B = X A + B X^AX^B = X^{A+B} XAXB=XA+B
X A X B = X A − B X^A\over X^B = X^{A-B} XB=XA−BXA
( X A ) B = X A B (X^A)^B = X^{AB} (XA)B=XAB
X N + X N = 2 X N X^N + X^N = 2X^N XN+XN=2XN
1.2 对数
在计算机科学中,所有对数一般都是以2为底的。
定义:
X
A
=
B
,
当
且
仅
当
l
o
g
x
B
=
A
X^A = B,当且仅当logxB = A
XA=B,当且仅当logxB=A
定理:
l
o
g
A
B
=
l
o
g
C
B
l
o
g
C
A
;
C
>
0
logAB = {logCB\over logCA}; C>0
logAB=logCAlogCB;C>0
l o g A B = l o g A + l o g B logAB = logA + logB logAB=logA+logB
l o g A / B = l o g A − l o g B logA/B = logA - logB logA/B=logA−logB
l o g ( A B ) = B l o g A log(A^B) = BlogA log(AB)=BlogA
2 递归简论
2.1 定义
当一个函数用它自己来定义时就称为是递归(recursive)的。
递归的底层实现依靠的是栈存储结构,遵循先进后出原则,例如,要求F(3),先依次将F(3)、F(2)、F(1)、F(0)压入栈中,已知F(0)=0,便可依次弹出F(1)=1,F(2)=6,F(3)=21。
定义一个函数满足F(0) = 0且F(X) = 2F(X-1) + X*X。
int F(int x)
{
if(x == 0) return 0;
else return 2*F(x-1) + x*x;
}
2.2 基本法则
基准情况:不用递归就能求解。
不断推进:递归调用必须能朝着产生基准情况的方向推进。
设计法则:所有递归调用都能运行。
合成效益法则:切勿在不同递归调用中做重复性的工作。
打印整数的例子:PrintDigit打印单个数字。
#include <stdio.h>
void PrintDigit(unsigned int N)
{
printf("%d", N);
}
void PrintOut(unsigned int N)
{
if(N >= 10) PrintOut(N/10);
PrintDigit(N%10);
}
3 算法分析
3.1 定义
算法是为了解决某类问题而规定的一个有限长的操作序列。算法具有五个特性:
**有穷性:**有限步骤,有限时间。
**确定性:**不产生二义性。
**可行性:**基本操作运算执行有限次来实现。
**输入:**有零个或者多个输入。
**输出:**有一个或者多个输出。
3.2 时间复杂度
顺序结构和分支结构中的每段代码只运行一次;循环结构中的代码的运行时间要看循环的次数。估算算法的时间复杂度,循环结构对算法的执行时间影响更大。所以,算法的时间复杂度,主要看算法中使用到的循环结构中代码循环的次数(称为“频度”)。
表示:算法的时间复杂度的表示方式为:O(频度),称为大O记法。
简化:总结为3步
- 去掉运行时间中的所有加法常数。(例如 n2+n+1,直接变为 n2+n)
- 只保留最高项。(n2+n 变成 n2)
- 如果最高项存在但是系数不是1,去掉系数。(n2 系数为 1)
排序:常见算法时间复杂度排序。
O
(
1
)
常
数
阶
<
O
(
l
o
g
N
)
对
数
阶
<
O
(
N
)
线
性
阶
<
O
(
N
2
)
平
方
阶
<
O
(
N
3
)
(
立
方
阶
)
<
O
(
2
N
)
(
指
数
阶
)
O(1)常数阶 < O(logN)对数阶 < O(N)线性阶 < O(N^2)平方阶 < O(N^3)(立方阶) < O(2^N) (指数阶)
O(1)常数阶<O(logN)对数阶<O(N)线性阶<O(N2)平方阶<O(N3)(立方阶)<O(2N)(指数阶)
一般法则:
for循环:运行时间至多是该for循环内语句的运行时间乘以迭代次数N,线性阶。
嵌套for循环:一组嵌套内部的一条语句的总运行时间为该语句的运行时间乘以该组所有for循环的大小,平方阶或立方阶。
顺序语句:运行时间为各个语句的运行时间之和,常数阶。
if/else语句:运行时间为判断加上S1和S2中运行时间长者的运行时间之后,常数阶。
对数阶:若一个算法用常数时间(O(1))将问题的大小削减为其一部分(通常为1/2),则该算法是对数阶的,如分治算法。若使用常数时间只是把问题减少了一个常数,则该算法是线性阶的。
3.3 算法举例
最大子序列:
int MaxSubSeq(const int A[], int N)
{
int ThisSum, MaxSum, i, j, k;
MaxSum = 0;
for(i = 0; i < N; i++)
for(j = i; j < N; j++)
{
ThisSum = 0;
for(k = i; k <= j; k++)
ThisSum += A[k];
if(ThisSum > MaxSum)
MaxSum = ThisSum;
}
return MaxSum;
}
分析:第一个for循环大小为N,第二个for循环大小为N-i,假设最坏情况为N。第三个for循环大小为j-i+1,也要假设为最坏情况即N。故该算法的时间复杂度取决于三重嵌套for循环中的O(1)语句。
O
(
1
∗
N
∗
N
∗
N
)
=
O
(
N
3
)
O(1*N*N*N) = O(N^3)
O(1∗N∗N∗N)=O(N3)
对分查找:给定一个整数X和一个已经预先排序好的整数数组A。返回A[i] = X的下标i,X不在数组中则返回-1。每次循环结束后High-Low的值至少将该循环前的值减半,故时间复杂度为O(logN)。
#define NotFound -1
int BinarySearch(const int A[], int X, int N)
{
int Low, Mid, High;
Low = 0;
High = N - 1;
while(Low <= High)
{
Mid = (Low + High) / 2;
if(A[Mid] < X)
Low = Mid + 1;
else
{
if(A[Mid] > X) High = Mid - 1;
else return Mid;
}
}
return NotFound;
}
欧几里德算法:用于计算最大公因数,两个整数的最大公因数是能同时整除二者的最大整数。通过连续计算余数直到余数是0为止,最后的非零余数就是最大公因数。
typedef unsigned int uint;
uint Gcd(uint M, uint N)
{
uint Rem;
while(N > 0)
{
Rem = M % N;
M = N;
N = Rem;
}
return M;
}
幂运算:
long int Pow(long int X, unsigned int N)
{
if(N == 0) return 1;
if(N == 1) return X;
if(N % 2 == 0) return Pow(X * X, N / 2);
else return Pow(X * X, N / 2) * X;
}
幂运算:
long int Pow(long int X, unsigned int N)
{
if(N == 0) return 1;
if(N == 1) return X;
if(N % 2 == 0) return Pow(X * X, N / 2);
else return Pow(X * X, N / 2) * X;
}