0%

ARTS 第一周

Algorithm

引子

本周题目起源于一次QQ交流群的讨论, 一位群友参加面试,被问到如何在一个数组原地对元素进行重排列, 要求所有奇数元素在数组头部,所有偶数元素在数组尾部。不知道群友是如何作答的, 只是说方案为On复杂度的时候被面试官否了, 群主发出一道leetcode类似题目, 于是做了一下, 给出两种解答。

题目描述

这是一道easy难度的题目, 对算法实力可能并没有什么要求,确实到ac也就只花了五六分钟, 但是本周由于无休止工作上的事情耽搁, 就它了。

Given an array A of non-negative integers, return an array consisting of all the even elements of A, followed by all the odd elements of A.

You may return any answer array that satisfies this condition.



Example 1:

Input: [3,1,2,4]
Output: [2,4,3,1]
The outputs [4,2,3,1], [2,4,1,3], and [4,2,1,3] would also be accepted.


Note:

1 <= A.length <= 5000
0 <= A[i] <= 5000

分析

  1. 首先采取的想法是首尾指针, 向中间靠拢,当遇到不符合自己迭代条件的元素时交换他们, 这个做法,时间复杂度为On, 无需额外空间, 首先使用C++写了个实现测试了一下, 32ms, 11.2M空间使用, 差不多每个分量都在前5%了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
vector<int> sortArrayByParity(vector<int>& A) {
register int begin = 0, end = A.size() -1;
while (begin < end) {
while ((A[begin] & 0x01) == 0 && begin < end) begin++;
while ((A[end] & 0x01) == 1 && begin < end) end--;
if (begin >= end) break;
register int t = A[begin];
A[begin++] = A[end];
A[end--] = t;
}
return A;
}
};

其中, 在不声明关键字 register 时, 空间消耗要多0.1M, 将 begin,end 声明为寄存器变量后, 可以节约一点点内存使用,同时应该能加快一点点访存速度。实际上在声明它之前,我并不确定它有效, 测试了一下,果然是可以的。

  1. 这一个方案源于一次信口开河, 讨论时凭直觉我感觉可以只用一次循环完成任务, 而并不像第一个方案中需要大循环里面嵌两个平级的循环。当时没有想到方案, 当天睡前忽然灵光一闪, 第二天写出来试了一下, 果然可行。 这个思路是使用跟随指针(follow), 当步进指针(step)遇到不满足的元素后, 使用跟随指针标记位置, 步进指针去寻找合适的替换元素, 交换后跟随指针直接跟随步进指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
vector<int> sortArrayByParity(vector<int>& A) {
int follow=0, step=0, end = A.size();
bool finding = false;
while(step < end) {
if (!finding && (A[step] & 0x1) == 0) {
step++;
continue;
} else if (finding && (A[step] & 0x1) == 1) {
step++;
continue;
}
if (finding) {
A[step] ^= A[follow];
A[follow] ^= A[step];
A[step] ^= A[follow];
step = follow;
}
follow = step;
finding = !finding;
}
return A;
}
};

实际上, 上面代码并没有体现完整思路, 它是个阉割版, 而运行时间也证明了这一点60ms, 它在发生交换后, step指针回溯到了folow位置, 那么从follow位置,到当前的step位置的所有元素,都要重新进行比较计算,它时间复杂度可不是On, 这里应该再引入一个记录指针 rec, 用来在发生交换时, 记录step当前的位置, 这样, 我们就知道 follow ~ rec 之间的所有元素也是需要进行交换的, step可以不回溯而直接向前寻找,而又能使用单层的简单循环实现AC, 这个方案我没有贴出来 因为我还没有写一个AC版本, 本周实在太忙了。

Review

本周, 其实是上周日开始看一篇medium上的文, 作者是从业几十年的技术前辈,从经历看来开发过各种面向对象语言的程序, 他以诙谐幽默风趣又不是严谨的批判风格揭露了面向对象编程范式的缺陷。

