2022提前批 - 奇安信 - 服务端开发工程师 - Python - 8月7日 - 笔试复盘

博主参加了奇安信2022年服务端开发的提前批笔试,主要涵盖计算机网络、操作系统、数据库、Python编程等方面。题目包括单选、不定项选择和编程题。博主分享了答题过程中的思考和错题分析,特别强调了Linux、数据库和TCP/IP协议栈的相关知识。编程题为两道回溯法(DFS)题目,分别涉及芭蕾舞演员挑选和最大资源储备问题。博主提醒考生注意基础知识的巩固和刷题的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2022提前批 - 奇安信 - 服务端开发工程师 - Python - 8月7日 - 笔试复盘

前言

秋招临近,试着投了奇安信的服务端开发的提前批
07.27 晚上投递,在牛客上找的内推,简历进入初筛阶段
08.05 晚上9点多,邮件通知笔试时间,要求确认是否参加笔试
08.07 15:00 - 17:00 牛客上参加笔试,全程监控,手机小程序监控,自己也开启录屏软件进行录制,准备复盘

题型:
1、单选题20道
2、不定项10道
3、编程题2道
可以自由选择先做哪种大题,但是一旦选择了就必须做完当前大题中的每一题才能提交,提交大题后不能再次进入修改。

单选

第一题

在这里插入图片描述
我的答案:C

A、地址解析协议 = ARP
ARP位于网络层,作用是将IP地址转换成MAC地址,用于路由寻址,报文传输过程中的目标地址和源地址都是不变的,是通过ARQ协议不断选择最佳地址来实现路由的。

具体的地址解析流程,大概是:
每个主机维护一个ARP缓存,缓存中IP地址和MAC地址以键值对的形式保存,当主机需要发送报文时,需要知道目标主机的MAC地址,此时会遍历自己的ARP缓存,如果命中,则直接发送;如果未命中,就需要用到ARP协议,向局域网广播一条UDP报文,内容大概是“我的IP地址是XX,我的MAC地址是XX,我想和目的IP地址是XX的主机通信,谁知道它的MAC地址,请告诉我!”,此时局域网中的主机接收到广播报文后,检查自己的ARP缓存中是否有该报文所需的地址,如果没有,则直接丢弃该广播报文(即无视),如果命中,则会先把该报文中的请求方IP地址和MAC地址存到自己的ARP缓存中,用于发送响应报文,响应报文内容大概是“我的IP地址是XX,我的MAC地址是XX,你要的目标MAC地址是XX,接好!”。收到响应的主机就得到了自己想要的目标MAC地址,将其存到自己的ARP缓存中后就可以发数据了。

B、可靠传输、流量控制 = TCP
创建可靠传输通道的是TCP协议,位于传输层。
TCP协议实现的可靠传输通道并不是一条实际存在的物理链路,而是通过各种协议和机制实现的虚拟链路,这里为了实现可靠传输而用到机制,包括:

  • 请求和响应机制,体现在报文头就是序号seq和确认序号ack
  • ARQ协议 - 停止等待协议,即上一个报文发送后,必须等到接收相应的请求后,才会开始发送下一个报文
  • 校验和,也是报文头的一个字段,用于接收方检验报文在链路中是否被篡改或者失真
  • 流量控制,体现在报文头上的接收窗口字段,通过该字段控制发送方发送数据的速度,避免发送速度太快导致接收方缓冲区慢,之后的报文就会丢失
  • 拥塞控制,通过拥塞控制算法和门限机制控制发送方向网络中注入数据的速度,避免无脑发消息导致网络拥塞

UDP正是因为没有这些机制,所以才叫做面向无连接的不可靠传输,如果问UDP想实现可靠传输该怎么做,应该将上述TCP实现的机制在应用层实现,这样应用层 + UDP 就也可以实现可靠传输了。

C、动态主机配置协议 = DHCP
DHCP位于应用层(之前没有详细了解这个协议,一度以为是网络层的),作用大概是一台主机插上网线以后,会通过DHCP协议去分配一直局域网IP地址给主机。

