目录
基本概念(参考了小草莓|||||博主的《数据结构基础概念篇》)
线性表——零个或多个数据元素的有限序列
1.顺序表
a.线性表的顺序存储结构:把线性表的结点按逻辑顺序依次存放在一组地址连续的存储单元里。用这种方法存储的线性表简称顺序表。是一种随机存取结构。
2.线性链表
栈和队列
栈
栈的应用
队列
串
树
树的表示法
二叉树
遍历二叉树和线索二叉树
线索二叉树
树和森林
哈夫曼树
图(课程里讨论的都是简单图)
图的遍历
最小生成树
最短路径
拓扑排序
关键路径
查找
有序表查找
线性索引查找
二叉排序树
平衡二叉树
多路查找树
散列表查找(哈希表)概述
排序
冒泡排序算法
简单选择排序
直接插入排序
希尔排序
堆排序
归并排序
快速排序
1.逻辑结构:数据之间的相互关系。
a.集合 结构中的数据元素除了同属于一种类型外,别无其它关系。
b.线性结构 数据元素之间一对一的关系。
c.树形结构 数据元素之间一对多的关系。
d.图状结构或网状结构 结构中的数据元素之间存在多对多的关系。
2.物理结构:是把数据的逻辑结构在计算机中的存储形式(如:顺序结构、链式结构、索引结构、哈希结构等)。
3.算法五个特性: 有穷性、确定性、可行性、输入、输出。
4.算法设计要求:正确性、可读性、健壮性、时间效率高和存储量低。
线性表是一种典型的线性结构。头结点无前驱有一个后继,尾节点无后继有一个前驱。链表只能顺序查找,定位一个元素的时间为O(N),删除一个元素的时间为O(1)。
b.存取和存储是不一样的,存储是指按顺序的;而存取则是可以随机的,可以利用元素下标进行。
用一组任意的存储单元来依次存放线性表的结点,这组存储单元即可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。为了能正确表示结点间的逻辑关系,在存储每个结点值的同时,还必须存储指示其后继结点的地址。data域是数据域,用来存放结点的值。next是指针域(亦称链域),用来存放结点的直接后继的地址(或位置)。
A.单链表:其中每个结点的存储地址是存放在其前趋结点next域中,而开始结点无前趋,故应设头指针head指向开始结点。同时,由于最后一个结点无后继,故结点的指针域为空,即NULL。头插法建表(逆序)、尾插法建表(顺序)。增加头结点的目的是算法实现上的方便,但增大了内存开销。
a.查找:只能从链表的头指针出发,顺链域next逐个结点往下搜索,直到搜索到第i个结点为止。因此,链表不是随机存取结构。
b.插入:先找到表的第i-1的存储位置,然后插入。新结点先连后继,再连前驱。
c.删除:首先找到ai-1的存储位置p。然后令p–>next指向ai的直接后继结点,即把ai从链上摘下。最后释放结点ai的空间.r=p->next;p->next=r->next;delete r。
d.判断一个单向链表中是否存在环的最佳方法是快慢指针。
B.静态链表:用一维数组来实现线性链表,这种用一维数组表示的线性链表,称为静态链表。静态:体现在表的容量是一定的。(数组的大小);链表:插入与删除同前面所述的动态链表方法相同。静态链表中游标表示的是下一元素在数组中的位置。
是为了给没有指针的高级语言设计的一种实现单链表能力的方法。
基本和单链表相同,不过因为没有像动态链表的结点申请和释放问题,因此要自己实现malloc()和free()函数,通过更新备用链表来实现。前进通过k=L[k].cur;来实现。
C.循环链表:是一种头尾相接的链表。其特点是无须增加存储量,仅对表的链接方式稍作改变,即可使得表处理更加方便灵活。
其实质就是将单链表终端结点的指针域Null改为指向表头结点或开始结点。
D.双向链表:在单链表的每个结点里再增加一个指向其直接前趋的指针域prior。这样就形成的链表中有两个方向不同的链。双链表一般由头指针唯一确定的,将头结点和尾结点链接起来构成循环链表,并称之为双向链表。设指针p指向某一结点,则双向链表结构的对称性可用下式描述:p—>prior—>next=p=p—>next—>prior。从两个方向搜索双链表,比从一个方向搜索双链表的方差要小。
a.插入:先搞定插入节点的前驱和后继,再搞定后结点的前驱,最后搞定前结点的后继。
b.在有序双向链表中定位删除一个元素的平均时间复杂度为O(n)。(查找O(n),删除O(1))
c.可以直接删除当前指针所指向的节点。而不需要像单向链表中,删除一个元素必须找到其前驱。因此在插入数据时,单向链表和双向链表操作复杂度相同,而删除数据时,双向链表的性能优于单向链表。
栈是限定仅在表尾进行插入和删除操作的线性表。我们将允许插入和删除的一端称为栈顶(top),另一端称为栈底,不含任何数据元素的栈称为空栈。栈又称为先进后出的线性表,简称LIFO结构。空栈的判定条件定位top==-1。
A.四则运算表达式求值
a.中缀表达式---->后缀表达式(栈用来进出运算的符号)
规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断 其与栈顶符号的优先级,,是右括号或优先级低于栈顶符号则栈顶元素依次出栈并输出,并将当前符号进栈,一 直到最终输出后缀表达式为止。
b.将后缀表达式进行运算得出结果。(栈用来进出运算的数字)
规则:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到事符号,就将处于栈顶的两个数字出栈进行运 算,运算结果进栈,一直到最终获得结果。
队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。是一种先进先出的线性表,简称FIFO。允许插入的一端叫队尾,允许删除的一端称为队头。
A.顺序队列:顺序存储结构。当头尾指针相等时队列为空。在队列里,front指针指向队头元素,而rear指针指向队尾元素的下一位置。出队时front指针移动。(存在假溢出现象)
B.循环队列:把队列的头尾相接的顺序存储结构称为循环队列。判断循环队列队满的条件是(rear+1)%QueueSize==front。此时数组中还会有一个空闲单元。计算队列的长度为(rear-front+QueueSize)%QueueSize。
C.链队列:链式存储结构。限制仅在表头删除和表尾插入的单链表。显然仅有单链表的头指针不便于在表尾做插入操作,为此再增加一个尾指针,指向链表的最后一个结点。
串是零个或多个字符组成的有限序列。
串的表示和实现:
串匹配:将主串称为目标串,子串称之为模式串。朴素移位法匹配。KMP算法匹配。改进的KMP算法。
KMP匹配算法是在朴素移位算法基础上,去掉了i值回溯的部分。
改进的KMP匹配算法是在计算next基础上,如果a为字符与它next值指向的b位字符相等,那么该a位的nextval值就指向b位的nextval值,如果不等,则该a位的nextval值就是它自己a位的next值。
树(Tree)是n(n0)个结点的有限集。n=0时称为空树。在任意一棵非空树中:
(1)有且仅有一个根(Root)结点;
(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集,其中每一个集合本身又是一棵树,且称为根的子树(子树互不相交)。
1.结点拥有的子树数称为结点的度(Degree)。度为0的结点称为叶结点或终端结点;不为0的称为分支结点或非终端结点,也叫内部结点。树的度是树内各结点的度的最大值。
2.孩子结点,双亲结点,兄弟结点,堂兄弟结点。
3.树的高度或深度——树中结点的最大层次。
4.如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。
5.森林是m(m>0)棵互不相交的树的集合。
A.双亲表示法
与链表中的结点相比,数据域依然存数据,指针域存储该结点的双亲位置(为了解决其他问题,还可以增加右兄弟域或长子域)
B.孩子表示法
每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫做多重链表表示法。
三种方案:a.固定指针域个数为树的度;b.按需分配,专门取一个位置来存放结点的度。
c.把每个结点的孩子结点排列起来,以单链表作存储结构,如果是叶结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放于一个一维数组中。
C.孩子兄弟表示法(感觉是为了引出二叉树的,其内容和上述几个差不多)
二叉树(Binary Tree)是n(n0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
特点:a.每个结点最多有两棵子树。
b.左子树和右子树是有顺序的,次序不能颠倒。
c.即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
五种基本形态:空二叉树、只有一个根结点、根结点只有左子树、根结点只有右子树、根结点既有左子树又有右子树。
1.满树并不单是每个结点都存在左右子树,还必须所有的叶子都在同一层上。
2.对一棵具有n个结点的二叉树按层序编号,如果编号为i()的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。(所以注意,完全二叉树不是满二叉树,最底一层不需要子节点是满的。)
完全二叉树的特点:a.叶子结点只能出现在最下两层
b.最下层的叶子一定集中在左部连续位置
c.倒数二层,若有叶子结点,一定都在右部连续位置
d.如果结点度为1,则该结点只有左孩子,即不存在只有右子树的情况
e.同样结点的二叉树,完全二叉树的深度最小。
二叉树性质:
a.在二叉树的第i 层上至多有个结点(
),深度为k的二叉树至多有
个结点(
)。
b.对任何一棵二叉树T,如果其终端结点数为,度为2的结点数为
,则
。
c.具有n个结点的完全二叉树的深度为(
表示不大于x的最大整数)。
d.如果对一棵有n个结点的完全二叉树的结点按层序编号,则对任一结点i(1<=i<=n),有:
(1)如果i=1,则结点i无双亲,是二叉树的根;如果i>1,则其双亲的编号是 。
(2) 如果2i>n,则结点i无左孩子;否则,其左孩子是结点2i。
(3)如果2i+1>n,则结点i无右孩子;否则,其右孩子是结点2i+1。
二叉树存储结构:
1.顺序存储结构:一般只用于完全二叉树,否则会造成空间的浪费,比如深度为k的右斜树。
2.二叉链表法:二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域,这样的链表称为二叉链表。
二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树的所有结点,使得每个结点被访问一次且仅被访问一次。
二叉树遍历方法(三种遍历方法只是代码的顺序有所区别,将输出那一行代码放在前中后的位置)
1.前序遍历DLR:根节点->左子树->右子树
2.中序遍历LDR:中序遍历左子树,然后是访问根结点,最后中序遍历右子树。
3.后序遍历LRD:从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。
4.层序遍历:从树的根结点开始访问,从上而下逐层遍历,在同一层中从左到右对结点逐个访问。
推导遍历结果:已知中序遍历序列和前或后便利序列,可以唯一确定一棵二叉树。
对于一个有n个结点的二叉链表,每个结点有指向左右孩子的两个指针域,所以一共是2n个指针域。而n个结点的二叉树一共有n-1个分支线数,所以有2n-(n-1)=n+1个空指针域。为了有效利用这些空着的内存,我们将指向前驱和后继的指针存入其中。
我们把这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树。
1.线索化——对二叉树以某种次序遍历使其变为线索二叉树的过程。
2.为了区分指针域存的是孩子还是前驱或后继,增加只存放布尔型变量的标志域ltag和rtag。
3.线索化的过程就是在遍历的过程中修改空指针的过程。
基本步骤:
1.p=T->lchild; p指向线索链表的根结点;
2.若线索链表非空,循环:
a.循环,顺着p左孩子指针找到最左下结点;访问之;
b.若p所指结点的右孩子域为线索,p的右孩子结点即为后继结点循环: p=p->rchild; 并访问p所指结点。
c.一旦线索“中断”,p所指结点的右孩子域为右孩子指针,p=p->rchild,使 p指向右孩子结点;
将树转换为二叉树的步骤如下:
1.加线。在所有兄弟结点之间加一条连线。
2.去线。对树中的每个结点,只保留它与第一个孩子的连线,删除它与其它孩子结点的连线。
3.层次调整。旋转一定角度,使结构层次分明。第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。
森林转换为二叉树:
森林是由若干棵树组成的,所以可以理解为,森林中的每棵树都是兄弟,可以按兄弟的处理来操作。
1.把每一棵树转换为二叉树。
2.第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵的二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连起来。
判断一棵二叉树能否转换成一棵树还是森林,看这棵二叉树的根结点有没有右孩子,有就是森林,没有就是树。
森林/树的前序遍历和二叉树的前序遍历结果相同,森林的后序遍历和二叉树的中序遍历结果相同。
带权路径长度WPL最小的二叉树称为赫夫曼树。
赫夫曼算法:
1.根据给定的n个权值构成n棵二叉树的集合
,其中每棵二叉树
中只有一个带权为
根结点,其左右子树均为空。
2.在F中选取两棵根结点的权值最小的树作为左右子树构成一棵新的二叉树,且置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。
3.在F中删除这两棵树,同时将新得到的二叉树加入F中。
4.重复2和3,直到F只含一棵树为止。
赫夫曼编码:即在赫夫曼树基础上,规定赫夫曼树左分支代表0,右分支代表1,从根结点到叶结点所经过的路径分支组成的序列便为该结点对应字符的编码。
图(Graph)是由顶点的有穷非空集合(所以不允许没有顶点)和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
有向边:若从顶点vi到vj的边有方向,则称这条边为有向边,也称为弧,用有序偶对
图的一些基本概念
1.对于具有n个顶点和e条边数的图,无向图,有向图
。
2.如果任意两个顶点之间都存在边叫完全图。
3.顶点v的度是和v相关联的边的数目,记为TD(v)。
4.如果图中任意两个顶点vi、vj∈E,vi和vj都是连通的,则称G是连通图。
5.如果一个图有n个顶点和小于n-1条边,则是非连通图。
6.无向图中连通且n个顶点n-1条边叫生成树。有向图中顶点入度为0其余顶点入度为1的叫有向树。
图的存储形式
1.邻接矩阵 用两个数组来表示图,一个一维数组存储图中顶点信息,一个二维数组存储图中的边或弧的信息。
2.邻接表 a.图中顶点用一个一维数组存储,另外对于顶点数组中,每个数据元素还需要存储指向第一个邻接点的指针。
b.图中每个顶点vi的所有邻接点构成一个线性表,用单链表存储,无向图称为顶点vi的边表,有向图称为顶点vi作为弧尾的出边表。
3.十字链表 对于有向图,将其邻接表和逆邻接表结合起来。邻接表体现在存放弧尾弧头的下标和出的指针域,逆邻接表体现在入的指针域和存放弧头下标。这样相当于一个纵向的逆邻接表,所以被称为十字链表。
4.邻接多重表
5.边集数组
深度优先遍历(DFS) 类似于先序遍历
广度优先遍历(BFS) 类似于层序遍历
深度优先遍历更适合目标比较明确,以找到目标为主要目的的情况,而广度优先更适合在不断扩大遍历范围时找到相对最优解的情况。
Prim算法:从U={u0},TE={}开始。重复执行下述操作:在所有u∈U,v∈V-U的边(u,v)∈E中找一条代价最小的边(u0,v0)并入集合TE,同时v0并入U,直至U=V为止。此时TE中必有n-1条边,则T=(V,{TE})为N的最小生成树。
其实就是以某顶点为起点,逐步找各顶点上最小权值的边来构建最小生成树。适用于稠密图,即边多的。
Kruskal算法:假设N=(V,{E})是连通网,则令最小生成树的初始状态为只有n个顶点而无边的非连通图T={V,{}},图中每个顶点自成一个连通分量。在E中选择代价最小的边,若该边依附的顶点落在T中不同的连通分量上,则将此边加入到T中,否则舍去此边而选择下一代价最小的边。依次类推,直至T中所有顶点都在同一连通分量上为止。
其实就是从权重最小的边开始选,逐步找全顶点且不形成环路。适用于边少的情况。
Dijkstra算法:基于已经求出的最短路径的基础上,求得更远顶点的最短路径,最终得到你要的结果。
Floyd算法:求所有顶点到所有顶点的最短路径问题,两个二维矩阵,一个存权值,一个存路径,每一步以经过第i个点为中转,计算最短路径是否变化。
有一个有向图,V中的顶点序列满足若从顶点vi到vj有一条路径,则在顶点序列中vi必在顶点vj之前。我们称这样的顶点序列为一个拓扑序列。所谓拓扑排序,其实就是对一个有向图构造拓扑序列的过程。一个AOV(顶点表示活动)网的拓扑序列不唯一。
算法描述:1.将入度为0的顶点依次入栈
2.若栈不空,则栈顶元素出栈且输出;然后在其邻接表中查找该元素的后继,将其后继入度减1,若入度为0则进栈。
3.若栈空时输出的顶点个数不是n,则有向图有环;否则,拓扑排序完毕。
路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上的活动叫关键活动。AOE网:带权的有向无环图,其中顶点表示事件,弧表示活动,权表示活动持续时间。
分两步:第一步,用改进的拓扑排序算法求出拓扑序列和stack2,;第二步求事件最迟发生时间ltv,并求出lte和ete,比对二者是否相等。
ete表示最早开工时间,lte表示最晚开工时间,最晚开工时间并不是等vj事件发生才开始,而必须要在vj事件之前发生,所以lte=ltv[j]-len
查找就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素。
查找表分为静态查找表(查询是否存在和检索某个特定值的相关信息)和动态查找表(查找时插入或者删除数据元素)。
顺序表查找 就逐个比较嘛,改进就是添一个哨兵,类似于迭代器里的beg==end
折半查找: 前提是线性表中的记录必须是关键码有序,线性表必须采用顺序存储。
插值查找: 折半查找的改进,即改进mid的位置,按比例的去变化
mid=low+(high-low)*(key-a[low])/(a[high]-a[low]);//插值