对已排序数组进行二分搜索可以得到 O(log N) 比较和 O(1) 内存利用率的结果。对有序数组进行线性搜索可以得到 O(N) 比较和 O(1) 内存利用率的结果。
除了正常的记忆和比较测量之外,我们还有遍历步骤的想法。这对于没有随机访问的数据结构很重要。例如,在一个链表中,要从头部到达元素 j,我们需要向前走 j 步。这些步骤可以在没有任何比较的情况下发生。正如 cmets 中所指出的,进行遍历步骤的成本可能与进行比较的成本不同。此处的遍历步骤转换为内存读取。
问题是当我们的数据结构是一个排序的单链表时会发生什么?值得做二分查找吗?
为了解决这个问题,我们需要看看二进制搜索在排序单链表上的性能。代码如下所示:
struct Node {
Node* next;
int value;
};
Node* binarySearch(Node* n, int v) {
if (v <= n->value) return n;
Node *right, *left=n;
int size = count(n);
while (size > 1)
{
int newSize = (size / 2);
right = left;
for (int i = 0; (i < newSize) && (right->next!=nullptr); i++)
right = right->next;
if (v == right->value) return right;
else if (v > right->value) left = right;
size -= newSize;
}
if (right && (v < right->value)) return right;
else if (right->next) return right->next;
else return nullptr;
}
binarySearch 函数返回元素等于或大于v 的节点。参数n是排序单链表中的头节点。
很明显,外部循环迭代 O(log N) 次,其中 N = 列表的大小。对于每次迭代,我们进行 2 次比较,因此总比较次数为 O(log N)。
遍历步数是right = right->next;被执行的次数,也就是O(N)。这是因为内循环中的迭代次数在外循环的每次迭代中都会减少一半,因此 N/2 + N/4 + ... + 1 = N(加上或减去一些回旋余地)。
内存使用量仍为 O(1)。
相比之下,通过排序单链表进行线性搜索需要 O(n) 次遍历步骤、O(n) 次比较和 O(1) 次内存。
那么值得在单链表上进行二分查找吗?答案是几乎总是是的,但不完全是。
不考虑计数成本,如果我们要查找的元素是列表中的第二个元素,会发生什么?线性搜索需要 1 步和 1 次比较。二进制搜索需要〜N个步骤和〜log N个比较。现实不是很清楚。
总结如下:
排序数组
Binary: O(log N) comparisons, O(1) memory, O(log N) traversal steps
Linear: O(N) comparisons, O(1) memory, O(N) traversal steps
尽管从技术上讲,排序数组所需的遍历步骤数为 0。我们永远不必前进或后退。这个想法甚至没有意义。
排序单链表
Binary: O(log N) comparisons, O(1) memory, O(N) traversal steps
Linear: O(N) comparisons, O(1) memory, O(N) traversal steps
这些是最坏情况下的运行时间。然而,杯子可能并不总是半空:p