[ 再见!面向对象编程 (GoodBye, Object Oriented Programming) ](https://medium.com/@cscalfani/goodbye-object-oriented-programming-a59cda4c0e53)

读后反思

本人从一入行开始就接触的是面向对象编程, 并以之为圭臬, 从未敢质疑这个最主流的编程范式, 虽然有时对于继承感到不方便, 也总是找各种偏门左道的小技巧来完成, 看过这篇文章竟然有种恍然大悟的感觉, 没有什么范式是十全十美的, 应是各有优劣, 总有它最适合的舞台

文章反复读之,我认为作者并不是在纯粹反对面向对象编程范式, 而是针对它的缺点漏洞进行针砭, 尤其对于它重要的承诺"重用", 文中举的例子有力的证明了这一点。

第一点 继承是最大的错误

表面看, 继承似乎是面向对象的范式的最大好处, 层次结构明确,似乎逻辑意义明确, 也贴近现实世界的表达。但是重用这个年度词也就停留在了那一刻。 ### 第二点 香蕉猴丛林问题 这个问题就是由于继承引发的典型问题, 本来仅需要一个工具类A, 若它有父类, 那么则还需要引用PA, 如果PA中依赖了其他类,那么不得不映入PA', 以及PA'的父类, 这就导致了本来只想要一个香蕉, 但是却要拿来正片森林, 森林里有个猴子, 它手里拿着个香蕉, 特别形象, 忍不住拍案叫绝。是的, 这就是为了代码重用而引来的包袱负担。

原文机译

面向对象语言的问题在于它们具有所有这些隐含的环境,它们随身携带。你想要一个香蕉,但你得到的是一只拿着香蕉和整个丛林的大猩猩。

同时, 作者提出针对这个问题的方案, 尽量限制继承的层级, 因为继承而引入了对重用性的限制, 那么宁愿不要继承。

第三点 菱形引用问题

我个人认为应该是不单单针对C++编程语言中的多继承, 对于我熟悉的java来讲,它虽然不支持多继承, 但是也通过另一个形式"接口"的实现, 引入了这个问题。 作者在这里引入了大量的例子来阐述这个问题, 我并不打算一一列举, 大致就是, 如果继承自的多个类拥有同样签名的两个方法,那么在调用时会出现迷惑, 到底是哪个, 如果接口有同样的声明, 那么复写时又该是哪个, 更遑论c++中, 还存在父类对象的内存空间分配问题,造成污染和浪费。

第四点 脆弱的基类问题

在进行继承时, 父类可以隐藏其实现,那么当父类的实现发生了变化的时候, 子类的行为将会不可控, 例如, 父类的一个被子类复写的方法实现了一个统计值, 那么, 由于子类的复写, 这个值可能并不能被正确计数, 从而导致后面一系列的行为不可控。 想想确实可怕。

第五点 封装引入的问题暂时缺席

作者对于封装引入的一些问题, 我还有些内容需要再想想, 这里先不描述封装的一些问题。

第六点 多态

作者貌似也没整明白这里应该怎么说, 这里多态应该通过Interfaces提供, 而不是面向对象, 这样就可以弃其糟粕了, 貌似的推崇了下函数式编程, 我认为, golang所实现的面向对象方式也很好。

Tip 小技巧

在文件系统中按照内容搜索文件。 ### 引子

今天我遇到一个错误, 大概是这样的, 浏览器报出有个js文件找不到, 但是这个文件在浏览器端根本看不出它到底是在哪个位置(服务器做了路径重新映射), 只知道是个fancybox.js, 应该是某个配置或者是什么指定需要加载的文件, 然后我在服务器上搜索了一番, 果然应该不存在这样一个文件, 那我得找找看是哪里写了引用它了, 问题来了, 工程下包含资源文件在内几万个文件, 几百个目录, 找到这个文件将是个问题。

过程

为了这个几乎还是一次性的需求写一个脚本无疑是不合成本的。

隐约还记得在文件系统中查找文件有几个命令, 分别是whereis, search, find , 其中,前两个应该是必须要知道文件名的情况下,从文件系统索引数据库中找到该文件的完整路径, 而find是按照或者名称,或者类型或者修改时间列出所有符合条件的文件, 看起来,这几个命令并不适合直接用来寻找。

通常, 我过滤或者统计信息的时候,都是用grep和awk命令, awk命令主要用来做一些统计性工作多一些, 那么 grep 就跳出了视野。

所以, 我想, 利用 find命令列出所有文件路径, 通过grep 命令读取他们的内容并过滤我想要的结果

按照文件内容查找文件

1
find . | xargs grep "i wanted content"

首先我写出尝试

1
find . -name * | xargs grep "xxxx"
却发现会有报错

find: paths must precede expression: db.json
Usage: find [-H] [-L] [-P] [-Olevel] [-D help|tree|search|stat|rates|opt|exec|time] [path...] [expression]

表面意思大概是路径必须跟在表达式之前??? 虽然不明白什么意思,大概是通配符的问题, 把通配符加上''之后果然没问题了, 后来试了下, 如果 -name * 那么可以直接省略。 于是最后得到上面一个式子。

Share

动规问题, 从新手到专家 这篇并不是本周才看到的, 这是之前我还不懂什么是动规的时候(现在也称不上懂)反复阅读的一篇文, 发在这里, 贡献大家。

贝叶斯定理

贝叶斯定理定义

贝叶斯定理的定义:

对于随机事件 \(A、B\) 若有 \(P(B) \neq 0\) 则存在: \[ \begin{aligned} P(A|B)*\frac{P(B)}{P(A)} &= \frac{P(AB)}{P(B)} * \frac{P(B)}{P(A)} \\ &= \frac{P(AB)}{P(A)} \\ &= P(B|A) \end{aligned} \]

稍微变一下, 可以写一个更好记的形式

\[ P(B|A) = P(A|B)*\frac{P(B)}{P(A)} \]

贝叶斯定理的推导

由条件概率公式:

\[ P(A|B) = \frac{P(A\bigcap B)}{P(B)} \tag{1} \] \[ P(B|A) = \frac{P(A\bigcap B)}{P(A)} \tag{2} \]

我们将(1)式做如下变换:

将公式1乘以 \[ \frac{P(B)}{P(A)} \\ \] 得到 \[ P(A|B) * \frac{P(B)}{P(A)} = \frac{P(A\bigcap B)}{P(A)} \tag{3} \\ \]

公式(3)的等号右边即为公式(2)的右边, 即: \[ P(A|B) * \frac{P(B)}{P(A)} = P(B|A) \tag{4} \]

公式(4) 即为贝叶斯定理公式

ARTS-5

Algorithm

引言

本周的题目为从之前没做的题目中顺序拿的, 没有特别选取。 感觉下一周可以做这个Hard的sudoku resolver题目了。

Description

Determine if a 9x9 Sudoku board is valid. Only the filled cells need to be validated according to the following rules:

Each row must contain the digits 1-9 without repetition.
Each column must contain the digits 1-9 without repetition.
Each of the 9 3x3 sub-boxes of the grid must contain the digits 1-9 without repetition.
sudoku
A partially filled sudoku which is valid.

The Sudoku board could be partially filled, where empty cells are filled with the character '.'.

Example 1:

Input:
[
  ["5","3",".",".","7",".",".",".","."],
  ["6",".",".","1","9","5",".",".","."],
  [".","9","8",".",".",".",".","6","."],
  ["8",".",".",".","6",".",".",".","3"],
  ["4",".",".","8",".","3",".",".","1"],
  ["7",".",".",".","2",".",".",".","6"],
  [".","6",".",".",".",".","2","8","."],
  [".",".",".","4","1","9",".",".","5"],
  [".",".",".",".","8",".",".","7","9"]
]
Output: true

Example 2:

Input:
[
  ["8","3",".",".","7",".",".",".","."],
  ["6",".",".","1","9","5",".",".","."],
  [".","9","8",".",".",".",".","6","."],
  ["8",".",".",".","6",".",".",".","3"],
  ["4",".",".","8",".","3",".",".","1"],
  ["7",".",".",".","2",".",".",".","6"],
  [".","6",".",".",".",".","2","8","."],
  [".",".",".","4","1","9",".",".","5"],
  [".",".",".",".","8",".",".","7","9"]
]
Output: false
Explanation: Same as Example 1, except with the 5 in the top left corner being 
    modified to 8. Since there are two 8's in the top left 3x3 sub-box, it is invalid.
Note:

A Sudoku board (partially filled) could be valid but is not necessarily solvable.
Only the filled cells need to be validated according to the mentioned rules.
The given board contain only digits 1-9 and the character '.'.
The given board size is always 9x9.
    
    
    Success
    Details 
    Runtime: 32 ms, faster than 12.85% of C++ online submissions for Valid Sudoku.
    Memory Usage: 13.9 MB, less than 25.89% of C++ online submissions for Valid Sudoku.
    

Soduku 国内叫数独, 是一个填字游戏, 相信大多数人都玩过。 规则为在一个 9*9的矩阵的空白处填入取值为1-9的数字, 使的每一行, 每一列, 每个九宫格分块都是由 1-9 这九个数字组成, 都有且不重复。

思路

首先是最直观的思路, 因为表中有未填写的格子, 使用了 . 来填充, 所以这是个未完成的数独图, 那么验证是否能够完成这个数独, 仅需验证每一行/列/块中是否出现了重复的数字即可, 这依据的是完成的数独图的性质, 1-9数字出现且仅出现一次。

Result1

Status Runtime Memory Language
Accepted 32 ms 13.9 MB cpp

Code1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
bool isValidSudoku(vector<vector<char>>& board) {
vector<set<char>> columns = vector<set<char>>();
vector<set<char>> rows = vector<set<char>>();
vector<set<char>> blocks = vector<set<char>>();

for (int i=0; i<9; ++i) {
columns.push_back(set<char>());
rows.push_back(set<char>());
blocks.push_back(set<char>());
}

int ln = 0;
for (auto line : board) {
int col = 0;
for (auto c : line) {
if (c=='.') { ++col; continue;}
if (rows[ln].find(c) != rows[ln].end())
return false;
if (columns[col].find(c) != columns[col].end())
return false;
int blk = ln/3*3 + col/3;
if (blocks[blk].find(c) != blocks[blk].end())
return false;
rows[ln].insert(c);
columns[col].insert(c);
blocks[blk].insert(c);
++col;
}
++ln;
}
return true;
}
};

