数据结构面试题以及答案整理

参考网络整理的一些问题

一、什么是数据结构?

数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。结构包括逻辑结构和物理结构。

数据的逻辑结构包括4种

(1)集合:数据元素之间除了有相同的数据类型再没有其他的关系

(2)线性结构:数据元素之间是一对一的关系 ——线性表、栈、队列

(3)树形结构:数据元素之间是一对多的关系

(4)图状结构:数据元素之间是多对多的关系。

物理结构包括顺序存储结构和链式存储结构。

二、解释一下顺序存储与链式存储

顺序存储结构是用一段连续的存储空间来存储数据元素,可以进行随机访问,访问效率较高。链式存储结构是用任意的存储空间来存储数据元素,不可以进行随机访问,访问效率较低。

三、头指针和头结点的区别?

头指针:是指向第一个节点存储位置的指针,具有标识作用,头指针是链表的必要元素,无论链表是否为空,头指针都存在。

头结点:是放在第一个元素节点之前,便于在第一个元素节点之前进行插入和删除的操作,头结点不是链表的必须元素,可有可无,头结点的数据域也可以不存储任何信息。

四、线性结构的特点

(1)集合中必存在唯一的一个"第一个元素";

(2)集合中必存在唯一的一个"最后的元素";

(3)除最后元素之外,其它数据元素均有唯一的"后继";

(4)除第一元素之外,其它数据元素均有唯一的"前驱"。

五、数组和链表的区别?

从逻辑结构来看:数组的存储长度是固定的,它不能适应数据动态增减的情况。链表能够动态分配存储空间以适应数据动态增减的情况,并且易于进行插入和删除操作。

从访问方式来看:数组在内存中是一片连续的存储空间,可以通过数组下标对数组进行随机访问,访问效率较高。链表是链式存储结构,存储空间不是必须连续的,可以是任意的,访问必须从前往后依次进行,访问效率较数组来说比较低。

如果从第i个位置插入多个元素,对于数组来说每一次插入都需要往后移动元素,每一次的时间复杂度都是O(n),而单链表来说只需要在第一次寻找i的位置时时间复杂度为O(n),其余的插入和删除操作时间复杂度均为O(1),提高了插入和删除的效率。

六、单链表结构和顺序存储结构的区别?

当进行插入和删除操作时,顺序存储结构每次都需要移动元素,总的时间复杂度为O(n^2),而链式存储结构确定i位置的指针后,其时间复杂度仅为O(1)。由于顺序存储结构需要进行预分配存储空间,所以容易造成空间浪费或者溢出。链式存储结构不需要预分配存储空间,元素个数不受限制。

七、栈和队列的区别

队列是允许在一段进行插入另一端进行删除的线性表,对于进入队列的元素按“先进先出”的规则处理,在表头进行删除在表尾进行插入。

栈是只能在表尾进行插入和删除操作的线性表。对于插入到栈的元素按“后进先出”的规则处理,插入和删除操作都在栈顶进行。由于进栈和出栈都是在栈顶进行,所以要有一个size变量来记录当前栈的大小,当进栈时size不能超过数组长度,size+1,出栈时栈不为空,size-1。

八、栈的两个应用:括号匹配是怎么应用的?(如何实现要会用语言描述)

  • 括号匹配,表达式的计算

    将中缀表达式变为后缀表达式:

    ①从左往右,运算数输出,运算符号入栈

    ②栈内:(优先级低,()内符号依次入栈一起输出

    ​ 同级符号先进栈的先输出——b站2.2.4

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kVzQBPAL-1626507074517)(C:\Users\24380\Pictures\栈的应用1.jpg)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iTYMxGrX-1626507074522)(C:\Users\24380\Pictures\栈的应用2.jpg)]

