面试经典150题——生命游戏
"Push yourself, because no one else is going to do it for you." - Unknown

1. 题目描述

2. 题目分析与解析
2.1 思路一——暴力求解
之所以先暴力求解,是因为我开始也没什么更好的思路,所以就先写一种解决方案,没准写着写着就来新的灵感了。暴力求解思路还是很简单的,就是尝试遍历面板的每个格子,判断其周围八个位置的状态(对于边角需要特殊处理),根据边角种存在的活细胞(也就是1的个数)判断该位置应该填什么。

但是需要注意一点,为了避免我们在原矩阵上更改值后导致影响后续的判断,所以我们肯定需要先复制一个原始矩阵。
代码思路:
-
初始化,复制一个原始矩阵
-
遍历复制矩阵的每一个元素,查看其周围八个位置的状态,统计1的个数
-
根据题目提到的判定规则:少于 2 个或者大于 3 个 1 就判定当前位置为 0
-
等于 2 个 1 那么当前位置不需要更改
-
如果等于 3 个 1 那么当前位置如果为 0 需要改为 1
-
对于边角位置需要额外处理防止越界
-
2.2 思路二——进阶(原地算法)

根据题目中的进阶提示,要求使用原地算法,也就是不能用一个额外的面板存储现有的值,并且还提示了所有格子被同时更新。因此我们再想一想怎么解决。
如果使用原地算法,最主要的问题就是对于前面内容的更新会影响后面的结果,因为你不知道原来前面的内容是什么样子。但是记住,原始状态只有两种,要么是0,要么是1。
而变化也只有四种
-
要么原来是0,后来变成1
-
要么原来是0,保持不变为0
-
要么原来是1,后来变成0
-
要么原来是1,后来不变为1
如下图:

因为我们担心原始信息被覆盖,因此我们是不是可以添加几个数字也就相当于几种状态,来存储这些被覆盖的信息?这样我们看见某一个数字就知道它之前是什么状态,就相当于在原始数据的基础上进行操作了!在这里我们假设:
-
用 0 和 1 还是表示原来是什么现在就是什么的情况,也就是对应上图中两种不变的情况
-
而用数字2表示 0 改变为 1
-
用数字3表示 1 改变为 0
作图表示如下:

对于这种原地算法,如果你需要用到之前的信息,但是可能之前的信息会被修改,就想办法把原始信息用一种方式存储起来。
因此我们在遍历面板的每一个元素时,我们就知道之前的位置原始数据是什么,这样就能正确计算结果,等到最后我们再根据每一种数字的情况将它归为正确的表示,比如最后我们处理完了所有数据,然后我们再遍历每个元素:
-
发现值为0或者1就不动
-
发现值为2就变为1
-
发现值为3就变为0
这样就可以得到最终结果!
代码思路:
-
遍历面板每一个元素,根据原始状态和需要改变为的值确定该位置的值
-
对于面板每一个元素,遇见周围八个位置中有1和3就把它当作1
-
对于面板每一个元素,遇见周围八个位置中有2和0就把它当作0
-
-
处理完每个元素后再次遍历整个面板,将1与3替换回正确的值
2.3 思路三——思路一的优化(位运算)
现在我们看看还有没有什么优化空间,有时间提示信息不是白给的哦:

题目提示我们board[i][j] 为 0 或 1,0和1,有没有想到什么?学计算机的0和1分别表示什么?在java中int是怎么存储的?
再看看面板的大小?1 <= m, n <= 25,在联想一下int的存储大小:
在不同编程语言中,int 类型的大小可以有所不同。以下是一些常见编程语言中 int 类型的大小:
C/C++:
根据编译器和操作系统的不同,
int类型通常为 4 字节,范围大约是 -2,147,483,648 到 2,147,483,647。Java:
Java 中的
int类型固定为 4 字节,范围是 -2,147,483,648 到 2,147,483,647。Python:
Python 中的
int类型大小是动态的,它可以根据需要自动调整。在 32 位系统上,通常为 4 字节,范围约为 -2,147,483,648 到 2,147,483,647;在 64 位系统上,它可以是 4 字节或 8 字节,取决于所使用的 Python 版本。JavaScript:
JavaScript 中的
int类型实际上是一个 64 位浮点数,范围大约是 -9,007,199,254,740,992 到 9,007,199,254,740,992。Swift:
Swift 中的
Int类型的大小取决于当前平台的位数。在 32 位平台上,Int是 32 位,范围大约是 -2,147,483,648 到 2,147,483,647;在 64 位平台上,Int是 64 位,范围大约是 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807。
可以看到在大多数情况下至少是按照4字节存储的,也就是32位,而一位可以表示0或者1两个数,联想到这里是不是又有了一种思路?我们是不是可以按照思路一的解决方案,虽然我们copy了一个原始面板,但是我们面板的每一个值都是一个int,如果我们把面板的一行设置位一个int来存储,通过位运算来求解,是不是能省好多空间?
所以代码思路还是思路一的代码思路,但是我们此时需要使用位运算来解决!

