复杂性分析
对各种操作的时间复杂性的分析。
主要是链表,树,排序等简单一些的分析。 分析的时候,从简单的入手,学会方法。后续的各种都可能让你分析时间复杂度。
线性结构
-
链表
-
线性链表(顺序表和单链表)
-
循环链表
-
双向链表
-
-
队列(循环队列)
-
栈
-
链表主要操作:找某一个元素,插入一个(在哪个位置增加),删除一个(在哪个位置删除)。
-
栈:查找,插入(位置固定),删除(位置固定)
-
队列:查找,插入(位置固定),删除(位置固定)
二叉树
基本概念
二叉树是每个节点最多有两个子树的有序树。二叉树常被用于实现二叉查找树和二叉堆。值得注意的是,二叉树不是树的特殊情形。
二叉树是每个结点最多有两个子树的有序树。通常根的子树被称作“左子树”(leftsubtree)和“右子树”(right subtree)。二叉树常被用作二叉查找树和二叉堆或是二叉排序树。二叉树的每个结点至多只有二棵子树(不存在出度大于2的结点),二叉树的子树有左右之分,次序不能颠倒。
二叉树不是树的一种特殊情形,尽管其与树有许多相似之处,但树和二叉树有两个主要差别:
-
树中结点的最大度数没有限制,而二叉树结点的最大度数为2;
-
树的结点无左、右之分,而二叉树的结点有左、右之分。
二叉树是递归定义的,其结点有左右子树之分,逻辑上二叉树有五种基本形态:
空二叉树——如图(a)
-
只有一个根结点的二叉树——如图(b);
-
只有左子树——如图©;
-
只有右子树——如图(d);
-
完全二叉树——如图(e)
注意:尽管二叉树与树有许多相似之处,但二叉树不是树的特殊情形。
性质
-
在非空二叉树中,第i层的结点总数不超过, i>=1;
-
深度为h的二叉树最多有2^h-1个结点(h>=1),最少有h个结点;
-
对于任意一棵二叉树,如果其叶结点数为N0,而度数为2的结点总数为N2,则N0=N2+1;
-
具有n个结点的完全二叉树的深度为
-
有N个结点的完全二叉树各结点如果用顺序方式存储,则结点之间有如下关系: 若I为结点编号则 如果I>1,则其父结点的编号为I/2; 如果2I<=N,则其左儿子(即左子树的根结点)的编号为2I;若2I>N,则无左儿子; 如果2I+1<=N,则其右儿子的结点编号为2I+1;若2I+1>N,则无右儿子。
-
给定N个节点,能构成h(N)种不同的二叉树。h(N)为卡特兰数的第N项。h(n)=C(2*n,n)/(n+1)。(设有i个枝点,I为所有枝点的道路长度总和,J为叶的道路长度总和J=I+2i
存储结构
顺序存储表示
二叉树可以用数组或线性表来存储,而且如果这是满二叉树,这种方法不会浪费空间。用这种紧凑排列,如果一个结点的索引为i,它的子结点能在索引2i+1和2i+2找到,并且它的父节点(如果有)能在索引floor((i-1)/2)找到(假设根节点的索引为0)。这种方法更有利于紧凑存储和更好的访问的局部性,特别是在前序遍历中。然而,它需要连续的存储空间,这样在存储高度为h的n个结点组成的一般普通树时将会浪费很多空间。一种最极坏的情况下如果深度为h的二叉树每个节点只有右孩子需要占用2的h次幂减1,而实际却只有h个结点,空间的浪费太大,这是顺序存储结构的一大缺点。
/*二叉树的顺序存储表示*/
#define MAX_TREE_SIZE 100 /*二叉树的最大节点数*/
typedef TElemType SqBiTree[MAX_TREE_SIZE]; /* 0号单元存储根节点*/
typedef struct{
int level,order; /*节点的层,本层序号(按满二叉树计算) */
}position;
二叉链表存储表示
/*二叉樹的二叉鏈表存儲表示*/
typedef struct BiTNode
{
TElemType data;
struct BiTNode *lchild,*rchild; /*左右孩子指針*/
}BiTNode,*BiTree;
遍历算法
二叉树的遍历三种方式,如下:
-
前序遍历(DLR),首先访问根结点,然后遍历左子树,最后遍历右子树。简记根-左-右。
-
中序遍历(LDR),首先遍历左子树,然后访问根结点,最后遍历右子树。简记左-根-右。
-
后序遍历(LRD),首先遍历左子树,然后遍历右子树,最后访问根结点。简记左-右-根。
深度优先遍历
在深度优先中,我们希望从根结点访问最远的结点。和图的深度优先搜索不同的是,不需记住访问过的每一个结点,因为树中不会有环。
广度优先遍历
和深度优先遍历不同,广度优先遍历会先访问离根节点最近的节点。
完全二叉树,满二叉树
-
满二叉树:一棵深度为k,且有2k-1个节点称之为满二叉树
-
完全二叉树:深度为k,有n个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树中,序号为1至n的节点对应时,称之为完全二叉树
树
基本概念
树(tree)是包含n(n>0)个结点的有穷集,其中:
-
每个元素称为结点(node);
-
有一个特定的结点被称为根结点或树根(root)。
-
除根结点之外的其余数据元素被分为m(m≥0)个互不相交的集合T1,T2,、、、,Tm-1,其中每一个集合Ti(1<=i<=m)本身也是一棵树,被称作原树的子树(subtree)。
树也可以这样定义:树是由根结点和若干颗子树构成的。树是由一个集合以及在该集合上定义的一种关系构成的。集合中的 元素称为树的结点,所定义的关系称为父子关系。父子关系在树的结点之间建立了一个层次结构。在这种层次结构中有一个结点具有特殊的地位,这个结点称为该树 的根结点,或称为树根。
我们可以形式地给出树的递归定义如下:
单个结点是一棵树,树根就是该结点本身。
设T1,T2,…,Tk是树,它们的根结点分别为n1,n2,…,nk。用一个新结点n作为 n1,n2,…,nk的父亲,则得到一棵新树,结点n就是新树的根。我们称n1,n2,…,nk为一组兄弟结点,它们都是结点n的子结点。我们还称 T1,T2,…,Tk为结点n的子树。 空集合也是树,称为空树。空树中没有结点。
术语
1. 节点的度:一个节点含有的子树的个数称为该节点的度;
2. 树的度:一棵树中,最大的节点的度称为树的度;
3. 叶节点或终端节点:度为零的节点;
4. 非终端节点或分支节点:度不为零的节点;
5. 父亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
6. 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;
7. 兄弟节点:具有相同父节点的节点互称为兄弟节点;
8. 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
9. 树的高度或深度:树中节点的最大层次;
10. 堂兄弟节点:父节点在同一层的节点互为堂兄弟;
11. 节点的祖先:从根到该节点所经分支上的所有节点;
12. 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
13. 森林:由m(m>=0)棵互不相交的树的集合称为森林;
存储
父节点表示法
/*树节点的定义*/
#define MAX_TREE_SIZE 100typedef struct
{
TElemType data;
int parent; /*父节点位置域*/
}PTNode;
typedef struct
{
PTNode nodes[MAX_TREE_SIZE];
int n; /*节点数*/
}PTree;
孩子链表表示法
/*树的孩子链表存储表示*/
typedef struct CTNode
{ //孩子节点
int child;
struct CTNode *next;
} *ChildPtr;typedef struct
{
ElemType data;//节点的数据元素
ChildPtr firstchild;//孩子链表头指针
} CTBox;typedef struct
{
CTBox nodes[MAX_TREE_SIZE];
int n, r;//节点数和根节点的位置
} CTree;
森林
森林(forest)是m(m≥0)棵互不相交的树的集合。任何一棵树,删除了根结点就变成了森林。
森林、树与二叉树的转换 将树转换为二叉树
树中每个结点最多只有一个最左边的孩子(长子)和一个右邻的兄弟。按照这种关系很自
然地就能将树转换成相应的二叉树:
-
在所有兄弟结点之间加一连线;
-
对每个结点,除了保留与其长子的连线外,去掉该结点与其它孩子的连线。
注意:由于树根没有兄弟,故树转化为二叉树后,二叉树的根结点的右子树必为空。
将一个森林转换为二叉树
具体方法是:
-
将森林中的每棵树变为二叉树
-
因为转换所得的二叉树的根结点的右子树均为空,故可将各二叉树的根结点视为兄弟从左至右连在一起,就形成了一棵二叉树。
二叉树到树、森林的转换
把二叉树转换到树和森林自然的方式是:若结点x是双亲y的左孩子,则把x的右孩子,右孩子的右孩子,都与y用连线连起来,最后去掉所有双亲到右孩子的连线。
图
二元组的定义
图G是一个有序二元组(V,E),其中V称为顶集(VerticesSet),E称为边集(Edgesset),E与V不相交。它们亦可写成V(G)和E(G)。 E的元素都是二元组,用(x,y)表示,其中x,y∈V。
三元组的定义
图G是指一个三元组(V,E,I),其中V称为顶集,E称为边集,E与V不相交;I称为关联函数,I将E中的每一个元素映射到 。如果e被映射到(u,v),那么称边e连接顶点u,v,而u,v则称作e的端点,u,v此时关于e相邻。同时,若两条边i,j有一个公共顶点u,则称i,j关于u相邻。
有/无向图
如果给图的每条边规定一个方向,那么得到的图称为有向图。在有向图中,与一个节点相关联的边有出边和入边之分。相反,边没有方向的图称为无向图。
简单图
一个图如果
-
没有两条边,它们所关联的两个点都相同(在有向图中,没有两条边的起点终点都分别相同);
-
每条边所关联的是两个不同的顶点
则称为简单图(Simplegraph)。简单的有向图和无向图都可以使用以上的“二元组的定义”,但形如必须是对称的,即如果(x,y)∈E,那么(y,x)∈E。
基本术语
阶(Order):图G中顶集V的大小称作图G的阶。
子图(Sub-Graph):当图G’=(V’,E’)其中V‘包含于V,E’包含于E,则G’称作图G=(V,E)的子图。每个图都是本身的子图。
生成子图(Spanning Sub-Graph):指满足条件V(G’) = V(G)的G的子图G。 导出子图(Induced Subgraph):以图G的顶点集V的非空子集V1为顶点集,以两端点均在V1中的全体边为边集的G的子图,称为V1导出的导出子图;以图G的边集E的 非空子集E1为边集,以E1中边关联的顶点的全体为顶点集的G的子图,称为E1导出的导出子图。
度(Degree):一个顶点的度是指与该顶点相关联的边的条数,顶点v的度记作d(v)。
入度(In-degree)和出度(Out-degree):对于有向图来说,一个顶点的度可细分为入度和出度。一个顶点的入度是指与其关联的各边之中,以其为终点的边数;出度则是相对的概念,指以该顶点为起点的边数。
自环(Loop):若一条边的两个顶点为同一顶点,则此边称作自环。
路径(Path):从u到v的一条路径是指一个序列v0,e1,v1,e2,v2,…ek,vk,其中ei的顶点为vi及vi-1,k称作路径的长度。如果它的起止顶点相同,该路径是“闭”的,反之,则称为“开”的。一条路径称为一简单路径(simplepath),如果路径中除起始与终止顶点可以重合外,所有顶点两两不等。
行迹(Trace):如果路径P(u,v)中的边各不相同,则该路径称为u到v的一条行迹。
轨道(Track):如果路径P(u,v)中的顶点各不相同,则该路径称为u到v的一条轨道。
闭的行迹称作回路(Circuit),闭的轨称作圈(Cycle)。
(另一种定义是:walk对应上述的path,path对应上述的track。Trail对应trace。)
**桥(Bridge):**若去掉一条边,便会使得整个图不连通,该边称为桥。
图的存储表示
1.数组(邻接矩阵)存储表示(有向或无向)
typedef struct
{
VRType adj;//顶点关系类型。对无权图,用1(是)或e(否)表示相邻否;对带权图,则为权值
InfoType info; //该弧相关信息的指针(可无)
}ArcCe1l, AdjMatrix[MAX_ VERTEX NUM][MAX_ VERTEX_ NUM]; // 二维数组
struct MGraph
{
VertexType vexs[MAX_ VERTEX_ NUM];//顶点向量
AdjMatrix arcs; //邻接矩阵
Int vexnum, arcnum; //图的当前顶点教和呱数
GraphKind kind; //图的种类标志
};
2.邻接表存储表示
邻接表是图的一种链式存储结构。
邻接表中,对图中每个顶点建立一个单链表,第i个单链表中的结点表示依附于顶点Vi的边(对有向图是以顶点Vi为尾的弧)。 邻接表中的表结点和头结点结构:
有向图的邻接表和逆邻接表
-
在有向图的邻接表中,第i个单链表链接的边都是顶点i发出的边。
-
为了求第i个顶点的入度,需要遍历整个邻接表。因此可以建立逆邻接表。
-
在有向图的逆邻接表中,第i个单链表链接的边都是进入顶点i的边。
图的遍历
图的遍历方法有深度优先搜索法和广度(宽度)优先搜索法。
深度优先搜索法是树的先根遍历的推广,它的基本思想是:从图G的某个顶点v0出发,访问v0,然后选择一个与v0相邻且没被访问过的 顶点vi访问,再从vi出发选择一个与vi相邻且未被访问的顶点vj进行访问,依次继续。如果当前被访问过的顶点的所有邻接顶点都已被访问,则退回到已被 访问的顶点序列中最后一个拥有未被访问的相邻顶点的顶点w,从w出发按同样的方法向前遍历,直到图中所有顶点都被访问。
广度优先搜索是树的按层次遍历的推广,它的基本思想是:首先访问初始点vi,并将其标记为已访问过,接着访问vi的所有未被访问过的邻 接点vi1,vi2,„,vi t,并均标记已访问过,然后再按照vi1,vi2,„, vi t的次序,访问每一个顶点的所有未被访问过的邻接点,并均标记为已访问过,依次类推,直到图中所有和初始点vi有路径相通的顶点都被访问过为止。
图的连通性问题
无向图的连通分量和生成树
有向图的强连通分量
最小生成树
边赋以权值的图称为网或带权图,带权图的生成树也是带权的,生成树T各边的权值总和称为该树的权。
最小生成树(MST):权值最小的生成树。
生成树和最小生成树的应用:要连通n个城市需要n-1条边线路。可以把边上的权值解释为线路的造价。则最小生成树表示使其造价最小的生成树。
构造网的最小生成树必须解决下面两个问题:
1、尽可能选取权值小的边,但不能构成回路;
2、选取n-1条恰当的边以连通n个顶点;
MST性质:假设G=(V,E)是一个连通网,U是顶点V的一个非空子集。若(u,v)是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边(u,v)的最小生成树。
顺序表的查找
顺序表的存储
typedef struct{
ElemType*elem; //定义了顺序表中元素类型的数组指针,指向顺序表存储空间的基址
int length; //顺序表的长度(也即元素个数)
int listsize; //当前分配给顺序表的存储容量
}SqList;
顺序查找
顺序表可以看做第一部分的单链表和顺序表,这样的话查找问题就可以解决了。 主要是:平均查找长度的计算
有序表的查找(理解有序表和顺序表的不同)
有序表就是因为存储的数据是有续的,所以可以进行折半查找。
顺序表可以是单链表存储和顺序表存储。
而有序表的存储必须是顺序表存储。这样才能找到中点。
折半查找
索引顺序表的查找
二叉排序树
定义
二叉排序树(Binary Sort Tree)又称二叉查找树(Binary Search Tree),亦称二叉搜索树。 它或者是一棵空树;或者是具有下列性质的二叉树: (1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值; (2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值; (3)左、右子树也分别为二叉排序树;
查找
步骤:
若根结点的关键字值等于查找的关键字,成功。
否则,若小于根结点的关键字值,递归查左子树。
若大于根结点的关键字值,递归查右子树。
若子树为空,查找不成功。
插入
二叉排序树是一种动态树表。其特点是:树的结构通常不是一次生成的,而是在查找过程中,当树中不存在关键字等于给定值的节点时再进行插入。新插入的结点一定是一个新添加的叶子节点,并且是查找不成功时查找路径上访问的最后一个结点的左孩子或右孩子结点。
首先执行查找算法,找出被插结点的父亲结点。
判断被插结点是其父亲结点的左、右儿子。将被插结点作为叶子结点插入。 若二叉树为空。则首先单独生成根结点。
删除
在二叉排序树删去一个结点,分三种情况讨论:
-
若*p结点为叶子结点,即PL(左子树)和PR(右子树)均为空树。由于删去叶子结点不破坏整棵树的结构,则只需修改其双亲结点的指针即可。
-
若*p结点只有左子树PL或右子树PR,此时只要令PL或PR直接成为其双亲结点f的左子树(当p是左子树)或右子树(当*p是右子树)即可,作此修改也不破坏二叉排序树的特性。
-
若*p结点的左子树和右子树均不空。在删去*p之后,为保持其它元素之间的相对位置不变,可按中序遍历保持有序进行调整,可以有两种做法:
其一是令p的左子树为f的左/右(依p是f的左子树还是右子树而定)子树,s为p左子树的最右下的结点,而p的右子树为s的右子树;
其二是令p的直接前驱(或直接后继)替代p,然后再从二叉排序树中删去它的直接前驱(或直接后继)即让f的左子树(如果有的话)成为p左子树的最左下结点(如果有的话),再让f成为p的左右结点的父结点。
性能分析
每个结点的C(i)为该结点的层次数。最坏情况下,当先后插入的关键字有序时,构成的二叉排序树蜕变为单支树,树的深度为,其平均查找长度为(n+1)/2(和顺序查找相同),最好的情况是二叉排序树的形态和折半查找的判定树相同,其平均查找长度和log 2 (n)成正比