bool match(string str)
{
	stack con;
	int count = 0;
	string::iterator iter = str.begin();
	while (iter != str.end())
	{
		if (*iter == '(')
			con.push(*iter);
		else if (*iter == ')')
		{
			if (con.empty())
				return 0;
			else
			{
				count++;
				con.pop();
			}
		}
		iter++;
	}
	if (con.empty())
	{
		cout << "匹配次数:" << count << endl;
		return 1;
 
	}
	else
	{
		return 0;
	}
 
}
#include
#include
#include
using namespace  std;
int cerr_flag=0;
int calculate(int len, char* str)
{
	stack Num;
	stack Symbol;
	bool bFlag = false;
	char c,OperSymbol;
	int temp;
	int num1, num2;
	for (int i = 0; i < len; i++)
	{
		c = str[i];
		if (isdigit(c))
		{
			temp = c - '0';
			Num.push(temp);
			continue;
		}
		else if (c == '*' || c == '/')
		{
			if (Symbol.size() > 0 && (Symbol.top() == '*' || Symbol.top() == '/'))
 
				bFlag = true;
			else
				bFlag = false;
 
		}
		else if (c == '+'||c=='-')
		{
			if (Symbol.size() == 0)
				bFlag = false;
			else
				bFlag = true;
 
		}
		if (bFlag)
		{
			num1 = Num.top();
			Num.pop();
			num2 = Num.top();
			Num.pop();
			OperSymbol = Symbol.top();
			Symbol.pop();
 
			switch (OperSymbol)
			{
			case '+':
				temp = num2 + num1;
				Num.push(temp);
				break;
			case '-':
				temp = num2 - num1;
				Num.push(temp);
				break;
			case '*':
				temp = num2 * num1;
				Num.push(temp);
				break;
			case '/':
				try{
					if (num1 == 0)
						throw invalid_argument("除数为0");	
				}
				catch (const invalid_argument& e)
				{
					cerr << "捕获异常:" << e.what() << endl;
					cerr_flag = 1;
					return NULL ;
				}
				temp = num2 / num1;
				Num.push(temp);
				break;
			default:
				break;
			}
 
		}
		Symbol.push(c);
	}
	while (Symbol.size() > 0)
	{
		num1 = Num.top();
		Num.pop();
		num2 = Num.top();
		Num.pop();
		OperSymbol = Symbol.top();
		Symbol.pop();
 
		switch (OperSymbol)
		{
		case '+':
			temp = num2 + num1;
			Num.push(temp);
			break;
		case '-':
			temp = num2 - num1;
			Num.push(temp);
			break;
		case '*':
			temp = num2 * num1;
			Num.push(temp);
			break;
		case '/':
			try{
				if (num1 == 0)
					throw invalid_argument("除数为0");
			}
			catch (const invalid_argument& e)
			{
				cerr << "捕获异常:" << e.what() << endl;
				cerr_flag = 1;
				return NULL;
			}
			temp = num2 / num1;
			Num.push(temp);
			break;
		default:
			break;
		}
	}
	return Num.top();
}
int main()
{
	char *a = "1+4*4-8/2";
	char *b = "2-9/0+2*3";
	int result1 = calculate(strlen(a), a);
	if (cerr_flag == 0)
	{
		cout << "运算结果为:" << result1<

九、介绍一下字符串匹配算法:朴素的匹配算法和KMP算法。(如何实现要会用语言描述)

1.BF算法(Brute Force)

  • 目标串t(待匹配串)
  • 模式串p(短的那个串)

①t的第一个字符和S的第一个比较,相等则继续t-2VSp-2,相等则继续t-3VSp3…

②不等则t-1VSp-2,t-2VSp-3

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T95hx3eP-1626507074525)(https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimage1.bubuko.com%2Finfo%2F202006%2F20200602235224951301.png&refer=http%3A%2F%2Fimage1.bubuko.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629074166&t=f3889fd2f0fbe07b9874e5eb11736b29)]

2.KMP算法:快速从主串找到子串

①上下子串前缀匹配

②找到公共前后缀(取最长且小于比较的上下字串长度)

③将下面的p子串前缀移动到后缀位置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2bGwD1FG-1626507074529)(https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.it610.com%2Fimage%2Fproduct%2F122af17cb1b14d198924ac73b585995e.jpg&refer=http%3A%2F%2Fimg.it610.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629074510&t=cd64bedb510629e98b4a751290feee31)]

Brute- Force算法在模式串中有多个字符和主串中的若干个连续字符比较都相等,但最后一个字符比较不相等时,主串的比较位置需要回退。KMP算法在上述情况下,主串位置不需要回退,从而可以大大提高效率

代码实现:

https://blog.csdn.net/xxibgdrgn_liqian_a_/article/details/80690593

