栈和队列
栈:是限制在表的一端进行插入和删除操作的线性表。又称为后进先出线性表
用bottom表示栈底指针,top指向栈顶的下一个存储位置
用top - bottom = stacksize - 1表示栈满
队列:也是运算受限的线性表。是一种先进先出的线性表。只允许在表的一端进行插入,而在另一端进行删除。
队首:允许进行删除的一端称为队首
队尾:允许进行插入的一端称为队尾
特殊矩阵的压缩存储
- 对称矩阵压缩
n*n ——》n(n+1)/2
A[0..n-1,0..n-1] ←→ B[0..n(n+1)/2-1]
-
上三角矩阵
- 对角矩阵压缩
当b=1时称为三对角矩阵。其压缩地址计算公式如下: k=2i+j
树
基本概念
深度是到根的距离 高度是到树叶的距离
二叉树的顺序存储和链式存储
(a)顺序存储分配的内存空间大小是固定的,不好根据二叉树结点数目的增多而动态扩展。如果内存空间分配过大,势必造成内存空间的浪费;如果分配过小,则会产生数组溢出的问题。另外,顺序存储在增加和删除结点时时间复杂度为O(n^2)。顺序存储的好处是方便查找结点。
(b)链式存储可以动态分配内存空间,比顺序存储更加灵活、方便,结点的最大数目仅与系统存储空间相关。另外,在插入/删除结点时的时间复杂度仅为O(n)。因此,在对二叉树操作时更多采用链式存储结构
二叉树的遍历
线索二叉树
对于n个结点的二叉树,在二叉链存储结构中有n+1个空链域,利用这些空链域存放在某种遍历次序下该结点的前驱结点和后继结点的指针,这些指针称为线索,加上线索的二叉树称为线索二叉树。
线索二叉树可以分为前序线索二叉树、中序线索二叉树和后序线索二叉树。对一棵二叉树中的所有结点的空指针域按照某种遍历方式加线索的过程称为线索化,被线索化了的二叉树称为线索二叉树。
在二叉树线索化的过程中会把树中的空指针利用起来作为寻找当前结点前驱或后继的线索,这样就出现了一个问题,即线索和树中原有指向孩子结点的指针无法区分。
ltag和rtag就是为了区分这两类指针,它们为标志域,具体意义如下:
若ltag=0,则表示lchild为指针,指向结点的左孩子;若ltag=1,则表示lchild为线索,指向结点的直接前驱。
若rtag=0,则表示rchild为指针,指向结点的右孩子;若rtag=1,则表示rchild为线索,指向结点的直接后继。
树的存储结构
1、双亲表示法:
我们假设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指向其双亲结点到链表中的位置。也就是说每个结点除了知道自己之外还需要知道它的双亲在哪里。
2、孩子表示法
换一种不同的考虑方法。由于每个结点可能有多棵子树,可以考虑使用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫做多重链表表示法。不过树的每个结点的度,也就是它的孩子个数是不同的。所以可以设计两种方案来解决。
方案一:
一种是指针域的个数就等于树的度(树的度是树的各个结点度的最大值)
不过这种结构由于每个结点的孩子数目不同,当差异较大时,很多结点的指针域就都为空,显然是浪费空间的,不过若树的各结点度相差很小时,那就意味着开辟的空间都被利用了,这时这种缺点反而变成了优点。
方案二:
第二种方案是每个结点指针域的个数等于该结点的度,我们专门取一个位置来存储结点指针域的个数。
3、孩子兄弟表示法
我们发现,任意一颗树,它的结点的第一个孩子如果存在就是的,它的右兄弟如果存在也是唯一的。因此,我们设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。
树森林二叉树互相转换
树转换为二叉树
(1)加线。在所有兄弟结点之间加一条连线。
(2)去线。树中的每个结点,只保留它与第一个孩子结点的连线,删除它与其它孩子结点之间的连线。
(3)层次调整。以树的根节点为轴心,将整棵树顺时针旋转一定角度,使之结构层次分明。(注意第一个孩子是结点的左孩子,兄弟转换过来的孩子是结点的右孩子)
森林转换为二叉树
(1)把每棵树转换为二叉树。
(2)第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。
二叉树转换为树
是树转换为二叉树的逆过程。
(1)加线。若某结点X的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子结点…,都作为结点X的孩子。将结点X与这些右孩子结点用线连接起来。
(2)去线。删除原二叉树中所有结点与其右孩子结点的连线。
(3)层次调整。
二叉树转换为森林
假如一棵二叉树的根节点有右孩子,则这棵二叉树能够转换为森林,否则将转换为一棵树。
(1)从根节点开始,若右孩子存在,则把与右孩子结点的连线删除。再查看分离后的二叉树,若其根节点的右孩子存在,则连线删除…。直到所有这些根节点与右孩子的连线都删除为止。
(2)将每棵分离后的二叉树转换为树。
树和森林的遍历
一、树的遍历
1、先根(次序)遍历树
先访问树的根节点,然后依次先根遍历根的每棵子树
2、后根(次序)遍历
先依次后根遍历每棵子树,然后访问根结点。
上面的先根遍历为:A B C D E
上面的后根遍历为:B D C E A
二、森林的遍历
1、先序遍历森林
若森林非空,则可按照下述规则遍历之:
(1)访问森林中第一棵树的根节点
(2)先序遍历第一棵树中根结点的子树森林
(3)先序遍历出去第一棵树之后剩余的树构成的森林。
2、中序遍历森林
若森林非空,则可按照下述规则遍历:
(1)中序遍历森林中第一棵树的根节点的子树森林
(2)访问第一棵树的根节点
(3)中序遍历除了第一棵树之后剩余的树构成的子树森林
该树的先序遍历结果:A B C D E F G H I J。
该树的中序遍历结果:B C D A F E H J I G 。
二叉排序树
二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树。是数据结构中的一类。在一般情况下,查询效率比链表结构要高。
一棵空树,或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)左、右子树也分别为二叉排序树;
(4)没有键值相等的结点。
查找 P(n)=O(logn)
平衡二叉树
它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。
平衡二叉树是二叉查找树
哈夫曼树
给定n个权值作为n个叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
构造哈夫曼树
第一步:按从小到大排序。
【5、8、4、11、9、13】→【4、5、8、9、11、13】
第二步:选最小两个数画出一个树,最小数为4和5。
给定的4、5、8、9、11、13为白色, 红色的9为4+5,与给定的白9无关,新序列为:【红9(含子节点4、5)、8、9、11、13】
之后一直重复第一、第二步:排序然后取两个最小值。实际就是一个递归过程
哈夫曼编码
左图为构造哈夫曼树的过程的权值显示。右图为将权值左分支改为0,右分支改为1后的哈夫曼树。
我们对这六个字母用其从树根到叶子所经过的路径的0或1来编码,可以得到下表:
图
图G由顶点集V和边集E组成,记为G=(V,E),其中V(G)表示图G中顶点的有限非空集;E(G)表示图G中顶点之间的关系(边)的集合。
注意:线性表可以是空表,树可以是空树,图不可以是空图,图可以没有边,但是至少要有一个顶点。
有向图(边<>) 无向图(边())
简单图 :不存在重复边 不存在顶点到自身的边
完全图:无向图中任意两点之间都存在边,称为无向完全图 有向图中任意两点之间都存在方向向反的两条弧,称为有向完全图
子图:若有两个图G=(V,E),G1=(V1,E2),若V1是V的子集且E2是E的子集,称G1是G的子图。
连通,连通图 连通分量:若有两个图G=(V,E),G1=(V1,E2),若V1是V的子集且E2是E的子集,称G1是G的子图。
在有向图中,两顶点两个方向都有路径,两顶点称为强连通。若任一顶点都是强连通的,称为强连通。有向图中极大强连通子图为有向图的强连通分量。
连通图的生成树是包含图中全部顶点的一个极小连通子图,若图中有n个顶点,则生成树有n-1条边。所以对于生成树而言,若砍去一条边,就会变成非连通图。
在非连通图中,连通分量的生成树构成了非连通图的生成森林。
顶点的度为以该顶点为一个端点的边的数目。
对于无向图,顶点的边数为度,度数之和是顶点边数的两倍。
对于有向图,入度是以顶点为终点,出度相反。有向图的全部顶点入度之和等于出度之和且等于边数。顶点的度等于入度与出度之和。
若两顶点存在路径,其中最短路径长度为距离。
有一个顶点的入度为0,其余顶点的入度均为1的有向图称作有向树
邻接矩阵法
1无向图的数组表示
(1) 无权图的邻接矩阵
无向无权图G=(V,E)有n(n≧1)个顶点,其邻接矩阵是n阶对称方阵,如下图所示。其元素的定义如下:
(2) 带权图的邻接矩阵
无向带权图G=(V,E) 的邻接矩阵如下图所示。其元素的定义如下:
(3) 无向图邻接矩阵的特性
◆ 邻接矩阵是对称方阵;
◆ 对于顶点vi,其度数是第i行的非0元素的个数;
◆ 无向图的边数是上(或下)三角形矩阵中非0元素个数。
2有向图的数组表示
(1) 无权图的邻接矩阵
若有向无权图G=(V,E)有n(n≧1)个顶点,则其邻接矩阵是n阶对称方阵,如图7-7所示。元素定义如下:
(2) 带权图的邻接矩阵
有向带权图G=(V,E)的邻接矩阵如图所示。其元素的定义如下:
⑶ 有向图邻接矩阵的特性
◆ 对于顶点vi,第i行的非0元素的个数是其出度OD(vi);第i列的非0元素的个数是其入度ID(vi) 。
◆ 邻接矩阵中非0元素的个数就是图的弧的数目。
邻接表法
邻接表,存储方法跟树的孩子链表示法相类似,是一种顺序分配和链式分配相结合的存储结构。如这个表头结点所对应的顶点存在相邻顶点,则把相邻顶点依次存放于表头结点所指向的单向链表中。
十字链表(针对有向图改造)
十字链表的好处就是因为把邻接表和逆邻接表整合在了一起,这样既容易找到以vi为尾的弧,也容易找到以vi为头的弧,因而容易求得
顶点的出度和入度。除了结构复杂一点外,其实创建图算法的时间复杂度和邻接表是相同的,因此很好的应用在有向图中。
邻接多重表(针对无向图邻接表改造)
图的遍历
- 深度优先
假设初始状态是图中所有顶点未曾被访问,则深度优先搜索可从图中某个顶点发v 出发,访问此顶点,然后依次从v 的未被访问的邻接点出发深度优先遍历图,直至图中所有和v 有路径相通的顶点都被访问到;若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
当图采用邻接矩阵存储时,由于矩阵元素个数为n2,因此时间复杂度就是O(n2)。
当图采用邻接表存储时,邻接表中只是存储了边结点(e条边,无向图也只是2e个结点),加上表头结点为n(也就是顶点个数),因此时间复杂度为O(n+e)。
- 广度优先
广度优先搜索(Breadth_First Search) 遍历类似于树的按层次遍历的过程。
从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使得“先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问,直至图中所有已被访问的顶点的邻接点都被访问到。如果此时图中尚有顶点未被访问,则需要另选一个未曾被访问过的顶点作为新的起始点,重复上述过程,直至图中所有顶点都被访问到为止。
假设图有V个顶点,E条边,广度优先搜索算法需要搜索V个节点,时间消耗是O(V),在搜索过程中,又需要根据边来增加队列的长度,于是这里需要消耗O(E),总得来说,效率大约是O(V+E)。
图的遍历主要就是这两种遍历思想,深度优先搜索使用递归方式,需要栈结构辅助实现。广度优先搜索需要使用队列结构辅助实现。
最小生成树
n-1条边
一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。最小生成树可以用kruskal(克鲁斯卡尔)算法或prim(普里姆)算法求出。
普利姆算法:从图中任意取出一个顶点,把他当作一棵树,然后从这棵树相接的边中选取一条最短(权值最小)的边,并将这条边及其所连接的顶点也并入这棵树中,此时得到一颗有两个顶点的树。然后在这棵树中相连的顶点中选取最短的边,并将图中的所有顶点并入树中为止,此时得到的树就是最小生成树。
克鲁斯卡算法:(1)将图中的所有边都去掉。(2)将边按权值从小到大的顺序添加到图中,保证添加的过程中不会形成环(3)重复上一步直到连接所有顶点,此时就生成了最小生成树。这是一种贪心策略。
对短路径
Dijkstra算法采用的是一种贪心的策略,声明一个数组dis来保存源点到各个顶点的最短距离和一个保存已经找到了最短路径的顶点的集合:T,初始时,原点 s 的路径权重被赋为 0 (dis[s] = 0)。若对于顶点 s 存在能直接到达的边(s,m),则把dis[m]设为w(s, m),同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大。初始时,集合T只有顶点s。
然后,从dis数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点加入到T中,OK,此时完成一个顶点,
然后,我们需要看看新加入的顶点是否可以到达其他顶点并且看看通过该顶点到达其他点的路径长度是否比源点直接到达短,如果是,那么就替换这些顶点在dis中的值。
然后,又从dis中找出最小值,重复上述动作,直到T中包含了图的所有顶点。
拓扑排序
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序,有多种结果
- 从DGA图中找到一个没有前驱的顶点输出。(可以遍历,也可以用优先队列维护)
- 删除以这个点为起点的边。(它的指向的边删除,为了找到下个没有前驱的顶点)
- 重复上述,直到最后一个顶点被输出。如果还有顶点未被输出,则说明有环!
关键路径
所有活动都完成才能到达终点,因此完成整个工程所必须花费的时间(即最短工期)应该为源点到终点的最大路径长度。具有最大路径长度的路径称为关键路径。关键路径上的活动称为关键活动。
事件的最早发生时间:ve[k]
根据AOE网的性质,只有进入Vk的所有活动
ve[0] = 0
ve[k] = max(ve[j] + len
事件的最迟发生时间:vl[k]
vl[k]是指在不推迟整个工期的前提下,事件Vk允许的最迟发生时间。根据AOE网的性质,只有顶点Vk代表的事件发生,从Vk出发的活动
活动的最早发生时间:ee[i]
ai由有向边
活动的最迟发生时间:el[i]
el[i]是指在不推迟真个工期的前提下,活动ai必须开始的最晚时间。若活动ai由有向边
查找
顺序查找法 时间复杂度O(N)
折半查找 要求有序 顺序存储结构 每次和中间关键字比较大小 时间复杂度 log2(n+1) – 1
分块查找 前两者的结合:
要求各块是有序的 块内可以无序
step1 先选取各块中的最大关键字构成一个索引表; [1]
step2 查找分两个部分:先对索引表进行二分查找或
顺序查找,以确定待查记录在哪一块中;
然后,在已确定的块中用顺序法进行查找。
B树
(1)排序方式:所有节点关键字是按递增次序排列,并遵循左小右大原则;
(2)子节点数:非叶节点的子节点数>1,且<=M ,且M>=2,空树除外(注:M阶代表一个树节点最多有多少个查找路径,M=M路,当M=2则是2叉树,M=3则是3叉);
(3)关键字数:枝节点的关键字数量大于等于ceil(m/2)-1个且小于等于M-1个(注:ceil()是个朝正无穷方向取整的函数 如ceil(1.1)结果为2);
(4)所有叶子节点均在同一层、叶子节点除了包含了关键字和关键字记录的指针外也有指向其子节点的指针只不过其指针地址都为null对应下图最后一层节点的空格子;
构造:
判断当前结点key的个数是否小于等于m-1,如果满足,直接插入即可,如果不满足,将节点的中间的key将这个节点分为左右两部分,中间的节点放到父节点中即可。
删除:
(1)节点合并规则:当前是要组成一个5路查找树,那么此时m=5,关键字数必须大于等于ceil(5/2)(这里关键字数<2就要进行节点合并);
(2)满足节点本身比左边节点大,比右边节点小的排序规则;
(3)关键字数小于二时先从子节点取,子节点没有符合条件时就向向父节点取,取中间值往父节点放
B+树
B+树有两种类型的节点:内部结点(也称索引结点)和叶子结点。内部节点就是非叶子节点,内部节点不存储数据,只存储索引,数据都存储在叶子节点。
内部结点中的key都按照从小到大的顺序排列,对于内部结点中的一个key,左树中的所有key都小于它,右子树中的key都大于等于它。叶子结点中的记录也按照key的大小排列。
每个叶子结点都存有相邻叶子结点的指针,叶子结点本身依关键字的大小自小而大顺序链接。
父节点存有右孩子的第一个元素的索引。
散列表
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
字符串模式匹配
字符串模式匹配,也称子串的定位操作,通俗的说就是在一个主串中判断是否存在给定的子串(又称模式串),若存在,则返回匹配成功的索引。
1.朴素模式匹配算法也称为BF(Brute-Force)算法 :从主串的第一个字符起与子串的第一个字符进行比较,若相等,则继续逐对字符进行后续的比较;若不相等,则从主串第二个字符起与子串的第一个字符重新比较,以此类推,直到子串中每个字符依次和主串中的一个连续的字符序列相等为止,此时称为匹配成功。
2.KMP模式
按照BF算法是不假思索的把子串整体右移一位,主串不动,然后再逐次对应比较。而KMP算法的核心思想是尽可能的让子串向右远移。
子串的子串的各个前缀后缀的最大相同元素长度
失配时,子串向右尽可能远移的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值
排序问题
直接插入排序
每次将排序中的元素,插入到前面已经排好序的有序序列中去,直到排序完成。
时间复杂度为:O(n2)
优点 : 稳定,相对于冒泡排序与选择排序更快;
缺点 : 比较次数不一定,比较次数越少,插入点后的数据移动越多,特别是当数据总量大的时候;
折半插入排序
原理:折半插入算法是对直接插入排序算法的改进,排序原理同直接插入算法。
区别:在插入到已排序的数据时采用来折半查找(二分查找),取已经排好序的数组的中间元素,与插入的数据进行比较,如果比插入的数据大,那么插入的数据肯定属于前半部分,否则属于后半部分,依次不断缩小范围,确定要插入的位置。
优点 : 稳定,相对于直接插入排序元素减少了比较次数;
缺点 : 相对于直接插入排序元素的移动次数不变;
时间复杂度:可以看出,折半插入排序减少了比较元素的次数,约为O(nlogn),比较的次数取决于表的元素个数n。因此,折半插入排序的时间复杂度仍然为O(n²),但它的效果还是比直接插入排序要好。
冒泡排序
两个数比较大小,较大的数下沉,较小的数冒起来。(从后向前比较)
一共n-1趟 每次比较i-1次 时间复杂度仍然为O(n²)
选择排序
在长度为N的无序数组中,第一次遍历n-1个数,找到最小的数值与第一个元素交换;
第二次遍历n-2个数,找到最小的数值与第二个元素交换;
。。。
第n-1次遍历,找到最小的数值与第n-1个元素交换,排序完成。
平均时间复杂度:O(n2)
希尔排序
第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2
快速排序
先从数列中取出一个数作为key值;
将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;
对左右两个小数列重复第二步,直至各区间只有1个数。
平均时间复杂度:O(N*logN)
假设最开始的基准数据为数组第一个元素23,则首先用一个临时变量去存储基准数据,即tmp=23;然后分别从数组的两端扫描数组,设两个指示标志:low指向起始位置,high指向末尾.
首先从后半部分开始,如果扫描到的值大于基准数据就让high减1,如果发现有元素比该基准数据的值小(如上图中18<=tmp),就将high位置的值赋值给low位置 ,
然后开始从前往后扫描,如果扫描到的值小于基准数据就让low加1,如果发现有元素大于基准数据的值(如上图46=>tmp),就再将low位置的值赋值给high位置的值,
此时low或high的下标就是基准数据23在该数组中的正确索引位置
堆排序
平均时间复杂度:O(N*logN)
a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
归并排序
把长度为n的输入序列分成两个长度为n/2的子序列;
对这两个子序列分别采用归并排序;
将两个排序好的子序列合并成一个最终的排序序列。
O(nlogn)的时间复杂度
基数排序
(1)首先确定基数为10,数组的长度也就是10.每个数34都会在这10个数中寻找自己的位置。
(2)不同于BinSort会直接将数34放在数组的下标34处,基数排序是将34分开为3和4,第一轮排序根据最末位放在数组的下标4处,第二轮排序根据倒数第二位放在数组的下标3处,然后遍历数组即可。
待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
外部排序
外部排序指的是大文件的排序,即待排序的记录存储在外存储器上,待排序的文件无法一次装入内存,需要在内存和外部存储器之间进行多次数据交换,以达到排序整个文件的目的。
归并排序