修复这个错误的 Bingo Sort 实现

Posted

技术标签:

【中文标题】修复这个错误的 Bingo Sort 实现【英文标题】:Fixing this faulty Bingo Sort implementation 【发布时间】:2022-01-12 21:19:35 【问题描述】:

在学习选择排序时,我遇到了一种称为宾果排序的变体。根据这个字典条目here,宾果排序是:

选择排序的一种变体,它通过首先找到最小值来对项目进行排序,然后重复地将具有该值的所有项目移动到它们的最终位置,并为下一次遍历找到最小值。

根据上面的定义,我想出了下面的 Python 实现:

def bingo_sort(array, ascending=True):
    from operator import lt, gt

    def comp(x, y, func):
        return func(x, y)

    i = 0
    while i < len(array):
        min_value = array[i]
        j = i + 1

        for k in range(i + 1, len(array), 1):
            if comp(array[k], min_value, (lt if ascending else gt)):
                min_value = array[k]
                array[i], array[k] = array[k], array[i]
            elif array[k] == min_value:
                array[j], array[k] = array[k], array[j]
                j += 1
        i = j

    return array

我知道这个实现是有问题的。当我在一个非常小的数组上运行算法时,我得到了一个正确排序的数组。但是,使用更大的数组运行算法会导致数组的排序大多不正确。要在 Python 中复制该问题,可以在以下输入上运行该算法:

test_data = [[randint(0, 101) for i in range(0, 101)],
             [uniform(0, 101) for i in range(0, 101)],
             ["a", "aa", "aaaaaa", "aa", "aaa"],
             [5, 5.6],
             [3, 2, 4, 1, 5, 6, 7, 8, 9]]
    
for dataset in test_data:
    print(dataset)
    print(bingo_sort(dataset, ascending=True, mutation=True))
    print("\n")

我一生都无法意识到问题出在哪里,因为我研究这个算法太久了,而且我对这些东西并不精通。除了 2020 年的 an undergraduate graduation project written 之外,我无法在线找到 Bingo Sort 的实现。任何可以为我指明正确方向的帮助将不胜感激。

【问题讨论】:

【参考方案1】:

我认为您的主要问题是您尝试在第一个条件语句中设置 min_value,然后根据您刚刚在第二个条件语句中设置的相同 min_value 进行交换。这些过程应该是交错的:宾果排序的工作方式是在一次迭代中找到 min_value,然后在下一次迭代中将该 min_value 的所有实例交换到前面,同时为下一次迭代找到下一个 min_value。这样, min_value 应该只在每次迭代结束时改变,而不是在迭代期间改变。当您在给定的迭代过程中将要交换的值更改为最前面时,您最终可能会无意中改变一些东西。

如果你想参考一些东西,我在下面有一个实现,有一些注意事项:由于你允许自定义比较器,我将 min_value 重命名为 swap_value,因为我们并不总是抓住最小值,我修改了如何将比较器定义/传递到函数中以使算法更加灵活。另外,你实际上并不需要三个索引(我认为这里甚至有几个错误),所以我将 i 和 j 折叠到 swap_idx 中,并将 k 重命名为 cur_idx。最后,由于交换给定的 swap_val 和查找 next_swap_val 是如何交错的,所以您需要预先找到初始的 swap_val。我为此使用了一个 reduce 语句,但是您可以在那里对整个数组使用另一个循环;他们是等价的。代码如下:

from operator import lt, gt
from functools import reduce

def bingo_sort(array, comp=lt):
  if len(array) <= 1:
    return array

  # get the initial swap value as determined by comp
  swap_val = reduce(lambda val, cur: cur if comp(cur, val) else val, array)
  swap_idx = 0 # set the inital swap_idx to 0

  while swap_idx < len(array):
    cur_idx = swap_idx
    next_swap_val = array[cur_idx]
    while cur_idx < len(array):
      if comp(array[cur_idx], next_swap_val): # find next swap value
        next_swap_val = array[cur_idx]
      if array[cur_idx] == swap_val: # swap swap_vals to front of the array
        array[swap_idx], array[cur_idx] = array[cur_idx], array[swap_idx]
        swap_idx += 1
      cur_idx += 1

    swap_val = next_swap_val

  return array

