2022年面试准备

2022面试题总结归纳——三年工作经验

语言

PHP

PHP7 性能提升的原因

  1. 存储变量的结构体(ZVAL)变小(原来24现在16),尽量使结构体里成员共用内存空间,减少引用,这样内存占用降低,变量的操作速度得到提升。
  2. 字符串结构体(zend_string)的改变,字符串信息和数据本身原来是分成两个独立内存块存放,php7 尽量将它们存入同一块内存,提升了 cpu 缓存命中率。这是一种称为 C struct hack 的内存优化。基本上,它允许引擎为 zend_string 结构和要存储的字符分配空间,作为一个单独的 C 指针。这优化了内存,因为内存访问将是一个连续分配的块,而不是两个分散的块(一个用于存储 zend_string *,另一个用于存储 char *)。
  3. 数组结构的改变(由 hashtable 变为 zend array),数组元素和 hash 映射表在 php5 中会存入多个内存块,php7 尽量将它们分配在同一块内存里,降低了内存占用、提升了 cpu 缓存命中率。
  4. 改进了函数的调用机制,通过对参数传递环节的优化,减少一些指令操作,提高了执行效率。

PHP 启动方式

php-fpm 的生命周期:

  1. 初始化模块。
  2. 初始化请求,指的是请求 PHP 代码的意思。
  3. 执行 PHP 脚本。
  4. 结束请求。
  5. 关闭模块。

性能慢的原因:并发靠多进程,单个进程只能处理一个连接,会阻塞下面的请求。启动大量进程太过占用服务器资源。

swoole 的生命周期:

  1. 初始化模块。
  2. 初始化请求,cli 方式运行,不会初始化 PHP 全局变量,如 $_SERVER, $_POST, $_GET 等。
  3. 执行 PHP 脚本,master 进入监听状态,不会结束进程。

高性能的原因:常驻进程节省 PHP 代码初始化的时间,由 Reactor(epoll 的 IO 多路复用)负责监听 socket 句柄的事件变化,解决高并发问题。

对比不同:

PHP-FPM

  1. Master 主进程 / Worker 多进程模式。
  2. 启动 Master,通过 FastCGI 协议监听来自 Nginx 传输的请求。
  3. 每个 Worker 进程只对应一个连接,用于执行完整的 PHP 代码。
  4. PHP 代码执行完毕,占用的内存会全部销毁,下一次请求需要重新再进行初始化等各种繁琐的操作。
  5. 只用于 HTTP Server。

Swoole

  1. Master 主进程(由多个 Reactor 线程组成)/ Worker 多进程(或多线程)模式
  2. 启动 Master,初始化 PHP 代码,由 Reactor 监听 Socket 句柄的事件变化。
  3. Reactor 主线程负责子多线程的均衡问题,Manager 进程管理 Worker 多进程,包括 TaskWorker 的进程。
  4. 每个 Worker 接受来自 Reactor 的请求,只需要执行回调函数部分的 PHP 代码。
  5. 只在 Master 启动时执行一遍 PHP 初始化代码,Master 进入监听状态,并不会结束进程。
  6. 不仅可以用于 HTTP Server,还可以建立 TCP 连接、WebSocket 连接。

PHP 垃圾回收

引用计数。

参考:https://www.php.cn/topic/php7/449594.html

内存分配堆与栈的区别

  1. 管理方式不同。栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;
  2. 空间大小不同。每个进程拥有的栈的大小要远远小于堆的大小。理论上,程序员可申请的堆大小为虚拟内存的大小,进程栈的大小 64bits 的 Windows 默认 1MB,64bits 的 Linux 默认 10MB;
  3. 生长方向不同。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。
  4. 分配方式不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由 alloca 函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由操作系统进行释放,无需我们手工实现。
  5. 分配效率不同。栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是由 C/C++ 提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。
  6. 存放内容不同。栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等。当主函数调用另外一个函数的时候,要对当前函数执行断点进行保存,需要使用栈来实现,首先入栈的是主函数下一条语句的地址,即扩展指针寄存器的内容(EIP),然后是当前栈帧的底部地址,即扩展基址指针寄存器内容(EBP),再然后是被调函数的实参等,一般情况下是按照从右向左的顺序入栈,之后是被调函数的局部变量,注意静态变量是存放在数据段或者 BSS 段,是不入栈的。出栈的顺序正好相反,最终栈顶指向主函数下一条语句的地址,主程序又从该地址开始执行。堆,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的。

