一文带你进入算法世界

623 阅读11分钟

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)严格下界

定理

  1. g(n) = O(f(n))<=>f(n) =W(g(n))
  2. g(n) = θ(f(n))<=>f(n) = θ(f(n))
  3. 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)

求解递归方程

方法一: 直接展开 ![](file://C:/Users/24241/Documents/Gridea/post-images/1560396883760.jpg) 对于 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]
}