十、如何构造哈夫曼树?(如何实现要会用语言描述)

找w最小求和,再找w最小;左小右大;构造结束后,左0右1

[例]频率表 A:60, B:45, C:13 D:69 E:14 F:5 G:3**

img

每个 字符 的 二进制编码 为(从根节点 数到对应的叶子节点,路径上的值拼接起来就是叶子节点字母的应该的编码)

字符 编码
A 10
B 01
C 0011
D 11
E 000
F 00101
G 00100

https://blog.csdn.net/qq_29519041/article/details/81428934

十一、最小生成树

(各方法如何实现要会用语言描述)

!!最小生成树是要找到最小的边可以把所有的节点都连接起来,而最短路径是要求某个节点到其余节点的最短的路径。

最小生成树:

在一给定的无向图G = (V, E) 中,(u, v) 代表连接顶点 u 与顶点 v 的边(即),而 w(u, v) 代表此边的权重,若存在 T 为 E 的子集(即)且为无循环图,使得 w(T) 最小,则此 T 为 G 的最小生成树
在这里插入图片描述

最小生成树其实是最小权重生成树的简称。

普里姆(prim)算法的基本思想为:顶点集到其他点权值最小边,加入新的顶点集,再找边…直到遍历所有点

从联通网络N={V,E}中某一顶点u0出发,选择与它关联的最小权值的边,将其顶点加入到顶点集S中,此后就从一个顶点在S集中,另一个顶点不在S集中的所有顶点中选择出权值最小的边,把对应顶点加入到S集中,直到所有的顶点都加入到S集中为止。

克鲁斯卡尔(kruskal)算法的基本思想为:依次选择最小边,使得无环且所有点遍历结束

假设有一个有n个顶点的联通网络N={V,E},初试时建立一个只有n个顶点,没有边的非连通图T,T中每个顶点都看作是一个联通分支,从边集E中选择出权值最小的边且该边的两个端点不在一个联通分支中,则把该边加入到T中,否则就再从新选择一条权值最小的边,直到所有的顶点都在一个联通分支中为止。

十二、最短路径的算法

Dijkstra时间复杂度为O(n^2)
Flyod时间复杂度为O(n^3) 空间复杂度为O(n ^ 2);

Dijkstra算法和Floyd算法 - LeftBody - 博客园 (cnblogs.com)

https://www.bilibili.com/video/BV1LE411R7CS?p=1&share_medium=android&share_plat=android&share_source=COPY&share_tag=s_i×tamp=1626505134&unique_k=Pf1030

最短路径:

用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。

迪杰斯特拉(dijastra)算法

经典的单源最短路径算法主要是其采用的动态规划思想.

弗洛伊德(floyd)算法

经典的求任意顶点之间的最短路径,采用贪心思想

数据结构面试题以及答案整理_第1张图片

十三、介绍一下深度优先搜索和广度优先搜索是如何实现的?

深度优先搜索:(1)访问起始点v0(2)若v0的第一个邻接点没有被访问过,则深度遍历该邻接点;(3)若v0的第一个邻接点已经被访问,则访问其第二个邻接点,进行深度遍历;重复以上步骤直到所有节点都被访问过为止

广度优先搜索:(1)访问起始点v0(2)依次遍历v0的所有未访问过得邻接点 (3)再依次访问下一层中未被访问过得邻接点;重复以上步骤,直到所有的顶点都被访问过为止

十四、介绍一下拓扑排序以及是如何实现的?

经典算法之拓扑排序_有酒醉三生丶-CSDN博客_拓扑排序算法

拓扑排序的步骤:(1)在有向图中任意选择一个没有前驱的节点输出(2)从图中删去该节点以及与它相连的边(3)重复以上步骤,直到所有的顶点都输出或者当前图中不存在无前驱的顶点为止,后者代表该图是有环图,所以可以通过拓扑排序来判断一个图是否存在环。

*十五、各种查找方法(各方法是如何实现的要会用语言描述)

查找分为静态查找表和动态查找表;静态查找表包括:顺序查找、折半查找、分块查找;动态查找包括:二叉排序树和平衡二叉树。