具体地址分配过程大概是:
发现阶段:客户端不知道DHCP服务器的地址,所以以广播的形式向局域网中发送DHCP-DISCOVER报文。

提供阶段:所有接收到DHCP-DISCOVER报文的DHCP服务器,都会从自己的地址池中取出一个可用的IP地址,通过DHCP-OFFER报文发送给客户端,服务器判断该地址是否可用的方法是向该地址不断发送ping报文(ICMP报文),如果一直得不到回应,说明该地址没人用,可以分配出去。

选择阶段:客户端可能会收到多个DHCP服务器发来的DHCP-OFFER报文,但客户端只会选取第一个收到的作为IP地址使用,之后客户端会广播DHCP-REQUEST报文,告知服务器,自己选取了哪一个服务器提供的IP地址使用,这样其他服务器可以将刚才分配出去的地址进行回收复用。

确认阶段:DHCP服务器检查该地址的租约信息,判断该地址是否可用,若可用,则会返回一个DHCP-ACK报文作为确认,同意客户端使用该地址,客户端收到确认后,通过发送ARP报文判断局域网中是否有其他主机使用这个地址,如果ARP报文没有响应,说明该地址可以使用,DHCP分配地址结束。

D、网络层目的是实现两个端系统之间的数据透明传送

这里只看透明传输的话,其实网络模型中每一层都实现了数据的透明传输,所谓透明传输就是指数据的传输本质上是要借助相邻层来实现的,但对于用户来说,就像是在本机的X层和对方的X层通信一样,比如说传输层,TCP报文本质上是通过下面的网络层、链路层、物理层,再到对面的物理层、链路层、网络层才最终到达对方的传输层,但对用户来说,该过程就是透明的,在用户看来,就是直接从主机的传输层发送数据到对方的传输层了。

而再看选项中说的是“端系统”,那么就应用确定是网络层。
传输层实现的是端口和端口之间(进程间)的透明传输。
网络层实现的是主机和主机之间(端系统间)的透明传输。

第二题

在这里插入图片描述
我的答案:C - 正确答案应该是D

top 命令可以动态地持续监听进程地运行状态。
这里Linux属于是盲区了,基本靠蒙。考完查了一下资料,A和B都是能实现的,继续查发现 C也是会发生的,所以答案应该是D

第三题

在这里插入图片描述
我的答案:D - 正确答案 - 不确定
在这里插入图片描述
根据给定的边,画了这样一棵树。

A、D是G的双亲;A D是G的祖先 - 没问题
B、树的深度是 5 而不是 4
C、树的根为A;叶子节点少了 H
D、H、J、I 是 G的兄弟,K、L、M是G的孩子 - 没问题

但本题是单选,AD感觉都对啊
最后反正我选了D,但是出来以后查资料,感觉定义上都没问题。

第四题

在这里插入图片描述
我的答案:B - 正确答案应该是C

MySQL存储引擎的题,考察InnoDB存储引擎的特性
A、自适应哈希索引,自适应不考虑的话,哈希索引是支持的,这里我不太理解自适应的意思,就算它对了。InnoDB支持两种索引的数据结构:B+树索引和哈希索引,MyISAM存储引擎只支持B+树索引,Memery存储引擎只支持哈希索引。

B、插入缓存

C、效率高,B+树索引与B树相比,做了两处改进:
1)B+树只在叶子节点上存储键值对,其余节点只存储键;而B树是在全部节点上都存储键值对。由于键本身会占用存储空间,所以对于B+树来说,读取一个非叶子节点时,可以读取到更多的键值,这一点在树这种数据结构上的体现就是——树的叉数会更多,当两棵树的总节点数相同时,如果一个树的叉数更多,那么这棵树的深度就会更浅(想象一下,叉数多代表这棵树很宽很肥),而树的深度浅,意味着IO次数就会少(IO次数就是树的深度),而现在的程序会慢,基本上都是IO引起的,只有尽可能减少IO次数,才能提高程序运行的效率,这就使得B+树比起B树来说更浅,IO次数少,索引速度快。其次,由于B树所有节点都存储键值对,所以索引时可能会在不同深度就找到数据返回,因此B树索引是不稳定的;而B+树因为无论如何都要从根节点搜索到叶子节点(只有叶子节点存了值),所以B+树的搜索效率的稳定的。
2)B+树的叶子节点,根据键的大小进行排序,通过双向环形链表组织起来,使得B+树是一种有序的数据结构,有序这一点在索引时很关键,可以实现排序、范围索引等功能,对于无序的数据结构(比如哈希表),要进行排序或范围索引,时间开销就等价于一次次执行等值索引。而B+树可以只进行一次等值索引,然后在根节点链表上进行顺序IO即可。

