【LeetCode刷题笔记(6-2)】【Python】【三数之和】【双指针】【中等】
文章目录
- 引言
- 三数之和
- 题目描述
- 示例
- 示例1
- 示例2
- 示例3
- 提示
- 解决方案3:【双指针】
- 结束语
三数之和
引言
编写通过所有测试案例的代码并不简单,通常需要深思熟虑和理性分析。虽然这些代码能够通过所有的测试案例,但如果不了解代码背后的思考过程,那么这些代码可能并不容易被理解和接受。我编写刷题笔记的初衷,是希望能够与读者们分享一个完整的代码是如何在逐步的理性思考下形成的。我非常欢迎读者的批评和指正,因为我知道我的观点可能并不完全正确,您的反馈将帮助我不断进步。如果我的笔记能给您带来哪怕是一点点的启示,我也会感到非常荣幸。同时,我也希望我的分享能够激发您的灵感和思考,让我们一起在编程的道路上不断前行~
三数之和
题目描述
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。
请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例
示例1
- 输入:nums = [-1,0,1,2,-1,-4]
- 输出:[[-1,-1,2],[-1,0,1]]
示例2
- 输入:nums = [0,1,1]
- 输出:[]
- 解释:唯一可能的三元组和不为 0 。
示例3
- 输入:nums = [0,0,0]
- 输出:[[0,0,0]]
- 解释:唯一可能的三元组和为 0 。
提示
- 3 <= nums.length <= 3000
- -105 <= nums[i] <= 105
解决方案3:【双指针】
在博客【LeetCode刷题笔记(6-1)】【Python】【三数之和】【哈希表】【中等】中,我们详尽地探讨了如何利用【哈希表】精妙设计算法,从而顺利通关【三数之和】这一挑战。
在此过程中,我们深度挖掘了问题细节,确保算法能够通过所有的测试案例(基于哈希表设计的算法在【三数之和】上很容易超时) —> 这不仅是一次技术上的磨练,更是对细心与耐心的极致考验。
问题1:为什么【三数之和】问题可以用【双指针】来解决?
在博客【LeetCode刷题笔记(6-1)】【Python】【三数之和】【哈希表】【中等】中,我们从两数之和的解决方案中汲取灵感,通过举一反三,巧妙地运用哈希表来解决【三数之和】问题。而对于【双指针】的使用,背后又隐藏着怎样的深刻逻辑呢?—> 先回到三数之和的本质:查找目标值。
在【哈希表】解决方案中,我们通过两层遍历的方式,首先固定两个值,然后利用哈希表快速查找剩下的一个值。那么,除了哈希表外,是否还有其他算法可以达到类似的效果呢?答案是肯定的,那就是双指针方法。
通过固定一个值,然后使用双指针在数组中查找剩余的两个值,同样可以达到解决问题的目的。这种方法的优势在于,它避免了哈希表的开销,同时保持了查找的效率。
我们可以把双指针分为左指针left和右指针right。在循环遍历数组nums的每个元素nums[i]时,设计算法找到正确的nums[left]和nums[right], 共同组成满足题意的三元组[nums[i], nums[left], nums[right]]。
代码的初始版本如下:
class Solution: def threeSum(self, nums: List[int]) -> List[List[int]]: # 如果输入的数组为空或长度小于3,直接返回空列表 if not nums or len(nums) < 3: return [] # 对输入的数组进行排序nums.sort() # 获取列表的长度 n = len(nums) # 初始化结果列表 result_list = [] # 遍历数组中的每个元素 --> 固定元素nums[i]for i in range(n): # 初始化左指针和右指针,分别指向剩余两个元素的位置 left = 0 right = n - 1 # 当左指针小于右指针时,进行循环 while left < right: # 如果三个元素的和等于0,则将这三个元素添加到结果列表中,并同时移动左指针和右指针if nums[i] + nums[left] + nums[right] == 0: result_list.append([nums[i], nums[left], nums[right]]) left += 1 # 左指针右移right -= 1 # 右指针左移 # 如果三个元素的和大于0,则右指针向左移动,以减少和的值 elif nums[i] + nums[left] + nums[right] > 0: right -= 1 else: # 如果三个元素的和小于0,则左指针向右移动,以增加和的值 left += 1 return result_list # 返回结果列表
初步运行结果

