注:本篇文章的内容纯属学术性质,代码相较于常规可能会更慢,更占用空间,请读者以读懂为主;虽然在本文平台上(python)是慢的,但它们却是在函数式语言中快的存在. "cons list" 就是所谓的列表处理器 (list processor) 所处理的列表。

Scheme 曾经是我最喜爱的语言,其中的列表和其它语言的数组有很大的不同:首先,它是递归嵌套的,其次,每一个列表的“长度”都是 2,再之,每一个列表都必须有一个“空列表”。起初听着会让人感觉比较奇怪,尤其是习惯了常规的过程式思想的玩家。

为了不让大家学习 Lisp,我在 Python 上稍微翻译了一下具有函数思维的代码,来让读者在不了解 lisp 的情况下了解所谓的 cons 是什么东西。 当然本人能力有限,代码肯定也有能够改进的地方,过于核心的东西也讲解不了(<del>本人也不会</del>)。

lisp list 首先,长这样:

'()nullempty:这是一个空列表,长度为 0,什么也没有。

(cons 1 null): 这个列表有两个元素,1 和另一个空列表。一般来说后面的这个空列表不算数,所以长度是 1. 前面的 cons 是前缀式代表着列表的开始,可以理解成 cons(1, null)

(cons 1 (cons 2 (cons 3 (cons 4 null)))):长度是4. 由此可见列表是嵌套组成的。在一对括号中,第一个就是我们要存的变量,第二项则必须是另一个列表。所以一个列表里的最后一项必须是空列表。如果要更改其中的 1,则需要把带有 1 的这项剥开扔掉,换成新的:

    (      cons 1 (cons 2 (cons 3 (cons 4 null))))
--->              (cons 2 (cons 3 (cons 4 null)))
--->(cons "hello" (cons 2 (cons 3 (cons 4 null))))

所以这其实是产生了一个新列表。

(cons 1 . 2):这货无视就好了。

随便定义一些前缀式函数

from typing import Union, Callable
Number = Union[int, float]def add(a: Number, b: Number) -> Number:return a + b
def sub(a: Number, b: Number) -> Number:return a - b
def mul(a: Number, b: Number) -> Number:return a * b
def div(a: Number, b: Number) -> Number:return a / b
def string_append(a: str, b:str) -> str:return a + badd(1,2) # ==> 3

这样看着更像是 scheme。

定义 cons

以下的函数会都在类里定义,这叫取其精华,要不然括号有点太多。

class cons(tuple):def __new__(cls, *arg):if len(arg) != 0:if len(arg) != 2: # 如果列表非空,必须有两项raise ValueError('arity mismatch')if not isinstance(arg[1], cls): # 第二项必须也是一个列表raise ValueError('second argument must be a list')self = super().__new__(cls, arg)return self

可以看见我是继承的元组。其实像链表一样定义左右两边也是可以的。这里利用了元组不能改变的特性,所以这强迫我们如果要修改列表,必须要生成一个新的。其中 *arg 是和普通元组不一样的地方,一般的如果我们想要一个元组是 tuple([1,2]),解包的写法比较方便。 看一看效果:

# 空列表
> empty := cons()
()
> type(empty)
__main__.cons
# 长度为 2
> cons(1, cons(2, cons()))
(1, (2, ()))
# 长度为 4, 第一项又嵌套一个长度为 2 的列表
> cons(cons(1, cons(2, cons())), cons("Hi", cons(int, cons(lambda: print('hello world'), cons()))))
((1, (2, ())), ('Hi', (int, (<function __main__.<lambda>()>, ()))))

一般来说,一对括号里的第一项称之为 first,第二项称之为 rest (剩余的列表部分)。

    def first(self): return self[0]def rest(self): return self[1]car = first # first 的别名cdr = rest # rest 的别名def isnull(self) -> bool: # 判断一个列表是否是空的return len(self) == 0 

