查找算法 Search
查找算法
顺序查找
最简单形式的顺序查找
public static int LinearSearch<T>(IList<T> list, T target) {
for (int i = 0, count = list.Count; i != count; ++i) // 一次比较 i and count
if (list[i].Equals(target)) // 又一次比较 list[i] and target
return i;
return -1;
}
函数主体只有四行,一个个搜索就是了
看上去这个函数似乎无法改进了,但我们可以用一些程序设计的小技巧来加速
- 哨兵(Sentinel)
- 循环展开
public static int LinearySearch<T>(IList<T> list, T target)
{
int count = list.Count;
if (count == 0)
return -1;
T hold = list[count - 1];
list[count - 1] = target;
int i = 0;
while (!list[i].Equals(target)) // 只有一处比较操作
++i;
list[count - 1] = hold;
if (i == count - 1)
return -1;
return i;
}
虽然代码变长了,但是减少了一处比较,我们多了三到四次赋值,但少了n次比较,这是合算的
public static int LinearySearch<T>(IList<T> list, T target)
{
int count = list.Count;
if (count == 0)
return -1;
T hold = list[count - 1];
list[count - 1] = target;
int i;
for (i = 0; ; i += 8)
{
if (list[i].Equals(target))
{
break;
}
if (list[i + 1].Equals(target))
{
i += 1;
break;
}
if (list[i + 2].Equals(target))
{
i += 2;
break;
}
if (list[i + 3].Equals(target))
{
i += 3;
break;
}
if (list[i + 4].Equals(target))
{
i += 4;
break;
}
if (list[i + 5].Equals(target))
{
i += 5;
break;
}
if (list[i + 6].Equals(target))
{
i += 6;
break;
}
if (list[i + 7].Equals(target))
{
i += 7;
break;
}
}
list[count - 1] = hold;
if (i == count - 1)
return -1;
return i;
}
将循环展开有利于减少分支,增加指令并行性,也可以展开成8次比较,这取决于实际情况,如果能完全抛弃循环就更好了
以上技巧在接近底层的语言中作用更明显
二分查找(折半查找)
public static int BinarySearch<T>(IList<T> list, T target)
where T : IComparable<T>
{
int low = 0, high = list.Count - 1;
while (low <= high) {
int mid = (high - low) / 2 + low;
int comp = list[mid].CompareTo(target);
if (comp < 0)
low = mid + 1;
else if (comp > 0)
high = mid - 1;
else
return mid;
}
return ~(high + 1);
}
这段代码问题有两个
- 做了三次比较:
comp <>= 0
,这里因为比较的是int
所以不明显,但是如果是没有CompareTo
只能用<>=
那么将会非常明显 - 返回的是
target
存在的任意位置,如果有一片连续的target
,那么返回哪一个是任意的
public static int BinarySearch<T>(IList<T> list, T target)
where T : IComparable<T>
{
int low = -1, high = list.Count;
while (low + 1 != high) // assert: list[high] >= target
{
int mid = (high - low) / 2 + low;
if (list[mid].CompareTo(target) < 0)
low = mid;
else
high = mid;
}
if (high == list.Count || list[high].CompareTo(target) != 0)
return ~high;
return high;
}
这里我们规定list[-1] = -inf, list[list.Count] = inf
这是可行的,因为我们永远不会真正访问它们
同时我们动用循环不变式来论证我们的逻辑
- 开始时
list[high] = inf >= target
成立 - 经过一轮判断,若有
list[mid] >= target
则list[high = mid] >= target
- 所以始终
list[high] >= target
- 同理
list[low] < target
恒成立 - 因为
low + 1 < high
所以low < mid = (high - low) / 2 + low = (high + low) / 2 < high
,所以每次循环high - low
必定减小,即范围逐渐缩小 - 最终
low + 1 == high
且list[low] < target && list[high] >= target
此时比较list[high]
和target
,若相同则返回high
,若不同则返回~high
注意此时high
总是target
第一次出现的位置,如果想要最后一个位置也很简单,只需要改为list[low] <= target
恒成立就行了
循环不变式是论证算法的重要手段
如果输入的数组的长度总是固定的,那么是否可以用循环展开呢?