从以上可以看到,堆和栈相比,由于大量 malloc()/free() 或 new/delete 的使用,容易造成大量的内存碎片,并且可能引发用户态和核心态的切换,效率较低。栈相比于堆,在程序中应用较为广泛,最常见的是函数的调用过程由栈来实现,函数返回地址、EBP、实参和局部变量都采用栈的方式存放。虽然栈有众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,主要还是用堆。

参考:https://phpmianshi.com/?id=32

Swoole 协程和 Go 协程的区别

  1. swoole 协程环境必须在上下文中声明,go 协程是语言层面的,无需声明。
  2. swoole 基于单线程,无法利用多核 CPU。

PHP 数组实现

PHP 万物皆数组,数组功能强大:

  1. 可以使用数字或者字符串作为 key
  2. 可以用 foreach 顺序读取数组
  3. 可以随机读取
  4. 数组可扩容

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)实现。协程具有一下特点:

  1. 用户空间,避免了内核态和用户态的切换导致的成本。
  2. 可以由语言和框架层进行调度。
  3. 更小的栈空间允许创建大量的实例。

Go 内部有三个对象:

  1. P(Processor)代表上下文(或者可以认为是 CPU),用来调度 M 和 G 之间的关联关系,数量默认为核心数。
  2. M(Machine)代表工作线程,数量对应真实的 CPU 数。
  3. 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
}

死锁的条件,如何避免

  1. 同一个线程先后两次调用 lock,在第二次调用的时候,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着,没有机会释放,所以产生了死锁。
  2. 线程 A 获得了锁1,线程 B 获得了锁2,这时 A 调用 lock 试图获得锁2,这时候需要线程 B 释放锁2,而这时线程 B 也调用 lock 试图获得锁1,于是产生了死锁。
  3. 协程不配对,数据要发送,但是没有人接收,数据要接收,但是没有人发送。

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。

  1. 事务开始
  2. 记录 A = 1 到 undo log buffer
  3. 修改 A = 3
  4. 记录 A = 3 到 redo log buffer
  5. 记录 B = 2 到 undo log buffer
  6. 修改 B = 4
  7. 记录 B = 4 到 redo log buffer // 在这里崩溃,因为没有持久化到磁盘,所以数据还是事务开始之前的状态
  8. sync undo log buffer to disk // 先写 undo 日志,如果在 9-10 崩溃,可以回滚。
  9. sync redo log buffer to disk
  10. 提交事务

两阶段提交

  1. 数据页是否在内存中?在的话直接返回行数据,不在的话从磁盘读入内存再返回行数据。
  2. 修改数据更新到内存。
  3. redo log prepare: write
  4. binlog: write
  5. redo log prepare: fsync
  6. binlog: fsync
  7. redo log commit: write

写 binlog 是分两步的:

  1. 先把 binlog 从 binlog cache 中写到磁盘上的 binlog 文件;
  2. 调用 fsync 持久化。

如果想要提升 binlog 组提交的效果,可以通过设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 来实现。

  1. binlog_group_commit_sync_delay 参数,表示延迟多少微秒后才调用 fsync;
  2. 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 转账发生死锁的条件,如何避免

主从同步延迟的原因

  1. 网络,建议同机房。
  2. 硬件,建议同等配置。
  3. 大事务。
  4. 备库压力太大,影响了写能力,建议一主多备。
  5. 主库写太多,备库跟不上,建议组提交延迟,能提升备库复制并发度。

并行复制策略

  1. COMMIT_ORDER(默认),表示的就是前面介绍的,根据同时进入 prepare 和 commit 来 判断是否可以并行的策略。
  2. WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的 hash 值,组成集合 writeset。如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行。
    • writeset 是在主库生成后直接写入到 binlog 里面的,这样在备库执行的时候,不需要解析 binlog 内容(event 里的行数据),节省了很多计算量;
    • 不需要把整个事务的 binlog 都扫一遍才能决定分发到哪个 worker,更省内存;
    • 由于备库的分发策略不依赖于 binlog 内容,所以 binlog 是 statement 格式也是可以的;
    • 对于“表上没主键”和“外键约束”的场景,WRITESET 策略也是没法并行的,也会暂时退化为单线程模型。
  3. 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 的区别

  1. InnoDB 支持事务,MyISAM 不支持,MyISAM 没有 redo log。
  2. InnoDB 支持行锁,MyISAM 只支持表锁。
  3. InnoDB 支持外键,而 MyISAM 不支持。
  4. 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);
posted @ 2022-05-10 22:35  灯无焰  阅读(84)  评论(0编辑  收藏  举报