(1)顺序查找:把待查关键字key放入哨兵位置(i=0),再从后往前依次把表中元素和key比较,如果返回值为0则查找失败,表中没有这个key值,如果返回值为元素的位置i(i!=0)则查找成功,设置哨兵的位置是为了加快执行速度,时间复杂度为O(n),其特点是:结构简单,对顺序结构和链式式结构都适用,但查找效率太低。

(2)折半查找:要求查找表为顺序存储结构并且有序,若关键字在表中则返回关键字的位置,若关键字不在表中时停止查找的典型标志是:查找范围的上界<=查找范围的下界。

(3)分块查找:先把查找表分为若干子表,要求每个子表的元素都要比后面的子表的元素小,也就是保证块间是有序的(但是子表内不一定有序),把各子表中的最大关键字构成一张索引表,表中还包含各子表的起始地址。特点是:块间有序,块内无序,查找时块间进行索引查找,块内进行顺序查找。

(4)二叉排序树:二叉排序树的定义为:一棵空树,或者是一棵具有如下特点的树:如果该树有左子树,则其左子树的所有节点值小于根的值;若该树有右子树,则其右子树的所有节点值均大于根的值;其左右子树也分别为二叉排序树

(5)平衡二叉树:平衡二叉树又称为AVL树,它或者是一棵空树或者具有如下特点:他的左子树和右子树的高度差的绝对值不能大于1,且他的左右子树也都是平衡二叉树。

如果再一个平衡二叉树中插入一个节点可能造成失衡,这时就要进行树结构的调整,即平衡旋转。包括4中情况:在左子树的左子树上插入节点时向右进行单向旋转;在右子树的右子树上插入节点时向左进行单向旋转;在左子树的右子树插入节点时先向左旋转再向右旋转;在右子树的左子树插入节点时先向右旋转再向左旋转。

*十六、哈希表的概念、构造方法、冲突的解决办法?

哈希表又称为散列表,是根据关键字码的值直接进行访问的数据结构,即它通过把关键码的值映射到表中的一个位置以加快查找速度,其中映射函数叫做散列函数,存放记录的数组叫做散列表。

哈希函数的构造方法包括:直接定址法,除留余数法,数字分析法,平方取中法,折叠法,随机数法

(1)直接定址法:取关键字的某个线性函数值作为散列地址,H(key)=a*key+b。

(2)除留余数法:取关键字对p取余的值作为散列地址,其中p

(3)数字分析法:当关键字的位数大于地址的位数,对关键字的各位分布进行分析,选出分布均匀的任意几位作为散列的地址,适用于所有关键字都已知的情况。

(4)平方取中法:对关键字求平方,再取结果中的中间几位作为散列地址。

(5)折叠法:将关键字分为位数相同的几部分,然后取这几部分的叠加和作为散列地址。适用于关键字位数较多,且关键字中每一位上数字分布大致均匀。

(6)随机数法:选择一个随机函数,把关键字的随机函数值作为散列地址。适合于关键字的长度不相同时。

哈希冲突的解决方法包括:开放定址法和拉链法,当冲突发生时,使用某种探测技术形成一个探测序列,然后沿此序列逐个单单元查找,直到找到该关键字或者碰到一个开放的地址为止,探测到开放的地址表明该表中没有此关键字,若要插入,则探测到开放地址时可将新节点插入该地址单元。其中开放定址法包括:线性探查法,二次探查法,双重散列法

(1)线性探查法:基本思想,探查时从地址d开始,首先探查T[d],在探查T[d+1]…直到查到T[m-1],此后循环到T[0],T[1]…直到探测到T[d-1]为止。

(2)二次探查法:基本思想,探查时从地址d开始,首先探查T[d],再探查T[d+12],T[d+22]…等,直到探查到有空余地址或者探查到T[d-1]为止,缺点是无法探查到整个散列空间。

(3)双重散列法:基本思想,使用两个散列函数来确定地址,探查时从地址d开始,首先探查T[d],再探查T[d+h1(d)],T[d+2*h1(d)]…

链接法:将所有关键字为同义词的节点链接在同一个单链表中,若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组,凡是散列地址为i的节点均插入到头指针为i的单链表中。

*十七、各种排序算法(各方法如何实现要会用语言描述)

内部排序包括:插入排序、选择排序、交换排序、归并排序、基数排序。其中插入排序包括:直接插入排序、折半插入排序、希尔排序;选择排序包括:简单选择排序,堆排序;交换排序包括:冒泡排序、快速排序。

