后端开发面经系列--百度内容生态C++一面

百度内容生态C++一面

公众号:阿Q技术站

来源:https://www.nowcoder.com/discuss/456910795583086592

1、用户输入网址到显示对应页面的全过程?

  1. DNS解析:

    • 浏览器首先会检查网址中的域名部分(例如,www.xxxxx.com)。

    • 浏览器会向本地DNS缓存查询,如果之前已经解析过这个域名,就可以跳过后续步骤。

    • 如果没有找到缓存,浏览器会向操作系统的DNS解析器发送请求,请求解析该域名的IP地址。

    • DNS解析器可能会向根域名服务器、顶级域名服务器、权威域名服务器等层级进行查询,直到找到该域名对应的IP地址。

    • 一旦获取到IP地址,浏览器就知道要连接的服务器位置。

  2. 建立TCP连接:

    • 浏览器使用获取到的IP地址和HTTP默认端口(通常是80,或者443用于HTTPS)来建立与Web服务器的TCP连接。

    • 这个过程包括TCP的三次握手,确保浏览器和服务器之间的可靠通信。

  3. 发送HTTP请求:

    • 浏览器向服务器发送一个HTTP请求,该请求包括请求的资源、请求方法(GET、POST等)、HTTP协议版本以及其他一些头部信息。

    • 服务器根据请求的资源路径和方法来响应请求,通常会检查请求头中的一些信息,如Cookie、User-Agent等。

  4. 服务器处理请求:

    • 服务器根据HTTP请求的内容来执行相应的处理,这可能包括查询数据库、调用应用程序、生成动态内容等。

    • 服务器生成HTTP响应,包括响应状态码、响应头和响应正文。

  5. 接收响应:

    • 浏览器接收到来自服务器的HTTP响应。

    • 如果响应状态码指示成功(例如,200 OK),浏览器将继续处理响应。

  6. 渲染页面:

    • 浏览器根据响应中的HTML、CSS和JavaScript开始渲染页面。

    • 浏览器解析HTML文档,构建DOM(文档对象模型)树。

    • 浏览器解析CSS,构建CSSOM(CSS对象模型)树。

    • 浏览器将DOM和CSSOM合并成一个渲染树,用于确定页面上各个元素的布局和样式。

    • 浏览器执行JavaScript,可以修改DOM和CSSOM,以及处理用户交互。

    • 浏览器根据渲染树绘制页面。

  7. 显示页面:

    • 浏览器将渲染页面的结果显示在用户的屏幕上。

    • 这包括文本、图像、多媒体元素等。

    • 浏览器还处理用户的交互事件,如鼠标点击和键盘输入。

  8. 持续通信:

​ 如果页面包含与服务器的WebSocket或长轮询等通信,浏览器将保持与服务器的连接,以接收实时数据。

2、DNS 的解析过程涉及到了哪两种查询?

  1. 递归查询(Recursive Query):客户端向本地 DNS 服务器发起递归查询。本地 DNS 服务器在自己的缓存中查找域名与 IP 地址的映射,如果找到,则直接返回给客户端。如果本地 DNS 服务器缓存中没有找到,它将向根 DNS 服务器发起递归查询。
  2. 迭代查询(Iterative Query):根 DNS 服务器收到递归查询后,它并不直接返回所需的 IP 地址,而是向本地 DNS 服务器提供对应顶级域(TLD)的 IP 地址,让本地 DNS 服务器继续查询。本地 DNS 服务器继续向 TLD 服务器发起迭代查询,获取下一级域的 IP 地址。这个过程一直迭代下去,直到本地 DNS 服务器最终获取到目标域名对应的 IP 地址。

