重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
使用go语言的好处: go语言的设计是务实的, go在针对并发上进行了优化, 并且支持大规模高并发, 又由于单一的码格式, 相比于其他语言更具有可读性, 在垃圾回收上比java和Python更有效, 因为他是和程序同时执行的.
创新互联专注于企业全网营销推广、网站重做改版、伊川网站定制设计、自适应品牌网站建设、HTML5、成都做商城网站、集团公司官网建设、外贸营销网站建设、高端网站制作、响应式网页设计等建站业务,价格优惠性价比高,为伊川等各大城市提供网站开发制作服务。
1. 进程, 线程, 协程的区别, 协程的优势
2. 讲一下GMP模型(重点)
3. Go的GC, 混合写屏障(重点)
4. go的Slice和数组的区别, slice的扩容原理(重点)
5. 讲一下channel,实现原理(重点)
6. 讲一下Go的Map的实现原理, 是否线程安全, 如何实现安全(重点)
7. new 和 make 的区别
8. 说一下内存逃逸
9. 函数传指针和传值有什么区别
10. goroutine之间的通信方式
11. 测试是怎么做的(单元测试, 压力测试)
12. 堆和栈的区别
原文:【 】
如果有解答的不对的,麻烦各位在评论写出来~
go的调度原理是基于GMP模型,G代表一个goroutine,不限制数量;M=machine,代表一个线程,最大1万,所有G任务还是在M上执行;P=processor代表一个处理器,每一个允许的M都会绑定一个G,默认与逻辑CPU数量相等(通过runtime.GOMAXPROCS(runtime.NumCPU())设置)。
go调用过程:
可以能,也可以不能。
因为go存在不能使用==判断类型:map、slice,如果struct包含这些类型的字段,则不能比较。
这两种类型也不能作为map的key。
类似栈操作,后进先出。
因为go的return是一个非原子性操作,比如语句 return i ,实际上分两步进行,即将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机正是跳转前,所以说defer执行时还是有机会操作返回值的。
select的case的表达式必须是一个channel类型,所有case都会被求值,求值顺序自上而下,从左至右。如果多个case可以完成,则会随机执行一个case,如果有default分支,则执行default分支语句。如果连default都没有,则select语句会一直阻塞,直到至少有一个IO操作可以进行。
break关键字可跳出select的执行。
goroutine管理、信息传递。context的意思是上下文,在线程、协程中都有这个概念,它指的是程序单元的一个运行状态、现场、快照,包含。context在多个goroutine中是并发安全的。
应用场景:
例子参考:
waitgroup
channel
len:切片的长度,访问时间复杂度为O(1),go的slice底层是对数组的引用。
cap:切片的容量,扩容是以这个值为标准。默认扩容是2倍,当达到1024的长度后,按1.25倍。
扩容:每次扩容slice底层都将先分配新的容量的内存空间,再将老的数组拷贝到新的内存空间,因为这个操作不是并发安全的。所以并发进行append操作,读到内存中的老数组可能为同一个,最终导致append的数据丢失。
共享:slice的底层是对数组的引用,因此如果两个切片引用了同一个数组片段,就会形成共享底层数组。当sliec发生内存的重新分配(如扩容)时,会对共享进行隔断。详细见下面例子:
make([]Type,len,cap)
map的底层是hash table(hmap类型),对key值进行了hash,并将结果的低八位用于确定key/value存在于哪个bucket(bmap类型)。再将高八位与bucket的tophash进行依次比较,确定是否存在。出现hash冲撞时,会通过bucket的overflow指向另一个bucket,形成一个单向链表。每个bucket存储8个键值对。
如果要实现map的顺序读取,需要使用一个slice来存储map的key并按照顺序进行排序。
利用map,如果要求并发安全,就用sync.map
要注意下set中的delete函数需要使用 delete(map) 来实现,但是这个并不会释放内存,除非value也是一个子map。当进行多次delete后,可以使用make来重建map。
使用sync.Map来管理topic,用channel来做队列。
参考:
多路归并法:
pre class="vditor-reset" placeholder="" contenteditable="true" spellcheck="false"p data-block="0"(1)假设有K路a href=""数据流/a,流内部是有序的,且流间同为升序或降序;
/pp data-block="0"(2)首先读取每个流的第一个数,如果已经EOF,pass;
/pp data-block="0"(3)将有效的k(k可能小于K)个数比较,选出最小的那路mink,输出,读取mink的下一个;
/pp data-block="0"(4)直到所有K路都EOF。
/p/pre
假设文件又1个G,内存只有256M,无法将1个G的文件全部读到内存进行排序。
第一步:
可以分为10段读取,每段读取100M的数据并排序好写入硬盘。
假设写入后的文件为A,B,C...10
第二步:
将A,B,C...10的第一个字符拿出来,对这10个字符进行排序,并将结果写入硬盘,同时记录被写入的字符的文件指针P。
第三步:
将刚刚排序好的9个字符再加上从指针P读取到的P+1位数据进行排序,并写入硬盘。
重复二、三步骤。
go文件读写参考:
保证排序前两个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同的排序叫稳定排序。
快速排序、希尔排序、堆排序、直接选择排序不是稳定的排序算法。
基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法。
参考:
head只请求页面的首部。多用来判断网页是否被修改和超链接的有效性。
get请求页面信息,并返回实例的主体。
参考:
401:未授权的访问。
403: 拒绝访问。
普通的http连接是客户端连接上服务端,然后结束请求后,由客户端或者服务端进行http连接的关闭。下次再发送请求的时候,客户端再发起一个连接,传送数据,关闭连接。这么个流程反复。但是一旦客户端发送connection:keep-alive头给服务端,且服务端也接受这个keep-alive的话,两边对上暗号,这个连接就可以复用了,一个http处理完之后,另外一个http数据直接从这个连接走了。减少新建和断开TCP连接的消耗。这个可以在Nginx设置,
这个keepalive_timout时间值意味着:一个http产生的tcp连接在传送完最后一个响应后,还需要hold住keepalive_timeout秒后,才开始关闭这个连接。
特别注意TCP层的keep alive和http不是一个意思。TCP的是指:tcp连接建立后,如果客户端很长一段时间不发送消息,当连接很久没有收到报文,tcp会主动发送一个为空的报文(侦测包)给对方,如果对方收到了并且回复了,证明对方还在。如果对方没有报文返回,重试多次之后则确认连接丢失,断开连接。
tcp的keep alive可通过
net.ipv4.tcp_keepalive_intvl = 75 // 当探测没有确认时,重新发送探测的频度。缺省是75秒。
net.ipv4.tcp_keepalive_probes = 9 //在认定连接失效之前,发送多少个TCP的keepalive探测包。缺省值是9。这个值乘以tcp_keepalive_intvl之后决定了,一个连接发送了keepalive之后可以有多少时间没有回应
net.ipv4.tcp_keepalive_time = 7200 //当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时。一般设置为30分钟1800
修改:
可以
tcp是面向连接的,upd是无连接状态的。
udp相比tcp没有建立连接的过程,所以更快,同时也更安全,不容易被攻击。upd没有阻塞控制,因此出现网络阻塞不会使源主机的发送效率降低。upd支持一对多,多对多等,tcp是点对点传输。tcp首部开销20字节,udp8字节。
udp使用场景:视频通话、im聊天等。
time-wait表示客户端等待服务端返回关闭信息的状态,closed_wait表示服务端得知客户端想要关闭连接,进入半关闭状态并返回一段TCP报文。
time-wait作用:
解决办法:
close_wait:
被动关闭,通常是由于客户端忘记关闭tcp连接导致。
根据业务来啊~
重要指标是cardinality(不重复数量),这个数量/总行数如果过小(趋近于0)代表索引基本没意义,比如sex性别这种。
另外查询不要使用select *,根据select的条件+where条件做组合索引,尽量实现覆盖索引,避免回表。
僵尸进程:
即子进程先于父进程退出后,子进程的PCB需要其父进程释放,但是父进程并没有释放子进程的PCB,这样的子进程就称为僵尸进程,僵尸进程实际上是一个已经死掉的进程。
孤儿进程:
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
子进程死亡需要父进程来处理,那么意味着正常的进程应该是子进程先于父进程死亡。当父进程先于子进程死亡时,子进程死亡时没父进程处理,这个死亡的子进程就是孤儿进程。
但孤儿进程与僵尸进程不同的是,由于父进程已经死亡,系统会帮助父进程回收处理孤儿进程。所以孤儿进程实际上是不占用资源的,因为它终究是被系统回收了。不会像僵尸进程那样占用ID,损害运行系统。
原文链接:
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免方法:
端口占用:lsof -i:端口号 或者 nestat
cpu、内存占用:top
发送信号:kill -l 列出所有信号,然后用 kill [信号变化] [进程号]来执行。如kill -9 453。强制杀死453进程
git log:查看提交记录
git diff :查看变更记录
git merge:目标分支改变,而源分支保持原样。优点:保留提交历史,保留分支结构。但会有大量的merge记录
git rebase:将修改拼接到最新,复杂的记录变得优雅,单个操作变得(revert)很简单;缺点:
git revert:反做指定版本,会新生成一个版本
git reset:重置到某个版本,中间版本全部丢失
etcd、Consul
pprof
节省空间(非叶子节点不存储数据,相对b tree的优势),减少I/O次数(节省的空间全部存指针地址,让树变的矮胖),范围查找方便(相对hash的优势)。
explain
其他的见:
runtime2.go 中关于 p 的定义: 其中 runnext 指针决定了下一个要运行的 g,根据英文的注释大致意思是说:
所以当设置 runtime.GOMAXPROCS(1) 时,此时只有一个 P,创建的 g 依次加入 P, 当最后一个即 i==9 时,加入的最后 一个 g 将会继承当前主 goroutinue 的剩余时间片继续执行,所以会先输出 9, 之后再依次执行 P 队列中其它的 g。
方法一:
方法二:
[图片上传失败...(image-4ef445-1594976286098)]
方法1:to_days,返回给的日期从0开始算的天数。
方法2:data_add。向日期添加指定时间间隔
[图片上传失败...(image-b67b10-1594976286098)]
本文目录如下,阅读本文后,将一网打尽下面Golang Map相关面试题
Go中的map是一个指针,占用8个字节,指向hmap结构体; 源码 src/runtime/map.go 中可以看到map的底层结构
每个map的底层结构是hmap,hmap包含若干个结构为bmap的bucket数组。每个bucket底层都采用链表结构。接下来,我们来详细看下map的结构
bmap 就是我们常说的“桶”,一个桶里面会最多装 8 个 key,这些 key 之所以会落入同一个桶,是因为它们经过哈希计算后,哈希结果是“一类”的,关于key的定位我们在map的查询和插入中详细说明。在桶内,又会根据 key 计算出来的 hash 值的高 8 位来决定 key 到底落入桶内的哪个位置(一个桶内最多有8个位置)。
bucket内存数据结构可视化如下:
注意到 key 和 value 是各自放在一起的,并不是 key/value/key/value/... 这样的形式。源码里说明这样的好处是在某些情况下可以省略掉 padding字段,节省内存空间。
当 map 的 key 和 value 都不是指针,并且 size 都小于 128 字节的情况下,会把 bmap 标记为不含指针,这样可以避免 gc 时扫描整个 hmap。但是,我们看 bmap 其实有一个 overflow 的字段,是指针类型的,破坏了 bmap 不含指针的设想,这时会把 overflow 移动到 extra 字段来。
map是个指针,底层指向hmap,所以是个引用类型
golang 有三个常用的高级类型 slice 、map、channel, 它们都是 引用类型 ,当引用类型作为函数参数时,可能会修改原内容数据。
golang 中没有引用传递,只有值和指针传递。所以 map 作为函数实参传递时本质上也是值传递,只不过因为 map 底层数据结构是通过指针指向实际的元素存储空间,在被调函数中修改 map,对调用者同样可见,所以 map 作为函数实参传递时表现出了引用传递的效果。
因此,传递 map 时,如果想修改map的内容而不是map本身,函数形参无需使用指针
map 底层数据结构是通过指针指向实际的元素 存储空间 ,这种情况下,对其中一个map的更改,会影响到其他map
map 在没有被修改的情况下,使用 range 多次遍历 map 时输出的 key 和 value 的顺序可能不同。这是 Go 语言的设计者们有意为之,在每次 range 时的顺序被随机化,旨在提示开发者们,Go 底层实现并不保证 map 遍历顺序稳定,请大家不要依赖 range 遍历结果顺序。
map 本身是无序的,且遍历时顺序还会被随机化,如果想顺序遍历 map,需要对 map key 先排序,再按照 key 的顺序遍历 map。
map默认是并发不安全的,原因如下:
Go 官方在经过了长时间的讨论后,认为 Go map 更应适配典型使用场景(不需要从多个 goroutine 中进行安全访问),而不是为了小部分情况(并发访问),导致大部分程序付出加锁代价(性能),决定了不支持。
场景: 2个协程同时读和写,以下程序会出现致命错误:fatal error: concurrent map writes
如果想实现map线程安全,有两种方式:
方式一:使用读写锁 map + sync.RWMutex
方式二:使用golang提供的 sync.Map
sync.map是用读写分离实现的,其思想是空间换时间。和map+RWLock的实现方式相比,它做了一些优化:可以无锁访问read map,而且会优先操作read map,倘若只操作read map就可以满足要求(增删改查遍历),那就不用去操作write map(它的读写都要加锁),所以在某些特定场景中它发生锁竞争的频率会远远小于map+RWLock的实现方式。
golang中map是一个kv对集合。底层使用hash table,用链表来解决冲突 ,出现冲突时,不是每一个key都申请一个结构通过链表串起来,而是以bmap为最小粒度挂载,一个bmap可以放8个kv。在哈希函数的选择上,会在程序启动时,检测 cpu 是否支持 aes,如果支持,则使用 aes hash,否则使用 memhash。
map有3钟初始化方式,一般通过make方式创建
map的创建通过生成汇编码可以知道,make创建map时调用的底层函数是 runtime.makemap 。如果你的map初始容量小于等于8会发现走的是 runtime.fastrand 是因为容量小于8时不需要生成多个桶,一个桶的容量就可以满足
makemap函数会通过 fastrand 创建一个随机的哈希种子,然后根据传入的 hint 计算出需要的最小需要的桶的数量,最后再使用 makeBucketArray 创建用于保存桶的数组,这个方法其实就是根据传入的 B 计算出的需要创建的桶数量在内存中分配一片连续的空间用于存储数据,在创建桶的过程中还会额外创建一些用于保存溢出数据的桶,数量是 2^(B-4) 个。初始化完成返回hmap指针。
找到一个 B,使得 map 的装载因子在正常范围内
Go 语言中读取 map 有两种语法:带 comma 和 不带 comma。当要查询的 key 不在 map 里,带 comma 的用法会返回一个 bool 型变量提示 key 是否在 map 中;而不带 comma 的语句则会返回一个 value 类型的零值。如果 value 是 int 型就会返回 0,如果 value 是 string 类型,就会返回空字符串。
map的查找通过生成汇编码可以知道,根据 key 的不同类型,编译器会将查找函数用更具体的函数替换,以优化效率:
函数首先会检查 map 的标志位 flags。如果 flags 的写标志位此时被置 1 了,说明有其他协程在执行“写”操作,进而导致程序 panic。这也说明了 map 对协程是不安全的。
key经过哈希函数计算后,得到的哈希值如下(主流64位机下共 64 个 bit 位):
m: 桶的个数
从buckets 通过 hash m 得到对应的bucket,如果bucket正在扩容,并且没有扩容完成,则从oldbuckets得到对应的bucket
计算hash所在桶编号:
用上一步哈希值最后的 5 个 bit 位,也就是 01010 ,值为 10,也就是 10 号桶(范围是0~31号桶)
计算hash所在的槽位:
用上一步哈希值哈希值的高8个bit 位,也就是 10010111 ,转化为十进制,也就是151,在 10 号 bucket 中寻找** tophash 值(HOB hash)为 151* 的 槽位**,即为key所在位置,找到了 2 号槽位,这样整个查找过程就结束了。
如果在 bucket 中没找到,并且 overflow 不为空,还要继续去 overflow bucket 中寻找,直到找到或是所有的 key 槽位都找遍了,包括所有的 overflow bucket。
通过上面找到了对应的槽位,这里我们再详细分析下key/value值是如何获取的:
bucket 里 key 的起始地址就是 unsafe.Pointer(b)+dataOffset。第 i 个 key 的地址就要在此基础上跨过 i 个 key 的大小;而我们又知道,value 的地址是在所有 key 之后,因此第 i 个 value 的地址还需要加上所有 key 的偏移。
通过汇编语言可以看到,向 map 中插入或者修改 key,最终调用的是 mapassign 函数。
实际上插入或修改 key 的语法是一样的,只不过前者操作的 key 在 map 中不存在,而后者操作的 key 存在 map 中。
mapassign 有一个系列的函数,根据 key 类型的不同,编译器会将其优化为相应的“快速函数”。
我们只用研究最一般的赋值函数 mapassign 。
map的赋值会附带着map的扩容和迁移,map的扩容只是将底层数组扩大了一倍,并没有进行数据的转移,数据的转移是在扩容后逐步进行的,在迁移的过程中每进行一次赋值(access或者delete)会至少做一次迁移工作。
1.判断map是否为nil
每一次进行赋值/删除操作时,只要oldbuckets != nil 则认为正在扩容,会做一次迁移工作,下面会详细说下迁移过程
根据上面查找过程,查找key所在位置,如果找到则更新,没找到则找空位插入即可
经过前面迭代寻找动作,若没有找到可插入的位置,意味着需要扩容进行插入,下面会详细说下扩容过程
通过汇编语言可以看到,向 map 中删除 key,最终调用的是 mapdelete 函数
删除的逻辑相对比较简单,大多函数在赋值操作中已经用到过,核心还是找到 key 的具体位置。寻找过程都是类似的,在 bucket 中挨个 cell 寻找。找到对应位置后,对 key 或者 value 进行“清零”操作,将 count 值减 1,将对应位置的 tophash 值置成 Empty
再来说触发 map 扩容的时机:在向 map 插入新 key 的时候,会进行条件检测,符合下面这 2 个条件,就会触发扩容:
1、装载因子超过阈值
源码里定义的阈值是 6.5 (loadFactorNum/loadFactorDen),是经过测试后取出的一个比较合理的因子
我们知道,每个 bucket 有 8 个空位,在没有溢出,且所有的桶都装满了的情况下,装载因子算出来的结果是 8。因此当装载因子超过 6.5 时,表明很多 bucket 都快要装满了,查找效率和插入效率都变低了。在这个时候进行扩容是有必要的。
对于条件 1,元素太多,而 bucket 数量太少,很简单:将 B 加 1,bucket 最大数量( 2^B )直接变成原来 bucket 数量的 2 倍。于是,就有新老 bucket 了。注意,这时候元素都在老 bucket 里,还没迁移到新的 bucket 来。新 bucket 只是最大数量变为原来最大数量的 2 倍( 2^B * 2 ) 。
2、overflow 的 bucket 数量过多
在装载因子比较小的情况下,这时候 map 的查找和插入效率也很低,而第 1 点识别不出来这种情况。表面现象就是计算装载因子的分子比较小,即 map 里元素总数少,但是 bucket 数量多(真实分配的 bucket 数量多,包括大量的 overflow bucket)
不难想像造成这种情况的原因:不停地插入、删除元素。先插入很多元素,导致创建了很多 bucket,但是装载因子达不到第 1 点的临界值,未触发扩容来缓解这种情况。之后,删除元素降低元素总数量,再插入很多元素,导致创建很多的 overflow bucket,但就是不会触发第 1 点的规定,你能拿我怎么办?overflow bucket 数量太多,导致 key 会很分散,查找插入效率低得吓人,因此出台第 2 点规定。这就像是一座空城,房子很多,但是住户很少,都分散了,找起人来很困难
对于条件 2,其实元素没那么多,但是 overflow bucket 数特别多,说明很多 bucket 都没装满。解决办法就是开辟一个新 bucket 空间,将老 bucket 中的元素移动到新 bucket,使得同一个 bucket 中的 key 排列地更紧密。这样,原来,在 overflow bucket 中的 key 可以移动到 bucket 中来。结果是节省空间,提高 bucket 利用率,map 的查找和插入效率自然就会提升。
由于 map 扩容需要将原有的 key/value 重新搬迁到新的内存地址,如果有大量的 key/value 需要搬迁,会非常影响性能。因此 Go map 的扩容采取了一种称为“渐进式”的方式,原有的 key 并不会一次性搬迁完毕,每次最多只会搬迁 2 个 bucket。
上面说的 hashGrow() 函数实际上并没有真正地“搬迁”,它只是分配好了新的 buckets,并将老的 buckets 挂到了 oldbuckets 字段上。真正搬迁 buckets 的动作在 growWork() 函数中,而调用 growWork() 函数的动作是在 mapassign 和 mapdelete 函数中。也就是插入或修改、删除 key 的时候,都会尝试进行搬迁 buckets 的工作。先检查 oldbuckets 是否搬迁完毕,具体来说就是检查 oldbuckets 是否为 nil。
如果未迁移完毕,赋值/删除的时候,扩容完毕后(预分配内存),不会马上就进行迁移。而是采取 增量扩容 的方式,当有访问到具体 bukcet 时,才会逐渐的进行迁移(将 oldbucket 迁移到 bucket)
nevacuate 标识的是当前的进度,如果都搬迁完,应该和2^B的长度是一样的
在evacuate 方法实现是把这个位置对应的bucket,以及其冲突链上的数据都转移到新的buckets上。
转移的判断直接通过tophash 就可以,判断tophash中第一个hash值即可
遍历的过程,就是按顺序遍历 bucket,同时按顺序遍历 bucket 中的 key。
map遍历是无序的,如果想实现有序遍历,可以先对key进行排序
为什么遍历 map 是无序的?
如果发生过迁移,key 的位置发生了重大的变化,有些 key 飞上高枝,有些 key 则原地不动。这样,遍历 map 的结果就不可能按原来的顺序了。
如果就一个写死的 map,不会向 map 进行插入删除的操作,按理说每次遍历这样的 map 都会返回一个固定顺序的 key/value 序列吧。但是 Go 杜绝了这种做法,因为这样会给新手程序员带来误解,以为这是一定会发生的事情,在某些情况下,可能会酿成大错。
Go 做得更绝,当我们在遍历 map 时,并不是固定地从 0 号 bucket 开始遍历,每次都是从一个**随机值序号的 bucket 开始遍历,并且是从这个 bucket 的一个 随机序号的 cell **开始遍历。这样,即使你是一个写死的 map,仅仅只是遍历它,也不太可能会返回一个固定序列的 key/value 对了。
应聘程序员,在技术面试的时候,结束时面试官通常会问一个问题:你还有什么问题吗?众所周知,面对这个问题不能直接说没问题了,因为这是你掰回一句或者加深认可的好机会。但是下面这4个问题在技术面试时最好不要问:
1、“我能拿多少工资?”
注意你参加的是技术面试,尽量不要问跟技术不相关的东西,这在技术面试的过程中是一个减分项。一般面试官如果对你有兴趣会主动地询问你的理想薪资。
2、“五险一金有没有?交通补助有没有?”
这个问题一般不建议去问,这些问题在技术面试后人事会主动告诉你或者自己主动去询问人事都可以的,但是在技术面试官面前,问这些跟他本职工作没有关系的问题会让面试官觉得不耐烦。
3、“公司经常加班吗?”
作为开发人员加班的情况肯定是会有的,只是经不经常可能得看公司的实际情况。在面试时问这个问题你可能只是想了解一下公司的加班情况,但却会让面试官质疑你的抗压能力,给面试留下不好的印象。
4、“您觉得我今天能面上吗?”
有些小伙伴可能急于求职,所以会有些迫切地问这个问题。如果面试官觉得你有希望肯定会给你一些信号,如果面试官不看好你,问这个问题可能让双方都比较尴尬。
上面说了4个不该问的问题, 那在面试官问“你还有什么问题吗?”时应该问一些什么问题?
再次点题,在技术面试最好提跟技术相关或跟本职工作相关的的问题。第一,可以问一下关于产品的问题,比如一下产品用的什么技术,想回去了解一下,或者关于一些新的技术比如大数据、spring boot公司是怎么用的。这些问题既能让面试官有兴趣回答,又能展现你的知识面。
大家在面试的时候,难免会遇到让人摸不着头脑的逻辑题,这类题目让同学们往往连答案应该回答些什么都摸不清楚,只能和面试官四目相对,非常尴尬。
其实,很多面试的考官,都是从题库随机挑选逻辑题来考验同学们,面试官有时候自己也未必完全摸透这类题目,所以面试的时候不必过于紧张,就算答不出来啊也非常正常。
在我的理解中,这类题目主要还是考大家的思路,至于答案标准与否,其实不是特别重要。
本文总结了面试中我自己面试中遇到的几道非常常见的逻辑题,大家可以作为面试前的突击复习材料。
一群人开舞会,每人头上都戴着一顶帽子。帽子只有黑白两种,黑的至少有一顶。每个人都能看到其它人帽子的颜色,却看不到自己的。主持人先让大家看看别人头上戴的是什么帽子,然后关灯,如果有人认为自己戴的是黑帽子,就打自己一个耳光。第一次关灯,没有声音。于是再开灯,大家再看一遍,关灯时仍然鸦雀无声。一直到第三次关灯,才有劈劈啪啪打耳光的声音响起。问有多少人戴着黑帽子?
三个人
若是两个人,设A、B是黑帽子,第二次关灯就会有人打耳光。原因是A看到B第一次没打耳光,就知道B也一定看到了有带黑帽子的人,可A除了知道B带黑帽子外,其他人都是白帽子,就可推出他自己是带黑帽子的人!同理B也是这么想的,这样第二次熄灯会有两个耳光的声音。
如果是三个人,A,B,C。A第一次没打耳光,因为他看到B,C都是带黑帽子的;而且假设自己带的是白帽子,这样只有BC戴的是黑帽子;按照只有两个人带黑帽子的推论,第二次应该有人打耳光;可第二次却没有...于是他知道B和C一定看到了除BC之外的其他人带了黑帽子,于是他知道BC看到的那个人一定是他,所以第三次有三个人打了自己一个耳光
N个人是黑帽子,就会在第N天,有N个人打自己一个耳光。
一个是两种药片,每种有两个,一个人需要早上吃两种药片各一个,现在这四个药片混在一起了这个人什么方法吃。
把所有的4颗药丸都切开成相等的两半,然后早上和晚上,分别吃掉每颗药丸的一半
一个5L,一个6L的瓶子,要得到3L的水,问什么方法
6-5=1 1L水放在5L那个瓶里面,然后再装6L水,往5L(里面已经有1L)里面倒,这样就会剩下2L水在6L里面,再把2L水放在5L里面,再装一次,不就可以6L那里到处3L水到5L里面,自己就剩下3L了
一共1000瓶酒,其中一瓶有毒。如果一只老鼠喝了有毒的酒,会在一天之后死亡,那么如果给你一天时间,然你判定哪瓶酒有毒,至少需要几只老鼠?
答案是10只。这个需要使用二进制编码来解决,1000瓶酒至少需要10位二进制数来进行编码。然后取十只杯子分别代表这是个二进制数的十个位,分别将1000瓶酒倒入其编码为1的对应的杯子中。取十个老鼠分别喝十个杯子中的酒,一天之后,就可以根据喝哪些杯子的老鼠死掉来确定出有毒的那瓶酒的编码,从而确定哪瓶酒有毒。其根据就是只有有毒酒的编码对应的毒死老鼠的杯子位置。这个题目就是利用了二进制编码的一些特性。
还有一些其他的题目也使用这些特性,比如使用特殊的位运算,一般使用比较多的位运算就是与、或和异或。
这样,就可以对应到现实生活中的一些为题,比如一个类似的问题原本我们想需要用900多台服务器来解决,经过这样分析后就可以使用10台服务器来解决,大大节约了成本。
再比如,国王有10000桶酒,已知一桶酒有毒,喝了之后一定会在23-24小时内死亡(例如0点喝,会在23-第二天0点这个时间段死亡)。现在国王要在48小时后举办一个宴会,需要用罪犯实验,请问最少几个罪犯。(可以混合酒)
如果是常规利用二进制解题的话,那就需要14个犯人,2^14=1638410000,但是这样一来死亡时间这个条件就用不到,也不是最优解。
应该利用酒死的时间是固定的,一个罪犯像上面那样可以表示成25种状态,三个罪犯就可以表示25 x 25 x25种状态,超过10000了,所以只需要三个罪犯。
有8个小球,其中七个的重量是相同的,有一个较轻。给你一个天平,问秤几次能找出那个较轻的小球,若天平只能秤两次,又该怎么秤
第一次两边各放随机三个,如果平了,则另外一个是轻的,若不平,还有第二次,拿出那三个轻的,在两边随机放一个,就能测出哪个最轻了。
本体图解参考:
已知: 每个飞机只有一个油箱,飞机之间可以相互加油(注意是相互,没有单独的加油机),一箱油可供一架飞机绕地球飞半圈
问题:为使至少一架飞机绕地球一圈回到起飞时的飞机场,至少需要出动几架飞机?(所有飞机从同一机场起飞,而且必须安全返回机场,不允许中途降落,中间没有飞机场)
分为3架飞机5架次和3架飞机6架次
1. 3架飞机6架次
(上图)ABC 3架同时起飞
(上图)1/8处,C给AB加满油,C返航。此时飞机的油量分别是:A: 3/4, B: 3/4, C: 3/4。此时C分别给A和B加满油,三架飞机当前油量分别是:A: 1, B: 1, C: 1/4。C返回机场。A、B继续向前飞行。
(上图)1/4处,B给A加满油,B返航,A到达1/2处,此时C已经返回机场,三家飞机此时油量分别是:A: 3/4, B: 3/4, C: 0。此时B给A加满油,C加满油,此时三架飞机的油量分别是:A: 1, B: 1/2, C: 1。然后B返回机场,A继续向前飞行。
(上图)当A飞行至半圈位置时,B已经返回机场并且加满了油(假设加油时间为0),此时,B和C沿逆时针方向飞行,三架飞机当前油量分别是:A: 1/2, B: 1, C: 1。A继续向前飞行。
(上图)当A飞行至另外半圈的1/4位置时,三架飞机剩余油量分别是:A: 1/4, B: 3/4, C: 3/4。此时,C给B加满油。此时三架飞机油量分别是:A: 1/4, B: 1, C: 1/2。C返回机场,B和A继续向前飞行。
当A飞行至另外半圈的1/2位置时,C已经返回机场,A和B相遇,此时三架飞机剩余油量分别是:A: 0, B: 3/4, C: 0。B给A加1/4的油,三架飞机剩余油量:A: 1/4, B: 1/2, C: 1。C加满油从机场逆时针飞出,B返回机场,A继续向前飞行。
(上图)当A飞行至另外半圈的3/4位置时,A和C相遇。此时三架飞机的油量分别是:A: 0, B: 1/4, C: 3/4。C给A加1/4的油,此时三架飞机的油量分别是:A: 1/4, B: 1/4, C: 1/2。C掉头返回机场,A和B继续向前飞行。
(上图)三架飞机顺利回到机场!
2. 3飞机5架次
(1)3 架飞机同时从机场出发,飞行八分之一周(A点),各耗油四分之一。此时某架飞机给其余两架补满油,自己返回基地;
(2)另一架飞机和目标机结伴,飞至四分之一周(B点),给目标机补满油,自己返回;
(3)目标机独自飞行半周(C点);
(4)与从基地反向出发的一架飞机相遇,2 机将油平分,飞至最后八分之一处(D点);
(5)与从基地反向出发的另一机相遇,各分四分之一油,返回。
75道程序员面试逻辑题和答案