1.在计算机科学中,所有的对数都是以2为底的,除非另有声明。
2.递归不是循环逻辑,虽然用一个函数本身来定义这个函数,但是并没有用一个函数实例本身来定义该特定实例。
2.5矩阵模板
class matrix
{
public:
matrix(int rows,int cols):array(rows)
{
for(int i=0;i<rows;i++)
array[i].resize(cols);
}
const vector<Object>& operator[](int row)const
{
return array[row];
}
vector<Object>& operator[](int row)
{
return array[row];
}
int numrows()const
{
return array.size();
}
int numcols()const
{
return numrows()?array[0].size():0;
}
private:
vector<vector<Object> > array;
};
3.如下递归是低效的(计算任何事情不要超过一次)
long fib(int n)
{
if(n<=1)
return 1;
else
return fib(n-1)+fib(n-2);
}
因为在计算fib(n-1)时已经计算了fib(n-2)的值,其后该信息被舍弃并重新计算了fib(n-2)。
4.求解最大子序列和
int maxSubSum4(const vector<int> &a)
{
int maxSum=0,thisSum=0;
for(int j=0;j<a.size();j++){
thisSum+=a[j];
if(thisSum>maxSum)
maxSum=thisSum;
else if(thisSum<0)
thisSum=0;
}
return maxSum;
}
在任意时刻,算法对要操作的数据只读入(扫描)一次,一旦被读入并处理,它就不需要在被记忆了。而在此处理过程中算法能对它已经读入的数据立即给出相应子序列问题的正确答案。具有这种特性的算法叫做联机算法(on-line algorithm)。
5.欧几里得算法(求两数最大公因子)
long gcd(long m,long n)
{
while(n!=0){
long rem=m%n;
m=n;
n=rem;
}
return m;
}
由于当M>N,则 M mod N<M/2(分取N<=M/2时和N>m/2时可证),因此迭代次数至多是 2 log N=O(log N)(M和N各取余一次)。
6.求整数的幂
bool isEven(int n)
{
return n%2==0;
}
long pow(long x,int n)
{
if(n==0)
return 1;
if(isEven(n))
return pow(x*x,n/2);//一次乘法
else
return pow(x*x,n/2)*x;//两次乘法
}
因为把问题分半最多需要两次乘法(当N为奇数的时候),因此需要的乘法次数最多是2 log N。
7.当队列采用数组实现的时候,应使用循环数组,即回绕队头和队尾。
8.UNIX文件系统中每个目录还有一项指向该目录本身以及另一项指向该目录的父目录。因此,严格来说,UNIX文件系统不是树,而是类树(treelike)
9.在处理二叉树的时候应注意处理重复元的情况。二叉树平均深度是O(根号 N)。
10.对任意常数k,(logN)的k次等于O(N)。它告诉我们对数增长得非常缓慢。
11.大O分析假设所有的操作都是相等的。当涉及磁盘IO的时候这么假设是不合适的。几乎在所有的情况下,控制运行时间的都是磁盘访问的次数。于是如果我们把磁盘访问的次数减少一半,那么运行时间也将减少一半。
12.树-----二叉树-----二叉查找树-----AVL树
-----伸展树
-----完全二叉树-----满二叉树
-----二叉堆
-----左式堆
-----斜堆
-----线索二叉树
-----表达式树(二元运算符)
-----扩充二叉树-----最优二叉树-----哈夫曼树
-----决策树
-----M叉树 -----M叉查找树-----B树
-----d堆
森林-----二项队列
树:Father数组可顺序存储树。链接存储:Father链接;孩子链表链接;左孩子右兄弟
满二叉树:高度为k的有2^(k+1) -1个节点,叶结点都在第k层上,每个分支节点都有两个字节点。
完全二叉树:一棵包含n个节点的高为k的二叉树T,当按层次顺序编号T的所有节点,对应于一棵高为k的满二叉树中编号由1至n的那些节点。可采用二叉树顺序存储,A[1]存储根,A[i]的左儿子存放在A[2i],右儿子存放在A[2i+1]。
二叉堆:一般堆即指二叉堆,是一棵完全二叉树,可采用数组存储。构建一个堆为O(N);插入和删除最小(最小堆)最坏为O(log N),插入操作平均为常数。合并为O(N)。
d堆:每个节点都有d个儿子。它的insert 时间为O(logd N),对于deleteMin,若使用标准算法,每层需要进行d-1次比较,于是时间为O(d logd N)。
左式堆:对于堆中的每一个节点X,左儿子的零路径长至少与右儿子的零路径长一样大。零路径长定义为从X到一个不具有两个儿子节点的最短路径长。合并(插入)和删除最小(最小堆)时间为O(log N)。
斜堆:是左式堆的自调节形式,不保留零路径长,右路径可以任意长。对于任意M次连续操作,总的最坏情形运行时间是O(M log N),因此每次操作的摊还开销为O(log N)。
二项队列:二项树的集合。二项队列的合并、插入和删除最小摊还时间为O(log N),插入为O(1)。
斐波那契堆:是以O(1)摊还时间支持所有基本的堆操作的一种数据结构,包括merge、insert、decreaseKey,但deleteMin和remove花费O(log N)摊还时间。
二叉查找树:节点X的左子树中所有项的值小于X中的项,右子树中所有项大于X中的项。二叉查找树平均深度为O(log N)。
表达式树:树叶是操作数,其他节点为操作符。
AVL树(平衡树):每个节点的左子树和右子树的高度最多差1的二叉查找树(空树的高度定义为-1)。必须保证树的深度是O(log N)。当插入破坏AVL树的特性时,要通过旋转来修正。插入:(1)左左、(2)左右、(3)右左、(4)右右。(1)(4)需要通过单旋转,(2)(3)通过双旋转。单旋转是插入路径上第一个不平衡的节点及其子节点,旋转两者的位置。双旋转是插入路径上第一个不平衡节点和其子节点、孙节点,将孙节点旋转到另外两者之间(先是子节点的单旋转,再是节点的单旋转)。
伸展树:它保证从空树开始任意连续M次对树的操作最多花费O(M log N)时间。当M次操作的序列总的最坏情形运行时间为O(M f(N))时,我们就说它的摊还运行时间为O(f(N))。因此,一棵伸展树每次操作的摊还代价是O(log N)。在访问后将实施伸展操作。之字形(zig-zag):双旋转。一字形(zig-zig):将祖-父-孙伸展为孙-父-祖。伸展时需要一路伸展到根。
B+树:(1)数据项存储在树叶上。(2)非叶节点存储直到M-1个键,以指示搜索的方向;键i代表子树i+1中的最小的键。(3)树的根或者是一片树叶,或者其儿子数在2和M之间。(4)除根外,所有非树叶节点的儿子数在(M/2)取天棚和M之间。(5)所有的树叶都在相同深度上并有(L/2)取天棚和L之间个数据项。分裂树根将增高树。删除根将降低树。从运行时间考虑,M的最好选择是3或4,如果我们只关心主存的速度,则更高阶的B树就没有优势了。B树实际用于数据库系统,在那里树被存储在物理的磁盘上而不是主存中。
线索二叉树:可直接查找任一节点在某种遍历顺序下的前驱节点和后继节点的二叉树。对于找前驱节点和后继节点的操作,线索树优于非线索树。
红黑树:红黑树是AVL树变种,最坏情形花费O(log N) 时间。每个节点是红色或黑色,根是黑色,红色节点子节点为黑色,从一个节点到一个NULL指针的每一条路径都必须包含相同数目的黑色节点。红黑树高度最多为2log(N+1)。
扩充二叉树:每当原二叉树中出现空子树时,就增加特殊的节点--空树叶,由此生成的二叉树称为扩充二叉树。树叶为外节点,非树叶为内节点。
最优二叉树:在外节点权值分别为w0, w1, ..., wn-1的扩充二叉树中,加权外通路长度最小的扩充二叉树。
哈夫曼树:取权值最小的两个根节点。。。哈夫曼树是最优二叉树。
set和map使用自顶向下红黑树。
13.一棵树所有节点的深度和称为内部路径长。
14.懒惰删除:当一个元素要被删除时,它仍留在树中,而只是做了个被删除的记号。
15.前缀码:字符集中任何字符的编码都不是其他字符的编码的前缀。显然等长编码是前缀码
16.散列函数。程序根据霍纳(Horner)法则计算一个(37)的多项式函数。
int hash(const string& key,int tableSize)
{
int hashVal=0;
for(int i=0;i<key.length();i++)
hashVal=hashVal*37+key[i];
hashVal%=tableSize;
if(hashVal<0) //散列值溢出为负数
hashVal+=tableSize;
return hashVal;
}
17.装填因子λ:散列表中的元素个数与散列表大小的比值。分离链接散列法的一般法则是使得表的大小尽量与预测的元素个数差不多(让λ≈1)。非分离链接散列法一般低于0.5
18.平方探测
template <typename HashedObj>
int findPos(const HashObj& x) const
{
int offset=1;
int currentPos=myhash(x);
while(array[currentPos].info!=EMPTY &&
array[currentPos].element!=x)
{
currentPos+=offset;
offset+=2;
if(currentPos>array.size())
currentPos-=array.size();
}
return currentPos;
}
由平方解法函数的定义可知,f(i)=i^2, 则f(i)=f(i-1)+2i-1;可令offset=2i-1.
19.线性探测有一次聚集的问题,平方探测有二次聚集的问题。散列表大小最好为素数。
20.STL的priority_queue例程。STL实现一个最大堆而不是最小堆,使用greater函数对象作为比较器可以得到最小堆。
#include <iostream>
#include <vector>
#include <queue>
#include <functional>
#include <string>
using namespace std;
//Empty the priority queue and print its contents.
template <typename PriorityQueue>
void dumpContents(const string & msg,PriorityQueue & pg)
{
cout<<msg<<":"<<endl;
while(!pg.empty())
{
cout<<pg.top()<<endl;
pg.pop();
}
}
//Do some inserts and removes(done in dumpContents).
int main()
{
priority_queue<int> maxPQ;
priority_queue<int,vector<int>,greater<int> > minPQ;//注意minPQ左面的> >中间有一个空格。否则就识别为右移了。
minPQ.push(4);minPQ.push(3);minPQ.push(5);
maxPQ.push(4);maxPQ.push(3);maxPQ.push(5);
dumpContents("minPQ",minPQ); //3 4 5
dumpContents("maxPQ",maxPQ); //5 4 3
return 0;
}
20.5 不相交集类,主要有union和find操作。find操作时间为O(log N),union操作时间为常数。生成迷宫可以采用该算法。代码如下
#include <iostream>
#include <vector>
using namespace std;
class DisjSets
{
public:
explicit DisjSets(int numElements):s(numElements)
{
for(int i=0;i<s.size();i++)
s[i]=-1;
}
int find(int x)const;
int find(int x);
void unionSets(int root1,int root2);
private:
vector<int> s;
};
int DisjSets::find(int x)const
{
if(s[x]<0)
return x;
else
return find(s[x]);
}
void DisjSets::unionSets(int root1,int root2)
{
if(s[root2]<s[root1])
s[root1]=root2;
else
{
if(s[root1]==s[root2])
s[root1]--;
s[root2]=root1;
}
}
//Do some inserts and removes(done in dumpContents).
int main()
{
DisjSets ds(8);
ds.unionSets(4,5);
ds.unionSets(6,7);
ds.unionSets(4,3);
return 0;
}
21.稠密图采用邻接矩阵(O(|V|^2)),稀疏图采用邻接表(O(|E|+|V|)表示。
22.拓扑排序是对有向无环图的顶点的一种排序。算法是计算每个顶点的入度,然后将所有入度为0 的顶点放入一个初始为空的队列中。当队列不空时,删除一个顶点v,并将与v邻接的所有顶点入度减1。只要顶点的入度降为0,就把该顶点放入队列中。此时,拓扑排序就是顶点出队的顺序。用时O(|E|+|V|)。
23.单源最短路径问题
假定权图中不存在负值回路。当前还不存在找出从s到一个顶点的路径比找出从s到所有顶点的路径更快(快得超出一个常数因子)的算法。
无权最短路径算法采用队列运行时间是O(|E|+|V|)。
加权最短路径采用Dijkstra算法。每个顶点有4个域,邻接表、是否已知标志、距离、路径。对于稠密图,可采用扫描顶点数组找出每一步的最小值,每步时间O(|V|),总共为O(|E|+|V|^2);对于稀疏图,即边数|E|=θ(|V|),采用最小堆实现,可达O(|E| log |V|)。
无环图可采用拓扑排序方法,运行时间为O(|E|+|V|)。
24.关键路径分析,最早完成时间取最大值,最晚完成时间取最小值,松弛时间为后继节点的最晚完成时间减去前驱节点的最早完成时间,再减去两者之间动作的时间。
25.网络流问题,最大流问题,可采用贪心策略,另外对采用并去掉的每一边都添加一条等价反向的边。
26.最小生成树。
Prim算法在每一阶段都选择边(u,v),使得(u,v)的值是所有u在树上,但v不在树上的边的值中的最小者。算法基本和Dijkstra算法相同,时间界也相同。
Kruskal算法,这种贪心策略是连续地按照最小的权选择边,并且当所选的边不产生回路时就把它作为去顶的边。采用最小堆选取边,若不在同一个等价类则使用union算法合并边集。
27.深度优先搜索,记录一个顶点是否已被访问,可采用递归。
深度优先生成树。若点已访问加后向边。
双连通性是指连通的无向图中任意顶点被删除,剩下的图仍然连通。将某点删除后不在连通,则该点是割点。采用深度优先搜索,并计算Num(v)和Low(v),当Num(v)≤Low(w)时,是割点。
欧拉回路在无向图中找出一条路径,使得该路径恰好访问每一边并最终回到起点。所有顶点的度均为偶数的任何连通图必然有欧拉回路。
欧拉环游是必须访问图的每一边但最后不一定必须回到起点。如果奇数度的顶点多于两个,那么欧拉环游不可能存在。
查找强分支:首先进行深度优先搜索,然后把所有边反向,然后对反向边的图执行一次深度优先搜索。
28.摊还分析:一次操作的摊还时间等于时间时间和位势变化的和。选择位势函数的关键在于,保证最小的位势要产生在算法的开始,并使得位势对低廉的操作增加而对高昂的操作减少。