结果分析1

原以为这个方案效率可以接受, 但是从结果上来看, 位于c++方案的后半截, 现在看来有明显浪费的地方就是行、列、块这27个set了, 感觉它们不需要每个都存在,仅当需要进行判断是存在即可, 但是感觉这样会导致对输入数据多次遍历, 权当测试了, 所以, 后面优化了一版。

方案2

方案二待改


Review

What Is a Pure Function in JavaScript?

JavaScript中纯函数是什么

引言

本篇是ARTS-3中的Review部分 an introduction of to principles of Functional Programming 文内的引用的使用javascript做介绍的部分。 由于我之前阅读了函数式编程简介, 本周给我的周推送, 感觉推送还挺精准的。

纯函数的特征

在阅读本文之前,我觉得有必要先回顾一下纯函数的概念(或者说我自己的总结)

一个函数,具备以下两个特征, 那它是纯函数 * 多次执行,给定相同的参数必然得到相同的结果。 * 执行中,不会带来可以观察到的副作用状态改变。

关于内容

Same Input => Same Output

文章开始简介了纯函数的特征, 作者并给出了一个配图, 图片如下 pure function

原图出自 https://cdn-images-1.medium.com/max/1600/0*a_yub2gTwY-1eK8j.png

特征与我之前总结的一般无二, 作者在这里根据JavaScript语言的应用场景将之更加细化了, 直接指出了 记日志、http请求、写磁盘 等行为 是会造成可观测到的影响的。 文章原文中使用了 side-effects 来描述这一现象。

