【数据结构】常见面试题

写在前面:
保研夏令营已经开始了,下周就要开始第一场面试了,于是赶紧整理一下可能会考的知识点

1.二叉树

在这里插入图片描述两个重要概念:

完全二叉树:对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。

满二叉树:除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树
简单例题:
1.求二叉树中的节点个数

递归解法:
(1)如果二叉树为空,节点个数为0
(2)如果二叉树不为空,二叉树节点个数 = 左子树节点个数 + 右子树节点个数 + 1
参考代码如下:

int GetNodeNum(BinaryTreeNode * pRoot)
{
    if(pRoot == NULL) // 递归出口
        return 0;
    return GetNodeNum(pRoot->m_pLeft) + GetNodeNum(pRoot->m_pRight) + 1;
}

2、求二叉树的深度

递归解法:
(1)如果二叉树为空,二叉树的深度为0
(2)如果二叉树不为空,二叉树的深度 = max(左子树深度, 右子树深度) + 1
参考代码如下:

int GetDepth(BinaryTreeNode * pRoot)
{
    if(pRoot == NULL) // 递归出口
        return 0;
    int depthLeft = GetDepth(pRoot->m_pLeft);
    int depthRight = GetDepth(pRoot->m_pRight);
    return depthLeft > depthRight ? (depthLeft + 1) : (depthRight + 1); 
}

3.求二叉树中叶子节点的个数
递归解法:
(1)如果二叉树为空,返回0
(2)如果二叉树不为空且左右子树为空,返回1
(3)如果二叉树不为空,且左右子树不同时为空,返回左子树中叶子节点个数加上右子树中叶子节点个数
参考代码如下:

int GetLeafNodeNum(BinaryTreeNode * pRoot)
{
    if(pRoot == NULL)
        return 0;
    if(pRoot->m_pLeft == NULL && pRoot->m_pRight == NULL)
        return 1;
    int numLeft = GetLeafNodeNum(pRoot->m_pLeft); // 左子树中叶节点的个数
    int numRight = GetLeafNodeNum(pRoot->m_pRight); // 右子树中叶节点的个数
    return (numLeft + numRight);
}

4.求二叉树中两个节点的最低公共祖先节点
递归解法:
(1)如果两个节点分别在根节点的左子树和右子树,则返回根节点
(2)如果两个节点都在左子树,则递归处理左子树;如果两个节点都在右子树,则递归处理右子树
参考代码如下:

bool FindNode(BinaryTreeNode * pRoot, BinaryTreeNode * pNode)
{
    if(pRoot == NULL || pNode == NULL)
        return false;
 
    if(pRoot == pNode)
        return true;
 
    bool found = FindNode(pRoot->m_pLeft, pNode);
    if(!found)
        found = FindNode(pRoot->m_pRight, pNode);
 
    return found;
}
 
BinaryTreeNode * GetLastCommonParent(BinaryTreeNode * pRoot, 
                                     BinaryTreeNode * pNode1, 
                                     BinaryTreeNode * pNode2)
{
    if(FindNode(pRoot->m_pLeft, pNode1))
    {
        if(FindNode(pRoot->m_pRight, pNode2))
            return pRoot;
        else
            return GetLastCommonParent(pRoot->m_pLeft, pNode1, pNode2);
    }
    else
    {
        if(FindNode(pRoot->m_pLeft, pNode2))
            return pRoot;
        else
            return GetLastCommonParent(pRoot->m_pRight, pNode1, pNode2);
    }
}

完全二叉树

判断是否是完全二叉树:
·如果二叉树上某个结点 有右孩子无左孩子则一定不是完全二叉树;
·如果不是两个孩子都全(即 要么有左没右,要么左右均无)
层序遍历,一个萝卜一个坑,碰到第一个没有左孩子或者右孩子的节点你就跳出来,如果目前数量等于节点总数,那就是的,否则就不是

二叉搜索树