(1)直接插入排序(稳定):基本思想为:将序列分为有序部分和无序部分,从无序部分依次选择元素与有序部分比较找到合适的位置,将原来的元素往后移,将元素插入到相应位置上。时间复杂度为:O(n^2),空间复杂度为O(1)

(2)折半插入排序(稳定):基本思想为:设置三个变量low high mid,令mid=(low+high)/2,若a[mid]>key,则令high=mid-1,否则令low=mid+1,直到low>high时停止循环,对序列中的每个元素做以上处理,找到合适位置将其他元素后移进行插入。比较次数为O(nlog2n),但是因为要后移,因此时间复杂度为O(n^2),空间复杂度为O(1)。 优点是:比较次数大大减少。

(3)希尔排序(不稳定):基本思想为:先将序列分为若干个子序列,对各子序列进行直接插入排序,等到序列基本有序时再对整个序列进行一次直接插入排序。优点是:让关键字值小的元素能够很快移动到前面,且序列基本有序时进行直接插入排序时间效率会提升很多,空间复杂度为O(1)。

(4)简单选择排序(不稳定):基本思想为:将序列分为2部分,每经过一趟就在无序部分找到一个最小值然后与无序部分的第一个元素交换位置。优点是:实现简单,缺点是:每一趟只能确定一个元素的位置,时间效率低。时间复杂度为O(n^2),空间复杂度为O(1)。

(5)堆排序(不稳定):设有一个任意序列,k1,k2,…,kn,当满足下面特点时称之为堆:让此序列排列成完全二叉树,该树具有以下特点,该树中任意节点均大于或小于其左右孩子,此树的根节点为最大值或者最小值。优点是:对大文件效率明显提高,但对小文件效率不明显。时间复杂度为O(nlog2n),空间复杂度为O(1)。

(6)冒泡排序(稳定):基本思路为:每一趟都将元素进行两两比较,并且按照“前小后大”的规则进行交换。优点是:每一趟不仅能找到一个最大的元素放到序列后面,而且还把其他元素理顺,如果下一趟排序没有发生交换则可以提前结束排序。时间复杂度为O(n^2),空间复杂度为O(1)。

(7)快速排序(不稳定):基本思路为:在序列中任意选择一个元素作为中心,比它大的元素一律向后移动,比它小的元素一律向前移动,形成左右两个子序列,再把子序列按上述操作进行调整,直到所有的子序列中都只有一个元素时序列即为有序。优点是:每一趟不仅能确定一个元素,时间效率较高。时间复杂度为O(nlog2n),空间复杂度为O(log2n).

(8)归并排序(稳定):基本思想为:把两个或者两个以上的有序表合并成一个新的有序表。时间复杂度为O(nlogn),空间复杂度和待排序的元素个数相同。

(9)基数排序:时间复杂度为:对于n个记录进行链式基数排序的时间复杂度为O(d(n+rd)),其中每一趟分配的时间复杂度为O(n),回收的时间复杂度为O(rd)。

“前小后大”的规则进行交换。优点是:每一趟不仅能找到一个最大的元素放到序列后面,而且还把其他元素理顺,如果下一趟排序没有发生交换则可以提前结束排序。时间复杂度为O(n^2),空间复杂度为O(1)。

(7)快速排序(不稳定):基本思路为:在序列中任意选择一个元素作为中心,比它大的元素一律向后移动,比它小的元素一律向前移动,形成左右两个子序列,再把子序列按上述操作进行调整,直到所有的子序列中都只有一个元素时序列即为有序。优点是:每一趟不仅能确定一个元素,时间效率较高。时间复杂度为O(nlog2n),空间复杂度为O(log2n).

(8)归并排序(稳定):基本思想为:把两个或者两个以上的有序表合并成一个新的有序表。时间复杂度为O(nlogn),空间复杂度和待排序的元素个数相同。

(9)基数排序:时间复杂度为:对于n个记录进行链式基数排序的时间复杂度为O(d(n+rd)),其中每一趟分配的时间复杂度为O(n),回收的时间复杂度为O(rd)。

参考了知乎专栏:小昕学姐说保研
有参考,如有侵权请联系。

你可能感兴趣的:(数据结构)