duiying / backend-qa Goto Github PK
View Code? Open in Web Editor NEWLicense: Apache License 2.0
License: Apache License 2.0
1、中文和英文之间,中文和数字之间用空格。
2、标点符号用中文的标点符号,比如逗号:,句号:。
3、贴代码要高亮,符合 Markdown 的语法规范。
传送门:leecode-225 用队列实现栈。
思路:
实现:
<?php
class MyStack
{
private $queue;
private $help;
function __construct()
{
$this->queue = new SplQueue();
$this->help = new SplQueue();
}
/**
* Push element x onto stack.
* @param Integer $x
* @return NULL
*/
function push($x)
{
$this->queue->enqueue($x);
}
/**
* Removes the element on top of the stack and returns that element.
* @return Integer
*/
function pop()
{
if ($this->queue->isEmpty()) {
return -1;
}
if ($this->queue->count() === 1) {
return $this->queue->dequeue();
}
// queue 队列除了队尾,都移入 help
while ($this->queue->count() > 1) {
$this->help->enqueue($this->queue->dequeue());
}
$res = $this->queue->dequeue();
// 交换 queue 和 help
$this->swap();
return $res;
}
function swap()
{
$tmp = $this->queue;
$this->queue = $this->help;
$this->help = $tmp;
}
/**
* Get the top element.
* @return Integer
*/
function top()
{
if ($this->queue->isEmpty()) {
return -1;
}
if ($this->queue->count() === 1) {
$res = $this->queue->dequeue();
$this->queue->enqueue($res);
return $res;
}
while ($this->queue->count() > 1) {
$this->help->enqueue($this->queue->dequeue());
}
$res = $this->queue->dequeue();
$this->help->enqueue($res);
$this->swap();
return $res;
}
/**
* Returns whether the stack is empty.
* @return Boolean
*/
function empty()
{
return $this->queue->isEmpty();
}
}
递归法:
class Solution
{
private $list = [];
/**
* @param TreeNode $root
* @return Integer[]
*/
function postorderTraversal($root)
{
if ($root === null) return [];
$this->postorderTraversal($root->left);
$this->postorderTraversal($root->right);
$this->list[] = $root->val;
return $this->list;
}
}
迭代法:
前序遍历的顺序是根->左->右,将其转换为根->右->左,由于栈是先进后出的顺序,所以入栈时先将左子树入栈,最后将结果倒序。
class Solution
{
private $stack = [];
private $list = [];
/**
* @param TreeNode $root
* @return Integer[]
*/
function postorderTraversal($root)
{
if ($root === null) return [];
array_push($this->stack, $root);
while (!empty($this->stack)) {
$node = array_pop($this->stack);
$this->list[] = $node->val;
if ($node->left !== null) {
array_push($this->stack, $node->left);
}
if ($node->right !== null) {
array_push($this->stack, $node->right);
}
}
return array_reverse($this->list);
}
}
力扣传送门:leecode-912 排序数组。
1、**
归并排序使用的就是分治**。分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。
实现思路为:递归划分为左右两半部分,然后使用双指针思路对左右两半部分合并排序。
归并排序的递推公式:merge_sort(p…r) = merge(merge_sort(p…q), merge_sort(q+1…r))
。
归并排序的终止条件:p >= r 不用再继续分解
。
解释一下这个递推公式:
merge_sort(p…r) 表示,给下标从 p 到 r 之间的数组排序。我们将这个排序问题转化为了两个子问题,merge_sort(p…q) 和 merge_sort(q+1…r),其中下标 q 等于 p 和 r 的中间位置,也就是 (p+r)/2。当下标从 p 到 q 和从 q+1 到 r 这两个子数组都排好序之后,我们再将两个有序的子数组合并在一起,这样下标从 p 到 r 之间的数据就也排好序了。
2、实现
<?php
class MergeSort
{
/**
* 归并排序
*
* @param $arr
* @param $left
* @param $right
*/
public static function sort(&$arr, $left, $right)
{
if ($left >= $right) {
return;
}
$mid = intval(floor(($left + $right) / 2));
// 递归前面的
self::sort($arr, $left, $mid);
// 递归后面的
self::sort($arr, $mid + 1, $right);
// 归并
self::merge($arr, $left, $mid, $right);
}
/**
* 归并
*
* @param $arr
* @param $left
* @param $mid
* @param $right
*/
public static function merge(&$arr, $left, $mid, $right)
{
$p1 = $left;
$p2 = $mid + 1;
// 创建一个临时数组,用于存放归并之后的结果
$help = [];
while ($p1 <= $mid && $p2 <= $right) {
$help[] = $arr[$p1] <= $arr[$p2] ? $arr[$p1++] : $arr[$p2++];
}
// 将剩余的数组直接放入临时数组
while ($p1 <= $mid) {
$help[] = $arr[$p1++];
}
while ($p2 <= $right) {
$help[] = $arr[$p2++];
}
// 把临时数组存入原数组
for ($i = 0; $i < count($help); $i++) {
$arr[$left + $i] = $help[$i];
}
}
}
// Test
$arr = [6, 4, 3, 7, 5, 1, 2];
MergeSort::sort($arr, 0, count($arr) - 1);
print_r($arr);
3、复杂度分析
时间复杂度:归并排序的执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度是非常稳定的,不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn)。
空间复杂度:归并排序每次递归需要用到一个辅助表,长度与待排序的表相等,虽然递归次数是 O(logn) ,但每次递归都会释放掉所占的辅助空间,因而空间复杂度还是 O(n)。
4、稳定性分析
合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。
5、它是原地排序算法吗?
归并排序是非原地排序算法,主要原因是合并函数无法在原地执行。
思路 1:DFS(深度优先遍历)。
max(头结点左⼦树的最⼤深度, 头结点右⼦树的最⼤深度) + 1
实现:
/**
* @param TreeNode $root
* @return Integer
*/
function maxDepth($root)
{
if ($root === null) {
return 0;
}
$maxLeftHigh = $this->maxDepth($root->left);
$maxRightHigh = $this->maxDepth($root->right);
return max($maxLeftHigh, $maxRightHigh) + 1;
}
思路 2:BFS(广度优先遍历)。
/**
* @param TreeNode $root
* @return Integer
*/
function maxDepth($root)
{
if (is_null($root)) {
return 0;
}
// 广度优先搜索的队列里存放的是「当前层的所有节点」
$queue = [];
array_push($queue, $root);
// 二叉树深度
$res = 0;
while (!empty($queue)) {
// 这一层有多少个节点
$len = count($queue);
// 拿出各个节点,并把左右子节点入队
while ($len > 0) {
$node = array_shift($queue);
if (!is_null($node->left)) {
array_push($queue, $node->left);
}
if (!is_null($node->right)) {
array_push($queue, $node->right);
}
$len--;
}
// 当前层所有节点已经出列,如果队列不为空,说明有下一层节点,深度 + 1
$res++;
}
return $res;
}
传送门:leetcode-21 合并两个有序链表。
题目描述:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路:
代码实现:
<?php
class ListNode
{
public $val = 0;
public $next = null;
function __construct($val = 0, $next = null)
{
$this->val = $val;
$this->next = $next;
}
}
class Solution
{
/**
* @param ListNode $l1
* @param ListNode $l2
* @return ListNode
*/
function mergeTwoLists($l1, $l2) {
// 创建 dummy 空节点和 curr 指针
$dummy = new ListNode();
$curr = $dummy;
while ($l1 !== null && $l2 !== null) {
if ($l1->val <= $l2->val) {
$curr->next = $l1;
$l1 = $l1->next;
} else {
$curr->next = $l2;
$l2 = $l2->next;
}
$curr = $curr->next;
}
if ($l1 !== null) {
$curr->next = $l1;
}
if ($l2 !== null) {
$curr->next = $l2;
}
return $dummy->next;
}
}
思路:
双指针:
实现:
<?php
/**
* Definition for a singly-linked list.
* class ListNode {
* public $val = 0;
* public $next = null;
* function __construct($val) { $this->val = $val; }
* }
*/
class Solution
{
/**
* @param ListNode $head
* @param Integer $k
* @return ListNode
*/
function getKthFromEnd($head, $k)
{
$fast = $head;
$slow = $head;
for ($i = 0; $i < $k; $i++) {
$fast = $fast->next;
}
while ($fast !== null) {
$fast = $fast->next;
$slow = $slow->next;
}
return $slow;
}
}
操作系统八股文:互联网面试相关知识点学习笔记 。
1、说一下网络模型都有哪些?什么是 I/O 多路复用?select 和 epoll 的区别?水平触发和边缘触发的区别是啥?使用的时候需要注意什么?epoll 储存描述符的数据结构是什么?select 有描述符限制吗?是多少?
2、进程、线程、协程的区别?
一个进程可以由多个称为线程的执行单元组成。每个线程都运行在进程的上下文中,共享着同样的代码和全局数据。
虚拟内存是操作系统为每个进程提供的一种抽象,每个进程都有属于自己的、私有的、地址连续的虚拟内存,当然我们知道最终进程的数据及代码必然要放到物理内存上,那么必须有某种机制能记住虚拟地址空间中的某个数据被放到了哪个物理内存地址上,这就是所谓的地址空间映射,也就是虚拟内存地址与物理内存地址的映射关系,那么操作系统是如何记住这种映射关系的呢,答案就是页表,页表中记录了虚拟内存地址到物理内存地址的映射关系。有了页表就可以将虚拟地址转换为物理内存地址了,这种机制就是虚拟内存。
每个进程都有自己的虚拟地址空间,进程内的所有线程共享进程的虚拟地址空间。
进程切换与线程切换的一个最主要区别就在于进程切换涉及到虚拟地址空间的切换而线程切换则不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。
多线程比多进程之间更容易共享数据,在上下文切换中线程一般比进程更高效。
协程(Coroutine)是用户态的线程。通常创建协程时,会从进程的堆中分配一段内存作为协程的栈。
协程本质上就是用户态下的线程,协程不是被操作系统内核所管理,而完全是由程序所控制,所以也有人说协程是 “轻线程”,但我们一定要区分用户态和内核态的区别,很关键。
协程适用于 IO 阻塞且需要大量并发的场景,当发生 IO 阻塞,由协程的调度器进行调度,通过将数据流 yield 掉,并且记录当前栈上的数据,阻塞完后立刻再通过线程恢复栈,并把阻塞的结果放到这个线程上去运行。
面试题:什么是协程,协程和线程的区别和联系?进程和线程的区别和联系?
进程是计算机资源(包括 CPU、内存、磁盘 IO 等)分配的最小单位,线程是程序执行(CPU 调度和分配)的最小单位。
进程有独立的地址空间,每启动一个进程,系统都会为进程分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种开销非常昂贵。线程没有地址空间,但是线程共享所属进程的地址空间。
同一进程中的多个线程共享所属进程的代码段(代码和常量)、数据段(全局变量和静态变量)、扩展段(堆存储),但是每个线程拥有自己的栈段(又叫运行时段,用于存放所有局部变量和临时变量)、寄存器的内容。
健壮性:因为不同进程有自己独立的地址空间,所以一个进程死掉,不会对其他进程造成影响。而一个线程死掉,线程所属的整个进程可能也会死掉,所以多进程方式比多线程方式更加健壮。
3、说一下内存管理,虚拟内存,分段分页?操作系统中,内存管理怎么做的?如何保证碎片都能被有效利用?
为什么需要虚拟内存?
早期计算机在运行时,直接把整个程序加载入内存。这会产生一些问题:
虚拟内存是什么?
于是就有了虚拟内存,它是一种内存管理技术,能为每个进程提供一个独有的、连续的虚拟地址空间。并通过内存交换技术,把不常用的内存换出到硬盘,需要时再换入内存,从而让有限的内存能运行更大的程序。
虚拟内存怎么用?
当有进程被创建时,操作系统会为其分配虚拟内存,并通过内存管理单元(MMU)的映射关系,将虚拟地址转化为物理地址。这主要有分段和分页两种方式。
分段
分段根据程序的逻辑角度,分成了栈、堆、数据、代码等不同属性的段。
地址转换分为以下三步:
分段保留了逻辑特征,便于共享和保护,但每个段的大小不一,会产生内存碎片的问题。解决办法就是内存交换,先把不连续的段换出到硬盘,再换入内存,使之连续。但如果要交换的段很大,就会产生内存交换效率低的问题。
分页
简单分页
于是,就有了分页,把虚拟和物理内存分成固定大小的页。
地址转换分为以下三步:
若进程访问的虚拟地址不在页表上,则产生一个缺页异常,先将进程阻塞,再将硬盘中对应的页换入内存、更新页表,再让进程运行。采用分页后,释放内存都是以页为单位,这样就不会产生进程无法使用的小内存(内存碎片)。同时,在内存交换时,只需交换若干页,大大提高了效率。
但操作系统可以同时运行很多进程,这会使简单分页产生的页表过大。
多级页表
于是,就有了多级页表。若上一级页表的页表项未被用到,则无需创建对应的下一级页表,从而节省了内存。但寻址时需要多级表参与,会增加时间的开销。
TLB
于是根据程序的局部性原理,即在一段时间内,程序的执行仅限于其中某一部分。在 CPU 中加入 TLB,缓存最近常被访问的页表项,从而提高地址转换的速度。
段页式
分段和分页并不是对立的,它们可以组合形成段页式。先把程序划分成多个段,再把段划分成多个页,从而在保留逻辑特征的基础上,提高内存的利用率,但会增加硬件成本和系统开销。
页面置换算法
进程运行时,若其访问的页面不在内存而需将其调入,但内存已无空闲空间时,就需要从内存中调出一页程序或数据,送入磁盘的交换区。选择调出页面的算法就称为页面置换算法:
Linux 内存管理
Linux 系统主要采用分页,同时也用分段,用于访问控制和内存保护。另外,Linux 系统中虚拟空间可分为用户态和内核态两部分。
4、进程的内存空间分配?各个段的作用是什么?
能够手画下图:
一个进程的内存空间布局如下:
一个程序分为如下段:
从低地址到高地址的用户空间内存区域排列为:
代码段 Text:
CPU 执行的程序指令部分,为了程序的安全性,通常情况下是只读的,程序段可以被父子进程共享。
已初始化的数据段 Data:
在程序执行之前已经明确赋值的变量部分,所以有初值的全局变量和 static 变量在 Data 区。
未初始化的数据段 BSS:
用来存放程序中未初始化的全局变量的一块内存区域,在程序载入时由内核清 0,段内包含:
堆 Heap:
堆通常用作动态内存分配,堆空间起始于 BSS 段的末尾,并向高地址处生长,堆空间通常由 malloc, realloc 及 free 管理,这些接口可能再使用 brk/sbrk 系统调用来调整大小,在一个进程中,堆空间被进程内所有的共享库及动态加载模块所共享。
栈 Stack:
栈保存函数的局部变量(但不包括 static 声明的变量, static 意味着在数据段中存放变量)、参数以及返回值,栈区域由高地址向低地址增长。
5、进程间通信的方式有哪些?区别是什么?
面试题:什么是协程,协程和线程的区别和联系?进程和线程的区别和联系?
进程是计算机资源(包括 CPU、内存、磁盘 IO 等)分配的最小单位,线程是程序执行(CPU 调度和分配)的最小单位。
进程有独立的地址空间,每启动一个进程,系统都会为进程分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种开销非常昂贵。线程没有地址空间,但是线程共享所属进程的地址空间。
同一进程中的多个线程共享所属进程的代码段(代码和常量)、数据段(全局变量和静态变量)、扩展段(堆存储),但是每个线程拥有自己的栈段(又叫运行时段,用于存放所有局部变量和临时变量)、寄存器的内容。
健壮性:因为不同进程有自己独立的地址空间,所以一个进程死掉,不会对其他进程造成影响。而一个线程死掉,线程所属的整个进程可能也会死掉,所以多进程方式比多线程方式更加健壮。
6、什么是进程状态?进程状态都有哪些?三态模型之间是如何转换的?
7、进程切换和线程切换的区别?
虚拟内存是操作系统为每个进程提供的一种抽象,每个进程都有属于自己的、私有的、地址连续的虚拟内存,当然我们知道最终进程的数据及代码必然要放到物理内存上,那么必须有某种机制能记住虚拟地址空间中的某个数据被放到了哪个物理内存地址上,这就是所谓的地址空间映射,也就是虚拟内存地址与物理内存地址的映射关系,那么操作系统是如何记住这种映射关系的呢,答案就是页表,页表中记录了虚拟内存地址到物理内存地址的映射关系。有了页表就可以将虚拟地址转换为物理内存地址了,这种机制就是虚拟内存。
每个进程都有自己的虚拟地址空间,进程内的所有线程共享进程的虚拟地址空间。
进程切换与线程切换的一个最主要区别就在于进程切换涉及到虚拟地址空间的切换而线程切换则不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。
8、什么是用户态?内核态?为什么要区分内核空间和用户空间?
9、磁盘 I/O 都有哪些方式?各自的区别是什么?
1、缓存 IO:
缓存 IO 的优点:
缓存 IO 的缺点:
2、直接 IO:
直接 IO 的特点:直接 IO 是不经过 Page Cache 的,多用于数据库,如果访问的数据不在应用程序缓存中,那么每次数据都会直接从磁盘进行加载,这种直接加载会非常缓慢。
3、mmap:
10、孤儿进程是什么?有危害吗?
父进程已经退出,而它的一个或多个子进程还在运行,这些子进程将成为孤儿进程,孤儿进程会被 init 进程(进程号为 1)收养,并由 init 进程完成对它们的状态收集工作。
当孤儿进程结束之后,init 进程会回收其占用的资源,因此孤儿进程其实没有多少危害。
11、什么是僵尸进程?如何解决?
在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件、占用的内存等。但是仍然为其保留一定的信息(包括进程号、退出状态、运行时间等)。直到父进程通过 wait / waitpid 来获取时才释放。
如果一个进程通过 fork 创建了子进程,如果子进程退出而父进程没有调用 wait() 或 waitpid() 获取子进程的状态信息,那么子进程占用的资源就不会释放,系统能使用的进程号是有限的,如果有大量的僵尸进程,将因为没有可用的进程号而导致无法创建新的进程。
如果父进程退出了,子进程将由僵尸进程变成孤儿进程,从而被 init 进程接管,init 进程会释放其占用的资源。
我们在进行多进程编程的时候,一定要用 wait 来回收退出的子进程。
12、软链接和硬链接是什么?区别是什么?
什么是硬链接?
硬链接通过索引节点进行连接,多个文件名指向同一索引节点,同一索引节点指向的是同一文件。
什么是软链接?
软链接本质是新建一个文件,保存原文件的路径名,相当于快捷方式,因此新建文件和原文件的 inode 是不同的。
软链接和硬链接的区别是什么?
13、同一个进程内的线程会共享什么资源?
线程和进程资源比较:
线程占有的都是不共享的:栈、寄存器、状态、程序计数器。
线程共享进程的:地址空间、全局变量、打开的文件、子进程、堆空间等。
传送门:leetcode - 226 翻转二叉树。
思路:递归。
实现:
/**
* @param TreeNode $root
* @return TreeNode
*/
function invertTree($root) {
// 递归函数的终止条件,节点为空时返回
if (is_null($root)) {
return null;
}
// 下面三句是将当前节点的左右子树交换
$tmp = $root->left;
$root->left = $root->right;
$root->right = $tmp;
//递归交换当前节点的左子树
$this->invertTree($root->left);
// 递归交换当前节点的右子树
$this->invertTree($root->right);
return $root;
}
1、什么是荷兰国旗问题?
题目描述:给定一个数组 arr,和一个数 num,请把小于 num 的数放在数组的左边,等于 num 的数放在数组的中间,大于 num 的数放在数组的右边。
思路:
将数组分为小于区域、等于区域、大于区域三部分。
假如数组为 [3, 6, 4, 2, 7],num 为 4。
less) [3, 6, 4, 2, 7] (more
^ ^ ^
less curr more
当 curr 小于 num 时,当前数和小于区域的下一个数交换,curr 右移,小于区域右移。
less [3), 6, 4, 2, 7] (more
^ ^ ^
less curr more
当 curr 大于 num 时,当前数和大于区域的下一个数交换,curr 不移,大于区域左移。
less [3), 7, 4, 2, (6] more
^ ^ ^
less curr more
于是,curr 继续比较。
less [3), 2, 4, (7, 6] more
^ ^ ^
less curr more
此时,curr 小于 num。
less [3, 2), 4, (7, 6] more
^ ^ ^
less curr more
当 curr 等于 num 时,小于区域不移,大于区域也不移,curr 右移。
代码实现:
<?php
class NetherlandsFlag
{
public static function partition(&$arr, $left, $right, $num)
{
// 小于区域指针
$less = $left - 1;
// 大于区域指针
$more = $right + 1;
// 当前位置指针
$curr = $left;
while ($curr < $more) {
// 当前数和小于区域的下一个数交换,curr 右移,小于区域右移
if ($arr[$curr] < $num) {
self::swap($arr, ++$less, $curr++);
}
// 当前数和大于区域的下一个数交换,curr 不移,大于区域左移
elseif ($arr[$curr] > $num) {
self::swap($arr, --$more, $curr);
}
// 小于区域和大于区域都不移,curr 右移
else {
$curr++;
}
}
// 返回等于区域
return [$less + 1, $more - 1];
}
public static function swap(&$arr, $i, $j)
{
$tmp = $arr[$i];
$arr[$i] = $arr[$j];
$arr[$j] = $tmp;
}
}
// Test
$arr = [3, 6, 4, 2, 7];
// [2, 2]
var_dump(NetherlandsFlag::partition($arr, 0, count($arr) - 1, 4));
// [3, 2, 4, 7, 6]
var_dump($arr);
2、什么是颜色分类问题? 传送门:【leetcode-75】颜色分类
思路:和上面的荷兰国旗相同,只是把荷兰国旗中需要比较的数置为 1 即可。
代码实现:
class Solution
{
static function swap(&$arr, $i, $j)
{
$tmp = $arr[$i];
$arr[$i] = $arr[$j];
$arr[$j] = $tmp;
}
/**
* @param Integer[] $nums
* @return NULL
*/
function sortColors(&$nums)
{
$num = 1;
$less = -1;
$more = count($nums);
$curr = 0;
while ($curr < $more) {
if ($nums[$curr] < $num) {
self::swap($nums, $curr++, ++$less);
} elseif ($nums[$curr] > $num) {
self::swap($nums, $curr, --$more);
} else {
$curr++;
}
}
}
}
1、为什么需要 MQ?
核心就三点:异步、削峰、解耦。
2、消息模型有哪两种?区别是什么?
点对点模型、发布/订阅模型。
发布订阅模型可以实现多个订阅者消费同一个 Topic。
3、Zookeeper 在 Kafka 中的作用?
4、说一下 Kafka 集群的组成?
最好能够手画下面这张图:
5、Kafka 可以保证某个 Topic 下的消息有序吗?
Kafka 保证的是分区有序而不是主题有序。
6、什么是 AR、ISR、OSR?
分区中的所有副本统称为 AR(Assigned Replicas)。所有与 leader 副本保持一定程度同步的副本(包括 leader 副本在内)组成 ISR(In-Sync Replicas),ISR 集合是 AR 集合中的一个子集。消息会先发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步,同步期间内 follower 副本相对于 leader 副本而言会有一定程度的滞后。
前面所说的一定程度的同步
是指可忍受的滞后范围,这个范围可以通过参数进行配置。与 leader 副本同步滞后过多的副本(不包括 leader 副本)组成 OSR(Out-of-Sync Replicas),由此可见,AR = ISR + OSR
。在正常情况下,所有的 follower 副本都应该与 leader 副本保持一定程度的同步,即 AR=ISR
,OSR 集合为空。
7、什么是 HW、LEO?
HW 是 High Watermark 的缩写,俗称高水位,它标识了一个特定的消息偏移量(offset),消费者只能拉取到这个 offset 之前的消息。
LEO 是 Log End Offset 的缩写,它标识当前日志文件中下一条待写入消息的 offset。
7、Kafka 支持读写分离吗?为什么?
Kafka 是不支持读写分离的,因为读写分离会带来数据不一致的问题。
8、为什么 Kafka 这么快?
9、Kafka 是如何保证高可用的?
Replica 备份机制、ACK 机制、ISR 机制、故障恢复机制。
10、ISR 是一种什么同步机制?
Kafka 不是完全同步,也不是完全异步,是一种 ISR 机制,它是基于 Replica 机制和 ACK 机制同步完成的。
11、Kafka 会不会丢数据?
会。
12、如何保证消息不被重复消费?
保证幂等。
13、如何保证消息的顺序性?
同一个 Partition 下消息有序,多个 Partition 下消息不一定有序,可以将消息根据 key 路由到指定的 Partition 上,这样就保证了有序。
另外,实际业务中,Producer 写入消息的时候还可以写入微秒时间戳,可以判断哪条消息先写入的,哪条消息后写入的。
14、如何保证消息的可靠性传输?
一般是要求起码设置如下 4 个参数:
replication.factor
参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本。min.insync.replicas
参数:这个值必须大于 1,这个是要求一个 eader 至少感知到有至少一个 Follower 还跟自己保持联系,没掉队,这样才能确保 Leader 挂了还有一个 Follower 吧。acks=all
:这个是要求每条数据,必须是写入 ISR 中所有 replica 之后,才能认为是写成功了。retries=MAX
(很大很大很大的一个值,无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里了。15、Acks 参数的作用是什么?
生产者发送消息中包含 acks
字段,该字段代表 Leader 应答生产者前 Leader 收到的应答数。
acks = 0
:生产者无需等待服务端的任何确认,消息被添加到生产者套接字缓冲区后就视为已发送,因此不能保证服务端已收到消息acks = 1
:只要 Partition Leader
接收到消息而且写入本地磁盘了,就认为成功了,不管它其他的 Follower 有没有同步过去这条消息了acks = all
:Leader 将等待 ISR 中的所有副本确认后再做出应答,因此只要 ISR 中任何一个副本还存活着,这条应答过的消息就不会丢失16、Kafka 副本是怎么管理的?
Leader 负责维护和跟踪 ISR 集合中所有 Follower 副本的滞后状态,当 Follower 副本落后过多时,就会将其放入 OSR 集合,当 Follower 副本追上了 Leader 的进度时,就会将其放入 ISR 集合。
17、如何确定当前能读到哪一条消息?
考察 HW 和 LEO 的概念。
17、发送消息的分区策略有哪些?
18、Kafka 是怎么去实现负载均衡的?
Kafka 的负责均衡主要是通过分区来实现的,我们知道 Kafka 是主写主读的架构,如下图:
共三个 broker ,里面各有三个副本,总共有三个 Partation, 深色的是 Leader,浅色的是 Follower,上下灰色分别代表生产者和消费者,虚线代表 Follower 从 Leader 拉取消息。
我们从这张图就可以很明显的看出来,每个 broker 都有消费者拉取消息,每个 Broker 也都有生产者发送消息,每个 Broker 上的读写负载都是一样的,这也说明了 Kafka 独特的架构方式可以通过主写主读来实现负载均衡。
19、Kafka 的负责均衡会有什么问题呢?
20、副本 Leader 是怎么选举的?
一般是从 ISR 队列中选举,但是可以开启 Unclean 领导者选举。
猴子选大王:一群猴子排成一圈,按 1,2,…,n
依次编号。然后从第 1 只开始数,数到第 m 只,把它踢出圈,从它后面再开始数,再数到第 m 只,在把它踢出去…,如此不停的进行下去,直到最后只剩下一只猴子为止,那只猴子就叫做大王。要求编程模拟此过程,输入 m、n,输出最后那个大王的编号。用程序模拟该过程。
思路 1:
/**
* @param Integer $n
* @param Integer $m
* @return Integer
*/
function lastRemaining($n, $m)
{
// 从第一个开始循环
$i = 1;
// 创建一个从 1 到 $n - 1 的数组
$arr = range(0, $n - 1);
while (count($arr) > 1) {
// 不被整除则压入数组尾部
if ($i % $m !== 0) {
array_push($arr, $arr[$i - 1]);
}
unset($arr[$i - 1]);
$i++;
}
return $arr[$i - 1];
}
思路 2:
这个问题实际上是约瑟夫问题。
既然约塞夫问题就是用人来举例的,那我们也给每个人一个编号(索引值),每个人用字母代替,下面这个例子是 N=8 m=3
的例子,我们定义 F(n,m)
表示最后剩下那个人的索引号,因此我们只关心最后剩下来这个人的索引号的变化情况即可。
从 8 个人开始,每次杀掉一个人,去掉被杀的人,然后把杀掉那个人之后的第一个人作为开头重新编号。
最终活着的人编号的反推
现在我们知道了 G 的索引号的变化过程,那么我们反推一下从 N = 7
到 N = 8
的过程。
如何才能将 N = 7
的排列变回到 N = 8
呢?
我们先把被杀掉的 C 补充回来,然后右移 m 个人,发现溢出了,再把溢出的补充在最前面。神奇了,经过这个操作就恢复了 N = 8
的排列了!
因此我们可以推出递推公式 f(8,3) = [f(7, 3) + 3] % 8
。
进行推广泛化,即 f(n,m) = [f(n-1, m) + m] % n
。
/**
* @param Integer $n
* @param Integer $m
* @return Integer
*/
function lastRemaining($n, $m)
{
$x = 0;
for ($i = 2; $i <= $n; $i++) {
$x = ($x + $m) % $i;
}
return $x;
}
堆是一个利用完全二叉树的结构来维护的一维数组,任意结点小于(或大于)它的左右子节点,最小值(或最大值)在根结点位置。
最小值在根节点(堆顶)的叫做小根(顶)堆,最大值在根节点(堆顶)的叫做大根(顶)堆。
完全二叉树:完全二叉树除最后一层外,每一层的结点数均达到最大值,在最后一层上只缺少右边的若干结点,即最后一层的结点优先填在左边。
如何将一棵完全二叉树构建为最大堆?
比如现在有一个数组为:arr = [5, 1, 13, 3, 16, 7, 10, 14, 6, 9]
,它的完全二叉树结构如下图:
将该完全二叉树构建为最大堆的代码如下:
<?php
$arr = [5, 1, 13, 3, 16, 7, 10, 14, 6, 9];
for ($i = 1; $i < count($arr); $i++) {
// 0 - i 区间形成大根堆
heapInsert($arr, $i);
}
/**
* 构建大根堆(加入一个结点并向上调整的过程)
*
* @param $arr
* @param $index
*/
function heapInsert(&$arr, $index)
{
while ($arr[$index] > $arr[intval(($index - 1) / 2)]) {
// 父结点
$parent = intval(($index - 1) / 2);
// swap
list($arr[$index], $arr[$parent]) = [$arr[$parent], $arr[$index]];
// 索引来到父结点位置
$index = $parent;
}
}
print_r($arr);
执行结果:
Array
(
[0] => 16
[1] => 14
[2] => 10
[3] => 13
[4] => 9
[5] => 5
[6] => 7
[7] => 1
[8] => 6
[9] => 3
)
大根堆的构建结果如下图:
构建堆的时间复杂度分析:构建堆时,从第 2 个结点开始向上调整,第 2 个结点调整次数 log2,第 3 个结点调整次数 log3,第 N 个结点调整次数 logN,所以,构建堆的时间复杂度为:log2 + log3 + ... + logN = O(N)
,其中 O(N) 是推导得出的结论。
传送门:剑指 Offer 50 - 第一个只出现一次的字符。
思路:有序哈希表。
Java 使用 LinkedHashMap 实现有序哈希表,而 PHP 的 Array 结构已经实现了有序哈希。
实现:
class Solution
{
/**
* @param String $s
* @return String
*/
function firstUniqChar($s)
{
$map = [];
$len = strlen($s);
for ($i = 0; $i < $len; $i++) {
if (isset($map[$s[$i]])) {
$map[$s[$i]] += 1;
} else {
$map[$s[$i]] = 1;
}
}
foreach ($map as $k => $v) {
if ($v === 1) return $k;
}
return ' ';
}
}
思路:动态规划。
实现:
class Solution
{
/**
* @param Integer[] $prices
* @return Integer
*/
function maxProfit($prices)
{
$cost = PHP_INT_MAX;
$profit = 0;
for ($i = 0; $i < count($prices); $i++) {
$cost = min($cost, $prices[$i]);
$profit = max($profit, $prices[$i] - $cost);
}
return $profit;
}
}
传送门:从尾到头打印链表。
思路:利用栈后进先出的特点。
实现:
<?php
/**
* Definition for a singly-linked list.
* class ListNode {
* public $val = 0;
* public $next = null;
* function __construct($val) { $this->val = $val; }
* }
*/
class Solution
{
private $help = [];
/**
* @param ListNode $head
* @return Integer[]
*/
function reversePrint($head)
{
$curr = $head;
while ($curr !== null) {
// 往数组头部插入元素
array_unshift($this->help, $curr->val);
$curr = $curr->next;
}
return $this->help;
}
}
思路:滑动窗口。
什么是滑动窗口?
其实就是一个队列,比如例题中的 pwwkew
,进入这个队列(窗口)为 pw 满足题目要求,当再进入 w,队列变成了 pww,这时候不满足要求。所以,我们要移动这个队列!
如何移动?
我们只要从队列的左边开始将元素移出,直到满足题目要求!一直维持这样的队列,找出队列出现最长的长度时候,求出解!
时间复杂度:O(n)。
实现:
class Solution
{
/**
* @param String $s
* @return Integer
*/
function lengthOfLongestSubstring($s) {
if (empty($s)) return 0;
$map = [];
$max = 0;
$start = 0;
for ($end = 0; $end < strlen($s); $end++) {
if (isset($map[$s[$end]])) {
$start = max($start, $map[$s[$end]] + 1);
}
$map[$s[$end]] = $end;
$max = max($max, $end - $start + 1);
}
return $max;
}
}
思路:
1、状态,即子问题。
dp[i] 代表以元素 nums[i] 为结尾的连续子数组最大和。
2、转移策略,自带剪枝。
若 dp[i−1]≤0 ,说明 dp[i−1] 对 dp[i] 产生负贡献,即 dp[i−1]+nums[i] 还不如 nums[i] 本身大。
3、状态转移方程,根据前两步抽象而来。
实现:
class Solution
{
/**
* @param Integer[] $nums
* @return Integer
*/
function maxSubArray($nums)
{
$res = $nums[0];
for ($i = 1; $i < count($nums); $i++) {
if ($nums[$i - 1] > 0) {
$nums[$i] = $nums[$i - 1] + $nums[$i];
}
$res = max($res, $nums[$i]);
}
return $res;
}
}
class Solution
{
/**
* @param String $s
* @param Integer $n
* @return String
*/
function reverseLeftWords($s, $n)
{
$leftStr = substr($s, 0, $n);
$rightStr = substr($s, $n);
return $rightStr . $leftStr;
}
}
传送门:二分查找。
实现:
class Solution
{
/**
* @param Integer[] $nums
* @param Integer $target
* @return Integer
*/
function search($nums, $target)
{
$left = 0;
$right = count($nums) - 1;
while ($left <= $right) {
$mid = intval(floor(($left + $right) / 2));
if ($nums[$mid] === $target) {
return $mid;
} elseif ($nums[$mid] > $target) {
$right = $mid - 1;
} else {
$left = $mid + 1;
}
}
return -1;
}
}
传送门:leetcode - 20 有效的括号。
思路:
使用栈数据结构来解决。
实现:
/**
* @param String $s
* @return Boolean
*/
function isValid($s)
{
// 哈希表
$map = ['(' => ')', '[' => ']', '{' => '}'];
// 存储左括号的栈
$stack = [];
$len = strlen($s);
for ($i = 0; $i < $len; $i++) {
$char = $s[$i];
// 左括号入栈
if (isset($map[$char])) {
array_push($stack, $map[$char]);
}
// //右括号,分 2 种情况判断
else {
if (empty($stack) || array_pop($stack) != $char) {
return false;
}
}
}
// 返回时,判断栈是否为空
return empty($stack);
}
思路 1:暴力法。
class Solution
{
/**
* @param Integer[][] $matrix
* @param Integer $target
* @return Boolean
*/
function findNumberIn2DArray($matrix, $target)
{
foreach ($matrix as $k => $line) {
foreach ($line as $kk => $num) {
if ($num === $target) {
return true;
}
}
}
return false;
}
}
思路 2:
若使用暴力法遍历矩阵 matrix ,则时间复杂度为 O(NM)。暴力法未利用矩阵从上到下递增、从左到右递增的特点,显然不是最优解法。
如下图所示,我们将矩阵逆时针旋转 45°
,并将其转化为图形式,发现其类似于二叉搜索树,即对于每个元素,其左分支元素更小、右分支元素更大。因此,通过从根节点开始搜索,遇到比 target 大的元素就向左,反之向右,即可找到目标值 target 。
class Solution
{
/**
* @param Integer[][] $matrix
* @param Integer $target
* @return Boolean
*/
function findNumberIn2DArray($matrix, $target)
{
$i = count($matrix[0]) - 1;
$j = 0;
while ($i >= 0 && $j < count($matrix)) {
if ($matrix[$j][$i] > $target) {
$i--;
} elseif ($matrix[$j][$i] < $target) {
$j++;
} else {
return true;
}
}
return false;
}
}
1、网络模型有哪几种?不同模型的区别是什么?
目前存在 3 种划分网络模型的方式:
OSI 七层模型
TCP/IP 四层模型
五层模型
七层网络模型:正统意义上的国际标准网络模型
四层网络模型:简称 TCP/IP 协议,最核心的两个协议是 TCP/IP,是事实上的国际标准网络模型
五层网络模型:学习和开发用的网络模型
2、TCP/IP 指的是 TCP、IP 这两种协议吗?
从字面意义看,很多人会认为 TCP/IP 是 TCP、IP 这两种协议,实际上 TCP/IP 指的是网络通信过程中用到的协议的统称。由于该协议族中最核心的两个协议分别为 TCP(传输控制协议)和 IP(网际协议),因此它也被称为 TCP/IP 协议族(TCP/IP Protocol Suite 或 TCP/IP Protocols),简称 TCP/IP,它具有四层网络结构。
3、五层网络模型都有哪五层?简单描述一下各层的作用是什么?各层的传输单元是什么?
五层:应用层、传输层、网络层、数据链路层、物理层。
应用层:规定应用程序之间如何传递报文,比如 HTTP 协议,HTTP 客户端和 HTTP 服务端的首要工作就是根据 HTTP 协议的标准组装和解析 HTTP 数据包,每个 HTTP 报文格式由三部分组成:请求行/响应行、请求头/响应头、请求体/响应体。
传输层:为两台主机提供端到端的逻辑通信,相隔几千里的两台主机的进程就好像在直接通信一样。
网络层:提供了主机到主机的通信,将传输层产生的报文段封装成分组数据包发送到目的主机,并提供路由选择的能力。
数据链路层:以太网、Wifi、蓝牙工作在这一层,网络访问层提供了主机连接到物理网络需要的硬件和相关的协议。
4、五层网络模型中,各层都有哪些协议?
5、假设你正在电脑上用微信跟女朋友聊天,用 QQ 跟技术大佬们讨论技术细节,当电脑收到一个数据包时,它怎么知道这是一条微信的聊天内容,还是一条 QQ 的消息呢?
通过端口号,传输层用端口号来标识不同的应用程序。
6、IP 协议在哪一层?这一层给传输层交付的报文段封装了什么重要信息?IP 协议是无连接的吗?
IP 协议是网络层的主要协议,TCP 和 UDP 都是用 IP 协议作为网络层协议。这一层的主要作用是给包加上源地址和目标地址,将数据包传送到目标地址。
IP 协议是一个无连接的协议,也不具备重发机制,这也是 TCP 协议复杂的原因之一就是基于了这样一个不靠谱的协议。
7、收到 IP 数据包解析以后,它怎么知道这个分组应该投递到上层的哪一个协议(TCP 和 UDP)?
IP 协议中有一个 Protocol 字段,为 6 表示 TCP。
8、五层网络模型中,各层的传输单元是什么?
9、网络分层的好处是什么?
各层独立、灵活性更好、易于测试和维护、能促进标准化
10、徒手画出五层网络模型,包括各层的协议都有哪些、封装了什么信息等
11、什么是 MAC 地址?
集线器 HUB 工作在物理层,它的每个接口仅仅简单地转发比特,收到 1 就转发 1,收到 0 就转发 0,不进行差错检测,由于转发到了所有出口,那么这几台机器怎么知道数据包是不是发给自己的呢?
首先,你要给所有的连接到集线器的设备,都起个名字。原来你们叫 ABCD,但现在需要一个更专业的,全局唯一的名字作为标识,你把这个更高端的名字称为 MAC 地址。
你的 MAC 地址是 aa-aa-aa-aa-aa-aa,你的伙伴 B 的 MAC 地址是 bb-bb-bb-bb-bb-bb,以此类推,不重复就好。
MAC 地址又称为物理地址、硬件地址,但是不要被物理二字误导认为物理地址属于物理层范畴,物理地址属于数据链路层范畴。
MAC 地址是 6 个字节,48 位,IEEE 的注册管理机构 RA 负责向厂家分配地址字段的前三个字节(即高位 24 位)。地址字段中的后三个字节(即低位 24 位)由厂家自行指派,称为扩展标识符,必须保证生产出的适配器没有重复地址。
12、集线器和交换机的区别是什么?
13、交换机最开始的时候,MAC 地址表是空的,是怎么逐步建立起来的呢?
交换机的自学习过程。
14、为什么有了 MAC 地址,还需要 IP 地址?
交换机需要维护 MAC 地址与端口的映射关系,当机器数量增多时,交换机就无法维护如此庞大的映射关系了。
IP 地址是地域相关的,多个 IP 地址可以汇聚表示,而 MAC 地址是地域无关的难以汇聚,所以大规模网络只能增加路由器进行三层路由组网了。
注意,路由器的每一个端口,都有独立的 MAC 地址。
15、机器 A 给机器 C 发送数据包,怎么知道二者是否在同一个子网下,不需要通过路由器转发?
通过子网掩码,判断机器 A 和 机器 C 是否在同一个子网下。
假如机器 A 的 IP:192.168.0.1,机器 A 的子网掩码:255.255.255.0,机器 C 的 IP:192.168.1.1。
将源 IP:192.168.0.1 与目的 IP:192.168.0.1 分别同这个子网掩码:255.255.255.0 进行与运算,相等则是在一个子网,不相等则是在不同子网,就这么简单。
192.168.0.1 & 255.255.255.0 = 192.168.0.0
192.168.1.1 & 255.255.255.0 = 192.168.1.0
因此 A 和 C 不在同一个子网下。
16、什么是 ARP 协议?
我们只知道 IP 地址,可是数据链路层需要知道 MAC 地址怎么办?
假如你(A)此时不知道你同伴 B 的 MAC 地址(现实中就是不知道的,刚刚我们只是假设已知),你只知道它的 IP 地址,你该怎么把数据包准确传给 B 呢?
答案很简单,在网络层,我需要把 IP 地址对应的 MAC 地址找到,也就是通过某种方式,找到 192.168.0.2 对应的 MAC 地址 BBBB。
这种方式就是 ARP 协议,同时电脑 A 和 B 里面也会有一张 ARP 缓存表,表中记录着 IP 地址与 MAC 地址的对应关系。
IP 地址 | MAC 地址 |
---|---|
192.168.0.2 | BBBB |
一开始的时候这个表示空的,电脑 A 为了知道电脑 B(192.168.0.2)的 MAC 地址,将会广播一条 ARP 请求,B 收到请求后,带上自己的 MAC 地址给 A 一个响应。此时 A 便会更新自己的 ARP 表。
这样通过大家不断地广播 ARP 请求,最终所有电脑里面都将 ARP 缓存表更新完整。
17、分别从电脑视角、交换机视角、路由器视角来分析数据的传输过程
电脑视角:
交换机视角:
路由器视角:
从始至终这个数据包的两个 IP 地址都是不变的,只有 MAC 地址在不断变化。
18、机器 A 怎么知道,哪个设备是路由器?
如果 A 给 C 发消息,A 和 C 的 IP 地址分别是 & A 机器配置的子网掩码,发现不相等,则 A 认为 C 和自己不在同一个子网,于是把包发给路由器,就不管了,之后怎么转发,A 不关心。
A 如何知道,哪个设备是路由器?
上一步 A 通过是否与 C 在同一个子网内,判断出自己应该把包发给路由器,那路由器的 IP 是多少呢?
其实说发给路由器不准确,应该说 A 会把包发给默认网关。
对 A 来说,A 只能直接把包发给同处于一个子网下的某个 IP 上,所以发给路由器还是发给某个电脑,对 A 来说也不关心,只要这个设备有个 IP 地址就行。
所以默认网关,就是 A 在自己电脑里配置的一个 IP 地址,以便在发给不同子网的机器时,发给这个 IP 地址。
19、描述一下 TCP 的三次握手过程?握手过程中客户端和服务端各自的状态变化是什么?
能够手动画出下面这张图。
三次握手最重要的是交换彼此的 ISN(初始序列号)。
除了交换彼此的初始序列号,三次握手的另一个重要作用是交换一些辅助信息,比如最大段大小(MSS)、窗口大小(Win)、窗口缩放因子(WS)、是否支持选择确认(SACK_PERM)等。
20、为什么 SYN 段不携带数据却要消耗一个序列号呢?
不占用序列号的段是不需要确认的(都没有内容确认个啥),比如 ACK 段。SYN 报文段需要对方的确认,因此需要占用一个序列号。
关于这一点,可以记住如下的规则:
但凡消耗序列号的报文,一定要对端确认,否则会重传直到指定次数为止。
21、为什么 FIN 段不携带数据却要消耗一个序列号呢?
如果发送 FIN 之后没有消耗序列号,那么无法分辨 ACK 是对最后一个 TCP 报文的确认还是 FIN 报文的确认。
和问题 20 一样,还是需要确认的问题。
22、如果客户端发出 SYN 包之后,久久没有收到服务端的 ACK 包会发生什么?
在客户端发出 SYN 包以后,便进入 SYN-SENT 的状态,如果没有收到服务端的 ACK,那么客户端便会一直维持在这个状态,多次重发 SYN 数据包,当重发达到一定次数时便会停止重发,反馈连接失败,Java 中就会报错:java.net.ConnectException: Connection timed out
的异常,这个重发的次数对于 Linux 系统可以查看这个文件 /proc/sys/net/ipv4/tcp_syn_retries
,每一次发出 SYN 包后下一次发出的时间便会翻倍,假如 tcp_syn_retries
为 6,那么消耗的时间为 65s = 1s+2s+4s+8s+16s+32s
。
23、为什么采用三次握手而不是两次握手?
一句话,为了防止已失效的连接请求报文到达服务器而造成服务器资源浪费的问题。
所谓防止已失效的连接请求报文是这样产生的。考虑一种正常情况。A 发出连接请求,但因连接请求报文丢失而未收到确认。于是 A 再重传一次请求连接。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接。A 共发送了两个连接请求报文段。其中第一个丢失,第二个到达了 B。没有已失效的请求连接报文段。
现假定出现一种异常情况,即 A 发出的第一个请求连接报文段并没有丢失,而是在某些网络结点长时间滞留了,以至到连接释放以后的某个时间才到达 B。本来这是一个已失效的报文段。但 B 收到此失效的连接请求报文段后,就误认为是 A 又发出一次新的连接请求。于是就向 A 发出确认报文段,同意建立连接。假定不采用三次握手,那么只要 B 发出确认,新的连接就建立了。
由于现在 A 并没有发出建立请求的连接,因此不会理睬 B 的确认,也不会向 B 发送数据,但 B 却以为新的运输连接已经建立了,并一直等待 A 发来的数据。B 的许多资源就这样白白浪费了。
采用三次握手的办法可以防止上述现象发生。例如在刚才的情况下,A 不会向 B 的确认发出确认。B 由于收不到确认,就知道 A 并没有要求建立连接,所以就不会分配资源给这个连接。
为什么不是四次握手?
变成四次理论上完全可以,把三次握手的第二次的 SYN + ACK
拆成先回 ACK 包,再发 SYN 包就变成了「四次握手」。
24、描述一下 TCP 的四次挥手过程?挥手过程中客户端和服务端各自的状态变化是什么?
能够手动画出下面这张图。
1、客户端调用 close 方法,执行「主动关闭」,会发送一个 FIN 报文给服务端,从这以后客户端不能再发送数据给服务端了,客户端进入 FIN-WAIT-1
状态,FIN 报文其实就是将 FIN 标志位设置为 1。
FIN 段是可以携带数据的,比如客户端可以在它最后要发送的数据块可以捎带 FIN 段,当然也可以不携带数据。
客户端发送 FIN 包以后不能再发送数据给服务端,但是还可以接受服务端发送的数据,这个状态就是所谓的「半关闭(half-close)」。
主动发起关闭的一方称为主动关闭方,另外一方被称为被动关闭方。
2、服务端收到 FIN 包以后回复确认 ACK 报文给客户端,服务端进入 CLOSE_WAIT,客户端收到 ACK 以后进入 FIN-WAIT-2
状态。
3、服务端也没有数据要发送了,发送 FIN 报文给客户端,然后进入 LAST-ACK
状态,等待客户端的 ACK。
4、客户端收到服务端的 FIN 报文以后,回复 ACK 报文用来确认第三步里的 FIN 报文,进入 TIME_WAIT 状态,等待 2 个 2MSL 以后进入 CLOSED 状态。服务端收到 ACK 以后进入 CLOSED 状态。
25、为什么客户端发送最后一个确认包后还需要等待 2MSL(报文最大生存时间)时间?
1、数据报文可能在发送途中延迟但最终会到达,因此要等老的“迷路”的重复报文段在网络中过期失效,这样可以避免用相同源端口和目标端口创建新连接时收到旧连接姗姗来迟的数据包,造成数据错乱。
2、确保可靠实现 TCP 全双工终止连接。关闭连接的四次挥手中,最终的 ACK 由主动关闭方发出,如果这个 ACK 丢失,对端(被动关闭方)将重发 FIN,如果主动关闭方不维持 TIME_WAIT 直接进入 CLOSED 状态,则无法重传 ACK,被动关闭方因此不能及时可靠释放。
2MSL = 去向 ACK 消息最大存活时间(MSL) + 来向 FIN 消息的最大存活时间(MSL)
26、为什么挥手要四次,变为三次可以吗?
是有可能的。
我们知道,TCP 四次挥手里,第二次和第三次挥手之间,是有可能有数据传输的。第三次挥手的目的是为了告诉主动方:被动方没有数据要发了。
所以,在第一次挥手之后,如果被动方没有数据要发给主动方。第二和第三次挥手是有可能合并传输的。这样就出现了三次挥手。
上面提到的是没有数据要发的情况,如果第二、第三次挥手之间有数据要发,就不可能变成三次挥手了吗?
并不是。TCP 中还有个特性叫延迟确认。可以简单理解为:接收方收到数据以后不需要立刻马上回复 ACK 确认包。
在此基础上,不是每一次发送数据包都能对应收到一个 ACK 确认包,因为接收方可以合并确认。
而这个合并确认,放在四次挥手里,可以把第二次挥手、第三次挥手,以及他们之间的数据传输都合并在一起发送。因此也就出现了三次挥手。
27、简单描述一下 TCP 协议?(四个特点)
如果要用一句话来描述 TCP 协议,我想应该是:TCP 是一个可靠的(reliable)、面向连接的(connection-oriented)、基于字节流(byte-stream)、全双工的(full-duplex)协议。
面向连接:
面向连接的协议要求正式发送数据之前需要通过「握手」建立一个逻辑连接,结束通信时也是通过有序的四次挥手来断开连接。
可靠:
IP 是一种无连接、不可靠的协议:它尽最大可能将数据报从发送者传输给接收者,但并不保证包到达的顺序会与它们被传输的顺序一致,也不保证包是否重复,甚至都不保证包是否会达到接收者。
TCP 要想在 IP 基础上构建可靠的传输层协议,必须有一个复杂的机制来保障可靠性。 主要有下面几个方面:
校验和:checksum,每个 TCP 包首部中都有两字节用来表示校验和,防止在传输过程中有损坏。如果收到一个校验和有差错的报文,TCP 不会发送任何确认直接丢弃它,等待发送端重传。
超时重传:超时重传 TCP 发送数据后会启动一个定时器,等待对端确认收到这个数据包。如果在指定的时间内没有收到 ACK 确认,就会重传数据包,然后等待更长时间,如果还没有收到就再重传,在多次重传仍然失败以后,TCP 会放弃这个包。
基于字节流:
TCP 是一种字节流协议,流的含义是没有固定的报文边界。
假设你调用 2 次 write 函数往 socket 里依次写 500 字节、800 字节。write 函数只是把字节拷贝到内核缓冲区,最终会以多少条报文发送出去是不确定的,如下图所示:
上面出现的情况取决于诸多因素:路径最大传输单元 MTU、发送窗口大小、拥塞窗口大小等。
当接收方从 TCP 套接字读数据时,它是没法得知对方每次写入的字节是多少的。接收端可能分2 次每次 650 字节读取,也有可能先分三次,一次 100 字节,一次 200 字节,一次 1000 字节进行读取。
全双工:
在 TCP 中发送端和接收端可以是客户端/服务端,也可以是服务器/客户端,通信的双方在任意时刻既可以是接收数据也可以是发送数据,每个方向的数据流都独立管理序列号、滑动窗口大小、MSS 等信息。
28、TCP 是如何解决网络包乱序、重复的问题的?
通过 TCP 报文的序列号保证了接收数据的乱序和重复问题。
假设我们往 TCP 套接字里写 3000 字节的数据导致 TCP发送了 3 个数据包,每个数据包大小为 1000 字节:第一个包序列号为 [11001),第二个包序列号为 [10012001),第三个包序号为 [2001~3001)。
假如因为网络的原因导致第二个、第三个包先到接收端,第一个包最后才到,接收端也不会因为他们到达的顺序不一致把包弄错,TCP 会根据他们的序号进行重新的排列然后把结果传递给上层应用程序。
如果 TCP 接收到重复的数据,可能的原因是超时重传了两次但这个包并没有丢失,接收端会收到两次同样的数据,它能够根据包序号丢弃重复的数据。
29、TCP 报文段中的端口号的作用是什么?
区分进程。
30、TCP 报文段中的序列号的作用是什么?
在 SYN 报文 中,序列号用于交换彼此的初始序列号,在其它报文中,序列号用于保证包的顺序。
31、TCP 中确认号的作用是什么?
告知对方下一个期望接收的序列号,小于此确认号的所有字节都已经收到。
32、TCP 协议的首部字段有哪些?
最好能够牢记下面这张图:
端口号:每个 TCP 报文段都包含源端口号和目的端口号,用于寻找发送端和接收端的应用进程,这两个端口号再加上 IP 首部的源 IP 地址和目的 IP 地址以及 TCP 协议,这五元组就确定了一个唯一的 TCP 连接。
序号:TCP 是面向字节流的协议,通过 TCP 传输的字节流的每个字节都分配了序列号,序列号(Sequence Number)指的是本报文段第一个字节的序列号。序列号是一个 32 位的无符号整数,达到 2^32 - 1
后循环到 0。
在 SYN 报文 中,序列号用于交换彼此的初始序列号,在其它报文中,序列号用于保证包的顺序。
因为网络层(IP 层)不保证包的顺序,TCP 协议可以利用序列号来解决网络包乱序、重复的问题,以保证数据包以正常的顺序组装传递给上层应用。
如果发送方发送的是四个报文,序列号分别是 1、2、3、4,但到达接收方的顺序是 2、4、3、1,接收方就可以通过序列号的大小顺序组装出原始的数据。
确认号:TCP 使用确认号(Acknowledgement number,ACK)来告知对方下一个期望接收的序列号,小于此确认号的所有字节都已经收到。
关于确认号有几个注意点:
数据偏移(也叫首部长度):部中的选项部分的长度是可变的,因此首部的长度也是可变的,所以需要这个字段来明确表示首部的长度,这个字段占 4bit,4 位的二进制数最大可以表示 15,而首部长度是以 4 个字节为一个单位的,因此首部的最大长度是 15*4 = 60 字节。
保留字段:占 6 位,未来可能有具体用途,目前默认值为 0。
TCP Flags:
窗口大小:
可以看到用于窗口大小的 Window Size
只有 16 位,可能 TCP 协议设计者们认为 16 位的窗口大小已经够用了,也就是最大窗口大小为 65535 字节(64KB)。就像网传盖茨曾经说过:640KB 内存对于任何人都足够了
一样。
自己挖的坑当然要自己填,因此 TCP 协议引入了 TCP 窗口缩放 选项作为窗口缩放的比例因子,比例因子值的范围是 0-14
,其中最小值 0 表示不缩放,最大值 14。比例因子可以将窗口扩大到原来的 2 的 n 次方,比如窗口大小缩放前为 1050,缩放因子为 7,则真正的窗口大小为:1050 * 128 = 134400
,如下图所示:
可选项:
常用的选项有以下几个:
MSS:最大段大小选项,是 TCP 允许的从对方接收的最大报文段
SACK:选择确认选项
Window Scale:窗口缩放选项
33、TCP 是如何保证可靠传输的?
34、能不能说一下 TCP 的流量控制?
发送方不能无脑的发数据给接收方,要考虑接收方处理能力。如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制,从而导致网络流量的无端的浪费。
为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。
流量控制的底层实现机制是滑动窗口。
当发送端的滑动窗口变为 0 了,经过一段时间接收端从高负载中缓过来,可以处理更多的数据包,如果发送端不知道这个情况,它就会永远傻傻的等待了。于是乎,TCP 又设计了零窗口探测的机制(Zero window probe),用来向接收端探测,你的接收窗口变大了吗?我可以发数据了吗?
35、能不能说一下 TCP 的拥塞控制?
流量控制这种机制确实可以防止发送端向接收端过多的发送数据,但是它只关注了发送端和接收端自身的状况,而没有考虑整个网络的通信状况。于是出现了拥塞控制。
拥塞控制主要涉及到下面这几个算法:
为了实现上面的算法,TCP 的每条连接都有两个核心状态值:
拥塞窗口指的是在收到对端 ACK 之前自己还能传输的最大 MSS 段数。
它与前面介绍的接收窗口(rwnd)有什么区别呢?
拥塞窗口与前面介绍的发送窗口(Send Window)又有什么关系呢?
真正的发送窗口大小 = 「接收端接收窗口大小」 与 「发送端自己拥塞窗口大小」 两者的最小值
如果接收窗口比拥塞窗口小,表示接收端处理能力不够。如果拥塞窗口小于接收窗口,表示接收端处理能力 ok,但网络拥塞。
cwnd = cwnd/2
、ssthresh = cwnd
cwnd = cwnd/2
、ssthresh = cwnd
、cwnd = ssthresh + 3
36、能不能说一下 TCP 的重传机制?
TCP 针对数据包丢失的情况,会用重传机制解决。
常见的重传机制:
超时重传:重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据,也就是我们常说的超时重传。
快速重传:重传的时间间隔,要等几百毫秒才会进行第一次重传。聪明的网络协议设计者们想到了一种方法:「快速重传」。快速重传的含义是:当发送端收到 3 个或以上重复 ACK,就意识到之前发的包可能丢了,于是马上进行重传,不用傻傻的等到超时再重传。
SACK:为了解决不知道该重传哪些 TCP 报文,于是就有 SACK 方法。
D-SACK:Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。
37、在一个非常繁忙的服务器上,如果有大量 TIME_WAIT 状态的连接会怎么样呢?
首先要明确一点,在高并发的场景下,TIME_WAIT 连接存在,属于正常现象。
TIME_WAIT 状态存在的意义是:
虽说是正常现象,但是凡事都有利弊,过多的 TIME_WAIT 状态 TCP 连接,有什么业务上的影响吗?
1、在高并发(几万 QPS)并且采用短连接方式进行交互的系统中运行一段时间后,系统中就会存在大量的 TIME_WAIT 状态,如果 TIME_WAIT 状态把系统所有可用端口都占完了且尚未被系统回收时,就会出现无法向服务端创建新的 socket 连接的情况。
每一个 TIME_WAIT 状态,都会占用一个「本地端口」,上限为 65535(16 bit)。
2、大量的 TIME_WAIT 状态也会系统一定的 fd,内存和 CPU 资源,当然这个量一般比较小,并不是主要危害。
38、TIME_WAIT 过多,其本质原因是什么?如何优化 TIME_WAIT 过多的问题?
本质原因是大量的短连接存在:
如何优化 TIME_WAIT 过多的问题?
1、使用连接池技术,减少短链接的使用;
2、如果你使用了短连接,那么可以增加服务器,让每台服务器各自承担一部分 TIME_WAIT 状态连接
3、还有好多教程说,修改 /etc/sysctl.conf
参数。
个人认为改这种参数治标不治本,不建议使用。因为 TIME_WAIT 本身是正常的状态,我们要去找到导致 TIME_WAIT 过多的原因,去优化短链接,去增加服务器数量等等。
39、TCP 的 Keepalive 和 HTTP 的 Keep-Alive 是一个东西吗?
HTTP 的 Keep-Alive 也叫 HTTP 长连接,该功能是由「应用程序」实现的,可以使得用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,减少了 HTTP 短连接带来的多次 TCP 连接建立和释放的开销。
TCP 的 Keepalive 也叫 TCP 保活机制,该功能是由「内核」实现的,当客户端和服务端长达一定时间没有进行数据交互时,内核为了确保该连接是否还有效,就会发送探测报文,来检测对方是否还在线,然后来决定是否要关闭该连接。
40、说一下滑动窗口?窗口大小由发送方决定还是接收方决定?
滑动窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值,可以实现 TCP 的累计确认、流量控制的功能。
通常窗口的大小是由接收方的窗口大小来决定的。发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。
41、TCP 流量控制和拥塞控制的区别是什么?
拥塞控制:是让网络能够承受现有的网络负荷,是一个全局性的过程,涉及所有的主机、所有的路由器,以及与降低网络传输性能有关的所有因素。
流量控制:往往指点对点的通信量的控制,是个端到端的问题(接收端控制发送端),它所要做的是抑制发送端发送数据的速率,以便使接收端来及接收。
42、TCP 和 UDP 的区别?
43、HTTP 和 HTTPS 的区别是什么?对称加密和非对称加密的区别?加密算法都有哪些?
常见的加密算法:对称加密、非对称加密、Hash 算法。
HTTPS 协议由 HTTP 协议和 SSL 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。防止数据在传输中被窃取、改变。
44、什么是跨域?如何解决跨域?
出于浏览器的同源策略限制,浏览器会拒绝跨域请求。
同源:如果两个页面拥有相同的协议(protocol),端口(port)和域名(domain),那么这两个页面就属于同一个源(origin)。
为什么浏览器要限制跨域访问呢?(同源策略确实能规避一些危险,不是说有了同源策略就安全,只是说同源策略是一种浏览器最基本的安全机制,毕竟能提高一点攻击的成本。其实没有刺不穿的盾,只是攻击的成本和攻击成功后获得的利益成不成正比。)
解决跨域问题的方法有哪些?
1、JSONP
使用方式就不赘述了。
优点是:兼容性好(兼容低版本 IE)
缺点是:1、JSONP 只支持 GET 请求;2、XMLHttpRequest 相对于 JSONP 有着更好的错误处理机制。
2、代理
例如 www.123.com/index.html
需要调用 www.456.com/server.php
,可以写一个接口 www.123.com/server.php
,由这个接口在后端去调用 www.456.com/server.php
并拿到返回值,然后再返回给 index.html
,这就是一个代理的模式。相当于绕过了浏览器端,自然就不存在跨域问题。
3、CORS:后端修改响应 header
// 允许访问的来源
header('Access-Control-Allow-Origin:*');
// 允许访问的方式
header('Access-Control-Allow-Method:POST,GET');
45、DNS 协议的主要作用是什么?DNS 协议位于哪一层?DNS 用什么协议传输?
域名相比于 IP 更适合用户记忆,而 IP 更适合机器处理,DNS 协议的作用就是将主机名解析为 IP 地址。
DNS 协议位于应用层。
DNS 协议大多数情况下用 UDP 协议来进行传输。在两种情况下会使用 TCP 进行传输:
DNS 是怎么查询的?
46、常见的 HTTP 状态码都有哪些?301 和 302 的区别?什么情况下会出现 304 状态码?Nginx 499 状态码分析?什么情况下会出现 502 和 504?
301 和 302 的区别?
301 和 302 都是重定向,到底该用哪个?
301 应用场景:域名到期不想继续用这个,换了个域名(永久性)
302 应用场景:做活动时候,从首页跳到活动页面(临时性)
Nginx 499 状态码分析?
当一个客户端关闭时,HTTP 不为这种情形定义代码。同时我们处理它的请求时,在一个客户端在我们尝试向其发送 HTTP 头之前关闭连接时,使用自己的代码(也就是 499 状态码)来记录这种情况。
总结一下就是:
记录 499 的情形:
proxy_ignore_client_abort on; # 让代理服务端不要主动关闭客户端的连接
。什么情况下会出现 502 和 504?
502:
504:
504 即 Nginx 超过了自己设置的超时时间,不等待 php-fpm 的返回结果,直接给客户端返回 504 错误。但是此时 php-fpm 依然还在处理请求(在没有超出自己的超时时间的情况下)。
47、单机最多可以支撑多少 TCP 连接?
如何标识一个 TCP 连接?
系统用一个五元组来标识一个连接:服务器 IP + 服务器端口 + 协议(TCP / UDP) + 客户端 IP + 客户端端口。
端口号在 TCP、UDP 协议中占两个字节,意味着一台机器一共有 2^16 = 65536
个端口号可用。
当该机器作为 Client 时:
Client 在每次发起 TCP 连接请求时,通常会让系统选取一个空闲的本地端口(除非指定绑定哪个端口),端口 0 有特殊含义,所以如果机器作为 Client,理论上最多支持 65535 个 TCP 连接。
但是直到 Kernel4.2
版本,一个新的 socket option IP_BIND_ADDRESS_NO_PORT
的引入,上面最多支持 65535 的这个问题才得以解决。(解决的思路就是复用本地的端口)
当该机器作为 Server 时:
本机器的 IP、Port、协议(TCP)都确定了,因此在五元组中只剩下 Remote IP(也就是 Client IP)和 Remote Port(客户端 Port)是可变的,所以对于 IPV4,最大的 TCP 连接数约为 2 的 32 次方(IP 数)乘以 2 的 16 次方(Port 数),即约为 2^48
。
但是上面的 2^48
只是理论上的单机最大连接数,在实际环境中最大连接数远不能达到理论上限,限制连接数的主要因素是内存和允许的文件描述符个数(每个 TCP 连接都要占用一定的内存,每个 TCP 连接都要占用一个 socket)。
对 Server 端,通过增加内存、修改最大文件描述符个数等参数,单机最大并发 TCP 连接数超过 10W 是没问题的,国外 Urban Airship 公司在产品环境中已做到 50W 并发。
48、如何解决 TCP 粘包问题?
粘包这个说法已经被诟病很久了,既然坊间流传这个说法咱们就沿用吧,关于这个问题比较准确的解释可以参考下面几点:
所以,本质上这是一个没有正确使用 TCP 协议的而产生的问题。
通常来说,一般有下面几种方式解决:
\r\n
为字段的分隔符49、HTTP 是无状态的协议,这里的无状态怎么理解?
HTTP 是无状态协议,是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
50、建立 socket 都需要哪些步骤?
什么是 socket 呢?我们经常把 socket 翻译为套接字,socket 是在应用层和传输层之间的一个抽象层,它把复杂的 TCP/IP 协议族隐藏在 socket 接口后面,我们所说的 TCP/IP 协议是在操作系统内核实现的,而 socket 就是操作系统内核提供给应用层的一系列接口,它把 TCP/IP 层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。
以使用 TCP 协议通信的 socket 为例,其交互流程大概是这样子的:
51、浏览器从输入 url 开始,到展示网页都经历了什么?
52、HTTP1.0、HTTP1.1、HTTP2.0 的区别?
HTTP1.1 相比于 HTTP1.0 的改进
HTTP1.0
client 的每次 HTTP 请求都需要和 server 建立一个 TCP 连接,server 响应完成断开 TCP 连接,这样无法复用 TCP 连接。
HTTP1.1
Connection: keep-alive
),即一个 TCP 连接上可以传送多个 HTTP 请求和响应,从而复用 TCP 连接。www.baidu.com
的 Host 是 www.baidu.com
,xueshu.baidu.com
的 Host 是 xueshu.baidu.com
。HTTP2.0 相比于 HTTP1.1 的改进
53、什么场景下用 UDP,为什么不用 TCP?
实时视频直播、手游等适合用 UDP,因为:
54、TCP 和 UDP 可以同时监听相同的端口吗?
我们可以在同一个端口上同时拥有 UDP 和 TCP 请求,因为每个请求都由源 IP,目标 IP,源端口,目标端口,PROTOCOL(协议可以是 TCP 或 UDP)所包含的五元组来标识。
所以只要协议不同,即使端口相同,操作系统有能力根据接受的 IP 数据包里面的 Protocol 字段判断这个报文是什么报文(是 TCP 还是 UDP)。
55、什么是半连接队列和全连接队列?
下面一图足以说明:
55、你管这破玩意儿叫 TCP?
传送门:leetcode-141 环形链表。
思路:
设置两个链表指针 fast、slow,初始值都指向链表头结点,然后两个指针都往后走,不同的是 slow 每次前进一步,即前进一个节点。fast 每次前进两步,如果存在环,两个指针必定相遇。
实现:
<?php
/**
* Definition for a singly-linked list.
* class ListNode {
* public $val = 0;
* public $next = null;
* function __construct($val) { $this->val = $val; }
* }
*/
class Solution
{
/**
* @param ListNode $head
* @return Boolean
*/
function hasCycle($head)
{
$slow = $head;
$fast = $head;
while ($fast !== null && $fast->next !== null) {
$fast = $fast->next->next;
$slow = $slow->next;
if ($fast === $slow) {
return true;
}
}
return false;
}
}
思路:和二叉树的最大深度中的 BFS 思路类似。
实现:
class Solution
{
/**
* @param TreeNode $root
* @return Integer[][]
*/
function levelOrder($root)
{
if ($root === null) return [];
$curr = $root;
$queue = [];
array_push($queue, $curr);
$res = [];
while (!empty($queue)) {
$tmpRes = [];
$len = count($queue);
while ($len > 0) {
$node = array_shift($queue);
$tmpRes[] = $node->val;
if ($node->left !== null) {
array_push($queue, $node->left);
}
if ($node->right !== null) {
array_push($queue, $node->right);
}
$len--;
}
$res[] = $tmpRes;
}
return $res;
}
}
传送门:剑指 Offer 24 - 反转链表。
思路:
迭代:考虑遍历链表,并在访问各节点时修改 next 引用指向。
复杂度分析:
实现:
class Solution
{
/**
* @param ListNode $head
* @return ListNode
*/
function reverseList($head)
{
$curr = $head;
$prev = null;
while ($curr !== null) {
$tmp = $curr->next;
$curr->next = $prev;
$prev = $curr;
$curr = $tmp;
}
return $prev;
}
}
先来看下栈和队列的数据结构特点:
传送门:剑指 Offer 09-用两个栈实现队列。
题目描述:用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1)
解析:
维护两个栈 stackIn 和 stackOut,其中 stackIn 负责入队操作,stackOut 负责出队操作。
如何入队?只要有新数据,就压入 stackIn。
如何出队?
代码实现:
<?php
class CQueue
{
// 入队栈
private $stackIn;
// 出队栈
private $stackOut;
function __construct()
{
$this->stackIn = new SplStack();
$this->stackOut = new SplStack();
}
/**
* @param Integer $value
* @return NULL
*/
function appendTail($value)
{
$this->stackIn->push($value);
}
/**
* @return Integer
*/
function deleteHead()
{
if ($this->stackIn->isEmpty() && $this->stackOut->isEmpty()) {
return -1;
}
if ($this->stackOut->isEmpty()) {
while (!$this->stackIn->isEmpty()) {
$this->stackOut->push($this->stackIn->pop());
}
}
return $this->stackOut->pop();
}
}
思路:
构造一个获取当前子树的深度的函数 maxDepth(root)
,通过比较某子树的左右子树的深度差 abs($this->maxDepth($root->left) - $this->maxDepth($root->right)) <= 1
是否成立,来判断某子树是否是二叉平衡树。若所有子树都平衡,则此树平衡。
abs($this->maxDepth($root->left) - $this->maxDepth($root->right)) <= 1
:判断当前子树是否是平衡树;$this->isBalanced($root->left)
: 先序遍历递归,判断当前子树的左子树是否是平衡树;$this->isBalanced($root->right)
: 先序遍历递归,判断当前子树的右子树是否是平衡树;实现:
class Solution
{
/**
* @param TreeNode $root
* @return Boolean
*/
function isBalanced($root)
{
if ($root === null) return true;
// 1、判断当前子树是否是平衡树;2、判断当前子树的左子树是否是平衡树;3、判断当前子树的右子树是否是平衡树;
return abs($this->maxDepth($root->left) - $this->maxDepth($root->right)) <= 1 && $this->isBalanced($root->left) && $this->isBalanced($root->right);
}
function maxDepth($root)
{
if ($root === null) return 0;
$leftMaxDepth = $this->maxDepth($root->left);
$rightMaxDepth = $this->maxDepth($root->right);
return max($leftMaxDepth, $rightMaxDepth) + 1;
}
}
思路:
设跳上 n 级台阶有 f(n) 种跳法。在所有跳法中,青蛙的最后一步只有两种情况: 跳上 1 级或 2 级台阶。
f(n) 为以上两种情况之和,即 f(n)=f(n-1)+f(n-2),以上递推性质为斐波那契数列。本题可转化为求斐波那契数列第 n 项的值,与斐波那契数列等价,唯一的不同在于起始数字不同。
实现:
class Solution
{
// 使用数组存储已经计算的结果,避免重复计算
private $map = [];
/**
* @param Integer $n
* @return Integer
*/
function numWays($n)
{
if ($n === 0) return 1;
if (isset($this->map[$n])) {
return $this->map[$n];
}
if ($n <= 2) {
$this->map[$n] = $n;
return $n;
}
$this->map[$n] = $this->numWays($n - 1) + $this->numWays($n - 2);
return $this->map[$n] % 1000000007;
}
}
传送门:剑指 Offer 53 - I 在排序数组中查找数字 I。
思路:先通过二分查找找到下标,然后尝试往左、往右遍历,累加相等的次数。
实现:
class Solution
{
/**
* @param Integer[] $nums
* @param Integer $target
* @return Integer
*/
function search($nums, $target)
{
$index = $this->binarySearch($nums, $target);
if ($index === -1) {
return 0;
}
$count = 0;
// 往左遍历
for ($i = $index; $i >= 0; $i--) {
if ($nums[$i] != $target) break;
if ($nums[$i] === $target) {
$count++;
}
}
// 往右遍历
for ($i = $index + 1; $i < count($nums); $i++) {
if ($nums[$i] != $target) break;
if ($nums[$i] === $target) {
$count++;
}
}
return $count;
}
/**
* 二分查找
*
* @param Integer[] $nums
* @param Integer $target
* @return Integer
*/
function binarySearch($nums, $target)
{
$left = 0;
$right = count($nums) - 1;
while ($left <= $right) {
$mid = floor(($left + $right) / 2);
if ($nums[$mid] === $target) {
return $mid;
} elseif ($nums[$mid] > $target) {
$right = $mid - 1;
} else {
$left = $mid + 1;
}
}
return -1;
}
}
思路:
实现:
class Solution
{
/**
* @param ListNode $head
* @param Integer $val
* @return ListNode
*/
function deleteNode($head, $val)
{
if ($head->val === $val) {
return $head->next;
}
$curr = $head;
while ($curr) {
if ($curr->val === $val) {
$prev->next = $curr->next;
return $head;
}
$prev = $curr;
$curr = $curr->next;
}
}
}
实现:
class Solution
{
// 使用数组存储已经计算的结果,避免重复计算
private $map = [];
/**
* @param Integer $n
* @return Integer
*/
function fib($n)
{
if (isset($this->map[$n])) {
return $this->map[$n];
}
if ($n <= 1) {
$this->map[$n] = $n;
return $n;
}
$this->map[$n] = $this->fib($n - 1) + $this->fib($n - 2);
return $this->map[$n] % 1000000007;
}
}
递归法:
class Solution
{
private $list = [];
/**
* @param TreeNode $root
* @return Integer[]
*/
function inorderTraversal($root)
{
if ($root === null) return [];
$this->inorderTraversal($root->left);
$this->list[] = $root->val;
$this->inorderTraversal($root->right);
return $this->list;
}
}
迭代法:
class Solution
{
private $stack = [];
private $list = [];
/**
* @param TreeNode $root
* @return Integer[]
*/
function inorderTraversal($root)
{
if ($root === null) return [];
$curr = $root;
while ($curr !== null || !empty($this->stack)) {
// 不断往左子树方向走,每走一次就将当前节点保存到栈中
while ($curr !== null) {
array_push($this->stack, $curr);
$curr = $curr->left;
}
$node = array_pop($this->stack);
$this->list[] = $node->val;
if ($node->right !== null) {
$curr = $node->right;
}
}
return $this->list;
}
}
36 匹马分 6 个组,分别为 A、B、C、D、E、F 组。
第一轮,每个组各跑一次,取每组前三名,标识为 A1、A2、A3,B1、B2、B3,以此类推。
第二轮,每个组的第一名(A1~F1)拉出来跑一次,假设名次是:A1 第一名,B1 第二名,C1 第三名。
那么:
第三轮,A2、A3、B1、B2、B3、C1 六匹马跑,取前两名。
其中第一轮跑 6 次,第二轮第三轮都各只跑 1 次,一共 8 次。
前言:来自
知乎
的一道面试题。
传送门:剑指 Offer 53 - II 0~n-1 中缺失的数字。
思路:
排序数组中的搜索问题,首先想到二分法解决。
实现:
class Solution
{
/**
* @param Integer[] $nums
* @return Integer
*/
function missingNumber($nums)
{
$left = 0;
$right = count($nums) - 1;
while ($left <= $right) {
$mid = intval(floor(($left + $right) / 2));
if ($nums[$mid] === $mid) {
$left = $mid + 1;
} else {
$right = $mid - 1;
}
}
return $left;
}
}
传送门:剑指 Offer 52 - 两个链表的第一个公共节点。
思路:剑指 Offer 52 - 两个链表的第一个公共节点(双指针,清晰图解)。
实现:
public class Solution
{
public ListNode getIntersectionNode(ListNode headA, ListNode headB)
{
ListNode A = headA;
ListNode B = headB;
while (A != B) {
A = A != null ? A.next : headB;
B = B != null ? B.next : headA;
}
return A;
}
}
递归法:
class Solution
{
private $list = [];
/**
* @param TreeNode $root
* @return Integer[]
*/
function preorderTraversal($root)
{
if ($root === null) return [];
$this->list[] = $root->val;
$this->preorderTraversal($root->left);
$this->preorderTraversal($root->right);
return $this->list;
}
}
公式如下:
前序遍历的递推公式:preOrder(r) = print r->preOrder(r->left)->preOrder(r->right)
中序遍历的递推公式:inOrder(r) = inOrder(r->left)->print r->inOrder(r->right)
后序遍历的递推公式:postOrder(r) = postOrder(r->left)->postOrder(r->right)->print r
要理解递归的思路并且熟练的使用它,就是要想清楚你想做什么,什么时候停止。我们并不需要太过于在意具体的递归过程,而是要想清楚让计算机干什么。计算机都可能溢出,用人脑去遍历就不现实了。
迭代法:
class Solution
{
private $stack = [];
private $list = [];
/**
* @param TreeNode $root
* @return Integer[]
*/
function preorderTraversal($root)
{
if ($root === null) return [];
array_push($this->stack, $root);
while (!empty($this->stack)) {
$node = array_pop($this->stack);
$this->list[] = $node->val;
if ($node->right !== null) {
array_push($this->stack, $node->right);
}
if ($node->left !== null) {
array_push($this->stack, $node->left);
}
}
return $this->list;
}
}
传送门:两数之和。
暴力法:
function twoSum($nums, $target) {
$len = count($nums);
for ($i = 0; $i < $len - 1; $i++) {
for ($j = $i + 1; $j < $len; $j++) {
if ($nums[$i] + $nums[$j] === $target) {
return [$i, $j];
}
}
}
}
Map 法:
function twoSum($nums, $target) {
$map = [];
$len = count($nums);
for ($i = 0; $i < $len; $i++) {
// 当前数字和目标和的差值
$diff = $target - $nums[$i];
if (isset($map[$diff])) {
return [$map[$diff], $i];
}
$map[$nums[$i]] = $i;
}
}
思路 1:哈希表法。
class Solution
{
function findRepeatNumber($nums)
{
$map = [];
foreach ($nums as $k => $v) {
if (isset($map[$v])) {
return $v;
}
$map[$v] = $v;
}
}
}
思路 2:原地交换。
题目说明尚未被充分使用,即 在一个长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内 。 此说明含义:数组元素的索引 和值是一对多
的关系。
因此,可遍历数组并通过交换操作,使元素的索引与值一一对应(即 nums[i] = i )。因而,就能通过索引映射对应的值,起到与字典等价的作用。
class Solution
{
function findRepeatNumber($nums)
{
$len = count($nums);
$i = 0;
while ($i < $len) {
if ($nums[$i] === $i) {
$i++;
continue;
}
if ($nums[$nums[$i]] === $nums[$i]) {
return $nums[$i];
}
// 交换
$tmp = $nums[$nums[$i]];
$nums[$nums[$i]] = $nums[$i];
$nums[$i] = $tmp;
}
}
}
传送门:leetcode - 231 2 的幂。
方法 1:暴力法。
/**
* @param Integer $n
* @return Boolean
*/
function isPowerOfTwo($n) {
if ($n === 1) return true;
$res = 1;
while ($res <= $n) {
$res *= 2;
if ($res === $n) return true;
}
return false;
}
方法 2:位运算法。
/**
* @param Integer $n
* @return Boolean
*/
function isPowerOfTwo($n) {
return $n > 0 && ($n & ($n - 1)) === 0;
}
传送门:剑指 Offer 30 - 包含 min 函数的栈。
思路:
普通栈的 push() 和 pop() 函数的复杂度为 O(1);而获取栈最小值 min() 函数需要遍历整个栈,复杂度为 O(N)。
本题难点: 将 min() 函数复杂度降为 O(1),可通过建立辅助栈实现;
因此,只需设法维护好栈 B 的元素,使其保持非严格降序,即可实现 min() 函数的 O(1) 复杂度。
实现:
<?php
class MinStack
{
private $stack;
private $help;
/**
* initialize your data structure here.
*/
function __construct()
{
$this->stack = new SplStack();
$this->help = new SplStack();
}
/**
* @param Integer $x
* @return NULL
*/
function push($x)
{
$this->stack->push($x);
if ($this->help->isEmpty() || $this->help->top() >= $x) {
$this->help->push($x);
}
}
/**
* @return NULL
*/
function pop()
{
if ($this->stack->isEmpty()) return;
$element = $this->stack->pop();
if ($element === $this->help->top()) {
$this->help->pop();
}
}
/**
* @return Integer
*/
function top()
{
return $this->stack->top();
}
/**
* @return Integer
*/
function min()
{
return $this->help->top();
}
}
思路 1:暴力法。
class Solution
{
/**
* @param Integer[] $numbers
* @return Integer
*/
function minArray($numbers)
{
$min = $numbers[0];
for ($i = 1; $i < count($numbers); $i++) {
if ($numbers[$i] < $min) {
return $numbers[$i];
}
}
return $min;
}
}
思路 2:二分法。
如下图所示,寻找旋转数组的最小元素即为寻找右排序数组的首个元素 nums[x],称 x 为 旋转点 。
排序数组的查找问题首先考虑使用二分法解决,其可将遍历法的线性级别时间复杂度降低至对数级别 。
算法流程:
class Solution
{
/**
* @param Integer[] $numbers
* @return Integer
*/
function minArray($numbers)
{
$left = 0;
$right = count($numbers) - 1;
while ($left < $right) {
$mid = floor(($left + $right) / 2);
if ($numbers[$mid] > $numbers[$right]) {
$left = $mid + 1;
} elseif ($numbers[$mid] < $numbers[$right]) {
$right = $mid;
} else {
$right--;
}
}
return $numbers[$left];
}
}
传送门:剑指 Offer 05 - 替换空格。
class Solution
{
/**
* @param String $s
* @return String
*/
function replaceSpace($s)
{
return implode('%20', explode(' ', $s));
}
}
传送门:leetcode-876 链表的中间结点。
思路:用两个指针 slow 与 fast 一起遍历链表。slow 一次走一步,fast 一次走两步。那么当 fast 到达链表的末尾时,slow 必然位于中间。
实现:
<?php
/**
* Definition for a singly-linked list.
* class ListNode {
* public $val = 0;
* public $next = null;
* function __construct($val = 0, $next = null) {
* $this->val = $val;
* $this->next = $next;
* }
* }
*/
class Solution
{
/**
* @param ListNode $head
* @return ListNode
*/
function middleNode($head)
{
$fast = $head;
$slow = $head;
while ($fast !== null && $fast->next !== null) {
$fast = $fast->next->next;
$slow = $slow->next;
}
return $slow;
}
}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.