记录面试题目

79 阅读20分钟

前言

在经历了职业转换的半个月后,我有幸加入了新的团队。在此,我愿分享我在面试过程中遇到的一些基本问题,这些问题都是基于实际经验的真实反馈,旨在为同样面临职场挑战的你提供参考。文末有彩蛋希望你一定要读到最后!

面试题目

1、多进程和多线程的使用场景

多进程

  • CPU密集型任务:当程序需要进行大量的CPU计算时,可以使用多进程来充分利用多核CPU,提高计算效率。
  • 并行计算:在需要进行并行计算的应用中,多进程可以实现真正的并行执行,每个进程独享一部分系统资源,不会受到GIL(全局解释器锁)的限制。
  • 服务端程序:在需要同时处理多个客户端连接的服务器程序中,可以使用多进程来处理每个客户端的请求,以提高并发处理能力。

多线程

  • I/O密集型任务:当程序需要进行大量的I/O操作(如文件读写、网络通信等)时,可以使用多线程来提高效率,因为在I/O操作时,CPU大部分时间处于空闲状态,可以让其他线程继续执行。
  • GUI应用程序:在用户界面程序中,需要保持UI的响应性,而且有很多后台任务需要同时执行,这时可以使用多线程来处理后台任务,以免阻塞主线程导致UI无响应。
  • 异步编程:通过多线程可以很方便地实现异步编程,例如在Web开发中处理并发请求、消息处理等场景。

2、在多线程中,如何避免出现死锁、竞争等问题

在多线程编程中,避免死锁和竞争条件是至关重要的,它们不仅能够提高程序的效率,还能确保数据的一致性和完整性。以下是一些避免这些问题的策略:

  1. 理解死锁和竞争条件

    • 死锁:当两个或多个线程在执行过程中,由于竞争资源或者彼此通信而造成的一种阻塞的现象,若无外力作用,这些线程都将无法向前推进。
    • 竞争条件:当多个线程同时访问和修改共享资源时,由于执行顺序的不确定性,导致最终结果取决于线程的执行顺序,从而可能出现的问题。
  2. 避免死锁的策略

    • 避免嵌套锁定:确保每个线程以相同的顺序请求锁,这样可以避免循环等待的条件,是预防死锁的关键策略之一。
    • 使用超时锁定:设置锁超时可以避免线程无限期地等待一个永远不会释放的锁。如果一个线程在指定的超时时间内无法获得锁,它应该释放自己持有的锁并回退,试图重新获取需要的锁。
    • 设置线程优先级:通过给线程设置优先级,可以在死锁发生时让一部分低优先级的线程回退,高优先级的线程继续执行,从而减少死锁的发生概率。确保在死锁发生时随机设置优先级,以避免固定优先级导致的不公平问题。
  3. 避免竞争条件的策略

    • 同步访问共享资源:确保每次只有一个线程可以访问共享资源。这可以通过使用同步机制如互斥锁(mutexes)、信号量(semaphores)或锁关键字(如Java中的synchronized关键字)来实现。
    • 使用原子操作:利用原子操作或原子变量可以在不需要显式锁定的情况下安全地进行操作,因为原子操作是不可中断的,这保证了操作的原子性。
    • 序列化访问:通过对共享资源的访问进行序列化,确保一次只有一个线程可以修改数据,从而避免竞争条件。
  4. 高级并发控制技术

    • 死锁检测和恢复:使用算法和工具来监控线程锁的状态,并在检测到死锁时自动解锁部分线程以恢复程序的运行。
    • 使用非阻塞同步机制:采用乐观锁、比较并交换(CAS)等非阻塞同步机制可以减少锁的使用,从而降低死锁的风险。
    • 限制线程数量:通过限制系统中活跃的线程数量,可以减少资源竞争和死锁的可能性。

总的来说,虽然多线程编程增加了程序的复杂性,但通过遵循上述策略,可以有效地避免死锁和竞争条件,从而提高程序的稳定性和性能。在设计和实现多线程程序时,始终保持对线程行为的精确控制和对资源访问的管理,是确保并发安全性的关键。

3、Python内部是如何定义可变数据结构和不可变数据结构