The Checklist
A function must pass two tests to be considered “pure”:

* Same inputs always return same outputs
* No side-effects

作者使用了几个简单的代码例子来举例说明了 Same Input => Same Output 感觉这几个例子有点刻意, 可以丑化了非纯函数的实现, 用来说明纯函数的优点的话感觉这里并不贴切, 毕竟,正常人也不能写出demo中的代码。

No Side-Effects

avoid side-effects
This test itself is a checklist. A few examples of side-effects are

Mutating your input
console.log
HTTP calls (AJAX/fetch)
Changing the filesystem (fs)
Querying the DOM

简译为: 1. 改变输入(这里应当为参数) 2. 打印日志 console.log 3. 发起HTTP请求(ajax/fetch) 4. 文件系统的改变 5. 查询DOM

最明显, 我感觉 console.log 是不必要的, 在平时很多场景下, 我都会在函数里打log, 它只用于输出必要信息, 如果日志系统影响到了正常的业务逻辑, 那只能是说明代码结构有问题了, 日志系统应完全独立于业务系统, 所以,这个点上我是不赞同的。即使, 后文中作者说了console.log可能不会造成恶劣的影响。

其他几点上, http请求可能也不那么赞同, 发起请求并不会对系统内产生可观测的影响吧, 感觉这里是, 在函数里使用http请求的结果更合适一些。(后来想想, 我是错了, 发起请求后可能会收到响应, 这个会改变系统的整体状态, 按照严格意义上来说的话,是产生了影响)

