一、一个高级语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:
– 1. 算法采用的策略,方案
– 2. 编译产生的代码质量
– 3. 问题的输入规模
– 4. 机器执行指令的速度
二、函数的渐近增长:给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大,那么,我们说f(n)的增长渐近快于g(n)。
三、void function(intcount) {
int j;
for(j=count; j< n; j++) {
printf(“%d”, j);
}
}
事实上,这和之前我们讲解平方阶的时候举的第二个例子一样:function内部的循环次数随count的增加(接近n)而减少,所以根据游戏攻略算法的时间复杂度为O(n^2)。
T(n)和S(n)
四、顺序存储结构封装需要三个属性:
存储空间的起始位置,数组data,它的存储位置就是线性表存储空间的存储位置。
线性表的最大存储容量:数组的长度MaxSize。
线性表的当前长度:length。
五、各操作的时间复杂度:
插入与删除:O(n)
存、读:O(n)
六、 头指针与头结点
头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
头指针具有标识作用,所以常用头指针冠以链表的名字(指针变量的名字)。
无论链表是否为空,头指针均不为空。
头指针是链表的必要元素。
头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(但也可以用来存放链表的长度)。
有了头结点,对在第一元素结点前插入结点和删除第一结点起操作与其它结点的操作就统一了。
头结点不一定是链表的必须要素
七、 创建单链表:
a) 声明一结点p和计数器变量i;
b) 初始化一空链表L;
c) 让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
d) 循环实现后继结点的赋值和插入。
——头插法,尾插法
八、 静态链表:用数组描述的链表叫做静态链表,这种描述方法叫做游标实现法。
– 我们对数组的第一个和最后一个元素做特殊处理,他们的data不存放数据。
– 我们通常把未使用的数组元素称为备用链表。
– 数组的第一个元素,即下标为0的那个元素的cur就存放备用链表的第一个结点的下标。
– 数组的最后一个元素,即下标为MAXSIZE-1的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点作用。
优缺点:
• 优点:
– 在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点。
• 缺点:
– 没有解决连续存储分配(数组)带来的表长难以确定的问题。
– 失去了顺序存储结构随机存取的特性。
九、 循环链表:
• 循环链表的单链表的主要差异就在于循环的判断空链表的条件上,原来判断head->next是否为null,现在则是head->next是否等于head。
• 注意初始化问题
• 链表删除节点时,务必记得循环中的p指向删除节点的前一个节点,否则无法操作
十、 判断单链表中是否有环:
• 方法一:使用p、q两个指针,p总是向前走,但q每次都从头开始走,对于每个节点,看p走的步数是否和q一样。如图,当p从6走到3时,用了6步,此时若q从head出发,则只需两步就到3,因而步数不等,出现矛盾,存在环。
• 方法二:使用p、q两个指针,p每次向前走一步,q每次向前走两步,若在某个时候p == q,则存在环。
十一、双向循环链表的实践:
初始化时,用p\q两个节点。代码见视频,。
十二、栈和队列
Top指向栈顶,是最上端数据的上一个位置
清空一个栈:
• 所谓清空一个栈,就是将栈中的元素全部作废,但栈本身物理空间并不发生改变(不是销毁)。
注意 栈内的元素还在,只不过作废了,指针不指向它。
• 计算栈的当前容量也就是计算栈中元素的个数,因此只要返回s.top-s.base即可。(地址相减得出的是它们地址之间相差多少个元素,而非纯数字相减。
队列:
• 创建一个队列要完成两个任务:一是在内存中创建一个头结点,二是将队列的头指针和尾指针都指向这个生成的头结点,因为此时是空队列。
• 出队列操作是将队列中的第一个元素移出,队头指针不发生改变,改变头结点的next指针即可。
• 如果原队列只有一个元素,那么我们就应该处理一下队尾指针。
循环队列!
十三、递归与分治思想。
不到万不得已不要用递归。递归效率比较低。大量的递归调用会建立函数的副本,会消耗大量的时间和内存,而迭代则不需要此种付出。
• 递归函数分为调用和回退阶段,递归的回退顺序是它调用顺序的逆序。
汉诺塔问题。x->z转化为(n-1)x->y与一个x->z.不断递归
斐波那契数列。迭代递归都很简单。
要将一个字符串反向地输出:
void print()
{
char a;
scanf(“%c”, &a);
if( a !=‘#’) print();
if( a !=‘#’) printf(“%c”, a);
}
十四、杂谈指针与数组:
关于为什么普通指针不能指向一个二维数组。
因为这样会无法运算。因为计算不出移动距离。比如一维指针
int a[10];
int *p=a;
这样如果int是32位的,则p每次加1移动4个字节(4*8=32)。
但是二维指针
int a[10][2];
假设可以这样
int *p=a;
那么p+1应该在内存中移动多少字节呢?如果按逻辑上指针在数组的第一维度的移动来看答案是4*2=8字节。但是p是一维int指针,每次只能移动4字节,所以这样指其实是错的。(当然因为二维数组在内存中也是连续储存的,所以其实可以计算出每个元素的具体位置,但是这和你这样写程序的本意不符合)
应该要 int(*p)[2];
这样编译器就知道每次移动的是4*2=8字节。
约瑟夫环,解决最小公倍数问题
八皇后问题重要,难点回溯算法的典型案例
十五、KMP算法
问题由模式串决定,不是由目标决定!
十六、树
树的存储结构表示方法:双亲表示法、孩子表示法、孩子兄弟表示法
十七、二叉树
特点:
l 每个结点最多有两棵子树(即度不大于2
l 左子树和右子树是有顺序的,次序不能颠倒
l 即使树中某个结点只有一个子树,也要区分它是左子树还是右子树
五种基本形态:
• 空二叉树
• 只有一个根结点
• 根结点只有左子树
• 根结点只有右子树
• 根结点既有左子树又有右子树
完全二叉树、满二叉树的区别
满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树
• 完全二叉树的特点有:
叶子结点只能出现在最下两层。
最下层的叶子一定集中在左部连续位置。
倒数第二层,若有叶子结点,一定都在右部连续位置。
如果结点度为1,则该结点只有左孩子。
同样结点树的二叉树,完全二叉树的深度最小。
• 满二叉树的特点有:
叶子只能出现在最下一层。
非叶子结点的度一定是2。
在同样深度的二叉树中,满二叉树的结点个数一定最多,同时叶子也是最多。
二叉树的性质:(记忆时,可假设出一个满二叉树来,记住第i层结点数==前i-1层结点数之和+1)
l 在二叉树的第i层上至多有2^(i-1)个结点(i>=1)
l 深度为k的二叉树至多有2^k-1个结点(k>=1)
l 对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1
l 具有n个结点的完全二叉树的深度为⌊log₂n⌋+1
l 二叉树的性质五:如果对一棵有n个结点的完全二叉树(其深度为⌊log₂n⌋+1)的结点按层序编号,对任一结点i(1<=i<=n)有以下性质:
n 如果i = 1,则结点 i是二叉树的根,无双亲;如果i > 1,则其双亲是结点⌊i/2⌋
n 如果2i > n,则结点 i 无做左孩子(结点 i 为叶子结点);否则其左孩子是结点2i
n 如果2i+1 > n,则结点 i 无右孩子;否则其右孩子是结点2i+1
二叉树的存储结构:
顺序存储结构:使用一维数组
链式存储结构:两个指针一个data域
二叉树的遍历方法:
前序遍历、中序遍历、后序遍历、层序遍历(弄明白,根据根结点的位置来决定的方式)
线索二叉树:
枚举类型在C#或C++,java,VB等一些计算机编程语言中是一种基本数据类型而不是构造数据类型,而在C语言等计算机编程语言中是一种构造数据类型[1] 。它用于声明一组命名的常数,当一个变量有几种可能的取值时,可以将它定义为枚举类型
中序遍历
– ltag为0时指向该结点的左孩子,为1时指向该结点的前驱。
– rtag为0时指向该结点的右孩子,为1时指向该结点的后继。
lchild | ltag | data | rtag | rchild |
线索二叉树代码实现(重要!!注意看代码)
森林、树及二叉树的转换:
1、 普通树转换为二叉树
a) 加线,在所有兄弟结点之间加一条连线。
b) 去线,对树中每个结点,只保留它与第一孩子结点的连线,删除它与其他孩子结点之间的连线。
c) 层次调整,以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。
2、 森林转换为二叉树:
a) 把每棵树转换为二叉树。
b) 第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。
3、 二叉树转化为树、森林
• 二叉树转换为普通树是刚才的逆过程,步骤也就是反过来做而已。
• 判断一棵二叉树能够转换成一棵树还是森林,标准很简单,那就是只要看这棵二叉树的根结点有没有右孩子,有的话就是森林,没有的话就是一棵树。
• 第一步:若结点x是其双亲y的左孩子,则把x的右孩子、右孩子的右孩子……都与y连接起来
• 第二部:去掉所有双亲与右孩子的连线。
树与森林的遍历
树的遍历分为两种方式:先根遍历和后根遍历。
森林的遍历分为两种方式:前序遍历和后序遍历。
树、森林的前根(序)遍历和二叉树的前序遍历结果相同,树、森林的后根(序)遍历和二叉树的中序遍历结果相同!
十八、赫夫曼树(代码复杂)
• 结点的路径长度:
– 从根结点到该结点的路径上的连接数。
• 树的路径长度:
– 树中每个叶子结点的路径长度之和。
• 结点带权路径长度:
– 结点的路径长度与结点权值的乘积。
• 树的带权路径长度:
– WPL(WeightedPath Length)是树中所有叶子结点的带权路径长度之和。
WPL的值越小,说明构造出来的二叉树性能越优(如何?,看动画)
代码实现:
– builda priority queue
– builda huffmanTree
– builda huffmanTable
– encode
– decode
十九、图
• 图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
• 对于图的定义,我们需要明确几个注意的地方:
– 线性表中我们把数据元素叫元素,树中叫结点,在图中数据元素我们则称之为顶点(Vertex)。
– 线性表可以没有数据元素,称为空表,树中可以没有结点,叫做空树,而图结构在咱国内大部分的教材中强调顶点集合V要有穷非空。
– 线性表中,相邻的数据元素之间具有线性关系,树结构中,相邻两层的结点具有层次关系,而图结构中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。
无向边:若顶点Vi到Vj之间的边没有方向,则称这条边为无向边(Edge),用无序偶(Vi,Vj)来表示
有向边:若从顶点Vi到Vj的边有方向,则称这条边为有向边,也成为弧(Arc),用有序偶<Vi,Vj>来表示,Vi称为弧尾,Vj称为弧头。
• 简单图:在图结构中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。(有指向自己的边,也非简单图)
• 无向完全图:在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。含有n个顶点的无向完全图有n*(n-1)/2条边。
• 有向完全图:在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。含有n个顶点的有向完全图有n*(n-1)条边。
• 稀疏图和稠密图:这里的稀疏和稠密是模糊的概念,都是相对而言的,通常认为边或弧数小于n*logn(n是顶点的个数)的图称为稀疏图,反之称为稠密图。
• 有些图的边或弧带有与它相关的数字,这种与图的边或弧相关的数叫做权(Weight),带权的图通常称为网(Network)。
• 如果一个有向图恰有一个顶点入度为0,其余顶点的入度均为1,则是一棵有向树。
图的存储结构:邻接矩阵、邻接表(没懂代码)、邻接多重表、十字链表、边集数组
图的遍历:深度优先遍历和广度优先遍历。
深度优先遍历其实就是一个递归的过程,整个遍历过程就像是一棵树的前序遍历!
广度优先遍历的实现,用队列。
解决深度优先遍历问题时,请务必理解回溯,什么时候需要回溯,需要if或者是while(1).
马踏棋盘问题,用深度优先遍历解决。
广度优先遍历:队列实现(出队列的顺序即为广度优先遍历的顺序)
二十、最小生成树算法
Prim算法(以顶点为起点):写出代码
Kruskal算法(以边为着手点):如何巧妙的解决边形成回路的问题----parent数组的实现原理
二十一、最短路径
迪杰斯特拉算法Dijkstra
弗洛伊德算法Floyd
迪杰特斯拉算法对比弗洛伊德算法
时间复杂度O(N^2)对O(N^3)
弗洛伊德算法的P数组,理解。注意看代码,要能写。
二十二、拓扑排序(定义)
• 一个无环的有向图称为无环图(Directed Acyclic Graph),简称DAG图。
• 所有的工程或者某种流程都可以分为若干个小的工程或者阶段,我们称这些小的工程或阶段为“活动”。
• 在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称之为AOV网(Active On Vertex Network)。
AOV网不能存在回路!
• 对AOV网进行拓扑排序的方法和步骤如下:
– 从AOV网中选择一个没有前趋的顶点(该顶点的入度为0)并且输出它;
– 从网中删去该顶点,并且删去从该顶点发出的全部有向边;
– 重复上述两步,直到剩余网中不再存在没有前趋的顶点为止。
由于需要删除顶点,所以用邻接表来表示会更方便。
二十三、关键路径
• AOE网:在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,我们称之为AOE网(Activity On Edge Network)。
– etv(EarliestTime Of Vertex):事件最早发生时间,就是顶点的最早发生时间;
– ltv(LatestTime Of Vertex):事件最晚发生时间,就是每个顶点对应的事件最晚需要开始的时间,如果超出此时间将会延误整个工期。
– ete(EarliestTime Of Edge):活动的最早开工时间,就是弧的最早发生时间。
– lte(LatestTime Of Edge):活动的最晚发生时间,就是不推迟工期的最晚开工时间。
Lte可以通过求出ltv然后减去arc求出
Ete可以推断出etv
Ete=lte的活动就是关键活动(arc),关键活动组成的路径,是关键路径。
二十四、查找算法
• 静态查找:数据集合稳定,不需要添加,删除元素的查找操作。
• 动态查找:数据集合在查找的过程中需要同时添加或删除元素的查找操作。
• 对于静态查找来说,我们不妨可以用线性表结构组织数据,这样可以使用顺序查找算法,如果我们再对关键字进行排序,则可以使用折半查找算法或斐波那契查找算法等来提高查找的效率。
• 对于动态查找来说,我们则可以考虑使用二叉排序树的查找技术,另外我们还可以使用散列表hash结构来解决一些查找问题,这些技术我们都将在这部分教程里边介绍给大家。
静态查找:顺序查找、插值查找(按比例查找)(时间复杂度log2N)(斐波那契查找等)
二十五、数组的函数传递的两种方式(数组名和指针)
1. void f1(int a[]){
2. int i=0;
3. int len=GetLen(a);
4. for(;i<len;i++){
5. a[i]=i+10;
6. }
7. }
8. void f2(int * a){
9. int i;
10. int len=GetLen(*a);//$$1
11. printf("$$%d\n",len);
12. for(i=0;i<5;i++)
13. *(a+i)=i+20;
14. }
voidShortestPath_Floyd(MGraph G, Pathmatirx *P, ShortPathTable *D)
{
int v, w, k;
// 初始化D和P
for( v=0; v < G.numVertexes; v++ )
{
for( w=0; w < G.numVertexes;w++ )
{
(*D)[v][w]= G.arc[v][w];
(*P)[v][w] = w;
}
}
// 优美的弗洛伊德算法
for( k=0; k < G.numVertexes; k++ )
{
for( v=0; v < G.numVertexes;v++ )//注意k,v的顺序,先终点,然后起点,最后中间点。这里k和v似乎互换也没关系
{
for( w=0; w < G.numVertexes;w++ )
{
if( (*D)[v][w] >(*D)[v][k] + (*D)[k][w] )
{
(*D)[v][w] =(*D)[v][k] + (*D)[k][w];
(*P)[v][w] =(*P)[v][k]; // 请思考:这里换成(*P)[k][w]可以吗?为什么? buneng
}
}
}
}
}
二十六、线性索引查找
介绍了 稠密索引(应用于数据量不是特别大的数据)、分块索引(如下图,块内无序)、倒排索引(较好,主要是记录号表的插入删除占用时间)。
二十七、二叉排序树
二叉排序数(BinarySort Tree)又称为二叉查找树,它或者是一棵空树,或者是具有下列性质的二叉树:
若它的左子树不为空,则左子树上所有结点的值均小于它的根结构的值;
若它的右子树不为空,则右子树上所有结点的值均大于它的根结构的值;
它的左、右子树也分别为二叉排序树(递归)。
按照中序遍历,可以得出从小到大的一个排序
二叉排序树的查找与插入(简单)、删除(写代码时,先画图,注意特殊情况)
平衡二叉排序树(Self-BalanceBinary Search Tree/Height-Balance Binary Search Tree)AVL树(AV/L两位大师完成)
要么是一棵空树,要么左子树和右子树都是平衡二叉树,且要求每一结点的左子树与右子树之间的高度差不超过1
平衡二叉树的实现原理与代码(重要)(难理解,注意画图)
二十八、多路查找树
2-3树*:多路查找树中每一个结点都具有两个孩子或者三个孩子我们称之为2-3树。
一个结点拥有两个孩子和一个元素我们称之为2结点,Ta跟二叉排序树类似,左子树包含的元素小于结点的元素,右子树包含的元素大于结点的元素。不过与二叉排序树不同的是,这个2结点要么没有孩子,要有就应该有两个孩子,不能只有一个孩子。
所有叶子都在同一层。
了解它的插入原理和删除原理。
插入:分情况(尽可能利用每一层的空间(能够设计出算法))
1、 空树:直接插入,作为根节点
2、 插入进2结点:将2结点变为3结点
3、 插入进3结点(稍微复杂困难):找到2结点扩充,从下往上拆
删除:
1、位于3结点叶子上:直接3结点变成2结点
2、位于2结点叶子上:多重考虑
二十九、B树
• 一个m阶的B树具有如下属性:
如果根结点不是叶结点,则其至少有两棵子树
每一个非根的分支结点都有k-1个元素(关键字)和k个孩子,其中k满足:⌈m/2⌉ <= k<= m
所有叶子结点都位于同一层次
每一个分支结点包含下列信息数据:
n, A₀, K₁, A₁, K₂, A₂, K₃, A₃……
其中K为关键字,且Ki < Ki+1
Ai为指向子树根结点的指针
N为关键字的个数
在B树中的查找过程是一个顺指针查找结点和在结点中查找关键字的交叉过程
B树减少数据读取的次数,只需要将B树的阶数变得很大
三十、散列表查找(哈希表)(代码实现)
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应唯一的一个存储位置f(key),这块连续存储空间成为散列表或哈希表。
查找步骤:
• 当存储记录时,通过散列函数计算出记录的散列地址
• 当查找记录时,我们通过同样的是散列函数计算记录的散列地址,并按此散列地址访问该记录
不同关键字通过散列函数不能得到相同的地址,否则为冲突
计算简单 + 分布均匀 =好的散列函数
散列函数的构造方法:
• 直接定址法:例 :如果现在要统计的是1980年以后出生的人口数,那么我们对出生年份这个关键字可以变换为:用年份减去1980的值来作为地址。F(key) = a*key+b ——需要知道关键字的分布情况,适合查找表比较小,连续
• 数字分析法:通常适合处理关键字位数比较大的情况,例如我们现在要存储某家公司员工登记表,如果用手机号作为关键字,那么我们发现抽取后面的四位数字作为散列地址是不错的选择。
• 平方取中法是将关键字平方之后取中间若干位数字作为散列地址。(适合于不知道关键字的分布,而且位数不大的情况。)
• 折叠法是将关键字从左到右分割成位数相等的几部分,然后将这几部分叠加求和,并按散列表表长取后几位作为散列地址。
• 除留余数法,最为常用。对于散列表长为m的散列函数计算公式为:
f(key) = key mod p(p<=m)。事实上,这个方法不仅可以对关键字直接取模,也可以通过折叠、平方取中后再取模。
• P的选择是关键
• 随机数法:选择一个随机数,取关键字的随机函数值为它的散列地址。即f(key) = random(key)。这里的random是随机函数,当关键字的长度不等时,采用这个方法构造散列函数是比较合适的
• 现实中,我们应该视不同的情况采用不同的散列函数,这里给大家一些参考方向:
计算散列地址所需的时间
关键字的长度
散列表的大小
关键字的分布情况
记录查找的频率
处理散列冲突的方法:
开放定址法:
• 所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
• 它的公式是:
fi(key) = (f(key)+di) MOD m (di=1,2,…,m-1)
• 可以修改di的取值方式,例如使用平方运算来尽量解决堆积问题:
• 还有一种方法是,在冲突时,对于位移量di采用随机函数计算得到,我们称之为随机探测法:
再散列函数法:
fi(key) = RHi(key) (i=1,2,3,…k)
当第一个散列函数冲突时,再调用第二个散列函数。
链地址法:
公共溢出区法:
三十一、排序
影响排序算法性能的几个要素:
• 时间性能
• 辅助空间
• 算法的复杂性
稳定排序、不稳定排序
内排序和外排序
冒泡排序:3种代码,要点:
- 两两注意是相邻的两个元素的意思
- 如果有n个元素需要比较n-1次,每一轮减少1次比较
- 既然叫冒泡排序,那就是从下往上两两比较,所以看上去就跟泡泡往上冒一样。
注意看优化后的代码(第3种,flag的利用!有问题)
选择排序:找出每个i情况下的min值,然后与预设的位置互换(代码)
直接插入排序(结合代码和动画去看):将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增加1的有序表。
希尔排序:第一个突破o(n^2)的时间复杂度的算法,为o(n*logn)(看代码并且理解)
堆排序:
• 根结点一定是堆中所有结点最大或者最小者,如果按照层序遍历的方式给结点从1开始编号,则结点之间满足如下关系:
ki>=k2i ki<=k2i
或 (1<=i<=⌊n/2⌋)
ki>=k2i+1 ki<=k2i+1
• 下标i与2i和2i+1是双亲和子女关系。
• 那么把大顶堆和小顶堆用层序遍历存入数组,则一定满足上面的表达式。
堆排序思想:
• 堆排序(Heap Sort)就是利用堆进行排序的算法,它的基本思想是:
将待排序的序列构造成一个大顶堆(或小顶堆)。
此时,整个序列的最大值就是堆顶的根结点。将它移走(就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值)。
然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次大值。
如此反复执行,便能得到一个有序序列了。
归并排序(迭代实现和递归实现):将两个或者两个以上的有序表组成一个新的有序表
快速排序!!!!(注意优化的方式:point的选取三点取中法以及消除不必要的交换,优化小数组时的方案、优化递归)