数据项 → → 数据元素 → → 数据对象 → → 数据
数据项:数据项组成数据元素,数据项是不可分割的最小单位
数据元素:组成数据的基本单位
数据对象:性质相同的数据元素的集合
数据:描述客观事物的符合
逻辑结构:数据对象中数据元素的相互关系
物理结构:数据的逻辑结构在计算机中的存储形式,也就是将数据元素存储到存储器
物理结构包括:顺序存储结构、链式存储结构
顺序存储结构:把数据元素存放在地址连续的存储单元,数据间逻辑关系和物理关系是一样的
链式存储结构:把数据元素存储到任意存储单元中,这些单元可以是连续的也可以是不连续的
链式存储很灵活,不用在意存储的位置,只要有一个存放地址的指针就好了。
算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,且每条指令表示一个或多个操作。
输入、输出、有穷性、确定性、可行性
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随着n的变化情况而确定T(n)的数量级。
时间复杂度,也就是算法的时间度量,表示岁问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同。
一般情况下,随着n的增大,T(n)增大最慢的方法是最优算法
用大写的O()来体现算法复杂度的记法,称为大O法。
如何推导时间复杂度:
1、常数阶:O(1)
2、线性阶:O(n)
关键在于分析循环结构内部情况,下面程序复杂度为O(n)
int i
for (i=0;ii++)
{
//时间复杂度为O(1)的程序
}
3、对数阶:O(log n)
int count=1;
while(countcount=count*2 //时间复杂度为O(1)的程序
}
由于每次count*2以后,就距离n更近了,也就是说有多少个2相乘后大于n,则会退出循环, 2x=n−>x=log2n 2 x = n − > x = l o g 2 n ,所以时间复杂度为O(log n)
4、平方阶:O(n^2)
int i,j;
for(i=0;ii++)
{
for(j=0;j1 )的程序
}
}
相当于循环了n*n次,时间复杂度为O(n^2),如果外部循环m次,时间复杂度为O(n^m)
总结:时间复杂度为循环体的复杂度乘以该循环运行次数
线性表(List):零个或多个数据元素的有限序列
元素之间是有顺序的:第一个元素无前驱,最后一个元素无后继,其他元素都有前驱和后继
线性表强调是有限的
数学表达:
顺序存储结构:用一段地址连续的存储单元一次存储线性表的数据元素
顺序存储结构的三个属性:
数据元素的序号和存放它的数组下标之间的对应关系:
第i+1个数据元素和第i个数据元素存储位置的关系: LOC(ai+1)=LOC(ai)+c L O C ( a i + 1 ) = L O C ( a i ) + c (c为每个数据元素所占存储单元)
第i个数据元素可以由a1推出: LOC(ai)=LOC(a1)+(i−1)∗c L O C ( a i ) = L O C ( a 1 ) + ( i − 1 ) ∗ c
线性表的存取时间性能为:O(1)
(1)获取元素
时间复杂度为 O(1) O ( 1 )
(2)插入
(3)删除
(4)时间复杂度
如果插入最后一个位置,或删除最后一个位置,时间复杂度为 O(1) O ( 1 )
如果插入第一个位置,或删除第一个元素,时间复杂度为 O(n) O ( n )
平均的情况:插入第i个位置,或删除第i个元素,需要移动n-i个元素,根据概率原理,每个位置插入或删除可概率是相同的,所以最终平均移动次数和最中间那个元素的移动次数相等为 n−1n n − 1 n ,所以时间复杂度还是 O(n) O ( n )
总结:读取的时候,复杂度为 O(1) O ( 1 ) ,插入或删除复杂度为 O(n) O ( n ) ,说明其适合元素个数不太变换,而存储操作更多的数据。
(5)线性表顺序存储结构的优缺点
链式结构:元素信息+后继元素的地址
结点Node:存储数据元素信息的域(数据域)+存储后继元素的地址的域(指针域)
单链表:每个结点只包含一个指针域的线性链表
头指针:链表第一个结点的存储位置
最后一个结点的后继不存在,即最后一个结点的指针为空(NULL)
头结点:第一个结点前设一个结点,可以不存储任何信息,也可以存储长度等附加信息
头结点与头指针的异同:
空链表:
设p是指向线性表第i个元素的指针,节点ai的指针域可以用p->data来表示,p->data的值是一个数据元素,节点ai的指针域可以用p->next来表示,p->next的值是一个指针,指向第i+1个元素,也就是ai+1的指针。
也就是,p->data=ai,p->next->data=ai+1
2、插入
不用改变其他结点,只需要让s->next和p->next的指针做一点改变即可
s->next=p->next;
p->next=s;
也就是让p的后继结点变为s的后继节点,再把s变为p的后继结点
3、删除
q=p->next;
p->next=q->next;
也就是让p的后继的后继结点,改为p的后继结点。
用数组描述的链表叫做静态链表
数组的元素=两个数据域组成(数据域data+游标cur)
数组的第一个和最后一个元素作为特殊处理单元,不存数据
单链表的终端结点的指针端原本为空指针,之后改为指向头结点,使整个单链表形成一个环,称为单循环链表,简称循环链表。
解决的问题:可以从当前一个结点出发,访问到列表的全部结点
在单链表的每个结点中,再设置一个指向前驱结点的指针域,所有每个结点都有两个指针域,一个指向后继,一个指向前驱。
栈是限定仅在表尾进行插入和删除操作的线性表
队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表
栈是限定仅在表尾进行插入和删除操作的线性表
栈顶:允许插入和删除的一端
栈底:不允许任何操作
规则:先进后出(线性表)
进栈:插入
出栈:删除
进栈:插入,push
出栈:删除(弹栈),pop
队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表
队列是一种先进先出的线性表(First in first out)的线性表,FIFO。
队尾:允许插入的一端
对头:允许删除的一端
由零个或多个字符组成的有限序列,又叫字符串
树是n个结点的有限集,n=0时,称为空树,在任意一颗非空树中:
树的结点包含一个数据元素及若干个指向其子树的分支,结点拥有的子树数量称为结点的度。
结点的分类:
上图的树的度为3
结点间的关系:
结点的子树的根称为该结点的孩子,该结点称为孩子的双亲,同一个双亲的孩子称为兄弟,结点的祖先是从根节点到该结点所经分支上的所有结点。
结点的层次:
层次(Level)从根开始定义,树中结点的最大层次称为树的深度(Depth)或高度。
有序树:树中结点的各个子树看成是从左至右有次序的,不能互换
无序树:可以互换
二叉树(Binary tree)是n个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根节点和两颗互不相交、分别称为根节点的左子树和右子树的二叉树组成。
二叉树的特点:
二叉树的五种形态:
1、斜树
所有结点都只有左子树的二叉树叫左斜树
所有结点都只有右子树的二叉树叫右斜树
2、满二叉树
所有分支节点都存在左子树和右子树,并且所有叶子都在同一层上的二叉树。
对一棵有n个结点的二叉树按层序编号,如果编号为i的结点与同样深度的蛮二叉树中编号为i的节点在二叉树中的位置完全相同,则成全完全二叉树。
满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。
完全二叉树特点:
1、在二叉树的第i层,至多有 2i−1 2 i − 1 个结点
2、深度为k的二叉树最多有 2k−1个结点 2 k − 1 个 结 点
3、对任何一棵二叉树T,如果其终端结点数为 n0 n 0 ,度为2的结点数为 n2 n 2 ,则 n0=n2+1 n 0 = n 2 + 1
4、具有n个结点的完全二叉树的深度为 [log2n]+1([x]表示不大于x的最大整数) [ l o g 2 n ] + 1 ( [ x ] 表 示 不 大 于 x 的 最 大 整 数 )
5、对完全二叉树的任意结点i:
1、二叉树的顺序存储结构
用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组的下标要能体现结点之间的逻辑关系。
一般二叉树:
可以将其按完全二叉树来编号,不过把不存在的结点设置为空而已:
但是这样一来会对存储空间浪费,所以顺序存储结构一般只用于完全二叉树
2、二叉链表
data->数据域,lchile和rchile->左孩子和右孩子的指针
从根节点出发,按照某种次序依次访问二叉树中所有结点,使得每个借点被访问一次,且仅被访问一次。
1、前序遍历
若二叉树为空,则空操作返回,否则先访问根节点,然后前序遍历左子树,再前序遍历右子树,下图遍历顺序为:ABDGHCEIF
2、中序遍历
若二叉树为空,则空操作返回,否则从根节点开始(并不访问),中序遍历左子树,然后访问根节点,最后中序遍历右子树,下图遍历顺序为:GDHBAEICF
3、后序遍历
若二叉树为空,则空操作返回,否则从左到右,先叶子后结点的方式遍历访问左右子树,最后访问根节点,,下图遍历顺序为:GHDBIEFCA
4、层序遍历
若二叉树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,同一层中,按从左到右的顺序对结点逐个访问,遍历顺序为:ABCDEFGHI
上述四种方法其实都是在把树中的结点变成某种意义的线性序列,给程序实现带来好处。
结点的带权路径:该结点到树根之间的路径长度与结点上权的乘积
树的带权路径:树中所有结点的带权路径长度之和
哈夫曼树:带权路径长度WPL最小的二叉树,称为哈夫曼树,也叫最优二叉树
构造哈夫曼树的过程:
1、无向边:顶点之间没有方向,称这条边为无向边,用无序偶对 (vi,vj) ( v i , v j ) 来表示
2、有向边:顶点之间有方向,称这条边为有向边,也称为弧,用有序偶对 <vi,vj> < v i , v j > 来表示,如果图中任意两个顶点的边都是有向边,则称该图为有向图,要区分开弧尾vj和弧头vi
无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图,含有n个顶点的无向完全图有 n(n−1)2 n ( n − 1 ) 2 条边。
有向图中,如果任意两个顶点间都存在方向互为相反的两条弧,则称为有向完全图。含有n个顶点的有向完全图有 n(n−1) n ( n − 1 ) 条边。
3、连通图:无向图中,如果两个顶点之间有路径,说明两顶点是连通的,如果对于图中任意两个顶点都是连通的,则称该无向图是连通图。
图1不是连通图,图2是连通图
4、极大连通子图称为连通分量
图7-2-12的图1是一个无向非连通图,但是他有两个连通分量,即图2 和图3,而图4虽然是图1的子图,但是不满足连通子图的极大顶点数,所以不是图1的无向图的连通分量。
有向图中,如果对于每一对vi,vj(vi不等于vj),从vi到vj都存在路径,称其为强连通图。
图的定义的总结:
图的结构比较复杂,任意两个顶点之间都可能存在联系,所以不可能用简单的顺序存储结构来表示,所以用下面的五种方法来存储图。
邻接矩阵的存储方式:两个数组来表示图,一个一维数组存储图中顶点的信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
临近矩阵的问题:对边数相对顶点数较少的图,这种结构是对存储空间的极大浪费,太稀疏。
邻接表:将数组与链表结合起来
从图中某一顶点出发访问图中其余顶点,且使每一个顶点仅被访问一次,该过程叫图的遍历。
从图中某个顶点v出发,访问此顶点,然后从v的未被访问的邻接点出发,深度优先遍历,直到图中所有和v有路径相通的顶点都被访问到。
深度优先遍历类似于树的前序遍历
广度优先遍历类似于树的层序遍历
深度和广度优先遍历在时间复杂度上是一致的,没有优劣之分,只是视情况选择而已。
深度优先更适合目标比较明确,以找到目标为主的情况,广度优先更适合在不断扩大遍历访问时找到最优解的情况。
我们将构成连通网的最小代价生成树称为最小生成树。
最短路径是两顶点之间经过的边上权值之和最少的路径,并且成第一个顶点是源点,最后一个顶点是终点。
1、迪杰斯特拉(Dijkstra)算法
一步步求出他们之间顶点的最短路径,过程都是基于已经求出的最短路径的基础上,求得更远顶点的最短路径。
2、弗洛伊德(Floyd)算法
根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素。
静态查找和动态查找:
1、顺序查找
顺序查找的优化:
2、有序表查找
a、折半查找
折半查找也叫二分查找,前提是线性表中的记录必须是关键码有序(通常从小到大有序),线性表必须采用顺序存储。
折半查找的思想:在有序表中,若给定值小于中间记录的关键字,则在中间记录的左半边继续查找,若给定的值大于中间记录的关键字,则在中间记录的右半边继续查找,不断重复,直到查找成功,或查找区域无记录,查找失败为止。
b、插值查找
3、线性索引查找
内排序与外排序:
根据排序过程中,待排序记录是否全部被放置在内存中,排序分为:内排序和外排序
两两比较相邻记录的关键字,如果反序则交换,如直到没有反序的记录为止。
冒泡排序优化:
复杂度分析:
通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i个记录交换。
复杂度分析:
将一个记录插入已经排好序的有序表中,从而得到一个新的,记录数增1的有序表。
复杂度分析:
突破前面的时间复杂度为O(n^2)的第一批算法之一
直接插入排序在什么时候有优势:
但是上述要求在现实中过于严苛,所以希尔研究出的方法对直接插入排序改进后,可以增加效率。
希尔排序:采取跳跃分割的策略,将相距某个“增量(gap)”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后的结果是基本有序的,而不是局部有序的。
初始序列:
第一轮:
① i=5,第1位和第5位比较,gap=4,将第5位的3和第1位的9交换了位置,变为:
② i=6,第2位和第6位比较,第二位>第六位,不交换
③i=7, 第三位和第七位比较,第三位>第七位,交换,变为
④ i=8,第四位和第八位比较,第四位>第八位,交换,变为
⑤ i=9,第五位和第九位比较,第五位>第九位,交换,变为
下标:0 1 2 3 4 5 6 7 8 9
数值:2 3 1 4 6 2 7 5 8 9
还要比较第五位和第1位的数据,最终变为2 1 4 6 3 7 5 8 9
第二轮:
① increment=4/3+1=2,i从2+1=3开始,到9结束,
将上图利用层序遍历存入数组,满足下列结构:
第一个for循环:将整个待排序序列构成一个大顶锥
第二个for循环:逐步将每个最大的值的根节点与末尾元素交换,并调整其成为大顶锥
堆排序的时间复杂度为 O(nlogn) O ( n l o g n )
利用归并的思想实现,原理是假设初始序列含有n个记录,则可以看成n个有序的子序列,每个子序列的长度为1,然后两两归并,得到[n/2](表示除了之后的最大整数)个长度为2或1的有序子序列,再两两归并,如此重复,得到一个长度为n的有序序列为止,称为3路归并排序。
通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
时间/空间复杂度: O(logn) O ( l o g n )