白话算法(6) 散列表(Hash Table) 从理论到实用(下)
【澈丹,我想要个钻戒。】【小北,等等吧,等我再修行两年,你把我烧了,舍利子比钻戒值钱。】
——自扯自蛋
无论开发一个程序还是谈一场恋爱,都差不多要经历这么4个阶段:
1)从零开始。没有束缚的轻松感。似乎拥有无限的可能性,也有相当多的不确定,兴奋、紧张和恐惧。
2)从无到有。无从下手的感觉。一步一坎,进展缓慢。走弯路,犯错,投入很多产出很少。目标和现实之间产生强大的张力。疑惑、挫败、焦急和不甘心。
3)渐入佳境。快速成长。创新,充实,满足。但是在解决问题的同时往往会不可避免地引入更多的问题和遗憾。
4)接近成功。已经没有那么多的新鲜感和成就感,几乎是惯性般地努力前行。感觉成功在望,但又好像永远也不能100%搞定。有时,一心想要完成的欲望甚至超越了原本的目标。
经过前面2篇,我们也来到了第4阶段。让我们深吸一口气,把遗留下来的这几个问题全部搞定吧。
1)能不能支持所有的对象而不仅限于整数?
2)如何支持所有整数而不只是正整数?
3)被删除了的槽仍然占用查找时间。
4)随着时间的推移,被标记为碰撞的槽越来越多,怎么办?
5)必须在创建容器的时候指定大小,不能自动扩张。
6)只是一个 HashSet,而不是HashTable。
继续改造上一篇最后给出的 IntSet4。
支持所有对象而不仅限于整数
要想支持所有对象而不只是整数,就需要一个能把各种类型的对象变换成整数的方法。这一点得到了 .net 特别的优待,Object 类的 GetHashCode() 就是专门干这个的。它提供的默认实现是:对于引用类型,每创建一个新对象都会把Object里的一个内部的计数器增加1,并把计数器的值作为这个对象的 HashCode;对于 struct 对象,将基于每个字段的 HashCode 计算得出一个整型值作为对象的 HashCode。Object 的子类型可以 override GetHashCode() 函数,对于整数类型的变量,GetHashCode() 的返回值与变量的值相同;小数、字符串等都有自己的变换规则(至于具体的规则限于篇幅将不再详细介绍)。总之,我们只要调用对象的 GetHashCode() 函数,把得到的整型值作为 k 的值就行了。另外还需要一个 Object 类型的变量 Key 保存添加的对象,我们把这两个变量封装到一个名为 Bucket 的 struct 里,Add()、Remove()、Contains() 函数也要做相应的修改:
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
public class HashSet1 { [DebuggerDisplay( "Key = {Key} k = {k}" )] private struct Bucket { public Object Key; public int k; // Store hash code; } private Bucket[] _buckets; private readonly int DELETED = -1; private int GetHashCode(Object key) { return key.GetHashCode(); } public HashSet1( int capacity) { int size = GetPrime(capacity); _buckets = new Bucket[size]; } public void Add(Object item) { int i = 0; // 已经探查过的槽的数量 Bucket bucket = new Bucket { Key = item, k = GetHashCode(item) }; do { int j = DH(bucket.k, i); // 想要探查的地址 if (_buckets[j].Key == null || _buckets[j].k == DELETED) { _buckets[j] = bucket; return ; } else { i += 1; } } while (i <= _buckets.Length); throw new Exception( "集合溢出" ); } public bool Contains(Object item) { int i = 0; // 已经探查过的槽的数量 int j = 0; // 想要探查的地址 int hashCode = GetHashCode(item); do { j = DH(hashCode, i); if (_buckets[j].Key == null ) return false ; if (_buckets[j].k == hashCode && _buckets[j].Key.Equals(item)) return true ; else i += 1; } while (i <= _buckets.Length); return false ; } public void Remove(Object item) { int i = 0; // 已经探查过的槽的数量 int j = 0; // 想要探查的地址 int hashCode = GetHashCode(item); do { j = DH(hashCode, i); if (_buckets[j].Key == null ) return ; if (_buckets[j].k == hashCode && _buckets[j].Key.Equals(item)) { _buckets[j].k = DELETED; return ; } else { i += 1; } } while (i <= _buckets.Length); } // 其它部分与 IntSet4 相同 } |
让 HashSet 支持所有整数
GetHashCode() 的返回值是 int 类型,也就是说可能为负数。这时,因为数组的大小 m 是正数,所以由 k mod m 计算得出的数组下标也为负数,这可不行。解决方法是先把GetHashCode() 的返回值变换成正数再赋值给 k,最直接的做法是:
int t = key.GetHashCode();
uint k = (uint)t;
由于负数在计算机里以补码的形式保存,所以当 t 是负数时,相当于 k = uint.MaxValue - |t| + 1。这个方案的好处是能够利用 0 ~ uint.MaxValue 范围内的所有整数,更不容易发生碰撞。
不过 .net framework 的 HashTable 却是:
int t = key.GetHashCode();
int k = t & 0x7FFFFFFF; // 把 t 的最高位设为0
由于负数在计算机里以补码的形式保存,所以当 t 是负数时,相当于 k = int.MaxValue - |t| + 1。这个方案的缺点是如果添加 -9 和 2147483639 这两个数字就会发生碰撞,但是因为实际的输入总是比较扎堆的,所以这个缺点也不太容易造成太大的性能问题。它的优点是省下了最高的一个比特位。因为我们在解决问题(3)的时候需要增加一个布尔类型的变量记录是否发生了碰撞,到时候如果利用这个节省下来的比特位,就不用增加一个布尔类型的变量了。按照这个方法修改一下代码:
1
2
3
4
5
6
7
8
|
public class HashSet2 { private int GetHashCode(Object key) { return key.GetHashCode() & 0x7FFFFFFF; } // ... } |
另外,由于最高的那个比特位有别的用处了,所以不能再把删除的槽的 k 设置成 -1 了,那要设置成什么值好呢? 设置成什么值都不好,因为输入的 k 可能是任何正整数,所以总会有冲突的可能。所以我们改成把 Key 赋值为 _buckets 作为被删除的标志,完整的代码如下:
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
public class HashSet2 { [DebuggerDisplay( "Key = {Key} k = {k}" )] private struct Bucket { public Object Key; public int k; // Store hash code; sign bit means there was a collision. } private Bucket[] _buckets; private int GetHashCode(Object key) { return key.GetHashCode() & 0x7FFFFFFF; } // 将 bucket 标记为已删除 private void MarkDeleted( ref Bucket bucket) { bucket.Key = _buckets; bucket.k = 0; } // 判断 bucket 是否是空槽或者是已被删除的槽 private bool IsEmputyOrDeleted( ref Bucket bucket) { return bucket.Key == null || bucket.Key.Equals(_buckets); } public HashSet2( int capacity) { int size = GetPrime(capacity); _buckets = new Bucket[size]; } public void Add(Object item) { int i = 0; // 已经探查过的槽的数量 Bucket bucket = new Bucket { Key = item, k = GetHashCode(item) }; do { int j = DH(bucket.k, i); // 想要探查的地址 if (IsEmputyOrDeleted( ref _buckets[j])) { _buckets[j] = bucket; return ; } else { i += 1; } } while (i <= _buckets.Length); throw new Exception( "集合溢出" ); } public bool Contains(Object item) { int i = 0; // 已经探查过的槽的数量 int j = 0; // 想要探查的地址 int hashCode = GetHashCode(item); do { j = DH(hashCode, i); if (_buckets[j].Key == null ) return false ; if (_buckets[j].k == hashCode && _buckets[j].Key.Equals(item)) return true ; else i += 1; } while (i <= _buckets.Length); return false ; } public void Remove(Object item) { int i = 0; // 已经探查过的槽的数量 int j = 0; // 想要探查的地址 int hashCode = GetHashCode(item); do { j = DH(hashCode, i); if (_buckets[j].Key == null ) return ; if (_buckets[j].k == hashCode && _buckets[j].Key.Equals(item)) { MarkDeleted( ref _buckets[j]); return ; } else { i += 1; } } while (i <= _buckets.Length); } } |
减少已删除的槽对查找时间的影响
我们在上一篇举过这样一个例子:假设有一个容量 m 为10 的 HashSet,先依次添加 0、1、2、3、4、5、6、7、8、9,然后再删除 0、1、2、3、4、5、6、7、8,这时调用 Contains(0),此函数会依次检查 _values[0]、_values[1]..._values[9],也就是把整个数组遍历了一遍!为什么会这样呢?因为我们在删除一个数字的时候,由于不知道这个数字的后面是否还有一个因与它碰撞而被安排到下一个空槽的数字,所以我们不敢直接把它设为 null。为了解决这个问题,需要一种方法可以指出每个槽是否发生过碰撞。最直接的方法是增加一个 bool 类型的变量,不过为了节省空间,我们将利用在解决问题(2)的时候预留出来的 k 的最高位。如果新添加的项与某个槽发生了碰撞,就把那个槽的碰撞位设为1。有了碰撞位,就可以知道:
1)如果碰撞位为0,说明要么没发生过碰撞,要么它是碰撞链的最后一个槽。
2)如果碰撞位为1,说明它不是碰撞链的最后一个槽。
对于碰撞位为0的槽,删除时可以直接把 Key 设为 null;对于碰撞位为1的槽,因为它不是碰撞链的最后一个槽,所以在删除时还是不能把它的 Key 设为null,而是设为 _buckets 表示已删除,并且要保留 k 的最高位为 1,把 k 的其它位设为0。由于我们其实是把一个 int 类型的变量 _k 当成了一个 bool 类型的变量和一个正整数类型的变量来用,所以要先改造一下 Bucket。(ps:用了很多位操作,要是 C# 也有类似于 C++ 的共用体那种东东的话就不用这么麻烦了)
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
|
public class HashSet3 { [DebuggerDisplay( "Key = {Key} k = {k} IsCollided = {IsCollided}" )] private struct Bucket { public Object Key; private int _k; // Store hash code; sign bit means there was a collision. public int k { get { return _k & 0x7FFFFFFF; } // 返回去掉最高的碰撞位之后的 _k set { _k &= unchecked (( int )0x80000000); // 将 _k 除了最高的碰撞位之外的其它位全部设为0 _k |= value; // 保持 _k 的最高的碰撞位不变,将 value 的值放到 _k 的后面几位中去 } } // 是否发生过碰撞 public bool IsCollided { get { return (_k & unchecked (( int )0x80000000)) != 0; } // _k 的最高位如果为1表示发生过碰撞 } // 将槽标记为发生过碰撞 public void MarkCollided() { _k |= unchecked (( int )0x80000000); // 将 _k 的最高位设为1 } } // ... } |
不需要修改 Contains() 函数,把 Add() 和 Remove() 函数按上面的讨论进行修改:
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
public class HashSet3 { public void Add(Object item) { int i = 0; // 已经探查过的槽的数量 int k = GetHashCode(item); do { int j = DH(k, i); // 想要探查的地址 if (IsEmputyOrDeleted( ref _buckets[j])) { _buckets[j].Key = item; _buckets[j].k = k; // 仍然保留 _k 的最高位不变 return ; } else { _buckets[j].MarkCollided(); i += 1; } } while (i <= _buckets.Length); throw new Exception( "集合溢出" ); } public void Remove(Object item) { int i = 0; // 已经探查过的槽的数量 int j = 0; // 想要探查的地址 int hashCode = GetHashCode(item); do { j = DH(hashCode, i); if (_buckets[j].Key == null ) return ; if (_buckets[j].k == hashCode && _buckets[j].Key.Equals(item)) // 找到了想要删除的项 { if (_buckets[j].IsCollided) { // 如果不是碰撞链的最后一个槽,要把槽标记为已删除 MarkDeleted( ref _buckets[j]); } else { // 如果是碰撞链的最后一个槽,直接将 Key 设为 null _buckets[j].Key = null ; _buckets[j].k = 0; } return ; } else { i += 1; } } while (i <= _buckets.Length); } // ... } |
如您所见,一旦被扣上了“发生过碰撞的槽”的帽子,可就一辈子摘不掉了,即使碰撞链的最后一个槽已经被删除了,删除碰撞链的倒数第二个槽时也不会把碰撞位设为0,随着时间的推移,被标记为碰撞过的槽只会越来越多。如果频繁地删完了添、添完了删,发展到极致就可能产生每个槽都被标记为碰撞的情况,这时候 Contains() 函数的复杂度又变成 O(n) 了。解决这个问题的方法是发现被标记为碰撞的槽的超过一定的比例之后,重新排列整个数组,详细的方法会在后面实现容器的自动扩张时一并给出。
我们可以很自然地想到,如果不是使用一个比特来位标记是否发生过碰撞,而是为每个槽增加一个整型变量精确记录发生过多少次碰撞,并且在删除碰撞链的最后一个槽时,把整条碰撞链的每一个槽的碰撞次数减少1,这样就可以完美解决上面那个问题了。这跟垃圾回收机制很像。之所以没有采用这种方法,一是因为这样做会使耗费的存储空间增加1倍,二是因为使用了双重散列法之后,不是很容易产生一长串碰撞链,如果不是特别频繁地删除、添加的话,问题不会很严重。
HashSet 的自动扩张
在数组满了之后,再要添加新项,就得先把数组扩张得大一些。显然,让新数组只比老数组多一个槽是不恰当的,因为那样岂不是以后每添加一个新项都得去扩张数组?那么应该创建多大的新数组才合适呢?目前通常的做法是让新数组比老数组大1倍。
另外,我们在前一篇已经提到过,开放寻址法在座位全部坐满的情况下性能并不好。上座率越低性能越好,但是也越浪费空间。这个上座率——也就是装载因子(_loadFactor)设为多少能达到性能与空间的平衡呢?.net framework 使用的是 0.72 这个经验值。数组允许添加的项的数量上限 _loadSize = _buckets.Length * _loadFactor。我们会添加一个整型的变量 _count 记录添加到数组中的项的个数(Add() 时 _count 增加1;Remove() 时 _count 减少1),当检测到 _count >= _loadSize 时就要扩张数组,而不会等到数组满了之后。
还需要添加一个整型变量 _occupancy 用于记录被标记为碰撞的槽的数量。当 _occupancy > _loadSize 时,有可能造成 O(n) 复杂度的查找,所以这时也应该重新排列数组。
首先,添加上面提到那几个变量,并在构造函数里初始化它们:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class HashSet4 { private int _count = 0; // 添加到数组中的项的总数 private int _occupancy = 0; // 被标记为碰撞的槽的总数 private float _loadFactor = 0.72f; // 装载因子 private int _loadsize = 0; // 数组允许放置的项的数量上限,_loadsize = _bucket.Length * _loadFactor,在确定数组的大小时被初始化。 public HashSet4( int capacity) { int size = GetPrime(( int )(capacity / _loadFactor)); if (size < 11) size = 11; // 避免过小的size _buckets = new Bucket[size]; _loadsize = ( int )(size * _loadFactor); } // ... } |
然后实现用于扩张容器的 Expand() 函数和重新排列数组的 Rehash() 函数。因为 Rehash() 函数需要使用新数组的长度计算下标,所以需要改造一下 H1()、H2() 和 DH(),让它们可以接收数组的大小 m 做为参数。Rehash() 与 Add() 函数很像:
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
public class HashSet4 { private int H1( int k, int m) { return k % m; } private int H2( int k, int m) { return 1 + (k % (m - 1)); } private int DH( int k, int i, int m) { return (H1(k, m) + i * H2(k, m)) % m; } // 扩张容器 private void Expand() { int newSize = GetPrime(_buckets.Length * 2); // buckets.Length*2 will not overflow Rehash(newSize); } // 将老数组中的项在大小为 newSize 的新数组中重新排列 private void Rehash( int newSize) { _occupancy = 0; // 将标记为碰撞的槽的数量重新设为0 Bucket[] newBuckets = new Bucket[newSize]; // 新数组 // 将老数组中的项添加到新数组中 for ( int oldIndex = 0; oldIndex < _buckets.Length; oldIndex++) { if (IsEmputyOrDeleted( ref _buckets[oldIndex])) continue ; // 跳过已经删除的槽好空槽 // 向新数组添加项 int i = 0; // 已经探查过的槽的数量 do { int j = DH(_buckets[oldIndex].k, i, newBuckets.Length); // 想要探查的地址 if (IsEmputyOrDeleted( ref newBuckets[j])) { newBuckets[j].Key = _buckets[oldIndex].Key; newBuckets[j].k = _buckets[oldIndex].k; break ; } else { if (newBuckets[j].IsCollided == false ) { newBuckets[j].MarkCollided(); _occupancy++; } i += 1; } } while ( true ); } // 用新数组取代老数组 _buckets = newBuckets; _loadsize = ( int )(newSize * _loadFactor); } // ... } |
Add() 和 Remove() 需要稍稍修改一下,加上统计 _count 和 _occupancy 的代码,并在需要的时候扩张或重排数组:
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
public class HashSet4 { public void Add(Object item) { if (_count >= _loadsize) Expand(); // 如果添加到数组中的项数已经到达了上限,要先扩张容器 else if (_occupancy > _loadsize && _count > 100) Rehash(_buckets.Length); // 如果被标记为碰撞的槽的数量和 _loadsize 一般多,就要重新排列所有的项 int i = 0; // 已经探查过的槽的数量 int k = GetHashCode(item); do { int j = DH(k, i, _buckets.Length); // 想要探查的地址 if (IsEmputyOrDeleted( ref _buckets[j])) { _buckets[j].Key = item; _buckets[j].k = k; // 仍然保留 _k 的最高位不变 _count++; return ; } else { if (_buckets[j].IsCollided == false ) { _buckets[j].MarkCollided(); _occupancy++; } i += 1; } } while (i <= _buckets.Length); throw new Exception( "集合溢出" ); } public void Remove(Object item) { int i = 0; // 已经探查过的槽的数量 int j = 0; // 想要探查的地址 int hashCode = GetHashCode(item); do { j = DH(hashCode, i, _buckets.Length); if (_buckets[j].Key == null ) return ; if (_buckets[j].k == hashCode && _buckets[j].Key.Equals(item)) // 找到了想要删除的项 { if (_buckets[j].IsCollided) { // 如果不是碰撞链的最后一个槽,要把槽标记为已删除 MarkDeleted( ref _buckets[j]); } else { // 如果是碰撞链的最后一个槽,直接将 Key 设为 null _buckets[j].Key = null ; _buckets[j].k = 0; } _count--; return ; } else { i += 1; } } while (i <= _buckets.Length); } // ... } |
终于完成了一个比较实用的 HashSet 了!附上完整代码:
HashSet 到 HashTable
前面为了简单起见,一直都是只有 Key 而没有 Value。现在我们若想添加一个 Value 简直就跟玩儿似的:
1
2
3
4
5
6
7
8
9
10
11
|
public class HashTable { [DebuggerDisplay( "Key = {Key} k = {k} IsCollided = {IsCollided}" )] private struct Bucket { public Object Key; public Object Value; // ... } // ... } |
当然,Add() 函数也要加上一个 Value 参数,还有按 Key 查找 Value 等等功能限于篇幅就不再啰嗦了。
HashTable 和泛型 Dictionary
在 .net framework 的 HashTable 的源代码的注释里写着“泛型 Dictionary 是 Copy 自 HashTable(The generic Dictionary was copied from Hashtable's source - any bug fixes here probably need to be made to the generic Dictionary as well.)”,但是瞎了我的极品高清三角眼,怎么看了半天也没找着双重散列的身影呢?除了是泛型的以外,Dictionary 的实现与 HashTable 还有多少不同之处呢?也许下次我们可以一起研究下 Dictionary 的源代码。
白话算法(6) 散列表(Hash Table) 从理论到实用(下)的更多相关文章
- 白话算法(6) 散列表(Hash Table)从理论到实用(上)
处理实际问题的一般数学方法是,首先提炼出问题的本质元素,然后把它看作一个比现实无限宽广的可能性系统,这个系统中的实质关系可以通过一般化的推理来论证理解,并可归纳成一般公式,而这个一般公式适用于任何特殊 ...
- 白话算法(6) 散列表(Hash Table)从理论到实用(中)
不用链接法,还有别的方法能处理碰撞吗?扪心自问,我不敢问这个问题.链接法如此的自然.直接,以至于我不敢相信还有别的(甚至是更好的)方法.推动科技进步的人,永远是那些敢于问出比外行更天真.更外行的问题, ...
- [转载] 散列表(Hash Table)从理论到实用(上)
转载自:白话算法(6) 散列表(Hash Table)从理论到实用(上) 处理实际问题的一般数学方法是,首先提炼出问题的本质元素,然后把它看作一个比现实无限宽广的可能性系统,这个系统中的实质关系可以通 ...
- [转载] 散列表(Hash Table)从理论到实用(中)
转载自:白话算法(6) 散列表(Hash Table)从理论到实用(中) 不用链接法,还有别的方法能处理碰撞吗?扪心自问,我不敢问这个问题.链接法如此的自然.直接,以至于我不敢相信还有别的(甚至是更好 ...
- [转载] 散列表(Hash Table) 从理论到实用(下)
转载自: 白话算法(6) 散列表(Hash Table) 从理论到实用(下) [澈丹,我想要个钻戒.][小北,等等吧,等我再修行两年,你把我烧了,舍利子比钻戒值钱.] ——自扯自蛋 无论开发一个程序还 ...
- 算法导论-散列表(Hash Table)-大量数据快速查找算法
目录 引言 直接寻址 散列寻址 散列函数 除法散列 乘法散列 全域散列 完全散列 碰撞处理方法 链表法 开放寻址法 线性探查 二次探查 双重散列 随机散列 再散列问题 完整源码(C++) 参考资料 内 ...
- 散列表(Hash Table)
散列表(hash table): 也称为哈希表. 根据wikipedia的定义:是根据关键字(Key value)而直接访问在内存存储位置的数据结构.也就是说,它通过把键值通过一个函数的计算,映射到表 ...
- Java 集合 散列表hash table
Java 集合 散列表hash table @author ixenos 摘要:hash table用链表数组实现.解决散列表的冲突:开放地址法 和 链地址法(冲突链表方式) hash table 是 ...
- 散列表(Hash table)及其构造
散列表(Hash table) 散列表,是根据关键码值(Key value)而直接进行访问的数据结构.它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度.这个映射函数叫做散列函数,存放记录 ...
随机推荐
- Command 'java' not found during running appium
Question: When Execution code:driver = new RemoteWebDriver(new Uri("http://127.0.0.1:4723/wd/hu ...
- Linux下安装 activemq 并指定jdk 1.8
1. 下载安装包 <apache-activemq-5.15.4-bin.tar.gz> 下载地址:https://pan.baidu.com/s/18xzjBAchjWqsHNA1HuY ...
- windowsphone8.1学习笔记之位图编程
说位图,先把image控件简单过下,Image的Source设置 <Image Name="img" Source="可以是网络图片的Uri.应用文件的Uri或者安 ...
- URAL 1181 Cutting a Painted Polygon【递归+分治】
题目: http://acm.timus.ru/problem.aspx?space=1&num=1181 http://acm.hust.edu.cn/vjudge/contest/view ...
- jQuery-Ajax-Timeout属性不生效的问题
async必须设置为async:ture,timeout才生效: 如果设置为async:false,则锁住浏览器,禁止一切操作,直到请求有返回结果.
- 我的Android进阶之旅------>Android中StateListDrawable支持的状态
Android中StateListDrawable支持的状态 android:state_active 代表是否处于激活状态 android:state_checked 代表是否处于已勾选状态 an ...
- centos7 使用 maven
http://www.cnblogs.com/jackluo/archive/2013/02/06/2901816.html
- 使用 Python 为 KVM 编写脚本,第 1 部分: libvirt
虚拟化是目前市场上大多数服务器操作系统的标准设备.在 Linux® 的世界里,服务器虚拟化有两个主要选择:基于 Kernel 的虚拟机 (KVM) 和 Xen.KVM 是 Red Hat 和其他公司采 ...
- c#命名规则参考
命名规则参考:1.从组件类型名中移去T前缀.例如TButton变成Button.2.除了第一个元音,删去所有元音字母.例如,Button变成bttn,Edit变成edt.3.压缩双字母.例如,bttn ...
- DELPHI-Delphi常用类型及定义单元
DELPHI-Delphi常用类型及定义单元 Type Unit Date SysUtils DeleteFile SysUtils or Windows (different versions) D ...