在Python内部,可变数据结构和不可变数据结构的定义主要基于内存管理和对象变量的关联方式。这些定义决定了数据结构的使用方式和性能特点。以下是具体介绍:

  • 不可变数据结构

    1. 内存分配:当一个不可变数据类型被创建时,如整数、字符串或元组,Python为其分配一块特定的内存空间。这块内存中存储的数据在对象的整个生命周期内都不会改变。
    2. 变量与值:对于不可变类型的变量,实际上存储的是数据的内存地址。因此,当变量需要被赋予新值时,实际上是改变了这个变量所指向的内存地址,而不是修改原有内存地址中的数据。
    3. 数据更新:如果需要“更新”一个不可变类型的变量值,实际上是创建了一个新的对象,并在内存中为其分配新的空间。原来的对象如果没有被其他变量引用,则会被垃圾回收机制回收。
  • 可变数据结构

    1. 内存分配:可变数据类型如列表、字典和集合在创建时也会被分配一块内存。与不可变类型不同的是,这块内存中存储的数据在对象生命周期内是可以被修改的。
    2. 变量与值:对于可变类型的变量,同样存储的是数据的内存地址。不同的是,这块内存中的具体内容可以通过各种方法进行原地修改,例如添加、删除或更改元素。
    3. 数据更新:更新可变类型的变量时,可以直接在原有的内存地址上进行修改,不需要为新的数据值分配额外的内存空间。这种方式使得可变类型在处理大规模数据变动时更加高效。

总的来说,Python内部通过不同的内存管理和数据处理机制来定义可变数据结构和不可变数据结构。不可变数据类型一旦创建便不能更改,任何看起来像是变更的操作实际上是创建了新的实例;而可变数据类型则允许在原有内存基础上直接修改数据,提供了灵活的数据操作能力。

4、Python继承的顺序

  1. 广度优先(BFS) :新式类的继承顺序则是采用广度优先的顺序进行的。这意味着当查找一个属性或方法时,会先在直接父类中查找,然后逐层往外扩展,直到找到为止。
  2. MRO算法:新式类使用C3线性化算法,即Method Resolution Order算法,来生成一个列表保存继承顺序表,确保每个类只出现一次,且满足单调性原则。

5、Python的迭代器如何理解

迭代器是一种支持迭代协议的对象,可以按照特定的顺序逐个访问数据。在Python中,大多数容器(列表,字符串,字典)都是可迭代的,并且可以使用迭代器来遍历其中的元素。

  • 迭代器协议

    迭代器协议是一种规范,用于定义迭代器对象必须实现的方法。根据迭代器协议,一个迭代器对象必须实现以下两个方法:

    • iter():返回迭代器对象本身
    • next():返回迭代器的下一个值。如果已经没有更多的元素,抛出StopIteration异常
    class MyIterator:
        def __init__(self, data):
            self.data = data
            self.index = 0
        
        def __iter__(self):
            return self
        
        def __next__(self):
            if self.index >= len(self.data):
                raise StopIteration
            value = self.data[self.index]
            self.index += 1
            return value
    
    # 使用自定义迭代器遍历列表
    my_iter = MyIterator([1, 2, 3, 4, 5])
    for item in my_iter:
        print(item)
    

    在上述示例中,MyIterator是一个自定义的迭代器类,实现了迭代器协议中的 iternext 方法。通过在for循环中使用自定义迭代器,我们可以逐个遍历列表中的元素。

  • Python提供一些内置的函数和语法来简化迭代过程

    iter()函数

    **iter()**函数可以将可迭代对象转换为迭代器。当我们需要使用迭代器遍历一个可迭代对象时,可以使用 **iter()**函数进行转换。

    下面是一个使用 **iter()**函数遍历字符串的示例:

    string = 'Hello, World!'
    
    # 使用 iter() 函数将字符串转换为迭代器
    string_iter = iter(string)
    
    # 使用 next() 函数逐个遍历字符
    print(next(string_iter))  # 输出:H
    print(next(string_iter))  # 输出:e
    print(next(string_iter))  # 输出:l
    print(next(string_iter))  # 输出:l
    print(next(string_iter))  # 输出:o
    

    在上述示例中,我们使用 **iter()**函数将字符串转换为迭代器,并使用 **next()**函数逐个遍历其中的字符。

    zip()函数

    zip() 函数可以将多个可迭代对象按照索引位置进行压缩,返回一个元组组成的迭代器。这样我们就可以同时遍历多个可迭代对象。

    下面是一个使用 zip() 函数遍历多个列表的示例:

    names = ['Alice', 'Bob', 'Charlie']
    ages = [25, 30, 35]
    
    # 使用 zip() 函数同时遍历多个列表
    for name, age in zip(names, ages):
        print(f'{name} is {age} years old.')
    

    在上述示例中,我们使用 **zip()**函数将 namesages列表压缩成一个元组组成的迭代器,并使用 for循环同时遍历两个列表。

