2022年面试准备
2022面试题总结归纳——三年工作经验
语言
PHP
PHP7 性能提升的原因
- 存储变量的结构体(ZVAL)变小(原来24现在16),尽量使结构体里成员共用内存空间,减少引用,这样内存占用降低,变量的操作速度得到提升。
- 字符串结构体(zend_string)的改变,字符串信息和数据本身原来是分成两个独立内存块存放,php7 尽量将它们存入同一块内存,提升了 cpu 缓存命中率。这是一种称为 C struct hack 的内存优化。基本上,它允许引擎为 zend_string 结构和要存储的字符分配空间,作为一个单独的 C 指针。这优化了内存,因为内存访问将是一个连续分配的块,而不是两个分散的块(一个用于存储 zend_string *,另一个用于存储 char *)。
- 数组结构的改变(由 hashtable 变为 zend array),数组元素和 hash 映射表在 php5 中会存入多个内存块,php7 尽量将它们分配在同一块内存里,降低了内存占用、提升了 cpu 缓存命中率。
- 改进了函数的调用机制,通过对参数传递环节的优化,减少一些指令操作,提高了执行效率。
PHP 启动方式
php-fpm 的生命周期:
- 初始化模块。
- 初始化请求,指的是请求 PHP 代码的意思。
- 执行 PHP 脚本。
- 结束请求。
- 关闭模块。
性能慢的原因:并发靠多进程,单个进程只能处理一个连接,会阻塞下面的请求。启动大量进程太过占用服务器资源。
swoole 的生命周期:
- 初始化模块。
- 初始化请求,cli 方式运行,不会初始化 PHP 全局变量,如 $_SERVER, $_POST, $_GET 等。
- 执行 PHP 脚本,master 进入监听状态,不会结束进程。
高性能的原因:常驻进程节省 PHP 代码初始化的时间,由 Reactor(epoll 的 IO 多路复用)负责监听 socket 句柄的事件变化,解决高并发问题。
对比不同:
PHP-FPM
- Master 主进程 / Worker 多进程模式。
- 启动 Master,通过 FastCGI 协议监听来自 Nginx 传输的请求。
- 每个 Worker 进程只对应一个连接,用于执行完整的 PHP 代码。
- PHP 代码执行完毕,占用的内存会全部销毁,下一次请求需要重新再进行初始化等各种繁琐的操作。
- 只用于 HTTP Server。
Swoole
- Master 主进程(由多个 Reactor 线程组成)/ Worker 多进程(或多线程)模式
- 启动 Master,初始化 PHP 代码,由 Reactor 监听 Socket 句柄的事件变化。
- Reactor 主线程负责子多线程的均衡问题,Manager 进程管理 Worker 多进程,包括 TaskWorker 的进程。
- 每个 Worker 接受来自 Reactor 的请求,只需要执行回调函数部分的 PHP 代码。
- 只在 Master 启动时执行一遍 PHP 初始化代码,Master 进入监听状态,并不会结束进程。
- 不仅可以用于 HTTP Server,还可以建立 TCP 连接、WebSocket 连接。
PHP 垃圾回收
引用计数。
参考:https://www.php.cn/topic/php7/449594.html
内存分配堆与栈的区别
- 管理方式不同。栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;
- 空间大小不同。每个进程拥有的栈的大小要远远小于堆的大小。理论上,程序员可申请的堆大小为虚拟内存的大小,进程栈的大小 64bits 的 Windows 默认 1MB,64bits 的 Linux 默认 10MB;
- 生长方向不同。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。
- 分配方式不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由 alloca 函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由操作系统进行释放,无需我们手工实现。
- 分配效率不同。栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是由 C/C++ 提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。
- 存放内容不同。栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等。当主函数调用另外一个函数的时候,要对当前函数执行断点进行保存,需要使用栈来实现,首先入栈的是主函数下一条语句的地址,即扩展指针寄存器的内容(EIP),然后是当前栈帧的底部地址,即扩展基址指针寄存器内容(EBP),再然后是被调函数的实参等,一般情况下是按照从右向左的顺序入栈,之后是被调函数的局部变量,注意静态变量是存放在数据段或者 BSS 段,是不入栈的。出栈的顺序正好相反,最终栈顶指向主函数下一条语句的地址,主程序又从该地址开始执行。堆,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的。
从以上可以看到,堆和栈相比,由于大量 malloc()/free() 或 new/delete 的使用,容易造成大量的内存碎片,并且可能引发用户态和核心态的切换,效率较低。栈相比于堆,在程序中应用较为广泛,最常见的是函数的调用过程由栈来实现,函数返回地址、EBP、实参和局部变量都采用栈的方式存放。虽然栈有众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,主要还是用堆。
参考:https://phpmianshi.com/?id=32
Swoole 协程和 Go 协程的区别
- swoole 协程环境必须在上下文中声明,go 协程是语言层面的,无需声明。
- swoole 基于单线程,无法利用多核 CPU。
PHP 数组实现
PHP 万物皆数组,数组功能强大:
- 可以使用数字或者字符串作为 key
- 可以用 foreach 顺序读取数组
- 可以随机读取
- 数组可扩容
PHP 数组的底层实现是散列表(Bucket 即储存元素的数组),随机查找的时间复杂度是 O(1)。先将 key 通过 Time 33
算法转为一个整形的下标(在中间隐射表上),然后通过下标从 Bucket 中读取。
为了实现顺序读取,PHP 维护了一张中间映射表,用于保存元素实际储存的 value 在 Bucket 中的下标(整形)。Bucket 中的数据是有序的,而中间映射表中的数据是无序的。这样顺序读取时只需要访问 Bucket 中的数据即可。
参考:https://juejin.cn/post/6844903696988373005
Go 数组是固定大小的,切片是可以扩容的。
GO
Go 垃圾回收
标记清除。
参考:https://geektutu.com/post/qa-golang-2.html
通道
一、无缓冲通道
ch = make(chan string) // 发送阻塞直到数据被接收,接收阻塞直到读到数据。
二、有缓冲通道
ch = make(chan string, 3) // 缓冲满时发送阻塞,缓冲空时接收阻塞。
Go 并发机制以及它所使用的 CSP 并发模型
Goroutine 是 Go 并发的实体,底层由协程(coroutine)实现。协程具有一下特点:
- 用户空间,避免了内核态和用户态的切换导致的成本。
- 可以由语言和框架层进行调度。
- 更小的栈空间允许创建大量的实例。
Go 内部有三个对象:
- P(Processor)代表上下文(或者可以认为是 CPU),用来调度 M 和 G 之间的关联关系,数量默认为核心数。
- M(Machine)代表工作线程,数量对应真实的 CPU 数。
- G(Goroutine)代表协程,每个协程对象中 sched 保存着其上下文的信息。
正常情况下一个 CPU 对象启动一个工作线程对象,线程去检查并执行 goroutine 对象。碰到 goroutine 对象阻塞的时候,会启动一个新的工作线程,可以充分利用 CPU 资源。所以有时候线程对象会比处理器对象多很多。
Go 并发模型
一、channel
func main() {
ch := make(chan struct{})
go func() {
fmt.Println("start working")
time.Sleep(time.Second * 1)
ch <- struct{}{}
}()
<-ch
fmt.Println("finished")
}
二、WaitGroup
func main() {
var wg sync.WaitGroup
var nums = []int{1, 2, 3}
for _, num := range nums {
wg.Add(1) // 添加 goroutine 的数量,此处相当于++
go func(num int) {
defer wg.Done() // 相当于--
fmt.Println(num)
time.Sleep(time.Second * 1)
}(num)
}
wg.Wait() // 执行后会堵塞主线程,直到 WaitGroup 里的值减至0
}
三、Context
type Context interface {
// Done returns a channel that is closed when this `Context` is canceled
// or times out.
Done() <-chan struct{}
// Err indicates why this Context was canceled, after the Done channel
// is closed.
Err() error
// Deadline returns the time when this Context will be canceled, if any.
Deadline() (deadline time.Time, ok bool)
// Value returns the value associated with key or nil if none.
Value(key interface{}) interface{}
}
Context 对象是线程安全的,你可以把一个 Context 对象传递给任意个数的 gorotuine,对它执行取消操作时,所有 goroutine 都会接收到取消信号。一个 Context 不能拥有 Cancel 方法,同时我们也只能 Done channel 接收数据。 其中的原因是一致的:接收取消信号的函数和发送信号的函数通常不是一个。 典型的场景是:父操作为子操作操作启动 goroutine,子操作也就不能取消父操作。
Go 有哪些方式安全读写共享变量
一、通道:chan
var (
sema = make(chan struct{}, 1) // 用来保护 balance 的通道
balance int
)
func Deposit(amount int) {
sema <- struct{}{} // 获取令牌
balance = balance + amount
<-sema // 释放令牌
}
func Balance() int {
sema <- struct{}{} // 获取令牌
b := balance
<-sema // 释放令牌
return b
}
二、互斥锁:sync.Mutex
import "sync"
var (
mu sync.Mutex
balance int
)
func Deposit(amount int) {
mu.Lock()
balance = balance + amount
mu.Unlock()
}
func Balance() int {
mu.Lock()
b := balance
mu.Unlock()
return b
}
死锁的条件,如何避免
- 同一个线程先后两次调用 lock,在第二次调用的时候,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着,没有机会释放,所以产生了死锁。
- 线程 A 获得了锁1,线程 B 获得了锁2,这时 A 调用 lock 试图获得锁2,这时候需要线程 B 释放锁2,而这时线程 B 也调用 lock 试图获得锁1,于是产生了死锁。
- 协程不配对,数据要发送,但是没有人接收,数据要接收,但是没有人发送。
goroutine 泄露
https://juejin.cn/post/6987561570184200223
数据库
MySQL
事务的实现原理
A 原子性(Atomicity)
- undo log 回滚日志
C 一致性 (Consistency)是通过原子性,持久性,隔离性来实现的
I 隔离性(Isolation)
- 锁
- MVCC
- undo log
D 持久性(Durability)
- redo log 重做日志
索引的实现原理
B+Tree N 差不多是1200,树高是4的时候,可以存储1200的三次方,约等于17亿。根节点在内存里,访问只需要3次磁盘 IO。查找的时间复杂度是 O(log(N)),更新的时间复杂度也是 O(log(N))。
MySQL 如何保证数据不丢失
假设有 A、B 两个数据,值分别是 1 和 2,在一个事务中先后把 A 设置为 3,B 设置为 4。
- 事务开始
- 记录 A = 1 到 undo log buffer
- 修改 A = 3
- 记录 A = 3 到 redo log buffer
- 记录 B = 2 到 undo log buffer
- 修改 B = 4
- 记录 B = 4 到 redo log buffer // 在这里崩溃,因为没有持久化到磁盘,所以数据还是事务开始之前的状态
- sync undo log buffer to disk // 先写 undo 日志,如果在 9-10 崩溃,可以回滚。
- sync redo log buffer to disk
- 提交事务
两阶段提交
- 数据页是否在内存中?在的话直接返回行数据,不在的话从磁盘读入内存再返回行数据。
- 修改数据更新到内存。
- redo log prepare: write
- binlog: write
- redo log prepare: fsync
- binlog: fsync
- redo log commit: write
写 binlog 是分两步的:
- 先把 binlog 从 binlog cache 中写到磁盘上的 binlog 文件;
- 调用 fsync 持久化。
如果想要提升 binlog 组提交的效果,可以通过设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 来实现。
- binlog_group_commit_sync_delay 参数,表示延迟多少微秒后才调用 fsync;
- binlog_group_commit_sync_no_delay_count 参数,表示累积多少次以后才调用 fsync。
这两个条件是或的关系,也就是说只要有一个满足条件就会调用 fsync。
所以,当 binlog_group_commit_sync_delay 设置为0的时候,binlog_group_commit_sync_no_delay_count 也就无效了。
MySQL 为什么不建议单表数据量超过千万
Innodb 存储引擎的最小存储单元是页,每一页的大小为16K,页可以用于存放数据也可以用于存放键值+指针,在 B+ 树中叶子节点存放数据,非叶子节点存放键值+指针,mysql 的指针大小一般是6个字节,索引大小一般是8个字节,指针大小为6个字节,一行记录的数据大小为1K,一页能存储16条数据,1170(16K / 14)个指针。
树高为2时,可存储 1170 * 16 = 18720 条数据。
树高为3时,可存储 1170 * 1170 * 16 = 21902400 条数据。
所以如果单表数据量超过2000万,树高会变成4,会额外增加 IO 次数。
AB 转账发生死锁的条件,如何避免
主从同步延迟的原因
- 网络,建议同机房。
- 硬件,建议同等配置。
- 大事务。
- 备库压力太大,影响了写能力,建议一主多备。
- 主库写太多,备库跟不上,建议组提交延迟,能提升备库复制并发度。
并行复制策略
- COMMIT_ORDER(默认),表示的就是前面介绍的,根据同时进入 prepare 和 commit 来 判断是否可以并行的策略。
- WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的 hash 值,组成集合 writeset。如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行。
- writeset 是在主库生成后直接写入到 binlog 里面的,这样在备库执行的时候,不需要解析 binlog 内容(event 里的行数据),节省了很多计算量;
- 不需要把整个事务的 binlog 都扫一遍才能决定分发到哪个 worker,更省内存;
- 由于备库的分发策略不依赖于 binlog 内容,所以 binlog 是 statement 格式也是可以的;
- 对于“表上没主键”和“外键约束”的场景,WRITESET 策略也是没法并行的,也会暂时退化为单线程模型。
- WRITESET_SESSION,是在 WRITESET 的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。
主库单线程,备库想要快速追上,应该用哪个策略?
针对三种不同的策略,COMMIT_ORDER:没有同时到达 redo log 的 prepare 状态的事务,备库退化为单线程;WRITESET:通过对比更新的事务是否存在冲突的行,可以并发执行;WRITE_SESSION:在WRITESET 的基础上增加了线程的约束,则退化为单线程。综上,应选择 WRITESET 策略。
Join 为什么建议小表驱动大表?
小表 t1 m 行,大表 t2 n 行。
Index Nested-Loop Join(被驱动表有索引)
扫描行数 m + m * log2n。(用小表驱动好)
Simple Nested-Loop Join(被驱动表没有索引)×
扫描行数 m * n。
Block Nested-Loop Join(被驱动表没有索引)
把 t1 数据都读入 join_buffer,扫描表 t2,把表 t2 中的每一行取出来,跟 join_buffer 中的数据做对比,满足 join 条件的,作为结果集的一部分返回。扫描行数 m + n。如果内存不够的话,把 m 分成 k(k=λ*m) 段,扫描的行数是 m + k * n。(join_buffer_size 足够大,都一样,不够大,用小表驱动好)
唯一索引和普通索引的区别
唯一索引:
-
查找
普通索引会一直找到不满足的记录为止,唯一索引找到第一个就会停止检索。(性能差别不大)
-
更新
-
更新目标在内存中
普通索引直接插入,唯一索引插入之前先判断有没有冲突。(差别不大)
-
更新目标不在内存中
普通索引将更新写入 change buffer,结束。
唯一索引需将数据页读入内存,判断有没有冲突,再插入值。
-
change buffer:
change buffer 用的是 buffer pool 里的内存,通过 innodb_change_buffer_max_size 参数来控制。当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InooDB 会将这些更新操作缓存在 change buffer 中(会写入 redo log),这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。(如果一写就要读,那么会频繁地 merge,影响性能)
为什么 MySQL 的默认隔离级别是 Repeatable Read
从 5.7.22 版本开始,MySQL binlog 的默认格式由 STATEMENT 改为了 ROW。
STATEMENT 是写修改的 sql 语句,ROW 是写修改的行数据(日志量最大)。
binlog 都是事务提交才写日志的,在 RC 级别下,STATEMENT 格式在主从同步下会有 bug。
session1 | session2 |
---|---|
begin; | begin; |
delete from user where id <= 3; | |
insert into user values(1, 'Rick'); | |
insert into user values(2, 'Morty'); | |
commit; | |
commit; |
在主库上,Rick and Morty 是存在的,因为 session1 删除的时候 session2 还没有提交,所以不会删除不可见的数据。而日志是根据 commit 来写的,也就是说从库上会先执行 insert 再执行 delete,那么 Rick and Morty 就无了。这就造成了主从数据不一致的问题。那么,为什么在 RR 级别下,不会有这样的问题?
因为在 RR 下,session1 在 delete 的时候会锁住间隙(id = 1, id = 2, id = 3),那么 session2 insert 语句会阻塞,直至 session1 commit。因此,binlog 日志是先删后插,与主库一致。
所以要么 STATEMENT + RR
,要么 ROW + RC
。使用后者可以减少加锁的粒度,提高并发。
InnoDB 和 MyISAM 的区别
- InnoDB 支持事务,MyISAM 不支持,MyISAM 没有 redo log。
- InnoDB 支持行锁,MyISAM 只支持表锁。
- InnoDB 支持外键,而 MyISAM 不支持。
- InnoDB 是聚集索引,MyISAM 是非聚集索引,都是 B+Tree,InnoDB 的主键索引的叶子节点就是数据文件,辅助索引的叶子节点是主键的值;而 MyISAM 的主键索引和辅助索引的叶子节点都是数据文件的地址指针。
缓存
Redis
消息队列
Kafka
网络协议
HTTP 和 HTTPS 的区别
HTTP 1.1 和 HTTP 2.0 的区别
TCP 和 UDP 的区别
数据结构与算法
数据结构
数组
栈、队列
链表
散列表
树
堆
<?php
/**
* 堆是一个完全二叉树
* 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值
*/
class Heap
{
public $arr = [];
public $count; // 堆中已经存储的数据个数
public function insert($data)
{
$this->count++;
$this->arr[$this->count] = $data;
$n = $this->count;
while (intval($n / 2) > 0 && $this->arr[$n] > $this->arr[intval($n / 2)]) {
[$this->arr[$n], $this->arr[intval($n / 2)]] = [$this->arr[intval($n / 2)], $this->arr[$n]];
$n = intval($n / 2);
}
}
public function removeMax()
{
if (!$this->count) return;
$this->arr[1] = $this->arr[$this->count];
unset($this->arr[$this->count]);
$this->count--;
$this->heapify($this->arr, $this->count, 1); // 从上到下堆化
}
/**
* 堆化
*/
public function heapify(array &$arr, $n, $i)
{
while (true) {
$maxPos = $i;
if ($arr[$i * 2] > $arr[$i] && $i * 2 <= $n) $maxPos = $i * 2;
if ($arr[$i * 2 + 1] > $arr[$maxPos] && $i * 2 + 1 <= $n) $maxPos = $i * 2 + 1;
if ($i == $maxPos) break;
[$arr[$i], $arr[$maxPos]] = [$arr[$maxPos], $arr[$i]];
$i = $maxPos;
}
}
}
$h = new Heap();
$h->insert(1);
$h->insert(15);
$h->insert(6);
$h->insert(6);
$h->insert(16);
$h->insert(8);
$h->removeMax();
print_r($h->arr);
图
<?php
/**
* 邻接表实现
*/
class Node
{
public $data;
public $next = [];
public function __construct($data = null)
{
$this->data = $data;
}
}
class Graph
{
public $graph = [];
public function insertVertex($vertex, $advtex)
{
$this->graph[$vertex][] = $advtex;
}
public function createGraph()
{
$vertexes = array_keys($this->graph);
$res = [];
foreach ($vertexes as $vertex) {
$res[$vertex] = new Node($vertex);
}
foreach ($this->graph as $vertex => $advtexes) {
foreach ($advtexes as $advtex) {
$res[$vertex]->next[] = $res[$advtex];
print_r($res);
}
}
return $res;
}
}
$vertexes = ['a', 'b', 'c', 'd'];
$g = new Graph();
$g->insertVertex('a', 'b');
$g->insertVertex('a', 'c');
$g->insertVertex('b', 'a');
$g->insertVertex('c', 'b');
$g->insertVertex('c', 'd');
$g->insertVertex('d', 'a');
$g->insertVertex('d', 'b');
$g->insertVertex('d', 'c');
$g->createGraph();
排序算法
冒泡排序
function bubbleSort(array &$arr)
{
$n = count($arr);
for ($i = 0; $i < $n; $i++) {
for ($j = $i + 1; $j < $n; $j++) {
if ($arr[$i] > $arr[$j]) {
[$arr[$i], $arr[$j]] = [$arr[$j], $arr[$i]];
}
}
}
}
选择排序
function selectSort(array &$arr)
{
$n = count($arr);
for ($i = 0; $i < $n - 1; $i++) {
$min = $i;
for ($j = $i + 1; $j < $n; $j++) {
if ($arr[$j] < $arr[$min]) {
$min = $j;
}
}
[$arr[$i], $arr[$min]] = [$arr[$min], $arr[$i]];
}
}
插入排序
function insertSort(array &$arr)
{
$n = count($arr);
for ($i = 1; $i < $n; $i++) {
$value = $arr[$i];
for ($j = $i - 1; $j >= 0; $j--) {
if ($arr[$j] > $value) {
$arr[$j + 1] = $arr[$j];
} else {
break;
}
}
$arr[$j + 1] = $value;
}
}
归并排序
function mergeSort(&$arr)
{
$n = count($arr);
$mid = intval($n / 2);
if ($n <= 1) return $arr;
$left = array_slice($arr, 0, $mid);
$right = array_slice($arr, $mid);
$left = mergeSort($left);
$right = mergeSort($right);
return _merge($left, $right);
}
function _merge(array $left, array $right)
{
$tmp = [];
while (count($left) && count($right)) {
$tmp[] = $left[0] < $right[0] ? array_shift($left) : array_shift($right);
}
return array_merge($tmp, $left, $right);
}
快速排序
/**
* 递归实现
*/
function quickSort(array &$arr)
{
$n = count($arr);
_quickSort($arr, 0, $n - 1);
}
function _quickSort(array &$arr, $l, $r)
{
if ($l >= $r) return;
$q = parition($arr, $l, $r);
_quickSort($arr, $l, $q - 1);
_quickSort($arr, $q + 1, $r);
}
function parition(array &$arr, $l, $r)
{
$pivot = $arr[$r];
$i = $l;
for ($j = $l; $j < $r; $j++) { // $arr[$j] 比 $pivot 大
if ($arr[$j] < $pivot) { // 不满足,i j 换位置
[$arr[$i], $arr[$j]] = [$arr[$j], $arr[$i]];
$i++;
}
}
[$arr[$i], $arr[$r]] = [$arr[$r], $arr[$i]]; // i r 换位置,这样左边的都比 r 小,右边的都比 r 大
return $i;
}
/**
* 非递归实现
*/
function unRecursiveQuickSort(array &$arr)
{
$n = count($arr);
$l = 0;
$r = $n - 1;
$tmp = [];
if (true) {
array_push($tmp, $r);
array_push($tmp, $l);
while (!empty($tmp)) {
$l = array_pop($tmp);
$r = array_pop($tmp);
$q = parition($arr, $l, $r);
if ($l < $q - 1) {
array_push($tmp, $q - 1);
array_push($tmp, $l);
}
if ($r > $q + 1) {
array_push($tmp, $r);
array_push($tmp, $q + 1);
}
}
}
}
计数排序
function countSort(array &$arr)
{
$n = count($arr);
$max = max($arr);
$tmp = [];
for ($i = 0; $i <= $max; $i++) {
$tmp[$i] = 0;
}
for ($i = 0; $i < $n; $i++) {
$tmp[$arr[$i]]++;
}
for ($i = 1; $i <= $max; $i++) {
$tmp[$i] += $tmp[$i - 1];
}
$r = [];
for ($i = $n - 1; $i >= 0; $i--) {
$value = $arr[$i];
$r[$tmp[$value] - 1] = $value;
$tmp[$value]--;
}
for ($i = 0; $i < $n; $i++) {
$arr[$i] = $r[$i];
}
}
其他
链表成环怎么找到结点
A->B->C->D->B-C->D
一、穷举遍历
依次遍历单链表的每一个节点。每遍历到一个新节点,就从头节点重新遍历新节点之前的所有节点,用新节点 ID 和此节点之前所有节点 ID 依次作比较。如果发现新节点之前的所有节点当中存在相同节点 ID ,则说明该节点被遍历过两次,链表有环。
A => [A]
B => [A, B]
C => [A, B, C]
D => [A, B, C, D]
B => [A, B, C, D, B] // B 重复,所以 D => B 成环。
二、快慢指针
首先创建两个指针1和2,同时指向这个链表的头节点。然后开始一个大循环,在循环体中,让指针1每次向下移动一个节点,让指针2每次向下移动两个节点,然后比较两个指针指向的节点是否相同。如果相同,则判断出链表有环。
1 => A, 2 => A
1 => B, 2 => C
1 => C, 2 => B
1 => D, 2 => D // 2追上了1,则说明 D => B 成环。
链表反转
public function reverse()
{
$current = $this->head;
$pre = null;
while ($current->next) {
$tmp = $current->next;
$current->next = $pre;
$pre = $current;
$current = $tmp;
}
$current->next = $pre;
return $current;
}
100G 的文件怎么找到重复行(1G 内存)
遍历文件,将每一行进行 md5 加密再取模,结果相同的放到同一个文件里,将大文件切割成100个小文件,相同的两行必定在同一个文件里,可直接在内存中查找。
$str = 'gkjghbnjk';
$mod = crc32(md5($str)) % 100;
二分法查找
function bsearch(array $arr, $value)
{
$n = count($arr);
$low = 0;
$high = $n - 1;
while ($low <= $high) {
$mid = intval(($low + $high) / 2);
if ($arr[$mid] == $value) {
return $mid;
} elseif ($arr[$mid] > $value) {
$high = $mid - 1;
} else {
$low = $mid + 1;
}
}
return -1;
}
堆排序
/**
* 建堆
*/
public function buildHeap(array &$arr, $n)
{
for ($i = intval($n / 2); $i >= 1; $i--) {
$this->heapify($arr, $n, $i);
}
}
/**
* 堆排序
*/
public function sort(array &$arr, $n)
{
$k = $n;
while ($k > 1) {
[$arr[$k], $arr[1]] = [$arr[1], $arr[$k]];
$k--;
$this->heapify($arr, $k, 1);
}
}
$h = new Heap();
$arr = [null, 1, 15, 6, 6, 16, 8];
$n = count($arr) - 1;
$h->buildHeap($arr, $n);
$h->sort($arr, $n);
print_r($arr);