1. 绪论
-
与数据元素本身的形式、内容、大小个数等无关的是数据的(B)
A.存储结构B.逻辑结构C.存储实现D.运算实现 -
从逻辑上可以把数据结构分成(C)
A.动态结构和静态结构B.紧凑结构和非紧凑结构C.线性结构和非线性结构D. 内部结构和外部结构 -
下面哪个是非线性数据结构(A)
A.树B.字符串C.队列D.栈
逻辑结构描述的的是关系,与数据元素本身特点以及计算机参数等没有关系。
算法的五个特性:有穷,确定,可行,输入和输出。
算法的四个评价准则:正确性,可读性,健壮性,高效性。
分析时间复杂度,直接考虑最坏的情况。
-
算法的输入范围为[0,1000],因此 0 和 1000 是该算法输入的边界情况,而超出边界将出现死循环,说明该算法在处理边界情况的时候不够仔细,导致出错,术语上称这种情况为不够健壮或不够鲁棒(robust)。
-
逻辑结构只描述数据元素之间的逻辑关系,在数据结构上发生的实际操作效率受到其存储结构的影响,如用通过下标访问线性表元素这一操作在顺序表实现的复杂度为。如用通过下标访问线性表元素这一操作在顺序表实现的复杂度为 O(1),但在链表实现的复杂度则为 O(n)。
-
逻辑结构:线性,树形(层次),图形,集合(无序)
-
存储结构:顺序,链式
数据结构是一门研究非数值计算的程序设计问题中计算机的操作对象以及他们之间的关系和操作等的学科。
一个数据元素有若干个数据项组成,数据项是数据的不可分割的最小单元。
一些表面上很不相同的数据可以有相同的逻辑结构。
2. 线性表
线性表具有以下特征:
-
有序性:线性表中的元素是有序的,每个元素都有一个确定的位置。通常使用下标来表示元素的位置,从0开始递增。
-
一对一关系:线性表中的每个元素都只有唯一的后继和前驱元素,除了第一个元素没有前驱元素,最后一个元素没有后继元素。
-
可变长度:线性表的长度是可变的,可以动态地添加或删除元素。
-
同类型:线性表中的元素类型通常是相同的,所有元素都属于同一种类型。
-
存储区间连续性:数组连续,链表不连续。
线性表的常见实现方式有数组和链表。
顺序表中有n个数组元素,读取第i个数组元素的平均时间复杂度为O(1)。
顺序表可以近似看做是数组,增删元素会引发很多元素的移动。
单链表的存储密度 < 1。(存储密度 = (结点数据本身所占的存储量)/(结点结构所占的存储总量))
顺序表,单链表插入删除元素的空间复杂度 O(1),时间复杂度为 O(n)。
顺序表n个元素,插入或者删除一个元素,平均要移动元素个数是 n/2 。如果n为127,则平均63.5(可以为小数)。
单链表增加和删除元素虽然不用移动元素,但需先找到其前驱结点,复杂度为(n)。
若线性表需要频繁更新元素,选择顺序表。若需要频繁插入删除元素,选择链表。但是二者的定位元素效率接近,都需要挨个查找。
线性表的集合式合并,时间复杂度O(mn),空间复杂度:1. 顺序表O(m+n) 2. 链表 O(1)
合并两个有序表为一个有序顺序表,时间复杂度和空间复杂度都是 O(m+n)
合并两个有序单链表:时间复杂度O(m+n),空间复杂度O(1)
时间复杂度不含有常数。
链式存取,先查后存取。
无论两个链表多长,合并他们只需要额外开辟一个头节点,空间复杂度为O(1)。
使用头结点实现单链表课降低增删代码的编写难度。
寻址公式:
- 线性寻址公式:
目标地址 = 基地址 + 偏移量
这种寻址方式常用于数组、矩阵等连续内存结构,其中基地址是起始地址,偏移量是元素相对于起始地址的偏移量。 - 哈希寻址公式:
目标地址 = 哈希函数(关键字)
哈希寻址公式使用哈希函数将给定的关键字映射到内存地址上,通常用于哈希表等数据结构,能够快速的进行查找、插入和删除操作。
3. 栈和队列
顺序栈top=base表示空栈,base指向栈底,top指向栈顶元素的后一个元素,随着元素入栈自增。
将一个递归函数实现的算法改为非递归函数实现需要利用栈。 函数调用的本质就是将被调函数压入调用栈使其成为当前栈顶,以优先处理被调函数,再其结束后弹出,让原函数重回栈顶继续执行。
循环队列的好处是避免假溢出现象。而非共享数据空间。
线性表适合随机存取,链表适合顺序存取。
栈(Stack):
- 后进先出(Last-In-First-Out,LIFO)的数据结构。
- 只能在一端进行插入和删除操作,该端被称为栈顶。
- 从栈顶插入新元素称为入栈(Push),从栈顶删除元素称为出栈(Pop)。
- 栈的基本操作包括入栈、出栈和获取栈顶元素。
- 常见的应用有表达式求值、函数调用和递归等。
队列(Queue):
- 先进先出(First-In-First-Out,FIFO)的数据结构。
- 可以在一端进行插入操作,称为队尾,另一端进行删除操作,称为队首。
- 从队尾插入新元素称为入队(Enqueue),从队首删除元素称为出队(Dequeue)。
- 队列的基本操作包括入队、出队和获取队首元素。
- 常见的应用有任务调度、缓冲区管理和广度优先搜索等。
循环队列:
- 判断队空
Q.rear == Q.front
- 队尾指针的后一个位置为队头指针,即队满
(Q.rear + 1) % MaxSize == Q.front
,队尾指针指向队尾元素的后一个位置
4. 树和二叉树
4.1 度
节点的度(Degree)是指一个节点拥有的子树的数量,也即该节点的子节点数目。对于二叉树来说,节点的度最多为2,因为每个节点最多只能有左右两个子节点。
树的度(Degree)是指树中节点的最大子树度。换句话说,树的度是树中各个节点的度的最大值。
4.2 一些概念
把一棵树转换为二叉树后,这棵二叉树的形态是唯一的。
二叉树:
-
满二叉树:装满的二叉树。
-
完全二叉树:可能会在右下角有空缺的二叉树。
-
二叉树中,节点的度指的是结点拥有的子树的数目。
- 将森林转化为二叉树:
- 森林转化为二叉树是确定且唯一的。一棵树转化为二叉树形态唯一。
线索二叉树利用了二叉树中部分空指针域的空间来存储指向前驱节点或后继节点的线索(thread),从而实现快速遍历和查找。在线索二叉树中,指针域原本用于指向左子节点和右子节点的空指针被利用起来,分别指向遍历中的前驱节点和后继节点。这样在进行遍历时,可以不必使用递归或栈来保存遍历路径,不用使用递归,而是用前驱和后继指针快速定位下一个访问的节点。
顺序存储二叉树:
- 顺序树中结点 i(顺序索引) 的左右孩子分别是 2i+1 和 2i+2 (i从0开始计数)
先中后序遍历二叉树,层序遍历。
可以通过 中序 + 先序 / 后序 / 层序 来还原二叉树结构。
1+2+4+8+16+32+48=111 (是第六层,而不是只有六层)
树中元素是一对多的关系。
有n个结点的线索二叉树有n+1条线索。
4.1 哈夫曼树
哈夫曼树(Huffman Tree),也称为最优二叉树(Optimal Binary Tree),是一种特殊的二叉树,它是用于实现数据压缩算法中的一种重要数据结构。
哈夫曼树的特点是,权值较小的节点在树中的深度较大,权值较大的节点在树中的深度较小,这样可以使得权值较大的节点距离根节点较近,从而在编码时生成较短的编码,实现数据的高效压缩。
哈夫曼树的构建过程如下:
- 给定一组权值,每个权值代表一种字符或数据。将每个权值看作是一个树种的一个节点。
- 从这些节点中选择两个权值最小的节点作为左右子节点,构建一个新的父节点,并将父节点的权值设为左右子节点的权值之和。
- 将构建的父节点作为新的节点,加入到剩余节点中。
- 重复步骤2和步骤3,直到所有节点都合并为一个根节点为止,构建完成的二叉树即为哈夫曼树。
构建完成的哈夫曼树具有一个重要特性:权值较小的节点离根节点较远,权值较大的节点离根节点较近。
在数据压缩中,哈夫曼树常被用于生成哈夫曼编码(Huffman Coding)来实现数据的压缩和解压缩。哈夫曼编码是一种可变长度编码,将出现频率高的字符用较短的编码表示,出现频率低的字符用较长的编码表示,从而节省了存储空间。
为什么只依靠 先序 + 后序 不可以还原二叉树的结构?因为中序遍历提供了左右子树如何划分的信息,而仅依靠先+后无法划分左右子树。
任何一颗二叉树的叶子结点在其先序/中序/后序遍历结果中的次序相同。
D. 00 没有兄弟结点,不能成为哈夫曼编码。
分支数 = 结点个数 - 1
总分支数 = 总度数 = n - 1,总度数 = 10,故 n 为 11。
5. 图
一个无向图中,所有顶点的度数之和为边数量的2倍。
一个有向图中,所有顶点的出度之和等于所有顶点的入度之和。
简单路径是指从一个顶点到另一个顶点,具有不重复顶点的连续边的路径。
图结构中,任何两个结点都可能存在多对多的关系。
对于有n个顶点的带权连通图,它的最小生成树是指图中任意一个由n个顶点构成的极小连通子图,且边的权值之和最小。
5.1 普里姆算法和克鲁斯卡尔算法
普里姆算法:
普里姆算法适合构造一个稠密图G的最小生成树。
克鲁斯卡尔算法:
5.2 拓扑排序
拓扑排序(Topological Sort)是对有向无环图(DAG)进行排序的一种算法,它将图中的节点按照一种线性的顺序排列,使得对于图中的任意两个节点 u 和 v,如果存在一条从 u 到 v 的路径,那么 u 在排序中应该在 v 的前面。
拓扑排序的应用场景很多,比如任务调度、依赖关系分析、编译顺序等。在实现拓扑排序时,常用的方法是使用深度优先搜索(DFS)或广度优先搜索(BFS)实现。
拓扑排序的时间复杂度为 O(V + E),其中 V 表示节点的个数,E 表示边的个数。
需要注意的是,拓扑排序只对有向无环图有效,即图中不能存在环。 如果图中存在环,那么不可能找到一个满足条件的排序。
图的深度遍历类似于二叉树的先序遍历,广度遍历类似于层序遍历。
若图中每个顶点之间都没有边连接,那么每个顶点都是一个独立的连通分量,此时最多有n个连通分量。若图中每个顶点都与其他顶点都有边连接,那么整个图是一个强连通图,只有一个连通分量。
连通分量指的是无向图或有向图中的一组顶点,其中任意两个顶点之间存在一条路径可以相互到达,而与其他顶点不连通。
强连通分量指的是有向图中的一组顶点,其中任意两个顶点都可以互相到达。
6. 串
串也是线性表。
字符串匹配:此时索引从 1 开始。
字符串的数据元素是单个字符。
match(a, b, c) // 子串 b 在主串 a 中出现第 c 的位置索引(从1开始)(没有c默认为第一次)。
KMP 算法提前扫描一趟模式串,计算了模式串各个位置的 next 值,其本质上是一个前后缀部分匹配长度表,其目的是能让匹配过程中遇到不匹配字符时,无需回溯,根据该位置的next 值直接移动子串,从而降低了时间复杂度。
字符串线性结构,支持多种存储结构,包括链式存储。
判断两个串是否相等的时间复杂度取决于长度小的串。O(n)(n为长度更小的串)。
长度为1的串(字符串)作为一种数据结构,支持CRUD,与字符串常量有这本质的区别。
7. 查找
二叉查找需要采用顺序存储。
由题目已知元素序号(即下标)范围为1~12。查找1次成功的结点为:6。查找2次成功的结点为:3,9。查找3次成功的结点为:1,4,7,11。查找4次成功的结点为:2,5,8,10,12。成功查找所有结点的总的比较次数为:1×1+2×2+3×4+4×5=37平均比较次数为37/12。
层序排列序列,堆可以看成一棵完全二叉树,根据堆分为大根堆和小根堆分类判断。
二叉排序树=二叉查找树=二叉搜索树,他们的任何一个结点的所有子树都满足左小右大。
构造二叉查找树,第一步是令第一个元素为根。
平衡二叉树,也称为AVL树,是一种特殊的二叉查找树,它的左右子树的高度差不超过1。
二叉查找树的中序遍历就是其所有元素的有序序列。
哈希表在理想情况下可以达到O(1)的查找时间复杂度。
7.2 哈希表
哈希表的构建。 设有一组关键字{9,1,23,14,55,20,80,27},采用哈希函数:H(key)=kay%7,表长为10,请分别用线性探测法、二次探测法解决冲突。对该关键字序列构造哈希表,并求查找成功时的平均查找长度。要求写出步骤。
8. 排序
8.1 插入排序和希尔排序
插入排序是稳定的,时间复杂度是O(n^2),空间复杂度O(1)。
插入排序是将一个待排序的元素按照大小插入到已排序元素序列的合适位置。
具体的步骤如下:
- 从第一个元素开始,将其视为已排序序列。
- 取出下一个待排序元素,依次与已排序序列中的元素进行比较。
- 如果待排序元素小于已排序元素,则将已排序元素后移一位,继续与下一个元素比较。
- 如果待排序元素大于或等于已排序元素,则将该元素插入到已排序元素的后面位置。
- 重复步骤2~4,直到所有待排序元素都被插入到已排序序列中。
简而言之,就是一个一个往已知序列中插入。
希尔排序的基本思想是通过定义一个增量序列,将待排序序列分割成多个子序列,对每个子序列进行插入排序。随着增量的减小,子序列的长度逐渐变小,当增量减小为1时,整个序列被分割成一个子序列,然后再进行最后一次插入排序,最终完成排序。
具体的希尔排序算法如下:
- 选择一个增量序列,可以使用不同的增量序列,例如希尔增量(n / 2,n / 4,…,1)。
- 对于每个增量,将待排序序列分割成若干个子序列,每个子序列的元素下标相差增量。
- 对每个子序列进行插入排序,将子序列内的元素按照插入排序的方式进行排序。
- 重复步骤2和步骤3,直到增量减小为1。
- 最后,使用增量为1进行插入排序,完成整个序列的排序。
希尔排序的时间复杂度与增量序列的选择有关,最坏情况下的时间复杂度为 O(n^2),但平均情况下的时间复杂度要比普通的插入排序低,可以达到 O(nlogn)。希尔排序的空间复杂度为 O(1),是一种原地排序算法。
希尔排序在实践中具有较好的性能,尤其适用于中等大小的数据集。它相对简单易实现,且对于较小规模的数据集比其他复杂度为 O(nlogn) 的排序算法具有更好的性能。然而,希尔排序不具备稳定性,可能会打乱相等元素之间的相对顺序。
用冒泡法对n个关键码排序,在最好情况下,只需做 n - 1 次比较和 0 次移动;在最坏的情况下要做 n(n + 1) / 2 次比较。
8.2 选择排序和堆排序
选择排序是不稳定的,时间复杂度是O(n^2),空间复杂度O(1)。
将序列分为已排序和未排序部分,依次从未排序部分挑选一个最小的(或最大的),添加到已完成部分的末尾。
堆排序(Heap Sort)是一种基于比较的排序算法,利用二叉堆这种数据结构来实现排序。堆排序的基本思想是先将待排序的序列构建成一个最大堆(或最小堆),然后依次将堆顶元素(最大值或最小值)与堆的最后一个元素交换,再重新调整堆,重复这个过程直到整个序列排序完成。
堆排序过程的详细步骤如下:
- 将待排序的序列构建成一个最大堆(或最小堆)。如果要进行升序排序,则构建最大堆;如果要进行降序排序,则构建最小堆。构建堆的过程可以使用下面的操作:
- 从序列的最后一个非叶子节点开始,依次向上调整每个节点,将当前节点与其子节点进行比较,如果当前节点小于(或大于)其子节点,则交换位置,以保持堆的性质。
- 重复上述过程,直到根节点。
- 将堆顶元素与堆的最后一个元素交换位置,这样最大(或最小)的元素就被放在了序列的最后。
- 由于交换后,堆的性质被破坏,需要将堆进行调整,使其满足堆的性质。
- 重复步骤2和步骤3,直到堆中只剩下一个元素,此时整个序列已经有序。
堆排序的时间复杂度为 O(nlogn),其中 n 是序列的长度。堆排序是一种原地排序算法,即不需要额外的空间来存储待排序的数据,所以它的空间复杂度为 O(1)。
堆排序在实践中具有较好的性能,尤其适用于大规模数据集的排序。与其他常见的排序算法相比,堆排序相对复杂一些,需要对堆的构建与调整进行理解和操作。然而,堆排序具有稳定性,可以保留相等元素之间的相对顺序。
冒泡排序是稳定的,因为相邻元素相等不会发生交换,时间复杂度是O(n^2),空间复杂度O(1)。冒泡排序在初始序列比较有序的情况下,效率较高,如果初始序列是倒序的,效率很低。
8.3 二路归并排序
2路-归并排序,稳定,时间复杂度O(n*lgn),空间复杂度O(n)。
对于n个记录的表进行2路归并排序,整个归并排序需进行 log2 n 趟(遍),共计移动 n*log2 n 次记录。(log2 n代表以2为底,n的对数)
归并排序所需要的辅助空间的大小是 n。
逐小堆进行交换,直到全部替换为止。
解题方法可以看这个类似的解析:
8.4 快速排序
快速排序(Quick Sort)是一种常用的基于比较的排序算法,它通过将一个数组分成较小和较大的两个子数组,递归地对子数组进行排序,从而完成整个数组的排序。
快速排序的基本思想是选择一个基准元素(pivot),并将数组中小于基准元素的元素放在它的左边,将大于基准元素的元素放在它的右边,然后对左右两个子数组再进行递归排序。
具体的快速排序算法如下:
- 选择一个基准元素(pivot),通常选择数组的第一个元素或者随机选择一个元素。
- 将数组分割成两个子数组,使得左边的子数组中的元素都小于等于基准元素,右边的子数组中的元素都大于基准元素。这一步通常使用两个指针从两端往中间扫描,当左指针指向元素大于等于基准元素时,停止;当右指针指向元素小于等于基准元素时,停止,然后交换左右指针所指向的元素。
- 递归地对左右两个子数组进行快速排序。
- 重复步骤2和步骤3,直到子数组的大小为1或者为空,即完成排序。
快速排序的时间复杂度为 O(nlogn),其中 n 是数组的长度。在最坏情况下,快速排序的时间复杂度为 O(n^2),但平均情况下的时间复杂度很接近 O(nlogn)。 快速排序是一种原地排序算法,空间复杂度为 O(logn)。
快速排序在实践中具有较好的性能,并且很常见。它具有较高的效率和速度,并且相较于其他排序算法,快速排序的实现相对简单。然而,快速排序不具备稳定性,可能会打乱相等元素之间的相对顺序。在处理大规模数据集时,快速排序通常是一个不错的选择。
每次直接或通过基准元素间接比较两个元素,若出现逆序排列时就交换它们的位置,此种排序方法叫做冒泡排序;每次使两个相邻的有序表合并成一个有序表的排序方法叫做归并排序。
8.5 基数排序
基数排序,稳定。
如果一颗完全二叉树上每一个结点都比其左右孩子大,称之为大顶堆;反之。
8.6 堆排序
8.6 冒泡排序
依次比较相邻元素,将逆序者交换位置。
稳定的,时间复杂度O(n2),空间复杂度O(1)。