测试题
1. 二叉树是一种重要的数据结构,可以用于多项式计算等应用。
(1)请画出多项式中缀表达式8*(2+3)+(4+5)/(9-6)对应的二叉树;
答:
(2)写出该二叉树的先序遍历序列;
答:
二叉树先序遍历序列为 + * 8 + 2 3 / + 4 5 - 9 6
(3)请写出该多项式的后缀表达式;
答:
后缀表达式为:8 2 3 + * 4 5 + 9 6 - / +
(4)用该后缀表达式计算多项式结果的过程需要用到栈,
请写出计算过程中入栈出栈的过程,入栈用Push( ),出栈用Pop( )。
该栈的深度最小是多少?即最少需要多少存储空间
(设一个数字或运算符占一个存储空间)。
答:
Push(8); Push(2); Push(3); Pop(); Pop(); Push(5);
Pop(); Push(40); Push(4); Push(5); Pop(); Pop();
Push(9); Push(9); Push(6); Pop(); Push(3); Pop();
Pop(); Push(3); Pop(); Pop(); Push(43); Pop();
深度最小为4。
(5)如果对该多项式对应的二叉树进行层序遍历,请写出层序遍历的序列; 答:
层序遍历序列为:+ * / 8 + + - 2 3 4 5 9 6。
(6)层遍历过程需要用到队列,该队列最少需要多少存储空间? 答:
7
(7)分析队列和栈这两种线性结构在操作方面的不同特点。 答:
栈(Stack)是限定只能在表的一端进行插入和删除操作的线性表。
队列(Queue)是限定只能在表的一端进行插入和在另一端进行删除操作的线性表。
栈必须按"后进先出"的规则进行操作,而队列必须按"先进先出"的规则进行操作。
2. 事物的矛盾法则——顺序表与链表
毛泽东同志在1937年所著《矛盾论》中指出“事物的矛盾法则,即对立统一的法则,是唯物辩证法的最根本的法则”,“矛盾着的两方面中,必有一方面是主要的,其他方面是次要的。其主要的方面,即所谓矛盾起主导作用的方面。事物的性质,主要地是由取得支配地位的矛盾的主要方面所规定的。”
(1)数据结构中,时间复杂度和空间复杂度往往是一对矛盾,常常无法兼顾。线性表的物理存储方式可以是顺序存储,也可以是链式存储,数据的常见操作包括增加、删除、查找、修改,请从时间、空间消耗的角度以及适用的操作等多个方面分析顺序存储和链式存储二者的优缺点。
答:
① 顺序存储时,相邻数据元素的存放地址也相邻(逻辑与物理统一);要求内存中可用存储单元的地址必须是连续的。
优点:存储密度大(=1),易于查找和修改。
缺点:插入或删除元素时不方便;存储空间利用率低,预先分配内存可能造成存储空间浪费。
②链式存储时,相邻数据元素可随意存放,但所占存储空间分两部分,一部分存放结点值,另一部分存放表示结点间关系的指针
优点:插入或删除元素时很方便,使用灵活,存储空间利用率高。
缺点:存储密度小(<1),查找和修改需要遍历整个链表。
从使用情况比较,顺序表适宜于做查找这样的静态操作;链表宜于做插入、删除这样的动态操作。若线性表的长度变化不大,且其主要操作是查找,则采用顺序表;若线性表的长度变化较大,且其主要操作是插入、删除操作,则采用链表。
比较
顺序表与链表的比较,首先基于空间的比较,顺序表和链表的存储空间均为O(n)。按存储分配的方式比较,顺序表的存储空间是静态分配的,链表的存储空间是动态分配的,若存储密度 = 结点数据本身所占的存储量/结点结构所占的存储总量,则顺序表的存储密度 = 1,链表的存储密度 < 1。
基于时间的比较按存取方式比较,顺序表可以随机存取,也可以顺序存取。链表是顺序存取的,插入/删除时移动元素个数比较,顺序表平均需要移动近一半元素,链表不需要移动元素,只需要修改指针
(2)根据《矛盾论》,不同情况下要考虑矛盾的主要方面,我们如果设计一个学生成绩管理系统,记录一个班级的学生某一门课的成绩,以方便以后查询和统计,这种情况下对数据的主要操作是什么?顺序存储和链式存储哪一种更好?为什么?
答:
对数据的主要操作是读操作,顺序存储查询效率更高。
(3)如果设计一个外卖点餐系统,需要频繁的对订单数据进行增加和删除操作,这种情况更适合用顺序存储还是链式存储?为什么?
答:
更适合链式存储,链式存储分配的内存不连续,当删除,增加或者修个一个数据,不需要移动整张表,只需要修改指针即可。
(4)在学生成绩管理系统中根据学生学号查询某学生成绩,可以采用顺序查找,也可以用二分查找(假设二分查找之前已根据学号对学生记录进行排序)。请分析比较两种查找算法的时间复杂度。
答:
顺序查找和二分查找的时间复杂度分析:
顺序查找需要遍历整个表,直到找到需要的数据即可,时间复杂度为O(n)二分查找的时间复杂度为:总共有n个元素,每次查找的区间大小就是n,n/2,n/4,…,n/2^k(接下来操作元素的剩余个数),其中k就是循环的次数。
由于n/2^k取整后>=1,即令n/2^k=1,
可得k=log2n,(是以2为底,n的对数),所以时间复杂度可以表示O()=O(log2n)
(5)根据学生成绩对全班学生进行排序,要求算法是稳定的,那么选择排序和冒泡排序应该选择哪一种?请用伪代码/代码写出该排序算法函数StudentScoreSort的内容。
答:
要求算法稳定应该选择冒泡排序
原因:
(1)冒泡排序是比较相邻位置的两个数,而选择排序是按顺序比较,找最大值或者最小值;
(2)冒泡排序每一轮比较后,位置不对都需要换位置,选择排序每一轮比较都只需要换一次位置;
(3)冒泡排序是通过数去找位置,选择排序是给定位置去找数;
冒泡排序优缺点:优点:比较简单,空间复杂度较低,是稳定的;
缺点:时间复杂度太高,效率慢;
选择排序优缺点:优点:一轮比较只需要换一次位置;
缺点:效率慢,不稳定伪代码:
要求根据成绩对学生排序伪代码:(写出了StudentScoreSort函数的内容即可得分,不限编程语言,伪代码亦可;排序可以从小到大,也可以从大到小。)
定义一个结构体代表一个学生实体:
struct Student {
public int num;
public string Code;
public string Name;
public decimal Score;
}
static void Main(string[] args) {
ArrayList list = new ArrayList();
Student stu = new Student;
从控制台输入学生的相关信息
List.add(stu)
Print(“-- -- -- -- -- -- -- -- -- -- -- -学生数据展示-- -- -- -- -- -- -- -- -- -- -- -- --”)
for (int i = 1; i < list.size(); i++) {
打印学生的相关信息
}
对学生信息排序调用
StudentScoreSort(list, int
length)
printStudent()
{
For(list
list:
list){
Print(“排序好的学生信息”)
}
}
}
List StudentScoreSort(list, int length) {
for (int i = 0; i < list.Count - 1; i++) {
for (int j = i + 1; j < list.Count - i - 1; j++) {
Student s1 = (Student) list[i];
Student s2 = (Student) list[j];
if (s1.Score < s2.Score) {
交换两条记录的位置
}
}
}
}
3. 散列表
设有一组关键字{16,1,30,7,48,62,91,20},采用哈希函数:H(key)=key mod
7,表长为10,用开放地址法的线性探测法解决冲突。要求:对该关键字序列构造哈希表。
(1)依次给出哈希表地址0至9单元的值。
答:
哈希表:
哈希地址 0 1 2 3 4 5 6 7 8 9
关键字 7 1 16 30 91 48 62 20
冲突次数 0 0 0 1 4 0 1 2
(2)计算该哈希表查找成功、失败的平均查找长度。
答:
查找成功的平均查找次数ASL=(1+1+1+2+5+1+2+3)/8=2
查找失败的平均查找次数为 ASL=(6+5+4+3+2+1+4+3+2+1)/10 = 3.1
4. 哈夫曼树
(1)给定一组数据{6,2,7,10,3,12},构造一棵哈夫曼树,请画出该哈夫曼树。
答:
(2)该哈夫曼树的高度是多少?
答:
根节点从1开始,Huffman树的高度为5。
(3)带权路径长度WPL的值是多少?
答:
wpl =(2+3)*4+6*3+(12+7+10)*2=96
5. 图的应用
目前,我国高铁运营里程突破4万公里,稳居世界第一。
(1)如果我们要为某一地区若干城市建设高铁,要求所有城市都要通车,同时建设费用最少,请问可以通过什么算法得到?
答:
要求全部连通且路径最优可以选择最小生成树算法/Prim算法/Kruskal算法
(2)下图为该地区城市及其距离,假设费用与距离成正比,在资金量有限的情况下,请用两种算法得到费用最低的修建方案,分别写出使用两种算法得到费用最低路线图的每一步增加的线路(用一条线路两端城市名称代表这条线路,如城市A和城市F之间的线路为AF),并画出最终修建方案图。
答:
prim算法:
从最左上角的顶点C出发,从初始只有这个顶点的当前树开始,不断加入边和相关顶点到树中使得当前树不断增长,最终成为一颗最小生成树。
若从C出发 最小生成树的节点集合计为 prim={}
步骤 路径 代价 记录的节点集合
1 c->f 120 {c,f}
2 f->s 150 {c,f,s}
3 s->a 160 {c,f,s,a}
4 a->g 50 {c,f,s,a,g}
5 g->b 70 {c,f,s,a,g,b}
6 b->j 80 {c,f,s,a,g,b,j}
7 a->d 170 {c,f,s,a,g,b,j,d}
8 d->e 90 {c,f,s,a,g,b,j,d,e}
9 e->q 100 {c,f,s,a,g,b,j,d,e,q}
Kruskal算法:
步骤 路径 代价 记录的节点集合
1 a->g 50 {a,g}
2 g->b 70 {a,g,b}
3 b->j 80 {a,g,b,j}
4 e->d 90 {a,g,b,j,e,d}
5 e->q 100 {a,g,b,j,e,d,q}
6 c->f 120 {a,g,b,j,e,d,q,c,f}
7 f->s 150 {a,g,b,j,e,d,q,c,f,s}
8 s->a 160 {a,g,b,j,e,d,q,c,f,s}
9 a-d 170 {a,g,b,j,e,d,q,c,f,s}
(3)随着经济发展,资金量比较充足后,下图的所有线路都修建完成,此时城市F的市民到其他城市游玩,希望能查询到其他每个城市最省钱的路径(路程越短费用越少),如果开发一个软件,提供最省钱(或最快)路径查询功能,应该选择什么算法?并请写出该算法计算城市F到其他城市最省钱路径的步骤。
答:
Dijkstra算法(2分)
(计算过程用文字、表格、图皆可,结果正确可得4分,如有错误酌情扣分。)
FC F->C 120
FE F->E 280
FQ F->E->Q 380
FS F->S 150
FD F->D 250
FA F->A 300
FG F->A->G 350
FB F->A->G->B 420
FJ F->A->J 420
(4)城市A的某销售员准备从A出发到其他所有城市推销他的产品,请选择分别使用深度优先和广度优先算法把该销售员到这一地区所有城市推销的顺序列出来(同一层城市按照城市名字母顺序排列)。
答:
深度优先:ADEFCQSGBJ
广度优先:ADFGJSECBQ
附加题
答:
答:
答:
答:
答:
答:
答:
答:
知识点整理
线性表
线性表是由一系列节点组成的数据结构,每个节点有一个前驱和一个后继。
下表是顺序存储和链式存储的优缺点对比,以表格的形式展示:
存储方式 | 优点 | 缺点 |
---|---|---|
顺序存储 | 访问、查找快,有下标可以直接访问 | 插入、删除效率低,需要大量顺序移动元素;空间浪费,影响运行效率 |
链式存储 | 插入、删除快,只需要修改指针 | 无法随机访问,只能从头找到末尾;需要额外的指针空间,浪费部分空间 |
堆栈和队列
堆栈(Stack)和队列(Queue)是两种常见的数据结构,它们都属于线性结构。
- 堆栈(Stack)
堆栈是一种后进先出的数据结构,即最后插入的元素最先弹出。
堆栈只允许在栈顶进行插入和删除操作。插入操作称为入栈(Push),删除操作称为出栈(Pop)。
堆栈的应用非常广泛,例如表达式求值、递归函数调用等。
- 堆栈的基本操作:
- 入栈(Push):将元素插入到栈顶。
- 出栈(Pop):弹出栈顶元素。
- 查看栈顶元素(Top):查看栈顶元素,但不弹出。
- 判断是否为空(IsEmpty):判断栈是否为空。
- 队列(Queue)
队列是一种先进先出的数据结构,即先插入的元素先弹出。
队列允许在队尾进行插入操作,在队头进行删除操作。
插入操作称为入队(Enqueue),删除操作称为出队(Dequeue)。
队列的应用也非常广泛,例如操作系统中的进程调度、打印机任务排队等。
- 队列的基本操作:
- 入队(Enqueue):将元素插入到队尾。
- 出队(Dequeue):从队头弹出元素。
- 查看队头元素(Front):查看队头元素,但不弹出。
- 判断是否为空(IsEmpty):判断队列是否为空。
树
树是由一组节点组成的层次结构,有顺序存储和链式存储两种方式。
树的遍历有四种方式:先序遍历、中序遍历、后序遍历和层序遍历。
遍历通常使用递归或栈的方法实现。其中,层序遍历需要使用队列来实现。
- 先序+中序,可以得到唯一的二叉树
- 后序+中序,可以得到唯一的二叉树
哈夫曼树是一种最优二叉树,可以用于哈夫曼编码。
考题
- 表达式变成表达式树
- 前缀、中缀和后缀表达式的互相转换
- 先序+中序,可以得到唯一的二叉树
- 后序+中序,可以得到唯一的二叉树
- 哈夫曼树及哈夫曼编码的实现
- 哈希查找中的平均成功查找和平均失败查找
- 图的遍历、最小生成树和最短路径问题
哈希查找
哈希查找(Hash Search)是一种通过计算关键字的哈希值来进行查找的算法,常用于快速查找和索引。哈希查找的核心思想是将关键字通过哈希函数映射到对应的存储位置,从而实现快速查找。
- 哈希函数和哈希表
哈希函数是将任意长度的输入转换为固定长度的输出的函数,通常将输出称为哈希值。
哈希函数的选择对哈希查找的效率有很大影响。常用的哈希函数包括取模法、乘法哈希等。
哈希表是一种将关键字和哈希值对应存储的数据结构,通常使用数组来实现。
在哈希表中,关键字的哈希值对应数组的下标,关键字对应数组元素的值。
- 解决冲突的方法
由于不同的关键字可能会映射到相同的哈希值,因此可能会出现冲突。
常用的解决冲突的方法包括:
- 开放地址法(Open Addressing):在发生冲突时,通过一定规则在哈希表中寻找下一个空闲的位置存储该关键字,直到找到空闲位置为止。
- 链地址法(Chaining):在哈希表的每个位置上维护一个链表,将哈希值相同的关键字存储在链表中。
- 平均成功查找和平均失败查找
平均成功查找是指在哈希表中查找到一个关键字的平均比较次数。
平均失败查找是指在哈希表中查找一个不存在的关键字的平均比较次数。
下表是哈希查找、散列查找以及解决冲突的方法的优缺点对比,以及平均成功查找和平均失败查找的计算公式。
查找方式 | 优点 | 缺点 | 解决冲突的方法 | 平均成功查找 | 平均失败查找 |
---|---|---|---|---|---|
哈希查找 | 查找快速 | 哈希函数选取不当会导致冲突 | 开放地址法、链地址法 | O(1) | O(1) |
散列查找 | 查找快速 | 冲突较多时查找效率下降 | 开放地址法、链地址法 | O(1) | O(n) |
需要根据具体情况选择使用哪种查找方式和解决冲突的方法。
例如,对于数据量较小的情况,可以使用散列查找;
对于数据量较大的情况,可以使用哈希查找。
同时,还需要根据实际情况选择合适的哈希函数和解决冲突的方法,以达到更好的查找效率。
图
两种最小生成树算法: 克鲁斯卡尔算法和普里姆算法
- 克鲁斯卡尔算法
克鲁斯卡尔算法使用贪心策略,从小到大选择边来构建最小生成树。具体步骤如下:
- 将边按照权值从小到大排序。
- 依次选择最小的边,如果选择该边不会形成环,则将它加入最小生成树中。
- 直到选择的边数等于节点数减一为止。
- 普里姆算法
普里姆算法也使用贪心策略,从一个点出发,不断选择与当前树相邻的权值最小的边来扩展生成树。具体步骤如下:
- 选择任意一个节点作为起始点,将它加入已选择的节点集合。
- 从已选择的节点集合中选择距离最近的边连接到未选择的节点中,将该节点加入已选择的节点集合。
- 重复步骤 2,直到已选择的节点集合包含了全部节点。
两种遍历方式:深度优先遍历和广度优先遍历
- 深度优先遍历
深度优先遍历是一种先走尽可能深的搜索策略,从一个起始节点出发,不断沿着未访问过的路径走下去,直到走到尽头或者遇到已经访问过的节点。然后回溯到前一个节点,继续搜索其它未访问过的路径,直到所有节点都被访问过为止。
深度优先遍历通常使用递归或栈的方法实现,在遍历时可以记录节点的访问状态以及路径。
- 广度优先遍历
广度优先遍历是一种从起始节点开始逐层遍历的搜索策略。从起始节点开始,先遍历所有与之相邻的节点,再遍历这些相邻节点的相邻节点,直到遍历到所有节点为止。
广度优先遍历通常使用队列的方法实现,在遍历时可以记录节点的访问状态以及路径长度。
深度优先遍历和广度优先遍历都可以用于图的遍历,但是它们的效率和适用场景不同。深度优先遍历适用于稠密图或者需要搜索整个图的场景,而广度优先遍历适用于稀疏图或者需要搜索特定层数的场景。
最短路径:迪杰斯特拉算法
迪杰斯特拉算法(Dijkstra’s Algorithm)是一种用于求解带权重图中单源最短路径的算法,可以处理有向图或无向图,但不能处理负权边(即边的权重为负数的情况)。
迪杰斯特拉算法的基本思路是从起点开始,依次确定每个节点到起点的最短距离,并加入已确定最短距离的节点集合中。在每次确定最短距离时,需要找到与已确定集合中的节点相邻的未确定节点中距离最短的那个节点,并将其加入已确定集合中。重复这个过程直到所有节点都被标记为已确定最短距离或者找不到更短的路径为止。
具体实现时,可以使用一个数组来记录每个节点到起点的最短距离,初始值为无穷大。然后从起点开始,将起点到自身的距离设为 0,然后将所有相邻节点的距离更新为起点到这些节点的距离。接着,从未确定集合中找到距离最短的节点,将它加入已确定集合中,并更新它的相邻节点到起点的最短距离。重复这个过程直到所有节点都被标记为已确定最短距离或者找不到更短的路径为止。
迪杰斯特拉算法的时间复杂度为 O(n^2),其中 n 是节点数。如果使用堆优化可以将时间复杂度优化到 O(mlogn),其中 m 是边数。因此,在实际应用中,可以根据具体情况选择优化算法来提高效率。
排序
下面是常见的排序算法及其算法稳定性、时间复杂度和空间复杂度的总结:
排序算法 | 算法稳定性 | 时间复杂度(平均) | 空间复杂度 |
---|---|---|---|
冒泡排序 | 稳定 | O(n^2) | O(1) |
插入排序 | 稳定 | O(n^2) | O(1) |
选择排序 | 不稳定 | O(n^2) | O(1) |
快速排序 | 不稳定 | O(nlogn) | O(logn) |
归并排序 | 稳定 | O(nlogn) | O(n) |
堆排序 | 不稳定 | O(nlogn) | O(1) |
其中,算法稳定性指的是排序算法在排序过程中是否能够保持相同元素的顺序不变。时间复杂度指的是排序算法在最坏情况下需要比较的次数,其中 n 是元素个数。空间复杂度指的是排序算法在排序过程中需要占用的额外空间。
- 冒泡排序:比较相邻两个元素的大小,如果前一个元素比后一个元素大,则交换这两个元素的位置,每一轮将最大的元素沉到最后。重复这个过程直到所有元素都有序。时间复杂度为 O(n^2),空间复杂度为 O(1)。由于每次比较都是相邻的元素之间进行的,因此冒泡排序是稳定的排序算法。
- 插入排序:将待排序的元素插入到已经排序好的序列中,插入时从后往前逐个比较,找到插入位置并插入元素。时间复杂度为 O(n^2),空间复杂度为 O(1)。由于插入排序中每次比较都是相邻的元素之间进行的,因此插入排序是稳定的排序算法。
- 选择排序:每次选择未排序部分中最小的元素,然后将其放到已排序部分的末尾。时间复杂度为 O(n^2),空间复杂度为 O(1)。由于选择排序中元素的交换可能会改变相同元素的相对顺序,因此选择排序是不稳定的排序算法。
- 快速排序:选择一个基准元素,将序列分为两部分,一部分小于基准元素,另一部分大于等于基准元素。然后对两部分分别递归进行快速排序。时间复杂度为 O(nlogn),空间复杂度为 O(logn)。由于快速排序中元素的交换可能会改变相同元素的相对顺序,因此快速排序是不稳定的排序算法。
- 归并排序:将序列分为两部分,对两部分分别递归进行归并排序,然后将两部分合并。时间复杂度为 O(nlogn),空间复杂度为 O(n)。由于归并排序中元素的合并过程保证了相同元素的顺序不变,因此归并排序是稳定的排序算法。
- 堆排序:将序列建立最大堆或最小堆,然后每次取出堆顶元素放到已排序部分的末尾。时间复杂度为 O(nlogn),空间复杂度为 O(1)。由于堆排序中元素的交换可能会改变相同元素的相对顺序,因此堆排序是不稳定的排序算法。
需要注意的是,以上时间复杂度和空间复杂度均为平均情况下的复杂度。在最坏情况下,冒泡排序、插入排序和选择排序的时间复杂度均为 O(n^2),而快速排序的时间复杂度为 O(n^2),归并排序和堆排序的时间复杂度为 O(nlogn)。此外,在实际应用中,排序算法的性能还受到数据分布情况、数据规模等因素的影响,因此需要根据具体情况选择合适的排序算法。