其余几点没什么好说的。

采用返回新对象的形式避免更改输入参数

针对避免影响外部的第一点, 避免修改输入参数, 先贴一段作者的demo代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
const pureAssoc = (key, value, object) => {
const newObject = { ...object };
newObject[key] = value;
return newObject;
};
const person = {
name: 'Bobo'
};
const result = pureAssoc('shoeSize', 400, person);
console.log({
person,
result
});

按照一般的OOP风格, 这段代码的关键函数 pureAsoc 很可能绝大多数人,当然也包括在下, 实现成如下的形式:

1
2
3
4
const pureAssoc = (key, value, object) => {
object[key] = value
return object;
};

这个实现方式就是典型的违背了避免修改入参这一点。

看起来这个代码是完美了, 但是, {...object} 语法实现的是浅拷贝(shallow copy), 一旦有嵌层的对象,还是没办法从根本上解决问题。于是, 作者又贴了一段代码:

1
2
3
4
5
6
7
const person = {
name: 'Bobo',
address: { street: 'Main Street', number: 123 }
}
const deepPersonClone = JSON.parse(JSON.stringify(person));
deepPersonClone.address.number = 456;
console.log({ person, deepPersonClone });

这段代码确实实现了深拷贝, 但是作为一个不太写JavaScript的强迫症患者, 感觉先将对象 stringify 成一个字符串,再重新解析一遍成js对象的做法深感受不了。

总结

A Function’s pure if it’s free from side-effects and returns the same output, given the same input.

Side-effects include: mutating input, HTTP calls, writing to disk, printing to the screen.

You can safely clone, then mutate, your input. Just leave the original one untouched.

Spread syntax (… syntax) is the easiest way to shallowly clone objects.

JSON.parse(JSON.stringify(object)) is the easiest way to deeply clone objects. Thanks again Rodrigo Fernández Díaz!

Tips

打破直观的编程方式实现对数据配置代码的压缩

引言

上周连续的加班,周日的时候老大过去视察, 我跟他说了我在上一篇ARTS中分享的Tips点, 使用代理的模式来避免代码修改。老大很惊奇的跟我说, 我们的配置数据一直是这么做的啊, 你没有看过么。

好吧,我确实看过, 但是, 动态语言有一些写法我真的没看懂, 技术领导力极强的老大不辞劳苦给我讲了一遍, 于是, 就有了这周的Tips。

背景

我现在做的项目是一个手游MMORPG游戏, 其中客户端使用lua实现逻辑, 我们需要将策划同学配表(csv表格)转换成lua代码, 以便我们在实现业务代码的时候直接调用。

一般的配置文件就是一个二维表格, 有时候这个表格会非常大举个例子

场景Id 地图Id 场景名 容纳上限 进入等级Min 进入等级Max 是否允许Pk param1 param2 param3 param4
1 1 主城 200 1 999 TRUE 0 0 0 0
.. .. .. .. .. .. .. .. .. .. ..

简单直观做法

按照最简单直观的做法生成的配置, 大概是这样的

1
2
3
config = {}
config[1] = { 场景Id = 1, 地图Id = 1, 场景名 = '主城', 容纳上限 = 200, 进入等级Min = 1, 进入等级Max = 999, 是否允许Pk = TRUE, param1 = 0 , param2 = 0 , param3 = 0 , param4 = 0}
return config

使用时代码

1
2
3
4
local cfg = config[1]
if cfg.param1 > 122 then
-- do game logic
end