对于 Python 来说, _getitem_ 最多索引到 1.

看看列表是怎么递归的

首先是按照序列来取内容。

    def ref(self, index: int):if index == 1:return self.first()else:return self.rest().ref(index-1)

rest 会返回列表的剩余部分,这部分是一个新的列表。这代码的意思是一直剥离掉当前列表中最外面的那一层,当剥的数量足够了,剩下列表的第一项就是想要的内容。

> x = cons(cons(1, cons(2, cons())), cons("Hi", cons(int, cons(lambda: print('hello world'), cons()))))
> x.ref(3)
int

其次是取得列表的最后一项。当一个列表只有一个元素和一个空列表的时候,即第二项是空列表,他的第一项就是最后一项,这就是常说的 base case。在取得最后一个之前,一直剥离外层的括号就OK了。

    def last(self):def last1(lst: cons):if lst.rest().isnull():return lst.first()else:return last1(lst.rest())return last1(self)

这里面放了一个本地函数,不放的话其实和上面的函数一模一样。(练习:把这个函数写成5行)我比较喜欢这样,免得让人老以为他在更改 "self"。

> x.last()
<function __main__.<lambda>()>

对于列表的递归操作一般就是这样,套路感十分明显。再来一个获取列表长度的例子

    def length(self) -> int:def length1(lst: cons, n: int) -> int:if lst.isnull():return nelse:return length1(lst.rest(), n+1)return length1(self, 0)

> x.length()
4
> x.car().length()
2

下面的例子返回一个反转之后的列表:

    def reverse(self):def reverse1(lst: cons, reversedd: cons):if lst.isnull():return reverseddelse:return reverse1(lst.rest(), cons(lst.first(), reversedd))return reverse1(self, cons())

首先,把原来的列表放在左边,右边放一个空表。把左边列表最外层和右边列表结合形成一个新列表,这样原来的最外项就成了右边的最内项。最后当左边取完了的时候,返回右边的。

> null = cons()
> cons(1, cons(2, cons(3, null))).reverse()
(3, (2, (1, ())))

还有许多的诸如替换,增加,复制等操作大多都是 if list is null then ... else do ... 套路的,很好学习。

最后来一个打印整个列表的函数:

    def quasi_repr(self) -> str:if self.isnull():return ''()'def reprB(item) -> str:if isinstance(item, cons):if item.isnull():return '()'else:return string_append('(', repr1(item))else:return str(item)def repr1(lst: cons) -> str:if lst.rest().isnull():return string_append(reprB(lst.first()), ')')else:return string_append(reprB(lst.first()), string_append(', ', repr1(lst.rest()) ))return string_append(''(', repr1(self))

是的,你没看错,这是两个递归函数在相互调用!再有嵌套列表的时候,要考虑两层,一方面是当前对象是列表中第一项的时候,一方面是当前对象又是一个列表的时候。在函数式和列表里,这是常规操作。在其他语言中,需要用到这种思维的地方一般由一个循环和一个递归来实现,比如获取文件夹的大小。

> print(cons(1, cons(2, null)) )
`(1, 2)
> print(cons(cons(1, cons(2, cons())), cons("Hi", cons(int, cons(lambda: print('hello world'), null)))).quasi_repr() )
`((1, 2), Hi, <class 'int'>, <function <lambda> at 0x000002279DCE5488>)

