地平线C++一面
公众号:阿Q技术站
来源:https://www.nowcoder.com/discuss/608452700895711232
1、分布式事务是否了解?
分布式事务是指涉及多个数据库或应用之间的事务操作,需要确保这些操作要么全部成功,要么全部失败,以保持数据的一致性。在分布式系统中,由于数据分布在不同的节点上,因此需要特殊的处理来保证事务的 ACID 特性(原子性、一致性、隔离性和持久性)。
分布式事务的实现通常基于两阶段提交(Two-Phase Commit, 2PC)协议或者三阶段提交(Three-Phase Commit, 3PC)协议。这些协议确保了在分布式环境下的事务的一致性和可靠性。
- 两阶段提交(2PC):在两阶段提交中,涉及的节点分为协调者和参与者。第一阶段,协调者向所有参与者发送事务准备请求,并等待参与者的响应。如果所有参与者都成功响应,协调者发出 commit 请求,否则发出 abort 请求。第二阶段,参与者根据协调者的请求执行事务的提交或者中止操作。
- 三阶段提交(3PC):为了解决两阶段提交可能出现的一些问题(如协调者故障导致的阻塞),引入了三阶段提交协议。在三阶段提交中,增加了一个准备阶段,在这个阶段,协调者询问参与者是否可以提交事务,但不进行最终提交。这样即使在第二阶段出现故障,也可以通过超时机制来处理。
2、Redis 是否支持事务?
Redis 支持事务。Redis 的事务是通过 MULTI 和 EXEC 命令来实现的,使用 MULTI 命令可以开启一个事务块,然后在事务块内执行多个命令,最后通过 EXEC 命令来执行事务块内的所有命令。
在事务块中执行的命令并不会立即执行,而是被放入一个队列中。只有在执行 EXEC 命令时,Redis 才会将队列中的命令依次执行。如果在执行 EXEC 命令之前发生了错误,比如语法错误或者命令错误,Redis 会取消事务,并且不会执行任何命令。
Redis 事务的特点包括:
- 原子性:事务中的所有命令要么全部执行成功,要么全部失败,Redis 保证事务的原子性。
- 隔离性:Redis 事务是隔离的,即事务执行过程中对其他客户端是不可见的。
- 一致性:事务在执行过程中,不会破坏 Redis 中的数据一致性。
- 持久性:事务执行成功后,会按照配置进行持久化,保证数据的持久性。
3、Redis 的隔离性,让简单介绍一下几种隔离级别?
- 读未提交(Read Uncommitted):允许事务读取其他事务未提交的数据。在 Redis 中,默认情况下是不支持这种级别的,因为 Redis 的事务是原子性的,不会读取到未提交的数据。
- 读提交(Read Committed):事务只能读取到已经提交的数据。在 Redis 中,即使在事务执行过程中其他客户端对数据进行了修改,事务也只会读取到提交前的数据。
- 可重复读(Repeatable Read):事务执行过程中,不会读取到其他事务提交的数据。在 Redis 中,可以通过 WATCH 命令来实现类似的效果,可以监视一个或多个键,在事务执行过程中如果被监视的键被修改,事务就会被打断。
- 串行化(Serializable):事务串行执行,确保每个事务都完全独立运行,不会出现并发读取或写入数据的情况。在 Redis 中,可以通过单线程模式来实现类似的效果,即 Redis 默认是单线程处理命令的,可以避免并发问题。
4、Redis 的命令模型执行过程是否了解?
- 客户端发送命令:客户端通过网络连接发送命令给 Redis 服务器。命令的格式通常是一个字符串,包括命令名称和参数。
- 命令解析:Redis 服务器接收到命令后,会对命令进行解析,将命令名称和参数提取出来,并进行相应的验证。
- 命令处理:根据命令名称和参数,Redis 服务器会执行相应的操作。这包括读取、写入、修改数据,执行事务、管道等操作。
- 响应生成:在命令处理完成后,Redis 服务器会生成一个响应,表示命令执行的结果。响应的格式通常也是一个字符串,包括执行结果或者错误信息。
- 响应发送:最后,Redis 服务器将生成的响应通过网络连接发送给客户端,客户端接收到响应后可以进行相应的处理。
5、Redis 的线程模式?
Redis 是单线程的,这意味着 Redis 服务器在任何时候都只会执行一个命令,不会并发处理多个命令。这种单线程模式是 Redis 的核心特点之一,它带来了一些优势和限制:
优势:
- 避免竞态条件:单线程模式可以避免多线程并发情况下可能出现的竞态条件和数据不一致问题,简化了并发控制。
- 更少的上下文切换:单线程模式下,不需要频繁地进行线程切换,可以减少系统开销,提高性能。
- 简化设计:单线程模式下的设计更加简单直观,不需要考虑多线程并发带来的复杂性。
限制:
- 单个命令阻塞:由于 Redis 是单线程的,如果某个命令需要耗费较长时间,会导致其他命令被阻塞,影响系统的响应速度。
- 不适合 CPU 密集型任务:由于 Redis 的单线程模式,对于 CPU 密集型任务处理能力有限,不适合处理大量的计算密集型任务。
为了充分利用多核 CPU 的性能,Redis 通过多路复用 I/O 和事件驱动的方式实现并发处理。Redis 使用了 epoll (Linux)、kqueue (BSD)、select 等 I/O 多路复用机制,监听多个套接字上的事件,当事件发生时立即处理,从而实现高效的 I/O 处理能力。虽然 Redis 是单线程的,但通过这种方式可以处理大量并发连接,提高了系统的并发处理能力。
6、如果 Redis 是单线程的话,它的隔离级别应该是什么?
Redis 的单线程模式并不直接决定其隔离级别,因为隔离级别通常是与事务处理和并发控制相关的概念。Redis 的单线程模式主要影响了它对命令的执行方式,以及在执行过程中的并发控制。
虽然 Redis 是单线程的,但它仍然支持事务(Transaction)和 WATCH 命令来实现一定程度的隔离性。具体来说,Redis 的隔离级别:
- 读提交(Read Committed):事务中的命令只会读取到已经提交的数据。在 Redis 中,虽然是单线程模式,但在事务执行过程中,其他客户端对数据的修改不会影响到事务中已经读取的数据。但是如果事务执行中途出现了 WATCH 键被修改的情况,Redis 会终止事务,这可以看作是一种类似于读提交的隔离级别。
- 可重复读(Repeatable Read):虽然 Redis 的 WATCH 机制可以实现类似可重复读的效果,但是 Redis 的单线程模式无法完全保证在事务执行期间,其他客户端对数据的修改不会影响到事务中已经读取的数据。因此,Redis 的隔离级别在这方面并不完全符合可重复读。
7、Redis 一致性如何实现的?
- 单线程模型:Redis 的单线程模型确保了命令的原子性,即在执行命令时不会被其他命令中断,从而保证了数据的一致性。
- 持久化:Redis 提供了两种持久化方式,即 RDB(Redis DataBase)和 AOF(Append Only File)。RDB 是周期性地将内存中的数据快照保存到磁盘上,而 AOF 则是将每条写命令追加到文件末尾。这两种持久化方式可以保证在服务器重启后数据可以恢复,从而保证了数据的一致性。
- 复制:Redis 支持主从复制,即可以将一个 Redis 服务器的数据复制到多个从服务器上。主从复制可以提高系统的读取性能,并且在主服务器故障时可以快速切换到从服务器,保证了数据的可用性和一致性。
- 哨兵机制:Redis 的哨兵(Sentinel)机制可以监控 Redis 服务器的运行状态,并在主服务器故障时自动将一个从服务器升级为主服务器,从而保证了系统的高可用性和一致性。
- 集群:Redis 集群是一个分布式的 Redis 系统,可以将数据分片存储在多个节点上,从而提高系统的性能和扩展性。Redis 集群通过对数据进行哈希分片来实现数据的分布式存储,同时通过 Gossip 协议来实现节点间的通信和数据同步,从而保证了数据的一致性。
8、Redis 持久化的数据是否一定不会丢失?
Redis 的持久化机制可以将数据保存到磁盘上,以防止数据在服务器重启时丢失。但是,持久化的数据并不是一定不会丢失的,这主要取决于使用的持久化方式和配置。
Redis 提供了两种主要的持久化方式:RDB(Redis DataBase)和 AOF(Append Only File)。
- RDB 持久化:RDB 持久化通过定期将内存中的数据快照保存到磁盘上来实现持久化。虽然 RDB 可以保证数据在某个时间点的一致性,但在最后一次持久化和服务器故障之间的时间段内,如果发生故障,可能会丢失这段时间内的数据。
- AOF 持久化:AOF 持久化将每条写命令追加到文件末尾,通过重放这些命令来恢复数据。AOF 持久化可以提供更好的持久化保证,因为它可以在每次写操作之后都进行 fsync 操作将数据同步到磁盘上。但是,在极端情况下(比如硬件故障),仍然可能会丢失部分数据。
为了提高持久化数据的安全性,可以结合使用 RDB 和 AOF。使用 RDB 来定期创建快照,同时使用 AOF 来记录每个写操作,可以提供更好的数据保护。此外,还可以配置 Redis 的持久化策略,如设置自动触发持久化的条件和频率,以及选择是否在每次写操作后立即进行 fsync 操作,来进一步提高数据的安全性。
9、如果 Redis 每次都是立刻刷盘,是否一定不会丢失数据?如果是采取这种方式,会有什么问题?
如果 Redis 每次写操作都立即进行 fsync 操作将数据同步到磁盘上,可以保证写入数据的持久性,即数据不会因为服务器故障而丢失。
但是,这种方式会带来一些问题:
- 性能影响:每次写操作都进行 fsync 操作会增加磁盘的 I/O 负载,降低性能。特别是在高并发情况下,频繁的 fsync 操作可能会成为性能瓶颈。
- 磁盘消耗:频繁的 fsync 操作会增加磁盘的写入次数,加速磁盘的磨损,降低磁盘的寿命。
- 数据一致性:虽然立即刷盘可以保证数据的持久性,但如果在刷盘过程中发生故障,可能会导致部分数据丢失,因为数据在写入磁盘之前是存储在操作系统的缓冲区中的。
- 性能不稳定:由于 fsync 操作的开销比较大,可能会导致性能的不稳定性,特别是在写入负载较高时。
10、MySQL 的索引作用,和其他几种数据结构(二叉树、B 树等)的对比?
索引是一种数据结构,用于提高数据库系统对数据的查询速度。在 MySQL 中,索引的作用主要包括:
- 加快数据检索速度:通过索引,数据库系统可以快速定位到需要查询的数据行,减少了扫描整个表的时间,提高了查询速度。
- 加快排序和分组操作:如果查询包含 ORDER BY 或 GROUP BY 子句,索引可以帮助数据库系统避免对整个表进行排序或分组操作,提高了排序和分组的效率。
- 加快连接操作:如果查询涉及多个表的连接操作,索引可以帮助数据库系统快速定位到连接所需的数据行,提高了连接操作的效率。
- 唯一约束:通过在列上创建唯一索引,可以保证该列的值在表中是唯一的,实现了唯一性约束。
MySQL 中常用的索引类型包括 B 树索引、哈希索引和全文索引。与其他数据结构相比,它们各有特点:
- B 树索引:B 树(或者是 B+ 树)是一种多路搜索树,它的节点可以存储多个键值,适合范围查询。B 树索引在磁盘存储上有很好的性能,因为它是按照页(通常是 4KB)来存储数据的。MySQL 的 InnoDB 存储引擎就是使用 B+ 树来实现索引的。
- 哈希索引:哈希索引是将键值通过哈希函数映射到哈希表中的一个位置,适合等值查询。哈希索引在等值查询上具有很高的性能,但不支持范围查询,并且不适合于排序操作。MySQL 中的 MEMORY 存储引擎可以使用哈希索引。
- 全文索引:全文索引是用于全文搜索的索引类型,可以在文本列上进行高效的全文搜索。全文索引在匹配文本内容时很有用,但不适合于一般的查找操作。
11、如何高效的使用索引?
- 选择合适的索引:根据查询的字段和条件选择合适的索引类型和列。对于经常用于查询的字段,可以考虑创建索引。
- 避免在索引列上使用函数:使用函数会导致索引失效,因此应尽量避免在索引列上使用函数操作,可以在查询前对数据进行预处理。
- 避免在索引列上使用运算符:在索引列上使用运算符(如!=、<>、LIKE)会导致索引失效,尽量避免这样的操作。
- 避免使用 OR 操作符:在 WHERE 子句中使用 OR 操作符会导致索引失效,应尽量避免使用 OR,可以使用 UNION 或者重写查询语句来替代。
- 使用覆盖索引:如果查询只需要从索引中获取数据而不需要访问表中的其他列,可以使用覆盖索引来避免访问表数据,提高查询性能。
- 定期维护索引:定期对索引进行优化和维护,可以通过重建索引、分析索引等操作来提高索引的效率。
- 使用复合索引:对于经常一起使用的多个列,可以考虑创建复合索引,可以减少索引的数量,提高查询效率。
- 避免过度索引:不要在每个列上都创建索引,只在需要的列上创建索引,避免过度索引导致维护成本增加。
- 注意索引列的顺序:对于复合索引,索引列的顺序很重要,应根据查询的频率和条件选择合适的列顺序。
- 使用索引提示:在查询中使用索引提示(INDEX 或者 USE INDEX)可以强制 MySQL 使用指定的索引,提高查询性能。
12、一条 URL 输入到浏览器过程中发生了什么过程?
- URL 解析:浏览器首先会解析输入的 URL,包括协议(如 http、https)、域名、端口号、路径等信息。
- DNS 查询:浏览器会根据解析得到的域名(如 www.xxx.com)向 DNS 服务器发起查询,获取该域名对应的 IP 地址。
- 建立 TCP 连接:浏览器通过 IP 地址和端口号与服务器建立 TCP 连接,发起 HTTP 请求。
- 发起 HTTP 请求:浏览器向服务器发送 HTTP 请求,包括请求方法(如 GET、POST)、请求头(如 User-Agent、Accept)、请求体(如 POST 请求中的表单数据)等信息。
- 服务器处理请求:服务器接收到请求后,根据请求的内容进行处理,可能包括查询数据库、读取文件等操作。
- 服务器返回响应:服务器处理完成后,会返回一个 HTTP 响应,包括状态码(如 200 表示成功、404 表示未找到资源)、响应头(如 Content-Type、Content-Length)和响应体(如 HTML、图片等数据)。
- 浏览器接收响应:浏览器接收到服务器返回的响应后,会根据响应的内容进行解析和处理。
- 渲染页面:如果响应内容是 HTML 页面,浏览器会解析 HTML、CSS 和 JavaScript,并渲染页面显示给用户。
- 显示页面:最终,浏览器会将渲染好的页面显示给用户,用户可以查看页面内容并与页面进行交互。
- 连接关闭:当页面加载完成后,浏览器和服务器之间的 TCP 连接会被关闭,页面加载过程结束。
13、TCP 的三次握手为什么是三次?不是两次、不是四次?
TCP 的三次握手是为了确保双方的通信能够正常建立起来,保证数据的可靠传输。三次握手的过程如下:
- 第一次握手(SYN):客户端发送一个 SYN(同步)包给服务器,表示客户端请求建立连接,并指定自己的初始序列号(seq=x)。
- 第二次握手(SYN + ACK):服务器收到 SYN 包后,如果同意建立连接,会发送一个 SYN+ACK 包给客户端,表示服务器接受请求,同时指定自己的初始序列号(seq=y),确认客户端的序列号(ack=x+1)。
- 第三次握手(ACK):客户端收到 SYN+ACK 包后,会发送一个 ACK 包给服务器,表示确认连接建立,同时指定自己的序列号(seq=x+1),确认服务器的序列号(ack=y+1)。
为什么是三次握手而不是两次或四次呢?
- 防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误:如果只有两次握手,客户端发送的连接请求到达了服务端,但是在返回确认的ACK之前网络发生了延迟,客户端重新发送连接请求,这时服务端已经收到了客户端的连接请求,并且建立了连接,但是客户端没有收到服务端的确认,认为连接未建立,再次发送请求,就会出现错误。
- 防止服务端发送的连接确认报文段丢失,客户端不知道是否连接成功:如果只有两次握手,服务端发送了连接确认报文段,但是这个报文段在传输过程中丢失了,服务端认为连接已经建立,开始发送数据,但是客户端没有收到确认,认为连接未建立,不发送数据,这样就会导致连接出现错误。
14、HTTP 是基于 TCP 的吗?
HTTP(Hypertext Transfer Protocol)是基于 TCP(Transmission Control Protocol)的。HTTP 是一种应用层协议,用于在客户端和服务器之间传输超文本(如网页、图片等)内容。而 TCP 是一种传输层协议,提供可靠的、面向连接的数据传输服务。
HTTP 使用 TCP 作为传输层协议的原因包括:
- 可靠性:TCP 提供了可靠的数据传输机制,通过数据序号、确认应答、重传等机制可以确保数据的可靠传输,适合用于传输网页、图片等对数据完整性要求较高的内容。
- 流量控制:TCP 提供了流量控制机制,可以控制数据传输的速率,避免了数据发送方发送速度过快导致接收方无法处理的情况。
- 连接管理:TCP 提供了连接管理机制,包括建立连接、维护连接状态、关闭连接等,可以确保客户端和服务器之间的通信是可靠的、有序的。
- 错误恢复:TCP 提供了错误恢复机制,可以检测和纠正传输过程中的错误,确保数据的完整性和正确性。
15、HTTP3.0 基于 UDP 的话实现了什么?
HTTP/3.0 是基于 UDP 的 QUIC 协议实现的,它相比于传统基于 TCP 的 HTTP/1.x 和 HTTP/2.0,带来了一些重要的改进和优势:
- 降低连接建立和头部开销:QUIC 使用单个连接复用多个流(类似于 HTTP/2 的多路复用),避免了 TCP 的慢启动和头部压缩,降低了连接建立和头部开销。
- 减少网络延迟:QUIC 支持快速连接迁移和零连接迁移,可以更快地恢复连接,减少网络延迟。
- 提高网络安全性:QUIC 内置了 TLS 1.3 加密,保护了数据的安全性和隐私性。
- 抗丢包和拥塞控制:QUIC 使用了自定义的拥塞控制算法,可以更好地适应网络状况,减少丢包和拥塞的影响。
- 改进了流量控制:QUIC 使用了更加灵活的流量控制机制,可以更好地管理数据传输。
- 改进了头部压缩:QUIC 使用 QPACK 头部压缩算法,相比于 HTTP/2 的 HPACK 算法,可以更高效地压缩头部。
16、HTTP 的首部字段大概有哪些?
HTTP 协议的首部字段(Header Fields)用于描述请求或响应的各种属性和属性值,包括控制缓存、授权、连接管理等。HTTP 首部字段可以分为通用首部字段、请求首部字段、响应首部字段和实体首部字段四种类型。
- 通用首部字段(General Header Fields):适用于请求和响应消息的首部字段,常见的有:
Cache-Control
:控制缓存行为。Connection
:控制连接的选项。Date
:表示消息创建时间。Pragma
:兼容性控制,通常用于控制缓存。Trailer
:指示消息尾部存在哪些字段。Transfer-Encoding
:表示消息主体的传输编码方式。Upgrade
:要求服务器升级到另一个协议。Via
:显示出发生了哪些代理服务器的通信。Warning
:警告实体可能存在的问题。
- 请求首部字段(Request Header Fields):包含有关请求消息的信息,常见的有:
Accept
:告诉服务器客户端能够处理的媒体类型。Accept-Encoding
:告诉服务器客户端支持的内容编码。Authorization
:包含用于验证用户身份的凭据。Host
:指定服务器的域名和端口号。User-Agent
:包含了发出请求的用户代理的信息。
- 响应首部字段(Response Header Fields):包含有关响应消息的信息,常见的有:
Age
:响应的年龄。Content-Encoding
:表示响应主体的内容编码方式。Content-Length
:表示响应主体的长度。Content-Type
:表示响应主体的媒体类型。Server
:包含了服务器用于处理请求的软件信息。
- 实体首部字段(Entity Header Fields):包含有关实体主体的信息,常见的有:
Content-Language
:实体主体的自然语言。Content-Location
:实体主体的备用位置。Content-Disposition
:指定如何显示响应主体的附加信息。
17、HTTP 的状态码大概有哪些?
- 1xx(Informational):信息性状态码,表示请求已被接收,继续处理。
- 100(Continue):服务器已经收到请求头,并且客户端应该继续发送请求的其余部分。
- 2xx(Success):成功状态码,表示请求已成功被服务器接收、理解、并接受。
- 200(OK):请求成功,返回的信息包含在响应中。
- 201(Created):请求已经被成功处理,并且创建了新的资源。
- 204(No Content):服务器成功处理请求,但未返回任何内容。
- 3xx(Redirection):重定向状态码,表示需要客户端采取进一步的操作才能完成请求。
- 301(Moved Permanently):请求的资源已被永久移动到新的位置。
- 302(Found):请求的资源已临时移动到新的位置。
- 304(Not Modified):资源未被修改,可以使用缓存的版本。
- 4xx(Client Error):客户端错误状态码,表示客户端发送的请求有错误。
- 400(Bad Request):请求无效,服务器无法处理。
- 401(Unauthorized):请求需要用户身份验证。
- 403(Forbidden):服务器拒绝请求。
- 404(Not Found):请求的资源不存在。
- 5xx(Server Error):服务器错误状态码,表示服务器在处理请求时发生错误。
- 500(Internal Server Error):服务器遇到了一个未知的错误。
- 502(Bad Gateway):服务器作为网关或代理,从上游服务器接收到无效的响应。
- 503(Service Unavailable):服务器当前无法处理请求,通常是由于维护或过载。
18、介绍一下虚拟内存?
虚拟内存是计算机系统中一种管理内存的技术,它使得应用程序能够访问比物理内存更大的地址空间。虚拟内存的主要目的是提供一种将物理内存和磁盘空间结合起来使用的方法,从而增加了系统对内存的管理和使用效率。
虚拟内存的工作原理如下:
- 地址空间划分:虚拟内存将整个地址空间划分为固定大小的页(Page),通常为4KB。物理内存也被划分为相同大小的页框(Page Frame)。
- 分页机制:当应用程序访问内存时,操作系统将虚拟地址转换为物理地址。如果所需的页不在物理内存中,操作系统将进行页面调度,将页从磁盘加载到物理内存中。
- 页面置换:当物理内存不足时,操作系统需要选择一个页面进行置换。常见的页面置换算法包括最近最少使用(LRU)、先进先出(FIFO)等。
- 内存保护:虚拟内存还提供了内存保护的功能,可以防止应用程序访问未分配或非法的内存区域,提高了系统的安全性和稳定性。
虚拟内存的优点:
- 更大的地址空间:虚拟内存使得应用程序可以使用比物理内存更大的地址空间,提高了系统的灵活性和扩展性。
- 更高的内存利用率:虚拟内存可以将不常用的数据存储在磁盘上,只在需要时才将其加载到物理内存中,提高了内存的利用率。
- 更好的内存管理:虚拟内存提供了更灵活的内存管理机制,包括页面调度、页面置换等,可以更有效地管理内存。
虚拟内存的缺点:
- 性能损失(由于页面调度和页面置换等操作引起的额外开销)
- 复杂性(管理虚拟内存需要更多的系统资源和算法)
- 可能引起的内存泄漏和性能问题。
19、进程的内存地址空间分布?
- 代码段(Text Segment):也称为只读段,存储程序的可执行指令。这部分内存通常是只读的,以防止程序意外修改代码,造成程序错误。代码段通常是共享的,多个进程可以共享同一段代码,以节省内存。
- 数据段(Data Segment):存储程序的全局变量和静态变量。数据段通常包括初始化的全局变量(已经被赋初值)和未初始化的全局变量(未被赋初值,通常初始化为0)。数据段是可读写的。
- 堆(Heap):存储动态分配的内存空间,如通过
malloc
、new
等函数动态分配的内存。堆的大小可以动态增长或缩小,由操作系统动态管理。 - 栈(Stack):存储函数的局部变量、函数参数、函数返回地址等信息。每个函数调用都会在栈上分配一块内存空间,函数返回时释放这块空间。栈是一个后进先出(LIFO)的数据结构,栈的大小是固定的,通常由操作系统设置。
- 内核空间(Kernel Space):用于存储操作系统的内核代码和数据结构。内核空间通常是独立的,无法被用户程序直接访问,需要通过系统调用(System Call)来访问内核空间的内容。
20、栈这个数据结构有哪些应用场景?
- 函数调用栈:在程序中,每次函数调用都会在栈上创建一个称为“栈帧”的数据结构,用于存储函数的局部变量、函数参数、返回地址等信息。当函数返回时,对应的栈帧会被销毁,函数调用栈就可以正确地返回到上一级函数。
- 表达式求值:在编译器和解释器中,常用栈来实现表达式的求值。例如,中缀表达式转换为后缀表达式时,可以利用栈来存储运算符,通过比较优先级来确定运算顺序。
- 括号匹配:在编程中经常需要检查代码中的括号是否匹配。可以使用栈来存储左括号,遇到右括号时弹出栈顶元素并比较,如果匹配则继续,否则说明括号不匹配。
- 递归:在一些递归算法中,也会使用栈来存储每次递归调用的参数和返回地址,以实现递归的效果。
- 迭代器:在实现迭代器时,可以使用栈来存储遍历的状态,以便在需要时回溯到上一个状态。
- 内存管理:在操作系统中,栈也用于管理进程的内存空间,包括存储函数调用信息、局部变量和函数参数等。
21、栈区和堆区对于不同的编程语言来说是否都有这两个概念?
- 栈区:
- 概念:栈区是一种内存区域,用于存储函数调用时的局部变量、函数参数、返回地址等信息。它具有后进先出(LIFO)的特性,每次函数调用都会在栈上创建一个称为“栈帧”的数据结构。
- 实现:在大多数编程语言中,栈区是由编译器或解释器自动管理的,程序员无需手动管理栈区的内存。栈区的大小通常是固定的,由操作系统或编程语言规定。
- 应用:栈区主要用于函数调用、局部变量存储等场景。
- 堆区:
- 概念:堆区是用于动态分配内存的一种内存区域。堆区的大小通常是不固定的,可以根据程序的需要动态增长或缩小。
- 实现:堆区的内存由程序员手动分配和释放,通常使用
malloc
、new
等函数进行分配,使用free
、delete
等函数进行释放。 - 应用:堆区主要用于存储动态分配的内存,如对象、数组等。
不同编程语言对栈区和堆区的实现方式和管理方式有所不同。例如,C 和 C++ 等语言需要程序员显式地管理堆区的内存,而像 Java、Python 等语言则由垃圾回收器自动管理堆区的内存,程序员只需关注对象的创建和使用,不需要手动释放内存。
22、算法题:求 x 的平方根,有一个精度问题?
思路
- 初始化左边界
left
为 0,右边界right
为x
。 - 进入循环,直到
left
大于right
:- 计算中间值
mid = left + (right - left) / 2
。 - 如果
mid
的平方等于x
,则返回mid
。 - 如果
mid
的平方小于x
,则更新left = mid + 1
。 - 如果
mid
的平方大于x
,则更新right = mid - 1
。
- 计算中间值
- 循环结束后,返回
right
。
参考代码
C++
class Solution {
public:
int mySqrt(int x) {
// 定义左边界和右边界
int left = 0, right = x;
// 进入循环
while (left <= right) {
// 计算中间值
int mid = left + (right - left) / 2;
// 判断中间值的平方与 x 的关系
if ((long long)mid * mid == x) {
return mid; // 如果相等,返回中间值
} else if ((long long)mid * mid < x) {
left = mid + 1; // 如果平方小于 x,更新左边界
} else {
right = mid - 1; // 如果平方大于 x,更新右边界
}
}
// 循环结束后,返回右边界
return right;
}
};
int main() {
Solution solution;
int x1 = 4;
int result1 = solution.mySqrt(x1);
std::cout << "sqrt(" << x1 << ") = " << result1 << std::endl; // 输出 2
int x2 = 8;
int result2 = solution.mySqrt(x2);
std::cout << "sqrt(" << x2 << ") = " << result2 << std::endl; // 输出 2
return 0;
}