该做法有个一眼能看出来的严重的弊端, config是一个数组, 它里面的每个元素都是一个table,而且是map形式的table, 此时, 每行数据都多余的存了一个key的信息, 实际上, 我们严谨的二维表格是不需要这个信息的。于是, 就给了我们一个优化空间。

简单优化做法

方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
config = {}
data = {}
index = { 场景Id = 1, 地图Id = 2, 场景名 = 3, 容纳上限 = 4, 进入等级Min = 5, 进入等级Max = 6, 是否允许Pk = 7, param1 = 8 , param2 = 9 , param3 = 10 , param4 = 11}
data[1] = { 1, 1, '主城', 200, 1, 999, TRUE, 0, 0, 0, 0}
data[2] = { --[[ ... ]]}

local newget = function(i)
local instance = {_i = i}
setmetatable ({},
{
__index = function(table, key)
local idx = index[key]
return data[table._i][idx]
end
}
return instance
)
end

table.insert(config, newget(1))
table.insert(config, newget(2))

config.get = function(idx) return config[idx] end
config.all = config
return config

使用方法

1
2
3
4
5
6
7
8
9
10
11
local cfg = config.get(1)
if cfg.param1 > 122 then
-- do game logic
end

-- 遍历方案
for _, cfg in pairs(config.all) do
if cfg.param1 > 122 then
-- do something
end
end
经过上述优化, 使用方式基本不变, 其内存占用量可以节省百分之五十以上。

优化原理

直观方案的浪费源头主要在对于每一行数据, 都需存它的key, 那么有N行的话, 就有(N-1)份的key是浪费的, 明显的, 一个配置列数据基本是一个简单类型, 那么, 直观方案我们冗余的key的存储空间比实际上数据占用的空间更多!!!

优化时, 我们只保存一份key, 然后建立一个索引表存储对应关系, 这样, 我们在使用key访存时, 可以做一个映射, 然后去数组结构中获取相应的数据, 但是, 我们需要多开辟一份空间来存这个索引表。 一个表的行数越多, 节省就越明显。

更激进的优化

这个方案这里我不贴代码了, 因为实在太复杂, 老大只是口头传授了我, 也没有给我一个代码demo看一下。

其原理类似于稀疏矩阵的存储, 对于绝大多数的表, 尤其是大表来说, 其中很多列的内容都是相同的, 或者为空, 或者是同一个值, 或者是 0-5之间取值, 甚至列类型是布尔值。 对于这种列, 我们就不再存储到 data 行中, 而是在行中插入一个函数, 它实际上可以被大多数列共享, 用于返回公共数据。

请参考: 稀疏矩阵存储


Share

这是一篇翻译国外大神前辈关于插值实践的文章, 读完收获很大, 本周又多看了一遍。

插值那些事儿


End


ARTS-6

Algorithm

引言

放了个劳动节假, 玩的有点疯了, 所以这个题目就来个简单点吧

Description

题目

3Sum

Given an array nums of n integers, are there elements a, b, c in nums such that a + b + c = 0? Find all unique triplets in the array which gives the sum of zero.

Note:

The solution set must not contain duplicate triplets.

Example:

Given array nums = [-1, 0, 1, 2, -1, -4],

A solution set is:

    [
      [-1, 0, 1],
      [-1, -1, 2]
    ]

大意

给出一个整数数组nums, 找出数组中所有的不同组合 a, b, c , 使之满足 a + b + c 之和等于0, 输出他们。

思路

题目提供的数组是不保证顺序的,在这种情况下, 如果直接处理,那么我们不可避免的就要进行全遍历了, 显然这不可能是最优解, 所以, 最初的思路就是使源数组有序, 首先, 对它进行排序,使之升序排列。

对于有序数组, 我们只需要控制游标移动就可以逐步进行和的数值调整, 所以, 每一次, 选取一个基准元素作为最左侧元素, 然后, 设置两个游标在它右侧, 逐步调整这两个游标使和为0即可。

