数据结构与算法设计分析—— 数据结构及常用算法
线性表是`线性结构`,是包含n个数据元素的有限序列,通过顺序存储的线性表称为`顺序表`,它是将线性表中所有元素按照其逻辑顺序,依次存储到指定存储位置开始的一块连续的存储空间里;而通过链式存储的`链表`中,每个结点不仅包含该元素的信息,还包含元素之间的逻辑关系的信息。- 顺序表实现简单,`可以随机存取`,其`存储密度大`,但是执行插入、删除操作需要移动大量元素,`效率低`,另外其`存储空间需事先分配
目录
一、常用的数据结构
(一)线性结构
1、顺序表与链表
线性表是线性结构
,是包含n个数据元素的有限序列,通过顺序存储的线性表称为顺序表
,它是将线性表中所有元素按照其逻辑顺序,依次存储到指定存储位置开始的一块连续的存储空间里;而通过链式存储的链表
中,每个结点不仅包含该元素的信息,还包含元素之间的逻辑关系的信息。
- 顺序表实现简单,
可以随机存取
,其存储密度大
,但是执行插入、删除操作需要移动大量元素,效率低
,另外其存储空间需事先分配
,容易造成空间浪费或溢出。 - 链表不支持随机存取,
只能顺序存取
,通过指针
来体现元素之间的逻辑关系,存储密度比顺序表小,其执行插入、删除操作不需要移动元素,只需修改指针,效率高
,另外它还支持动态分配存储空间
,不会造成空间浪费或溢出。
2、栈
栈是一种只允许在一端进行插入或删除操作的线性表(限制在表尾),所以是一种特殊的线性表,它与队列具有相同的逻辑结构,都属于线性结构,区别在于对其中元素的处理不同,栈遵循的原则是先进后出
(FILO),即后进的元素先被取出来,它是被限制存取点的线性结构。
由于它是一种线性表,所以也有两种方式:顺序存储结构和链式存储结构,即对应顺序栈
和链式栈
,另外,栈的插入操作称为进栈
,栈的删除操作称为出栈
。
3、队列
队列与栈一样,它也是一种特殊的线性表,其操作受限,它与栈具有相同的逻辑结构,都属于线性结构,区别在于其中元素的处理不同,队列只允许在一端进行插入,且只允许在另一端进行删除,队列遵循的原则是先进先出
(FIFO),即先入队列的元素最先离开,与日常生活中的排队是一样的。
同样,也有两种方式存储队列,顺序存储结构和链式存储结构,即对应顺序队列
和链队列
,另外若将顺序队列的一维数组首尾相连形成一个环状,即称为循环队列
,也就是在逻辑上视为一个环连接起来。
4、串
串也是一种特殊的线性表,由零个或多个字符组成的有限序列,其数据元素就是字符,串的数据元素必须是单个字符。由任意多个连续的字符组成的子序列称为串的子串
,包含子串的串称为主串
,线性表是以单个元素进行相关操作,而串是以子串进行相关操作
的。
通过静态数组实现定长顺序存储
,一组地址连续的存储单元存储串值的字符序列,其中每个串都被分配一个固定长度的ch[MAXLEN]数组,即串的顺序存储结构,同时,也可以使用动态分配函数malloc()和free()来进行串的堆分配存储
,其分配的存储空间是动态的。串的链式存储
结构中,分为两个域和线性表一样,若通过单链表存储,则data域存储字符,next域存储指向下一个结点的指针。
(二)非线性结构
1、树与二叉树
(1)树
树是一种非线性结构
,它是树形结构,是含有n个有限元素的数据元素集合(其中n≥0,n=0时为空树),由m(m≥0)棵互不相交的树的集合称为森林。例如,线性结构中的数据元素之间是“一对一”的关系,而树形结构中的数据元素之间是“一对多
”的关系。树满足两个条件,一个树有且只有一个根结点
,其中根结点没有前驱结点,除了根结点其他所有结点都只有一个前驱结点;剩下的结点为m(m≥0)个互不相交的非空集合,其中每个集合又可以称为一棵树,即根的子树。
树中两个结点之间的路径
由两个结点间所经过的序列构成,路径长度
是路径上所经过的边的个数,而树的路径长度是指根结点到每个结点的路径长的总和。树中结点的子结点(孩子)个数称为该结点的度
,而树中结点的最大度数称为树的度
。
(2)二叉树
二叉树是一种特殊的树,与普通的树相比,普通树中结点的后继结点可以是任意多个,而二叉树中结点的后继结点最多只能有两个
,另外有两种特殊的二叉树,满二叉树和完全二叉树,满二叉树是完全二叉树的特例,可以说若一棵树是满二叉树,则它必是完全二叉树,但不能说一个完全二叉树必是满二叉树。
完全二叉树中,叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。
- 二叉树也是含有n个有限元素的数据元素集合(其中n≥0,n=0时为空二叉树),由一个根结点以及两个不相交的非空树,分别称为
左子树
和右子树
组成,二叉树是一个特殊的有序树
(其中结点的各子树从左到右有序),其中左右子树的次序不能任意交换,同样,左子树和右子树也是二叉树。
二叉树名称 | 特点 |
---|---|
满二叉树 | 深度(高度)为h,具有2h-1个结点的二叉树,其中每层结点都是满的` |
完全二叉树 | 树中除最后一层外,其余层的结点都是满的二叉树,或结点的右子树缺少连续的若干个结点 |
(3)二叉树的存储结构
二叉树的存储结构也分为顺序存储结构和链式存储结构,二叉树的顺序存储结构通过使用一组地址连续的存储单元数组进行存储,二叉树的链式存储结构通过链表实现,一个二叉树链表结点由数据域和指针域组成,除了data数据域
用于存放结点的信息外,每个结点含有两个指针,左指针域lchild
和右指针域rchild
,分别指向该结点的左孩子结点和右孩子结点,它们用于存放该结点左/右子树的地址,当左/右子树不存在,其指针域为空。
双亲表示法
- 顺序存储结构中的双亲表示法是通过采用一维数组来存储树中的结点,其中每个结点被赋予一个结构体类型,包含data域和parent域,分别
存储结点的数据域
和存储该结点双亲的数组下标
。
孩子链表表示法
- 链式存储结构中的孩子链表表示法是将所有结点存放在一个
顺序表
中,每个数据元素有两个域,分别是该结点的数据域和存放该结点的第一个孩子的地址,同时为树中每个结点都构建一个单链表
,它也有两个域,分别是存放该孩子结点在顺序表中的数组下标和存放下一个孩子的地址。
通过孩子链表表示法可以很容易地找到该结点的孩子结点,但是若要找到结点的双亲则较为麻烦,需遍历n个结点中孩子链表指针域所指向的n个孩子链表。
孩子兄弟表示法
- 同时,链式存储结构中还有一种称为
孩子兄弟表示法
, 是采用二叉链表,其中包含三个域,分别是数据域和两个指针,即*child指向第一个孩子结点的地址、*brother指向该结点的下一个兄弟结点的地址。
这种方法的缺点是若从当前结点查找其双亲结点较为麻烦,解决方法是为每个结点增设一个parent域用于指向其父结点。
2、图
图和树一样,也是一种非线性结构
,线性结构中的数据元素之间是“一对一”的关系,树形结构中的数据元素之间是“一对多”的关系,而图中的数据元素之间是“多对多
”的关系,每个数据元素可以有多个直接前驱和多个直接后继,即图的结构是网状结构
,可以将图定义由一个非空的顶点集合V(顶点集
)和一个描述顶点之间关系——边的有限非空集合E(边集
)所组成的一种数据结构,记为G=(V,E),其中图的顶点集V不一定为空,而图的边集E可以为空。
图与线性表、树不一样,线性表、树可以为空表、空树,但图不能为空图。
(1)无向图和无向完全图
- 按照图中的边是否有方向性,可以分为有向图和无向图。
无向图
中每条边都没有方向,一般用圆括号“()”表示两个顶点之间的边
,若边中带有数据信息,则称为权
,(vi,vj)表示顶点vi和顶点vj之间的无向边,若一个无向图中,若每个顶点都有一条边连接,则称为无向完全图
。
(2)有向图和有向完全图
有向图
中每条边都有方向,一般用尖括号“<>”表示两个顶点之间的首尾关系,<vi,vj>表示从顶点vi到顶点vj的弧
,同样,若弧中带有数据信息,也称为权
。<vi,vj>其中第一项vi称为弧尾,第二项vj称为弧头,也称为顶点vi邻接到顶点,若一个有向图中,若每个顶点都有互相相反的两条弧连接,则称为有向完全图
。
(3)度
- 在无向图中,对于一个顶点,其
边的个数
称为该顶点的度
,记为TD(v),而在有向图中,对于一个顶点,该结点的度等于顶点的入度+顶点的出度,该结点的弧头数目
称为入度
,记为ID(v);结点的弧尾数目
称为出度
,记为OD(v),即TD(v)=ID(v)+OD(v)。
(4)路径、路径长度和简单路径
路径
,即一个顶点到另一个顶点经过的顶点序列,路径上边或弧的数目称为路径长度
;若顶点序列中的各顶点不同,则称这样的路径称为简单路径
。
(5)回路和简单回路
- 在一个路径中,若起始结点与结束结点相同,则称该路径为一个
回路
或环
;另外,除了第一个结点和最后一个结点外,若顶点序列中的各顶点不同,则称这样的路径称为简单回路
。
(6)连通图和强连通图
- 连通图指的是无向图,强连通图指的是有向图:
连通图
- 在无向图中,若一个顶点到另一个顶点存在路径,则称这两个结点是
连通
的。若无向图中任意两个结点都是连通的(任意两个结点之间有路径),则称该图为连通图
,否则为非连通图;无向图的极大连通子图称为连通分量
。
强连通图
- 在一个有向图中,任意两个不同的顶点都存在相互之间的路径,则称为
强连通图
,其中任意顶点到其他顶点都有路径,但不一定有弧;同样,有向图的极大强连通子图称为强连通分量
。
另外,有向完全图一定是强连通图,但强连通图不一定是有向完全图,因为有向完全图中每个顶点都有互相相反的两条弧连接。
(7)距离
- 若从一个顶点到另一个顶点之间的最短路径存在,这称该路径的长度为该结点到另一个结点的距离,若两个结点之间不存在路径,则记距离为无穷(∞ )。
(8)图的存储结构
邻接矩阵
- 图的邻接矩阵存储结构用于表示顶点之间的相邻关系,其中通过一个一维数组存储顶点,一个二维数组存储顶点之间的相邻关系,一个顶点数为n的图的邻接矩阵是n×n(n行n列),即一个方阵,用邻接矩阵方法来表示一个图需要n2个存储空间,它
只与图中的顶点数有关
,其空间复杂度为O(n2)。
邻接表
- 邻接表方法采用顺序存储结构和链式存储结构来存储图,对于图中每个顶点vi,将所有邻接于vi的顶点连成一个单链表,即这个单链表就称为顶点vi的邻接表,另外还需将所有顶点的邻接表放进数组中,通过
邻接表存储图所用的空间大小取决于图的顶点数和边的个数
,顶点数n决定了顶点表的空间大小,边的个数决定了边表结点的空间大小。由于图G=(V,E),即需要在邻接表中定义两种结点结构,即顶点表和边表,另外若边上带有权值,则还需中边表上添加一个信息代表权值。顶点表
中除了数据域用于存储顶点信息,还有指向第一条与其邻接顶点的指针域;边表
中由邻接点域和指向下一条邻接边的指针域组成。
邻接多重表
- 邻接多重表是一种链式存储结构,用于表示
无向图
,邻接多重表也是由顶点表和边表组成,每个顶点由两个域组成,data域用于存储顶点的信息,firstedge域用于指向第一条与其邻接的边,顶点表的结构如下:
data | firstedge |
---|
由于无向图的特点,一条边两个结点,所以每个边结点同时连接在两个链表中,相对于邻接表,邻接多重表同一条边只需一个结点表示,边表的结构如下:
mark | ivex | ilink | jvex | jlink | info |
---|
首先,mark域为标识域,用于表示该边是否被访问过;ivex和jvex为该边邻接的两个顶点在图中的位置;ilink和jlink用于指向下一个邻接顶点ivex和jvex的边;info用于指向和边相关的各种信息的指针域。
十字链表
- 十字链表也是一种链式存储结构,用于表示
有向图
,data域用于存储顶点的信息,firstin和firstout域分别指向以该顶点弧头或弧尾的第一个弧结点,顶点表的结构如下:
data | firstin | firstout |
---|
十字链表中在表示弧时将其视为一个结点,tailvex和headvex域,分别表示弧的弧尾和弧头在图中的位置,hlink指向弧尾相同的下一条弧,tlink 指向弧头相同的下一条弧,info用于指向该弧的信息,从而使相同的弧尾和弧头分别在不同的一个链表上,弧结点表的结构如下:
tailvex | headvex | hlink | tlink | info |
---|
3、集合
集合是一种非线性结构
,集合的内容即数学上的集合概念,例如,集合的表示、空集、集合之间的关系、集合之间的运算等等,这里不再赘述。
二、算法的基本概念
(一)算法的定义
通常将解决问题的确定方法和有限步骤称为算法,在计算机中,指的是对特定问题求解步骤的一种描述,是若干条指令的有穷序列。
(二)算法的特性
算法具有以下五个特性:
(1)输入
(2)输出
(3)确定性
(4)可行性
(5)有穷性
(三)算法与数据结构
程序=算法+数据结构
,设计一个具有正确性、可读性、健壮性、高效率(执行速度快,时间复杂度低)和低存储(不费内存,空间复杂度低)的算法,再加上合适的数据结构,从而构成一个良好的程序。
三、算法设计步骤
(1)算法描述
(2)数学模型选择
(3)算法的详细设计
(4)算法的程序实现和测试
(5)算法分析
四、算法的效率分析
(一)时间复杂度
1、时间复杂度的定义
时间复杂度是对算法运行时间的数量级,有两种度量方法,分别是事后统计法
和事前分析估算法
。通常,对一个算法的时间复杂度考察三个方面,即最好情况、平均情况和最坏情况,从而对应最好时间复杂度、平均时间复杂度和最坏时间复杂度,其中平均时间复杂度是指所有输入在等概率的情况下,该算法的期望运行时间,例如直接插入排序:
排序算法 | 空间复杂度 | 平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 |
---|---|---|---|---|
直接插入排序 | O(1) | O(n2) | O(n) | O(n2) |
最好情况下,即元素都有序,此时只需比较元素而不需移动元素,最好时间复杂度
为O(n),而最坏情况下,即排好的序列刚好与初始序列相反,呈逆序排列,则此时比较次数和移动次数都到达最大值,最坏时间复杂度
为O(n2),而考虑平均情况下,总的比较次数和移动次数约为n2/4,故直接插入排序的平均时间复杂度
为O(n2)。
2、时间复杂度的规则
(1)常数规则
C为一个正常数,O[Cf(n)]=O[f(n)]。
(2)加法规则
O[f(n)]+O[g(n)]=O[max(f(n),g(n)]。
(3)乘法规则
O[f(n)]×O[g(n)]=O[(f(n)×g(n)],例如下列代码:
count=0; //执行一次
for(k=1;k<=n;k*=2) //执行log2n次
for(j=1;j<=n;j++) //循环n次
count++;
第一行代码执行一次,根据加法规则,该程序的运行时间由for循环决定,假设最外层m次循环结束,则有2m+k=n,可得:m=log2n,所以执行执行log2n次,另外,内层for循环n次,根据乘法规则,所以该程序段的时间复杂度为O(n)=n×log2n=nlog2n。
3、常见的时间复杂度的大小比较
时间复杂度为O(1) 常数阶算法的效率最高,而像O(2n) 、 O(n!) 、 O(nn)这类的算法的效率最低。
O(1) < O(log2n) < O(n) < O(nlog2n) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
根据其图像,如下:
(二)空间复杂度
空间复杂度是指定义该算法所需要耗费的存储空间
,根据其规模函数f(n),可记为O[f(n)],若算法需要的存储空间是常量,则其空间复杂度为O(1)。例如,由于快速排序代码中的递归进行需要栈来辅助,所以其需要的空间较大。其空间复杂度与递归层数(栈的深度)有关,为O(递归层数):
若将n个要排序的元素组成一个二叉树, 这个二叉树的层数就是递归调用的层数(栈的深度), 由于在n个结点的二叉树中,其最小高度=⌊ log2n⌋ (以2为底n的对数然后再向上取整,取比自己大的最小整数), 其最大高度=n。
所以,可知最好情况下,即最小高度,为⌊ log2n ⌋,最好空间复杂度为O(log2n);而最坏情况下,即最大高度,为n层,最坏空间复杂度为O(n);故平均情况下,为O(log2n)。
(三)渐进时间复杂性
设算法的运行时间为T(n),若存在T*(n),使得:
lim
n
→
∞
T
(
n
)
−
T
∗
(
n
)
T
(
n
)
=
0
\lim\limits_{n \rightarrow ∞ }\frac{T(n)-T*(n) }{T(n)} = 0
n→∞limT(n)T(n)−T∗(n)=0,
则称T*(n)为算法的渐进性态或渐进时间复杂性,当规模充分大时,T(n)和T*(n)近似相等。
五、渐近符号
(一)渐近上界Ο
通常使用的大O表示法
,O的含义是T(n)或S(n)的数量级,可定义为T[n] = O(f(n))或者S[n] = O(f(n)),则称函数T(n)以f(n)或S(n)以f(n)为界,通过这种方法来评估算法的时间复杂度和空间复杂度时是从当问题规模充分大的条件上所得到的一个上界,其阶数越低,评估的结果越精确。
(二)渐近下界Ω
渐进下界的表示方法刚好与渐进下界相反,用Ω
进行表示,它反映的是复杂度的一个下界,其阶数越高,评估的结果越精确。
(三)渐近精确界Θ
渐近精确界是当问题规模n满足一定条件的范围内时,函数T(n)和f(n)的阶数相同,通过Θ
进行表示。
六、递归算法
一个递归算法必须包括终止条件
和递归部分
,例如,n的阶乘是一个递归算法,该递归算法停止条件是n=0,函数可定义如下:
n
!
=
{
1
,n=0
n
(
n
−
1
)
!
,n>0
n!= \begin{cases} 1& \text{,n=0}\\ n(n-1)!& \text{,n>0} \end{cases}
n!={1n(n−1)!,n=0,n>0
递归算法求解问题的一般步骤如下:
(1)分析问题,寻找递归关系
(2)找出停止条件
(3)设计递归算法
例如,下列递归程序段是n次阶乘,一共执行n次递归调用fact()函数,所以即为O(n):
int fact(int n){
if(n<=1)
return 1;
return n*fact(n-1)
}
例如,二叉树的先序、中序、后序遍历中每一次函数调用都会在内存栈中分配空间,都运用了递归算法。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)