D、二次写
这个二次写应该说的是“二阶段提交”(我猜的,不是很确定),是指事务提交后,先写入binlog日志,此时事务状态为prepare(“准备”这个单词,不知道拼没拼对),然后再写入redolog,当写入redolog后,再将binlog中的事务状态设置为commit,保证数据写入的一致性。这里二次写之所以是InnoDB的特性,是因为MySQL服务层只提供了binlog(二进制日志),而redolog和undolog都是InnoDB实现的日志,所以二次写也算是InnoDB的特性。
如果不是二次写,而是先写redo再写binlog,或者先写binlog再写redo,都会导致宕机恢复数据后,一旦执行数据备份,发生数据不一致的问题。

问题是考完查资料的时候,发现ABD都是InnoDB的关键特性,只有C选项这个“效率高”模模糊糊的感觉是错的,InnoDB的写效率确实高,但读效率不如MyISAM,从这个角度理解的话,C选项就是错误答案。

第五题

在这里插入图片描述

我的答案:D
最后选了一个不带DISTINCT关键字的D选项

第六题

在这里插入图片描述
这里分析题目可以知道,P1是n,而n是最后一个入栈的,所以P2一定是n-1,P3一定是n-2,至此类推,可以得到 Px 中的 x + num = n + 1
当 x = i 时,num = n + 1 - i
答案是 A

第七题

在这里插入图片描述

我的答案:B
这个没什么好说的。TCP、UDP都是传输层协议

第八题

在这里插入图片描述
我的答案:C - 正确答案是 D

A、B、C都是HTTP报文头的字段,D不是。
端口号是传输层负责的,不出现在应用层的HTTP报文字段中。

第九题

在这里插入图片描述
我的答案:B
这里删除分区数据的语句用法应该是和ALTER TABLE一起使用,而不是单独使用

应该是这样:
ALTER TABLE table_name DROP PARTITION partition_name;

第十题

在这里插入图片描述

我的答案:B - 正确答案

本体考察的进程状态转移,本身不难,但是本人不太清楚这几个字母具体对应的是进程哪个状态,所以应该是错了。
在这里插入图片描述
从总体上来讲,进程状态转换只会有两个大方向,从R到非R、从非R到R
故正确答案应该是A

第十一题

在这里插入图片描述
我的答案:B

关于ARP表的作用就是建立IP地址到MAC地址的映射,直接选B

第十二题

在这里插入图片描述

我的答案:C
看程序写结果题,本科、考研都避无可避的题型,就是这种题型,培养出了人工DEBUG的能力。

看代码,意思是通过字典创建一种关系映射,将小写字母映射为ASCII码大15位的另一个小写字母,比如a的映射就是n,因为n比a在ASCII码上大15。

然后另一个点就是,要注意这里只对小写字母做了映射,大写字母在字典是没有键的,所以首字母P只会通过get方法取到自己原本的值,因此答案从AC里选,再通过a字母转换一下,可以确定答案是C

第十三题

在这里插入图片描述我的答案:B

B选项就是临界区的定义,直接选B

第十四题

在这里插入图片描述
我的答案:A

本题常刷算法题的应该不会陌生,想要获取单向链表倒数第x个节点,可以使用快慢指针法,具体来说就是,慢指针从链表头节点出发,而快指针先移动x次,使得快慢指针之间相差x-1个节点,对于快指针来说,慢指针就是快指针的倒数第x个节点,此时快慢指针同时顺着链表方向移动,直到快指针指向链表尾部,慢指针就是链表的倒数第x个节点,全过程只需要顺序遍历链表一次,因此时间复杂度是O(N)

