Git 非常强大,强大的同时伴随的时学习成本的提升。现在感触比较深的教训就是初学 Git 时不要想太多,把各个功能分离开来。我最初学习的时候总是在想如果我修改了一些工作区文件,然后切换了分支,那么工作区的文件会怎样?如果我撤销了修改,本地的暂存区会怎样?现在回想起来,其实有这种想法本身就是一个错误。
根据我自己经常用的一些操作,我把 Git 的操作分为三类:
下面来依次进行说明。
提交修改:
|
查看状态:
|
建议在 stage 之前以及 commit 之前都进行 git status
,这样有问题能够及时发现,比如有文件忘了提交,如果是在 commit 之前发现,只需要 stage 一下就好,如果是在 commit 之后发现,可以用新的提交覆盖掉:
|
当然,不排除可能某一天心血来潮查看 commit 历史发现两个 commit 应该合起来,那就需要 rebase -i
了,这里不多说。
通常我在提交前还会进行 diff 操作,diff 比较简单,像这样:
|
当然,不排除有想比较两个版本中文件的冲动,这当然是可行的:
|
查看项目历史也是非常需要的,众所周知需要 git log
命令,但是默认的 git log
往往不尽如人意,尽如人意的 git log
命令往往又长到让人吐血,这时候就需要 alias 啊,通常一两个关于 log 的 alias 就够用了:
|
与标签相关的操作虽然不常用但还是有必要了解的:
|
在讲 Git 的撤销操作之前,特别提醒:Git 中版本库中的文件同步到工作区中间一定会途经暂存区,同理,工作区的文件提交到版本库也一定会经过暂存区,暂存区作为一个缓冲带,我没有发现哪条命令可以跨越。
平时误操作基本都是 Ctrl+Z 或者 u 解决,但是这并不适用于所有场景。
把 Git 的撤销操作分为对文件的撤销操作与对 commit 的撤销操作,先说文件的,最常见的莫过于 git status
的提醒:
(use “git checkout –
…” to discard changes in working directory)
不用多说,这条命令就是将暂存区的文件同步到工作区。当然,实际需求可能不止如此,比如,把某个 commit 中的文件同步到工作区,这样操作:
|
对于 commit 的撤销,分几种情况:
1.在公共分支上,撤销后依然先于远端或者跟远端相同
|
2.在公共分支上,撤销后当前 commit 会滞后于远端,这时不要使用 reset,改用 revert(如果选择 reset 会影响到别人):
|
3.在个人分支上,这种情况是怎么折腾都无所谓的,为了有一个干净的历史,建议这样操作:
|
忘了还有一种常见的场景,如果想要撤销全部修改,恢复到上次同步的状态,只需两条命令:
|
这样本地的项目就又崭新如初了。
最后,如果找不到想要恢复的 commit,比如 reset 之后后悔,想要撤销 reset 操作,可以通过 git reflog
命令查找相应的 ID,这个 git reflog
会记录每一次对 HEAD 的操作。
特别提醒:在进行切换分支操作之前,一定要保持工作区和暂存区干净,如果当前的状态是可以提交的,就 git commit
,如果还未开发完成,就 git stash
,切回来之后 git stash pop
。
先说远程分支同步的事情,如果比较懒,通常会直接 git pull
,git pull
相当于:
|
实际项目中很难预期 Git 是否会进行快速合并,所以我更推荐这样操作:
|
当然,如果希望能一条命令完成,这是可行的:
|
但是你可能连 --rebase
都懒的打,依然可行:
|
接下来说一下分支合并时对于 merge 和 rebase 的选择吧,其实这是一个个人的喜好问题,如果用不明白 rebase 全都改用 merge 也无所谓,但是如果这样的话,切记不要在两个长期分支上来回 merge,那样项目历史会非常的混乱。
我对 merge 和 rebase 的选择依据非常简单,在个人分支上 rebase 公共分支,在公共分支上 merge 个人分支。
偶尔,对合并要求可能比较奇葩,要求只与指定的 commit 进行合并,这时候 cherry-pick
就派上用场了
|
Git 的命令还有很多,但是于我而言,上面的命令已经能满足绝大部分的场景了。
参考:
文档信息]]>
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
本文链接:www.snovey.com/2017/09/use-git.html
forEach() 方法对数组的每个元素执行一次提供的函数。
然后 MDN 还给出了一段示例代码:
|
到这里都没有什么问题,接下来我写一个例子:
|
return 无效,再换个方式:
|
仍然无效,再来看一下 MDN 的文档:
|
介绍:
forEach 方法按升序为数组中含有效值的每一项执行一次callback 函数
文档中还写到 callback 函数的返回值为 undefined,到这里原因已经呼之欲出了,因为函数传递参数时基本数据类型按值传递,所修改值并不会影响到数组本身,而且 callback 函数的返回值也与数组本身无关。
解决办法有两种:
1.获取到数组的引用
|
2.返回新的数组
|
参考:
文档信息]]>
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
本文链接:www.snovey.com/2017/09/javascrpt-foreach.html
放张图:
想感受一下效果么?戳这里:pattern-lock
要制作一个图案解锁,第一步要想好设计,对外开放哪些接口,如果我是一位开发者,我希望初始化的时候可以自定义行数列数,以及颜色大小等参数,如果用户不定义要有一套默认的参数,我希望可以调用方法获取用户的输入,并且提供一些额外的 API。就像买煎饼果子告诉老板不要加辣多放香菜,如果省略某些信息老板还要有自己的默认配置,最后给你你想要的煎饼果子。
JavaScript 实现类似这样:
|
想好之后就是实现的问题了,图案有斜线等元素,比较复杂,所以采用 canvas 实现,以常见的三排三列为例,大致需要画出如下的图案,采取坐标轴如下(别问我为什么是左手系):
这里定义 $xunit$ 与 $yunit$ 作为单位长度,方便后续的计算,这两个值根据 canvas 的宽高以及小圆的个数计算:
$$
\begin{cases}
xunit = \frac{width}{2 \times column} \\
yunit = \frac{height}{2 \times row}
\end{cases}
$$
根据 $xunit$ 与 $yunit$ 就可以计算出第 $i$ 排第 $j$ 列的圆的圆心的坐标:
$$
point[i][j] = (2 \times (i+1) \times xunit, 2 \times (j+1) \times yunit)
$$
有了公式后进行初始化,计算 $xunit$ 与 $yunit$,使用循环把各个点的坐标存入二维数组中,同时标记节点未触摸过。JavaScript 代码:
|
接下来是绑定三种事件, touchstart
,touchmove
和 touchend
,这三个事件处理的事情差不多,无论哪个事件首先都要获取位置,判断是否与圆相交,这里通过
$$
\begin{cases}
recentX = \left(2 \times \left \lfloor \frac{touchX}{2 \times xunit}\right \rfloor+1 \right) \times xunit \\\\
recentY = \left( 2 \times \left \lfloor \frac{touchY}{2 \times yunit}\right \rfloor + 1 \right) \times yunit
\end{cases}
$$
算出与触摸的点最近的圆心,然后计算这两个点的距离的平方, 与半径的平方进行比较即可得知是否在圆内。每个事件都要有这个操作,所以提出来做单独的函数,JavaScript 代码:
|
如果在圆内并且未触摸过该点,则标记该点,并将该点存入数组中。
当然,记得每次 touchmove
发生时都要清空画布重绘图案!重新绘制时需要将走过的节点两两连线,这里用一个 reduce
函数就搞定了:
|
最后,整个流程最消耗资源的就是每次发生 touchmove
事件时的重绘画布了,这里需要做一下函数节流和防抖,不过我做的时候出了点问题,就先搁置了。
想了解详细的使用请移步我的 GitHub
文档信息]]>
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
本文链接:www.snovey.com/2017/04/pattern-lock.html
我目前就想到这两个,那我就说说这两个,先说答案:他们没什么关系。
数学中的「闭包」:
数学中,若对某个集合的成员进行一种运算,生成的仍然是这个集合的成员,则该集合被称为 在这个运算下闭合 。
当一个集合 S 在某个运算下不闭合的时候,我们通常可以找到包含 S 的最小的闭合集合。这个最小闭合集合被称为 S 的(关于这个运算的) 闭包 。
中文维基百科
计算机科学中的「闭包」:
In programming languages, closures (also lexical closures or function closures) are techniques for implementing lexically scoped name binding in languages with first-class functions
Wikipedia
渣翻一下,大意是:
在编程语言中, 闭包 (也叫 词法闭包 或 函数闭包 )是在语言中使用一等函数实现词法作用域名称绑定的技术。
看定义他们就没什么关系,Peter J. Landin 在 1964 年将术语 闭包 定义为一种包含 环境成分 和 控制成分 的实体,来指代某些其开放绑定(自由变量)已经由其语法环境完成闭合(或者绑定)的 lambda 表达式,从而形成了 闭合的表达式 ,或称闭包。在 SICP 中也提到了这个:
The use of the word “closure” here comes from abstract algebra, where a set of elements is said to be closed under an operation if applying the operation to elements in the set produces an element that is again an element of the set. The Lisp community also (unfortunately) uses the word “closure” to describe a totally unrelated concept: A closure is an implementation technique for representing procedures with free.
Structure and Interpretation of Computer Programs, 2nd ed. p.133
注意加粗的部分。这里需要注意一下,维基百科对闭包的定义与我们通常意义上的闭包是有所区别的,我们通常说的闭包往往是指:
调用一个函数 A,返回一个函数 B,其中函数 B 引用了 A 中的自由变量。我们称函数 A 使用了闭包。
维基百科对闭包的定义是从理论角度出发的,而我们通常意义上所指的闭包是从实践角度出发的,实践角度的闭包是理论角度的一个特例。
「闭包」这个词不仅仅出现这两个地方,我再举几个相关的词。
集合论中的「自反闭包」:
在数学中,集合 X 上的二元关系 R 的 自反闭包 是 X 上包含 R 的最小的自反关系。
集合 X 上的关系 R 的自反闭包 S 的定义为
$$
S=R\cup \left\lbrace(x,x):x\in X\right\rbrace
$$
换言之,R 的自反闭包是 R 与 X 上的恒等关系的并集。
集合论中的「对称闭包」:
在数学中,集合 X 上的二元关系 R 的 对称闭包 是 X 上包含 R 的最小的对称关系。
集合 X 上的关系 R 的对称闭包 S 的定义为
$$
S=R\cup \left\lbrace(x,y):(y,x)\in R\right\rbrace.\,
$$
换言之,R 的对称闭包是 R 与 X 上的逆关系的并集。
集合论中的「传递闭包」:
在数学中,集合 X 上的二元关系 R 的 传递闭包 是 X 上包含 R 的最小的传递关系。
集合 X 上的关系 R 的传递闭包 S 的定义为
$$
S=\bigcup_{i\in \lbrace 1,2,3,\ldots\rbrace} R^i.
$$
其中
$$
\begin{cases}
R^1 = R\,\\[2ex]
R^{i+1} = R \circ R^i
\end{cases}
$$
换言之,R 的传递闭包是 R 与 X 上的传递关系的并集。
形式语言中的「Kleene 闭包」:
假定
$$
V_{0}=\lbrace\epsilon \rbrace\,
$$
递归的定义集合
$$
V_{i+1}=\lbrace wv:w\in V_{i}\wedge v\in V\rbrace\, 这里的\ i>0\,
$$
如果 $V$ 是一个形式语言,集合 $V$ 的第 $i$ 次幂是集合 $V$ 同自身的 i 次串接的简写。就是说, $V_{i}$ 可以被理解为是从 $V$ 中的符号形成的所有长度为 $i$ 的字符串的集合。
所以在 $V$ 上的 Kleene 星号的定义是
$$
V^{*}=\bigcup_{i=0}^{+\infty} V_i=\left\lbrace\varepsilon \right\rbrace\cup V\cup V^{2}\cup V^{3}\cup \ldots
$$
就是说,它是从 $V$ 中的符号生成的所有可能的有限长度的字符串的并集。
自动机理论中的 「ε-闭包」:
对于任何 $p\in Q$,从 p 可到达的状态的集合叫做 p 的 ε-闭包 ,并写为
$$
\,E(\lbrace p\rbrace)=\lbrace q\in Q:p{\stackrel {\epsilon }{\rightarrow }}q\rbrace。
$$
对于 $P\subset Q$ 的任何子集,定义 P 的 ε-闭包 为
$$
E(P)=\bigcup \limits _{p\in P}E(\lbrace p\rbrace)
$$
纵观以上种种「闭包」,除去「词法闭包」,「闭包」都是指具备某种性质的最小集合,他们本质上的思想是一样的。而「词法闭包」与他们没有什么关系。
内存管理中的「堆」:
在计算机科学中, 动态内存分配 (Dynamic memory allocation)又称为 堆内存分配 ,是指计算机程序在运行期中分配使用内存。它可以当成是一种分配有限内存资源所有权的方法。
数据结构中的「堆」:
堆(英语:Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。这种数据结构具有以下性质
- 任意节点小于(或大于)它的所有后裔,最小元(或最大元)在堆的根上(堆序性)。
- 堆总是一棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层尽可能地从左到右填入。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
这两个概念毫无关系,Donald Knuth 曾多次说起此事, TAOCP 中提到:
Several authors began about 1975 to call the pool of available memory a “heap.” But in the present series of books, we will use that word only in its more traditional sense related to priority queues.
The Art of Computer Programming, Third Ed., Vol. 1, p. 435
在 CLRS 中也说:
The term “heap” was originally coined in the context of heapsort, but it has since come to refer to “garbage-collected storage,” such as the programming languages Java and Lisp provide. Our heap data structure is not garbage-collected storage, and whenever we refer to heaps in this book, we shall mean a data structure rather than an aspect of garbage collection.
Introduction to Algorithms, Third Ed, p. 151
内存管理中的「堆」,原意是类似「一堆衣物」的「堆」,表示没有具体的顺序,而「堆内存分配」的实现也往往是链表。与数据结构中的「堆」完全没有关系,数据结构中的「堆」源自「堆排序」。
参考:
文档信息]]>
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
本文链接:www.snovey.com/2016/11/polysemy.html
好了,放代码:
Sample Input/Output
|
输入 +1
|
|
判断正负
|
Sample Input/Output
|
此时没有 abs
函数,需要手写
|
Sample Input/Output
|
得到 abs
函数,直接调用即可
|
Sample Input/Output
|
此时没有 pow
函数,需要手写
|
Sample Input/Output
|
得到 pow
函数,直接调用即可
|
Sample Input/Output
|
此时没有 length
,需要手写
|
Sample Input/Output
|
根据输入输出列表
|
Sample Input/Output
|
求两个数中大的那个,此时没有 max
函数,需要手写
|
Sample Input/Output
|
输出列表中最大的数
|
Sample Input/Output
|
判断奇偶,此时没有 mod
函数
|
Sample Input/Output
|
判断回文串
|
Sample Input/Output
|
字符排序,随便写个排序算法,我写个容易实现的冒泡排序
|
Sample Input/Output
|
补充缺失的数
|
Sample Input/Output
|
判断各元素是否相同
|
Sample Input/Output
|
二进制转换为十进制
|
Sample Input/Output
|
素数判定
|
Sample Input/Output
|
判断字符列表是否升序
|
Sample Input/Output
|
补充缺失的数字
|
Sample Input/Output
|
括号匹配,只有小括号,不需要栈
|
Sample Input/Output
|
将列表循环左移一位
|
Sample Input/Output
|
每个元素值 +1
,此时没有 map
|
Sample Input/Output
|
判断每个元素的正负
|
Nearest to [0, 0]
Sample Input/Output
|
找出列表中距离原点最近的点
|
Sample Input/Output
|
这里解释一下:
好了,上代码:
|
Sample Input/Output
|
括号匹配,有两种括号,用下栈就过了
|
Sample Input/Output
|
写个递归
|
画点
|
文档信息]]>
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
本文链接:www.snovey.com/2016/10/hacked-story.html
All function arguments in ECMAScript are passed by value.
Professional JavaScript for Web Developers, 3rd Edition
书中确实是这样写的,但是 JavaScript 中的对象就是简单的按值传递(call by value)的么?执行这段代码:
|
如果是纯粹的按值传递,那么函数内部的修改不会影响外部对象,应该都输出 unchanged
的才对,如果是按引用传递,那么二者都应该保持 changed
才对。
确切的说,JavaScript 中的基本类型值(primitive type)是按值传递的,引用类型值(Object type)是 按共享传递(call by sharing) 的。
这里补充一下 JavaScript 的内存分配方式,基本类型值是在栈空间分配的内存,而引用类型值则是在堆空间分配的内存,然后在栈空间分配一个指向对象的指针。在把对象作为参数传入函数时,会将指向对象的指针进行复制然后传递过去(不是直接传递的指针!),这样当对对象进行修改时,会改变对象,这就是 obj1.item
被改变的原因,而 y = {item: "changed"}
则是改变了指针的指向,也就与原来的对象无关了,所以 obj1.item
未被改变。更多信息请查看ECMA-262-3 in detail. Chapter 8. Evaluation strategy.
那么反过来看红宝书错了么?红宝书在这句话之后进行了解释,作者认为传递拷贝之后的对象地址也是一种传值。
参考:
文档信息]]>
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
本文链接:www.snovey.com/2016/09/call-by-value.html
代码是这样写的:
|
后来看到别人的写法是这样的:
|
觉得既然 STL 既然内置了 iterator,应该有它自己的优势,虽然这两种写法的时间复杂度都是 O(n)。
那么它们的具体区别是什么呢?在我闲逛 stackoverflow 时看到了相关的讨论,这两种方式在遍历 vector 时并无区别,但是第一种方法却不一定适用于其它容器,例如 map,而且后者也易于理解,从维护性,复用性等角度来看都是后者更具备优势。
在 STL 中还有反向迭代器,可以反序遍历容器,像这样:
|
这里反向迭代器将 ++
与 --
的含义反了过来,++
访问前一个元素,而 --
访问后一个元素。
但是为什么不像下面这样遍历呢?
|
有这种想法的人(比如我)运行一下就知道了,结果并不是想象中的那样,因为 v.end()
返回指向当前对象中 末尾之后( Past-the-end) 的元素的迭代器。一图胜千言:
|
为什么不将 end()
指向最后一个元素呢?这个问题有点类似为什么数组标号是从 0 开始的,之所以指向末尾之后的元素原因有很多,我举几个例子:
|
这样不仅不优雅,假设容器是空的,begin()
与 end()
的顺序也会令人费解。
|
所有参数与迭代器相关的函数都要补上 +1
,代码变得丑陋。
再比如,如果在容器内未找到元素之后返回的结果要改为 end()+1
而不是原来的 end()
。
还想了解更多原因的话,请看 Dijkstra 爷爷的原文:Why numbering should start at zero
如果你非常希望通过 begin()
和 end()
进行反向迭代也未尝不可,不过我不会告诉你写法,放弃这种方法吧。
最后,强制将正向迭代器反向不一定能得到想要的结果:
|
第一个输出的结果是错的,第二个与第三个的结果也不一样,所以请不要乱使用迭代器。
参考链接:
文档信息]]>
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
本文链接:www.snovey.com/2016/09/STL-iterator.html
下面详细列举一下:
a^=b^=a^=b
这个方法能够防止溢出,但是有缺点(&a != &b)。上面的推论推论可以应用于数据备份,RAID 5 用奇偶校验实现冗余。如果阵列中的一块磁盘出现故障,工作磁盘中的数据块与奇偶校验块一起来重建丢失的数据。
假设 A1、A2、A3 代表三块磁盘,Ap 用于备份。假设 A1 = 00000111、A2 = 00000101 以及 A3 = 00000000。A1、A2、A3 异或得到的 Ap 等于 00000010。如果第二个磁盘出现故障,A2 将不能被访问,但是可以通过 A1、A3 与 Ap 的异或进行重建:
A1$\oplus$A3$\oplus$Ap = 00000101
利用异或的逆运算是本身,可以进行简单的对称加密。
异或在集合中用 $\bigtriangleup$ 表示
与,或分别代表集合运算中的交,并而异或则代表对称差,证明如下:
$$
A\bigtriangleup B=(A-B)\cap (B-A)=(A\cup B)-(A\cap B)
$$
来看一道题:
有一列数,每个数字都出现了偶数次,只有一个数出现了一次,怎样在 O(1) 的空间复杂度内找到这个数?
这是一道基于交换律和归零律衍生出来的题,解法比较巧妙,这列数异或得到的结果便是那个只出现一次的数,因为相同的数在异或偶数次之后都变为 0 了。
倘若现在问题难度提高,有两个甚至更多个不同怎么办呢?网上有解到 3 个的,但是我认为从二进制的末位开始枚举,对序列不断的进行划分,问题的难度会不断的下降,最终退化为 1 个的情况,得解。
异或的一个重要应用便是格雷码(Gray code):格雷码是任意两个相邻数的代码只有一位二进制数不同的 BCD 码,它与奇偶校验码同属可靠性编码,它最初的出现是为了解决讯号传送错误。
格雷码的生成方式有三种:
有三个开关,如何在最短的步数内遍历所有的状态呢?
三位数格雷码的顺序是:000 -> 001 -> 011 -> 010 -> 110 -> 111 -> 101 -> 100
在三维空间中于相当于沿着立方体的棱不重不漏地经过每一个顶点:
当然,格雷码还可以做很多别的事情,比如分割集合、解汉诺塔,九连环。
我们进行下一题:
假设函数 $f(n)$ 是自然数 $1, 2, 3,…, n$ 的所有数的异或,即 $f(n)=f(n-1)\oplus n=1\oplus 2\oplus 3\oplus …\oplus n$,那么,任意的 $n$($n$ 为自然数),我们能够很快的计算出 $f(n)$ 的值
神奇么?我们可以先试探着写几个值:$f(0)=0$, $f(1)=1$, $f(2)=3$, $f(3)=0$, $f(4)=4$, $f(5)=1$, $f(6)=7$, $f(7)=0\ldots$ 发现 0 重复出现,可能有周期性,猜想答案为:
$$
\begin{cases}
f(4n)=4n \\
f(4n+1)=1 \\
f(4n+2)=4n+3 \\
f(4n+3)=0
\end{cases}
$$
这个答案可由数学归纳法得出,粗略的证明如下:
当 $n = 0$ 时显然成立
当 $n = k$ 时假设成立
当 $n = k+1$ 时:$f(4(k+1))=f(4k+3)\oplus 4(k+1)=0\oplus 4(k+1)=4(k+1)$
$f(4(k+1)+1)=f(4(k+1))\oplus (4(k+1)+1)=4(k+1)\oplus (4(k+1)+1)=1$(因为 $4(k+1)$ 的二进制末位一定是 $0$,所以 $4(k+1)$ 与 $4(k+1)$ 只有末位不同)
$f(4(k+1)+2)=f(4(k+1)+1)\oplus (4(k+1)+2)=1\oplus (4(k+1)+2)=4(k+1)+3$(因为 $4(k+1)+2$ 的二进制末两位一定是 $10$,所以 $1$ 与 $4(k+1)+2$ 异或后末两位为 $11$)
$f(4(k+1)+3)=f(4(k+1)+2)\oplus (4(k+1)+3)=4(k+1)+3\oplus (4(k+1)+3)=0$
参考
文档信息]]>
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
本文链接:www.snovey.com/2016/08/xor.html
~
,|
,^
,|
对其进行表示。<<
与 >>
分别表示左移(SHL)与右移(SHR)。简单介绍一下各种位操作:
AND 运算
参加运算的两个数据,按二进制位进行「与」运算。
运算规则:0|0=0
, 0|1=0
, 1|0=0
, 1|1=1
用途:对二进制位进行清零与读取值,例如 x|1 取末位判断奇偶。
OR 运算
参加运算的两个数据,按二进制位进行「或」运算。
运算规则:0|0=0
, 0|1=1
, 1|0=1
, 1|1=1
用途:对二进制位进行赋值为1。
XOR(/ˌɛksˈɔːr/) 运算
参加运算的两个数据,按二进制位进行「异或」运算。
运算规则:0^0=0
, 0^1=1
, 1^0=1
, 1^1=0
用途:异或运算非常神奇,用途在下一篇中讲。
NOT 运算
参加运算的一个数据,按二进制位进行「取反」运算。
运算规则:~1=0
, ~0=1
SHL 运算
将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。
SHR 运算
将一个运算对象的各二进制位全部右移若干位(左边的二进制位补位要视环境而定。
在位操作时要注意符号位,右移操作在 C/C++ 是与编译器相关的,不过几乎所有的编译器都使用算术右移。而在 Java, Javascript 中,所有的数都是有符号的,用 <<
与 >>
分别表示算术移位,用 >>>
表示逻辑移位。
举几个常见的位操作:
x|1
x|1-1
x^1
x|1
x=~x+1
or x=(x^-1)+1
在 C++ 中的 STL 中提供了 <bitset>
库,在 <bitset>
库中对位操作进行了重载,此外还提供了重载的 []
运算符以及count
,size
,set
,flip
等方法进行访问和操作,举个例子:
|
更多详细信息参考:bitset - C++ Reference
讲一则趣事感受一下位操作的强大吧:
|
据传当初做 3D 引擎时用这段代码计算 1/sqrt(x)
比调用库函数还要快,当然,效率提升之后精度会有一定的损失。
这段迷之代码我无法解释,讲个我能解释的吧:
|
这段代码可以返回一个 32 位整数的绝对值。当 x
为正数时,y
等于 0
,返回 x
本身;当 x
为负数时,y
等于 -1
,x^y=~x
,~x-(-1)=-x
,返回 x
的相反数,这段代码没有分支结构,是不是很神奇呢?
位运算在 ACM 中的一个重要应用是状态压缩,顾名思义,举个例子,做n皇后问题是通常用三个一维数组记录已经放置的皇后占据了哪些列、主对角线和副对角线。进而判断当前尝试的皇后所在的列和两个对角线是否已有其他皇后。如果将这三个一维数组换位三个整数同样可以解 n 皇后问题,而且效率更高,给出代码如下:
|
再比如可以用二进制表示集合的子集,每一位代表一个元素,通过判断该位的值进而判断该元素是否在子集中:
|
不仅如此,它还可以进行集合间的操作,数与数之间的 AND 和 OR 操作分别对应集合间的 ∪ 和 ∩
来看一下位运算的经典面试题:
有 1000 个一模一样的瓶子,其中有 999 瓶是普通的水,有一瓶是毒药。任何喝下毒药的生物都会在一星期之后死亡。现在,你只有 10 小白鼠和一星期的时间,如何检验出哪个瓶子里有毒药?
如果你有两个星期的时间(换句话说你可以做两轮实验),为了从 1000 个瓶子中找出毒药,你最少需要几只老鼠?注意,在第一轮实验中死掉的老鼠,就无法继续参与第二次实验了。
答案是:
第一只老鼠喝第 XXXXXXXXX1 瓶水,为 1、3、5、7..999 一共 500 瓶
第二只老鼠喝第 XXXXXXXX1X 瓶水,为 2、3、6、7…
…
第十只老鼠喝第 1XXXXXXXXX 瓶水,为 512、513…. 到最后的水
最后第几只死了就把那位记成 1,得到的 10 位二进制数就是那瓶水
参考
文档信息]]>
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
本文链接:www.snovey.com/2016/08/bitwise-operation.html
计算机只能存储二进制的数据,所以字符也一样只能通过将字符映射为相应的二进制形式才能保存,读取的时候由系统对字符进行图形渲染。
不同的映射方式导致了不同的 字符集 [1](character set),譬如说,「雪」字在 GBK 编码中对应的是「D1A9」,在 Unicode 编码中对应的是「96EA」。然而字符集只是规定了字符与二进制之间的映射,并没有规定具体如何实现,这个责任由 字符编码 (Character Encoding)承担,字符集与字符编码可能不同。
最初的时候,美帝只考虑了自己需要的英文,26 个大小写字母加数字符号控制字符乱七八糟一通搞,正好 128 个字符,一个字节表达完毕,这就是 ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)。后来计算机传到欧洲,字符不够用,为了对剩下的 128 个字符进行了利用并制定标准,ISO 推出 ISO 8859,显然 256 个字符也是不够这些国家用的,所以 8859 被分成了十几个部分,它覆盖了大部分使用拉丁字母的语言文字。
在 ISO 标准完全定型之前,IBM 就有一系列自己的字符编码,叫做代码页(code page),比如 437(扩展 ASCII)、850(西欧语言)、852(东欧语言)。IBM代码页通常被用于控制台(console)环境,也就是 MS-DOS 或 Unix Shell 那样的命令行环境。
微软将 IBM 代码页称为 OEM 代码页,自己定义的称为 ANSI 代码页, 比如 1252(西欧语言)、1250(东欧语言)、936(GBK 简体中文)、950(Big5 繁体中文)、932(SJIS 日文)、949(EUC-KR 韩文)等。
当计算机来到中国,这个问题的难度升级了,一个字节无论如何也表达不了博大精深的汉字,于是人们拿两个字节解决了这个问题,也就是 GB 2312 字符集,它是一个 94 * 94 的表,包括 7445 个字符。GB 2312 兼容 ASCII,GB 2312 编码对汉字/符号进行了分区(区位码)。每个汉字 / 符号以两个字节来表示。第一个字节称为「高位字节」,第二个字节称为「低位字节」。GB 2312 还有另一种编码方式 HZ,只是不常用。
GB 2312 的出现,基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆 99.75% 的使用频率。但对于人名、古汉语等方面出现的罕用字和繁体字,GB 2312不能处理,譬如说,某位领导人的名字写不出来,于是人们对 GB 2312 进行了扩展,这就是 GBK,K为汉语拼音 Kuo Zhan(扩展)中「扩」字的声母。GBK 全称是汉字内码扩展规范(Chinese Internal Code Extension Specification)。GBK 有一字节和双字节编码,这里有个事情就是计算机如何知道当前的字节是独立的字符还是跟相邻的字符共同表示一个字符,GBK 是通过第一个字符的范围来辨别的,00
–7F
范围内是一个字节,81
–FE
范围内是两个字节,GBK 向下完全兼容 GB 2312,GBK 与 CP936 大体相同,比它多 95 个字符。然而 GBK 但是毕竟只是规范,不是标准,随后国家推出 GBK 18030 以取代 GBK,它完全兼容 GB 2312,基本兼容 GBK,采用多字节编码,每个字可以由 1 个、2 个或 4 个字节组成。
为了使混乱的编码格局得到统一,ISO 于 1990 年推出了通用字符集(Unicode Character Set,UCS),它包含了一百多万个字符,UCS 有两种编码方式:UCS-2 和 UCS-4,分别用两个字节和四个字节表示一个字符,UCS-2 只能表示 65536 个字符,明显不够用,已经过时了。UCS-4 根据最高位为0的最高字节分成27=128 个 group。每个 group 再根据次高字节分为 256 个 plane。每个 plane 根据第 3 个字节分为 256 行(rows),每行包含256个 cells。当然同一行的 cells 只是最后一个字节不同,其余都相同。group 0 的 plane 0 就是 BMP。
ISO之外还有另外一个组织:统一码联盟(The Unicode Consortium),它于1991年推出了 Unicode 1.0。后来与 ISO 组织合并成果。
Unicode 全称 Universal Multiple-Octet Coded Character Set。Unicode 的码空间从 U+0000 到 U+10FFFF, Unicode 的码空间可以划分为 17 个平面(plane),每个平面包含 216(65,536) 个码位。每个平面的码位可表示为从 U+xx0000 到 U+xxFFFF,其中 xx 表示十六进制值从 00H 到 10H,共计 17 个平面。如果 xx 是 0,即第 0 平面,可省略不写,第一个 Unicode 平面(码位从 U+0000 至 U+FFFF)包含了最常用的字符,该平面被称为基本多语言平面(Basic Multilingual Plane),缩写为 BMP。其他平面称为辅助平面(Supplementary Planes)。
这里字符集跟字符编码的区别就出现了,Unicode 只是指定了字符的映射,并没有指定实现方式,出现了 UTF-8、UTF-16 和 UTF-32 三种编码方式。
UTF-16 可以看作是 UCS-2 的父集(严格的说这不正确,在 UTF-16 中从 U+D800 到 U+DFFF 的码位不对应于任何字符,而在使用 UCS-2 的时代,U+D800 到 U+DFFF 内的值被占用),为什么说它是父集呢,因为当字符不在 BMP 时 UTF-16 会使用四个字节来编码,所以要注意,UTF-16 是变长编码,四个字节的表示算法比较啰嗦,我不写了。
UTF-16 的坑在于字节序(Endianness)的问题,就是存储和传输的时候哪个在高地址哪个在低地址,举个例子:「雪」的大端序(big-endian,也叫大尾序)为 U+D1A9,小端序(little-endian,也叫小尾序)为 U+A9D1。一般来说,以 Macintosh 制作或储存的文字使用大端序格式,以 Microsoft 或 Linux 制作或储存存的文字使用小端序格式,网络传输一般采用大端序。
为此,出现了三种解决方案,也就是UTF-16LE,UTF-16BE, UTF-16。UTF-16 是大端序还是小端序取决于在文件头是否有 BOM,如果没有就听天由命了,如果有的话,就说说 BOM,BOM(byte-order mark) 是字节顺序标记,它不仅仅存在于 UTF-16 中,只要编码方式受字节序影响,就需要 BOM,在 UTF-8、UTF-32、GB 18030 中也有它的身影,不过在不同的编码方式中表示也不一样。在 UTF-16 中用 U+FEFF 表示大端序,用 U+FFFE 表示小端序。
UTF-32 是 UCS-4 的子集,它对所有的字符都采用四字节表示,强迫症表示很开心。当然,它也逃不了与 UTF-16 类似的遭遇,它也分 UTF-32LE、UTF-32BE、UTF-32。
重头戏来了,如果一个文件 99% 都是英文,采用上述的 Unicode 编码方案太浪费硬盘了,如果是一个网站,流量也要翻倍啊,UTF-8 采用可变长编码,向下兼容 ASCII 编码,良好的解决了这类问题。
码点的位数 | 码点起值 | 码点终值 | 字节序列 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | Byte 5 | Byte 6 |
---|---|---|---|---|---|---|---|---|---|
7 | U+0000 | U+007F | 1 | 0xxxxxxx | |||||
11 | U+0080 | U+07FF | 2 | 110xxxxx | 10xxxxxx | ||||
16 | U+0800 | U+FFFF | 3 | 1110xxxx | 10xxxxxx | 10xxxxxx | |||
21 | U+10000 | U+1FFFFF | 4 | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | ||
26 | U+200000 | U+3FFFFFF | 5 | 111110xx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | |
31 | U+4000000 | U+7FFFFFFF | 6 | 1111110x | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
Unicode 允许在 UTF-8 中使用 BOM,不过 UTF-8 的编码方式是字节序无关的,没必要使用 BOM。
打开记事本,文件->另存为,下方编码方式选项有四个
ANSI 看着好像 ASCII,但事实上,它的编码方式是系统默认的编码方式,对于一个 ANSI 文本,英文部分使用的就是 ASCII 编码,而中文部分使用的就是 GB 2312 编码,如果是繁体则会使用 BIG 5 编码。而第二个 Unicode 其实是带有 BOM 的小端序 UTF-16,最后那个 UTF-8 也是带 BOM 的。
注1:这是不严谨的说法,字符编码的层次不是简单的字符集与编码的映射,而是有五层模型。
参考:
文档信息]]>
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
本文链接:www.snovey.com/2016/06/char-encoding.html
CR
(carriage return, 回车)。继续用力,换行手柄大约会被扳动 30 度左右,纸会被上卷一行(行高,line height),这就是 LF
(line feed, 换行)。如果只换行不回车,那么第一行敲满以后,敲针始终在纸张右侧,无法继续输入;只回车不换行,所有的内容都敲到同一行里了。
可以看下这两个视频:Olympia SM9 Typewriter Demo 和 How to Use a Typewriter
在最早的 ASCII 标准(1963-1968)中,有两套标准,一套是 ISO 出的认为 CRLF
和 LF
都是标准的;然而另一套是 ASA 出的只认为 CRLF
是符合标准的。
所以 MS-DOS(1981)设计的时候是采用了在两种方法中都符合标准的 CRLF
,一方面是满足了两个标准,另一方面是兼容了当时大量采用 CRLF
的计算机。而 Unix 的前身 Multics 的设计者认为在每行的结尾加两个字符用于换行,实在是极大的浪费(那时的存储设备非常昂贵)。所以 Multics 里面用一个驱动程序自动将 LF
转换成 CR-LF
,所以他们用了单一的 LF
。
很明显,CRLF
才是正统,*nix 是异端。关于 CR
与 LF
的表示如下:
CR(carriage return, 回车)用 \r (return)表示,对应的 ASCII 码为 0x0D
LF(line feed, 换行)用 \n (newline)表示,对应的 ASCII 码为 0x0A
所以现在各操作系统对换行的表示方式如下:
LF:在 Unix 或 Unix 相容系统(GNU/Linux,AIX,Xenix,Mac OS X,…)、BeOS、Amiga、RISC OS
CR+LF:MS-DOS、微软视窗操作系统(Microsoft Windows)、大部分非 Unix 的系统
CR:Apple II 家族,Mac OS 至版本 9
假设有这样一段文本:
|
如果这段文本是在 Windows 下编辑的,当它移植到 Linux 下,用 vim 打开会显示:
|
如果你的 vim 正常显示(可能是vim的版本比较高),可以 cat -A
一下,会显示:
|
这里的 ^M
不是 ^
符号加 M
,而是一个组合字符,代表 CR
。
目前大多数文本编辑器都能够进行不同的换行符之间的转换(Windows 系统的记事本不行……)。如果是在 linux 下可使用如下命令进行转换:
|
当然,你的系统可能没有这个命令,那么你可以用 vi 或者 vim 打开,然后在命令模式下输入:
|
如果你的 vi 或者 vim 正常显示了,那么上面这个办法也是行不通的,这时可以使用 sed,命令如下:
|
或者使用 tr 命令:
|
如果是在 Mac OS 9 或者 Linux 系统下编辑的,当它移植到 Windows 下,用记事本打开会显示:
|
但是不妨写这样一段C语言程序:
|
在控制台下输出为:
|
即便是将输出定向到文件用记事本打开,显示依然如上所示。这就有些坑了,再来这样一段程序:
|
输入回车,输出 10,说好的 Windows 下按一下 Enter 键输入两个字符,为什么只剩下一个了呢?再比如这样一段程序:
|
输出都是一样的换行。编译器做了什么,Windows 系统做了什么,我无从知晓,既然已经理不清其中的原因,那最好的办法就是避开这个陷阱。如果你对此事的兴趣非常强烈,不妨看下这个:在 Windows 下键入 Enter 键,是在键盘缓冲区中存入 ‘\n’ 还是 ‘\r’’\n’ 两个?
参考
文档信息]]>
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
本文链接:www.snovey.com/2016/06/endofline.html
拓扑排序的算法思想是:
|
文档信息]]>
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
本文链接:www.snovey.com/2016/06/toposort.html