是否可以在已排序的链表中搜索带二进制搜索的元素? 如果不可能那么问题是"为什么不可能"?
答案 0 :(得分:8)
我觉得写这个答案是错的,因为我有一种偷偷摸摸的怀疑,认为这是一个家庭作业。但是,由于在评论中与某些人进行了讨论,我认为其他人可能对此感兴趣。
对已排序数组进行二进制搜索,可以得到O(log N)比较和O(1)内存利用率的结果。对排序数组进行线性搜索可以得到O(N)比较和O(1)内存利用率的结果。
除了正常的记忆和比较测量之外,我们还有遍历步骤的想法。这对于没有随机访问的数据结构很重要。例如,在链表中,要从头部获取元素j,我们需要向前迈出j步。这些步骤可以在没有任何比较的情正如评论中所指出的,进行遍历步骤的成本可能与进行比较的成本不同。这里的遍历步骤转换为内存读取。
问题是当我们的数据结构是一个排序的单链表时会发生什么?值得做二分搜索吗?
为了解决这个问题,我们需要在排序的单链表上查看二进制搜索的性能。代码如下所示:
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-&gt; 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
答案 1 :(得分:1)
ethang shows如何在单个链表中执行二进制搜索,只需O(1)额外空间,O(n)遍历时间和O(log n)比较。我以前没有相信这是可能的。为了好玩,我想我会在Haskell中实现类似的算法:
bs :: Ord k => Int -> k -> [(k,v)] -> Maybe v
bs 0 _ _ = Nothing
bs 1 needle ((k,v) : _)
| k == needle = Just v
| otherwise = Nothing
bs size needle left = case drop size' left of
right@((k,_):_)
| needle >= k -> bs (size - size') needle right
_ -> bs size' needle left
where size' = size `quot` 2
search :: Ord k => k -> [(k,v)] -> Maybe v
search k kvs = bs (length kvs) k kvs
这可以调整为使用O(log i)比较和O(i)遍历时间,其中i是从列表的开头到所寻找的键的位置或将要的位置的距离。这个实现可以改进,但要点很简单 - 用这个版本替换上面的search
:
import Control.Applicative ((<|>))
search :: Ord k => k -> [(k,v)] -> Maybe v
-- The 10 can be replaced by any positive integer
search = go 10
where
-- The 2 can be replaced by any integer > 1
go lim needle kvs@((k,_):_) | k <= needle =
bs lim needle kvs <|> go (lim*2) needle (drop lim kvs)
go _ _ _ = Nothing
答案 2 :(得分:0)
链接列表仅允许顺序访问,因此即使列表已排序,也无法进行二进制搜索。
修改强> 正如其他人所指出的那样,二分搜索是可能的,但是没有意义。
我们可以在链表中模拟随机访问,但这会很慢并且平均时间复杂度为O(n),因此二进制搜索(通常为O(lgn))将采用O(nlgn)
编辑2 :正如@ethang所指出的,如果它是双向链表,则二进制搜索只能采用O(n)。在每个步骤中,我们可以从前一个位置开始,而不是从头部/尾部开始,因此每次移动的距离将减半。
如果必须使用链接列表,最好使用线性搜索,其复杂度仅为O(n),并且比二进制搜索更简单。
如果您想要有效地搜索和插入/删除,您可以使用其他数据限制,例如二叉搜索树。