二分查找
# 二分查找
谓词通常用于使其他注释更清晰:
predicate sorted(a: array<int>)
requires a != null
reads a
{
forall j, k :: 0 <= j < k < a.Length ==> a[j] <= a[k]
}
method BinarySearch(a: array<int>, value: int) returns (index: int)
requires a != null && 0 <= a.Length && sorted(a)
ensures 0 <= index ==> index < a.Length && a[index] == value
ensures index < 0 ==> forall k :: 0 <= k < a.Length ==> a[k] != value
{
// This one is a little harder. What should go here?
}
method BinarySearch(a: array<int>, key: int) returns (index: int)
requires a != null && sorted(a)
ensures ...
{
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
我们有和线性搜索一样的后置条件,因为目标是一样的。不同的是,现在我们知道数组是有序的。因为Dafny
可以展开函数,所以在方法体中它也知道这一点。然后我们可以用这个性质来证明搜索的正确性。方法体如下所示:
predicate sorted(a: array<int>)
requires a != null
reads a
{
forall j, k :: 0 <= j < k < a.Length ==> a[j] <= a[k]
}
method BinarySearch(a: array<int>, value: int) returns (index: int)
requires a != null && 0 <= a.Length && sorted(a)
ensures 0 <= index ==> index < a.Length && a[index] == value
ensures index < 0 ==> forall k :: 0 <= k < a.Length ==> a[k] != value
{
var low, high := 0, a.Length;
while low < high
invariant 0 <= low <= high <= a.Length
invariant forall i ::
0 <= i < a.Length && !(low <= i < high) ==> a[i] != value
{
var mid := (low + high) / 2;
if a[mid] < value
{
low := mid + 1;
}
else if value < a[mid]
{
high := mid;
}
else
{
return mid;
}
}
return -1;
}
var low, high := 0, a.Length;
while low < high
invariant 0 <= low <= high <= a.Length
invariant forall i ::
0 <= i < a.Length && !(low <= i < high) ==> a[i] != value
{
var mid := (low + high) / 2;
if a[mid] < value
{
low := mid + 1;
}
else if value < a[mid]
{
high := mid;
}
else
{
return mid;
}
}
return -1;
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
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
这是一个相当标准的二分查找实现。首先,我们声明要搜索的范围。这可以被认为是钥匙可能存在的剩余空间。范围是包含-排他的,意味着它包括指数[low
,high
)。第一个不变量表示这个范围在数组内。第二个表示键不在这个范围之外的任何地方。在if
链的前两个分支中,我们发现区间中间的元素不是键,因此我们移动区间以排除该元素及其相应一侧的所有其他元素。当移动范围的下端时,我们需要加上1,因为它在低端是包含的。如果不加1,那么当mid == low
时,循环可能会永远继续下去,当low + 1 == high
时就会发生这种情况。我们可以改变它,当low
和high
相差1时,循环退出,但这意味着我们需要在循环之后进行额外的检查,以确定是否在剩下的一个索引中找到键。在上面的公式中,这是不必要的,因为当low == high
时,循环退出。但这意味着搜索范围内没有元素留下,因此没有找到键。这可以从循环不变量推导出来:
invariant forall i ::
0 <= i < a.Length && !(low <= i < high) ==> a[i] != value
1
2
2
当low == high
时,含义的第一部分的否定条件总是为真(因为没有i
可以至少或严格小于相同的值)。因此,不变量表示数组中的所有元素都不是键,第二个后置条件成立。正如您所看到的,在这段代码中很容易出现一个错误。有了这些不变量,Dafny
不仅可以证明代码是正确的,而且我们自己也可以更容易地理解代码的操作。
练习15.
点击查看题目及代码
修改BinarySearch
函数体中的赋值,将low
设置为mid
,或将high
设置为mid - 1
。在每种情况下,哪里出了问题?
predicate sorted(a: array<int>)
requires a != null
reads a
{
forall j, k :: 0 <= j < k < a.Length ==> a[j] <= a[k]
}
method BinarySearch(a: array<int>, value: int) returns (index: int)
requires a != null && 0 <= a.Length && sorted(a)
ensures 0 <= index ==> index < a.Length && a[index] == value
ensures index < 0 ==> forall k :: 0 <= k < a.Length ==> a[k] != value
{
var low, high := 0, a.Length;
while low < high
invariant 0 <= low <= high <= a.Length
invariant forall i ::
0 <= i < a.Length && !(low <= i < high) ==> a[i] != value
{
var mid := (low + high) / 2;
if a[mid] < value
{
low := mid + 1;
}
else if value < a[mid]
{
high := mid;
}
else
{
return mid;
}
}
return -1;
}
method BinarySearch(a: array<int>, value: int) returns (index: int)
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
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
#
编辑 (opens new window)
上次更新: 2022/03/26, 19:35:15