所谓二叉搜索树,可提供对数时间的元素插入和访问。二叉搜索树的节点放置规则是:任何节点的键值一定大于去其左子树中的每一个节点的键值,并小于其右子树的每一个节点的键值。

所以在二叉树中找到最大值和最小值是很简单的,比较麻烦的是元素的插入和移除。
插入新元素时,从根节点开始,遇键值较大者就向左,遇键值较小者就向右,一直到尾端,即为插入点。
移除旧元素时,如果它是叶节点,直接拿走就是了;如果它有一个节点,那就把那个节点补上去;如果它有两个节点,那就把它右节点的最小后代节点补上去。

平衡二叉树

判断二叉树是不是平衡二叉树
递归解法:
(1)如果二叉树为空,返回真
(2)如果二叉树不为空,如果左子树和右子树都是AVL树并且左子树和右子树高度相差不大于1,返回真,其他返回假
参考代码:

bool IsAVL(BinaryTreeNode * pRoot, int & height)
{
    if(pRoot == NULL) // 空树,返回真
    {
        height = 0;
        return true;
    }
    int heightLeft;
    bool resultLeft = IsAVL(pRoot->m_pLeft, heightLeft);
    int heightRight;
    bool resultRight = IsAVL(pRoot->m_pRight, heightRight);
    if(resultLeft && resultRight && abs(heightLeft - heightRight) <= 1) // 左子树和右子树都是AVL,并且高度相差不大于1,返回真
    {
        height = max(heightLeft, heightRight) + 1;
        return true;
    }
    else
    {
        height = max(heightLeft, heightRight) + 1;
        return false;
    }
}

红黑树

首先红黑树的节点要么是红色,要么是黑色。
红黑树是一种二叉查找树,但在每个结点上增加了一个存储位表示结点的颜色,可以是RED或者BLACK。通过对任何一条从根到叶子的路径上各个着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。

当二叉查找树的高度较低时,这些操作执行的比较快,但是当树的高度较高时,这些操作的性能可能不比用链表好。红黑树(red-black tree)是一种平衡的二叉查找树,它能保证在最坏情况下,基本的动态操作集合运行时间为O(lgn)。
重要性质:
1 根节点是黑色的

2 每个叶子节点是黑色的且不存储数据

3 任何相邻的节点不能同时为红色

4 每个节点,从该节点到可达的叶子节点的所有路径,其黑色节点的数目相同。

红黑树的应用场景:红黑树是一种不是非常严格的平衡二叉树,没有AVLtree那么严格的平衡要求,所以它的平均查找,增添删除效率都还不错。广泛用在C++的STL中。如map和set都是用红黑树实现的。

2.哈夫曼树

哈夫曼树也叫做最优二叉树,一种带权路径长度最短的二叉树。那么什么是树的带权路径长度,它是树中所有的叶子节点的权值乘上其根节点的路径长度。

3.B树

m阶B树定义

m阶B树是一棵平衡的m路搜索树,或者是空树,或者是满足以下条件:

树中的每个节点最多有m个孩子

除了根节点和叶子结点外,其他节点最少含有 (m+1)/2 个孩子

如果根节点不是叶子结点,则根节点最少2个孩子

所有叶子节点都在同一层,并不带任何信息

除了叶子结点,节点含有关键字属性,数目范围是 [M/2 - 1,M-1],即关键字个数 = 孩子个数 - 1。

4.Trie树(字典树)

又称前缀树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。

trie树的优点:利用字符串的公共前缀来节约存储空间,最大限度地减少无谓的字符串比较,查询效率比哈希表高。缺点:Trie树是一种比较简单的数据结构.理解起来比较简单,正所谓简单的东西也得付出代价.故Trie树也有它的缺点,Trie树的内存消耗非常大.

    根节点不包含字符,除根节点外每一个节点都只包含一个字符。
    从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
    每个节点的所有子节点包含的字符都不相同。

