1 算法概述
1.1 算法:解决问题的办法,是由若干条指令组成的有穷序列。
满足条件:
- 输入(I):有零个或多个输入
- 输出(o): 至少一个输入
- 确定性:组成算法的每条指令,必须是明白无误的无歧义。
- 有限性:算法每条指令执行次数是有限的,时间有限。
算法的描述:
- 自然语言
- 类计算机语言,如类c
- 计算机语言,如c++,c,java
- 混合
1.2 算法复杂度
例子:设算法输入规模为n,同时求最大最小
两两比较 =》 大的放偶序,小的放奇序=》最大值肯定在偶数组,最小值肯定在奇数组
比较次数 n/2+n/2-1+n/2-1
设算法的输入规模为 n
C(A,I,n) 包括 T(A,I,n) 时间 ,S(A,I,n) 空间
即算法复杂度包括时间复杂度和空间复杂度
设某计算机运行算法的某个实例I
| 原运算 | O1 | O2 | ...On |
|---|---|---|---|
| 用时 | t1 | t2 | ...tn |
| 次数 | e1 | e2 | ...ek |
则 ? T(A,I,n) = \sum_{i=1}^nei * ti
- 最坏 Tmax(A,n) = max T(A,I,n) I 属于 Dn Dn : 输入规模为n的全体空间
- 最好 Tmin(A,n) = min T(A,I,n) I 属于 Dn Dn : 输入规模为n的全体空间
- 平均 Tave(A,n) = 累加 T(A,I,n) * P(I) I 属于 Dn
注意:实际中:对时间的考察:对某个主要次数的考察,且单独处理
对矩阵乘积 Ann* Bnn=>
- 加法运算 A (n) = (n-1)* n^2 = n^3 - n^2
- 乘法运算 P(n) = n*n^2
- Aave(n) = Amin(n) = Amax(n) =n^3 - n^2 //等概率平均
- Pmin(n) = Pmax(n) = n^3
例如 线性查找元素
/*
* @param {Array} a
* @param {Number} b
* @param {Number} c
* @return {Number}
*/
find(a,b,c)
{
for(let i = 0;i<n;i++)
{
if(a[i] === b)
return i;
}
return -1;
}
主要运算: 比较"===",规模 n
- 最好情况: Tmin(n) = 1
- 最坏情况: Tmax(n) = n
- 平均 : Tave(n) = (n+1)/2 <= 若a里面的元素各不相等
算法是程序设计的灵魂
复杂度的度量
一个算法A,输入量为n
复杂度(cn)=》(时间,空间){最好,最坏,平均}是非递减函数
若 C 为函数 f(n),采用渐进分析方法
记号 : O,W,θ,o,w
- O(fn) f(n)为g(n)上界 g(n) = O(f(n)) 含义 : g(n)的复杂度不超过f(n) 例如 1/2 * n^2 = O(n^2)
- W(fn) f(n)为g(n)下界 g(n) = W(f(n)) 含义 : g(n)的复杂度超过f(n) 例如 2 * n^2 = W(n^2)
- θ(fn) g(n) = θf(n)含义:g(n)与f(n)同级别
- o(fn) f(n)为g(n)严格上界 例如 1000n = o(n^2)
- w(fn) f(n)为g(n)严格下界
定理
- g(n) = O(f(n))<=>f(n) =W(g(n))
- g(n) = θ(f(n))<=>f(n) = θ(f(n))
- g(n) = o(f(n))<=>f(n) =w(g(n))
多项式函数复杂度
- 设P(n**) = ak* n^k + a(k-1)* n^(k-1)+...a1* n + a0
- 则P(n) = θ(n^k)
- 当然 P(n) = O(n^k)
- 1n^2-10n+9 = θ(n^2) = O(n^2)
常用的复杂度函数
- logn,n^x(0<x<1),n,n*logn,n^(1+x),n^2,n^3....... =>多项式级别
- 2^n,n! =>指数级别
算法的共识:
- 容易解决的问题,即多项式级别
- 难的问题,即指数级别
定理:O(f(n)+g(n)) = O(max(f(n),g(n)))
主定理
设T(n)= C n =1
设T(n)= k*T(n/m) + θ(n^d)(θ(n^d) = D(n)+M(n) 或 O(n^d) 为多项式级别)
k>=1 , m>=1
存在
- T(n) = θ(n^d) d>logm(k)
- T(n) = θ(n^d*logn) d=logm(k)
- T(n) = θ(n^(logm(k))) d<logm(k)
如T(n) = 2T(n/2) + θ(n)
此处 k = 2 ,m = 2 , d =1 =>d=logm(k) =>T(n) = θ(n^d* logn) = θ(n* logn)
二分查找(最坏情况)
T(n) = T(n/2)+1
此处 k =1, m = 2 , d =0 =>d = logm(k) => T(n) = θ(logn)
同时求最大最小
T(n) = 2* (T/2) + 2;
=>k = 2,m = 2,d = 0 d<log2(2) =>θ(n)
求解递归方程
方法一: 直接展开