6、可迭代对象与迭代器的区别

  • 可迭代对象是一种可以被迭代的对象。迭代器是一种可以提供一个元素序列的对象。
  • 可迭代对象只需实现 iter 方法。迭代器还必须实现 next 方法。
  • 可迭代对象可以是任何类型,只要它实现了iter 方法。迭代器通常是特定类型的对象,例如列表迭代器或字符串迭代器。

7、数组和链表的区别

数组和链表是两种基础且常用的数据结构,它们在内存存储、访问效率及动态扩展等方面具有不同的特点。以下是具体介绍:

  1. 内存存储

    • 数组:数组在内存中占用连续的空间。这种连续的存储方式使得数组的随机访问非常高效,可以通过索引直接定位到任何元素。
    • 链表:链表的存储单元在内存中是非连续的。每个节点包含数据及指向下一个节点的指针,这使得链表在插入和删除操作时更为灵活,但访问元素时需要从头节点开始按顺序遍历。
  2. 访问效率

    • 数组:由于数组的元素是连续存储的,它支持快速的随机访问,时间复杂度为O(1)。这使得数组非常适合于查找和访问大量数据的场景。
    • 链表:链表的访问效率相对较低,因为需要逐个节点遍历来查找或访问元素,平均时间复杂度为O(n/2),在最坏情况下为O(n)。
  3. 动态扩展

    • 数组:在创建时需要预先定义大小,而且一旦定义后不易改变长度。如果需要扩展,必须创建一个新的更大的数组,并将旧数组的内容复制到新数组中。
    • 链表:链表采用动态内存分配,不需要一开始就定义大小。每个元素都是独立分配内存,因此可以灵活地增加或减少节点。
  4. 空间分配

    • 数组:从栈上分配内存,使用方便但自由度小。如果数组大小预估不准确,可能导致内存的浪费或不足。
    • 链表:从堆上分配内存,自由度大但需要注意内存泄漏。每个节点的内存是独立分配的,因此更加灵活。
  5. 应用场景

    • 数组:当数据规模固定且需要快速随机访问时,例如实现索引结构或有序数据集时,优先选择数组。
    • 链表:如果数据规模不断变化,如实现队列和栈等数据结构,或需要频繁进行元素的插入和删除操作,链表将是更合适的选择。

综上所述,数组更适合于数据规模固定且访问频繁的场景,而链表适合于数据规模不断变化且需要频繁更新数据的场景。理解这些差异有助于在选择数据结构时做出更合适的决策。希望上述对比能帮助您更好地理解这两种数据结构的差异和应用。

8、Python的垃圾回收机制

Python的垃圾回收机制主要依赖于引用计数和循环垃圾回收器。

  1. 引用计数:Python中的每个对象都有一个引用计数,用于记录有多少个变量或数据结构引用了该对象。当引用计数变为0时,表示该对象不再被使用,Python会自动回收其内存。
  2. 循环垃圾回收器:由于引用计数无法处理循环引用的情况,Python还引入了循环垃圾回收器来检测并回收循环引用的对象。循环垃圾回收器会定期检查内存中的对象,找出引用计数不为0但无法访问的对象,并将其回收。

在Python中,可以使用gc模块来控制垃圾回收器的运行。例如,可以使用gc.collect()手动触发垃圾回收,或者使用gc.disable()gc.enable()来关闭和开启自动垃圾回收。

9、在Docker中如何让两个容器网络通信

在Docker中,要实现两个容器之间的网络通信,可以通过创建自定义的网桥网络来连接它们。以下具体分析如何操作:

  1. 创建自定义网桥网络

    • 使用docker network create命令:可以通过这个命令创建一个bridge模式的网络。例如,执行docker network create --driver bridge my_net1会创建一个名为my_net1的网桥网络。
    • 指定子网和网关:如果需要,可以指定网络的子网和网关,如执行docker network create --driver bridge --subnet 172.20.0.0/24 --gateway 172.20.0.1 my_net2创建一个带有特定子网和网关的网络。
  2. 查看网络信息

    • 使用docker network ls命令:此命令可以列出所有创建的网络及其基本信息,帮助确认网络是否创建成功。
    • 使用docker network inspect命令:此命令可以查看网络的详细信息,包括IP地址段、网关等,例如执行docker network inspect my_net2
  3. 启动并连接到网络的容器

    • 使用docker run时指定网络:当运行容器时,可以通过-network参数指定新容器要加入的网络,例如docker run -it --name vm1 --network=my_net1 ubuntu将容器vm1加入到my_net1网络中。
    • 指定容器IP地址:如果需要为容器指定具体的IP地址,可以使用-ip参数,例如docker run -it --name vm2 --network=my_net2 --ip=172.20.0.10 ubuntu将容器vm2加入到my_net2网络并设置其IP地址为172.20.0.10。
  4. 容器间通信验证

    • 使用ping命令测试连通性:可以在一个容器内尝试ping另一个容器的IP地址以检查它们是否能够相互通信,例如在容器vm2内执行ping 172.19.0.2来测试与另一个容器vm1的连通性。
  5. 容器与外网通信

    • 了解容器访问外网机制:容器如何访问外网是通过iptables的SNAT实现的。
    • 配置端口映射和NAT:如果需要从外部访问容器内的服务,则可能需要配置DNAT和端口映射规则。

此外,在了解以上内容后,以下还有一些其他建议:

  • 网络驱动程序选择:Docker支持多种网络驱动程序,包括bridge、host、overlay等,每种驱动程序有其特定的用途和场景。
  • 网络安全考虑:在设置容器网络时,应考虑安全性因素,避免潜在的安全风险。
  • 跨主机容器通信:如果需要在多个宿主机上运行的容器之间进行通信,那么应该使用overlay网络驱动程序来创建跨主机的网络。

综上所述,通过创建自定义的网桥网络并正确地连接容器到这些网络,可以实现Docker中两个容器之间的网络通信。同时,需要注意合理地配置网络参数以确保容器间的顺畅通信,并考虑网络安全及跨主机通信的需求。

10、如何编写DockerFile

编写Dockerfile需要遵循一定的指令和格式,以下是具体的步骤和要点:

  1. 创建Dockerfile文件

    • 选择合适的文件名:Dockerfile文件通常命名为Dockerfile(首字母大写,无扩展名)。
    • 放置Dockerfile:将Dockerfile置于构建上下文(即docker build命令的路径)的根目录中。
  2. 编写Dockerfile指令

    • #开头的注释:在Dockerfile中,可以使用#符号来添加注释,有助于解释指令的作用。
    • 指定基础镜像:使用FROM指令指定基础镜像,例如FROM ubuntu:latest表示以Ubuntu的最新镜像作为起点。
  3. 配置工作目录

    • 使用WORKDIR指令:此指令设置容器的工作目录,例如WORKDIR /app将工作目录设置为/app
    • 后续指令相对路径:在WORKDIR之后指定的所有相对路径指令都会相对于这个工作目录。
  4. 安装软件包

    • 使用RUN指令:此指令用于执行命令行命令,如安装软件包,例如RUN apt-get update && apt-get install -y nginx
    • 减少层级:应尽量合并RUN指令以减少创建的中间层数,从而减小最终镜像的大小。
  5. 暴露端口

    • 使用EXPOSE指令:此指令用于指定容器将要监听的端口,例如EXPOSE 80
    • 注意与发布端口的区别EXPOSE只是声明容器内的服务将会使用某个端口,而实际对外公开则需在运行容器时通过p参数来做端口映射。
  6. 设置环境变量

    • 使用ENV指令:此指令用于设置环境变量,例如ENV APP_ENV=production
    • 影响后续指令:设置的环境变量会影响Dockerfile中后续指令的环境变量设置。
  7. 复制文件

    • 使用COPY指令:此指令用于将构建上下文的文件复制到容器中,例如COPY package.json /app/
    • 仅复制必要文件:应只复制必要的文件以减小最终镜像的大小。
  8. 设置默认命令

    • 使用CMD指令:此指令用于设置容器启动时默认执行的命令,例如CMD ["nginx", "-g", "daemon off;"]
    • ENTRYPOINT配合使用CMD指令可以与ENTRYPOINT配合使用以提供默认参数。
  9. 定义入口点

    • 使用ENTRYPOINT指令:此指令用于设置容器启动时要运行的程序,例如ENTRYPOINT ["python"]
    • CMD组合执行:如果同时指定了ENTRYPOINTCMD,则会合并执行,其中CMD的参数将作为参数传递给ENTRYPOINT指定的程序。
  10. 添加元数据

    • 使用LABEL指令:此指令用于给镜像添加元数据,例如LABEL version="1.0"
    • 为镜像和容器添加标签:元数据可用于给镜像和容器添加标签,以便于识别和管理。
  11. 编写多阶段构建

    • 多阶段构建概述:多阶段构建允许你使用多个FROM指令创建多个构建阶段,每个阶段产生一个中间镜像。
    • 减少无用文件:通过多阶段构建,你可以仅从最后一个阶段复制文件到最终镜像,从而减少无用文件和依赖,减小最终镜像的大小。
  12. 构建镜像

    • 使用docker build命令:通过此命令结合Dockerfile来构建Docker镜像,例如docker build -t myimage:latest .
    • 指定构建上下文docker build命令后面的.表示当前目录作为构建上下文。

此外,在了解以上内容后,以下还有一些其他建议:

  • 优化层级:尽量减少中间层的创建,合并可以合并的RUN指令,以减少镜像的体积。
  • 缓存利用:合理地安排Dockerfile中指令的顺序,充分利用Docker的缓存机制,加速构建过程。
  • 编写高效的Dockerfile:遵循最佳实践,编写高效且易于维护的Dockerfile

总的来说,编写Dockerfile需要仔细考虑每一行指令及其顺序,以确保构建过程既高效又可重复。通过遵循上述指南,您可以创建出高质量的Docker镜像,进而确保您的应用在容器化环境中顺利运行。

11、排序算法的时间复杂度

排序方法时间复杂度(平均)时间复杂度(最坏)时间复杂度(最好)空间复杂度稳定性
插入排序O(n^2)O(n^2)O(n)O(1)稳定
希尔排序O(n^1.3)O(n^2)O(n)O(1)不稳定
选择排序O(n^2)O(n^2)O(n^2)O(1)不稳定
堆排序O(nlog base 2 n)O(nlog base 2 n)O(nlog base 2 n)O(1)不稳定
冒泡排序O(n^2)O(n^2)O(n)O(1)稳定
快速排序O(nlog base 2 n)O(n^2)O(nlog base 2 n)O(nlog base 2 n)不稳定
归并排序O(nlog base 2 n)O(nlog base 2 n)O(nlog base 2 n)O(n)稳定
计数排序O(n+k)O(n+k)O(n+k)O(n+k)稳定
桶排序O(n+k)O(n^2)O(n)O(n+k)稳定
基数排序O(n*k)O(nk)O(nk)O(n+k)稳定

12、如何存储大量的媒体资源

借助阿里云oss服务存储数据,并结合cdn实现访问加速。

彩蛋在此

阅读至此,你可能正在探索新的职业机会。如果你感到任何焦虑或担忧,请放心,我能够自信地告诉你,你一定能成功找到属于你的职位!请不要被那些充满负能量的言论所影响,诸如“互联网已死”或“行业寒冬”等论调,这些往往只是为吸引眼球而制造的无稽之谈。相信自己的价值,你是独一无二的完美人选!

正如我在一次辩论赛中深有感触的一句话:“当你从现在展望未来,看似有无数可能的道路;但从未来回望,你会发现始终只有一条路是注定的。因此,不必迷茫,一切早已安排妥当。你需要做的,仅仅是享受当下的每一刻。”