在这里,在开头的括号前面加一个分号(嵌套的没有)是通常表示 cons list 的方法。与 cons 写法不一样,分号写法不包括最后的空列表。(如果一个空列表以成员的身份出现,则显示。比如cons(null, null)的分号记法是 '( () )

cons list 和普通列表的互换

虽然意义不大,但这是需要的。因为 Py 不是 Lisp 语言,在非常需要普通列表的地方 cons 一个第三者是办不到的。

转换到普通列表(的方法):

    def to_vector(self, store: list) -> list:def to_vector1(lst: cons):if not lst.isnull():store.append(lst.first())return to_vector1(lst.rest())return to_vector1(self)

需要一个空列表来储存内容,因为不是主要内容,不过多解释

普通列表转换到 cons list(的函数):

def vector_to_cons(vec: list) -> cons:length: int = len(vec)def vector_to_cons1(count: int) -> cons:if count == length:return cons()else:return cons(vec[count], vector_to_cons1(count+1))return vector_to_cons1(0)def lispList(*args) -> cons:return vector_to_cons(args)

通过这第三个函数就可以像这样来创建列表了:

  # 相当于 cons(1, cons(2, cons(cons(2, cons(4, cons(null, null))), null)))
> y = lispList(1,2,lispList(2,4,lispList()))
> y.quasi_repr()
`(1, 2, (2, 4, ()))
> y.rest().quasi_repr()
`(2, (2, 4, ()))

把这样的列表作为参数传递给函数

也就是 apply 函数。在 Python 中,可以这样实现

    def apply_to_sup(self, f: Callable):temp = list()self.to_vector(temp)return f(*temp) 

但如果我们不想展开这个列表成普通的列表(一般情况下也不能),那就要了解一下函数了。

其实函数也和 cons list 差不多。首先是组合函数:

def compose(f: Callable, g: Callable) -> Callable:def fg(*args):return f(g(*args))return fg

先算出 g的结果,再自动当作 f 的函数。实际上在这个表达式里,嵌套的括号就已经有点列表的意思了,但还不够。参数列表我们在这里假装它是一个 cons list。

函数的居里化:

这样对于 f,我可以暂时先生成一个不依赖于 x 的函数,等到知道 x 的时候,在把它传送到 f'(y) 中。这样的特点是本来需要一次性完成的工作,现在可以分好几步。最后一个表达式的意思是,我生成了一个两个参数都固定的函数,只要这个函数一执行,他的结果是固定的。

下面这个函数可以把传入函数的第一个参数挂起(传入参数的最小参数个数是 2*,这样居里化之后的函数参数个数是 1):

def curry(f: Callable) -> Callable:'requires the argument count for f is at least 2'def g(arg2, *args_rest):def h(arg1):return f(arg1, arg2, *args_rest)return hreturn g

> def f(x, y, z): return x - y * z
> f(1,10, 2)
-19
> curry(f)(10, 2)(1)
-19
> curry(curry(f))(2)(10)(1)
-19# 生成一个加一函数
> add1 = curry(add)(1)
> add1(3)
4

可以注意到,直接传参数的顺序是 1,10,2,而在完全把参数剥离开之后,顺序颠倒了过来。这是因为在 curry 的时候,只能从最左边的变量开始剥。如果观察这函数的参数的话,会发现它和 cons list 的原理是一样的,我们把一个函数从复杂的形态(要传一大堆参数)扒皮成最原始的形态(单变量函数),相当于把一个很长的列表进行索引,到最后只剩下了那个空列表,此空列表就是这种直接就能运行的函数。

这样的话,如果想要给一个多变量函数传参数的话,就先把它的未知量这层皮剥掉,把我们的 cons list 作为皮换上去,这样未知量变成已知量,函数就能运行了。

这种换衣服的操作就是这样的:

    def apply_to(self, f: Callable):if self.isnull():return f()def reduce_function(f1: Callable, n: int) -> Callable:if n == 1:return f1else:return reduce_function(curry(f1), n-1)def fill_args(f1: Callable, arg_lst_r: cons):if arg_lst_r.rest().isnull():return f1(arg_lst_r.first())else:return fill_args(f1(arg_lst_r.first()), arg_lst_r.rest())return fill_args(reduce_function(f, self.length()), self.reverse())

如果列表是空的,即代表函数不接受参数,那么直接返回他的执行结果。否则的话就把他扒成单变量函数**,再用我的参数列表(此时应该是倒序的)一层一层地穿上。

> lispList(1,10,2).apply_to(f)
-19
> lispList(1,2).apply_to(add)
3

这就是实现多参数函数的原理。

对于 map 和 折叠等的操作,看缘分有空再发。

从一个列表简洁地转换到另一个列表

介绍函数的部分算是一个小插曲,以下的部分更重要一些。

  • Filter

筛选函数的工作原理是:对于每一对括号,如果第一项是符合条件的,那么我们就保留这一项,接着嵌套剩余的部分,如果不符合条件,就丢掉这一项再继续。比如在 cons(1, cons(2, cons(3, null)))

中,如果只要奇数,那么遇到 1 的时候保留,遇到 2 的时候跳过,此时的列表解析成 cons(1, cons(3, ...))。此函数相当简单:

def lispFilter(cond: Callable, lst: cons) -> cons:if lst.isnull():return lstelif cond(lst.first()):return cons(lst.first(), lispFilter(cond, lst.rest()))else:return lispFilter(cond, lst.rest()) 

最后的两个语句就是不一样的地方。如果是常规列表,可能是这样的:if cond(array[i]) keep array[i,i+1,...] else keep array[i+1,...] loop 。举个例子

> def isodd(x): return x % 2 == 1
> res = lispFilter(isodd, lispList(-1,0,1,2,3,4,5))
> print(res.quasi_repr())
`(-1, 1, 3, 5)
> res = lispFilter(cons.isnull, lispList(null, null, lispList(2,3), lispList(null), lispList()))
> print(res.quasi_repr())
`((), (), ()) # 思考下为什么是三个空列表?

  • Map

对一个列表进行一些映射是非常常规的操作。在 cons list 里实现还是用一个新的项去替换原来的最外层的。

def lispMap1(f: Callable, lst: cons) -> cons:if lst.isnull():return cons()else:return cons(f(lst.first()),lispMap1(f, lst.rest()))

注意这两个函数的返回值都是列表。

> res = lispMap1(add1, lispList(1,2,3,4,5,-1))
> print(res.quasi_repr())
`(2, 3, 4, 5, 6, 0)

列表的折叠

  • 右折叠 (foldr)

如果函数是右结合的,那么其可以右折叠。一个表达式里最右面的和倒数第二个元素可以结合成一个新的元素,然后作为一个整体再和倒数第三个结合。怎么理解呢?想象一下如果我不写括号来写出一个列表:

cons 1 cons 2 cons 3 cons 4 cons 5 null

根据规定,一对括号如果非空里必须有两项,所以只能让两个元素结合,其次,第二个必须是列表,这样我们只能把最右边的 5 和 空集结合成一个新项。

(cons 1 cons 2) cons 3 cons 4 cons 5 null # 不对
cons 1 cons 2 cons 3 cons 4 (cons 5 null) # 对
cons 1 cons 2 cons 3 (cons 4 (cons 5 null)) # 对

所以实际上,列表的构造就是一个右折叠的实例。当然,这个表达式没法再简化了,但是,有可以继续简化的:

add 1 add 2 add 3 add 4 add 5 0

用右折叠的思维去想(这种前缀式函数是右结合的典范——好像我见过的大多数数学物件都是右结合的),故这行字就翻译成

 (add 1 (add 2 (add 3 (add 4 (add 5 0)))))
或者是
add(1, add(2, add(3, add(4, add(5, 0)))))

先让5和0相加,结果再加4,结果再加3……

看这个表达式和

cons(1, cons(2, cons(3, cons(4, cons(5, null)))))

的相似度。因为彼此都是右结合的,因此可以类比:add 就是 cons,而 0 就是 null。在每一对括号里,第一项就是任意元素,而第二项就是所谓的 base case (终止条件)。右折叠故而可以这样实现:

def lispFoldr1(f: Callable, end, lst: cons):if lst.isnull():return endelse:return f(lst.first(), lispFoldr1(f, end, lst.rest()))

如果和文章开头的几个函数类比,会觉得很像。

  # 列表的求和和求积
> lispFoldr(add, 0, lispList(1,2,3,4,5))
15
> lispFoldr(mul, 1, lispList(1,2,3,4,5))
120

右折叠函数所传入的第一个参数要接受两个变量,第一个变量称之为当前项,第二个称之为递归结果。对于这个 add 函数,准确的说要这么写: lambda x, rr: x + rr。如果你看上面的类比例子,就可以看到 add(1, add(2, add(3, add(4, add(5, 0)))))中, 1 可以看作是当前项,而 add(2, add(3, add(4, add(5, 0)))) 就是递归结果。而 5 可以看成是当前项,那个 0 作为递归的中止项,也是递归结果。

这样说的话,以下的结果读者也都猜到了:

  #仅仅遍历了一次而没做改变
> unchanged = lispFoldr(cons, null, lispList(1,2,3,4))
> print(unchanged.quasi_repr())
`(1, 2, 3, 4)

更为让人震惊的是,map 函数可以用 foldr 来实现:

def mapp(f: Callable, lst: cons) -> cons:return lispFoldr1(lambda x, rr: cons(f(x), rr), cons(), lst) 

请问数组能做到吗?

  • 左折叠 (foldl)

这部分其实挺坑,因为不同的语言的实现和原理都不同,并没有统一的标准,故略过。

(实际上左折叠和 cons list 背道而驰,这可能也是为什么 racket 要重新发明一遍它)

  • 降价

当一个函数可以说是即能右折叠又可以左折叠的时候,而且结果不变,那么它就是具有交换性。这个时候它就可以自然地写成中缀式(比如 add(1,2)1 + 2),当然写成中缀式不一定就代表他就可交换了,不深究。这个时候左折叠和右折叠合并成一个,称之为 reduce

同时处理多个列表

比如,如果我们想比较两个列表是不是相等,那么它的代码会是这样:

def listsEqual1(lst1: cons, lst2: cons) -> bool:if lst1.isnull() and lst2.isnull():return Trueelif (lst1.isnull() and not lst2.isnull()) or (not lst1.isnull() and lst2.isnull()):return Falseelif lst1.first() == lst2.first():return True and listsEqual1(lst1.rest(), lst2.rest())else:return False

不难理解。如果两个列表都空,那么返回是,这就是 base case。如果其中一个空,另一个不空,那么他们肯定不一样,如果都不空的话,就再比较这两项。

> listsEqual1(lispList(), lispList())
True
> listsEqual1(lispList(1,2,3), lispList(1,2))
False
> listsEqual1(lispList(1,2,3), lispList(1,2,3))
True

这样的例子有很多,比如合并列表,以及点乘:

  • 多变量 Map 函数

真正的 map 可以同时处理多个列表,然后返回一个新的处理完的列表。

> v = lispList(1,2,-1)
> w = lispList(-2,0,7)
# 每一项都相乘,返回新的列表
> res = lispMap(mul, v, w)
> print(res.quasi_repr())
`(-2, 0, -7)
# 然后再加起来就是
> lispFoldr1(add, 0, res)
-9

我刚才写的盗版 map1 只能处理一个列表,下面的这些玩意就能支持多列表了:

def lispMap(f: Callable, *varlists: cons) -> cons:def map1(f1, alst: cons) -> cons:if alst.isnull():return cons()else:return cons(f1(alst.first()),map1(f1, alst.rest()))if len(varlists) == 1:return map1(f, varlists[0])def var_map(lists: cons):if lists.first().isnull():return cons()else:return cons(map1(cons.first, lists).apply_to(f),var_map(map1(cons.rest, lists)))return var_map(vector_to_cons(varlists)) # 只能传入 cons list,先忽略掉 py 本身数组的特性

仍然需要注意,在这里 cons.first 等不带括号的是方法函数的本身。这个大函数的原理是把传入的参数列表中 ,每个列表的第一项提出来给函数 f 当参数,然后返回组成新列表的第一项。

这个函数默认所有列表等长。

所以在数组的转置(map(list, zip(*matrix)))之外,cons list 的转置方法如下:

> matrix = lispList(lispList(1,2,3,4),lispList(33,4,-7,0),lispList(2,0,0,-9),lispList(0,0,0,0))
> print(matrix.quasi_repr())
`((1, 2, 3, 4), (33, 4, -7, 0), (2, 0, 0, -9), (0, 0, 0, 0))
> matrixT = cons(lispList, matrix).apply_to(lispMap)
> print(matrixT.quasi_repr())
`((1, 33, 2, 0), (2, 4, 0, 0), (3, -7, 0, 0), (4, 0, -9, 0))

  • 多变量 foldr

真正的右折叠能同时处理多个列表。

 # 给定一堆列表,把列表的所有值相加,列表长度要相同
> lispFoldr(lambda x,y,z,t,rr: x+y+z+t+rr, 0,lispList(1,3,5),lispList(4,4,-5),lispList(4,4,-5),lispList(4,4,-5))
18

函数的真正的定义也是比较群魔乱舞:

def lispFoldr(f: Callable, end, *varlists: cons):def mapadd(f1: Callable, lst: cons, last) -> cons:if lst.isnull():return cons(last, cons())else:return cons(f1(lst.first()), mapadd(f1, lst.rest(), last))def var_foldr(lists: cons):if lists.first().isnull():return endelse:return mapadd(cons.first, lists, var_foldr(lispMap(cons.rest, lists)) ).apply_to(f)return var_foldr(vector_to_cons(varlists))

另一个例子:

 # 一个列表叫变量名,一个列表叫变量值,我想让对应变量名的值加一
> def incr_args(argn, argv, this):return lispFoldr(lambda x, y, rr: cons(add1(x), rr) if y == this else cons(x, rr),null, argv, argn)
> res = incr_args(lispList('x','y','z'), lispList(1, 5, 2.3),'y')
> print(res.quasi_repr())
`(1, 6, 2.3)

如果看完了觉得还行,建议关注专栏。

---------------------------------------------------------------------

*,**:其实也可以把一个单变量函数脱成一个无变量函数:

def curry1(f: Callable) -> Callable:'requires the argument count for f is 1'def g():def h(arg1):return f(arg1)return hreturn g
curry1(curry(curry(f)))()(2)(10)(1) # ==> -19

只不过在目前用处不是很大。

lisp封装成vla函数_Lisp List 和函数式编程 (in Python)相关推荐

  1. lisp封装成vla函数_[良心教程]分享最新最实用的按键精灵封装函数

    金猪脚本(原飞猪脚本)以按键精灵教学为主,涉及UiBot,Python,Lua等脚本编程语言,教学包括全自动办公脚本,游戏辅助脚本,引流脚本,网页脚本,安卓脚本,IOS脚本,注册脚本,点赞脚本,阅读脚 ...

  2. 封装成vla函数_不知道怎么封装代码?看看这几种设计模式吧!

    为什么要封装代码? 我们经常听说:"写代码要有良好的封装,要高内聚,低耦合".那怎样才算良好的封装,我们为什么要封装呢?其实封装有这样几个好处: 封装好的代码,内部变量不会污染外部 ...

  3. 封装成vla函数_第四章:Python之函数

    第一节:函数入门与定义函数 理解函数 所谓函数,就是为一段实现特定功能的代码"取"个名字,以后即可通过该名字来执行(调用)这段代码 从逻辑上看,函数相当于一个黑匣子 定义函数的语法 ...

  4. 如何编写高质量的 JS 函数(3) --函数式编程[理论篇]

    本文首发于 vivo互联网技术 微信公众号  链接:https://mp.weixin.qq.com/s/EWSqZuujHIRyx8Eb2SSidQ 作者:杨昆 [编写高质量函数系列]中, < ...

  5. es6 filter函数的用法_Python 函数式编程指北,不只是面向对象哦!超级详细!

    Python 函数式编程指北, 不只是面向对象哦 了解在Python中如何使用 lambda, map, filter 和 reduce 函数来转换数据结构 Photo by Markus Spisk ...

  6. less 函数_Python中的函数式编程教程,学会用一行代码搞定所有内容

    前言 在本文中,您将了解什么是函数范型,以及如何在Python中使用函数式编程.在Python中,函数式编程中的map和filter可以做与列表相同的事情.这打破了Python的禅宗规则之一,因此函数 ...

  7. Python 函数式编程,Python中内置的高阶函数:map()、reduce()、filter()与sorted(),Python中返回函数

    函数式编程 是一种编程范式,比函数更高层次的抽象. 函数式编程将计算视为函数而非指令. 纯函数式编程:不需要变量,没有副作用,测试简单. 支持高阶函数,代码简洁. Python 支持的函数式编程 不是 ...

  8. python内置高阶函数求导_Python——函数式编程、高阶函数和内置函数,及

    Python--函数式编程.高阶函数及内置函数 函数式编程 一.不可变数据:不用变量保存状态不修改变量 二.第一类对象:函数即"变量" 1.函数名可以当做参数传递 2.返回值可以是 ...

  9. lisp封装为vlx方法_lisp 创建自定义菜单行数过多为什么不能打包成VLX

    大师们.LISP创建菜单行数过多后怎么不能打包. ; 编译终止 ; 错误: 编译器发现致命错误 "菜单工具.lsp" _$ ;;; 说明:用lisp创建菜单 ;;; 作者:lang ...

最新文章

  1. SSM整合Shiro 身份验证及密码加密简单实现
  2. spring连接jdbc_在Spring JDBC中添加C3PO连接池
  3. C++primer第八章 IO库 8.3string流
  4. kibana-7.15.2 一分钟下载、安装、部署 linux
  5. python找钱_python 递归 找零钱
  6. Telephone Linse(POJ-3662)
  7. python读json文件太大github_GitHub上最火的开源项目是啥|JSON文件实战处理
  8. sql 只要一个字段相同则只显示一条数据_数据库
  9. 行内元素多出的空白文本节点的解决方法
  10. 论如何优雅的处理回文串 - 回文自动机详解.
  11. Windows SharePoint Services Search和Office SharePoint Server Search的区别
  12. 驱动精灵w8ndows xp sp2,爱普生Epson TM-U220打印机驱动官方正式版下载,适用于winxp,winvista,win7,win8,win10-驱动精灵...
  13. Kali Linux 1.0 新手折腾笔记(2013.3.21更新)
  14. XS9932A/XS9932B 4 通道模拟复合视频解码芯片方案
  15. 520送什么蓝牙耳机好?高颜值高性价比的无线蓝牙耳机推荐
  16. 股指期货开户的保证金和手续费是多少?怎么计算?
  17. 《异常检测——从经典算法到深度学习》5 Opprentice——异常检测经典算法最终篇
  18. Android LruCache和DiskLruCache相结合打造图片加载框架(仿微信图片选择,照片墙)
  19. 浅谈马尔可夫决策过程(一)
  20. Python生成英文词云图

热门文章

  1. s5pv210 uboot-2012-10移植(三) 之支持SPL
  2. iptables使用ipt_connlimit限制连接数
  3. pip安装包时遇到的Bug
  4. Oracle查看用户权限
  5. Unity3D NGUI学习(一)血条
  6. C#——语言基础 之 运算符!
  7. linux磁盘虚拟化
  8. iOS开发UI篇—Date Picker和UITool Bar控件简单介绍
  9. 数学之美系列二 -- 谈谈中文分词
  10. linux 网络状态表 /proc/net/tcp 各项参数说明