如上图,红色部分就相当于我们的面板。
代码思路:
-
设置一个和board数组一样行数的int数组命名位copy,每一个int值表示board的每一行
-
初始化,采用位运算初始化copy数组
-
遍历复制矩阵的每一个元素,查看其周围八个位置的状态,统计1的个数
-
根据题目提到的判定规则:少于 2 个或者大于 3 个 1 就判定当前位置为 0
-
等于 2 个 1 那么当前位置不需要更改
-
如果等于 3 个 1 那么当前位置如果为 0 需要改为 1
-
对于边角位置需要额外处理防止越界
-
2.4 思路四——压榨空间到极致
既然我们已经完成了思路三的代码,我想大家应该更清楚位运算的特点。这时我们再看看面板,面板中每一个位置是不是一个int值?那就是32位(假设java在通常情况下),而面板中的值0或者1肯定只用了最后一位,就像下面这样:

是不是这么多位置都空着想不想做点什么?空着的就是空间啊,由于1 <= m, n <= 25,那么是不是我们就可以用每一行的行首元素来当作我们思路三的copy数组,还是进行位运算操作,但是就不需要额外的空间了。
思路和思路三相似,但是唯一的改变就是我们将copy数组放在了board面板的每一行行行首位置而已。
比如对于题目中的示例:

将它放大看就是这样:

其中蓝色部分就是我们充当copy数组的位置。
比如对于题目中的

它转化后的结果位:

对应的二进制位为:
-
1073741824is represented as01000000000000000000000000000000 -
536870912is represented as00100000000000000000000000000000 -
-536870911is represented as11100000000000000000000000000001 -
0is represented as00000000000000000000000000000000
代码思路:
-
初始化,采用位运算初始化copy数组(实际上就是board的第一个元素的相应位)
-
遍历复制矩阵的每一个元素,查看其周围八个位置的状态,统计1的个数
-
根据题目提到的判定规则:少于 2 个或者大于 3 个 1 就判定当前位置为 0
-
等于 2 个 1 那么当前位置不需要更改
-
如果等于 3 个 1 那么当前位置如果为 0 需要改为 1
-
对于边角位置需要额外处理防止越界
-
-
最后需要更新第一列恢复为原来的值
2.5 思路五——压榨空间到极致2
改代码是看了自在飞花的解释学到的,确实很厉害,因为他写的c++版本,我在这解释一下核心思想,并写一个java版本。
这段代码的核心思路是两遍扫描棋盘:
-
第一遍扫描,计算每个细胞周围活细胞的数量,并用第二个比特位来存储细胞是否应该存活。由于细胞的状态是用0(死)和1(活)来表示的,所以作者通过按位与操作
&1来获取当前细胞的状态,也就是只取int的最后一位,也就是0或者1,仅累加最低位,来计算周围的存活细胞个数。 -
第二遍扫描,通过右移操作
>>= 1来更新细胞的状态。这是因为在第一遍扫描中,如果一个细胞在下一代应该是活的,那么它的第二个比特位将被设置为1。通过右移一位,我们可以用这个第二比特位来覆盖原来的状态,从而更新棋盘。

-
同时在代码中使用了两个数组dx和dy,他们用来表示周围的八个位置,减少了遍历周围八个位置的for循环造成变量
k或者l的重复开辟空间。
这个代码我就直接附在这里了:


其实效果和思路四差不多,但是思路值得借鉴。
补充
count的优化
如果还想继续优化还是有优化空间的,比如我们的count作为一个计数变量,是可以放在某一个board元素里面的,因为它的最大值不会超过8,因为周围最多也就八个元素。这样用一个3个bit就可以存储起来。
dx和dy优化
同时dx和dy也可以优化,因为dx和dy的范围就是在-1到1之间,因此可以用两个bit来存储一个值,dx和dy总共有8组,也就是16个元素,那么用32个bit就可以存储所有的dx和dy。
当然上面的优化有点太疯狂了,但是我们要举一反三想到这些思路。
3. 代码实现
3.1 思路一——暴力求解


3.2 思路二——原地算法


3.3 思路三——优化(位运算)

