文章目录
- 数据结构
- 什么是数据结构?
- 逻辑结构和物理结构有什么区别?
- 为什么对单链表设置头结点?
- 算法的特点?
- 常见的数据结构有哪些?
- 栈在后缀表达式求值的算法思想:
- 队列溢出现象?解决方法?
- 判断一个链表是否有环,如何找到这个环?
- 头指针和头结点的区别?
- 栈和队列相同之处和不同之处?
- 什么是树?
- 满二叉树,完全二叉树,二叉排序树,平衡二叉树特性
- 什么是线索二叉树?有什么优点?
- 什么是哈夫曼树?简述如何构造哈夫曼树。
- 图的存储结构
- 深度优先遍历与广度优先遍历?
- 如何判断有向图是否有环?
- 最小生成树是什么?用途?
- 简述最小生成树两种生成算法:
- 什么时候最小生成树唯一?
- 求最短路径的算法
- Djikstra(迪杰斯特拉)
- Floyd(佛洛依德)
- 各种排序
- 各种查找方法?
- 快速排序如何优化?
- B树和B+树的区别?
- 哈希表(概念、构造方法、冲突解决)?
- 用循环比递归效率高么?
数据结构
数据结构是指相互之间存在一种或多种关系的数据元素的集合。
逻辑结构是指 数据元素之间的逻辑关系 ,而物理结构则是 数据的逻辑结构在计算机中的存储形式 。
- 逻辑结构分类:
- 集合:各个元素之间是 “平等” 的,类似于数学里面的集合。
- 线性结构:数据结构中的数据元素是一对一关系的。
- 树形结构:数据结构中的数据元素之间存在一对多的层次关系。
- 图形结构:数据结构中的数据元素之间存在多对多的关系。
- 物理结构分类:
- 顺序存储结构:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
- 优点:可以实现随机存取,每个元素占用最少的存储空间。
- 缺点:只能使用相邻的一整块存储单元,因此插入和删除需要移动大量元素,可能产生较多的外部碎片。
- 链式存储结构:不要求逻辑上相邻的元素在物理位置上也相邻,借助只是元素存储地址的指针来表示元素之间的逻辑关系。
- 优点:不会出现碎片的现象,能充分利用所有的存储单元,插入、删除可以在O(1)的时间完成。
- 缺点:查找某个特定的结点时,需要从表头开始遍历,依次查找,是非随机存取的存储结构。
- 索引存储结构:在存储元素信息的同时,还建立附加的索引表,索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)。
- 优点:检索速度快。
- 缺点:附加的索引表额外占用存储空间。另外,增加和删除数据也要修改索引表,因而会花费较多的时间。
- 散列存储结构:根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储。
- 优点:检索、增加、删除结点的操作都很快。
- 缺点:若散列函数不好,则可能出现元素存储单元的冲突,而解决冲突会增加时间和空间开销。
- 顺序存储结构:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
- 保证每一个结点都有前驱结点,使得插入和删除结点的处理统一
- 带头结点的链表,在表空时存在头结点,使得空表和非空表的处理统一
- 带头结点的链表可以在头结点数据域中存放一些信息,如结点的个数
-
算法的五大特征:
- 有穷性。一个算法必须总在执行有穷步之后结束,且每一步都可以在有穷时间内完成。
- 确定性。算法中每条指令必须有确切的含义,对于相同的输入只能得出相同的输出。
- 可行性。算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
- 输入。一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合。
- 输出。一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。
-
“好”的算法应考虑达到的目标:
- 正确性。算法应能够正确地解决求解问题。
- 可读性。算法应具有良好的可读性,以帮助人们理解。
- 健壮性。输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名其妙的输出结果。
- 效率与低存储量需求。效率是指算法执行的时间,存储量需求是指算法执行过程中所需要的最大存储空间,这两者都与问题的规模有关。
-
时间复杂度:一个语句的频度是指该语句在算法中被重复执行的次数。算法中所有语句的频度之和记为T(n),它是该算法问题规模n的函数,时间复杂度主要分析T(n)的数量级。算法最深层循环内的语句频度与T(n)同数量级。
-
空间复杂度:算法的空间复杂度S(n)定义为该算法所消耗的存储空间,它是问题规模n的函数。算法原地工作是指算法所需的辅助空间为常量,即O(1)。
- 数据结构:指相互之间存在一种或者多种特定关系的数据元素的集合。
- 常见的数据结构:
- 数组:一维数组、二维数组
- 链表:单链表、循环链表
- 栈:先进后出、递归、后缀表达式求值、函数调用、括号匹配
- 队列:先进先出、树的层次遍历、图的广度遍历、缓冲器的管理
- 树:二叉树、森林、平衡二叉树、线索二叉树、遍历
- 图:有向图、无向图、遍历、最短路径
- 数组和链表的区别。
- 数组:
- 事先定义长度,不能适应数据动态地递减
- 从栈中分配空间
- 快速访问数据元素,插入删除不方便
- 链表:
- 动态地进行存储分配,可以适应数据动态地递减
- 从堆中分配空间
- 查找访问数据不方便,插入删除数据方便
(1)是操作数则进栈
(2)是运算符则将两个元素出栈并将得到的结果进栈
(3)表达式扫描完成后,栈顶元素为所求结果
- 下溢现象:队列为空时出队产生的现象;是正常现象;
- 真上溢现象:队列满时继续往进队;建立一个足够大的存储空间来解决;
- 假上溢现象:队列未满而无法继续进队;采用循环队列来解决;
题问:给定一个单链表,只给出头指针h:
- 如果判断是否存在环?
- 对于判断一个单链表是否存在环,可以利用追赶的方式,设立两个指针slow、fast,从头指针开始,每次分别前进一步和两步,如果存在环,则两者相遇;如果没有环,fast遇到NULL退出。
- 如何知道环的长度?
- 在slow和fast相遇的地方标记,再次相遇所走过的操作数就是环的长度。
- 如何找出环的连接点在哪里?
- 分别从相遇点和头指针开始走,再次相遇的那个点就是连接点。
- 带环链表的长度是多少?
- 连接点距离头指针的长度,加上环的长度,即为链表长度。
- 头指针:
- 是链表指向第一个结点的指针,若链表有头节点,则是指向头节点的指针
- 是必需的
- 具有标识作用
- 头节点:
- 头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前
- 不是必需的,为了方便操作
- 对于插入和删除第一个结点,和其他结点操作统一
-
相同点:
- 栈和队列都是线性结构
- 栈和队列在插入时都是在表尾进行
- 栈和队列都可以用顺序存储结构和链式存储结构
- 栈和队列插入和删除操作的时间复杂度和空间复杂度是一样的
-
不同点:
- 删除元素位置不同,栈在表尾,队在表头
- 用链表存储时可以实现多栈空间共享,队列不行
-
树是一种非线性的数据结构,其元素之间有明显的层次关系,由结点和边组成且不存在环;
-
在树的结构中,每个结点都只有一个前件称为父结点,没有前件的结点为树的根结点,简称为树的根;
-
每个结点可以有多个后件成为结点的子结点,没有后件的结点称为叶子结点。
- 树的存储结构、二叉树的存储结构。
-
树的存储结构,常用的有三种:
- 双亲存储结构(顺序存储):用数组来存储,数组下标表示树的结点,数组元素的内容表示该结点的双亲结点;
- 孩子兄弟存储结构(链式存储):用二叉链表存储树,结点中一个指针指向自己的孩子,另一个指针指向自己的兄弟;
- 孩子表示法(链式存储):每个结点的孩子结点用单链表连接起来;
-
二叉树的存储结构,常用的有两种:
- 顺序存储结构:先将二叉树补充为完全二叉树,再将树的值依次存入一个一维数组中;适用于完全二叉树,存储一般的树会浪费大量存储空间
- 链式存储结构:采用二叉链表,左指针指向左孩子,有指针指向右孩子,数据域存储对应的数据元素
- 满二叉树:每一层的结点数达到最大值
- 完全二叉树:除最后一层外每一层的结点数达到最大值且最后一层若有缺失结点也是从右往左缺失
- 二叉排序树:左子树结点中的值都小于结点的值,结点的值都小于右子树的值
- 平衡二叉树:在二叉排序树的基础上,保证了左右子树的高度之差的绝对值不大于1
- 由二叉链表存储的二叉树,n个结点有n+1个空链域,将这些空链域利用起来正是线索二叉树;
- 优点是能够较快地找到当前结点的前驱和后继结点。
- 哈夫曼树带权路径最短(带权路径:树中结点的值乘于结点到根的距离)
- 从集合中选取根结点权值最小的两棵树(集合中的树一开始都是结点)组成一颗新树,新树的权值为左右子树权值之和,删除集合选取的两个结点,增加新树的结点,重复上述操作;
- 特点:
- 每个初始结点最终都成为叶节点,且权值越小的结点到根节点的路径长度越大。
- 构造过程中共新建了n-1个结点(双分支结点),因此哈夫曼树的结点总数为2n-1。
- 每次构造都选择2棵树作为新结点的孩子,因此哈夫曼树中不存在度为1的结点。
- 邻接矩阵(顺序存储结构):矩阵的行数或列数表示顶点数,矩阵元素来表示边的情况;适合用于稠密图;
- 邻接表(链式存储结构):对每个顶点建立一个单链表,每个单链表第一个结点存放有关顶点的信息,其余结点存放有关边的信息;适合用于稀疏图;
- 十字链表:有向图;
- 邻接多重表:无向图。
-
深度优先遍历类似于二叉树的先序遍历
步骤:
<1>访问起始点v
<2>若v的第一个邻接点没有被访问过,则深度遍历该邻接点
<3>若v的第一个邻接点已经被访问,则访问其第二个连接点,进行深度遍历;重复以上步骤,直到所有节点都被访问为止
-
广度优先遍历类似于层次遍历
步骤:
<1>访问起始点v
<2>依次遍历v 的所有未访问过得邻接点
<3>再次访问下一层中未被访问过的邻接点;重复以上步骤,直到所有节点都被访问过为止
- 两种方法:
- 深度搜索遍历:若图中有一个顶点被访问两次则证明有环
- 拓扑排序:查找图中入度为0的顶点,删除它,重复此操作;若图中最后还剩顶点则证明有环
-
连接图的各个顶点且边的权值之和最小的是最小生成树;
-
重要用途:如设计通信网。
- 设图的顶点表示城市,边表示两个城市之间的通信线路,边的权值表示建造通信线路的费用。n个城市之间最多可以建n(n-1)/2条线路,用最小生成树来选择其中的n-1条,使总的建造费用最低
-
Prim(普里姆):采用了贪心算法的思想
(1)将起始顶点并入生成树
(2)将各顶点到生成树距离最短的那个顶点并入生成树
(3)更新各顶点到生成树的距离(比较第二步并入的顶点到各顶点的距离是否会比原顶点距离短,会的话则更新顶点到生成树的距离)
(4)重复以上三步直到所有顶点并入,此时最小生成树完成
-
Kruskal(克鲁斯卡尔):
将连通网中所有的边按照权值大小做升序排序,从权值最小的边开始选择,只要此边不和已选择的边一起构成环路,就可以选择它组成最小生成树。对于 N 个顶点的连通网,挑选出 N-1 条符合条件的边,这些边组成的生成树就是最小生成树。
通常用并查集来判断已选边是否构成回路(若待加边的两个顶点同属一个集合则构成回路,不同属则将边的一个顶点加入另一个顶点的集合中,完成加边)
所有权值都不相同,或者有相同的边,但是在构造最小生成树的过程中权值相等的边都被并入到最小生成树中的图,其最小生成树是唯一的。
从源点出发,每次选择离源点最近的一个顶点前进,然后以该顶点为中心进行扩展,最终得到源点到其余所有点的最短路径。
a.从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。
b.对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短。如果是更新它。
注意:
(1)Dijkstra算法适用稠密图(邻接矩阵),因为稠密图问题与顶点关系密切;Floyd稠密图、稀疏图都适用;
(2)Dijkstra不能处理负权图,Flyod能处理负权图;
(3)Dijkstra处理单源最短路径,Flyod处理多源最短路径;
-
排序算法:
- 内部排序:
- 插入排序:直接插入排序、折半插入排序、希尔排序。
- 交换排序:冒泡排序、快速排序。
- 选择排序:简单选择排序、堆排序。
- 归并排序。
- 基数排序
- 外部排序:多路归并排序。
- 内部排序:
-
不稳定的排序:“心情不稳定,快些选堆朋友来聊天”
- 快----快速排序;
- 些----希尔排序(谐音);
- 选----选择排序;
- 堆----堆排序;
-
经过一趟排序,能够保证一个关键字到达最终位置:冒泡排序、快速排序、简单选择排序、堆排序
-
关键字比较次数和原始序列无关:简单选择排序、折半排序
-
排序最优和最差相同的排序算法:简单选择、归并排序、堆排序
-
时间复杂度为O(nlogn):快些以nlogn的速度归队;快(快速),些(希尔),归(归并),队(堆)。
- 查找方法分为静态查找表和动态查找表
- 静态查找表:顺序查找、折半查找、分块查找
- 顺序查找:结构简单,顺序结构和链式结构都可以,查找效率低
- 折半查找:要求查找表为顺序存储结构,并且有序
- 分块查找:先把查找表分为若干子表,要求每个子表的元素都要比他后面的子表的元素小,从而保存块间是有序的,把各子表中最大关键词构成一张索引表,表中还包含各子表的起始地址。
- 特点:块间有序,块内无序,查找时,块间索引查找,块内进行顺序查找。
- 动态查找表:二叉排序树、平衡二叉树
- 二叉排序树:是比根节点大的放在右子数,比根节点小的放在左子树,对二叉排序树进行中序遍历,可以得到一个递增的有序序列。
- 平衡二叉树:他的左右子树高度差不能大于1,且左右子树也都是平衡二叉树。
(1)当整个序列有序时退出算法
(2)当序列长度很小时(根据经验是大概小于8),应该使用常数更小的算法,比如插入排序等。
(3)随机选取分割位置
(4)当分割位置不理想时,考虑是否重新选取分割位置
(5)分割成两个序列时,只对其中一个递归进去,另一个序列仍可以在这一函数内继续划分,可以显著减小栈的大小(尾递归)
(6)将单向扫描改成双向扫描,可以减少划分过程中的交换次数
- 优化1:当待排序序列的长度分割到一定大小后,使用插入排序
- 原因:对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排。
- 优化2:在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与Key相等元素分割。
- 优化3:优化递归操作(快排函数再函数尾部有两次递归操作,我们可以对其使用递归优化)
- 优点:如果待排序的序列划分极端不平衡,递归的深度将趋近于n,而栈的大小是很有限的,每次递归调用都会耗费一定的栈空间,函数的参数越多,每次递归耗费的空间也越多。优化后,可以缩减堆栈深度,由原来的O(n)缩减为O(logn),将会提高性能。
- B树n个结点有n+1个分支,B+树n个结点有n个分支
- B树的每个结点包含信息,B+树非叶结点起索引作用,只有叶子结点包含信息(且包含了全部关键字)
- B+树有一个指针指向关键字最小的叶子结点,所有叶子结点链接成一个链表,B树没有
- B树和B+树每个结点的关键字个数取值范围不同;
- 概念:散列表根据关键字来计算出关键字在表中的地址,用来加快查找的速度;
- 构造方法:
- 直接定址法:
- H(Key)=a*Key+b
- 特点:计算简单,且不会产生冲突,若关键字发布不连续,空位较多,则会造成存储空间的浪费。
- 除留余数法:取关键字对p取余的值作为散列地址,其中p<m,即H(Key)=Key%p(p<m)。
- 直接定址法:
- 冲突解决方法:
- 开放定址法:
- 线性探查法:依次探查下一个地址,直到有空位置出现为止(任意产生堆积)
- 平方探查法:可以减少堆积问题,但是不能探查到hash上的所有单元,可探查到一半单元。
- 链地址法:把所有的同义词用单链表连接起来
- 开放定址法:
递归和循环两者完全可以互换。不能完全决定性地说循环的效率比递归的效率高
- 递归算法:
- 优点:代码简洁、清晰,并且容易验证正确性。
- 缺点:它的运行需要较多次数的函数调用,如果调用层数比较深,需要增加额外的堆栈处理(还有可能出现堆栈溢出的情况),比如参数传递需要压栈等操作,会对执行效率有一定影响。
但是,对于某些问题,如果不使用递归,将是极端难看的代码。再编译器优化后,对于多次调用的函数处理会有非常好的效率优化,效率未必低于循环。
- 循环算法:
- 优点:速度快,结构简单。
- 缺点:并不能解决所有的问题。有的问题适合使用递归而不是循环。如果使用循环并不困难的话,最好使用循环。
- 循环:例如,斐波那契额数列,采用循环来实现,不仅简洁,而且效率高,而采用递归,随着问题规模的增加,效率就越低。
- 递归:例如,汉诺塔问题,用递归来实现,不仅代码简洁,而且清晰,方便查找出问题。