3、递归查询和迭代查询的区别?

  1. 递归查询(Recursive Query):
    • 发起者:通常由客户端(如用户的计算机或设备)发起递归查询。
    • 过程:客户端向本地 DNS 服务器提出一个完整的查询请求,请求包含了要解析的域名。本地 DNS 服务器负责从根 DNS 服务器开始,依次向下进行查询,获取域名对应的 IP 地址。中间的 DNS 服务器协助完成查询过程,直到本地 DNS 服务器最终获取到目标域名对应的 IP 地址,然后将结果返回给客户端。
    • 责任:本地 DNS 服务器负责整个查询过程,一直到获取到最终结果。
  2. 迭代查询(Iterative Query):
    • 发起者:通常由 DNS 服务器之间相互发起迭代查询。
    • 过程:当一个 DNS 服务器收到一个查询请求时,它可能无法立即提供完整的答案。相反,它会向发起查询的 DNS 服务器提供指向下一级 DNS 服务器的信息,让发起查询的 DNS 服务器继续查询。这个过程一直迭代下去,直到最终的答案被找到。
    • 责任:每个 DNS 服务器只负责提供下一级 DNS 服务器的信息,而不负责最终结果的获取。整个查询过程需要多个 DNS 服务器协作完成。

4、DNS域名解析服务使用的默认端口号是多少?

DNS域名解析服务使用的默认端口号是53。 DNS的标准端口号为53/UDP和53/TCP。UDP通常用于一般的DNS查询,而TCP通常用于大型数据传输或特殊情况下的DNS查询。

5、MySql 和Redis的区别有哪些?

  1. 数据库类型:
    • MySQL是一种关系型数据库管理系统(RDBMS),它使用结构化查询语言(SQL)进行数据管理。
    • Redis是一种基于内存的键值对存储系统,属于NoSQL数据库,主要用于缓存和提供快速的数据访问。
  2. 数据存储模型:
    • MySQL采用表格形式的存储,支持复杂的关联查询,适用于需要保持数据一致性和结构化数据的场景。
    • Redis是一个键值对存储系统,数据存储在内存中,适用于对速度要求较高,而对一致性要求较低的场景。
  3. 数据持久性:
    • MySQL通常使用磁盘存储数据,可以持久保存数据。
    • Redis默认将数据存储在内存中,但可以通过持久化机制将数据写入磁盘,支持不同的持久化方式,如RDB快照和AOF日志。
  4. 数据查询语言:
    • MySQL使用SQL进行数据查询,支持复杂的查询、事务和关联操作。
    • Redis提供有限的查询操作,主要包括对键的增、删、改、查以及一些特定的数据结构的操作。
  5. 应用场景:
    • MySQL适用于需要保持数据结构完整性,支持复杂查询和事务的应用场景,例如电商平台、社交网络等。
    • Redis适用于需要快速读写,对一致性要求较低,以及需要缓存大量数据的应用场景,例如缓存、实时排行榜、会话存储等。
  6. 数据模型:
    • MySQL支持多种数据模型,包括表格、行存储、列存储等。
    • Redis主要以键值对形式存储数据,同时支持多种数据结构,如字符串、哈希表、列表、集合等。

6、如何理解关系型数据库和非关系型数据库?

  1. 数据存储模型:
    • 关系型数据库使用表格的形式来组织数据,表格中有固定的列和行,每列定义了特定的数据类型。数据之间的关系通过外键等方式建立连接。
    • 非关系型数据库则没有预定义的模式,数据可以以键值对、文档、列族等不同形式存储。它们更加灵活,不要求固定的表结构。
  2. 数据一致性:
    • 关系型数据库强调事务的一致性,支持复杂的事务操作,确保数据的完整性。
    • 非关系型数据库在一些场景下对一致性的要求较低,可能会牺牲一致性以获得更好的性能。
  3. 扩展性:
    • 关系型数据库通常采用垂直扩展,即通过增加硬件的性能来提高数据库的处理能力。
    • 非关系型数据库更注重横向扩展,即通过增加服务器节点来提高处理能力,适合处理大规模的分布式数据。
  4. 查询语言:
    • 关系型数据库使用结构化查询语言(SQL)进行数据查询,支持复杂的关联查询、聚合函数等。
    • 非关系型数据库使用各种不同的查询语言或者API,具体取决于数据库的类型,通常不支持复杂的SQL查询。
  5. 应用场景:
    • 关系型数据库适用于需要保持数据一致性、支持复杂查询、事务处理的应用,如金融系统、企业资源计划(ERP)等。
    • 非关系型数据库适用于需要处理大量数据、对数据结构要求不确定、需要横向扩展的应用,如大数据处理、实时数据分析、缓存等。