顺便贴一下自己在github上写的题解:剑指 Offer 22. 链表中倒数第k个节点

第十五题

在这里插入图片描述
我的答案:A

不重复子串,观察该字符串,由7个字符组成,且不包含重复字符
因此不重复子串的个数应该是 28 = 7 + 6 + 5 + 4 + 3 + 2 + 1
7 = 长度为1的子串个数
6 = 长度为2的子串个数
。。
1 = 长度为7的子串个数

第十六题

在这里插入图片描述
本体考察迭代器,属于Python高级特性之一。
这里关于迭代器协议,大概是这样描述的:
可迭代对象就是实现了iter()方法的对象,iter()方法返回一个迭代器。
迭代器是实现了__next__()方法的对象,每次执行__next__()方法,会返回迭代器的下一个元素,直到没有元素时,会抛出异常。

当我们写Python代码时,经常写这样的语句:
for i in range(n)
这其实就是 for 语句将 range(n) 这个可迭代对象转换成了迭代器,之后每一次循环都是将迭代器中的下一个元素通过 next()方法返回。

A、数据流中没有数据时,调用__next__()方法会抛出异常,而不是阻塞
B、迭代器必须支持__next__()方法,但只能返回下一个元素而不能返回上一个
C、迭代器每次返回一个或多个元素
D、iter() 方法接收的不是任意对象,而必须是可迭代对象

AD可以确定是错误的,但BC不好排除,考完查阅资料时,发现迭代器没有明确限制一次能返回一个还是多个元素,但明确说了迭代器只能前进不能后退,所以答案应该是C。
我在考试时,考虑的是,for i, num in enumerate(nums) 这种语句,此时调用一次__next__()方法就会返回多个元素(i 和 num),所以选的C。

第十七题

在这里插入图片描述

第十八题

在这里插入图片描述
我的答案:A

本体是关于用户态和内核态的知识。
之所以要划分用户态和内核态,是因为有些指令可能会危害到系统安全,这类指令被称为特权指令,只有内核态可以执行这些特权指令,用户态无法执行。但是用户进程运行时,很多时候又不可避免地要用到这些特权指令,此时就必须交出CPU,将其交给内核态来执行,保证系统安全,这里就会阻塞,是用户态到内核态的切换造成的。
这里提到的特权指令,包括文件存取、网卡数据发送接收之类的IO操作。

A、程序代码不能手动指定在哪个态中执行
B、用户态能直接创建线程,但不能直接创建进程
C、用户态不能直接对文件进行写操作
D、用户态和内核态相互隔离

第十九题

在这里插入图片描述
我的答案:C

本题继续考察链表,当要存取,注意是 存 和 取,第 l 个节点及其前驱和后继。

这里我觉得很容易会选到双向链表,但是要知道,从查询角度看,双向链表的查询效率和单链表是一样的,这里仅分析时间复杂度。
而题目要求的是存和取,我理解的是加入节点和删除节点,如果要对双向链表进行存取,那么涉及到 8 个指针的移动,而单链表只需要处理 4 个指针
在这里插入图片描述
具体来说,我们可以遍历到单链表的第 l - 2 个节点,然后执行相应操作,就可以存取想要的节点值。

第二十题

在这里插入图片描述
我的答案:A

本题考察Python列表的知识

A、列表通常用于存储同构数据的多项集 - 这句话我考试前,BOSS直聘推送的Python面试题中看到原话,属于运气好,马上就考到了
B、可以使用列表推导式构造列表
C、列表是可变序列
D、[] 确实可以表示空列表

不定项

共10题
错选没分、少选有 1/3 的分数

第一题

在这里插入图片描述
我的答案:D

本题考察Linux命令,不敢多选,只敢选D
选错没分,少选还有 1/3 的分数

第二题

在这里插入图片描述
我的答案:AC

