数据结构与算法总复习
2021/12/19
- 简述题
1.1. 算法
1.1.1. 解决问题步骤
当解决一个实际应用中的问题,通常情况下,要经过以下步骤:
找出问题
抽象出数学模型
选取合适的数据结构
算法设计
设计计算机程序解决实际问题
1.1.2. 算法的定义及特性
算法是为了解决某类问题而规定的一个有限长度的操作序列 - 有穷性(Finiteness)。算法的有穷性是指算法必须能在执行有限个步骤之后终止;
- 确定性(Definiteness)。算法的每一步骤必须有确切的定义;
- 输入项(Input)。一个算法有0个或多个输入,以刻画运算对象的初始情况,所谓0个输入是指算法本身定出了初始条件;
- 输出项(Output)。一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;
- 可行性(Effectiveness)。算法中执行的任何计算步骤都是可以被分解为基本的可执行的操作步,即每个计算步都可以在有限时间内完成(也称之为有效性)。
评价算法优劣的基本标准:
1、正确性:算法的正确性是评价一个算法优劣的最重要的标准。
2、可读性:算法的可读性是指一个算法可供人们阅读的容易程度。
3、健壮性:健壮性是指一个算法对不合理数据输入的反应能力和处理能力,也称为容错性。
4.高效性(效率与低存储量需求):高效性包括时间复杂度和空间负责度两个方面
1.1.3. 算法的时间复杂度?
算法的时间复杂度表示程序运行完成所需的总时间,它通常用大O表示法来表示。
两种度量方式:事后统计和事前分析
算法主要占据空间为:
1.算法本身要占据的空间(输入、输出、指令、常数、变量等等)。
2.算法所需要的辅助空间。
注意:对于输入数据所占的具体存储空间取决于问题本身,与算法无关。
1.1.4. 请问用于时间复杂度的符号类型是什么?
用于时间复杂度的符号类型包括:
Big Oh:它表示小于或等于目标多项式
Big Omega:它表示大于或等于目标多项式
Big Theta:它表示与目标多项式相等
Little Oh:它表示小于目标多项式
Little Omega:它表示大于目标多项式
1.1.5 频度
频度,在分析算法时间复杂度时,有时需要估算基本操作的原操作,它是执行次数最多的一个操作,该操作重复执行的次数称为频度。
1.1.6 数据结构是一门研究什么内容的学科?
.数据结构是一门研究在非数值计算的程序设计问题中,计算机的操作对象及对象间的关系和施加于对象的操作等的学科。
1.2. 绪论
1.2.1. 数据结构主要包括哪三方面内容? (缺一不可)
数据的逻辑结构、数据的存储结构、数据的运算。
1.2.2. 什么是逻辑结构?什么是存储结构?两者有何关系?
数据的逻辑结构是从逻辑关系上描述数据的,可以看作是从具体问题抽象出来的数学模型;逻辑结构通常可分为:集合结构、线性结构、树形结构、图形(网状)结构,其中线性结构常见的有:线性表、栈、队列、双向队列、数组、串
注意:逻辑结构独立于存储结构之上的,同一逻辑结构采用不同的存储方式,可以得到不一样的存储结构
数据的存储结构是逻辑结构用计算机语言的实现或在计算机中的表示,是逻辑结构在计算机中的存储方式。存储结构分为:顺序存储、链式存储、索引存储、散列存储(哈希存储),其中顺序存储和链式存储为存储结构主要两种
(1)顺序存储方式。数据元素顺序存放,每个存储结点只含一个元素。存储位置反映数据元素间的逻辑关系。存储密度大,但有些操作(如插入、删除)效率较差。
(2)链式存储方式。每个存储结点除包含数据元素信息外还包含一组(至少一个)指针。指针反映数据元素间的逻辑关系。这种方式不要求存储空间连续,便于动态操作(如插入、删除等),但存储空间开销大(用于指针),另外不能折半查找等。
(3)索引存储方式。除数据元素存储在一地址连续的内存空间外,尚需建立一个索引表,索引表中索引指示存储结点的存储位置(下标)或存储区间端点(下标),兼有静态和动态特性。
(4)散列存储方式。通过散列函数和解决冲突的方法,将关键字散列在连续的有限的地址空间内,并将散列函数的值解释成关键字所在元素的存储地址,这种存储方式称为散列存储。其特点是存取速度快,只能按关键字随机存取,不能顺序存取,也不能折半存取。
1.2.3. 数据结构的分类(线性结构与非线性结构)
集合:数据元素除了同属于一种类型外,别无其它关系。
线性结构:数据元素之间存在一对一的关系。
树型结构:数据元素之间存在一对多的关系。
图状结构或网状结构:数据元素之间存在多对多的关系。
1.2.4. 逻辑结构与具体计算机有关吗?存储结构呢?
逻辑结构与计算机实体无关,存储结构与计算机实体有关。
1.2.5. 算法与程序有何关联 ?
算法+数据结构=程序(由瑞士计算机科学家尼古拉斯·沃斯在1984年获得图灵奖的一句话。),算法是解题的方法,由程序设计语言描述的算法就是计算机程序。
1.2.6. 算法分析主要从哪些方面考虑?
算法分析从两方面考虑:时间复杂度与空间复杂度。
1.2.7 算法的时间复杂性
算法的时间复杂性是算法输入规模的函数。算法的输入规模或问题的规模是作为该算法输入的数据所含数据元素的数目,或与此数目有关的其它参数。有时考虑算法在最坏情况下的时间复杂度或平均时间复杂度。
1.2.8 算法的空间复杂度
空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度,记做S(n)=O(f(n))。(注意原地工作不是空间复杂度为0,而是O(1))
1.2.9逻辑结构与存储结构几类问题
(1)在数据结构课程中,数据的逻辑结构,数据的存储结构及数据的运算之间存在着怎样的关系?
(1)数据的逻辑结构反映数据元素之间的逻辑关系(即数据元素之间的关联方式或“邻接关系”),数据的存储结构是数据结构在计算机中的表示,包括数据元素的表示及其关系的表示。数据的运算是对数据定义的一组操作,运算是定义在逻辑结构上的,和存储结构无关,而运算的实现则是依赖于存储结构。
(2)若逻辑结构相同但存储结构不同,则为不同的数据结构。这样的说法对吗?举例说明之。
(2)逻辑结构相同但存储不同,可以是不同的数据结构。例如,线性表的逻辑结构属于线性结构,采用顺序存储结构为顺序表,而采用链式存储结构称为线性链表。
(3)在给定的逻辑结构及其存储表示上可以定义不同的运算集合,从而得到不同的数据结构。这样说法对吗?举例说明之。
(3)栈和队列的逻辑结构相同,其存储表示也可相同(顺序存储和链式存储),但由于其运算集合不同而成为不同的数据结构。
(4)评价各种不同数据结构的标准是什么?
(4)数据结构的评价非常复杂,可以考虑两个方面,一是所选数据结构是否准确、完整的刻划了问题的基本特征;二是是否容易实现(如对数据分解是否恰当;逻辑结构的选择是否适合于运算的功能,是否有利于运算的实现;基本运算的选择是否恰当。)
1.3. 抽象数据类型
1.3.1. 数据[Data]
数据是信息的载体,是描述客观事物的数、字符、以及所有能输入到计算机中,被计算机程序识别和处理的符号的集合。
数据分为数值性数据、非数值性数据。
1.3.2. 数据元素[Data Element]
是数据的基本单位,在计算机程序中通常作为一个整体进行考虑和处理。
在不同的条件下,数据元素又可称为元素、结点、顶点、记录等。
如考生信息系统中考生信息表中的一个记录。
1.3.3. 数据项[Data Item]
组成数据元素的有特定意义的最小单位。在有些场合下,数据项又称为字段或域。一个数据元素可由若干个数据项组成。
例如考生系统中考生信息表的每一个数据元素就是一个考生记录,包括考生的考号、姓名、性别、成绩等数据项。
1.3.4. 数据对象[Data Object]
具有相同性质的数据成员(数据元素)的集合,是数据的一个子集 。
整数数据对象 N = { 0, 1, 2, … }
学生数据对象
1.3.5. 数据类型[Data Type]
在一种程序设计语言中,变量所具有的数据种类。
数据类型是一个值的集合和定义在这个值上的一组操作的总称。
按照值的不同,高级程序设计语言中数据类型可分为两类:一类是非结构的原子类型,另一类是结构类型。
在C语言中数据类型:基本类型和构造类型
基本类型:整型、浮点型、字符型、指针、空类型
构造类型:数组、结构、联合、枚举型、自定义
1.3.6. 数据结构[Data Structure]
是相互之间存在一种或多种特定关系的数据元素的集合。在任何问题中,数据元素之间总是存在联系的。把某一数据对象及该数据对象中所有数据成员之间的关系组成的实体叫做数据结构。
研究数据结构,是指研究数据的逻辑结构和物理结构
数据的逻辑结构:数据元素之间的逻辑关系
数据的物理结构:数据元素在计算机存储器中是如何存储的
1.3.6.1 数据结构与数据类型有什么区别?
数据结构这一术语有两种含义,一是作为一门课程的名称;二是作为一个科学的概念。作为科学概念,目前尚无公认定义,一般认为,讨论数据结构要包括三个方面,一是数据的逻辑结构,二是数据的存储结构,三是对数据进行的操作(运算)。而数据类型是值的集合和操作的集合,可以看作是已实现了的数据结构,后者是前者的一种简化情况。
1.3.7.1 评价各种不同数据结构的标准是什么?
(1)数据的逻辑结构反映数据元素之间的逻辑关系(即数据元素之间的关联方式或“邻接关系”),数据的存储结构是数据结构在计算机中的表示,包括数据元素的表示及其关系的表示。数据的运算是对数据定义的一组操作,运算是定义在逻辑结构上的,和存储结构无关,而运算的实现则是依赖于存储结构。
(2)逻辑结构相同但存储不同,可以是不同的数据结构。例如,线性表的逻辑结构属于线性结构,采用顺序存储结构为顺序表,而采用链式存储结构称为线性链表。
(3)栈和队列的逻辑结构相同,其存储表示也可相同(顺序存储和链式存储),但由于其运算集合不同而成为不同的数据结构。
(4)数据结构的评价非常复杂,可以考虑两个方面,一是所选数据结构是否准确、完整的刻划了问题的基本特征;二是是否容易实现(如对数据分解是否恰当;逻辑结构的选择是否适合于运算的功能,是否有利于运算的实现;基本运算的选择是否恰当。)
1.3.7.2 数据类型和抽象数据类型是如何定义的。二者有何相同和不同之处,抽象数据类型的主要特点是什么?使用抽象数据类型的主要好处是什么?
数据类型是程序设计语言中的一个概念,它是一个值的集合和操作的集合。如C语言中的整型、实型、字符型等。整型值的范围(对具体机器都应有整数范围),其操作有加、减、乘、除、求余等。实际上数据类型是厂家提供给用户的已实现了的数据结构。“抽象数据类型(ADT)”指一个数学模型及定义在该模型上的一组操作。“抽象”的意义在于数据类型的数学抽象特性。抽象数据类型的定义仅取决于它的逻辑特性,而与其在计算机内部如何表示和实现无关。无论其内部结构如何变化,只要它的数学特性不变就不影响它的外部使用。抽象数据类型和数据类型实质上是一个概念。此外,抽象数据类型的范围更广,它已不再局限于机器已定义和实现的数据类型,还包括用户在设计软件系统时自行定义的数据类型。使用抽象数据类型定义的软件模块含定义、表示和实现三部分,封装在一起,对用户透明(提供接口),而不必了解实现细节。抽象数据类型的出现使程序设计不再是“艺术”,而是向“科学”迈进了一步。
1.3.7.3 当你为解决某一问题而选择数据结构时,应从哪些方面考虑?
通常考虑算法所需要的存储空间量和算法所需要的时间量。后者又涉及到四方面:程序运行时所需输入的数据总量,对源程序进行编译所需时间,计算机执行每条指令所需时间和程序中指令重复执行的次数。
1.3.7. 抽象数据类型(Abstract Data Type )
一个数学模型以及定义在该模型上的一组操作。
抽象数据类型实际上就是对该数据结构的定义。因为它定义了一个数据的逻辑结构以及在此结构上的一组算法。
抽象数据类型只是在数据的逻辑结构上讨论问题,与数据的存储结构无关。
1.3.8. 抽象数据类型分类
抽象数据类型按其值的不同特性,分为三种类型:
原子类型:变量的值是不可分解的。
固定聚合类型:变量的值由确定数目的成分按某种结构组成。如复数是由两个实数依确定的次序关系构成。
可变聚合类型:其值的成分数目不确定。如:可定义一个“有序整数序列”的抽象数据类型,其中序列的长度是可变的。
1.3.9. 抽象数据类型的表示法
用三元组描述如下:(D,R,P)
ADT 抽象数据类型名
{
数据对象:{数据对象定义}
数据关系:{数据关系定义}
基本操作:{基本操作定义}
}ADT 抽象数据类型名
其中,数据对象和数据关系的定义用伪码描述,基本操作的定义格式为:
基本操作名(参数表)
初始条件:{初始条件描述}
操作结果:{操作结果描述}
抽象数据类型:书包、书架、班级、宿舍、钱包等
操作:初始化、销毁、是否为空,清空
元素:增、删、改、查
1.3.10. 抽象数据类型示例
ADT Triplet
{
数据对象:D={e1,e2,e3 |e1,e2,e3∈Elemset}
数据关系:R1={〈e1,e2>,<e2,e3>〉
基本操作:
InitTriplet(&T,v1,v2,v3)
DestroyTriplet(&T)
Get(T,i,&e)
Put(&T,i,e)
IsAscending(T)
IsDescending(T)
Max(T,&e)
Min(T,&e)
} ;
其中: Elemset(定义了关系运算的某个集合)
1.3.11. 抽象数据特征
数据的抽象数据类型主要有以下特征:
代码的使用与实现分离开来
数据与操作数据的方法封装在结构中
对于结构内部的不可访问的信息进行隐藏
1.4. 线性表
1.4.1. 线性表
是由n(n>=0)个数据元素(节点) a1 ,a2 ,…,an组成的有限序列。它是一种线性结构。简而言之:是具有相同特性的数据元素的有限序列。
线性表的基本特征是:
1、集合中必存在唯一的一个第一元素。
2、集合中必存在唯一的一个最后元素 。
3、除最后一个元素之外,均有唯一的后继。
4、除第一个元素之外,均有唯一的前驱。
注意:一个顺序表的存储空间与表长度、元素类型、元素各字段类型有关,与元素存储顺序无关。
1.4.2. 顺序表
把线性表的节点按逻辑次序依次存放在一组地址连续的存储单元里。
又称线性表的顺序存储结构活顺序映像;
特点是:
- 逻辑上相邻的数据元素,其物理次序也是相邻的。
- 线性表的顺序存储结构是一种随机存储。
- 删除与插入数据元素时候需要移动大量的元素
- 查找时间复杂度为o(1),删除与插入时间复杂度为o(n)。
- 顺序表的存储密度大(只需要存储数据data即可,链表要存储数据data和指向下个数据元素的指针,指针存储在指针域中)。
在稀疏多项式表达式中,使用顺序存储结构的缺点分为两点:
1.存储空间不够灵活(一种是实际非零项数比较小,浪费很大的存储空间,另外一种是实际非零项数超过了最大存储范围,存储空间不够。); - 运算的空间复杂度高(需要辅助空间)
- 改进办法是:利用链表的存储表示多项式的表达,这个灵活性更大点。
1.4.2.1 线性表的顺序存储结构具有三个弱点
线性表的顺序存储结构具有三个弱点:其一,在作插入或删除操作时,需移动大量元素;其二,由于难以估计,必须预先分配较大的空间,往往使存储空间不能得到充分利用;其三,表的容量难以扩充。线性表的链式存储结构是否一定都能够克服上述三个弱点,试讨论之。
链式存储结构一般说克服了顺序存储结构的三个弱点。首先,插入、删除不需移动元素,只修改指针,时间复杂度为O(1);其次,不需要预先分配空间,可根据需要动态申请空间;其三,表容量只受可用内存空间的限制。其缺点是因为指针增加了空间开销,当空间不允许时,就不能克服顺序存储的缺点。
3.1.1. 三元组表
若线性表顺序存储的每一个节点均是三元组,则该线性表的存储结构称为三元组表。
3.1.2. 存储密度
指节点数据本身所占的存储量和整个节点结构所占的存储量之比。
3.1.3. 链表
用一组任意的存储单元来存放线性表的节点,这组存储单元既可以是连续的,也可以是不连续的。链表中节点的逻辑次序和物理次序不一定相同。
注意:链式存储设计时候,各个不同结点的存储空间可以不连续(这组存储单元可以连续的,也可以不连续的),但是节点内的存储单元地址则必须连续。
链表的特点:
- 线性表的链式存储结构是顺序存取方式。
- 查找时间复杂度为o(n),删除与插入时间复杂度为o(1)。
- 结点空间可以动态申请(sizeof)和释放(free)
- 插入与删除不需要移动大量的元素
- 存储密度小(要储存data数据域和next指针域)
- 链式存储为顺序存取,增加算法复杂度
3.1.4. 链表与顺序表的应用
(1)如果有 n个线性表同时并存,并且在处理过程中各表的长度会动态变化,线性表的总数也会自动地改变。在此情况下,应选用哪种存储结构? 为什么?
1)选链式存储结构。它可动态申请内存空间,不受表长度(即表中元素个数)的影响,插入、删除时间复杂度为O(1)。
(2)若线性表的总数基本稳定,且很少进行插入和删除,但要求以最快的速度存取线性表中的元素,那么应采用哪种存储结构?为什么?
2)选顺序存储结构。顺序表可以随机存取,时间复杂度为O(1)。
3.1.4.1 头指针与头结点之间的根本区别
说明在线性表的链式存储结构中,头指针与头结点之间的根本区别;头结点与首元结点的关系?
在线性表的链式存储结构中。
头指针指链表的指针,若链表有头结点则是链表的头结点的指针,头指针具有标识作用,故常用头指针冠以链表的名字。头结点是为了操作的统一、方便而设立的,放在第一元素结点之前,其数据域一般无意义(当然有些情况下也可存放链表的长度、用做监视哨等等)。
有头结点后,对在第一元素结点前插入结点和删除第一结点,其操作与对其它结点的操作统一了。而且无论链表是否为空,头指针均不为空。首元结点也就是第一元素结点,它是头结点后边的第一个结点。
3.1.5. 双链表
链表的每个节点结构中有两个链域,一个用来存放节点的直接后继的地址,另一个指向其直接前趋。
3.1.6. 循环链表
是一种首位相连的链表。单循环链表形成一个next环,而双循环链表形成next链环和prior链环。
3.1.7. 线性结构的逻辑关系是什么?
节点之间存在一对一的关系,开始节点和终端节点都是唯一的,除了开始节点和终端节点以外,其余节点都有且仅有一个前驱,一个后继。
3.1.8. 顺序表是如何表示数据元素的逻辑关系的?
顺序表把线性表中所有元素按照其逻辑顺序依次存储到从计算机存储器中指定存储位置开始的一块连续的存储空间中。
3.1.9. 单链表的操作特点是什么?
单链表每个节点除数据域外,只设置一个指针域,用以指向其后继节点。由于每个节点只包含有一个指向后继节点的指针,所以当访问过一个节点后,只能接着访问它的后继节点,而无法访问它的前驱结点。
3.1.10. 循环链表的操作特点是什么?
循环链表的特点是表中尾节点的指针域不再是空,而是指向头节点,整个链表形成一个环。因此,从表中任一节点出发,均可找到链表中其他节点。
3.1.11. 如何查找链表是否有循环?
要知道链表是否有循环,我们将采用两个指针的方法。如果保留两个指针,并且在处理两个节点之后增加一个指针,并且在处理每个节点之后,遇到指针指向同一个节点的情况,这只有在链表有循环时才会发生。
3.1.12. 顺序表与链表比较各自的优缺点是什么?
顺序表:存储密度大、存储空间利用率高;可以通过序号直接访问任何数据元素,可以随机存取;但插入和删除操作会引起大量元素的移动;
链表:比顺序表存储密度小、存储空间利用率低;在逻辑上相邻的节点在物理上不必相邻,因此不可以随机存取,只能顺序存取;插入和删除操作方便灵活,不必移动节点,只需修改节点中的指针域即可。
3.1.13. 带头结点的单链表有什么优点?
对带头结点的链表,在表的任何结点之前插入结点或删除表中任何结点,所要做的都是修改前一结点的指针域,因为任何元素结点都有前驱结点。若链表没有头结点,则首元素结点没有前驱结点,在其前插入结点或删除该结点时操作会复杂些。
简而言之:
- 便于首元结点的处理。
- 便于空表与非空表的统一处理。
3.2. 栈
3.2.1. 栈的定义
栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
栈可以解决的问题:
- 数值转换
- 表达式求值
- 括号匹配的检验
- 八皇后问题
- 行编辑程序
- 函数调用
- 迷宫求解
- 递归调用的实现
栈是一种“先进后出”的一种数据结构,有压栈出栈两种操作方式。如下图:
3.2.2. 简述栈的逻辑特点,栈与线性表的异同
栈是先进后出表,栈的插入和删除都从称为栈顶的一端进行,一般线性表可以在线性表的中间及两端进行插入、删除操作。栈是线性表子集(是插入与删除受限的线性表)
3.2.3. 链栈
栈的链式存储结构称为链栈,它是运算受限的单链表,其插入和删除操作仅限制在表头位置上进行。
3.2.4. 栈的“上溢”与“下溢”
当栈满的时候,不能进栈,否则将产生空间溢出,简称“上溢”。上溢是一种出错状态,应该设法避免。
当栈空的时候,还要出栈,产生空间错误,简称“下溢”。下溢是一种结束条件,即问题处理结束。
3.2.5. 栈的分类
栈主要分为两类:静态栈、动态栈。
静态栈:静态栈的核心是一维数组,连续存储,我们只能操作其栈顶元素。
动态栈:静态栈的核心是单链表,离散存储,(砍掉单链表的一些操作[对单链表增加一些限制条件]就可以形成栈),我们只能操作其栈顶节点。
3.2.6. 栈的顺序存储
栈的顺序存储,是有一维数组进行支持的,而基于数组的操作又有静态数组、动态数组。
静态数组是在栈中直接定义数组的大小,然后进行使用。
动态数组是在栈的定义结构中是指针,然后在栈初始化时进行栈大小的申请,如果在进行push操作时,发生栈满的情况,可以对栈容量进行动态扩容,从而适应在实际运行情况下避免发生栈溢出的现象。
3.2.7. 什么是递归算法?
递归算法是一个解决复杂问题的方法,将问题分解成较小的子问题,直到分解的足够小,可以轻松解决问题为止。通常,它涉及一个调用自身的函数。
3.2.8. 提到递归算法的三个定律是什么?
所有递归算法必须遵循三个规律
递归算法必须有一个基点
递归算法必须有一个趋向基点的状态变化过程
递归算法必须自我调用
3.2.9. 用循环比递归效率高吗?
递归和循环两者完全可以互换。不能完全决定性地说循环地效率比递归的效率高。
递归算法:
优点:代码简洁、清晰,并且容易验证正确性。
缺点:它的运行需要较多次数的函数调用,如果调用层数比较深,需要增加额外的堆栈处理(还有可能出现堆栈溢出的情况),比如参数传递需要压栈等操作,会对执行效率有一定影响。但是,对于某些问题,如果不使用递归,那将是极端难看的代码。在编译器优化后,对于多次调用的函数处理会有非常好的效率优化,效率未必低于循环。
循环算法:
优点:速度快,结构简单。
缺点:并不能解决所有的问题。有的问题适合使用递归而不是循环。如果使用循环并不困难的话,最好使用循环。
3.2.10. 对于一个栈,如果输入项序列由A、B、C所组成,试给出全部可能的输出序列
本题利用栈的“后进先出”特点,有如下几种情况:
A进A出B进B出C进C出产生输出序列ABC
A进A出B进C进C出B出产生输出序列ACB
A进B进B出A出C进C出产生颇出序列BAC
A进B进B出C进C出A出产生输出序列BCA
A进B进C进C出B出A出产生输出序列CBA
不可能产生输出序列CAB。
3.3. 队列
3.3.1. 队列
- 队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
解决的问题是:
2. 脱产机打印输出
3. 实时控制系统中,信号按照先后的处理
4. 多用户系统中,分配cpu
5. 按照用户优先级拍成队
6. 网络电文传输,按照到达时间先后处理
队列 (Queue)是一种先进先出(first in first out : FIFO), 运算受限的线性表。它只允许在表的一端进行插入,在另一端进行删除元素。
在队列中,允许插入的一端叫做队尾(rear),允许删除的一段则称为队头(front).
3.3.2. 链队列
队列的链式存储结构简称为链队列,它是限制仅在表头删除和表尾插入的单链表。
3.3.3. 循环队列
为克服顺序队列中“假上溢”现象,将向量空间想象为个首尾相接的圆环,存储在其中的队列称为循环队列。
为充分利用向量空间,克服"假溢出"现象的方法是:将向量空间想象为一个首尾相接的圆环,并称这种向量为循环向量。存储在其中的队列称为循环队列(Circular Queue)。循环队列是把顺序队列首尾相连,把存储队列元素的表从逻辑上看成一个环,成为循环队列。
3.3.4. 队列的分类
队列主要分为两类:
链式队列:链式队列即用链表实现的队列
顺序队列:顺序队列是用数组实现的队列,顺序队列通常必须是循环队列
3.3.5. 什么时候该使用顺序队列?什么时候该使用链式队列?
如果用户的应用程序中设有循环队列,则必须为它设定一个最大队列长度,若用户无法预估所用队列的最大长度,则宜采用链式队列。
3.4. 字符串/矩阵/数组
3.4.1. 字符串
由零个或多个字符组成的有限序列。
3.4.2. 简述有效位移与无效位移的区别
模式匹配中,所找到的与模式匹配的子串,其开始字符离开目标串首字符的位移称为有效位移。目标串除有效位移以外,其他字符的位移称为无效位移。
3.4.3. 行表
记录稀疏矩阵中每行非零元素在三元表组中的起始位置的表。
3.4.4. 带状矩阵
所有非零元素均集中在以主对角线为中心的带状区域的矩阵。
3.4.5. 对称矩阵
在一个A阶方阵 A中,若元素满足 a[i][j]=a[j][i](0<=i, j<=n-l),则称 A为对称矩阵。
3.4.6. 三角矩阵
主对角钱以上或以下的元素(不包括对角钱)均为常数的矩阵。
3.4.7. 递归
若在一个函数、过程或者数据结构定义的内部直接(或间接)出现有定义本身的应用,则称它们是递归的,或者是递归定义的。
3.5. 二叉树
1.树是一些点的集合,这个集合可以为空,若不为空,则它是由一个根节点和0个或多个为空的子树组成,且每个子树都被一条来自根节点的有向边相连。
2.树叶:没有儿子的节点;兄弟:具有相同父亲的节点;类似还有祖父和孙子节点。
3.路径:节点n1,n2,n3,…,nk的一个序列,使得对于1 <= i <= k节点ni是ni+1的父亲;路径的长为路径上边的数量,即K+1。
4. 深度:某节点的深度为树根到该节点的唯一路径的长度。
5. 层次:深度相同的节点在同一层中,深度值为层数。
6. 树高度:叶节点的深度最大值。
7. 树宽度:树的各层中节点数最多的一层的节点数为树的宽度。
8. 无序树:如果树中结点的各子树之间的次序是不重要的,可以交换位置。
9. 有序树:如果树中结点的各子树之间的次序是重要的, 不可以交换位置。
10. 森林:0个或多个不相交的树组成。对森林加上一个根,森林即成为树;删去根,树即成为森林。
11. 二叉树:一种特殊的树,每个双亲的孩子数不超过2个(0个,1个或2个),提供对元素的高效访问。有左孩子和右孩子。
12. 退化树:树中只有一个叶子结点,每个非叶子结点只有一个孩子。一颗退化树等价于一个链表。
13. 把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
(01) 每个节点有零个或多个子节点;
(02) 没有父节点的节点称为根节点;
(03) 每一个非根节点有且只有一个父节点;
(04) 除了根节点外,每个子节点可以分为多个不相交的子树。
二叉树有以下几个性质:TODO(上标和下标)
性质1:二叉树第i层上的结点数目最多为 2{i-1} (i≥1)。
性质2:深度为k的二叉树至多有2{k}-1个结点(k≥1)。
性质3:包含n个结点的二叉树的高度至少为log2 (n+1)。
性质4:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1。
13.1.1. 树与二叉树与森林区别
从概念上讲,树,森林和二叉树是三种不同的数据结构,将树,森林转化为二叉树的基本目的是什么,并指出树和二叉树的主要区别。
1.树的孩子兄弟链表表示法和二叉树二叉链表表示法,本质是一样的,只是解释不同,也就是说树(树是森林的特例,即森林中只有一棵树的特殊情况)可用二叉树唯一表示,并可使用二叉树的一些算法去解决树和森林中的问题。
树和二叉树的区别有三:一是二叉树的度至多为2,树无此限制;二是二叉树有左右子树之分,即使在只有一个分枝的情况下, 也必须指出是左子树还是右子树,树无此限制;三是二叉树允许为空,树一般不允许为空(个别书上允许为空)。
13.1.2. 树和二叉树之间有什么样的区别与联系?
树和二叉树逻辑上都是树形结构,树和二叉树的区别有三:一是二叉树的度至多为2,树无此限制;二是二叉树有左右子树之分,即使在只有一个分枝的情况下, 也必须指出是左子树还是右子树,树无此限制;三是二叉树允许为空,树一般不允许为空(个别书上允许为空)。二叉树不是树的特例。
13.1.2.1 请分析线性表、树、广义表的主要结构特点,以及相互的差异与关联。
线性表属于约束最强的线性结构,在非空线性表中,只有一个“第一个”元素,也只有一个“最后一个”元素;除第一个元素外,每个元素有唯一前驱;除最后一个元素外,每个元素有唯一后继。树是一种层次结构,有且只有一个根结点,每个结点可以有多个子女,但只有一个双亲(根无双亲),从这个意义上说存在一(双亲)对多(子女)的关系。广义表中的元素既可以是原子,也可以是子表,子表可以为它表共享。从表中套表意义上说,广义表也是层次结构。从逻辑上讲,树和广义表均属非线性结构。但在以下意义上,又蜕变为线性结构。如度为1的树,以及广义表中的元素都是原子时。另外,广义表从元素之间的关系可看成前驱和后继,也符合线性表,但这时元素有原子,也有子表,即元素并不属于同一
数据对象。
13.1.2.2 设有一棵算术表达式树,用什么方法可以对该树所表示的表达式求值?
方法有二。一是对该算术表达式(二叉树)进行后序遍历,得到表达式的后序遍历序列,再按后缀表达式求值;二是递归求出左子树表达式的值,再递归求出右子树表达式的值,最后按根结点运算符(+、-、、/ 等)进行最后求值。
(2) 一棵有n(n>0)个结点的d度树, 若用多重链表表示, 树中每个结点都有d个链域, 则在表示该树的多重链表中有多少个空链域? 为什么?
n(n>0)个结点的d度树共有nd个链域,除根结点外,每个结点均有一个指针所指,故该树的空链域有nd-(n-1)=n(d-1)+1个。
13.1.2.3 证明叶子结点和度为2结点关系
证明:设二叉树度为0和2的结点数及总的结点数分别为n0,n2 和n,则n=n0+n2 … (1)再设二叉树的分支数为B, 除根结点外,每个结点都有一个分支所指,则 n=B+1… … …
(2)度为零的结点是叶子,没有分支,而度为2的结点有两个分支,因此(2)式可写为
n=2n2+1 ……………
(3)由(1)、(3)得n2=n0-1,代入(1),并由(1)和(2)得B=2*(n0-1)。 证毕。
13.1.2.4 深度为L的满K叉树有以下性质
一个深度为L的满K叉树有以下性质:第L层上的结点都是叶子结点,其余各层上每个结点都有K棵非空子树,如果按层次顺序从1开始对全部结点进行编号,求:
1)各层的结点的数目是多少?
(1)kh-1(h为层数)
2)编号为n的结点的双亲结点(若存在)的编号是多少?
(2)因为该树每层上均有Kh-1个结点,从根开始编号为1,则结点i的从右向左数第2个孩子的结点编号为ki。设n 为结点i的子女,则关系式(i-1)k+2<=n<=ik+1成立,因i是整数,故结点n的双亲i的编号为ën-2)/kû+1。
3)编号为n的结点的第i 个孩子结点(若存在)的编号是多少?
(3) 结点n(n>1)的前一结点编号为n-1(其最右边子女编号是(n-1)*k+1),故结点 n的第 i个孩子的编号是(n-1)*k+1+i。
4)编号为n的结点有右兄弟的条件是什么?如果有,其右兄弟的编号是多少?
请给出计算和推导过程。
(4) 根据以上分析,结点n有右兄弟的条件是,它不是双亲的从右数的第一子女,即 (n-1)%k!=0,其右兄弟编号是n+1。
13.1.3. 二叉树序列问题
如果给出了一个二叉树结点的前序序列和对称序序列,能否构造出此二叉树?若能,请证明之。若不能,请给出反例。如果给出了一个二叉树结点的前序序列和后序序列,能否构造出此二叉树?若能,请证明之。若不能,请给出反例?
给定二叉树结点的前序序列和对称序(中序)序列,可以唯一确定该二叉树。因为前序序列的第一个元素是根结点,该元素将二叉树中序序列分成两部分,左边(设l个元素)表示左子树,若左边无元素,则说明左子树为空;右边(设r个元素)是右子树,若为空,则右子树为空。根据前序遍历中“根¬—左子树—右子树”的顺序,则由从第二元素开始的l个结点序列和中序序列根左边的l个结点序列构造左子树,由前序序列最后r个元素序列与中序序列根右边的r个元素序列构造右子树。
由二叉树的前序序列和后序序列不能唯一确定一棵二叉树,因无法确定左右子树两部分。例如,任何结点只有左子树的二叉树和任何结点只有右子树的二叉树,其前序序列相同,后序序列相同,但却是两棵不同的二叉树。
① 试找出满足下列条件的二叉树
1) 先序序列与后序序列相同 2)中序序列与后序序列相同
2) 先序序列与中序序列相同 4)中序序列与层次遍历序列相同
1.若先序序列与后序序列相同,则或为空树,或为只有根结点的二叉树
2.若中序序列与后序序列相同,则或为空树,或为任一结点至多只有左子树的二叉树.
3.若先序序列与中序序列相同,则或为空树,或为任一结点至多只有右子树的二叉树.
4.若中序序列与层次遍历序列相同,则或为空树 或为任一结点至多只有右子树的二叉树
13.1.4. 由三棵树组成的森林转换为二叉树
森林转为二叉树的三步:
(1)连线(将兄弟结点相连,各树的根看作兄弟);
(2)切线(保留最左边子女为独生子女,将其它子女分枝切掉);
(3)旋转(以最左边树的根为轴,顺时针向下旋转45度)。
其实经过(1)和(2),已转为二叉树,
执行(3)只是为了与平时的二叉树的画法一致
13.1.5. 中序遍历
中序遍历左子树,再访问根节点,最后中序遍历右子树。简记为LNR。
13.1.6. 二叉树的遍历
(1)前序遍历:先双亲、再左孩子、最后右孩子;
(2)中序遍历:先左孩子、再双亲、最后右孩子;
(3)后序遍历:先左孩子、再右孩子、最后双亲;
(4)层次遍历:一层一层,从左到右、从上到下遍历;
注意:
(1)已知前序、后序遍历结果,不能推导出一棵确定的树;
(2)已知前序、中序遍历结果,能够推导出后序遍历结果;
(2)已知后序、中序遍历结果,能够推导出前序遍历结果;
13.1.7. 二叉树
是一种特殊的树。在二叉树中,每个节点最多只有两棵子树,并且子树有左右之分。(它与度数为2的树有区别,在一般树中若某节点只有一个孩子,就无需区分其左右次序,而在二叉树中即使是一个孩子也有左右之分。)
13.1.8. 有序树
树中节点的各子树看成是从左至右依次有序且不能交换。
13.1.9. 满二叉树
一棵深度为k且有2k-1个节点的二叉树称为满二叉树。
13.1.10. 完全二叉树
在一棵二叉树中,除最底层外,其余层都是满的,并且最底层或者满的或者在右边缺少连续若干个节点。
13.1.11. 霍夫曼(Huffman)树
又称最优二叉树。它是n个带权叶子节点构成的所有二叉树中带权路径长度WPL最小的二叉树。
13.2. 图
13.2.1. 图定义
图G由两个集合V和E组成,记为G=(V, E),其中V是顶点(节点)的有穷非空集合,E是由V中顶点的序偶组成的有穷集,这些序偶称为边。
13.2.2. 完全图
图G中任意两个顶点都有一条边相连接,称该图为完全图。
13.2.3. 简单路径
一条路径上除了开始顶点和结束顶点外,其余顶点均不相同。
13.2.4. 有向图
图G中的每条边都是有方向的即E(G)为有向边的集合,称该图为有向图。
13.2.5. 稠密图
在很多边的图中,对无向图边接近(n-l)/2,对有向图边接近于n(n-l),此类图称为稠密图。
13.2.6. 生成树
连通图G的一个子图如果是一棵包含G的所有顶点的树,则该子图称为G的生成树。
13.3. 综合问答
13.3.1. 顺序存储方式是如何表示数据元素之间的关系?其存储地址一定连续吗?
顺序存储结构是,把逻辑上相邻的节点存储在物理位置上相邻的存储单元里,节点间的逻辑关系由存储单元的邻接关系来体现。其存储地址一定连续。
13.3.2. 链式存储方式是如何表示数据元素之间的关系?其存储地址一定连续吗?
链式存储结构,不要求逻辑上相邻的节点在物理位置上也相邻,节点间的逻辑关系由附加的指针字段表示。其存储地址不一定连续。
13.3.3. 链表的操作特点是什么?
单链表每个节点除数据域外,只设置一个指针域,用以指向其后继节点。由于每个节点只包含有一个指向后继节点的指针,所以当访问过一个节点后,只能接着访问它的后继节点,而无法访问它的前驱结点。
13.3.4. 循环链表的操作特点是什么?
循环链表的特点是表中尾节点的指针域不再是空,而是指向头节点,整个链表形成一个环。因此,从表中任一节点出发,均可找到链表中其他节点。
13.3.5. 顺序表与链表比较各自的优缺点是什么?
顺序表:存储密度大、存储空间利用率高;可以通过序号直接访问任何数据元素,可以随机存取;但插入和删除操作会引起大量元素的移动;
链表:比顺序表存储密度小、存储空间利用率低;在逻辑上相邻的节点在物理上不必相邻,因此不可以随机存取,只能顺序存取;插入和删除操作方便灵活,不必移动节点,只需修改节点中的指针域即可。
13.3.6. 带头结点的单链表有什么优点?举例说明。
对带头结点的链表,在表的任何结点之前插入结点或删除表中任何结点,所要做的都是修改前一结点的指针域,因为任何元素结点都有前驱结点。若链表没有头结点,则首元素结点没有前驱结点,在其前插入结点或删除该结点时操作会复杂些。
13.3.7. 简述顺序表和链表存储方式的特点。
答:顺序表的优点是可以随机访问数据元素,缺点是大小固定,不利于增减结点(增减结点操作需要移动元素)。链表的优点是采用指针方式增减结点,非常方便(只需改变指针指向,不移动结点)。其缺点是不能进行随机访问,只能顺序访问。另外,每个结点上增加指针域,造出额外存储空间增大。
13.3.8. 对链表设置头结点的作用是什么?(至少说出两条好处)
答:其好处有:
(1)对带头结点的链表,在表的任何结点之前插入结点或删除表中任何结点,所要做的都是修改前一个结点的指针域,因为任何元素结点都有前驱结点(若链表没有头结点,则首元素结点没有前驱结点,在其前插入结点和删除该结点时操作复杂些)。
(2)对带头结点的链表,表头指针是指向头结点的非空指针,因此空表与非空表的处理是一样的。
13.3.9. 链表的头结点和尾节点的用处
某些情况下设置尾指针的好处
尾指针是指向终端结点的指针,用它来表示单循环链表可以使得查找链表的开始结点和终端结点都很方便,设一带头结点的单循环链表,其尾指针为rear,则开始结点和终端结点的位置分别是rear->next->next 和 rear, 查找时间都是O(1)。 若用头指针来表示该链表,则查找终端结点的时间为O(n)。
在链表中设置头结点的好处
头结点即在链表的首元结点(即存储实际数据的第一个节点)之前附设的一个结点,该结点的数据域可以为空,也可存放表长度等附加信息,其作用是为了对链表进行操作时,可以对空表、非空表的情况以及对首元结点进行统一处理,编程更方便。
13.3.10. 链表中头指针和头结点的理解
线性表使用顺序(数组)存储时有个弊端,那就是在插入和删除时需要大量的移动数据,这显示是非常消耗时间的,所以可以采用链式存储,即有一个指针域(单链表),来记录下个结点的存储位置(地址),这样在插入和删除结点时只需要修改指针域即可,从而大量减少移动数据所消耗的时间。来看链表的定义:
struct node
{
int data;
struct node *next;
};
其中有两个元素,data为数据域,用于存储数据,next为指针域,用于存储下个结点的位置(地址)。
struct Node
{
ElemType data;
struct Node *next;
};
typedef struct Node LNode;
typedef struct Node *LinkedList;
我们把指向第一个结点的指针称为头指针,那么每次访问链表时都可以从这个头指针依次遍历链表中的每个元素,例如:
struct node first;
struct node *head = &first;
这个head指针就是头指针。
这个头指针的意义在于,在访问链表时,总要知道链表存储在什么位置(从何处开始访问),由于链表的特性(next指针),知道了头指针,那么整个链表的元素都能够被访问,也就是说头指针是必须存在的。
很多时候,会在链表的头部附加一个结点,该结点的数据域可以不存储任何信息,这个结点称为头结点,头结点的指针域指向第一个结点,例如:
struct node head, first;
head.next = &first;
13.3.11. 数据结构中的头结点、头指针、开始结点有什么区别
开始结点是指链表中的第一个结点,它没有直接前驱。
头指针是指指向开始结点的指针(没有头结点的情况下)。一个单链表可以由其头指针唯一确定,一般用其头指针来命名单链表。
头结点是在链表的开始结点之前附加的一个结点。
有了头结点之后头指针指向头结点,不论链表是否为空,头指针总是非空,而且头结点的设置使得对链表的第一个位置上的操作与在表中其它位置上的操作一致。
13.3.12. 头指针和头结点的区别
头指针:
头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
头指针具有标识作用,所以头指针冠以链表的名字(指针变量的名字)
无论链表是否为空,头指针均不为空
头指针是链表的必要元素
头结点:
头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(但也可以用来存放链表的长度)
有了头结点,对在第一元素结点前插入结点和删除第一结点起操作与其它结点的操作就统一了
头结点不一定是链表的必要元素
13.3.13. 链表头结点存在的意义
数据结构中,在单链表的开始结点之前附设一个类型相同的结点,称之为头结点。头结点的数据域可以不存储任何信息,头结点的指针域存储指向开始结点的指针(即第一个元素结点的存储位置)。
作用
1、防止单链表是空的而设的.当链表为空的时候,带头结点的头指针就指向头结点.如果当链表为空的时候,单链表没有带头结点,那么它的头指针就为NULL.
2、是为了方便单链表的特殊操作,插入在表头或者删除第一个结点.这样就保持了单链表操作的统一性!
3、单链表加上头结点之后,无论单链表是否为空,头指针始终指向头结点,因此空表和非空表的处理也统一了,方便了单链表的操作,也减少了程序的复杂性和出现bug的机会。
4、对单链表的多数操作应明确对哪个结点以及该结点的前驱。不带头结点的链表对首元结点、中间结点分别处理等;而带头结点的链表因为有头结点,首元结点、中间结点的操作相同 ,从而减少分支,使算法变得简单,流程清晰。对单链表进行插入、删除操作时,如果在首元结点之前插入或删除的是首元结点,不带头结点的单链表需改变头指针的值,在C 算法的函数形参表中头指针一般使用指针的指针(在C+ +中使用引用 &);而带头结点的单链表不需改变头指针的值,函数参数表中头结点使用指针变量即可。
13.3.14. 几种链表
带头链表:固定一个节点作为头结点(数据域不保存有效数据),起一个标志位的作用,以后不管链表节点如果改变,此头结点固定不变。
单向链表:节点中的指针域中只有一个指针,只能从一个方向进行查询,遍历
双向链表:节点的指针域有两个指针,可以从正反两个方向,对链表进行操作
循环链表:节点的指针域有两个指针,链表首尾相连
13.3.15. 数组和链表的区别
从逻辑结构上来看,数组必须实现定于固定的长度,不能适应数据动态增减的情况,即数组的大小一旦定义就不能改变。当数据增加是,可能超过原先定义的元素的个数;当数据减少时,造成内存浪费;链表动态进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。
从内存存储的角度看;数组从栈中分配空间(用new则在堆上创建),对程序员方便快速,但是自由度小;链表从堆中分配空间,自由度大但是申请管理比较麻烦。
从访问方式类看,数组在内存中是连续的存储,因此可以利用下标索引进行访问;链表是链式存储结构,在访问元素时候只能够通过线性方式由前到后顺序的访问,所以访问效率比数组要低。
13.3.16. 简述线形结构与非线形结构的不同点
线形结构的逻辑特征是除开始节点和终端节点外,其余每个节点只有一个直接前趋和一个直接后继,即节点间存在一对一的关系;而非线形结构的逻辑特征是一个节点可以有多个直接前趋和直接后继,即节点间存在多对多的关系。
13.3.17. 简述静态分配的顺序串与动态分配的顺序串的区别
程序运行前被分配以一个给定大小的存储空间的顺序串称为静态顺序串。在程序运行过程中,动态分配空间可以链表形式存在的顺序串称为动态顺序串,静态串存在于内存一片连续的数据区中,动态串存在与内存堆中。
13.3.18. 说明头指针和头节点的作用
头指针是指向链表表头节点的指针,只要链表存在,该指针始终不会改变,已知该指针便已知该链表。头节点是在链表的开始节点之前附加的一个节点,是链表的表头,当链表不空时,其内的指针指向链表的第一个节点,当链表是空链表时,该指针为空指针。这样在链表的第一个位置上的操作就和在表的其他位置上操作一样,无须进行特殊处理。当链表是空链表时,该指针为空指针。因此空表和非空表的处理也就统一了。
13.3.19. 与顺序队列相比,循环队列有哪些优点?
顺序队列中存在“假上溢”现象。因为在入队和出队操作中,头尾指针只增加不减少,致使被删元素的空间永远无法重新利用。因此,尽管队列中实际的元素个数远远小于向量空间的规模,但也可能由于尾指针已超越向量空间的上界而不能做入队操作。该现象称为“假上溢”。
为充分利用向量空间,克服上述假上溢现象的方法是将向量空间想象为一个首尾相接的圆环,并称这种向量为循环向量,存储在其中的队列称为循环队列。在循环队列中进行出队、入队操作时,头尾指针仍要加1,朝前移动。只不过当头尾指针指向向量上界时,其加1操作是指向向量的下界。
13.3.20. 写出按行优先与列优先顺序存储的三维数组元素aijk的地址计算公式。
按行优先存储:Address(aijk)=Address(a000)+[imn+jn+k]d
按行优先存储: Address(aijk)=Address(a000)+ [k l m+ j* l+i]* d
13.3.21. 比较栈和队列的异同点
栈和队列都是加了限制的线性表,栈是先进后出表,队列是先进先出表。栈和队列的插入和删除操作都在端点进行,栈的插入和删除在同一端点,队列的插入和删除在不同的端点进行。
13.3.22. 简述算法复杂度的评价方法
算法复杂度可分为时间复杂度和空间复杂度。时间复杂度是该算法所耗费的时间,它是问题规模n的函数,可用算法的渐近时间复杂度作为时间复杂度。时间复杂度是该算法所耗费的存储,也是问题规模n的函数,同样可用算法的渐近空间复杂度作为空间复杂度。
14. 数据结构
14.1. 抽象数据类型
14.1.1. 顺序存储、链式存储
#define MAX 1000
#define TRUE 1
#define FALSE 0
顺序存储
第一种 第二种 第三种
struct book
{
char bk_id[50];
char bk_name[50];
float price;
} ;
struct bag
{
Struct book bk[MAX];
Int len;
} typedef struct book
{
char bk_id[50];
char bk_name[50];
float price;
}Book ;
typedef struct bag
{
Book bk[MAX];
int len;
}Bag; typedef struct book
{
char bk_id[50];
char bk_name[50];
float price;
}Book ;
typedef struct bag
{
Book *bk;
int len;
}Bag;
int InitBag(Bag *bg) //第三种情况下的初始化
{
Book *tmpbk;
tmpbk =(Book *)malloc(sizoeof(Book)*MAX);
If(bk==NULL) return FASLE;
bg -> bk= tmpbk;
bg -> len=0;
Return TRUE;
}
链式存储
Book Bag//头指针 Bag//头结点
typedef struct book
{
char bk_id[50];
char bk_name[50];
float price;
struct book *next;
struct book *prior;
}Book ; typedef struct bag
{
Book *head; //头指针
int len;
}Bag;
typedef struct bag
{
Book head; //头结点
int len;
}Bag;
14.1.2. 链表
struct Node{
int data; //数据域
struct Node *pNext; // 指针域 - 指向相同类型的下一个节点
};
typedef struct Node *PNODE;// PNODE 等价于 struct Node * ,
typedef struct Node PNODE;//NODE 等价于 struct Node
struct list
{
PNODE pHead;
int len;
} ;
typedef struct list *Plist,List;
void init(Plist); /*初始化/
void InsertList(Plist,int); /*插入/
int DeleteList(Plist, int *); /*删除/
void traverse(Plist); /*遍历打印/
int isEmpty(Plist); /*是否为空链表/
void clearList(Plist); /*清空链表/
14.1.3. 栈
typedef struct Node{ // 节点
int data;
struct Node *pNext;
}*PNODE,NODE; typedef struct Stack{ // 栈
PNODE pTop;
PNODE pBottom;
int len;
}STACK,*PSTACK;
void init(PSTACK); /*栈的初始化/
void push(PSTACK,int); /*压栈/
int pop(PSTACK , int *); /*出栈/
void traverse(PSTACK); /*遍历打印栈/
int isEmpty(PSTACK); /*是否为空栈/
void clearStack(PSTACK); /*清空栈/
14.1.4. 队列
typedef struct Node{ // 节点
int data;
struct Node *pNext;
}*PNODE,NODE; typedef struct Queue{
NODE *front; // 队头
NODE rear; // 对尾
int len;
}QUEUE;
void init_queue(QUEUE pQueue) / 初始化队列 */
bool en_queue(QUEUE pQueue , NODE val) / 入队 */
bool de_queue(QUEUE pQueue , NODE val) / 出队 /
void tranverce_queue(QUEUE pQueue) / 遍历队列 */
14.1.5. 字符串
//字符数组 //字符指针
typedef struct string{
char str[MAX];
int len;
} String;
typedef struct node
{
char str;
struct node *next;
} Node;
typedef struct string{
Node *head;
int len;
} String; typedef struct node
{
char str[4];
struct node *next;
} Node;
typedef struct string{
Node *head;
int len;
} String;
字符串操作 Strcreate (S) 创建
Strassign(S, T) 赋值
Strdestroy(S) 释放
Strempty(S) 判断是否为空
Strclear(S) 清空
元素操作 Strlength(S) 求长度
Strcompare(S1,S2) 比较
Strconcat(S1,S2) 追加
Substring(S, i, len) 子串
StrIndex(P,T) 查找
Strinsert(S, i, T) 插入
Strdelete(S,i,len) 删除
Replace(S, T1, T2) 替换
14.1.6. 数组
// 定义个数组
typedef struct Array {
int length; // 数组长度
int count; // 数组当前元素数 count
int pBase; // 数组的首字节地址
} PMyArray,MyArray; //两个别名,PMyArray 类似java中类名,定义的对象不带 * , MyArray类似于OC中的类型,定义的对象带 * 。
void init_Arr(MyArray pArr, int len); /* 初始化数组*/
bool append_Arr(MyArray pArr, int value); /* 追加数组*/
bool insert_Arr(MyArray pArr, int index , int value); /* 插入数组*/
bool delete_Arr(MyArray pArr, int index , int * pVal); /* 删除数组*/
bool is_full(MyArray pArr); /* 是否满载*/
bool is_empty(MyArray pArr); /* 是否为空*/
void sort_Arr(MyArray pArr); /* 排序数组*/
void show_Arr(MyArray pArr); /* 展示数组*/
void inversion_Arr(MyArray pArr); /* 倒序数组*/
MyArray * get_Arr(void); /** 获取一个默认初始化的数组*/
14.1.7. 二叉树
typedef int ElementType;
struct TreeNode;
typedef struct TreeNode *BinTree;
typedef struct TreeNode *SearchTree;
struct TreeNode {
ElementType element;
SearchTree left;
SearchTree right;
};
树操作 Boolean isEmpty(BinTree BT);//判别BT是否为空
voidTraversal(BinTree BT);//遍历,按某个顺序访问每一个结点
BinTree CreatBinTree(); //创建一个二叉树
二叉树遍历方法 voidPreOrderTraversal(BinTree BT);//先序 ----- 根 、左子树、右子树
voidInOrderTraversal(BinTree BT);//中序 ----- 左子树、根、右子树
voidLevelOrderTraversal(BinTree BT);//后序遍历-----左子树、右子树、根
14.1.8. 图
ADT Graph
{
数据对象V:V是具有相同特性的数据元素的集合,成为顶点集。
数据关系R:
R={VR}
VR={〈v,w〉| v,w ∈且P(v,w),
〈v,w〉表示v到w的弧,
P(v,w)定义了弧〈v,w〉的意义或信息
}
}
图操作 CreatGraph(G)输入图G的顶点和边,建立图G的存储。
DestroyGraph(G)释放图G占用的存储空间。
顶点操作 GetVex(G,v)在图G中找到顶点v,并返回顶点v的相关信息。
PutVex(G,v,value)在图G中找到顶点v,并将value值赋给顶点v。
InsertVex(G,v)在图G中增添新顶点v。
DeleteVex(G,v)在图G中,删除顶点v以及所有和顶点v相关联的边或弧。
LocateVex(G,u)在图G中找到顶点u,返回该顶点在图中位置。
FirstAdjVex(G,v)在图G中,返回v的第一个邻接点。若顶点在G中没有邻接顶点,则返回“空”。
NextAdjVex(G,v,w)在图G中,返回v的(相对于w的)下一个邻接顶点。若w是v的最后一个邻接点,则返回“空”。
边操作 InsertArc(G,v,w)在图G中增添一条从顶点v到顶点w的边或弧。
DeleteArc(G,v,w)在图G中删除一条从顶点v到顶点w的边或弧。
遍历 DFSTraverse(G,v)在图G中,从顶点v出发深度优先遍历图G。
BFSTtaverse(G,v)在图G中,从顶点v出发广度优先遍历图G。
14.2. 树表示法
14.2.1. 父结点(双亲)表示法
这种结构的思想比较简单:除了根结点没有父结点外,其余每个结点都有一个唯一的父结点。将所有结点存到一个数组中。每个结点都有一个数据域data和一个数值parent指示其双亲在数组中存放的位置。根结点由于没有父结点,parent用-1表示。
14.2.2. 孩子表示法
换种思路,既然双亲表示法获取某结点的所有孩子有点麻烦,我们索性让每个结点记住他所有的孩子。但是由于一个结点拥有的孩子个数是一个不确定的值,虽然最多只有树的度那么多,但是大多数结点的孩子个数并没有那么多,如果用数组来存放所有孩子,对于大多数结点来说太浪费空间了,因此可以采用孩子指针来存放所有孩子。如果有几个孩子,就申请几个孩子空间存放,其中children_num存放当前结点的孩子数量。
struct node {
ElementType data;
struct node *children;
int children_num;
};
14.2.3. 双亲孩子表示法
再说获取某结点父结点的方法,从代码看出它遍历了所有结点。如果要改进,可以将双亲表示法融合进去,增加一个parent域就行。也就是说,Node类改成如下就行,这种实现可以称为双亲孩子表示法。
struct node {
ElementType data;
int parent;
struct node *children;
};
14.2.4. 孩子兄弟表示法
还有一种表示法,关注某结点的孩子结点之间的关系,他们互为兄弟。一个结点可能有孩子,也有可能有兄弟,也可能两者都有,或者两者都没。基于这种思想,可以用具有两个指针域(一个指向当前结点的孩子,一个指向其兄弟)的链表实现,这种链表又称为二叉链表。特别注意的是,双亲表示法和孩子结点表示法,都使用了数组存放每一个结点的信息,若稍加分析,使用数组是有必要的。但在这种结构中,我们摒弃了数组,根结点可以作为头指针,以此开始可以遍历到树的全部结点——根结点肯定是没有兄弟的(根结点如果有兄弟这棵树就有两个根结点了),如果它没有孩子,则这棵树只有根结点;若有孩子,就如下图,它的nextChild的指针域就不为空,现在看这个左孩子,有兄弟(实际就是根结点的第二个孩子)还有孩子,则左孩子的两个指针域都不为空,再看这个左孩子的nextSib,他有个孩子…一直这样下去,对吧,能够访问到树的全部结点的。
14.3. 二叉树的存储结构
14.3.1. 顺序存储(只适用于完全二叉树)
14.3.2. 链式存储(最普遍的存储方式)——由于结点可能为空,所以会比较浪费空间
如果有n个节点,则有2n个left、right指针,但是用到的只有n-1个指针
14.3.3. 线索存储(改进的方法)
14.4. 图
14.4.1. 邻接矩阵
所谓邻接矩阵(Adjacency Matrix)的存储结构,就是用一维数组存储图中顶点的信息,用矩阵表示图中各顶点之间的邻接关系。假设图G=(V,E)有n个确定的顶点,即V={v0,v1,…,vn-1},则表示G中各顶点相邻关系为一个n×n的矩阵,矩阵的元素为:
若G是网图,则邻接矩阵可定义为:
其中,wij表示边(vi,vj)或<vi,vj>上的权值;∞表示一个计算机允许的、大于所有边上权值的数。
14.4.2. 图的邻接表
邻接表(Adjacency List)是图的一种顺序存储与链式存储结合的存储方法。
邻接表表示法类似于树的孩子链表表示法。就是对于图G中的每个顶点vi,将所有邻接于vi的顶点vj链成一个单链表,这个单链表就称为顶点vi的邻接表,再将所有点的邻接表表头放到数组中,就构成了图的邻接表。
在邻接表表示中有两种结点结构,如图所示。
一种是顶点表的结点结构,它由顶点域(vertex)和指向第一条邻接边的指针域(firstedge)构成,另一种是边表(即邻接表)结点,它由邻接点域(adjvex)和指向下一条邻接边的指针域(next)构成。
对于网图的边表需再增设一个存储边上信息(如权值等)的域(info),网图的边表结构如图7.10所示。
14.4.3. 有向图的邻接表与逆邻接表
14.4.4. 图的十字链表
十字链表(Orthogonal List)是有向图的一种存储方法,它实际上是邻接表与逆邻接表的结合,即把每一条边的边结点分别组织到以弧尾顶点为头结点的链表和以弧头顶点为头顶点的链表中。
在弧结点中有五个域:其中尾域(tailvex)和头(headvex)分别指示弧尾和弧头这两个顶点在图中的位置,链域hlink指向弧头相同的下一条弧,链域tlink指向弧尾相同的下一条弧,info域指向该弧的相关信息。弧头相同的弧在同一链表上,弧尾相同的弧也在同一链表上。它们的头结点即为顶点结点,它由三个域组成:其中vertex域存储和顶点相关的信息,如顶点的名称等;firstin和firstout为两个链域,分别指向以该顶点为弧头或弧尾的第一个弧结点。
14.4.5. 邻接矩阵与邻接表
邻接矩阵表示法:在一个一维数组中存储所有的点,在一个二维数组中存储顶点之间的边的权值
邻接表表示法:图中顶点用一个一维数组存储,图中每个顶点vi的所有邻接点构成单链表
对比
1)在邻接矩阵表示中,无向图的邻接矩阵是对称的。矩阵中第 i 行或 第 i 列有效元素个数之和就是顶点的度。
在有向图中 第 i 行有效元素个数之和是顶点的出度,第 i 列有效元素个数之和是顶点的入度。
2)在邻接表的表示中,无向图的同一条边在邻接表中存储的两次。如果想要知道顶点的度,只需要求出所对应链表的结点个数即可。
有向图中每条边在邻接表中只出现一次,求顶点的出度只需要遍历所对应链表即可。求入度则需要遍历其他顶点的链表。
3)邻接矩阵与邻接表优缺点:
邻接矩阵的优点是可以快速判断两个顶点之间是否存在边,可以快速添加边或者删除边。而其缺点是如果顶点之间的边比较少,会比较浪费空间。因为是一个 n∗n 的矩阵。
而邻接表的优点是节省空间,只存储实际存在的边。其缺点是关注顶点的度时,就可能需要遍历一个链表。
- 编程题
15.1. 线性表
15.1.1. Union
例2-1 利用两个线性表LA和LB分别表示两个集合A和B,现要求一个新的集合A=A∪B。
void Union(list &list1, list list2) //第一种合并 将 list2元素逐个插入到kist1中 且 不重复(默认递增输入)
{
int temp;
int l1_len = ListLength(list1);
int l2_len = ListLength(list2);
for(int i =1;i<=l2_len;i++)
{
temp = GetElem(list2,i); //拿到list2第i个元素的值给temp
if(!LocateElem(list1,temp)) //再到list1中去找有没有相同的没有的话就把元素插到list1里面去
{
Insert(list1,temp);
}
}
}
15.1.2. Merge
例2-2 巳知线性表LA和线性表LB中的数据元素按值非递减有序排列,现要求将LA和LB归并为一个新的线性表LC,且LC中的元素仍按值非递减有序排列。
void mergelist(list la,list lb,list &lc)
{
initlist(lc);
i=j=1;k=0;
la_len=listlength(la);
lb_len=listlength(lb);
while((i<=la_len)&&(j<=lb_len))
{
getelem(la,i,ai);
getelem(lb,j,bj);
if(ai<=bj)
{
listinsert(lc,++k,ai);
++i ;
}
else
{
listinsert(lc,++k,bj) ;
++j;
}
}
while(i<=la_len)
{
getelem((la,i++,ai);
listinsert(lc,++k,ai);
}
while(j<=lb_len)
{
getelem((lb,j++,bj);
listinsert(lc,++k,bi);
}
}
15.2. 字符串查找
15.2.1. 朴素字符串匹配算法
初遇串的模式匹配问题,我们脑海中的第一反应,就是朴素字符串匹配(即所谓的暴力匹配),代码如下:
/* 字符串下标始于 0 */
int NaiveStringSearch(string S, string P)
{
int i = 0; // S 的下标
int j = 0; // P 的下标
int s_len = S.size();
int p_len = P.size();
while (i < s_len && j < p_len)
{
if (S[i] == P[j]) // 若相等,都前进一步
{
i++;
j++;
}
else // 不相等
{
i = i - j + 1;
j = 0;
}
}
if (j == p_len) // 匹配成功
return i - j;
return -1;
}
暴力匹配的时间复杂度为 O(nm),其中 n 为 S 的长度,m 为 P 的长度。很明显,这样的时间复杂度很难满足我们的需求。
15.2.2. KMP算法
在一个字符串中查找是否包含目标的匹配字符串。其主要思想是每趟比较过程让子串先后滑动一个合适的位置。当发生不匹配的情况时,不是右移一位,而是移动(当前匹配的长度– 当前匹配子串的部分匹配值)位。
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。时间复杂度O(m+n)。
15.3. 链表
15.3.1. 求单链表中结点的个数
// 求单链表中结点的个数
unsigned int GetListLength(ListNode * pHead)
{
if(pHead == NULL)
return 0;
unsigned int nLength = 0;
ListNode * pCurrent = pHead;
while(pCurrent != NULL)
{
nLength++;
pCurrent = pCurrent->m_pNext;
}
return nLength;
}
15.3.2. 从尾到头输出链表。题目:输入一个链表的头结点,从尾到头反过来输出每个结点的值。链表结点定义如下:
struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};
15.4. 栈
15.4.1. 栈初始化
15.4.2. 栈的遍历
思路:栈遍历,实际就是栈元素从栈顶一个个遍历到栈底,可以打印栈中元素的值
/*遍历打印栈/
void traverse(PSTACK pS){
// 只要不是空栈,就一直输出
PNODE p = pS->pTop;
while (p != pS->pBottom) {
printf("%d ",p->data);
p = p->pNext; // 把top的下一个节点付给top,继续遍历
}
printf("\n");
}
15.4.3. 栈的清空
15.4.4. 压栈push
15.4.5. 出栈pop
15.4.6. 对于一个栈,给出输入项A,B,C。如果输入项序列由A,B,C组成,试给出全部可能的输出序列。
15.4.7. 设有4个元素1、2、3、4依次进栈,而栈的操作可随时进行(进出栈可任意交错进行,但要保证进栈次序不破坏1、2、3、4的相对次序),请写出所有不可能的出栈次序和所有可能的出栈次序。
15.5. 队列
15.5.1. 初始化
15.5.2. 入队
15.5.3. 出队
15.6. 字符串
15.6.1. 翻转句子中单词的顺序。
题目:输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。
句子中单词以空格符隔开。为简单起见,标点符号和普通字母一样处理。
例如输入“I am a student.”,则输出“student. a am I”。
Answer:
Already done this. Skipped.
采用栈处理,方法2:
1.从开始逐一检索单词,将单词压栈,检索完毕,单词出栈
2. 句子翻转,检索单词,单词翻转。
15.6.2. 颠倒一个字符串。优化速度。优化空间。
void reverse(char *str) {
reverseFixlen(str, strlen(str));
}
void reverseFixlen(char str, int n) {
char p = str+n-1;
while (str < p) {
char c = *str;
*str = *p; *p=c;
}
}
-
应用题
16.1. 栈
栈结构固有的先进后出的特性,使它成为在程序设计中非常有用的工具,这里列举几个典型的例子。
16.1.1. 递归
递归定义:把一个直接调用自己或者通过一系列的调用语句间接地调用自己的函数,称作递归函数。
每个递归定义必须至少有一个条件,满足时递归不再进行,即不再引用自身二十返回值退出。
*迭代和递归的区别是:迭代使用的是循环结构,递归使用的是选择结构。递归能使程序的结构更清晰,更简洁,更容易让人理解,从而减少读懂代码的时间。但是大量的递归调用会建立函数的副本,会耗费大量的时间和内存。迭代则不需要反复调用函数和占用额外的内存。
递归过程退回的顺序是它前行顺序的逆袭。
16.1.2. 数制转换
十进制数 N 和其他 d 进制数的转换是计算机实现计算的基本问题,
需求:输入一个任意非负十进制整数,打印输出其对应的八进制整数
思路:由于上述计算过程是从低到高位顺序产生八进制数的各个数位,而打印输出,一般来说应从高位到低位进行,恰好和计算过程相反。因此可利用栈先进后出特性,将计算过程中得到的八进制数各位顺序进栈,再按出栈序列打印输出既为与输入对应的八进制数。
void conversion(void){
// 创建栈
STACK S;
init(&S);// 用户输入十进制数
scanf(“%d”,&N);// 放入栈中
while (N) {
push(&S, N % 8);
N = N / 8;
}// 打印出来
printf(“对应八进制数字为:”);
int a;
while (!isEmpty(&S)) {
pop(&S, &a);
printf(“%d”,a);
}
printf(“\n”);
}
思考 :用数组实现貌似更简单,为什么不用数组?
从算法上分析不难看出,栈的引入简化了程序设计的问题,划分了不同的关注层次,使思考范围缩小了。而使用数组不仅掩盖了问题的本质,还要分散精力去思路数组下标增减等细节问题。
这也是早期面向对象编程的一种思想,要把对应的功能划分关注层次,在逻辑的实现上面更加专注问题的本质。
16.1.3. 括号匹配的检验
编程语言中基本都允许使用 (),[],{}这几种括号,假设现在让使用两种,一段完整代码中其须成对匹配,检验括号是否匹配的方法可用"期待的紧迫程度"这个概念来描述。
当计算机接受了第一个括号后,它期待着与其匹配的第八个括号出现,然而等来的确实第二个括号,此时第一个括号[只能暂时靠边,而迫切等待与第二个括号匹配的第七个括号)出现,类似地,等来的是第三个括号[,其期待的匹配程度比第二个更加急迫,则第二个括号也只能靠边,让位于第三个括号,显然第二个括号的期待急迫性高于第一个括号,在接受了第四个括号之后,第三个括号的期待得到满足,消解之后,第二个括号的期待匹配变成最紧迫的任务了·····,以此类推。
可见此处理过程与栈的特点相吻合,由此,在算法中设置一个栈,每读入一个括号,若是右括号则使至于栈顶的最紧迫的期待得以消解,若是不合法的情况(左括号),则作为一个新的更紧迫的期待压入栈中,自然使原来所有未消解的期待的紧迫性都降了一级。另外在算法开始和结束的时候,栈都应该是空的。
算法实现:
/**
检测括号(本实例用数字代替括号)
[ ] --> 1 , 2
( ) --> 3 , 4
*/
void checkBracelet(void)
{
// 创建栈
STACK S;
init(&S);
// 用户输入括号
int N;
printf("请输入对应的括号(end结束):\n");
scanf("%d",&N);
if (isEmpty(&S)) {
push(&S, N);
printf("第一个括号输入\n");
traverse(&S); // 打印此时栈内容
}
while (!isEmpty(&S)) {
// 用户输入括号
int N;
printf("请输入对应的括号(0结束):\n");
scanf("%d",&N);
if (N == 0) {
break; // 用户输入0直接退出
}
// 判断当前栈顶是否符合标准,
if (S.pTop->data == N) {
printf("消除一对\n");
pop(&S, NULL);
traverse(&S); // 打印此时栈内容
}else
{
printf("未消除\n");
push(&S, N);
traverse(&S); // 打印此时栈内容
}
}
}
16.1.4. 表达式求值
表达式求值是数学中的一个基本问题,也是程序设计中的一个简单问题。我们所涉及的表达式中包含数字和符号,本实验中处理的符号包括‘+’、‘-’、‘*’、‘/’、‘(’和‘)’,要求按照我们所习惯的计算顺序,正确计算出表达式的值,并输出至屏幕上。
表达式求值的问题用栈来实现是十分合适的,分别用两个栈分别保存表达式中的数字和符号,以确定每对符号相遇时的优先级来决定当前应该进行什么操作。
符号栈的操作分为三种:一是直接入栈;一是直接出栈;一是将当前栈顶符号出栈并计算,然后根据新的栈顶符号与当前符号的优先级关系重复操作类型的判断。
算法的设计和实现
1、建立两个栈:一个是数字栈,一个是算符栈,首先向算符栈内添加一个符号‘#’。对于具体的符号栈的实现,可以通过编号的形式使用数字编号,也可以直接保存char类型的字符。
2、读取一个字符,自动滤掉空格、换行符等无关字符,当出现非法字符时结束程序并输出“表达式包含未被识别的符号。”
3、如果当前字符c是数字字符,即c满足条件“c>=’0’&&c<=’9’”,则继续读取,并由一个新的变量Num保存这个数字的真实值,具体实现是Num的初值为0,然后每次执行语句Num=Num*10+c-’0’,直到读取到非数字字符为止;如果当前字符c不是数字字符,调用函数OptrType(char c)得到该符号的编号。
4、当读到一个算符,考虑其入栈时,有三种情况:
(1)算符栈的栈顶元素优先级低于当前算符时,当前算符入栈;
(2)当两者优先级相同时,当前算符不入栈,算符栈的栈顶元素弹出;
(3)算符栈的栈顶元素优先级高于当前算符时,算符栈的栈顶元素弹出,数字栈弹出两个元素,按照顺序进行弹出的符号所对应的运算执行运算操作,将得到的结果压入数字栈。再将待入栈元素继续进行步骤4的判断。
5、当‘#’都弹出之后,计算结束。如果算式没有错误,则数字栈只剩一个元素,该元素就是算式的计算结果,输出即可。
6、清栈。
注:步骤4中提到的优先级是相对的,本实验中的实现方式是编号之后使用一个二维数组保存一个序对的关系的。比如Cmp[‘+’][‘-’]=1,而同样的有Cmp[‘-’][‘+’]=1。这样的规定方式是为了让四则运算中同级的运算能够先出现的先计算,从而避免了错误。具体关系见下表,其中值为-2的位置表示表达式有错误。
16.1.5. 迷宫求解
迷宫问题,是一个对栈(Stack)典型应用的例子之一。
假如,有如下10X10的迷宫(0代表通路,1代表障碍),我们需要用写程序来找出迷宫的出口.
1 1 1 1 1 1 1 1 1 1
1 1 1 0 1 1 1 0 1 1
0 0 0 0 1 0 0 0 1 1
1 1 0 1 1 0 1 0 0 1
1 1 0 1 0 0 1 0 1 1
1 1 0 1 1 1 1 0 0 1
1 1 0 0 0 0 0 0 1 1
1 1 0 1 0 1 1 0 1 1
1 1 0 1 0 1 1 0 1 1
1 1 1 1 1 1 1 0 1 1
16.1.6. 行编辑程序
一个简单的行编辑程序的功能是:接受用户从终端输入的程序或数据,并存入用户的数据区。由于用户在终端上进行输入时,不能保证不出差错,因此,若在行编辑程序中“每接受一个字符即存入用户区”的做法显然是不恰当的。
较好的做法是,设立一个输入缓冲区,用以接收用户输入的一行字符,然后逐行存入用户数据区。允许用户输入出差错,并在发现有误时及时改正。
例如:当用户发现刚刚建入的一个字符是错的时,可补进一个退格符“#”,以表示前一个字符无效;如果发现当前键入的行内差错较多或难以补救,则可以输入一个退格符“@”,以表示当前行中的字符均无效。
例如,假设从终端接受了这两行字符:
whil##ilr#e(s#*s)
outcha@putchar(*s=#++)
则实际有效的是下列两行:
while(*s)
putchar(*s++)
void LineEdit()
{
stack s;
stack s1;
char ch = getchar();
while (EOF != ch)
{
while (EOF != ch && '\n' != ch)
{
switch (ch)
{
case '#':
s.pop();
break;
case '@':
while (!s.empty())
{
s.pop();
}
break;
default:
s.push(ch);
break;
}
ch = getchar();
}
while (!s.empty())
{
s1.push(s.top());
s.pop();
}
while (!s1.empty())
{
cout << s1.top();
s1.pop();
}
cout << endl;
if (ch != EOF)
ch = getchar();
}
}
16.2. 霍夫曼树
16.2.1. 关于霍夫曼编码(Huffman编码)
Huffman是一种前缀编码;Huffman编码是建立在Huffman树的基础上进行的,因此为了进行Huffman编码,必须先构建Huffman树;树的路径长度是每个叶节点到根节点的路径之和;带权路径长度是(每个叶节点的路径长度*wi)之和;Huffman树是最小带权路径长度的二叉树;
构造Huffman树的过程:
(1)将各个节点按照权重从小到大排序;
(2)取最小权重的两个节点,并新建一个父节点作为这两个节点的双亲,双亲节点的权重为子节点权重之和,再将此父节点放入原来的队列;
(3)重复(2)的步骤,直到队列中只有一个节点,此节点为根节点;
构造完Huffman树之后,就可以进行Huffman编码了,编码规则:左分支填0,右分支填1;
16.2.2. Huffman解码过程
给定一个01串,将01串进行Huffman树,到叶子节点了就表明已经解码一个节点,然后再次遍历Huffman树;
16.3. 图
16.3.1. 生成树
生成树其实就是:对于一棵树G,若顶点数为n,则在原来图的基础上把边删除到n-1条边且能连通各点就是生成树。
注意生成树不唯一 。
利用遍历方法可以求得生成树,以邻接矩阵为存储方式代码如下:
int visit[MAXSIZE];//标记顶点是否访问
void DFSGraph(Graph* g, int n)
{
visit[n] = 1;//若访问过就标记为1
for (size_t i = 0; i < g->n; i++)
{
if ( !visit[i]&& g->edge[n][i] == 1 )
{
cout << g->vertex[n] << " "<<i<<endl;//输出生成树的边,其实就是对遍历算法改了输出的位置
DFS(g, i);
}
}
}
void DFSTraveseGraph(Graph *g)
{
for (size_t i = 0; i < g->n; i++)
{
visit[i] = 0;
}
for (size_t i = 0; i < g->n; i++)
{
if (!visit[i])
{
DFS(g, i);//防止连通图
}
}
}
16.3.2. 最小生成树(PRIM算法)
定义:我还是按照我的理解叙述吧,书上真的讲的太拖拉,对于一棵树G,若顶点数为n,则在原来图的基础上把边删除到n-1条边且能连通各点但权值和最小就是最小生成树。(如果错了望指教).
为了求一棵树的最小生成树,有两种不同的算法—–PRIM算法与KRUSKAL算法。
由于图示有顶点、边组成的,生成树的生成过程实际上就是:G(V,E)—》T(V,E)
开始T(V,E)是空的,由G(V,E)中根据最小规则选择合适的顶点或边加入T(V,E)中,其选择方法有两种,先从V中选,还是先中E中选,进而构成两种算法。
PRIM算法:从顶点开始
16.3.3. 最小生成树(KRUSKAL算法)
KRUSKAL算法:从最小边开始
16.3.4. AOE网络及拓扑排序
AOE网络是一个加权有向图,即每一条边都是带方向且带有权值的。对于一个有向边,箭头指向的点为终点,另一个点则是起点。
我们把入度为0的顶点叫做源点,出度为0的顶点叫做汇点。上图中v1是源点,v9是汇点。
可以理解为从源点出发,虽然中间会有很多不同的分岔口,但最终都会到达汇点。一个AOE网络至少包含一个源点和一个汇点。
实现拓扑排序的步骤如下:
步骤1:找到AOE网络中入度为0的顶点,输出它。如果找不到,停止排序。
步骤2:删除所有与该点关联的边,重复步骤1。
步骤3:如果输出顶点个数小于总顶点个数,则证明图中存在环,没有关键路径
16.3.5. AOV网拓扑排序及关键路径
在一个表示工程的有向图中,有顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称为AOV网。
AOV网中的弧表示活动之间存在的某种制约关系。
所谓拓扑排序,其实就是对一个有向图构造拓扑序列的过程。
关键路径:在AOE网中,从始点到终点具有最大路径长度(该路径上的各个活动所持续的时间之和)的路径称为关键路径。
关键活动:关键路径上的活动称为关键活动。关键活动:e[i]=l[i]的活动
由于AOE网中的某些活动能够同时进行,故完成整个工程所必须花费的时间应该为始点到终点的最大路径长度。
关键路径长度是整个工程所需的最短工期。
与关键活动有关的量:
⑴ 事件的最早发生时间ve[k]
ve[k]是指从始点开始到顶点vk的最大路径长度。这个长度决定了所有从顶点vk发出的活动能够开工的最早时间。
⑵ 事件的最迟发生时间vl[k]
vl[k]是指在不推迟整个工期的前提下,事件vk允许的最晚发生时间。
⑶ 活动的最早开始时间e[i]
若活动ai是由弧<vk , vj>表示,则活动ai的最早开始时间应等于事件vk的最早发生时间。因此,有:e[i]=ve[k]
⑷ 活动的最晚开始时间l[i]
活动ai的最晚开始时间是指,在不推迟整个工期的前提下, ai必须开始的最晚时间。若ai由弧<vk,vj>表示,则ai的最晚开始时间要保证事件vj的最迟发生时间不拖后。因此,有:l[i]=vl[j]-len<vk, vj>
- 代码
17.1. 学生信息管理
/* 基本线性表的操作 /
/ 学生信息管理 /
/ 集合–班级 ,元素–学生 */
/* 程序结构定义
具体步骤:
1、先定义元素
2、再定义集合,注意在集合中一定要定义目前该集合中包含元素数量的变量(属性),如果需要其他变量,也可以定义多个
如:钱包与钱币 ,钱包是集合,钱币是元素,通常我们除了关注钱包里共有多少张钱币外,还关注有多少钱(钱币中的数量之和),还可能关注百元的数量,50元的数量,每一个关注点就需要定义一个变量进行保存
*/
/* 头文件 */
#include <stdio.h>
#include <string.h>
#define MAX 100
/* 元素–学生结构定义*/
struct student
{
char stuid[12];
char stuname[10];
int age;
};
/* 类型定义,后期代码书写方便,阅读代码简洁 */
typedef struct student Student;
/* 集合–班级结构定义*/
struct class
{
Student stu[MAX];
int num;
};
typedef struct class Class;
/* 初始化班级信息 将班级学生成员数量置0即可*/
void initCls(Class *cls)
{
(*cls).num=0;
}
/* 清空班级学生信息 同样将班级学生成员数量置0即可*/
void clearCls(Class *cls)
{
(*cls).num=0;
}
/* 输入班级学生数量信息*/
void showStudentNum(Class cls)
{
printf(“当前班级学生成员数量为%d\n”,cls.num)
}
/* 输入学生信息*/
void inputStudent(Class *cls)
{
int idx;
idx=(*cls).num;
printf(“Please Input new Student Data:”);
printf(“StudentID:”);
scanf(“%s”,(*cls).stu[i].stuid);
printf(“StudentName:”);
scanf(“%s”,(*cls).stu[i].stuname);
printf(“StudentAge:”);
scanf(“%s”,(*cls).stu[i].age);
(*cls).num++;
}
/* 输出全部学生信息*/
void showAllStudent(Class cls)
{
int i;
for (i=0;i<cls.num;i++)
{
printf(“%d”,i+1);
printf(“Stuid=%s”,cls.stu[i].stuid);
printf(“Stuname=%s”,cls.stu[i].stuname);
printf(“Stuage=%s\n”,cls.stu[i].age);
}
}
/* 输出某个学生信息*/
void showAStudent(int idx,Class cls)
{
int i;
if (idx>cls.num)
{
printf(“当前学生数量%d \n”,cls.num);
printf(“要求输出的学生序号%d \n”,idx);
printf(“超出学生总数,输入错误!\n”);
}
else
{
i=idx-1;
printf(“%d”,idx);
printf(“Stuid=%s”,cls.stu[i].stuid);
printf(“Stuname=%s”,cls.stu[i].stuname);
printf(“Stuage=%s\n”,cls.stu[i].age);
}
}
/* 通过序号删除一个学生 */
void deleteAStudent(int idx,Class *cls)
{
int i;
if (idx>(*cls).num)
{
printf(“当前学生数量%d \n”,(*cls).num);
printf(“要求删除的学生序号%d \n”,idx);
printf(“超出学生总数,输入错误!\n”);
}
else
{
for (i=idx-1;i<(*cls).num-1;i++)
{
strcpy((*cls).stu[i].stuid,(*cls).stu[i+1].stuid);
strcpy((*cls).stu[i].stuname,(*cls).stu[i+1].stuname);
(*cls).stu[i].age=(*cls).stu[i+1].age;
}
(*cls).num–;
}
}
/* 通过姓名查找一个学生 */
void searchAStudent(char name,Class cls)
{
int i;
for (i=0;i<cls.num-1;i++)
{
if( strcmp(cls.stu[i].stuname,name)==0)
showAStudent(idx,cls);
}
}
/* 输出某个学生信息*/
void showStudentByIdx(Class cls)
{
int idx;
printf(“输入要显示的学生序号:”);
scanf(“%d”,&idx);
showAStudent(idx,cls);
}
/* 通过序号删除某个学生信息*/
void deleteStudentByIdx(Class *cls)
{
int idx;
printf(“输入要删除的学生序号:”);
scanf(“%d”,&idx);
deleteAStudent(idx,cls);
}
/* 通过姓名查找某个学生信息*/
void searchStudentByName(Class cls)
{
char name[20];
printf(“输入要查找的学生姓名:”);
scanf(“%s”,name);
searchAStudent(name,cls);
}
/* ================================= */
main()
{
Class cls;
char cc;
int sign=1;
while(sign)
{
printf(“\n");
printf(“= 1-init Class 2- Clear Class =\n”);
printf(“= 3-input student 4- show class student num =\n”);
printf(“= 5-show student by index 6- show all student =\n”);
printf(“= 7-delete student by index 8- search student by name =\n”);
printf(“= 0-exit =\n”);
printf("\n”);
scanf(“%c”,&cc);
/* 以下两行是为了对当前要执行的模块信息显示进行前面区别标志 */
printf("\n\n");
printf("-------------------------------------------------------------------------\n");
switch(cc)
{
case '0':
printf("= 0-exit =\n");
sign=0;
break;
case '1':
printf("= 1-init Class =\n");
initCls(&cls);
break;
case '2':
printf("= 2- Clear Class =\n");
clearCls(&cls);
break;
case '3':
printf("= 3-input student =\n");
inputStudent(cls);
break;
case '4':
printf("= 4- show class student num =\n");
showStudentNum(cls);
break;
case '5':
printf("= 5-show student by index =\n");
showStudentByIdx(cls);
break;
case '6':
printf("= 6- show all student =\n");
showAllStudent(cls);
break;
case '7':
printf("= 7-delete student by index =\n");
deleteStudentByIdx(&cls);
break;
case '8':
printf("= 8- search student by name =\n");
searchStudentByName(cls);
break;
}
/* 以下两行是为了对当前要执行的模块信息显示进行后面区别标志 */
printf("-------------------------------------------------------------------------\n");
printf("\n\n");
}
}
17.2. Union代码
#define MAX 100
struct list
{
int eval[MAX];
int len;
};
typedef struct list List;
List la,lb;
void union(List *La,List Lb);
void initList( List *la)
{
la->eval[0]=12;
la->eval[1]=132;
la->eval[2]=152;
la->eval[3]=212;
la->eval[4]=412;
la->len=5;
}
void initList( List *lb)
{
lb->eval[0]=512;
lb->eval[1]=3132;
lb->eval[2]=4152;
lb->eval[3]=5212;
lb->eval[4]=6412;
lb->eval[5]=35212;
lb->eval[6]=56412;
lb->len=7;
}
int listlength1(List la)
{
return la.len;
}
int listlength2(List *la)
{
return la->len;
}
void getelem(List Lb, int i, int *e)
{
*e=Lb.eval[i-1];
}
int locateelem(List La ,int e)
{
int returnVal=0;
int i;
for (i=0;i<La.len;i++)
{
if(La.eval[i]==e)
{
returnVal=1;
break;
}
}
return returnVal;
}
void listinsert(List *La ,int e)
{
La->eval[La->len]=e;
La->len ++;
}
void showList(List La)
{
int i;
for (i=0;i<La.len;i++)
printf(“%d %d\n”,i+1,La.eval[i]);
}
void union(List *La,List Lb)
{
int la_len;
int lb_len;
int i;
int e;
la_len=listlength2(La);
lb_len=listlength1(Lb);
for(i=1;i<=lb_len;i++)
{
getelem(Lb ,i,&e);
if(!locateelem(La ,e))
listinsert(La ,++la_len,e);
}
}
/* swap();/
void main()
{
List la; /={{1,3,5,7,8},5};*/
List lb;
initList(&la);
initList(&lb);
union(&la,lb);
showList(la);
}
17.3. 大数据求和
对于大整数数值,无法直接用整数类型进行计算,由于C语言的基本数据类型为数值类型及字符类型,数值类型不能进行计算,可用数字字符串进行计算。
#include <string.h>
#define MAX 100
main()
{
int l1;
int l2;
int ll;
int mlen;
int i,j0,tmp;
char str1[MAX]="123124762375249646426243536477574";
char str2[MAX]="1234567457474745754765464564564124762375249646426243536477574";
char str0[MAX]="";
strrev(str1); //字符串翻转
strrev(str2); //字符串翻转
//字符串翻转将字符串表示的数值的低位到高位,转化为字符数组的由低位到高位,从而使得对字符数组的由低序到高序的数字计算符合数值计算的由低位到高位运算。
l1=strlen(str1);
l2=strlen(str2);
ll =min(l1,l2);
mlen=max(l1,l2);
j0=0;
for (i=0;i<ll;i++)
{
tmp=str1[i]-'0' +str2[i]-'0' + j0;
str0[i]=tmp % 10 + '0';
j0=tmp/10;
}
if (mlen==l1)
{
for (i=ll;i<mlen;i++)
{
tmp=str1[i]-‘0’ + j0;
str0[i]=tmp % 10 + ‘0’;
j0=tmp/10;
}
if(j0>0)
str0[i++]=‘1’;
}
if (mlen==l2)
{
for (i=ll;i<mlen;i++)
{
tmp=str2[i]-‘0’ + j0;
str0[i]=tmp % 10 + ‘0’;
j0=tmp/10;
}
if(j0>0)
str0[i++]=‘1’;
}
str0[i++]=‘\0’;
strrev(str0);
}
以下内容为平时整理内容,主要是有关数据结构的主要内容,大家有时间可以看看,用于提高自己的计算机语言水平。
18. 内容总结
18.1. 项目
图有几种存储方式?邻接矩阵与邻接表存储结构的优缺点?什么时候用什么结构?
循环与递归的区别
什么是递归,递归的几个条件?写递归要注意些什么?
二叉树给出前序,中序求后序
字符串匹配,O(n+m)
字符串匹配(可以用KMP),本人写的KMP
给一个单链表如何判断有环?
用什么数据结构保存cookie
如何判断一个图是否有环
如何判断一个单链表是否有环?
链表判断环的入口
怎么判断两个链表是否相交
什么情况会栈溢出
链表与数组。
队列和栈,出栈与入栈。
链表的删除、插入、反向。
字符串操作。
二叉树的前中后续遍历:递归与非递归写法,层序遍历算法。
图的BFS与DFS算法,最小生成树prim算法与最短路径Dijkstra算法。
KMP算法。
18.2. 代码部分
1、给你一万个数,如何找出里面所有重复的数?用所有你能想到的方法,时间复杂度和空间复杂度分别是多少?
2、给你一个数组,如何里面找到和为K的两个数?
3、100000个数找出最小或最大的10个?
4、一堆数字里面继续去重,要怎么处理?
5、利用数组,实现一个循环队列类
6、从N个无序数中寻找Top-k个最小数( 经典海量数据 )?
7、1G的内存可以装入2G的程序么?怎么装?
8、n级台阶问题
9、手写代码,有序数组查找某个元素出现的次数
10、手写螺旋矩阵打印
11、象棋中马走日从A点到B点的最短路径走法
12、长为N的数组,元素范围是0-N-1,其中只有一个数是重复的,找出这个重复元素
13、矩阵从左上角向右下角走,每次只能向右或者向下移动,求经过最大的路径
14、数n可以由完全平方数构成,求最小的完全平方数构成数。
15、(1)两个栈实现一个队列
(2)怎么找出数组中出现两次的数(有两个数出现两次,其他的都是一次)
(3)旋转数组的最小值
(4)O(1)时间复杂度删除单链表结点
(5)约瑟夫环问题推导
(6)O(1)实现取栈的最小
16、给定一个2叉树,打印每一层最右边的结点
17、给定一个数组,里面只有一个数出现了一次,其他都出现了两次。怎么得到这个出现了一次的数?
18、在6基础上,如果有两个不同数的出现了一次,其他出现了两次,怎么得到这两个数?
19、无重复数组找出第K大的数字 引出堆排序(是否稳定,时间/空间复杂度)
20、 给定n个数,寻找第k小的数,同时给出时间复杂度
21、比较常见的算法题,也要考虑到n的大小,说了排序,最大堆,以及partition算法,面试官还让说,我说就知道这几种
22、对10G个数进行排序,限制内存为1G, 大数问题,但是这10G个数可能是整数,字符串以及中文改如何排序,对中文排序没有回答出来。
23、链表删去指定值的节点
23、写一个类似解析字符串的小程序(感觉考点是正则表达式)
24、求两个int数组的并集、交集
25、汉诺塔问题,打印出转移路径,接着写一个二叉树前序遍历的代码,最后让写一个多叉树实现,并层次遍历的代码
18.3. 数组编码
数组是最基本的数据结构,它将元素存储在一个连续的内存位置。这也是面试官们热衷的话题之一,在任何一次编程面试中,你都会听到很多关于数组的问题,比如将数组中元素位置颠倒,对数组进行排序,或者搜索数组上的元素。
数组数据结构的主要优点是,如果知道索引,它可以提供快速的O(1)搜索,但是从数组中添加和删除元素是很慢的,因为一旦创建了数组,就无法更改数组的大小。
为了创建更小或更大的数组,需要创建一个新数组并将所有元素从旧数组拷贝到新数组。
解决基于数组的问题的关键是对数组数据结构以及基本的编程构造函数(如循环、递归和基本运算符)要有很好的了解。
以下是一些热门的基于数组的编程面试问题:
如何在一个1到100的整数数组中找到丢失的数字?(方法)
如何在给定的整数数组中找到重复的数字? (方法)
如何在未排序整数数组中找到最大值和最小值? (方法)
如何找到数组所有和等于一个给定数的数对? (方法)
如果一个数组包含多重复制,那么如何找到重复的数字? (方法)
在Java中如何从给定数组中删除多重复制? (方法)
如何使用快速排序算法对整数数组进行排序? (方法)
如何从数组中删除多重复制? (方法)
如何在Java中对数组进行反向操作? (方法)
如何在不使用任何库的情况下从数组中删除多重复制? (方法)
这些问题不仅可以帮助你提高解决问题的能力,还可以提高你对数组数据结构的认识。
如果你需要更高级的基于数组的问题,那么你还可以看到编程面试训练营:算法+数据结构,一个专门为面试准备的关于算法的Bootcamp风格课程,以获得一个技术巨头公司的工作,如谷歌,微软,苹果,Facebook等。
18.4. 链表编程
链表是补充数组数据结构的另一种常见的数据结构。与数组类似,它也是一个线性数据结构,以线性方式存储元素。
然而,与数组不同的是,它不会将它们存储在连续的位置;相反,它们分散在内存中各处,内存使用节点相互连接。
链表就是节点列表,每个节点包含存储的值和下一个节点的地址。
由于这种结构,在链表中添加和删除元素很容易,因为只需要更改链接而不是创建数组,但是查找是困难的,并且通常需要O(n)来查找单个链表中的元素。
本文提供了更多关于数组和链表数据结构之间区别的信息。
链表也有多种形式,比如单链表,它允许你沿着一个方向(向前或向后)移动遍历;双链表,允许在两个方向(向前和向后)遍历;最后,循环链表则形成一个环。
为了解决基于链表的问题,对递归知识进行了解是很重要的,因为链表是递归数据结构。
如果从链表中取出一个节点,剩下的数据结构仍然是链表,因此,许多链表问题的递归解决方案比迭代解决方案更简单。
以下是一些最常见和最流行的面试问题及解决方法:
如何在一次遍历中找到单个链表的中值?(方法)
如何证明给定的链表是否包含循环?如何找到循环的头节点? (方法)
如何使链表反向? (方法)
如何在不使用递归的情况下逆转单链表? (方法)
如何删除未排序链表中的重复节点? (方法)
如何得到单链表的长度?(方法)
如何在单链表中从尾部找到第三个节点? (方法)
如何使用堆栈得到两个链表的和? (方法)
这些问题将帮助你发展解决问题的技能,以及提高你对链表数据结构的知识。
如果你在解决这些链表编程问题时遇到困难,那么我建议你通过浏览数据结构和算法来刷新数据结构和算法技能:使用Java课程进行深入研究。
18.5. 字符串编程
除了数组和链表数据结构外,字符串也是编程面试中的另一个热门话题。我参加过的面试,都有以字符串为基础的问题。
字符串的一个好处是,如果你了解数组,你可以很容易地解决基于字符串的问题,因为字符串是一个字符数组。
因此,通过解决基于数组的编程问题所学习的所有技术也可以用于解决字符串编程问题。
下面是编程面试中经常被问到的字符串编码问题:
如何从字符串打印重复字符?(方法)
如何检查两个字符串是否互相颠倒? (方法)
如何从字符串中打印第一个非重复字符? (方法)
如何使用递归反转给定的字符串? (方法)
如何检查字符串是否只包含数字? (方法)
如何在字符串中找到重复的字符? (方法)
在给定的字符串中,如何计算元音和辅音的数量? (方法)
如何计算字符串中给定字符的出现次数? (方法)
如何找到一个字符串的所有排列? (方法)
如何在不使用任何库方法的情况下逆转一个句子中的单词?(方法)
如何检查两个字符串是否互相旋转?(方法)
如何检查给定的字符串是否回文?(方法)
18.6. 二叉树编码
到目前为止,我们只研究了线性数据结构,但是现实世界中的所有信息都不能以线性方式表示,这就是树数据结构的作用所在。
树数据结构是一种允许以分层方式存储数据的数据结构。根据存储数据的方式的不同,树的类型不同,例如二叉树,其中每个节点最多有两个子节点。
除了近亲二叉搜索树,它也是最流行的树数据结构之一。因此,会有许多基于它们的问题,例如如何遍历它们、计算节点、查找深度以及检查它们是否为平衡二叉树。
解决二叉树问题的一个关键是要有很强的理论知识,例如二叉树的大小或深度是什么,什么是叶子节点,什么是节点,以及理解流行的遍历算法,例如先序遍历、后序遍历和顺序遍历。
下面是一些软件工程师或开发人员面试中常见的基于二进制树的编码问题:
如何实现二叉搜索树?(方法)
如何在给定的二叉树中执行先序遍历? (方法)
如何在不使用递归的情况下按顺序遍历给定的二叉树? (方法)
如何在给定的二叉树中执行顺序遍历? (方法)
如何在不使用递归的情况下,使用顺序遍历打印给定二叉树的所有节点? (方法)
如何实现后序遍历算法? (方法)
如何在不使用递归的情况下遍历后序遍历中的二叉树? (方法)
如何打印二叉搜索树的所有叶子? (方法)
如何计算给定二叉树中的叶节点数? (方法)
如何在给定数组中执行二分法搜索?(方法)
如果你觉得对二叉树编码的理解不够充分,而且你不能自己解决这些问题,我建议你回去选择一个好的数据结构和算法课程,比如从0到1:Java中的数据结构和算法。