对于 f(n) = θ(n^d)
T(n) = n^d + (k/(m^d))* n^d +......(k/(m^d))^(l-1)* n^d.....
注:1+r+r^2+.....r^l
- r<1 上式极限为常数
- r=1 上式为l
- r>1 相当于 r^(l+1)
方法二:运用主定理
当 k = m^d 即 d = logm(k)
=>T(n) = l* n^d + O(n^(logm(k))) = (logm(n))* n^d + θ(n^d) =(logm(n)+1)* n^d = θ(logn* n^d)
当 k < m^d 即 d > logm(k) =>T(n) = θ(n^d)
当 k > m^d 即 d < logm(k) =>T(n) = θ(n^logm(k))
2 递归
2.1 递归概论
递归:自身调用自身
定义:直接或间接的调用自身的算法称为递归算法,用函数自身给定出定义的函数称为递归函数
2.2 递归例子
线性查找
/*
* @param {Array} a
* @param {Number} l
* @param {Number} r
* @param {Array} x
* @return {Number}
*/
int find(a,l,r,x)//其实也就是从0依次往后找,emmm强行递归
{
//在a[l]....a[r] 查找x
if(r<l) return -1;
if(l===r&&a[l]===x)
{
return l;
}
else {
return -1;
}
int m = Math.floor((l+r)/2);
//区间分为l,...m 和 m+1....r
int r1 = find(a,l,m,x);
if(r1>=0) return r1;
else{
return find(a,m+1,r.x);
}
}
求树高
/**
* function ListNode(val) {
* this.val = val;
* this.l = null;
* this.r = null;
* }
*/
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {Number}
*/
//树高为层数
int getH(t)
{
if(t==NULL)//空树高为0
{
return 0;
}
let lt = t.l;
let rt = t.r;
let lh = getH(lt);
let rh = getH(rt);
let maxH = lh>rh?lh:rh;
return (maxh+1);
}
3.分治策略
分治策略的基本思想 : 问题->分解->分解....->归并
分治嘛,分而制之,把问题分解,再分解,再......直到问题规模足够小,然后处理。最后合并合并......起来
/**
*/
Divide_and_conquer(P){
if(|p|<=n0) //规模足够小
{
y = 非递归方法求解p
return y;
}
//分解 p 到 p1,p2,p3.....;
for(int i = 1;i<=k;i++)
{
yi = Divide_and_conquer(pi);
}
y = Merge(y1....yk);
return y;
}
分析(递归方程)
T(n) = C(常量) n<=n0
T(n) = k* T(n/m) + f(n) n>n0
注 :
- n/m 为子问题的规模
- k 为子问题个数
- f(n) = D(n) + M(n)分别为 分解工作量 和 合并工作量
- f(n) = θ(n^d) 或 f(n) = O(n^d) 即分解+合并工作量为多项式级别
归并排序
a[0].......a[n-1]
计算中间下标:m = (l+r )/2 无比较,即D(n) = 0
归并 合并工作量
- Mmax(n) = n - 1
- Mmin(n) = n/2
基本思想:al.....am.....ar -> 两边排序 然后再归并
归并工作量 最好为 n/2,最坏为 n-1
T(n) = 2* T(n/2)+θ(n* logn)=>由主定理(下面有) T(n) = θ(n* logn)
/*
* @param {Array} a
* @param {Number} l
* @param {Number} m
* @param {Number} r
* @param {Array} c
* @return
*/
function merge(T *a,int l,int m,int r,T *c)
{
//[al].....a[m] || a[m+1].....a[r]
//c[l].........c[m].....c[r]
let i = l,k = l,j = m+1;
while(i<=m&&j<=r)
{
if(a[i]<a[j])
{
c[k++] = a[i++];
}else{
c[k++] =a[j++];
}
}
if(i>m)//左侧归并完毕
{
while(j<=r)
{
c[k+1] = a[j++];
}
}else{
while(i<=m)
{
c[k++] = a[i++];
}
}
}
/*
* @param {Array} c
* @param {Number} l
* @param {Number} r
* @param {Array} a
* @return {Number}
*/
function Copy(T *c,int l,int r,T *a)
{
//c[l].....c[r] 复制到 a[l].....a[r]
for(int i = l;i<=r;i++)
{
a[i] = c[i];
}
}
//排序算法
/*
* @param {Array} c
* @param {Number} l
* @param {Number} r
* @param {Array} a
* @return {}
*/
function MergeSort(T *a,int l,int r,T *c)
{
if(r<=l) return ;
//以下为l<r
int m = ( l + r )/2;
//[l,m],[m+1,r]
MergeSort(a,l,m,c);
MergeSort(a,m+1,r,c);
Merge(a,l,m,r,c); //有比较,最坏n-1 ,最好 n/2
Copy(c,l,r,a); //无比较工作量,仅复制
}
//用法
let n = 100 0000;
let a =new Array(n);
for(let i = 0;i<n;i++)
{
a[i] = Math.random();
}
let c =new Array(n);//排序移动方向
MergeSort(a,0,n-1,c);
//已排序 a[0]<=...a[n-1]
输出a[i]...
- Tmin(n) = 0 n<=1
- Tmin(n) = 2 * Tmin(n/2) + n/2 n>1
- Tmax(n) = 0 n<=1
- Tmax(n) = 2 * Tmax(n/2) + n-1 n>1
或者
- T(n) = 0 n<=1
- T(n) = 2* T(n/2) + O(n) n>1
4.快速排序
要排a[0].....a[n-1]
先选取一个元素(如最左侧的元素)作为中轴元素
经过n-1次比较
得
元素<=中轴 中轴元素 >=中轴
分析 : Tmin(n) = 2* T(n/2)+n-1 = θ(n* logn)
最坏 : Tmax(n) = T(0)+T(n-1)+n-1 = θ(n^2)
快排中的分区算法(原地分区)
/*
* @param {Array} a
* @param {Number} p
* @param {Number} q
* @return {}
*/
function Partition (a,p,q)
{
//对于a[p]...a[q]
//以a[p]作为分界元素
let x = a[p]; //记录下分界点的值
let i = p;//用于记录分界点的位置
for(let j = p + 1; j<=q ;j++)
{
if(!x<a[j])
{
i++;
Swap(a[i],a[j]);
}
}
swap(a[p],a[i]);
return i;
}
/*
* @param {Array} a
* @param {Number} l
* @param {Number} r
* @return {}
*/
funciton Qsort(a,l,r)
{
if(r<=l) return;
let m = paritition(a,l,r)l//n-1次比较
//l....m...r
Qsort(a,l,m-1);
Qsort(a,m+1,r);
}
第k小问题
在一个序列或集合中找出"第k小"元素
解题方法:类快排
若 k =m 则找到 即是 a[m-1]
否则或左或右的去查找
/*
* @param {Array} n
* @param {Number} l
* @param {Number} r
* @param {Number} k
* @return {Number}
*/
funciton kthElement( T *n, int l, int r,int k)
{
//从a[l]....a[r]中找第k小
if(r<=l) return a[l];
let m = paritition(a,l,r);//分区(如原地分区)
let kk = m- l +1;
if(k==kk) { return a[m] };
else if(k<kk)
{
return kthElement(a,l,m-1,k);
}
else
{
return kthElement(a,m+1,r,k-kk);
}
}
分析:
- 最好:经过n-1比较分区后,分界点即是
- 最坏:T(n) = (n-1)+T(n-1)
设:分区后,分界点所处的位置有比例,如1/3* n,2/3* n 则 T(n) = (n-1) + T(2/3* n) = θ(n) (由主定理算出) 线性级别!
5.贪心算法
贪心算法通过一系列的选择来得到问题的解,它所做的每个选择都是对当前状态下局部最好选择,即贪心选择。贪心算法求解问题的两个重要性质:贪心选择性质和最优子结构性质
直接拿经典例子来说吧
活动安排
每个活动均有其开始时间与结束时间,两个活动相容,即[si,fi)与[sj,fj)交集为空.
求最大相容集合,即求活动数最多
解决办法 按照结束时间排序,降序排序
class TActivity
{
constructor(id,s,f,b){
this.id = id;
this.s = s;
this.f = f;
this.b = b;//选中最优解的标志
}
}
function bGreader(x,y)
{ return x.f>y.f; }
int n =...;
let a = ...;//一个待求数组
Math.sort(a,a+n,bGreader);
let last = 0;//最后选中的标号
a[0].b = true;
for(let i = 1;i<n;i++)
{
if(a[i].s>=a[last].f)
{
last = i;
a[i].b = true;
}
}
//从C++代码改过来的,改成四不像了emmm
最优装载问题
问题描述:有一批集装箱要装上一艘载重量为c的轮船,若体积不受限,求最大装载方案(指的是最多箱子数)
方法: 1. 按重量升序排序 2. 依次装船(只要不超重)
//伪c++代码,不过应该很好理解
template<class Type>
void Loading(int x[],Type w[],Type c,int n)
{
int *t =new int[n+1];
Sort(W,t,n);
for(int i = 1;i<=n;i++)
{
x[i] = 0;
}
for(int i = 1;i<=n&&w[t[i]]<=c;i++)
{
x[t[i]] = 1;
c-=w[t[i]];
}
}
6.动态规划(dynamic programming)
动态规划(dp)基础概念:
dp显著特征:该问题具有最优子结构性质
dp的两个基本要素 最优子结构和 重叠子问题
dp设计主要步骤:
- 问题具有最优子结构性质
- 构造最优值的递归关系表达式
- 最优值的算法描述
- 构造最优解
经典问题:
01背包
给定n种物品和一个背包,物品i的重量是wi,其价值为vi,背包容量为c,问应该如何选择装入背包中的物品,使得装入背包的物品的总价值最大?
01背包是一个特殊的整数划分问题,具有最优子结构性质
m(i,j) = max{m(i+1,j),m(i+1,j-w[i])+v[i]} j>=w[i]
m(i,j) = m(i+1,j) j<w[i]
m(n,j) = Vn j>=w[n]
m(n,j) = 0 j<w[n]
//c++伪代码
template<class Type>
void Knapsack(Type v, int w, int c,int n,Type ** m)
{
int jMax = min(w[n]-1,c);.//先将n填上
for(int j = 0;j<=jMax;j++)//此时容量比所需要小,装不下,为0
{
m[n][j] = 0;
}
for(int j = w[n];j<=c;j++)//此时容量比所需要大,可以装下,为v[n]
{
m[n][j] = v[n];
}
//初始化完成,开始填表
for(int i = n-1;i>1;i--)
{
jMax = min(w[i]-1,c);
for(int j = 0;j<=jMax;j++)
{
m[i][j] = m[i+1][j];
}
for(int j = w[i];j<=c;j++)
{
m[i][j] = max(m[i+1][j],m[i+1][j-w[i]+v[i]);
}
}
m[1][c] = m[2][c];
if(c>=w[1])
{
m[1][c] = max(m[1][c],m[2][c-w[1]]+v[1]);
}
}
template<class Type>
void Traceback(Type **m,int w,int c,int n,int x)
{
for(int i = 1;i<n;i++)
{
if(m[i][c]==m[i+1][c])//将是否选取背包记录下来
{
x[i] = 0;
}
else{
x[i] = 1;
c-=w[i];
}
}
x[n] = (m[n][c])?1:0;//或为0,或为v[n]
}