可以看到,虽然结果列表中,包含了正确的三元组,但更多的是重复的三元组(如上图的蓝色框部分),甚至还出现重复利用数组元素的三元组(上图红色框,重复利用元素2)。
尽管初始的代码版本问题很大,但还是有一些小细节值得拎出来说明一下:
细节1:为什么要对数组nums进行排序?
如果您已看完这篇博客,您会知道其中一个原因是为了方便去重。但目前的【初始代码】压根与去重无关。其实,现阶段对数组nums进行排序的原因是为了方便确定左指针left和右指针right的移动方向。
- 如果数组
nums是乱序的,如果nums[i] + nums[left] + nums[right] > 0,下一步是移动左指针还是右指针?显然是不确定的。 - 如果数组
nums是有序的,比如说从小到大排列。那么left右移 ==> 三元组之和将增加;同理,right左移 ==> 三元组之和将减少;那么nums[i] + nums[left] + nums[right] > 0,下一步自然是right左移,使三元组之和进一步接近0。
从细节1的分析中我们可以看出,对数组nums进行排序对于应用双指针算法解决三数之和问题至关重要。甚至可以说,方便去重只是这个过程中的一个额外的好处,就像“买一送一”一样。
细节2:while left < right: 这个语句能否取等,即while left <= right: ?
不能。我们可以从左右指针的定义中找到答案 ==> 分别指向剩余两个元素的位置。如果left == right成立 ⇒ 三元组会包含同一元素!
细节3:当三元组元素满足nums[i] + nums[left] + nums[right] == 0时,为什么下一步要同时进行left右移和right左移?
- 如果我们只进行
left右移,相当于nums[i]和nums[left]是固定的。想要nums[i] + nums[left] + nums[right] == 0继续成立,nums[right]即使元素位置变了,但元素值不可能变 ==> 只移动一个指针没有意义,都不移动更没有意义 —> 同时进行left右移和right左移。
想清楚三个细节后,紧接着就要思考为什么这个代码会生成这么多重复的三元组,甚至还出现重复利用数组元素的三元组?
问题1:左指针left不应该初始化为0
如果我们把左指针left初始化为0 ⇒ 在【双指针】查找剩余两个元素时,左指针left的右移和右指针right的左移都可能碰上事先固定的元素nums[i] ⇒ 出现重复利用数组元素的三元组。既然0不行,那么初始化为[0, i]的任何数都会出现这样的问题。
那将左指针left初始化为i+1?
是的,这样既可以保证左指针left的右移和右指针right的左移不可能会碰到事先固定的元素nums[i],也不会出现遗漏某个三元组的情况。
代码如下(见修改1):
class Solution: def threeSum(self, nums: List[int]) -> List[List[int]]: # 如果输入的数组为空或长度小于3,直接返回空列表 if not nums or len(nums) < 3: return [] # 对输入的数组进行排序nums.sort() # 获取列表的长度 n = len(nums) # 初始化结果列表 result_list = [] # 遍历数组中的每个元素 --> 固定元素nums[i]for i in range(n): # 初始化左指针和右指针, 分别指向剩余两个元素的位置 left = i + 1 # 修改1:初始化为i+1right = n - 1 # 当左指针小于右指针时,进行循环 while left < right: # 如果三个元素的和等于0,则将这三个元素添加到结果列表中,并同时移动左指针和右指针if nums[i] + nums[left] + nums[right] == 0: result_list.append([nums[i], nums[left], nums[right]]) left += 1 # 左指针右移right -= 1 # 右指针左移 # 如果三个元素的和大于0,则右指针向左移动,以减少和的值 elif nums[i] + nums[left] + nums[right] > 0: right -= 1 else: # 如果三个元素的和小于0,则左指针向右移动,以增加和的值 left += 1 return result_list # 返回结果列表
运行结果

