谈谈数据结构

来源:互联网 发布:内涵吧源码 编辑:程序博客网 时间:2024/06/04 18:34

https://mp.weixin.qq.com/s/w8P3ZlJLNT_m84KpicHiIA?utm_source=tuicool&utm_medium=referral

沃斯大神说过,程序 = 算法 + 数据结构。

程序君认为,等式的右边,数据结构的权重要大于算法。数据结构定义好,基本上,你所用的算法也就确定了,算法的时间复杂度也八九不离十。上周,我在 team 内部分享了一个关于数据结构的主题,在这里,也和诸位分享。

现代的编程语言,内置的数据结构越来越多,从 primitive 的类型:integer, float, boolean, string,一路到 complex 结构,如:array,list,map,set 等。这些结构即插即用,非常顺手,可是有时我们也需要了解其内部的实现(内部的数据结构),从而更好地打造高性能的应用。有些「聪明」的语言,如 clojure,会根据使用场景动态地切换内部的结构,比如说 map,当 key 的数量超过某个阈值,便会自动换成更高效的结构来应对需求。

今天,我们就来扒一扒,编程语言中的常见数据类型,其内在究竟是怎么一回事。

array / tuple

array 或者 tuple,是大家最熟知的容器型的数据类型,其内部可以承载其他数据结构。他们的特点是容器里的元素被分配在连续的内存中,可以按照 index 随机访问,时间复杂度是 O(1)。array 一般要求内部是同构的数据结构,而 tuple 则允许异构。

对函数式编程语言,或者提供 immutable data structure 的语言,高效地处理 array 是个挑战。如果代码中修改了 array 中的某个元素,拷贝并重新生成整个 array 的代价太大。我们需要一种合适的数据,能够在结构上共享大部分不受影响的数据。这种数据结构,便是大部分函数式编程语言使用的 persistent data structure。persistent data structure 用 DAG(一般是一个 Trie)来描述数据,当 DAG 某部分发生改变,只需要重新生成和改变相关的路径上的节点即可,非常高效。对于 array,我们可以使用 Bit Array Mapped Trie / Index Vector Trie:


通过把 index 的二进制切分并进行 trie 的遍历,对应的叶子节点就是要找的节点。如果我们要修改这个节点的内容,在原有结构保持不变的情况下,我们只需要生成一个新的 node,并生成这条路径上一直到根节点的相关中间节点即可。需要额外拷贝的内容仅仅和 trie 的深度有关。


List

大多数编程语言,提及 list,其内部一般是个带尾指针的双向链表。因而,在头部或者尾部添加新的节点非常高效,是 O(1) 的操作,然而查找,插入,删除则需要遍历链表,是 O(n) 的操作。由于是链表,获取链表的长度一般而言也需要遍历,是个 O(n) 的操作,有些编程语言会为其优化,在链表的头部维护链表的长度,这样获取长度的效率则为 O(1)。请注意弄清楚你用的语言,这个操作的效率如何。对于 erlang,length 函数是 O(n) 的复杂度,因而对大列表的length 操作不要滥用。

Map

map,在很多语言里,被称为 associate array (php),dict(python)等。他是一个 key/value pair 的容器。理论上可以容纳任意数量的 key/value pair。在 map 中,key 是唯一的,key 和 value 一般都可以是任意数据结构。当 key 只能是 string 时,有些语言会使用标准的 trie,或者 patricia trie 来处理。

早期的 map 用 hash array + collision avoidance 实现,典型的 collision avoidance 有 chaining 和 probing。一个 key 经过某个哈希函数后,对应 hash array 中的一个位置,如果这个位置已经有数据,要么,把数据 用 linklist chain 起来,这是 chaining;要么,在 array 中随后的某个固定位置找到(probe)一个空闲的 slot,写入,这是 probling。

chaining 的问题是如果 key 的数量大大超过 hash array 的长度,冲突会很严重,从而大大拉低性能。最坏情况下,时间复杂度是 O(n/k);probing 的问题是 hash table 必须和 key 空间一样大,如果 hash 函数选得不好,容易造成大面积的冲突。对于支持动态增减 key 的 map 来说,常常需要扩大 hash table,然后 rehash,内存和计算的代价都不小。python 的 dict 便是使用 hash array + collision avoidance 实现的。

C++ 及很多其他语言使用红黑树来支持 map。hash array 和红黑树的比较:

如今大部分 FP 语言,其 map 都使用了类似上文提到的 persistent data structure:HAMT (Hash Array Mapped Trie,作者:Phil Bagwell):

它将 key 通过哈希函数处理后,成为一个数字,然后从这个 hash value 的低位起,一路向下搜索,直到找到叶子节点,如果叶子节点上的数据和 key 匹配,则查找到;否则,没找到。HAMT 使用 32bit / 64bit 的 hash value(上述仅仅是示意),所以 hash collision 的几率很小,如果冲突发生,叶子节点会转换为 index node,然后用不同的 hash 方法计算原有的叶子节点和新的节点,并将其挂载到 index node 下。

Tree

Tree 的种类很多,这里讲讲常用的 AVL, Trie 和近期比较热门的 Merkle tree。

AVL 是一种比较简单的 tree,左右两棵子树层级不超过 1。如果新加入的节点打破了这个平衡,则需要翻转(可能多次)使其达到新的平衡。

Trie 是一种前缀树,可以用来做路由查询。erlang 的 pattern matching 机制,就是将使用 pattern matching 的代码转化成类似 Trie 的代码结构。

Merkle tree 脱胎于加密算法,可以验证 tree 的任何部分的正确性,被广泛用于文件系统(ZFS),文件分块下载(BT),block chain 等。

在 Merkle tree 里,每个数据单元生成一个 hash,相邻的 hash 凑在一起再生成 hash,这样两两结合,一直到根(如果数据单元的总数是奇数,那么最后一个的 hash 和自己结合)。

举个文件同步校验的例子。假设本地磁盘和网盘有 128 个文件(2^7),各自生成一个 Merkle tree,我们只需要从根节点一路比较下来,O(logN) 的效率,就可以找到有差异的文件。

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 苏宁的发票丢了怎么办 京东退货没有发票怎么办 发票发错了邮箱怎么办 苹果手机忘记电子邮箱验证码怎么办 退差价把红包退还了怎么办 网上购票票丢了怎么办 岗位人手不够老板又不招人来怎么办 辞职后提成不发怎么办 老板给客户吵架员工该怎么办 冰柜声音大怎么办嗡嗡响 交了钱电没有怎么办 小白熊电动吸奶器显示F1怎么办 花洒的水变小了怎么办 手机插卡处坏了怎么办 吉利帝豪一键启动钥匙没电怎么办 居民医保断交5年怎么办 社保和医保断了怎么办 停缴了两年社保怎么办 医保交不够20年怎么办 医保断交4个月怎么办 医保断交5个月怎么办 换城市后社保卡怎么办 苹果6s呼叫失败怎么办 苹果手机打电话显示呼叫失败怎么办 苹果手机经常呼叫失败怎么办 苹果手机呼叫号码呼叫失败怎么办 好友把我拉黑了怎么办 有人一直打骚扰电话怎么办 每天都有骚扰电话怎么办 苹果7被电话轰炸怎么办 苹果手机接听声音小怎么办 微信账号封了怎么办 在京东买的手机出现问题怎么办 销售没胆量扫楼怎么办 骨龄比实际年龄小怎么办 身体年龄比实际年龄大怎么办 吃了不熟的鸡蛋怎么办 六角龙尾巴烂了怎么办 兰花长出来的包怎么办 长寿花花开败了怎么办 手机分期0首付怎么办办