典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。字典树与字典很相似,当你要查一个单词是不是在字典树中,首先看单词的第一个字母是不是在字典的第一层,如果不在,说明字典树里没有该单词,如果在就在该字母的孩子节点里找是不是有单词的第二个字母,没有说明没有该单词,有的话用同样的方法继续查找.字典树不仅可以用来储存字母,也可以储存数字等其它数据。

5.并查集

就是既有合并又有查找操作的问题。举个例子,有一群人,他们之间有若干好友关系。如果两个人有直接或者间接好友关系,那么我们就说他们在同一个朋友圈中,这里解释下,如果Alice是Bob好友的好友,或者好友的好友的好友等等,即通过若干好友可以认识,那么我们说Alice和Bob是间接好友。随着时间的变化,这群人中有可能会有新的朋友关系,这时候我们会对当中某些人是否在同一朋友圈进行询问。这就是一个典型的合并-查找操作问题,既包含了合并操作,又包含了查找操作。

并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。

连通分量:无向图中的极大连通子图。(子图必须是连通的且含有极大顶点数)。图1有两个连通分量

强连通分量:有向图中的极大强连通子图。

图的遍历

1.深度优先遍历(DFS):从图中某个顶点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。
2.广度优先遍历(BFS):类似于树的层次遍历。

最小生成树

生成树:一个连通图的生成树是一个极小的连通子图,它含有图中全部的顶点,但只有足以构成一棵树的n-1条边。

最小(代价)生成树:Minimum (cost) spanning tree。构造连通网的最小代价生成树称为最小生成树。(一棵生成树的代价就是树上各边的代价之和)

必须满足条件:算法完成后不能存在回路;V个顶点就只有V-1条边;必须包含所有顶点
1.普里姆(prime):

第一种:先将一个起点加入最小生成树,之后不断寻找与最小生成树相连的边权最小的边能通向的点,并将其加入最小生成树,直至所有顶点都在最小生成树中。
2.克鲁斯卡尔(kluskal):在剩下的所有未选取的边中,找最小边,如果和已选取的边构成回路,则放弃,选取次小边。

最短路径

迪杰斯特拉算法(Dijkstra)

把图中的顶点集合V分成两组,第一组为已求出最短路径的顶点集合S(初始时S中只有源节点,以后每求得一条最短路径,就将它对应的顶点加入到集合S中,直到全部顶点都加入到S中);第二组是未确定最短路径的顶点集合U。

算法步骤:
(1)初始化时,S只含有源节点;
(2)从U中选取一个距离v最小的顶点k加入S中(该选定的距离就是v到k的最短路径长度);
(3)以k为新考虑的中间点,修改U中各顶点的距离;若从源节点v到顶点u的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值是顶点k的距离加上k到u的距离;
(4)重复步骤(2)和(3),直到终点在S中。

弗洛伊德算法(Floyd)

1,从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。
2,对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比已知的路径更短。如果是更新它。

拓扑排序

定义:如果从V 到W有一条有向路径,V一定要排在W前面。
能够进行拓扑排序的一定是有向无环图。

链表和数组

1.数组和链表的区别

数组将元素在内存中连续存放,可以通过下标迅速方位数组中的任何元素,插入删除效率低,但是随机读取效率高。需要预留空间,在使用前先申请占内存的大小,可能会浪费内存空间,不利于拓展。
链表通过存在元素的指针联系到一起,不指定大小,扩展方便,可随意增删。
从逻辑结构来看:
a) 数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费;数组可以根据下标直接存取。
b) 链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。(数组中插入、删除数据项时,需要移动其它数据项,非常繁琐)链表必须根据next指针找到下一个元素
从内存存储来看:
a) (静态)数组从栈中分配空间, 对于程序员方便快速,但是自由度小
b) 链表从堆中分配空间, 自由度大但是申请管理比较麻烦
从上面的比较可以看出,如果需要快速访问数据,很少或不插入和删除元素,就应该用数组;相反, 如果需要经常插入和删除元素就需要用链表数据结构了。

2.怎么判断链表是否有环

使用快慢指针:两个指针分别按照固定步长行走,P1一次走1布,P2一次走2布,如果链表有环,P1和P2会有相遇的时刻。