A、关系型数据库因为字段间往往存在关联关系,所以做数据结构变动时,会涉及多张表的联动,所以变动较为困难
B、事务处理能力弱,应该是对表进行垂直拆分后才有的缺陷,其实也算,但我这里就没选
C、大数据处理能力差,这是关系型数据库应对大量数据的一个缺陷
D、程序产出效率低,我觉得是无稽之谈了。。

第三题

在这里插入图片描述
我的答案:CD - 正确答案是ACD

本题考察Python基础
A、compile() 将一个字符串转换成字符代码,是内置函数。平时没用到过,不清楚,就没选
B、pass是关键字,不是函数
C、hex() 将十进制数转换成十六进制时使用,是内置函数
D、ord() 将字符转换成ASCII码,是内置函数

第四题

在这里插入图片描述

第五题

在这里插入图片描述
我的答案:AB

只敢选AB,其他选项不太敢选

第六题

在这里插入图片描述
我的答案:B

本题考察ICMP协议的作用,该协议位于网络层,作为IP协议的补充,实现了一些IP协议没有实现的功能,最常用的ping命令就是ICMP协议实现的。所以这里我只敢选B,其他不敢选。

第七题

在这里插入图片描述
我的答案:A

本题考察IO模型

A、epoll分为边沿触发和水平触发。
B、select监控的fd确实有限,上限是1024个,但如果要监控更多fd,可以使用poll,poll的上限是2048个。
C、不确定
D、poll和select出了使用形式不同,fd的上限也不同

这里BD可以确定是错误的,正常来说答案是AC,因为是不定项嘛,但是考试时突然想到,万一不定项也有可能是单选怎么办呢,所以最后还是只选了A

第八题

在这里插入图片描述
我的答案:ABC

本题考察Python基础

ABC都是对的
D选项提到的tuple() 是用来创建元组的

第九题

在这里插入图片描述
我的答案:D

我之所以敢选D,是因为D选项涉及的正是IP报文分报的原理。
其他不敢选是因为都忘了

第十题

在这里插入图片描述
我的答案:A - 蒙的

Linux命令题,再次倒下。。。

编程题

第一题 - 挑选芭蕾舞演员

在这里插入图片描述
在这里插入图片描述
本题的解法是采用暴力法,但暴力法也分很多种,我这里用的是回溯法 - 也可以理解成DFS - 深度优先搜索。

class Solution:
    def TeamNums(self , height ):
        # write code here
        self.res = 0
        n = len(height)
        vis = [False for _ in range(n)]

        def dfs(depth, i, cur):
            # 终止条件
            if depth == 3:
                self.res += 1
                return
            for j in range(i, n):
                if not vis[j] and (not cur or height[j] > cur[-1]):
                    cur.append(height[j])
                    vis[j] = True
                    dfs(depth + 1, j + 1, cur)
                    vis[j] = False
                    cur.pop()

        dfs(0, 0, [])
        height = height[::-1]
        dfs(0, 0, [])
        return self.res

这里值得注意的是,除了递增的组合,我们还要输出递减的组合,此时有两种思路:
1、再写一个DFS函数,将 大于号 改成 小于号即可
2、直接将height数组逆序翻转,再次执行DFS
我这里使用的是后者。

题外话:
我在复盘这道题时,总结本题的两个点:
1、找出递增子序列
2、递增子序列长度固定

联想到了之前蓝桥杯模拟赛时的一个题,因为这道题站内有人私信我,问过我,所以我印象就深了一些,下面贴出这道题。

出自第十二届蓝桥杯第三次模拟赛 - 第10题
在这里插入图片描述
仔细对比两道题就会发现,其实这两题抽象出来以后,是完全一样的思路。而当时我在写蓝桥杯这道题时,联想到的是动态规划,所以写起来确实不顺手,但当时也还是解出来了。

n, k  = list(map(int, input().split()))
nums = list(map(int, input().split()))

dp = [[0 for _ in range(k+1)] for _ in range(n)]
for i in range(n):
    dp[i][1] = 1
    for j in range(i):
        if nums[i] > nums[j]:
            for p in range(1, k):
                if dp[j][p] != 0:
                    dp[i][p+1] += (dp[j][p] % 1000007)