可以看到,相比于上一版代码,这一版结果虽然仍然存在重复的三元组,但已经没有出现重复利用数组元素的三元组!<— 这是由于这版代码中, i < left < right的关系是恒成立的,保证了不可能重复利用数组元素。
问题2:为什么仍然存在重复的三元组[-1, 0, 1]呢?
数组[-1,0,1,2,-1,-4]排序后的结果为[-4, -1, -1, 0, 1, 2] ⇒ 当遍历数组元素,依次固定元素值时,元素-1会连续两次作为固定的值 ⇒ 结果列表中多了一个三元组!!!
由于数组是有序的,我们可以很简单的对这种情况进行去重操作,代码如下(见修改2):
class Solution: def threeSum(self, nums: List[int]) -> List[List[int]]: # 如果输入的数组为空或长度小于3,直接返回空列表 if not nums or len(nums) < 3: return [] # 对输入的数组进行排序nums.sort() print(nums)# 获取列表的长度 n = len(nums) # 初始化结果列表 result_list = [] # 遍历数组中的每个元素 --> 固定元素nums[i]for i in range(n):# 修改2:去除连续相同的元素值if i > 0 and nums[i] == nums[i-1]:continue# 初始化左指针和右指针, 分别指向剩余两个元素的位置 left = i + 1 # 修改1:初始化为i+1right = n - 1 # 当左指针小于右指针时,进行循环 while left < right: # 如果三个元素的和等于0,则将这三个元素添加到结果列表中,并同时移动左指针和右指针if nums[i] + nums[left] + nums[right] == 0: result_list.append([nums[i], nums[left], nums[right]]) left += 1 # 左指针右移right -= 1 # 右指针左移 # 如果三个元素的和大于0,则右指针向左移动,以减少和的值 elif nums[i] + nums[left] + nums[right] > 0: right -= 1 else: # 如果三个元素的和小于0,则左指针向右移动,以增加和的值 left += 1 return result_list # 返回结果列表
运行结果

可以看到,目前代码已经通过132个测试用例,但仍然会出现重复的三元组。
问题3:为什么仍然存在重复的三元组[-2, 0, 2]呢?,我们可以从排序后的数组[-2, 0, 0, 2, 2]看出,0和2分别重复了两次 ⇒ 当我们固定元素-2时,并快速找到[-2, 0, 2]这个正确的三元组,此时左指针left右移,右指针right左移。巧的是,左指针left依然指向元素0,右指针依然指向元素2 ⇒ 出现重复的三元组[-2, 0, 2]。
因此我们在找到正确三元组的同时,应该把与左指针left指向的元素相同的过滤掉,也把与右指针right指向的元素相同的过滤掉。代码如下(见修改3):
class Solution: def threeSum(self, nums: List[int]) -> List[List[int]]: # 如果输入的数组为空或长度小于3,直接返回空列表 if not nums or len(nums) < 3: return [] # 对输入的数组进行排序nums.sort() print(nums)# 获取列表的长度 n = len(nums) # 初始化结果列表 result_list = [] # 遍历数组中的每个元素 --> 固定元素nums[i]for i in range(n):# 修改2:去除连续相同的元素值if i > 0 and nums[i] == nums[i-1]:continue# 初始化左指针和右指针, 分别指向剩余两个元素的位置 left = i + 1 # 修改1:初始化为i+1right = n - 1 # 当左指针小于右指针时,进行循环 while left < right: # 如果三个元素的和等于0,则将这三个元素添加到结果列表中,并同时移动左指针和右指针if nums[i] + nums[left] + nums[right] == 0: result_list.append([nums[i], nums[left], nums[right]]) # 修改3while left < right and nums[left] == nums[left+1]: left += 1 # 如果左边的元素相等,则移动左指针 while left < right and nums[right] == nums[right-1]: right -= 1 # 如果右边的元素相等,则移动右指针 left += 1 # 左指针右移right -= 1 # 右指针左移 # 如果三个元素的和大于0,则右指针向左移动,以减少和的值 elif nums[i] + nums[left] + nums[right] > 0: right -= 1 else: # 如果三个元素的和小于0,则左指针向右移动,以增加和的值 left += 1 return result_list # 返回结果列表
运行结果