Resolution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
int dimension = nums.size();
sort(nums.begin(), nums.end());
vector<vector<int>> result;
for (int i=0; i<dimension-2; ++i) {
if (nums[i] > 0) break;
int f=i+1, t=dimension-1;
while (f < t) {
int sum = nums[i] + nums[f] + nums[t];
if (sum == 0) {
result.push_back(vector<int>({nums[i], nums[f], nums[t]}));
while (i < dimension-2 && nums[i]==nums[i+1]) {i++;}
do {t--;} while(f < t && nums[t]==nums[t+1]);
do {f++;} while (f < t && nums[f]==nums[f-1]);
} else if (sum > 0) {
t--;
} else {
f++;
}
}
}
return result;
}
};

Result

Success
Details 
Runtime: 100 ms, faster than 91.61% of C++ online submissions for 3Sum.
Memory Usage: 14.9 MB, less than 97.80% of C++ online submissions for 3Sum.
Time Submitted Status Runtime Memory Language
a few seconds ago Accepted 100 ms 14.9 MB cpp

还可能的优化

在游标调整过程中, 感觉至少一个游标的调整可以使用二分调整, 不至于每次只调整一点, 这样感觉可以进一步优化执行效率。


Review

本次Review的文在这里: Synchronization and Object Locking

引言

之前就立了Flag, 要写一篇java虚拟机关于锁实现的分享, 在参考了郑雨迪老师关于HotSpot关于实现synchronized的文章后, 整理了我对hotspot做法的理解, 这篇文章是其中一篇参考文, 在阅读之余, 进行分享。

内容

作为java语言的主要优势之一, java语言提供了语言层面的内建多线程机制的支持。任意一个对象在不同线程间共享访问可以被加锁, 以同步不同线程的访问。java提供了原生的临界区代码设计, 它通过一个共享的锁对象, 一次仅允许一个进程进入临界区执行, 第一个进入临界区的线程会将对象加锁, 当下一个线程要进入同一个临界区之前就必须等待前一个对所对象加锁的线程释放锁。

对于 Java HotSpot™ VM 一个对象由指向类的指针和一个字的头作为先导, 本文中,把它称呼为对象头。对象头存储对象特征哈希码和在垃圾回收标记代数的bit位, 也用于实现瘦锁方案。

下图展示了对象头的布局结构和它在不同状态下所代表的含义。

其中, 对象头的最后两个位标记其用途,这里需要特别特别特别注意, 因为其取值特别容易被记混, 而且, 我记错了好多次。

对象头的标记位的取值和含义

  • 01 无锁或者偏向锁
  • 00 轻量锁
  • 10 重量锁
  • 11 垃圾回收相关
00 01 10 11
轻量级锁 无锁或者偏向锁 重量级锁 gc相关

为什么这么取值

如果不弄懂这个加锁的过程, 那么对于为何这么取值会感到很困惑, 为什么状态不是按照直观顺序的呢, 这里关键应该是轻量级锁的加锁过程。

  1. 判断如果标记位不为01(无锁), 那么按照加锁过程去处理,否则, 进行下一步
  2. 将对象头和对象地址构造一个锁记录, 存入当前栈帧。
  3. 将锁记录的地址cas存入对象头。 因为栈帧是按照字(word)对齐的, 其地址肯定是00结尾, 所以, cas成功后, 对象头的标记位肯定为00, 此状态为轻量级锁加锁成功。

所以,以上轻量级加锁过程说明了为什么轻量级锁时对象头的标记位为00。 记住了这个过程, 就不太容易记混了00的含义了。

HotSpot™ 对象头结构

图片内容引自OpenJDK的Wiki https://wiki.openjdk.java.net/download/attachments/11829266/Synchronization.gif?version=4&modificationDate=1208918680000&api=v2

图右侧部分展示的是正常(标准)的锁的状态转换过程。

下面是我按照文档自己总结的一个轻量锁的加锁过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
graph TB

SUCC[拿到锁,进入临界区]
END(结束)
Start(monitorenter)-->CheckBit{末位是否为 01}
CheckBit-->|是|LightLocking[复制对象头到当前栈帧]
LightLocking-->SwapHeader[构造锁记录到栈帧并将地址cas对象头]
SwapHeader-->CheckSwapSucc{CAS操作成功}
CheckSwapSucc-->|CAS成功|LightLocked[轻量级锁成功]
LightLocked-->SUCC
CheckSwapSucc-->|CAS失败|SwapFailed{对象头指针指向当前栈帧}

