数据结构 | 操作,最坏情形O | ||||
容器(container) | 静态(static) | 动态(dynamic) | 顺序(order) | ||
build(X) | find(k) | insert(x) delete(k) |
find_min() find_max() |
find_prev(k) find_next(k) |
|
数组 | n | n | n | n | n |
有序数组 | nlogn | logn | n | 1 | logn |
想要更快的查找以及动态操作。我们能让find(k)比 θ ( l o g n ) \theta(logn) θ(logn)更快?
在本模型中,假定算法仅可以通过比较进行区分
可比较项目:黑盒,仅支持两两比较
比较是:<、<=、>、>=、=、!=,输出是二进制:true或false
目标:存储一组n个可比较项目,支持find(k)操作
任何算法可被视作操作执行的决策树
一个内部节点代表一个二元比较,分叉到true或false
对于比较算法,决策树是二叉的
叶子代表算法终止,导致一个算法输出
根到叶子的路径代表算法对于输入的执行
每个算法的输出至少需要一个叶子,因此查询需要>=n+1个叶子
什么是比较查询算法最坏情形运行时间
运行时间>= 比较次数>=任意根到叶子的路径的最大长度>=树的高度
≥ \ge ≥n个节点的二叉树的最小高度是多少?
完整二叉树最小高度是多少(除了最后一行,其他行都是满的)
高度 ≥ ⌈ l g ( n + 1 ) ⌉ − 1 = Ω ( l o g n ) , \ge \lceil lg(n+1)\rceil - 1=\Omega(logn), ≥⌈lg(n+1)⌉−1=Ω(logn),因此任意比较排序的运行时间是 Ω ( l o g n ) \Omega(logn) Ω(logn)
有序数组实现这个边界
有 θ ( n ) \theta(n) θ(n)个叶子、最大分支因子b的树的高度为 Ω ( l o g b n ) \Omega(log_bn) Ω(logbn)
为了更快,需要一个操作:允许常量分支因子
利用Word-RAM O ( 1 ) \mathcal{O}(1) O(1)时间随机访问索引,线性分支因子
给项目独一无二的整数key:k,{0,…,k-1},在数组索引k处,存储项目
用一个数组索引关联一个目标
如果key适合机器字, u ≤ 2 w u\le2^w u≤2w,最坏情形的 O ( 1 ) \mathcal{O}(1) O(1)查找、动态操作
6.006:假设输入数字/字符串符合word,除非长度显示参数化
计算机内存中的任何东西都是二进制整数,要么是64位地址
但空间 O ( u ) \mathcal{O}(u) O(u),如果 n < < u n<n<<u,这也是很坏的
举例:如果key是10字母名称,所有可能的名字, 2 6 10 = 17.6 T B 26^{10}=17.6TB 2610=17.6TB空间
我们如何可以用更少的空间?
如果n< θ ( n ) \theta(n) θ(n),并使用更小的直接访问数组
哈希函数:h(k):{0,…,u-1}->{0,…,m-1}
直接映射数组称为哈希表,h(k)称为k的hash
如果m<
总是存在keys:a和b,h(a)=h(b)->冲突
不能存储两个项目在相同索引,那么存到哪?
要么存到数组其他地方(开放寻址)复杂的分析,常见且实用
存储到其他支持动态集合接口数据结构(链)
存储冲突到另外的数据结构(链表)
如果keys分发到索引处,链表尺寸为:n/m=n/ Ω ( n ) \Omega(n) Ω(n)= O ( 1 ) \mathcal{O}(1) O(1)
如果链表有 O ( 1 ) \mathcal{O}(1) O(1)尺寸,所有操作花费 O ( 1 ) \mathcal{O}(1) O(1)时间
如果不是如此,一些项目可能映射到相同位置,h(k)=constant,链尺寸是 θ ( n ) \theta(n) θ(n)
需要好的hash函数!那么什么是好的hash函数
除法(bad): h ( k ) = ( k m o d m ) h(k)=(k\ mod\ m) h(k)=(k mod m)
当keys均匀分布时很好
m要避免存储keys的相似性
远离2和10的幂的大素数是合理的
python使用它的某个版本,附带一些其他混合
若u>>n,每个hash函数将有一些输入集,它将创建一个 O ( n ) \mathcal{O}(n) O(n)尺寸的链
不使用固定的hash函数,随机地选择一个
universal(good,theoretically): h a b ( k ) = ( ( ( a k + b ) m o d p ) m o d m ) h_{ab}(k)=(((ak+b)mod\ p)mod\ m) hab(k)=(((ak+b)mod p)mod m)
Hash Family H ( p , m ) = { h a b ∣ a , b ∈ { 0 , . . . , p − 1 } , a ≠ 0 } H(p,m)=\{h_{ab}|a,b\in\{0,...,p-1\},a\neq0\} H(p,m)={hab∣a,b∈{0,...,p−1},a=0}
固定的素数p>u,a和b从{0,…,p-1}中选出
H是一个universal family: Pr h ∈ H { h ( k i ) = h ( k j ) } ≤ 1 / m ∀ k i ≠ k j ∈ { 0 , . . . , u − 1 } \Pr\limits_{h \in H}\{h(k_i)=h(k_j)\} \le 1/m\ \ \forall k_i\neq k_j \in\{0,...,u-1\} h∈HPr{h(ki)=h(kj)}≤1/m ∀ki=kj∈{0,...,u−1}
为什么universality是有用的?暗含了短链的长度!
X i j X_{ij} Xij标志随机变量 h ∈ H :如果 h ( k i ) = h ( k j ) , X i j = 1 ; 否则 X i j = 0 h\in H:如果h(k_i)=h(k_j),X_{ij}=1;否则X_{ij}=0 h∈H:如果h(ki)=h(kj),Xij=1;否则Xij=0
h ( k i ) h(k_i) h(ki)处链的长度是随机变量: X i = ∑ j X i j X_i=\sum_jX_{ij} Xi=∑jXij
链在 h ( k i ) h(k_i) h(ki)处的期望尺寸
E { X i } = E { ∑ j X i j } = ∑ j E { X i j } = 1 + ∑ j ≠ i E { X i j } = 1 + ∑ j ≠ i ( 1 ) P r { h ( k i ) = h ( k j ) } + ( 0 ) P r { h ( k i ) + h ( k j ) } ≤ 1 + ∑ j ≠ i 1 / m = 1 + ( n − 1 ) / m E\{X_i\}=E\{\sum\limits_jX_{ij}\}=\sum\limits_jE\{X_{ij}\}\\=1+\sum\limits_{j\neq i}E\{X_{ij}\}=1+\sum\limits_{j\neq i}(1)Pr\{h(k_i)=h(k_j)\}+(0)Pr\{h(k_i)+h(k_j)\} \\\le1+\sum\limits_{j\neq i}1/m=1+(n-1)/m E{Xi}=E{j∑Xij}=j∑E{Xij}=1+j=i∑E{Xij}=1+j=i∑(1)Pr{h(ki)=h(kj)}+(0)Pr{h(ki)+h(kj)}≤1+j=i∑1/m=1+(n−1)/m
因为 m = Ω ( n ) ,负载因子 α = n / m = O ( 1 ) ,因此 O ( 1 ) 是符合期望的 m=\Omega(n),负载因子\alpha=n/m=\mathcal{O}(1),因此\mathcal{O}(1)是符合期望的 m=Ω(n),负载因子α=n/m=O(1),因此O(1)是符合期望的
如果n/m远大于1,为新尺寸m用新随机选择的哈希函数进行重建(rebuild)
像动态数组那样做同样的分析,花费可以被一些动态操作分摊
因此哈希表能够,以所期望的摊还 O ( 1 ) \mathcal{O}(1) O(1)时间,实现动态集合操作
数据结构 | 操作,最坏情形O | ||||
容器(container) | 静态(static) | 动态(dynamic) | 顺序(order) | ||
build(X) | find(k) | insert(x) delete(k) |
find_min() find_max() |
find_prev(k) find_next(k) |
|
数组 | n | n | n | n | n |
有序数组 | nlogn | logn | n | 1 | logn |
直接访问数组 | u | 1 | 1 | u | u |
哈希表 | n | 1 | 1 | n | n |