7、Redis数据类型数据类型有哪些?

  1. 字符串(String):
    • 存储字符串、整数、浮点数等。
    • 常用命令:SETGETINCRDECR等。
  2. 哈希(Hash):
    • 存储字段和与其关联的值,类似于关联数组。
    • 常用命令:HSETHGETHDELHGETALL等。
  3. 列表(List):
    • 存储有序的字符串元素,支持在两端进行插入和删除操作。
    • 常用命令:LPUSHRPUSHLPOPRPOP等。
  4. 集合(Set):
    • 存储无序且唯一的字符串元素。
    • 常用命令:SADDSREMSMEMBERSSINTER等。
  5. 有序集合(Sorted Set):
    • 类似于集合,但每个元素关联一个分数,用于排序。
    • 常用命令:ZADDZREMZRANGEZSCORE等。
  6. 位图(Bitmap):
    • 存储二进制位,可以进行位运算。
    • 常用命令:SETBITGETBITBITCOUNTBITOP等。
  7. 超级日志结构(HyperLogLog):
    • 用于进行基数估计(即估计集合中不重复元素的数量)。
    • 常用命令:PFADDPFCOUNTPFMERGE等。
  8. 地理空间(Geospatial):
    • 存储地理空间信息,支持对坐标进行查询。
    • 常用命令:GEOADDGEODISTGEORADIUSGEOHASH等。

8、Hash数据结构的底层实现原理?

在Redis中,Hash数据结构的底层实现采用了一种称为哈希表(hash table)的数据结构。具体来说,Redis中的哈希表是一个数组,数组的每个元素都是一个链表的头指针,而链表的节点包含了哈希表中的键值对。

  1. 数组桶:哈希表由一个数组构成,每个数组元素称为桶(bucket)。每个桶存储一个链表,用于解决哈希冲突。
  2. 哈希函数:为了将键映射到数组索引,需要使用哈希函数。哈希函数将键转换为数组中的一个索引值,使得键均匀地分布在数组中。
  3. 哈希冲突:由于键的数量可能远远大于数组的大小,哈希冲突是不可避免的。当两个键被哈希到数组的同一位置时,就会发生冲突。为了解决冲突,可以使用链表将相同位置上的键值对连接起来。
  4. 拉链法解决冲突:Redis采用了一种称为拉链法(Separate Chaining)的方法来解决冲突。在拉链法中,每个数组元素(桶)都是一个链表的头指针。当发生冲突时,新的键值对会被插入到链表中。
  5. 负载因子和重新哈希:为了保持哈希表的效率,需要控制负载因子,即键值对数量与数组大小的比率。当负载因子过高时,可以通过重新哈希(Rehashing)来扩大数组的大小,减小冲突的概率。
  6. 渐进式重新哈希:Redis使用一种渐进式重新哈希的方式,避免在一次操作中重新哈希整个表。它在后台逐步地将旧表中的数据迁移到新表中,直到完成整个过程。

9、Hash数据结构常见的应用场景有哪些?

  1. 存储对象属性:哈希表适用于存储对象的属性。例如,可以使用一个哈希表表示用户对象,其中每个键值对对应一个用户属性,如用户名、年龄、电子邮件等。
  2. 计数器:哈希表可以用于实现计数器,其中键是计数对象,值是计数值。例如,可以使用哈希表跟踪网站上不同文章的阅读次数。
  3. 缓存:哈希表可用于实现缓存。每个键值对表示一个缓存条目,键是缓存的键,值是相应的缓存数据。这样可以快速检索和更新缓存。
  4. 存储用户信息:哈希表是存储用户信息的理想结构。每个用户可以表示为哈希表,其中包含有关用户的各种信息,如个人资料、设置等。
  5. 配置管理:哈希表可用于存储配置信息。键可以是配置项的名称,而值是相应的配置值。这样可以方便地检索和更新配置。
  6. 实现图数据结构:通过将节点和边映射到哈希表中的键值对,可以使用哈希表实现图数据结构。
  7. 实现分布式存储:在分布式系统中,可以使用哈希表来分片数据,使得数据可以分布在多个节点上,同时保持一定的均匀性。

10、不断往哈希表中写数据会发生什么问题?

  1. 内存占用增加:每次写入数据都会占用一定的内存。如果不断写入大量数据,可能导致内存使用量不断增加,最终达到系统的内存限制。
  2. 哈希冲突:哈希表的实现通常会使用哈希函数将键映射到表中的索引位置。如果多个键映射到相同的索引位置,就会发生哈希冲突。冲突可能导致性能下降,需要额外的处理方式,例如开放寻址法或链表法。
  3. 性能下降:随着哈希表中数据的增加,哈希冲突可能导致查找、插入和删除等操作的性能下降。解决冲突的方法和哈希表的装载因子(表中已用位置和总位置的比值)都会影响性能。
  4. 重新哈希:为了应对数据量的增加,哈希表可能需要进行重新哈希操作。这涉及创建一个新的更大的哈希表,并将现有数据重新散列到新表中。这样的操作可能导致一段时间内的性能下降。
  5. 频繁扩容:如果哈希表的设计支持动态扩容,当表中的数据达到一定阈值时,会进行扩容操作。频繁的扩容可能会影响性能。

11、讲一下Redis渐进式rehash过程?

Redis 的渐进式 rehash 是在进行哈希表扩容时采用的一种策略,它允许哈希表在进行扩容的同时仍然可以进行正常的读写操作,而不会阻塞整个 Redis 服务。

渐进式 rehash 过程:

  1. 为新哈希表分配空间: 当哈希表需要扩容时,Redis 会创建一个新的更大的哈希表,通常将当前哈希表的大小翻倍。
  2. 将新哈希表设置为主哈希表: 将新创建的哈希表设置为主哈希表,并将服务器的 rehashidx 属性设为 0,表示 rehash 过程从索引为 0 的哈希表节点开始。
  3. 在后台逐步迁移数据: Redis 在后台以异步的方式逐步将旧哈希表中的数据迁移到新哈希表。每次迁移一小部分数据,避免对整个数据集进行一次性的复制。
  4. 逐步更新 rehashidx: 每次迁移完成后,服务器会逐步增加 rehashidx 的值,表示下次从旧哈希表的下一个索引位置开始继续迁移。
  5. 渐进式 rehash 完成: 当 rehashidx 的值增加到哈希表的大小时,表示整个数据集已经迁移完成,新哈希表取代了旧哈希表,rehash 过程完成。

优点:

  • 非阻塞: 渐进式 rehash 过程不会阻塞对哈希表的读写操作,使得 Redis 在扩容过程中依然能够提供服务。
  • 逐步迁移: 数据迁移是逐步进行的,每次只迁移一小部分数据,避免了一次性大规模的数据复制。

12、Redis渐进式rehash过程中,每处理⼀个请求时,从哈希表1中的第 ⼀个索引位置开始,顺带着将这个索引位置上的所有entries拷贝到哈希表2中,这个请求是读请求还是写请求?

在 Redis 的渐进式 rehash 过程中,每处理一个请求时,如果这个请求涉及到哈希表的访问,那么 Redis 会根据当前的 rehash 进度,选择从旧哈希表(哈希表1)或新哈希表(哈希表2)中进行读取或写入操作。

读请求:

  • 如果请求的 key 在旧哈希表中,那么 Redis 会从旧哈希表中读取数据。
  • 如果请求的 key 在新哈希表中,那么 Redis 会从新哈希表中读取数据。
  • 如果 key 同时存在于旧哈希表和新哈希表中,Redis 会优先从旧哈希表中读取。

写请求:

  • 如果请求的 key 在旧哈希表中,写操作会在旧哈希表上进行,不涉及新哈希表。
  • 如果请求的 key 在新哈希表中,写操作会在新哈希表上进行。

因此,读请求和写请求都可能涉及旧哈希表和新哈希表,取决于 key 的位置。在 rehash 过程中,Redis 会通过 rehashidx 属性控制迁移的进度,逐步迁移数据,保证在渐进式 rehash 过程中对数据的读写操作。

13、讲一下MySQL的索引?

MySQL 的索引是一种数据结构,用于提高数据库的查询效率。索引可以加速数据的查找,类似于书的目录,通过查找目录可以快速找到书中的内容。MySQL 支持多种类型的索引,常见的有:

  1. B-Tree 索引:B-Tree(平衡树)是 MySQL 默认的索引类型。B-Tree 索引适用于所有数据类型,通过 B-Tree 索引,MySQL 能够快速定位到符合条件的记录。
  2. 哈希索引:哈希索引主要用于等值查询,例如 = 操作符。哈希索引在等值查询时非常快,但在范围查询和排序方面性能较差。InnoDB 存储引擎的默认索引类型不支持哈希索引,但内存表 MEMORY 存储引擎支持哈希索引。
  3. 全文索引:用于全文搜索的场景,主要用于 MATCH…AGAINST… 的查询。全文索引在 MyISAM 存储引擎上比较常见。
  4. 空间索引:用于 GIS 空间数据类型的索引。
  5. 前缀索引:只索引列值的前缀,可以节省存储空间,但查询时效率可能降低。
  6. 复合索引:将多个列组合在一起创建的索引,可以用于多列的查询。

索引的作用:

  • 提高数据检索速度。
  • 加速表的连接。
  • 在唯一性检查中起到约束作用。

但索引也有一些缺点:

  • 占用磁盘空间。
  • 在写入操作时,需要更新索引,可能导致写操作的性能下降。
  • 不适合所有查询,某些情况下可能导致性能下降。

14、讲一下聚簇索引和非聚簇索引的区别?

  1. 聚簇索引:
    • 聚簇索引决定了表中数据的物理排序方式,即表中的行的物理存储顺序与索引的顺序一致。
    • 一个表只能有一个聚簇索引,通常是主键索引,因为主键是唯一的且不为空,适合用于组织表中的数据。
    • 聚簇索引的叶子节点包含实际的数据行,因此聚簇索引不仅可以加速查找,还可以提高范围查询的性能。
    • InnoDB 存储引擎的表默认使用聚簇索引。
  2. 非聚簇索引:
    • 非聚簇索引的叶子节点并不包含实际的数据行,而是包含指向实际数据行的指针。
    • 一个表可以有多个非聚簇索引,包括唯一索引、普通索引等。
    • 非聚簇索引的数据存储在一个地方,而实际的数据存储在另一个地方,因此在使用非聚簇索引进行查询时,需要先通过索引找到行的指针,然后再到实际的数据位置读取数据。
    • 虽然非聚簇索引对于范围查询的性能可能不如聚簇索引好,但由于不改变实际数据的物理存储顺序,插入和更新操作的性能相对更高。

15、了解Hash索引吗?Hash索引的优点和缺点?

Hash索引是一种基于哈希表的索引结构,它使用哈希函数将索引列的值映射为哈希码,然后将哈希码作为索引进行查找。在数据库系统中,Hash索引主要用于等值查询,即根据索引列的值快速定位记录。

Hash索引的优点:

  • 快速的等值查询:Hash索引在进行等值查询时非常高效,因为哈希函数可以直接将查找的值映射为一个唯一的哈希码,从而快速定位记录。
  • 常数时间的查询复杂度:在理想情况下,哈希索引的查询复杂度是常数时间,与数据规模无关。

Hash索引的缺点:

  • 不适用范围查询:Hash索引不支持范围查询,因为哈希函数无法将一段连续的值映射为连续的哈希码。
  • 不适用于排序:Hash索引无法提供有序的结果,因为相邻的键值在哈希表中不一定对应相邻的位置。
  • 冲突处理:不同的键值可能映射到相同的哈希码,这就是哈希冲突。为了解决冲突,需要使用开放寻址法、链地址法等技术,这会增加索引的复杂度。
  • 不适用于部分匹配:无法高效地处理部分匹配查询,因为哈希函数将相似的键值映射为不同的哈希码。
  • 对内存要求高:Hash索引通常需要在内存中完整存储哈希表,对内存的要求相对较高。

16、非聚簇索引一定会回表查询吗?

在MySQL中,非聚簇索引并不一定会导致回表查询,这取决于覆盖索引(Covering Index)的使用。

覆盖索引: 当一个查询的SELECT列和WHERE条件都包含在索引中时,就可以称之为覆盖索引。在这种情况下,MySQL可以直接使用索引来满足查询的需求,而无需回表查询实际的数据行。

回表查询: 当查询需要使用到的数据列不在索引中,MySQL需要通过索引找到对应的主键值,然后再通过主键值回表查询获取实际的数据行。

给个例子来说明一下:

假设有一个表 mytable,有三列 id(主键)、nameage,并在 name 列上建立了非聚簇索引。如果有以下查询:

SELECT id, name FROM mytable WHERE name = 'John';

如果在 name 列上建立了非聚簇索引,而且索引中包含了 idname 列,那么这个查询就可以被称为覆盖索引,因为索引本身包含了查询所需的所有信息。

在这种情况下,MySQL可以直接使用索引来满足查询,而无需回表查询实际的数据行。如果索引中不包含足够的信息,MySQL可能需要进行回表查询。

17、创建索引的设计原则有哪些?

  1. 选择适当的列: 索引的目的是提高查询性能,因此选择对查询频繁、条件经常出现的列进行索引。同时,需要考虑到索引的存储和维护成本,不宜对过多的列创建索引。
  2. 唯一性: 对于经常用于唯一性检查的列,比如主键、唯一约束的列,应该创建唯一索引。
  3. 覆盖索引: 如果查询语句中的列都在索引中,就可以使用覆盖索引,避免回表查询,提高查询性能。
  4. 区分度高: 索引的选择应具有足够的区分度,以便提高检索效率。如果索引的区分度低,可能导致大量的行被检索,降低性能。
  5. 长度和前缀索引: 对于字符串类型的列,可以考虑创建长度较短或者前缀索引,以减少索引的大小,提高查询性能。
  6. 避免过度索引: 不要对表的每一列都创建索引。过多的索引不仅增加了存储空间,还会降低写操作的性能,因为每次写操作都需要更新索引。
  7. 不要滥用索引提示: 在大多数情况下,数据库系统能够根据查询语句的情况选择合适的索引。滥用索引提示可能导致不必要的性能问题。
  8. 定期维护: 定期对索引进行维护,包括重新构建、重新组织索引,以保证其效率。
  9. 考虑存储引擎: 不同的存储引擎对索引的支持和实现方式可能有所不同,选择合适的存储引擎也是索引设计的考虑因素。

18、讲一下最左匹配原则?

最左匹配原则是指在多列索引中,数据库引擎会尽量使用索引的最左边的列,但不一定使用索引的所有列。这个原则主要体现在查询条件中,如果查询条件使用了索引的最左边的列,那么索引就能够发挥作用。

例如,考虑一个复合索引 (A, B, C),如果查询条件中包含了列 A,那么索引就能够被用上。如果查询条件中包含了列 AB,那么同样可以使用这个索引。但如果查询条件中只包含了列 B 或者列 C,而不包含列 A,那么这个索引就不会被使用。

这个原则的背后是因为 B-Tree 索引的结构。B-Tree 是一种树状结构,查询时从树的根节点开始匹配最左边的列,然后依次匹配下一层的节点。因此,只有当查询条件中包含索引最左边的列时,整个索引才能够被有效利用。

最左匹配原则的应用使得设计索引时需要考虑查询条件的顺序,将最常用于查询的列放在索引的最左边,以提高查询性能。

19、什么是线程池?线程池有什么好处?

线程池是一种多线程处理的机制,它包含有限数量的线程,用于执行提交的任务。线程池的主要目的是为了线程的重用,管理和控制线程的数量,以提高程序的性能、降低系统开销,以及更好地管理系统资源。

线程池的主要好处包括:

  1. 降低线程创建和销毁的开销: 线程的创建和销毁是比较昂贵的操作,线程池可以重用已经创建的线程,避免不断地创建和销毁线程,减少资源开销。
  2. 提高程序响应速度: 线程池中的线程可以立即响应任务的到来,而不需要等待新线程的创建,因此能够更快速地执行任务。
  3. 更好的资源管理: 线程池可以限制并发线程的数量,防止系统因过多线程而耗尽资源,提高系统的稳定性。
  4. 任务队列: 线程池通常会使用一个任务队列来存储等待执行的任务,这样可以平滑地处理任务的到来,防止任务过多导致系统负载过大。
  5. 可控的并发度: 可以通过设置线程池的大小来控制并发度,使得系统在不同负载下都能够有一个合适的性能表现。

20、线程池有哪些核心参数?

  1. 核心线程数(Core Pool Size):线程池中能够同时执行任务的基本线程数量。即使线程处于空闲状态,核心线程也不会被销毁。
  2. 最大线程数(Maximum Pool Size):线程池中允许的最大线程数量,包括核心线程数。当任务队列满了,并且仍然有任务提交时,线程池会创建新的线程,但数量不会超过最大线程数。
  3. 任务队列(Work Queue):用于存储等待执行的任务的队列。当线程池中的线程数达到核心线程数时,新提交的任务会被放入任务队列。
  4. 保持存活时间(Keep Alive Time):当线程池中的线程数超过核心线程数时,多余的空闲线程会在经过一定时间(保持存活时间)后被销毁,直到线程池中的线程数不超过核心线程数。
  5. 拒绝策略(Rejected Execution Handler):当任务无法被提交给线程池执行时采取的策略。常见的策略包括抛出异常、丢弃任务、丢弃队列中最旧的任务等。

21、解决hash冲突的办法有哪些?

  1. 链地址法(Separate Chaining):将哈希表的每个槽都连接一个链表,当多个关键字映射到同一个槽时,它们会以链表的形式存储在这个槽中。链表中的每个节点包含具有相同哈希值的关键字。
  2. 开放地址法(Open Addressing):当发生冲突时,通过探测(probing)在哈希表中寻找下一个可用的槽。探测的方法包括线性探测、二次探测、双重散列等。线性探测就是顺序地检查每个槽,直到找到一个空槽。
  3. 再哈希法(Rehashing):当发生冲突时,通过应用另一个哈希函数来计算一个新的槽。这个方法可能需要多次尝试,直到找到一个空槽。
  4. 建立公共溢出区(Overflow Area):为哈希表提供一个额外的区域,用于存储发生冲突的关键字。这样,即使发生冲突,关键字仍然可以被成功插入到哈希表中。

22、hash算法有哪些应用场景?

  1. 散列存储结构:在数据结构中,哈希表是一种基于哈希算法的散列存储结构,用于快速检索、插入和删除数据。
  2. 数据校验与完整性:哈希算法常用于验证数据的完整性,例如通过计算数据的哈希值并与预期的哈希值比对,来确保数据在传输或存储过程中没有被篡改。
  3. 密码学:在密码学中,哈希函数被广泛用于生成消息摘要,例如在存储用户密码时,通常会存储密码的哈希值而不是明文密码。
  4. 负载均衡:在负载均衡算法中,哈希算法可以用于将请求映射到一组服务器中的某一个,以实现更均匀的负载分配。
  5. 分布式存储:在分布式存储系统中,哈希算法用于确定数据在分布式存储节点中的位置,以便快速定位和检索数据。
  6. 唯一标识和索引:哈希算法可用于生成唯一的标识符或索引,例如在数据库索引中,通过对关键字进行哈希计算,可以加速检索过程。
  7. 防止冲突和重复:在各种算法和应用中,哈希算法用于防止冲突和重复,确保不同数据映射到不同的位置,提高数据处理的效率。

23、LeetCode 206. 反转链表(完整输出与输出)

思路:
方法一:迭代法

初始化两个指针,prev用于记录前一个节点,current用于遍历链表。然后,我们使用一个循环来逐一反转链表中的节点,将当前节点的next指针指向前一个节点,同时更新prevcurrent指针。最终,prev指向的就是新链表的头节点。

参考代码:

#include <iostream>

struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

ListNode* reverseList(ListNode* head) {
    ListNode* prev = nullptr; // 初始化前一个节点为nullptr
    ListNode* current = head; // 初始化当前节点为链表头节点

    while (current != nullptr) {
        ListNode* nextNode = current->next; // 保存下一个节点
        current->next = prev; // 反转当前节点的next指针,指向前一个节点
        prev = current; // 更新prev指针
        current = nextNode; // 更新current指针
    }

    return prev; // prev指向新链表的头节点
}

int main() {
    ListNode* pHead = new ListNode(1); // 创建头节点
    pHead->next = new ListNode(2); // 添加节点2
    pHead->next->next = new ListNode(3); // 添加节点3

    std::cout << "原始链表:";
    ListNode* current = pHead;
    while (current != nullptr) {
        std::cout << current->val << " ";
        current = current->next;
    }
    std::cout << std::endl;

    // 调用反转函数
    ListNode* newHead = reverseList(pHead);

    std::cout << "反转后的链表:";
    current = newHead;
    while (current != nullptr) {
        std::cout << current->val << " ";
        current = current->next;
    }
    std::cout << std::endl;

    return 0;
}
方法二:递归法

利用递归函数,从链表的尾节点开始不断反转指针。

参考代码:

#include <iostream>

struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

ListNode* reverseList(ListNode* head) {
    if (head == nullptr || head->next == nullptr) {
        return head; // 如果链表为空或只有一个节点,直接返回链表头
    }

    // 递归反转剩余部分
    ListNode* newHead = reverseList(head->next);

    // 更新当前节点的next指针
    head->next->next = head;
    head->next = nullptr;

    return newHead; // 返回新链表的头节点
}

int main() {
    ListNode* pHead = new ListNode(1); // 创建头节点
    pHead->next = new ListNode(2); // 添加节点2
    pHead->next->next = new ListNode(3); // 添加节点3

    std::cout << "原始链表:";
    ListNode* current = pHead;
    while (current != nullptr) {
        std::cout << current->val << " ";
        current = current->next;
    }
    std::cout << std::endl;

    // 调用反转函数
    ListNode* newHead = reverseList(pHead);

    std::cout << "反转后的链表:";
    current = newHead;
    while (current != nullptr) {
        std::cout << current->val << " ";
        current = current->next;
    }
    std::cout << std::endl;

    return 0;
}

24、LeetCode 114. 二叉树展开为链表 (完整输出与输出)

非递归思路:
  1. 使用栈来模拟先序遍历的过程,先将根结点入栈。
  2. 弹出栈顶结点,如果该结点的右子树不为空,则将右子树结点入栈。
  3. 如果该结点的左子树不为空,则将左子树结点入栈。
  4. 将当前结点的左子树设为null,右子树设为栈顶结点。
  5. 重复步骤2到步骤4,直到栈为空。
参考代码:
#include <iostream>
#include <stack>

using namespace std;

// 定义二叉树结构体
struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

class Solution {
public:
    void flatten(TreeNode* root) {
        if (!root) return;
    
        stack<TreeNode*> nodeStack;
        nodeStack.push(root);
    
        while (!nodeStack.empty()) {
            // 弹出栈顶结点
            TreeNode* current = nodeStack.top();
            nodeStack.pop();
    
            // 如果右子树不为空,将右子树结点入栈
            if (current->right) {
                nodeStack.push(current->right);
            }
            // 如果左子树不为空,将左子树结点入栈
            if (current->left) {
                nodeStack.push(current->left);
            }
    
            // 将当前结点的右子树设为栈顶结点
            if (!nodeStack.empty()) {
                current->right = nodeStack.top();
            }
    
            // 左子树设为null
            current->left = NULL;
        }
    }
};

// 辅助函数,用于先序遍历输出单链表
void printList(TreeNode* head) {
    while (head) {
        cout << head->val << " ";
        head = head->right;
    }
    cout << endl;
}

int main() {
    // 创建二叉树
    TreeNode* root = new TreeNode(1);
    root->left = new TreeNode(2);
    root->right = new TreeNode(5);
    root->left->left = new TreeNode(3);
    root->left->right = new TreeNode(4);
    root->right->right = new TreeNode(6);

    // 创建解决方案对象
    Solution solution;

    // 调用展开函数
    solution.flatten(root);

    // 输出展开后的单链表
    printList(root);

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值