CheckBit-->|否|HaveLocked[此时已有线程进入了临界区]
HaveLocked-->CheckLockType{此时锁类型}
CheckLockType-->|标记位00|ThinLocked[此时该对象为轻量锁]
ThinLocked-->SwapFailed{对象头指针指向当前栈帧}

SwapFailed-->|是|HoldBySelf[栈帧内的锁记录置0]
HoldBySelf-->LightLocked

SwapFailed-->|否|Inflate[膨胀为重锁]
Inflate-->END

CheckLockType-->|标记位10|HeavyWeightLocked[此时该对象为重锁]
HeavyWeightLocked-->Suspend[线程挂起等待唤醒]
Suspend-->END
SUCC-->END

锁状态示意图, 图中忽略了偏向锁和自旋过程。


Tips

Vim 中, 往命令中粘贴内容。

引言

我是一个资深vimer, 我承认我现在分享这个内容真实丢vimer的脸。但是作为一个手动挡, 我一直都是这么做的。 每当要在文档中搜索或者替换一个内容的时候, 我都会不厌其烦的一个个字符敲入, :% s/search_my_content_balabalabala/i am the new balabalabala/g 然后认真反复检查3遍, 按下enter键, 一次替换完成, 我在过去的七八年里都是这样做的。

我见要分享的这个小技巧, 就是为了简化这个过程的, 虽然我自己并不常用它。 如何在命令行里粘贴剪切板的内容。

做法

<C-r> 在命令输入模式下, 按快捷键 Ctrl + R 开启粘贴模式, 再键入一个寄存器名, 就会将该寄存器内的内容粘贴在命令上。

于是, 搜索做法就变成了 1. 切换成visual模式 (v) 2. 复制想要搜索的内容 (y) 3. 进入命令模式, 敲入搜索/ 4. Ctrl + R 5. 按", 将刚复制的内容贴入命令, 然后Enter搜索

给出一个demo

预备一个文件:

原文件 1. 切换成visual模式, 复制搜索内容Text (normal下移动光标到Text按v,然后e到单词末尾, 按y)

复制待选内容
  1. 进入命令模式, 搜索/
搜索
  1. 键入Ctrl + r 进入选取寄存器步骤
选取寄存器
  1. 键入", 匿名寄存器刚复制的内容上屏, 回车搜索
搜索内容

这里有一点, 如果要粘贴系统剪切板的内容, 那么寄存器名字应输入0,而不是+


Share

引言

这是一篇版权文章, 非订阅用户阅读可能会受到限制。这是一篇讲解hotspot关于如何实现synchronized关键字的简单教学文, 后面我会分享我参考这篇文章写得自己关于jvm的synchronized的见解。

内容

Java虚拟机是怎么实现synchronized的?


End

Pip 切换源

背景

一些必备库往往位于海外, 由于伟大防火墙存在, 加之海外线路带宽有限, 往往使用pip的官方源的时候安装一些库的时候速度感人。所以, 有必要使用国内的源来加速库的安装。

方法

源地址

test

国内源: 新版ubuntu要求使用https源,要注意。

1
2
3
4
5
6
7
8
9
10
11
清华:https://pypi.tuna.tsinghua.edu.cn/simple

阿里云:http://mirrors.aliyun.com/pypi/simple/

中国科技大学 https://pypi.mirrors.ustc.edu.cn/simple/

华中理工大学:http://pypi.hustunique.com/

山东理工大学:http://pypi.sdutlinux.org/

豆瓣:http://pypi.douban.com/simple/

临时

pip 有一个参数-i可以用于指定仓库的url, 但是很遗憾这个参数我通过 pip --help无法查到, 大概是个黑科技。

仅临时修改使用的源, 可以使用 pip -i <理想源地址> 来指定使用的源

永久

Like unix

如果用户目录下不存在.pip目录, 则创建一个

/home/user/.pip 目录下创建一个pip.conf 文件, 并编辑其内容

1
2
[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple

Windows

用户目录下如果不存在pip目录, 则创建一个

在pip目录中创建pip.ini文件, 编辑其内容

1
2
[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple