查找算法 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这是可行的,因为我们永远不会真正访问它们

同时我们动用循环不变式来论证我们的逻辑

  1. 开始时list[high] = inf >= target成立
  2. 经过一轮判断,若有list[mid] >= targetlist[high = mid] >= target
  3. 所以始终list[high] >= target
  4. 同理list[low] < target恒成立
  5. 因为low + 1 < high所以low < mid = (high - low) / 2 + low = (high + low) / 2 < high,所以每次循环high - low必定减小,即范围逐渐缩小
  6. 最终low + 1 == highlist[low] < target && list[high] >= target此时比较list[high]target,若相同则返回high,若不同则返回~high

注意此时high总是target第一次出现的位置,如果想要最后一个位置也很简单,只需要改为list[low] <= target恒成立就行了

循环不变式是论证算法的重要手段

如果输入的数组的长度总是固定的,那么是否可以用循环展开呢?

posted @ 2022-10-28 11:22  Violeshnv  阅读(25)  评论(0编辑  收藏  举报