print(sum(dp[x][-1] for x in range(n)) % 1000007)

然后我就试着再用回溯的思路写一遍这道题:

n, k = list(map(int, input().split()))
height = list(map(int, input().split()))

n = len(height)
vis = [False for _ in range(n)]


def dfs(depth, i, cur, res):
    # 终止条件
    if depth == k:
        print(cur)
        res += 1
        return res
    for j in range(i, n):
        if not vis[j] and (not cur or height[j] > cur[-1]):
            cur.append(height[j])
            vis[j] = True
            res = max(res, dfs(depth + 1, j + 1, cur, res))
            vis[j] = False
            cur.pop()
    return res


res = dfs(0, 0, [], 0)
print(res)

这也说明了,DP和回溯,包括DFS,本质上都是暴力法,都是将所有可能的解遍历一边,搜个精光,只不过DP和DFS、回溯,搜索起来很有章法,能保证不重不漏!

第二题 - 最大资源储备

在这里插入图片描述
在这里插入图片描述
本题是在矩阵上进行搜索,一开始我觉得有点像动态规划的感觉,稍微一思考就觉得不对,本题不满足最优子结构,所以DP pass

再就是联想到之前做过的岛屿沉没系列的题,觉得很像,想从BFS - 广度优先搜索来下手,于是在草稿纸上写了一段伪代码,写着写着发现自己其实在写的是 DFS。仔细一思考,还真又是一道DFS题。

class Solution:
    def getMaximumResource(self , grid ):
        # write code here
        # 特判 - 题目范围中 m, n 可能为0
        if not grid or not grid[0]:
            return 0
        m, n = len(grid), len(grid[0])
        self.res = 0
        vis = set()

        def dfs(x, y, temp):
            # 终止条件 - node出界、node值为0、node已遍历过
            if x > m-1 or x < 0 or y > n-1 or y < 0 or x * n + y in vis or grid[x][y] == 0:
                self.res = max(self.res, temp)
                return
            # 此时node合法 - 加上当前node值
            temp += grid[x][y]
            vis.add(x * n + y)
            print(x, y, temp)
            # 向上下左右继续深搜
            for add_x, add_y in ((-1, 0), (1, 0), (0, -1), (0, 1)):
                # new_node = (x + add_x, y + add_y)
                dfs(x + add_x, y + add_y, temp)
            # 关键的回退 - traceback
            vis.discard(x * n + y)

        for i in range(m):
            for j in range(n):
                dfs(i, j, 0)
        return self.res

这里一开始写的时候,一直都忘了回溯关键的回退操作,所以答案一直不对,最后尝试加了print() ,打印搜索路径,发现一目了然。这里也推荐做回溯或者DFS时,用打印法打印搜索路径,对于找bug还是很有用的。

就拿测试用例来说,打印出的结果是:

0 1 6
1 1 14
2 1 23
1 0 19
1 2 21
1 0 5
1 1 13
0 1 19
2 1 22
1 2 20
1 1 8
0 1 14
2 1 17
1 0 13
1 2 15
1 2 7
1 1 15
0 1 21
2 1 24
1 0 20
2 1 9
1 1 17
0 1 23
1 0 22
1 2 24

当你发现输出结果和你想要的算法一致时,说明你的代码大概率就没问题了!

总结

选择题暴露了自己的短板:
1、Linux经验基本为0
2、数据库、计网、OS基础不牢、不细,虽然大家都知道的东西了解的很多,但了解的不够细,比如ICMP、DHCP等这些就掌握的不多,HTTP报文头字段等了解的就不够细
3、基础概念要多看,很多选项都是概念的原话,但是在我眼里就是不确定的选项

编程题:
奇安信的笔试编程题不难,两道都是DFS-回溯题,放在力扣上也就是中等题的难度,考前查漏补缺时发现自己回溯的短板了,及时补上了,算是不幸中的万幸。
提交两道题时,都是是100%用例通过。
需要继续保持刷题。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值