“ARTS” 是一个学习打卡活动,发起人是骨灰级程序员陈皓,网名左耳朵耗子。任务是做一道 LeetCode 算法题(Algorithm)、阅读并点评一篇英文技术文章(Review)、学习一个技术技巧(Tip),或分享一篇有观点和思考的技术文章(Share),都是基于学习和思考的刻意练习。
共勉
一苦一乐相磨炼,炼极而成福者,其福始久;一疑一信相参勘,勘极而成知者,其知始真。——《菜根谭》
Algorithm
在讲解算法题之前,先补充一个知识点:算法的时间与空间复杂度
我们评价一个算法的优劣主要从两个维度来考量:
-
时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。
-
空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。
1.时间复杂度
时间复杂度的表示方法:大 O 符号表示法,即 T(n)=O(f(n))
这个公式被称为:算法的渐进时间复杂度
f(n)
表示每行代码的执行次数之和,而 O 表示正比例关系。来看一个具体的例子:
for (int i = 0; i < n; i++) {
j = i;
j++;
}
上面一共 4 行代码,我们假设每一行代码的执行时间是一样的,我们用 一颗粒时间 来表示。
第一行代码的执行耗时:n 个颗粒时间 第二行代码的执行耗时:n 个颗粒时间 第三行代码的执行耗时:n 个颗粒时间 第四行代码的执行耗时:是符号,暂时忽略
总的时间:T(n) = 3n * 颗粒时间。从这个耗时可以看出,总的执行时间是随着 n 的变化而变化,因此,我们可以简化一下这个算法的时间复杂度表示:T(n) = O(n)。
之所以可以简化,是因为大 O 符号表示法并不是用来表示算法真实的执行时间,它是用来表示代码执行时间的增长变化趋势的。
常见的时间复杂度量级有:
- 常数阶O(1)
- 对数阶O(logN)
- 线性阶O(n)
- 线性对数阶O(nlogN)
- 平方阶O()
- 立方阶O()
- K次方阶O()
- 指数阶()
上面从上至下依次的时间复杂度越来越大,执行的效率越来越低。
2.空间复杂度
明白了时间复杂度,那么空间复杂度也是类似的。空间复杂度也不是用来计算程序实际占用的空间的。
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势。
空间复杂度比较常用的有:O(1)、O(n)、O(n²)
来看两个例子:
int i = 1;
int j = 2;
int sum = i + j;
代码里的 i 和 j 所分配的空间不会随着处理数据量而变化,因此它的空间复杂度 S(n) = O(1)。
第二个例子:
int[] m = new int[n];
for (int i = 0; i < n; i++) {
j = i;
j++;
}
在第一行代码中,通过 new 关键字创建了一个数组出来,数组 m 占用的大小为 n ,第 2 到第 5 行代码虽然是一个循环,但是没有再分配新的空间,因此,上述代码的空间复杂度是:S(n) = O(n)。
本期算法
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 的那 两个 整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。 你可以按任意顺序返回答案。
示例1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例3:
输入:nums = [3,3], target = 6
输出:[0,1]
解题思路
最直接的方法是采用 暴力枚举法
,用两个 for 循环来遍历两个数相加,等于 target 就返回两个数的下标,代码如下:
class Solution {
public int[] twoSum(int[] nums, int target) {
int n = nums.length;
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (nums[i] + nums[j] == target) {
return new int[]{i, j};
}
}
}
return new int[0];
}
}
复杂度分析
-
时间复杂度:O()
-
空间复杂度:O(1)
方法二:哈希表
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
if (map.containsKey(target - nums[i])) {
return new int[]{map.get(target - nums[i]), i};
}
map.put(nums[i], i);
}
return new int[0];
}
}
首先创建一个 HashMap,用来存放遍历过的元素。然后使用一个 for 循环来遍历数组,内部判断 HashMap 中是否包含了 target - x
,如果包含直接返回下标,如果不包含,则将该元素存入 HashMap,用元素的值做 key,下标做 value。
复杂度分析
-
时间复杂度:O(N)。其中 N 是数组中的元素数量。对于每一个元素 x,我们可以 O(1) 地寻找
target - x
。 -
空间复杂度:O(1)
Review
本期的文章来自 Medium。
原文:These 10 Flutter Widgets Every Developer Should Know
本文主要介绍了 Flutter 开发中常用的 10 个基础 Widget。
- SafeArea
- Expanded
- Wrap
- Opacity
- FutureBuilder
- FloatingActionButton
- PageView
- Table
- FadeInImage
- StreamBuilder
作者选取的这 10 个 Widget 对于实际开发来说还是很实用的,每一个 Widget 都能针对性的解决一些问题,并且在实际使用中也很简单并且稳定。
当然,在真实的项目中,肯定不止这 10 个 Widget,不过就像作者在开篇写的一样:programming is a field that you should practice or getting to know something about daily(编程是一个需要每天实践或者学习一点东西的领域)。
我们可以以这 10 个 Widget 作为基础,每天学习一点新知识,不断壮大自己的知识体系。
Tip
使用 Dart 的两个技巧
1.Use the call Method To Make Classes Callable Like a Function
使用 call 方法让类像函数一样可以被调用
定义一个类:
class CallMethodDemo {
// Defining call method
String call(String name) => 'hello $name';
}
使用:
void main() {
var call_input = CallMethodDemo();
// Calling the class through its instance
var call_output = call_input('jack');
print(call_output);
}
这种让实例像函数那样被直接调用的类,称为 callable class
。
注意:一个类只允许有一个 call 方法
2.Use entries To Iterate Through a Map
使用 entries 来遍历 map 集合
for (var entry in moneySpent.entries) {
// do something with keys and values
print('${entry.key}: ${entry.value}');
}
保证空安全。
Share
文章链接:Flutter state management for minimalists
状态管理一直是响应式编程中一个绕不开的话题。目前 Flutter 有很多状态管理的框架,比如 Provider、Bloc、Redux、MobX 等等。
这篇文章的目的并不是对比每个框架的优缺点,而是试图去解释清楚什么是状态管理。
学习了这篇文章再去理解各个状态管理框架的话,应该能有所收获。