一般而言,此算法的复杂性取决于处理了多少重复值以及处理它们的时间。这是因为在给定迭代期间每次处理 k 个重复值时,对于所有后续迭代,内部循环的长度都会减少 k。因此,在早期处理大量重复值的集群时(例如当数组的最小值包含许多重复值时),性能会得到优化。由此,基本上有两种方法可以分析算法的复杂性:您可以根据重复值倾向于出现在最终排序数组(类型 1)中的位置来分析它,或者您可以假设重复值的集群在排序后的数组中随机分布,并根据重复簇的平均大小(即 m 相对于 n 的大小:类型 2)分析复杂度。

您链接的定义使用第一种类型的分析(基于重复出现的位置)得出最佳 = Theta(n+m^2)、平均值 = Theta(nm)、最差 = Theta(nm)。第二种分析产生最佳 = Theta(n),平均值 = Theta(nm),最差 = Theta(n^2),因为您将 m 从 Theta(1) 变为 Theta(m) 再到 Theta(n)。

在最好的类型 1 情况下,所有重复项都将位于数组的最小元素中,这样内循环的运行时间迅速减少到 O(m),并且算法的最终迭代作为O(m^2) 选择排序。不过还是有前期的 O(n) pass 来选择初始的 swap 值,所以整体复杂度是 O(n + m^2)。

在最坏的类型 1 情况下,所有重复项都将位于数组的最大元素中。在算法的最后一次迭代之前,内部循环的长度并没有显着缩短,这样我们就可以实现类似于 n + n-1 + n-2 .... + n-m 的运行时。这是 m 个 O(n) 值的总和,总运行时间为 O(nm)。

在平均类型 1 情况下(以及所有类型 2 情况),我们不假设重复值的集群偏向排序数组的前面或后面。我们认为 m 个重复值簇根据它们的位置和大小随机分布在数组中。在这个分析下,我们期望在最初的 O(n) 次通过找到第一个交换值之后,外循环的 m 次迭代中的每一次都会将内循环的长度减少大约 n/m。这导致未知 m 和随机分布数据的整体运行时间表达式为:

我们可以将这个表达式用于具有随机分布数据和未知 m 的平均情况运行时间,Theta(nm),作为平均类型 2 运行时间,它也直接给出了我们最好和最坏情况下的运行时间——时间取决于我们如何改变 n 的大小。

在最好的类型 2 情况下,m 可能只是与 n 无关的某个常数值。如果我们有 m=Theta(1) 随机分布的重复集群,则最佳情况运行时间是 Theta(n*Theta(1))) = Theta(n)。例如,您会看到宾果排序的 O(2n) = O(n) 性能只有一个唯一值(一次查找查找值,一次将每个单个值交换到前面),并且此 O( n) 如果 m 以任何常数为界,则渐近复杂度仍然成立。

然而,在最坏的类型 2 情况下,我们可以有 m=Theta(n),并且宾果排序本质上会演变为 O(n^2) 选择排序。这显然是 m = n 的情况,但是如果预期内循环的运行时间随着每次迭代减少,n/m 是任何常数值,对于 Theta 中的任何 m 值都是这种情况( n),我们仍然看到 O(n^2) 复杂度。

【讨论】:

很好的解释!查看您的代码,我已经意识到为什么我的代码导致错误排序。预先计算最小值/最大值并将两个过程交错(找到最小值/最大值,根据该最小值/最大值排序)似乎确实是关键。 您的解释还有助于澄清本书第 137 - 139 页中 here 提供的伪代码。您是否认为您的答案仍然符合本书和 @987654323 中所述的时间复杂度@ 即:最佳和最坏情况输入的 O(mn)。 @ErenJGenç 我在答案中添加了复杂性分析。我没有那本书,但我展示了它如何匹配字典条目的复杂性。

以上是关于修复这个错误的 Bingo Sort 实现的主要内容,如果未能解决你的问题,请参考以下文章

bingo!

实现 data.sort 按标题对 json 进行排序,不断收到“未定义”错误。不知道我做错了啥

Sort() 导致分段错误

如何修复 JAVA VS 代码中的实现错误,“必须实现继承的抽象类”

如何在Java中修复/实现equals方法? [重复]

如何修复Parameter必须是实现Countable的数组或对象