三、计算机基础
01.操作系统
01.进程几种状态
02.锁
互斥锁:互斥锁是一种用于线程同步的工具,能够保证同一时刻只有一个线程可以访问共享资源。如果一个线程已经取得了互斥锁,其他尝试获得该锁的线程将会被阻塞,直到第一个线程释放了该锁。
乐观锁:乐观锁假定冲突发生的几率很小,因此在数据操作前并不会加锁,但会在进行更新等操作时检查是否有其他线程对数据进行了修改。如果有,则操作失败;没有,则操作成功。乐观锁一般适用于读多写少的场景。
悲观锁:悲观锁假设冲突发生的几率很大,所以在每次读写数据前都会先加锁。这种方式虽然保证了数据的一致性,但也可能造成资源的浪费。悲观锁主要通过数据库提供的锁机制实现,例如行级锁、表级锁等。
03.并发和并行
-
并发:两个或多个事件在同一时间间隔内发生,这些事件在宏观上是同时发生的,在微观上是交替发生的, 操作系统的并发性指系统中同时存在着多个运行的程序
-
并行:两个或多个事件在同一时刻发生
-
一个单核(CPU)同一时刻只能执行一个程序,因此操作系统会协调多个程序使他们交替进行(这些程序在宏观上是同时发生的,在微观上是交替进行的)
-
操作系统是伴随着“多道程序技术出现的”,因此操作系统和并发是一同诞生的
-
在如今的计算机中,一般都是多核cpu的,即在同一时刻可以并行执行多个程序,比如我的计算机是8核的,我的计算机可以在同一时刻并行执行8个程序,但是事实上我们计算机执行的程序并不止8个,因此并发技术是必须存在的,并发性必不可少。
04.内核态和用户态
CPU 中有一个寄存器叫 程序状态字寄存器(PSW),其中有个二进制位,1表示“内核态”,0表示“用户态”。
内核态→用户态:执行一条特权指令——修改 PSW 的标志位为“用户态”,这个动作意味着操作系统将主动让出CPU使用权
用户态→内核态:由“中断”引发,硬件自动完成变态过程,触发中断信号意味着操作系统将强行夺回CPU的使用权。
05.操作系统常用的调度算法
操作系统中几种最最最常见的调度算法(适用于软件设计师考试与期末考试复习)_循环扫描算法-CSDN博客
在进程调度算法常见的算法有:
-
先来先服务(FirstComeFirstServed,FCFS):即按作业到来或进程变为就绪状态的先后次序分派处理机,属于非抢占方式,当前作业或进程直到执行完或阻塞,才出让处理机。
-
短作业优先(Shortest Job First, SIF):短进程优先是从就绪队列中选择估计运行时间最短的进程,将处理机分配给它,使之执行直到完成或因发生某事件而阻塞放弃处理机时,再重新调度。
-
轮转法(Round Robin):将处理机的时间分成固定大小的时间片,不能取得过大或者过小,通常为10~100ms数量级。处理机调度程序按照FCFS原则,轮流地把处理机分给就绪队列中的每个进程,时间长度都是一个时间片。
-
优先级算法(Priority Scheduling):统按一定规则赋予每个进程一个调度优先级,把处理机分配给就绪队列中具有最高优先级的进程。
02.计算机网络
01.OSI七层模型
简记:物联网叔会使用
-
物理层:传输单位bit
-
数据链路层:传输单位帧。如:串口通信中使用到的 115200、8、N、1
-
网络层:传输单位数据报
-
传输层:传输单位报文。定义了一些传输数据的协议和端口号
-
会话层
-
表示层
-
应用层
02.TCP/IP四层模型
TCP/IP 网络协议栈分为应用层(Application)、传输层(Transport)、网络层(Network)和链路层(Link)四层。
03.应用层常见协议和端口号
应用层常见协议和端口号有很多,以下是一些常见的应用层协议及其对应的端口号:
-
HTTP(Hypertext Transfer Protocol)- 端口号:80
-
用于在Web浏览器和Web服务器之间传输超文本文档。是一种明文传输协议,数据在客户端和服务器之间以纯文本形式传输,容易被窃听和篡改。
-
-
HTTPS(Hypertext Transfer Protocol Secure)- 端口号:443
-
通过SSL/TLS加密的HTTP通信,用于安全地传输数据。
-
-
FTP(File Transfer Protocol)- 端口号:20和21
-
用于在客户端和服务器之间传输文件。在 FTP 中,端口 21 用于控制连接,端口 20 用于数据连接。当客户端建立与服务器的 FTP 连接时,首先会在端口 21 上进行控制连接,然后根据需要在端口 20 上建立数据连接进行文件传输。
-
-
SSH(Secure Shell)- 端口号:22
-
用于在网络中提供加密的通信会话。
-
-
SMTP(Simple Mail Transfer Protocol)- 端口号:25
-
用于在邮件服务器之间传输电子邮件。
-
-
POP3(Post Office Protocol version 3)- 端口号:110
-
用于从邮件服务器上收取电子邮件。
-
-
IMAP(Internet Message Access Protocol)- 端口号:143
-
用于在客户端与邮件服务器之间传输邮件。
-
-
DNS(Domain Name System)- 端口号:53
-
用于将域名解析为对应的IP地址。
-
-
SNMP(Simple Network Management Protocol)- 端口号:161
-
用于网络设备之间的管理和监控。
-
-
RDP(Remote Desktop Protocol)- 端口号:3389
-
用于远程桌面连接。
-
-
DHCP(Dynamic Host Configuration Protocol):用于自动分配IP地址。默认端口号为67和68,分别用于客户端和服务器。
这些是一些常见的应用层协议及其默认端口号,实际应用中也可能会使用非标准端口或根据需要进行自定义配置。
04.TCP三次握手
TCP的运输连接管理之三次握手四次挥手_第三次握手是可以携带数据的,前两次握手是不可以携带数据的-CSDN博客
-
-
第一次握手:客户端发送一个SYN包(同步序列编号),告诉服务器它想建立连接,并且客户端能发送数据。
-
第二次握手:服务器回应一个SYN-ACK包,确认收到了客户端的SYN包,同时告知客户端它也愿意建立连接,并且服务器能接收数据。
-
第三次握手:客户端再次发送ACK包响应服务器的SYN-ACK包,这次确认后,客户端和服务器都确认双方的发送和接收能力正常。
-
-
如果只有两次握手,那么只要服务器端发送了ACK包确认,就会建立连接。假设这个时候存在一个失效的连接请求延迟到达服务器,服务器处理这个错误的请求并打开连接,等待客户端发送数据,但客户端并没有实际发出这个请求或者已经不需要这个连接了,这就导致了服务器白白浪费资源等待一个不存在的发送方。
05.四次挥手
TCP三次握手和四次挥手详解_tcp三次握手和4次挥手的过程-CSDN博客
因为服务端在接收到FIN, 往往不会立即返回FIN ,必须等到服务端所有的报文都发送完毕了,才能发FIN。为了防止客户端等待时间过长触发FIN重传,因此先发一个ACK表示已经收到客户端的FIN,等服务器发送完了数据,再发送FIN包进行第三次挥手。
-
刚开始客户端和服务器端都处于ESTABLISHED状态,假如客户端发起关闭请求;
-
第一次挥手:客户端向服务器发送FIN报文(FIN=1,seq=u),发完后进入FIN_WAIT_1状态,即主动关闭TCP连接,不再发送数据,但可以接收服务器发来的报文,等待服务器回复;
-
第二次挥手:服务器接到FIN报文后,返回一个ACK报文(ACK=1,ack=u+1,seq=v),表明自己接收到此报文,服务器进入CLOSE_WAIT关闭等待状态,此时客户端就知道服务端接到自己的断开连接请求,进入到FIN_WAIT_2状态,TCP处于半关闭状态,但服务器端可能还有数据要传输。
-
第三次挥手:服务器关闭客户端连接,发送FIN报文(FIN=1,seq=w,ack=u+1)给客户端,此时服务器处于LAST_ACK状态,等待客户端回应。
-
第四次挥手:客户端收到FIN报文后,发送一个ACK(ACK=1,ack=w+1,seq=u+1)给服务器作为应答,此时客户端处于TIME_WAIT状态,这个状态是为了等待足够的时间以确保TCP接收到连接中断请求的确认
等待2MSL的原因
-
防⽌客户端最后⼀次发给服务器的确认在⽹络中丢失以⾄于客户端关闭,⽽服务端并未关闭,导致资源的浪费。
-
等待最⼤的2msl可以让本次连接的所有的⽹络包在链路上消失,以防造成不必要的⼲扰。
-
如果客户端直接closed,然后⼜向服务端发起了⼀个新连接,我们不能保证这个新连接和刚关闭的连接的端⼝号是不同的。假设新连接和已经关闭的⽼端⼝号是⼀样的,如果前⼀次滞留的某些数据仍然在⽹络中,这些延迟数据会在新连接建⽴后到达服务端,所以socket就认为那个延迟的数据是属于新连接的,数据包就会发⽣混淆。所以客户端要在TIME_WAIT状态等待2倍的MSL,这样保证本次连接的所有数据都从⽹络中消失。
为什么三次挥手不行 因为服务端在接收到FIN, 往往不会立即返回FIN ,必须等到服务端所有的报文都发送完毕了,才能发FIN。因此先发一个ACK表示已经收到客户端的FIN,延迟一段时间才发FIN。这就造成了四次挥手。
如果是三次挥手会造成: 如果将服务端的两次挥手合为一次,等于说服务端将ACK和FIN的发送合并为一次挥手,这个时候长时间的延迟可能会导致客户端误以为FIN没有到达客户端,从而让客户端不断的重发FIN。所有只能第二次握手先发送ACK确认接收到了客户端的数据,等服务器发送完了数据,再发送FIN包进行第三次挥手。
06.TCP和UDP区别
TCP和UDP都是在网络通信中使用的协议,它们都位于网络模型的第四层(传输层)。但它们之间有一些关键的区别:
-
连接类型:
-
TCP是一种面向连接的协议。在数据传输之前,它需要建立一个连接,这就像是打电话,你需要先拨号建立连接,然后才能通话。
-
UDP是一种无连接的协议。它不需要预先建立连接,就可以直接发送数据,这就像是寄信,你直接投递到邮筒,不需要先与对方建立联系。
-
-
数据传输的可靠性:
-
TCP提供了一种可靠的数据传输服务。它有确认、重传和拥塞控制机制,可以保证数据的正确性和顺序性。
-
UDP则不提供数据传输的可靠性保证,它只是简单地将数据包发送出去,不关心数据包是否到达目的地,因此可能会出现数据丢失的情况。
-
-
传输速度:
-
由于TCP需要进行连接建立、确认和重传等操作,所以相对来说,其传输速度比UDP慢。
-
UDP由于没有复杂的控制机制,所以其传输速度通常比TCP要快。
-
-
使用场景:
-
TCP常用于需要高可靠性的应用,如网页浏览(HTTP、HTTPS)、邮件发送(SMTP)等。
-
UDP则适合对实时性要求较高,可容忍少量数据丢失的应用,如视频会议、语音通话、直播等。
-
-
头部开销:
-
TCP的头部开销较大,最小20字节,提供了许多选项,如错误检测,序列号,确认号等。
-
UDP的头部开销小,只有8字节,只提供了最基本的功能。
-
07.滑动窗口和超时重传
滑动窗口
滑动窗口是TCP和其他网络协议中用于控制数据传输的一种技术。它的主要目的是防止发送方发送过多数据,从而导致接收方无法处理。
在TCP中,滑动窗口由发送窗口和接收窗口组成,它们分别代表了发送方可以发出的数据量和接收方可以接受的数据量。每次数据传输后,窗口会“滑动”以适应新的数据流。滑动窗口的大小根据网络拥塞情况、接收方处理能力等因素动态调整,从而实现TCP的流量控制和拥塞控制。
超时重传
超时重传是TCP中用于保证数据可靠传输的一种机制。当发送方发出一个数据包后,它会启动一个定时器等待接收方的确认。如果在定时器超时之前接收到确认,则表示数据包已成功传送;否则发送方会认为该数据包在网络中丢失,需要进行重传。
超时重传能够确保即使在网络环境不理想的情况下,数据也能最终被接收到。这是TCP提供可靠传输服务的关键机制之一。然而,由于超时重传可能增加网络拥塞,所以TCP还配备了拥塞控制机制来避免过度重传。
需要注意的是,TCP的超时时间并非固定,而是根据Round-Trip Time(往返时间)动态调整。这样可以更好地适应不同的网络条件,提高效率。
08.TCP粘包
TCP数据粘包的处理 | 爱编程的大丙 (subingwen.cn)
TCP粘包指的是接收方在一次读取数据时,将多个发送方发送的数据包合并成一个或者少于原始数据包数量的现象。TCP粘包通常发生在基于流式传输(如TCP)的网络通信中,其主要原因有以下几点:
-
TCP为面向流的协议:TCP是面向流的传输协议,发送方可以将数据划分为任意大小的数据块发送,接收方可能一次性接收到多个数据包,导致多个数据包被合并成一个大的数据块。
-
接收方缓冲区未及时读取:如果接收方没有及时从缓冲区中读取数据,而发送方持续发送数据,则多个数据包可能会在接收方的缓冲区中累积,从而造成粘包现象。
-
网络延迟和拥塞:网络延迟、拥塞等因素也可能导致数据包在传输过程中聚集在一起,形成粘包现象。
-
操作系统对数据处理方式不同:不同操作系统对于接收数据的处理方式可能不同,有些操作系统会尽量将接收到的数据进行合并,从而可能导致粘包现象。
为避免TCP粘包问题,可以采用以下方法:
-
在数据包中增加长度字段,让接收方根据长度字段来正确解析数据包。
-
使用特殊标记或分隔符来标识数据包的边界。
-
对数据进行序列化和反序列化处理,确保数据的完整性和正确性。
-
合理设计应用层协议,规范数据的传输方式,避免产生粘包问题。
09.DNS
DNS(域名系统):
过程总结: 浏览器缓存,系统缓存,路由器缓存,ISP服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。
-
DNS是一个分布式的服务,它将人类可读的域名(如 www.example.com)转换为机器可读的IP地址(如 192.0.2.1),使得用户能够通过域名访问网站而无需记住复杂的IP地址。
-
当你输入一个网址时,你的设备会使用DNS来查找对应的IP地址,从而能够连接到正确的服务器。
-
DNS查询通常在用户感知不到的情况下在后台进行,并且百度一下,你就知道大多使用UDP协议进行通信,因为它比TCP更快,而DNS查询需要速度。
10.DNS域名缓存
缓存目的和好处
DNS域名缓存的主要目的是减少对远端DNS服务器的查询次数,加快域名解析速度,减轻DNS服务器的负担,从而提高整个互联网的效率和性能。具体来说,DNS缓存带来的好处包括:
-
提高解析速度:通过从缓存中直接获取解析结果,避免了每次都进行完整的DNS解析流程,大大加快了域名到IP地址的转换速度。
-
减少网络延迟:由于减少了对远端DNS服务器的查询,从而降低了网络延迟。
-
减轻DNS服务器负担:缓存可以显著减少DNS服务器接收的请求数量,有助于缓解服务器负载。
缓存位置
-
浏览器缓存:现代Web浏览器都会维护自己的DNS缓存,以便重复访问的网站可以更快加载。
-
操作系统缓存:操作系统也会缓存DNS查询结果,当应用程序请求DNS解析时,首先会检查操作系统的DNS缓存。
-
递归DNS服务器缓存:当用户的查询请求发送到递归DNS服务器时,这些服务器也会缓存一份DNS查询结果,供后续相同的查询请求使用。
-
权威DNS服务器:虽然权威DNS服务器本身不缓存外部域名的解析结果,但它们会为自己负责的域名提供TTL(生存时间),告诉其他DNS服务器和客户端可以缓存解析结果的时间长度。
11.常见协议与功能
-
ARP(Address Resolution Protocol)是一种用于将IP地址解析为MAC地址的协议。在计算机网络中,每个网络接口都有一个唯一的MAC地址(物理地址),而IP地址用于标识网络中的主机。当一台主机需要发送数据包到另一台主机时,它需要知道目标主机的MAC地址,而不仅仅是IP地址。
-
NAT(Network Address Translation,网络地址转换)是一种网络技术,用于将私有网络内部的IP地址转换为公共网络(如Internet)上的IP地址,以实现私有网络内部主机与公共网络之间的通信。
-
DHCP(Dynamic Host Configuration Protocol,动态主机配置协议)是一种用于自动分配IP地址和其他网络配置参数的协议。它允许网络管理员集中管理和分配IP地址,而无需手动配置每个主机的网络参数。
-
DNS(Domain Name System,域名系统)是互联网中用于将域名(例如example.com)转换为与之相关联的IP地址(例如192.0.2.1)的分布式命名系统。它充当了互联网的电话簿,使得用户可以通过易记的域名访问网站,而不必记住复杂的IP地址。
12.长连接和短连接
在计算机网络和通信中,"长连接"和"短连接"是指客户端和服务器之间的连接持续时间的两种不同模式。
短连接:
-
每次通信都需要建立一个新的连接。
-
客户端向服务器发送请求后,服务器响应请求,数据发送完成之后,立即断开连接。
-
短连接适用于请求-响应模式,常见于HTTP/1.0协议的交互。
-
短连接由于频繁地进行TCP三次握手和四次挥手过程,对于服务器资源消耗较大,但可以较好地释放资源,适用于轻量级的、偶尔的数据交换。
-
短连接通常用于处理瞬间高并发的场景。
长连接:
-
一旦建立,连接会保持开放,直到客户端或服务器明确地关闭它。
-
客户端与服务器建立连接后,可以进行多次的数据传输,直到任一方主动关闭连接。
-
长连接减少了因为建立和关闭连接而产生的额外开销,适用于需要频繁交换数据的应用。
-
在HTTP/1.1协议中,默认使用长连接,通过
Connection: keep-alive
头部实现。 -
长连接适用于需要维持持久状态或频繁通信的场景,如数据库连接、文件传输、实时通信等。
-
心跳机制常用于维护长连接,确保连接的活性,并能及时发现异常断开。
如何选择:
-
应用场景:如果客户端与服务器之间的交云频繁且持续,长连接可以减少因建立和关闭连接而产生的开销。如果交互是偶尔的,可能短连接更合适。
-
资源消耗:长连接可以减少CPU和网络的消耗,但会占用更多的内存资源,因为连接需要保持状态。短连接会增加
03.计算机组成原理
01.冯·诺依曼架构&哈佛架构
冯·诺依曼架构和哈佛架构是计算机系统中两种常见的指令和数据存储方式。
冯·诺依曼架构:将指令和数据存储在同一个存储器中,并使用同一套总线进行数据传输。指令和数据必须按顺序在存储器中存储,并且通过共享的数据总线进行传输,指令必须按照严格的顺序执行,无法并行执行多条指令。优势在于其简洁性、通用性和灵活性,使得计算机能够执行不同类型的任务,并支持存储程序的概念,使得程序可以被修改和更新。
哈佛架构:指令存储器和数据存储器是物理上分开的,使用不同的总线进行数据传输。哈佛架构对硬件的要求更高,因为需要独立的指令和数据存储器。哈佛架构的优势在于其并行性和高效性。由于指令存储器和数据存储器分开,指令和数据可以同时进行读取和存储。这种并行访问使得计算机系统能够更高效地执行指令和处理数据,从而提高了系统的性能和吞吐量。
02.存储器分类
-
寄存器:通常是一个非常快速的、特殊的存储器,用于在CPU内部存储数据和指令。它位于CPU芯片内部,是CPU的一部分。寄存器的容量很小,通常只有几十个字节,因此它只能存储少量的数据。寄存器中存储的数据是二进制格式的,即由0和1组成的数字序列。这种格式非常适合CPU内部的处理,因为它可以直接传递给CPU中的算术逻辑单元(ALU)进行计算。
-
内存:通常是一个较慢的、可扩展的存储器,用于在计算机系统中存储大量的数据和程序。内存的容量很大,通常可以存储几GB的数据。内存中存储的数据可以是多种格式,包括文本、数字、图像、音频和视频等,这些数据格式不一定是二进制的。
04.Cache一致性问题
当系统中的多个处理器私有Cache存在共享数据的副本时,可能出现副本内容不一致的情况,即:在系统运行的某个时刻,相同的存储器位置(物理地址)在不同的处理器视图中具有不同的值。多个处理器或多个核心的计算机系统中,它们共享同一个内存区域时,保证每个处理器或核心的缓存中存储的数据是一致的。当一个处理器或核心修改共享内存中的数据时,它必须通知其他处理器或核心,以便它们更新自己的缓存。
05.SRAM、DRAM、SDRAM
(1)SRAM:静态的随机存储器,加电情况下,不需要刷新,数据不会丢失,CPU的缓存就是SRAM。 (2)DRAM:动态随机存储器,加电情况下,也需要不断刷新,才能保存数据,最为常见的系统内存。 (3)SDRAM:同步动态随机存储器,即数据的读取需要时钟来同步,也可用作内存。
06.存储单位
1B=8bit 2^10B=1024B=1KB 2^20B=1024KB=1MB 2^30B=1024MB=1GB
04.数据结构
01.顺序表和链表
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间 | 物理上一定连续 | 逻辑上连续,物理上不一定连续 |
随机访问 | 可以顺序也可以随机 | 只能从头顺序存取 |
任意位置插入或者删除 | 可能移动元素,效率低 | 修改指针指向 |
插入 | 动态顺序表,空间不够增容 | 没有容量概念 |
应用场景 | 高效存储+频繁访问 | 任意位置插入删除频繁 |
缓存利用率 | 高 | 低 |
02.链表有环
代码随想录 (programmercarl.com)
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: ListNode *detectCycle(ListNode *head) { ListNode* fast = head; ListNode* slow = head; while(fast != NULL && fast->next != NULL) { slow = slow->next; fast = fast->next->next; // 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇 if (slow == fast) { ListNode* index1 = fast; ListNode* index2 = head; while (index1 != index2) { index1 = index1->next; index2 = index2->next; } return index2; // 返回环的入口 } } return NULL; } };
03.循环队列FIFO
C语言构建环形缓冲区_环形缓冲区 c语言 实现-CSDN博客
循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。而对于循环队列来说,可以重复利用使用过的空间。解决了普通队列的问题。
typedef struct
{
void *buffer;
int front; // 队头指针
int rear; // 队尾指针
int capacity; // 队列容量
int elementSize; // 元素大小
} FIFO;
void initFIFO(FIFO* fifo, int capacity, int elementSize) {
fifo->buffer = malloc(capacity * elementSize);
fifo->front = 0;
fifo->rear = 0;
fifo->capacity = capacity;
fifo->elementSize = elementSize;
}
//队头指针(front)和队尾指针(rear)指向同一个位置时,表示队列中没有元素
int isFIFOEmpty(FIFO* fifo) {
return (fifo->front == fifo->rear);
}
/*当队列已满时,即队尾指针(rear)下一个位置是队头指针(front)时,表示队列中已经存满了元素。*/
int isFIFOFull(FIFO* fifo) {
return ((fifo->rear + 1) % fifo->capacity == fifo->front);
}
void enqueueFIFO(FIFO* fifo, void* data) {
if (isFIFOFull(fifo)) {
// printf("FIFO is full. Cannot enqueue.\n");
return;
}
void* dest = (char*)fifo->buffer + fifo->rear * fifo->elementSize;//计算出队列队尾的地址
memcpy(dest, data, fifo->elementSize);//将数据复制到该地址处
fifo->rear = (fifo->rear + 1) % fifo->capacity; // 更新队尾指针
}
int dequeueFIFO(FIFO* fifo, void* data) {
if (isFIFOEmpty(fifo)) {
// printf("FIFO is empty. Cannot dequeue.\n");
return 0;
}
void* src = (char*)fifo->buffer + fifo->front * fifo->elementSize;
memcpy(data, src, fifo->elementSize);
fifo->front = (fifo->front + 1) % fifo->capacity; // 更新队头指针
return 1;
}
03.二叉树分类
二叉树各种类型汇总_二叉树分类-CSDN博客
struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode(int x) : val(x), left(NULL), right(NULL) {} };
-
二叉树:每个节点最多有两个子树的树状结构,是n个节点的集合;度<=2;左子树和右子树是有顺序的,次序不能任意颠倒;
-
完全二叉树:除最后一层外,其余层节点数均达到最大值,并且最后一层的节点都集中在左侧;如果结点度为1,则该结点只有左孩子,没有右子树;同样结点数目的二叉树,完全二叉树深度最小;
-
满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树;叶子只能出现在最下一层;非叶子结点的度(
结点
拥有的子树数目
称为结点的度
)一定是2; -
二叉搜索树:若它的左子树不空,则左子树上的所有结点的值均小于根节点的值;若它的右子树不空,则右子树上的所有结点的值均大于根节点的值,左右子树分别为二叉排序树。
-
平衡二叉树:是二叉查找树的一个进化体,它有几种实现方式:
红黑树
、AVL树
,它是一个空树或它的左右两个子树的高度差的绝对值不超过1
,并且左右两个子树都是平衡二叉树,如果插入或者删除一个节点使得高度之差大于1,就要进行节点之间的旋转,将二叉树重新维持在一个平衡状态。插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)
04.二叉树的性质
【数据结构】之二叉树的5个性质_二叉树的性质-CSDN博客
-
二叉树第
i
层最多有2^(i-1)
个节点(i>=1)
-
深度为
k
的二叉树节点数最多为(2^k)-1
个 -
度为
i
的节点数为ni
,则n0=n2+1
-
具有
n
个节点的完全二叉树深为log(2x+1)
(其中x表示不大于n的最大整数)。 -
如果对一颗有n个结点的完全二叉树(其深度为[log2n]+1)的结点按层序编号(从第一层到[log2n]+1层,每层从左到右),对任一结点i(1<=i<=n)
(1)如果i=1,则结点i是二叉树的根,无双亲,如果i>1,则其双亲结点是结点[i/2] (2)如果2i>n,则结点i无左孩子(结点i为叶子结点)否则左孩子是结点2i。 (3)如果2i+1>n,则结点i无右孩子,否则其右孩子是结点2i+1.
-
二叉树的查找时间复杂度取决于树的类型和其平衡性。在平衡二叉树(如 AVL 树、红黑树等)中,查找的时间复杂度通常为 O(log n),其中 n 是树中节点的数量。这是因为平衡二叉树保持了树的平衡,使得树的高度保持在较低的水平上,从而保证了较快的查找时间。
然而,在非平衡的二叉树中,查找时间的复杂度可能会达到 O(n),其中 n 是树中节点的数量。在最坏情况下,非平衡二叉树可能会退化成链表,导致每次查找都需要遍历整个链表,时间复杂度为 O(n)。
05.什么是二叉树的退化
二叉树的退化指的是一棵二叉树变成类似链表的情况,也称为二叉树的退化为链表(Degenerate Binary Tree)。在这种情况下,二叉树的每个非叶子节点都只有一个子节点(要么左子节点,要么右子节点),而且所有的节点都沿着同一个方向排列,形成了一条线性结构。
二叉树的退化可能会导致树的高度(深度)变得很大,接近于节点的数量,这样会增加在树上进行搜索、插入、删除等操作的时间复杂度,使得树的性能下降。退化的二叉树失去了二叉树本身的优势,无法充分利用二叉树的快速查找和插入特性,甚至变得和单链表一样效率低下。
二叉树退化为链表的情况通常发生在以下情况下:
-
在插入节点时不平衡地选择节点的位置。
-
在特定的插入顺序下,比如按照升序或降序插入节点。
-
在某些特殊情况下,比如树的平衡性受到破坏时。
为了避免二叉树的退化,可以采取一些措施,如使用平衡二叉树(如 AVL 树、红黑树)或者在构建二叉树时保持平衡,以确保树的高度尽可能小,提高树的性能和效率。
二叉树遍历
06.图论
图(Graph)——图G是由顶点的有穷非空集合和顶点之间边的集合组成,其中:V(G)是顶点的非空有限集,E(G)是边的有限集合,边是顶点的无序对或有序对
数据结构——图的五种种类【无向图-有向图-简单图-完全无向图-有向完全图】_有向图和无向图的区别-CSDN博客
07.图的存储结构
邻接矩阵法——表示顶点间相联关系的矩阵,矩阵的第i行第j列表示i到j是否连接。
-
无向图的邻接矩阵对称,可压缩存储;有n个顶点的无向图需存储空间为n(n+1)/2;
-
有向图邻接矩阵不一定对称;有n个顶点的有向图需存储空间为n²;
-
无向图中顶点Vi的度TD(Vi)是邻接矩阵A中第i行元素之和;
-
有向图中, 顶点Vi的出度是A中第i行元素之和; 顶点Vi的入度是A中第i列元素之和;
邻接表法:为图中每个顶点建立一个单链表,第i个单链表中的结点表示依附于顶点Vi的边(有向图中指以Vi为尾的弧)
08.求最短路径算法
最短路径的四种算法_最短路径算法-CSDN博客
-
Floyd
-
Dijkstra 算法
09.排序算法
六大排序算法:插入排序、希尔排序、选择排序、冒泡排序、堆排序、快速排序_冒泡排序,快速排序,希儿排序,堆排序 需要额外辅助空间-CSDN博客
/*
O(N^2):冒泡,插入,选择
O(N^1.3):希尔
O(NlogN):快排,堆排
*/
void BubbleSort(int*arr,int size){
for(int i=0;i<size-1;i++){//size-1是因为不用与自己比较,所以比的数就少一个
for(int j=0;j<size-i-1;j++){//size-1-i是因为每一趟就会少一个数比较
if(arr[j]>arr[j+1]){//这是升序排法,所有数中最大的那个数就会浮到最右边
swap(arr[j],arr[j+1]);
}
}
}
}
//最坏情况下为O(N^2),此时待排序列为逆序,或者说接近逆序;最好情况下为O(N),此时待排序列为升序,或者说接近升序。
void insertionSort(int arr[], int n) {
int i, j, key;
for (i = 1; i < n; i++) {
key = arr[i];
j = i - 1;
// 将比key大的元素向后移动
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
//选择排序算法是通过遍历数组,选择出数组的最小或最大值,与指定位置交换数据,遍历完整个数组的所有位置就完成排序
void selectionSort(int arr[], int n) {
int i, j, min_index;
for (i = 0; i < n-1; i++) {
min_index = i;
// 寻找最小元素的下标
for (j = i+1; j < n; j++) {
if (arr[j] < arr[min_index]) {
min_index = j;
}
}
// 将最小元素与当前位置交换
int temp = arr[i];
arr[i] = arr[min_index];
arr[min_index] = temp;
}
}
/*希尔排序实质上是一种分组插入方法。它的基本思想是:对于 n 个待排序的数列,取一个小于 n 的整数 gap ( gap 被称为步长)将待排序元素分成若干个组子序列,所有距离为 gap 的倍数的记录放在同个一组中;然后,对各组内的元素进行直接插入排序。这一趟排序完成之后,每一个组的元素都是有序的。然后减小 gap 的值,并重复执行上述的分组和排序。重复这样的操作,当 gap =1时,整个数列就是有序的。
时间复杂度平均:O(N^1.3)
*/
void shellSort(int arr[], int n) {
int gap, i, j, temp;
for (gap = n/2; gap > 0; gap /= 2) {
// 对每个分组进行插入排序
for (i = gap; i < n; i++) {
temp = arr[i];
j = i;
// 对分组中的元素进行插入排序
while (j >= gap && arr[j-gap] > temp) {
arr[j] = arr[j-gap];
j -= gap;
}
arr[j] = temp;
}
}
}
10.哈希表
其通过建立键 key 与值 value 之间的映射,实现高效的元素查询。
哈希表是根据关键码的值而直接进行访问的数据结构。一般哈希表都是用来快速判断一个元素是否出现集合里。
std::set
适用于需要存储唯一元素且无序的场景,而 std::map
则适用于存储键值对且需要根据键排序的场景。
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O(logn) | O(logn) |
std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O(log n) | O(log n) |
std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O(1) | O(1) |
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。
11.哈希冲突
数据结构之----哈希表、哈希冲突、哈希算法-CSDN博客
指多个输入对应同一输出的情况,哈希函数的输入空间远大于输出空间,因此理论上哈希冲突是不可避免的
解决办法
-
扩容哈希表:需将所有键值对从原哈希表迁移至新哈希表,非常耗时
-
改良哈希表数据结构:链式地址 和 开放寻址
-
仅在必要时,即当哈希冲突比较严重时,才执行扩容操作。