目录
- 第六章、数据结构与算法基础
- 1、数组与矩阵
- 1.1、数组
- 1.2、稀疏矩阵
- 1.3、数据结构的定义
- 2、线性表
- 2.1、顺序表和链表
- 2.2、顺序存储与链式存储对比
- 2.3、 队列与栈
- 3、广义表
- 4、树与二叉树
- 4.1、特殊二叉树
- 4.2、二叉树遍历
- 4.3、反向构造二叉树
- 4.4、树转二叉树
- 4.5、查找二叉树
- 4.6、最优二叉树(哈夫曼树)
- 4.7、线索二叉树
- 4.8、平衡二叉树
- 5、图
- 5.1、图的存储 - 邻接矩阵
- 5.2、图的存储 - 邻接表
- 5.3、图的遍历
- 5.4、拓扑排序
- 5.5、图的最小生成树--普里姆算法
- 5.5、图的最小生成树--克鲁斯卡尔算法
- 6、算法基础
- 6.1、算法的特性
- 6.2、算法的复杂度
- 7、排序与查找
- 7.1、顺序查找
- 7.2、二分查找
- 7.3、散列表
- 7.4、排序
- 7.4.1、==插入排序==
- 7.4.2、==交换类排序==
- 7.4.3、==选择类排序==
- 7.4.4、==归并排序==
- 7.4.5、==基数排序==
第六章、数据结构与算法基础
1、数组与矩阵
1.1、数组
已知5行5列的二维数组a中的各元素占两个字节,求元素a[2][3]按行优先存储的存储地址?
按行存:a+(5*2+3)2=a+26
按列存:a+(53+2)*2=a+34
1.2、稀疏矩阵
在矩阵中,若数值为0的元素数目远远多于非0元素的数目,并且非0元素分布没有规律时,则称该矩阵为稀疏矩阵;与之相反,若非0元素数目占大多数时,则称该矩阵为稠密矩阵。定义非零元素的总数比上矩阵所有元素的总数为矩阵的稠密度。
设有如下所示的下三角矩阵A[0…8,0…8],将该三角矩阵的非零元素(即行下标不小于列下标的所有元素)按行优先压缩存储在数组M[1…m]中,则元素A[i,j](0≤i≤8,j≤i)存储在数组M的()中。
因为A0,0存储在M[1]中,所以,将0,0带入验证
A1,1存储在M[3]中,将1,1带入验证
A正确
1.3、数据结构的定义
1️⃣数据结构的概念
数据结构指的是计算机中存储和组织数据的方式,包括数据的逻辑结构和物理结构两个方面
2️⃣数据逻辑结构
逻辑结构是指数据元素之间的关系
-
线性结构(如数组、链表)
-
树形结构(如二叉树、堆、AVL树)
-
图形结构(如邻接表、邻接矩阵)
2、线性表
2.1、顺序表和链表
线性表的概念
线性表是由同类型数据元素构成有限序列的数据结构,其中数据元素之间的关系是一对一的关系,即除了第一个和最后一个元素之外,其它每个数据元素都有且只有一个直接前驱元素和一个直接后继元素
线性表常见的两种存储结构
顺序存储结构
顺序存储结构是指使用一段地址连续的存储单元依次存储线性表中的数据元素
顺序表:
链式存储结构
链式存储结构是通过指针将各个数据元素的存储单元连接起来
单链表删除结点需要先找到该结点的前驱结点,然后将前驱结点的 next 指针指向该结点的后继结点,然后释放该结点的内存空间。
p→next=q→next
单链表插入结点
- 创建一个新的结点,并给它赋予需要插入的值。
- 找到要插入的位置,即要插入结点的前驱结点。
- 将新结点的 next 指针指向前驱结点的 next 指针指向的结点。
s→next=p→next - 将前驱结点的 next 指针指向新结点。
p→next=s
2.2、顺序存储与链式存储对比
2.3、 队列与栈
循环队列是一种特殊的队列数据结构,它可以通过数组或链表实现。它与普通队列最大的不同在于,当队列满时,循环队列可以将新元素插入到队列头部,从而实现循环使用存储空间的目的
习题:元素按照a、b、c的次序进入栈,请尝试写出其所有可能的出栈序列。
a b c 、b a c 、 c b a 、 b c a
A :左进1 2 3 4 ,左出4 3 2 1 A正确
B:左进1 2 右进 3 左进4,左出4 2 1 3 B正确
C:左进 1 右进 2 左进 3 4,左出4 3 1 2 C正确
D:no
3、广义表
广义表是线性表的推广,它是由原子和广义表组成的多层次结构。广义表中的元素可以是原子,也可以是另一个广义表,用逗号分隔,整个广义表用括号括起来
通常用递归的形式进行定义,记做:LS=(ao,a1,…,an)。
注:其中LS是表名,a;是表元素,它可以是表(称做子表),也可以是数据元素(称为原子)。其中n是广义表的长度(也就是最外层包含的元素个数),n=0的广义表为空表;而递归定义的重数就是广义表的深度,直观地说,就是定义中所含括号的重数(原子的深度为0,空表的深度为1)。
基本运算:取表头head(Ls)和取表尾tail(Ls)
若有:LS1=(a,(b,c),(d,e))
head(LS1)=a
tail(LS1)=((b,c),(d,e))
在广义表中,第一个元素称为表头,它可以是一个原子或一个子表,表示广义表中第一个元素的值;剩余部分称为表尾,也是一个广义表,由原广义表中除第一个元素外的所有元素构成。如果广义表只有一个元素,那么这个元素既是表头,也是表尾。
例1,有广义表LS1=(a,(b,c),(d,e)),则其长度为?深度为?
例2,有广义表LS1=(a,(b,c),(d,e)),要将其中的b字母取出,操作就为?
长度为3,深度为2
head(head(tail(LS1)))
4、树与二叉树
结点的度:指结点拥有的子树数目
树的度:指树中结点的最大度数
叶子结点:度为0的结点,也称为终端结点
分支结点:度不为0的结点,也称为非终端结点或内部结点
内部结点:除根节点和叶子结点外的结点
父结点:某个结点下面的结点,该结点就是它下面的结点的父结点
子结点:某个结点上面的结点,该结点就是它上面的结点的子结点
兄弟结点:有着相同父结点的结点
层次:根结点的层数为1,其余结点的层数为其父结点的层数加1
4.1、特殊二叉树
满二叉树是一种特殊的二叉树,其所有非叶子节点都有两个子节点,且所有叶子节点都在同一层级上
完全二叉树是一种特殊的二叉树,它除了最后一层外,其他所有层都是满的,而且最后一层上的节点都靠左排列
非完全二叉树指的是不符合完全二叉树定义的二叉树,即有些层次不满,且最后一层的叶子节点不一定靠左对齐
二叉树的重要性
1、在二叉树的第i层上最多有2i-1个结点(i≥1)
2、深度为k的二叉树最多有2k-1个结点(k≥1)
3、对任何一棵二叉树,如果其叶子结点数为no,度为2的结点数为n2,则no=n2+1
4、如果对一棵有n个结点的完全二叉树的结点按层序编号(从第1层到log2n+1层,每层从左到右),则对任一结点i(1≥i≥n),有:
如果i=1,则结点i无父结点,是二叉树的根;如果i>1,则父结点是i/2;
如果2i>n,则结点i为叶子结点,无左子结点;否则,其左子结点是结点2i;
如果2i+1>n,则结点i无右子叶点,否则,其右子结点是结点2i+1。
4.2、二叉树遍历
前序遍历:先访问根结点,再依次递归访问左子树和右子树
中序遍历:先递归访问左子树,再访问根结点,最后递归访问右子树
后序遍历:先递归访问左子树和右子树,最后访问根结点
层次遍历:按照从上到下、从左到右的顺序依次访问每个结点
图中前序遍历结果是?(根左右)
1 2 4 5 7 8 3 6
图中中序遍历结果是?(左根右)
4 2 7 8 5 1 3 6
图中后序遍历结果是?(左右根)
4 8 7 5 2 6 3 1
图中层次遍历结果是?
1 2 3 4 5 6 7 8
4.3、反向构造二叉树
由前序序列为ABHFDECG;中序序列为HBEDFAGC构造二叉树。
4.4、树转二叉树
树转二叉树(Tree to Binary Tree)是一种将普通树转化为二叉树的过程。它的主要思想是,对于树中的每个结点,将它的第一个孩子作为左孩子,其他孩子都作为该孩子的右孩子。这样,就可以将普通树转化为二叉树
孩子结点-左子树结点
兄弟结点-右孩子结点
4.5、查找二叉树
二叉排序树
左孩子小于根
右孩子大于根
插入结点:
①若该键值结点已存在,则不再插入,如:48
②若查找二叉树为空树,则以新结点为查找二叉树
③将要插入结点键值与插入后父结点键值比较,就能确定新结点是父结点的左子结点,还是右子结点
删除结点:
①若待删除结点是叶子结点,则直接删除
②若待删除结点只有一个子结点,则将这个子结点与待删除结点的父结点直接连接,如:56
③若待删除的结点p有两个子结点,则在其左子树上,用中序遍历寻找关键值最大的结点s,用结点s的值代替结点p的值,然后删除节点s,节点s必属于上述①,②情况之一,如89
4.6、最优二叉树(哈夫曼树)
需要了解的基本概念:
树的路径长度:指的是从根节点到任意一个节点的边的条数之和
权:是指节点所带有的值,比如树中的权值可以表示节点的大小、权重、出现频率等
带权路径长度:指的是树中每一个叶子节点的权值乘以到根节点路径长度之和。即带权路径长度 = 叶子节点权值 × 路径长度
树的带权路径长度(树的代价):指的是树中所有叶子节点的带权路径长度之和。它可以用来衡量树的形态和节点权值的分布情况,是一种评估树结构优劣的指标
例:假设有一组权值5,29,7,8,14,23,3,11请尝试构造哈夫曼树。
4.7、线索二叉树
为什么要有线索二叉树
在二叉树的遍历过程中,我们需要用到递归或者栈来实现,这种方法虽然简单易行,但是却消耗了大量的空间,因为每次遍历都需要分配新的栈空间。为了避免这种浪费,我们可以使用线索二叉树来进行遍历,它不仅可以减少空间的浪费,还能提高遍历的效率。
线索二叉树的概念
线索二叉树是对普通二叉树的一种改进,它的每个结点除了有左右孩子指针之外,还有指向中序遍历下的前驱和后继的指针,称之为线索
线索二叉树的表示
对于一个二叉树结点来说,如果其左子树为空,则将左指针指向该结点的中序遍历下的前驱结点;如果其右子树为空,则将右指针指向该结点的中序遍历下的后继结点
如何将二叉树转化为线索二叉树
- 对于任意一个结点,如果其左子树不为空,则将其左孩子结点的指针指向该结点的中序遍历下的前驱结点
- 对于任意一个结点,如果其右子树不为空,则将其右孩子结点的指针指向该结点的中序遍历下的后继结点
- 对于根结点,其左指针指向其左子树的中序遍历下的最后一个结点,右指针指向其右子树的中序遍历下的第一个结点
- 对于中序遍历下的最后一个结点,其右指针指向中序遍历的结束标志(如 NULL 或者一个特殊结点)
4.8、平衡二叉树
平衡二叉树的提出原因
解决二叉查找树在极端情况下(如插入递增或递减的序列)可能退化成链表的问题,使得查找、插入、删除的时间复杂度保持在O(log n)的级别
平衡二叉树的定义
平衡二叉树是一种二叉查找树,它的左右子树的高度差不超过1,即任意节点的左右子树的高度差的绝对值不超过1
平衡树的建立过程
通常有两种方法,一种是在空树中直接插入节点,每次插入节点后检查树是否失去平衡,如果失去平衡则对不平衡的节点进行旋转操作;另一种是先将所有节点插入到一个无序表中,然后构建一棵平衡树
动态调平衡问题
在向平衡树中插入或删除节点时,可能会破坏树的平衡性,需要通过旋转操作或其他平衡调整算法来恢复平衡
5、图
完全图
在无向图中,若每对顶点之间都有一条边相连,则称该图为完全图(completegraph)。
在有向图中,若每对顶点之间都有二条有向边相互连接,则称该图为完全图。
5.1、图的存储 - 邻接矩阵
用一个n阶方阵R来存放图中各结点的关联信息,其矩阵元素Rij定义为:
5.2、图的存储 - 邻接表
首先把每个顶点的邻接顶点用链表示出来,然后用一个一维数组来顺序存储上面每个链表的头指针
5.3、图的遍历
5.4、拓扑排序
拓扑排序是指对有向无环图(DAG)进行排序,使得所有的顶点被排序成一个线性序列,满足若存在一条从顶点 A 到顶点 B 的有向路径,则在序列中顶点 A 应该出现在顶点 B 的前面
我们把用有向边表示活动之间开始的先后关系。这种有向图称为用顶点表示活动网络,简称AOV网络。
上图的拓朴序列有:02143567,01243657,02143657,01243567。
5.5、图的最小生成树–普里姆算法
普里姆算法是一种用于求加权连通图的最小生成树的算法。其基本思想是从图中任意选一个顶点开始,不断寻找与该顶点距离最近的未访问顶点,并将该顶点加入最小生成树中
5.5、图的最小生成树–克鲁斯卡尔算法
克鲁斯卡尔算法是求解带权无向连通图的最小生成树问题的一种贪心算法。该算法首先将所有边按照权值从小到大排序,然后从小到大依次加入边,直到生成树中包含所有顶点为止。在加入一条边时,需要判断该边的两个顶点是否已经在生成树中,如果在,则不能加入这条边,否则加入这条边
6、算法基础
6.1、算法的特性
有穷性:执行有穷步之后结束。
确定性:算法中每一条指令都必须有确切的含义,不能含糊不清。
输入(>=0)
输出(>=1)
有效性:算法的每个步骤都能有效执行并能得到确定的结果。例如a=0,b/a就无效
6.2、算法的复杂度
时间复杂度是指程序运行从开始到结束所需要的时间。通常分析时间复杂度的方法是从算法中选取一种对于所研究的问题来说是基本运算的操作,以该操作重复执行的次数作为算法的时间度量。一般来说,算法中原操作重复执行的次数是规模n的某个函数T(n)。由于许多情况下要精确计算T(n)是困难的,因此引入了渐进时间复杂度在数量上估计一个算法的执行时间。其定义如下:
如果存在两个常数c和m,对于所有的n,当n≥m时有f(n)≤cg(n),则有f(n)=O(g(n))。也就是说,随着n的增大,f(n)渐进地不大于g(n)。例如,一个程序的实际执行时间为T(n)=3n3+2n²+n,则T(n)=O(n3)。
常见的对算法执行所需时间的度量:0(1)<0(log2n)<0(n)<0(nlog2n)<0(n2)<0(n3)<0(2n)
空间复杂度是指对一个算法在运行过程中临时占用存储空间大小的度量。一个算法的空间复杂度只考虑在运行过程中为局部变量分配的存储空间的大小
7、排序与查找
7.1、顺序查找
顺序查找的思想:将待查找的关键字为key的元素从头到尾与表中元素进行比较,如果中间存在关键字为key的元素,则返回成功;否则,则查找失败。
7.2、二分查找
二分法查找的基本思想是:(设R[low,.….,high]是当前的查找区)
(1)确定该区间的中点位置:mid=[(low+high)/2];
(2)将待查的k值与R[mid].key比较,若相等,则查找成功并返回此位置,否则需确定新的查找区间,继续二分查找,具体方法如下。
若R[mid].key>k,则由表的有序性可知R[mid…,n].key均大于k,因此若表中存在关键字等于k的结点,则该结点必定是在位置mid左边的子表R[low,.…,mid-1]中。因此,新的查找区间是左子表R[low,….,high],其中high=mid-1。
若R[mid].key<k,则要查找的k必在mid的右子表R[mid+1,.….,high]中,即新的查找区间是右子表R[low,.….,high],其中low=mid+1。·若R[mid].key=k,则查找成功,算法结束。
(3)下一次查找是针对新的查找区间进行,重复步骤(1)和(2)
(4)在查找过程中,low逐步增加,而high逐步减少。如果high<low,则查找失败,算法结束。
折半查找在查找成功时关键字的比较次数最多为log2n+1次。
折半查找的时间复杂度为O(log2n)。
7.3、散列表
散列表查找的基本思想是:已知关键字集合U,最大关键字为m,设计一个函数Hash,它以关键字为自变量,关键字的存储地址为因变量,将关键字映射到一个有限的、地址连续的区间T[0…n-1](n<<m)中,这个区间就称为散列表,散列查找中使用的转换函数称为散列函数。
例:记录关键码为(3,8,12,17,9),取m=10(存储空间为10),p=5,散列函数h=key%p
7.4、排序
排序方法分类
7.4.1、插入排序
1️⃣直接插入排序:即当插入第i个记录时,R1,R2,…,Ri-1均已排好序,因此,将第i个记录R;依次与Ri-1,…,R2,R1进行比较,找到合适的位置插入。它简单明了,但速度很慢。
2️⃣希尔(Shell)排序:先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为d的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-1<O<d2<d1),即所有记录放在同一组中进行直接插入排序为止。该方法实质上是一种分组插入方法。
7.4.2、交换类排序
1️⃣直接选择排序的过程是,首先在所有记录中选出排序码最小的记录,把它与第1个记录交换,然后在其余的记录内选出排序码最小的记录,与第2个记录交换…….依次类推,直到所有记录排完为止。
2️⃣冒泡排序
冒泡排序的基本思想是,通过相邻元素之间的比较和交换,将排序码较小的元素逐渐从底部移向顶部。由于整个排序的过程就像水底下的气泡一样逐渐向上冒,因此称为冒泡算法。
3️⃣快速排序
快速排序采用的是分治法,其基本思想是将原问题分解成若干个规模更小但结构与原问题相似的子问题。通过递归地解决这些子问题,然后再将这些子问题的解组合成原问题的解。
快速排序通常包括两个步骤:
第一步,在待排序的n个记录中任取一个记录,以该记录的排序码为准,将所有记录都分成两组,第1组都小于该数,第2组都大于该数,如图所示。
第二步,采用相同的方法对左、右两组分别进行排序,直到所有记录都排到相应的位置为止。
7.4.3、选择类排序
1️⃣简单选择排序
2️⃣堆排序
堆是一种特殊的树形数据结构,其中每个节点都有一个值,并满足父节点的值总是大于/小于其子节点的值。
堆排序的基本思想为:先将序列建立堆,然后输出堆顶元素,再将剩下的序列建立堆,然后再输出堆顶元素,依此类推,直到所有元素均输出为止,此时元素输出的序列就是一个有序序列。堆排序的算法步骤如下(以大顶堆为例):
(1)初始时将顺序表R[1…n]中元素建立为一个大顶堆,堆顶位于R[1]待序区为R[1…n]。
(2)循环执行步骤3~步骤4,共n-1次。
(3)假设为第/次运行,则待序区为R[1…n-i+1],将堆顶元素R[1]与待序区尾元素R[n-i+1]交换,此时顶点元素被输出,新的待序区为R[1…n-i]。
(4)待序区对应的堆已经被破坏,将之重新调整为大顶堆。
设有n个元素的序列{K1,K2,….,Kn},当且仅当满足下述关系之一时,称之为堆。
(1)ki≤k2i且ki≤k2i+1;
(2)ki≥k2i且ki≥k2i+1;
其中(1)称为小顶堆,(2)称为大顶堆
假设有数组A={1,3,4,5,7,2,6,8,0},初建堆过程如下
大顶堆排序
将顺序表R{80,60,16,50,45,10,15,30,40,20}进行堆排序。
7.4.4、归并排序
归并也称为合并,是将两个或两个以上的有序子表合并成一个新的有序表。若将两个有序表合并成一个有序表,则称为二路合并。合并的过程是:比较A[i]和A[j]的排序码大小,若A[i]的排序码小于等于A[]]的排序码,则将第一个有序表中的元素A[i]复制到R[k]中,并令i和k分别加1;如此循环下去,直到其中一个有序表比较和复制完,然后再将另一个有序表的剩余元素复制到R中。
7.4.5、基数排序
基数排序是一种借助多关键字排序思想对单逻辑关键字进行排序的方法。基数排序不是基于关键字比较的排序方法,它适合于元素很多而关键字较少的序列。基数的选择和关键字的分解是根据关键字的类型来决定的,例如关键字是十进制数,则按个位、十位来分解。