3.怎样合并两个有序链表

指针不停地改变

4.怎样反转链表

有一个pre指针,一个p指针指向头节点,next节点指向头节点的下一个节点,然后p节点的next指向pre,pre等于p,p等于next,最后return pre

排序和查找

1.有哪些排序算法?时间和空间复杂度?是否稳定?

在这里插入图片描述
在这里插入图片描述

(1)平方阶(O(n2))排序
  各类简单排序:直接插入、直接选择和冒泡排序;
(2)线性对数阶(O(nlog2n))排序
  快速排序、堆排序和归并排序;
说明:
当原表有序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n);
而快速排序则相反,当原表基本有序时,将蜕化为冒泡排序,时间复杂度提高为O(n2);
原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。
稳定性:
排序算法的稳定性:若待排序的序列中,存在多个具有相同关键字的记录,经过排序,这些记录的相对次序保持不变,则称该算法是稳定的;若经排序后,记录的相对次序发生了改变,则称该算法是不稳定的。
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序

1.快速排序

1)选择一个基准元素,通常选择第一个元素或者最后一个元素,
2)通过一趟排序将待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的元素值比基准值大。
3)此时基准元素在其排好序后的正确位置
4)然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。

2.选择排序

选择排序算法准则:
设待排序元素的个数为n.
1)当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。
2)当n较大,内存空间允许,且要求稳定性:归并排序
3)当n较小,可采用直接插入或直接选择排序。
直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。
直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序

2.哈希

是什么

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数

解决哈希冲突的方法

当哈希 表关键字集合很大时,关键字值不同的元素可能会映像到哈希表的同一地址上。
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。

1.开放定址法:当发生哈希冲突时使用另一个哈希函数计算地址值,直到冲突不再发生。
·线性探查
·二次探查
·伪随机探测
2.链地址法:将所有哈希值相同的key通过链表存储,key按顺序插入到链表中
3.再哈希法:使用另一个哈希函数计算地址值,直到冲突不再发生
4.建立公共溢出区:采用一个溢出表存储产生冲突的关键字,如果公共溢出区还产生冲突,再采用处理冲突方法处理。

字符串

KMP算法

在一个字符串中查找是否包含目标的匹配字符串。其主要思想是每趟比较过程让子串先后滑动一个合适的位置。当发生不匹配的情况时,不是右移一位,而是移动(当前匹配的长度– 当前匹配子串的部分匹配值)位。

其它算法问题

1.用循环比递归效率高吗?

递归和循环两者完全可以互换。不能完全决定性地说循环地效率比递归的效率高。
递归算法:
优点:代码简洁、清晰,并且容易验证正确性。
缺点:它的运行需要较多次数的函数调用,如果调用层数比较深,需要增加额外的堆栈处理(还有可能出现堆栈溢出的情况),比如参数传递需要压栈等操作,会对执行效率有一定影响。但是,对于某些问题,如果不使用递归,那将是极端难看的代码。在编译器优化后,对于多次调用的函数处理会有非常好的效率优化,效率未必低于循环。
循环算法:
优点:速度快,结构简单。
缺点:并不能解决所有的问题。有的问题适合使用递归而不是循环。如果使用循环并不困难的话,最好使用循环。

2.了解贪心算法、动态规划、分治递归吗

贪心算法:局部最优,划分的每个子问题都最优,得到全局最优,但是不能保证是全局最优解,所以对于贪心算法来说,解是从上到下的,一步一步最优,直到最后。

动态规划:将问题分解成重复的子问题,每次都寻找左右子问题解中最优的解,一步步得到全局的最优解.重复的子问题可以通过记录的方式,避免多次计算。所以对于动态规划来说,解是从小到上,从底层所有可能性中找到最优解,再一步步向上。

分治法:和动态规划类似,将大问题分解成小问题,但是这些小问题是独立的,没有重复的问题。独立问题取得解,再合并成大问题的解。

整合了不少博主的内容,在此表示感谢!

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值