在Java中,表达式 (copy[k] & (1 << (31 - l))) 并不直接结果为0或1,而是执行了一个按位与(&)操作,这个操作的结果取决于copy[k]在指定位上的值。这里的操作细节如下:
1 << (31 - l):这部分是位移操作。它将数字1向左移动(31 - l)位。这意味着,如果l为0,那么1将被移动到最高位(假设是32位整数),如果l是其他值,1就会被移动到相应的位置上。这样做的目的是为了生成一个只在特定位置上有一个1的整数,其他位置都是0。
copy[k] & (1 << (31 - l)):这部分是按位与操作。它比较copy[k]和上面计算出的数值,在每个位上进行逻辑与操作。只有当copy[k]在相应的位上也是1时,这个操作的结果在那个位上才是1,否则结果为0。因此,这个表达式的结果是一个整数,它在大多数位上都是0,在特定的位上可能是0或者是2的某次幂(取决于l的值)。如果你想判断这个操作的结果是否为非零(即判断copy[k]在(31 - l)位上是否为1),你可以将整个表达式与0进行比较:
<span style="background-color:#f8f8f8"><span style="color:#008855">boolean</span> <span style="color:#000000">isBitSet</span> <span style="color:#981a1a">=</span><span style="color:#777777"> (</span><span style="color:#000000">copy</span><span style="color:#777777">[</span><span style="color:#000000">k</span><span style="color:#777777">] </span><span style="color:#981a1a">&</span> <span style="color:#777777">(</span><span style="color:#116644">1 << </span><span style="color:#777777">(</span><span style="color:#116644">31</span> <span style="color:#981a1a">-</span> <span style="color:#000000">l</span><span style="color:#777777">))) </span><span style="color:#981a1a">!=</span> <span style="color:#116644">0</span><span style="color:#777777">;</span></span>如果你的目的是确保结果严格为0或1,你需要进一步处理这个表达式,例如通过判断表达式是否非零来将结果转换为0或1:
<span style="color:#777777"><span style="background-color:#f8f8f8"><span style="color:#008855">int</span> <span style="color:#000000">bitValue</span> <span style="color:#981a1a">=</span> (<span style="color:#000000">copy</span>[<span style="color:#000000">k</span>] <span style="color:#981a1a">&</span> (<span style="color:#116644">1</span> << (<span style="color:#116644">31</span> <span style="color:#981a1a">-</span> <span style="color:#000000">l</span>))) <span style="color:#981a1a">!=</span> <span style="color:#116644">0</span> <span style="color:#981a1a">?</span> <span style="color:#116644">1</span> : <span style="color:#116644">0</span>;</span></span>这样,bitValue就会根据copy[k]在(31 - l)位上是否为1来分别存储1或0。

3.4 思路四——位运算,但是copy存储在board数组中


4. 相关复杂度分析
解法一:额外的复制矩阵
时间复杂度:O(MN),其中M是行数,N是列数。因为需要遍历整个矩阵两次,一次复制,一次计算。空间复杂度:O(MN),因为需要一个同样大小的矩阵来存储复制。
解法二:原地修改
时间复杂度:O(M*N),同样需要遍历整个矩阵来计算周围活细胞的数量。空间复杂度:O(1),除了原数组外,没有使用额外的空间,只是利用了额外的状态来标记中间状态。
解法三:位运算
时间复杂度:O(M*N),需要遍历整个矩阵来计算。空间复杂度:O(M),虽然没有使用额外的矩阵,但是使用了一个数组来存储行的状态。
解法四:位运算,但是copy存储在board数组中
时间复杂度:O(M*N),遍历整个矩阵。空间复杂度:O(1),所有操作都在原地完成,没有使用额外的存储空间。
解法五:位运算,将结果存储在每个元素的左边一位
时间复杂度:O(M*N),需要遍历整个矩阵来计算。空间复杂度:O(1),所有操作都在原地完成,没有使用额外的存储空间。
在上述解法中,除了第一种解法需要和原矩阵一样的额外空间,第三种解法使用了一个数组来存储行的状态,其他方法都采取了原地算法,即在原数组上直接修改,大大节约了空间。
相关文章:
面试经典150题——生命游戏
"Push yourself, because no one else is going to do it for you." - Unknown 1. 题目描述 2. 题目分析与解析 2.1 思路一——暴力求解 之所以先暴力求解,是因为我开始也没什么更好的思路,所以就先写一种解决方案,没准写着写…...
【C++】C++11下线程库
C11下线程库 1. thread类的简单介绍2.线程函数参数3.原子性操作库(atomic)4.mutex的种类5. RAII风格加锁解锁5.1Lock_guard5.2unique_lock 6.condition_variable 1. thread类的简单介绍 在C11之前,涉及到多线程问题,都是和平台相关的,比如wi…...
面试经典150题——矩阵置零
"Dream it. Wish it. Do it." - Unknown 1. 题目描述 2. 题目分析与解析 2.1 思路一——暴力求解 思路一很简单,就是尝试遍历矩阵的所有元素,如果发现值等于0,就把当前行与当前列的值分别置为0。同时我们需要注意,…...
多端开发围炉夜话
文章目录 一、多端开发 一、多端开发 uni-app 官网 UNI-APP中的UI框架:介绍常用的UI框架及其特点 uView UIVant WeappColor UIMint UI uniapp嵌入android原生开发的功能 uniapp使用安卓原生sdk uni-app中的uni.requireNativePlugin...
分治算法总结(Java)
目录 分治算法概述 快速排序 练习1:排序数组 练习2:数组中的第K个最大元素 练习3:最小k个数 归并排序 练习4:排序数组 练习5:交易逆序对的总数 练习6:计算右侧小于当前元素的个数 练习7࿱…...
【云原生系列之kubernetes】--Ingress使用
service的缺点: 不支持基于URL等机制对HTTP/HTTPS协议进行高级路由、超时、重试、基于流量的灰度等高级流量治理机制难以将多个service流量统一管理 1.1ingress的概念 ingress是k8s中的一个对象,作用是如何将请求转发到service的规则ingress controlle…...
练习:鼠标类设计之2_类和接口
前言 续鼠标类设计之1,前面解决了鼠标信号问题,这里解决显示问题 引入 鼠标伴随操作系统而生,考虑在屏幕上怎样显示 思路 1>鼠标显示是一个动态效果,所以需要一个“动态效果类”对象,添加进鼠标类的属性里。 在面…...
【程序员英语】【美语从头学】初级篇(入门)(笔记)Lesson 15 At the Department Store 在百货商店
《美语从头学初级入门篇》 注意:被 删除线 划掉的不一定不正确,只是不是标准答案。 文章目录 Lesson 15 At the Department Store 在百货商店会话A会话B笔记 Lesson 15 At the Department Store 在百货商店 会话A A: Can you help me, please? B: Sur…...
linux 安装、删除 JTAG驱动
安装 安装驱动需要sudo访问权限,所以得手动安装。 在petalinux安装目录下: 文件的路径。 cd tools/xsct/data/xicom/cable_drivers/lin64/install_script/install_drivers 然后执行文件 install_drivers。 sudo ./install_drivers安装成功。 删除 …...
CSS的伪类选择器:nth-child()
CSS的伪类选择器:nth-child() CSS的伪类选择器 :nth-child() 是一个非常强大的工具,它允许你根据元素在其父元素中的位置(序数)来选择特定的子元素。这个选择器可以应用于任何元素,并且可以与类型选择器、类选择器或ID选择器结合…...
python celery使用队列
在celery的配置方法中有个参数叫task_routes,是用来设置不同的任务 消费不同的队列(也就是路由)。 格式如下: { ‘task name’: { ‘queue’: ‘queue name’ }}直接上代码,简单明了,目录格式如下&#x…...
四非保研之旅
大家好,我是工藤学编程,虽有万分感概,但是话不多说,先直接进入正题,抒情环节最后再说,哈哈哈 写在开头 我的分享是来给大家涨信心的,网上的大佬们都太强了,大家拿我涨涨信心&#…...
基于Java+SpringBoot的旅游路线规划系统(源码+论文)
文章目录 目录 文章目录 前言 一、功能设计 二、功能实现 1.1 前端首页模块的实现 1.2 景点新闻 1.3 景点在线预订 1.4 酒店在线预订 1.5 管理员景点管理 1.6 管理员旅游线路管理 1.7 酒店信息管理 三、库表设计 前言 随着我国的经济的不断发展,现在的一些热门的景…...
AI与测试自动化:未来已来
AI与测试自动化注定融合。软件开发的速度和准确性要求已经远远超出了预期。测试自动化通过重复、详细和数据密集型测试来解决这个问题,确保敏捷和持续交付环境中的软件质量。AI的学习、适应和预测能力以完美的效率和准确性增强了测试自动化。复杂的算法现在充当质量…...
深度学习基础之《TensorFlow框架(6)—张量》
一、张量 1、什么是张量 张量Tensor和ndarray是有联系的,当我们print()打印值的时候,它返回的就是ndarray对象 TensorFlow的张量就是一个n维数组,类型为tf.Tensor。Tensor具有以下两个重要的属性: (1)typ…...
第三百六十六回
文章目录 1. 概念介绍2. 使用方法2.1 List2.2 Map2.3 Set 3. 示例代码4. 内容总结 我们在上一章回中介绍了"convert包"相关的内容,本章回中将介绍collection.闲话休提,让我们一起Talk Flutter吧。 1. 概念介绍 我们在本章回中介绍的内容是col…...
Fiddler工具 — 18.Fiddler抓包HTTPS请求(一)
1、Fiddler抓取HTTPS过程 第一步:Fiddler截获客户端发送给服务器的HTTPS请求,Fiddler伪装成客户端向服务器发送请求进行握手 。 第二步:服务器发回相应,Fiddler获取到服务器的CA证书, 用根证书(这里的根证…...
多租户数据库的缓冲区共享和预分配方案设计
多租户数据库的缓冲区共享和预分配方案设计 文章目录 多租户数据库的缓冲区共享和预分配方案设计简介初始化输入交互输出输入部分的输出交互部分的输出 评分注意点语言要求需要使用的模块系统框架图方案设计初始化阶段交互阶段 修改进度规划最终代码 简介 云计算技术使企业能够…...
C++:C++入门基础
创作不易,感谢三连 !! 一、什么是C C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机ÿ…...
利用System.Web.HttpRuntime.Cache制作缓存工具类
用到的依赖介绍 当谈到 ASP.NET 中的缓存管理时,常涉及到以下三个类:CacheDependency、HttpRuntime.Cache 和 System.Web.Caching。 CacheDependency(缓存依赖项): CacheDependency 类用于指定一个或多个文件或目录作…...
Python爬虫实战:研究MechanicalSoup库相关技术
一、MechanicalSoup 库概述 1.1 库简介 MechanicalSoup 是一个 Python 库,专为自动化交互网站而设计。它结合了 requests 的 HTTP 请求能力和 BeautifulSoup 的 HTML 解析能力,提供了直观的 API,让我们可以像人类用户一样浏览网页、填写表单和提交请求。 1.2 主要功能特点…...
8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂
蛋白质结合剂(如抗体、抑制肽)在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上,高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术,但这类方法普遍面临资源消耗巨大、研发周期冗长…...
质量体系的重要
质量体系是为确保产品、服务或过程质量满足规定要求,由相互关联的要素构成的有机整体。其核心内容可归纳为以下五个方面: 🏛️ 一、组织架构与职责 质量体系明确组织内各部门、岗位的职责与权限,形成层级清晰的管理网络…...
AspectJ 在 Android 中的完整使用指南
一、环境配置(Gradle 7.0 适配) 1. 项目级 build.gradle // 注意:沪江插件已停更,推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...
技术栈RabbitMq的介绍和使用
目录 1. 什么是消息队列?2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…...
iOS性能调优实战:借助克魔(KeyMob)与常用工具深度洞察App瓶颈
在日常iOS开发过程中,性能问题往往是最令人头疼的一类Bug。尤其是在App上线前的压测阶段或是处理用户反馈的高发期,开发者往往需要面对卡顿、崩溃、能耗异常、日志混乱等一系列问题。这些问题表面上看似偶发,但背后往往隐藏着系统资源调度不当…...
深入浅出深度学习基础:从感知机到全连接神经网络的核心原理与应用
文章目录 前言一、感知机 (Perceptron)1.1 基础介绍1.1.1 感知机是什么?1.1.2 感知机的工作原理 1.2 感知机的简单应用:基本逻辑门1.2.1 逻辑与 (Logic AND)1.2.2 逻辑或 (Logic OR)1.2.3 逻辑与非 (Logic NAND) 1.3 感知机的实现1.3.1 简单实现 (基于阈…...
mac 安装homebrew (nvm 及git)
mac 安装nvm 及git 万恶之源 mac 安装这些东西离不开Xcode。及homebrew 一、先说安装git步骤 通用: 方法一:使用 Homebrew 安装 Git(推荐) 步骤如下:打开终端(Terminal.app) 1.安装 Homebrew…...
Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)
引言 在人工智能飞速发展的今天,大语言模型(Large Language Models, LLMs)已成为技术领域的焦点。从智能写作到代码生成,LLM 的应用场景不断扩展,深刻改变了我们的工作和生活方式。然而,理解这些模型的内部…...
【从零开始学习JVM | 第四篇】类加载器和双亲委派机制(高频面试题)
前言: 双亲委派机制对于面试这块来说非常重要,在实际开发中也是经常遇见需要打破双亲委派的需求,今天我们一起来探索一下什么是双亲委派机制,在此之前我们先介绍一下类的加载器。 目录 编辑 前言: 类加载器 1. …...