可以看到,目前代码已经能够通过所有测试案例,但代码运行时间显然还是比较长的。
问题4:还有没有进一步优化的可能性?
有的。
首先,数组nums是排好序的,而我们的目标是找三个元素,让它们的和为0 ⇒ 如果当前固定的值为nums[i] > 0 ⇒ 根据i < left < right, 即nums[i] < nums[i] < nums[i]恒成立,不可能找到和为0的三元组,此时应该直接返回结果。
问题5:nums[i] = 0 的情况是否要保留,即返回条件是否可改为nums[i] >= 0 ?
不可以,因为可能存在[0, 0, 0]这个正确的三元组!代码如下(见修改4):
完整代码:
class Solution: def threeSum(self, nums: List[int]) -> List[List[int]]: # 如果输入的数组为空或长度小于3,直接返回空列表 if not nums or len(nums) < 3: return [] # 对输入的数组进行排序nums.sort() # print(nums)# 获取列表的长度 n = len(nums) # 初始化结果列表 result_list = [] # 遍历数组中的每个元素 --> 固定元素nums[i]for i in range(n):# 修改2:去除连续相同的元素值if i > 0 and nums[i] == nums[i-1]:continue# 修改4 if nums[i] > 0:return result_list# 初始化左指针和右指针, 分别指向剩余两个元素的位置 left = i + 1 # 修改1:初始化为i+1right = n - 1 # 当左指针小于右指针时,进行循环 while left < right: # 如果三个元素的和等于0,则将这三个元素添加到结果列表中,并同时移动左指针和右指针if nums[i] + nums[left] + nums[right] == 0: result_list.append([nums[i], nums[left], nums[right]]) # 修改3while left < right and nums[left] == nums[left+1]: left += 1 # 如果左边的元素相等,则移动左指针 while left < right and nums[right] == nums[right-1]: right -= 1 # 如果右边的元素相等,则移动右指针 left += 1 # 左指针右移right -= 1 # 右指针左移 # 如果三个元素的和大于0,则右指针向左移动,以减少和的值 elif nums[i] + nums[left] + nums[right] > 0: right -= 1 else: # 如果三个元素的和小于0,则左指针向右移动,以增加和的值 left += 1 return result_list # 返回结果列表
运行结果:

复杂度分析
- 时间复杂度:O(N2),其中 N 是数组
nums元素的数量。 - 空间复杂度:O(N)
- 存放数组排序后的新数组 ===> O(N)
结束语
- 亲爱的读者,感谢您花时间阅读我们的博客。我们非常重视您的反馈和意见,因此在这里鼓励您对我们的博客进行评论。
- 您的建议和看法对我们来说非常重要,这有助于我们更好地了解您的需求,并提供更高质量的内容和服务。
- 无论您是喜欢我们的博客还是对其有任何疑问或建议,我们都非常期待您的留言。让我们一起互动,共同进步!谢谢您的支持和参与!
- 我会坚持不懈地创作,并持续优化博文质量,为您提供更好的阅读体验。
- 谢谢您的阅读!
相关文章:
【LeetCode刷题笔记(6-2)】【Python】【三数之和】【双指针】【中等】
文章目录 引言三数之和题目描述示例示例1示例2示例3 提示 解决方案3:【双指针】结束语 三数之和 引言 编写通过所有测试案例的代码并不简单,通常需要深思熟虑和理性分析。虽然这些代码能够通过所有的测试案例,但如果不了解代码背后的思考过程…...
02_Web开发基础之JavaScript
Web开发基础之JavaScript 学习目标和内容 1、能够描述Javascript的作用 2、能够使用分支结构if语句逻辑判断 3、能够使用其中一种循环语句 4、能够定义javaScript中的函数 5、能够定义javaScript中的对象 6、能够描述DOM的作用 7、能够通过DOM操作HTML标签元素及其属性 8、能够…...
如何控制Elasticsearch搜索的相关性?
控制相关性 纯粹处理结构化数据(例如日期、数字和 字符串枚举)很简单:他们只需要检查一个文档(或 行,在关系数据库中)与查询匹配。 虽然布尔值是/否匹配是全文搜索的重要组成部分,但它们 光靠自己是不够的。相反,我们还需要知道每个的相关性 document 是查询。全文搜索…...
基于urllib库的网页数据爬取
实验名称: 基于urllib库的网页数据爬取 实验目的及要求: 【实验目的】 通过本实验了解和掌握urllib库。 【实验要求】 1. 使用urllib库爬取百度搜索页面。 2. 使用urllib库获取百度搜索的关键字搜索结果(关键字任选)。 实验原理及…...
Python如何匹配库的版本
目录 1. 匹配库的版本 2. Python中pip,库,编译环境的问题回答总结 2.1 虚拟环境 2.2 pip,安装库,版本 1. 匹配库的版本 (别的库的版本冲突同理) 在搭建pyansys环境的时候,安装grpcio-tools…...
日志审计在网络安全中的重要性
日志审计是一种通过分析、识别和验证各种日志信息,以帮助企业了解其网络和系统的安全状态和活动的过程。这些日志信息可能来自各种来源,包括服务器、网络设备、应用程序、操作系统等。 日志审计的主要功能包括: 1.识别潜在的安全威胁&#…...
浅谈基于不信任的防御性编程
背景 在实际开发过程中,我们经常遇到这样的场景: 后端报错了,手忙脚乱一顿排查,发现是前端传的参数为空,或者格式不对;后端又报错了,传参没问题,根据日志流发现,是某“给…...
线性代数(一)
1.标量:标量由只有⼀个元素的张量表⽰。 x np.array(3.0) y np.array(2.0) x y, x * y, x / y, x ** y (array(5.), array(6.), array(1.5), array(9.))2.向量:向量可以被视为标量值组成的列表,列向量是向量的默认⽅向。 x np.arange(4…...
k8s-learning-why we need pod
应用场景 应用从虚拟机迁移到容器中 为什么虚拟机中的应用不能无缝迁移到容器中 虚拟机中应用:一组进程,被管理在systemd或者supervisord中 容器的本质:一个容器一个进程 所以将运行在虚拟机中的应用无缝迁移到容器中,与容器…...
【CASS精品教程】cass11提示“请不要在虚拟机中运行此程序”的解决办法
文章目录 一、问题提示二、解决办法一、问题提示 按照正常安装教程安装好南方测绘cass 11之后,打开的时候可能会有以下提示:请不要在虚拟机中运行此程序,如下图所示: 遇到问题,咱们就想办法解决问题,下面将自己尝试的方法及最终解决情况跟大家说一下,供参考。 二、解决…...
【算法Hot100系列】正则表达式匹配
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...
html 基础学习笔记
Date:20231212 html标签 基础学习笔记 一、web和internet 1.1、Internet简介 Internet 是一个全球性的计算机互联网络,中文名称有"因特网"、“国际互联网”、“网际网”、"交互网络"等Internet提供的主要服务 Telnet、Email、www、BBS、FTP等…...
7-4 天梯赛的善良
天梯赛是个善良的比赛。善良的命题组希望将题目难度控制在一个范围内,使得每个参赛的学生都有能做出来的题目,并且最厉害的学生也要非常努力才有可能得到高分。 于是命题组首先将编程能力划分成了 106 个等级(太疯狂了,这是假的&…...
案例精选|聚铭综合日志分析系统助力长房集团“智慧房产”信息化建设
长沙房产(集团)有限公司(简称“长房集团”)始创于2004年3月,是一家由长沙市人民政府授权组建的国有独资企业。截至2021年底,企业总资产逾452亿元,总开发面积1300多万平方米,已开发项…...
HarmonyOS给应用添加消息通知
给您的应用添加通知 通知介绍 通知旨在让用户以合适的方式及时获得有用的新消息,帮助用户高效地处理任务。应用可以通过通知接口发送通知消息,用户可以通过通知栏查看通知内容,也可以点击通知来打开应用,通知主要有以下使用场景…...
【C语言】cache和程序访问的局部性对程序性能的影响
文章目录 1.源程序比较其性能影响2.内存分配(1)静态存储区(static):(2)栈区(stack):(3)堆区(heap&…...
数字棱形(课程F)
输入1个整数N,输出N行的如下形状的数字棱形。 例如:N4时: ___1 __222 _33333 4444444 _33333 __222 ___1 (注:上面使用下划线’_’表示空格,以避免看不清造成误解) 输入格式 第一行1个正整数:N࿰…...
如何查看PHP信息
创建一个 PHP 文件,比如 info.php,在其中添加以下代码: <?php phpinfo(); ?>访问这个文件(例如,在浏览器中输入 http://localhost/info.php),它会显示 PHP 的所有配置信息。在这个页面…...
Vue3+ts实现页面跳转及参数传递
## 列表页 <script lang"ts" setup> import { reactive, toRefs } from vue // 1 引入useRouter路由信息方法 import { useRouter } from vue-router // 2 获取实例 const router useRouter()const gotoDetail (index: string) > {router.push({path: …...
日志框架Log4j、JUL、JCL、Slf4j、Logback、Log4j2
1. JAVA日志框架 1.1 为什么程序需要记录日志 我们不可能实时的24小时对系统进行人工监控,那么如果程序出现异常错误时要如何排查呢?并且系统在运行时做了哪些事情我们又从何得知呢?这个时候日志这个概念就出现了,日志的出现对系…...
Java 语言特性(面试系列1)
一、面向对象编程 1. 封装(Encapsulation) 定义:将数据(属性)和操作数据的方法绑定在一起,通过访问控制符(private、protected、public)隐藏内部实现细节。示例: public …...
Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动
一、前言说明 在2011版本的gb28181协议中,拉取视频流只要求udp方式,从2016开始要求新增支持tcp被动和tcp主动两种方式,udp理论上会丢包的,所以实际使用过程可能会出现画面花屏的情况,而tcp肯定不丢包,起码…...
智能在线客服平台:数字化时代企业连接用户的 AI 中枢
随着互联网技术的飞速发展,消费者期望能够随时随地与企业进行交流。在线客服平台作为连接企业与客户的重要桥梁,不仅优化了客户体验,还提升了企业的服务效率和市场竞争力。本文将探讨在线客服平台的重要性、技术进展、实际应用,并…...
【Web 进阶篇】优雅的接口设计:统一响应、全局异常处理与参数校验
系列回顾: 在上一篇中,我们成功地为应用集成了数据库,并使用 Spring Data JPA 实现了基本的 CRUD API。我们的应用现在能“记忆”数据了!但是,如果你仔细审视那些 API,会发现它们还很“粗糙”:有…...
在WSL2的Ubuntu镜像中安装Docker
Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
中医有效性探讨
文章目录 西医是如何发展到以生物化学为药理基础的现代医学?传统医学奠基期(远古 - 17 世纪)近代医学转型期(17 世纪 - 19 世纪末)现代医学成熟期(20世纪至今) 中医的源远流长和一脉相承远古至…...
Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)
引言 在人工智能飞速发展的今天,大语言模型(Large Language Models, LLMs)已成为技术领域的焦点。从智能写作到代码生成,LLM 的应用场景不断扩展,深刻改变了我们的工作和生活方式。然而,理解这些模型的内部…...
MinIO Docker 部署:仅开放一个端口
MinIO Docker 部署:仅开放一个端口 在实际的服务器部署中,出于安全和管理的考虑,我们可能只能开放一个端口。MinIO 是一个高性能的对象存储服务,支持 Docker 部署,但默认情况下它需要两个端口:一个是 API 端口(用于存储和访问数据),另一个是控制台端口(用于管理界面…...
Chromium 136 编译指南 Windows篇:depot_tools 配置与源码获取(二)
引言 工欲善其事,必先利其器。在完成了 Visual Studio 2022 和 Windows SDK 的安装后,我们即将接触到 Chromium 开发生态中最核心的工具——depot_tools。这个由 Google 精心打造的工具集,就像是连接开发者与 Chromium 庞大代码库的智能桥梁…...
