【问题标题】:How to best sort a portion of a circular buffer?如何最好地对循环缓冲区的一部分进行排序?
【发布时间】:2010-09-21 20:13:17
【问题描述】:

我在 C 中有一个循环的、静态分配的缓冲区,我将其用作 depth 广度优先搜索的队列。我希望对队列中的前 N ​​个元素进行排序。只使用常规的 qsort() 会很容易——除了它是一个循环缓冲区,并且前 N 个元素可能会环绕。当然,我可以编写自己的排序实现,它使用模运算并知道如何环绕数组,但我一直认为编写排序函数是一个很好的练习,但最好留给库。

我想到了几种方法:

  1. 使用单独的线性缓冲区 - 首先从循环缓冲区复制元素,然后应用 qsort,然后将它们复制回来。使用额外的缓冲区意味着额外的 O(N) 空间需求,这让我
    • 使用 qsort 对“顶部”和“底部”两半进行排序,然后使用附加缓冲区将它们合并
    • 与 2 相同,但在原地进行最终合并(我没有发现太多关于原地合并的信息,但我看到的实现似乎不值得降低空间复杂度)

另一方面,花一个小时思考如何优雅地避免编写我自己的快速排序,而不是添加那 25 行(左右)行可能也不是最有效率的......

更正:在切换 DFS 和 BFS 时犯了一个愚蠢的错误(我更喜欢编写 DFS,但在这种特殊情况下我必须使用 BFS),很抱歉造成混淆。

原问题的进一步描述:

我正在实现 breadth 优先搜索(寻找与fifteen 拼图不同的东西,只是更复杂,每个状态大约有 O(n^2) 个可能的扩展,而不是 4 个)。 “蛮力”算法已经完成,但它是“愚蠢的”——在每一点,它以硬编码的顺序扩展所有有效状态。队列实现为循环缓冲区(无符号队列[MAXLENGTH]),它将整数索引存储到状态表中。除了两个简单的函数来对索引进行排队和出队之外,它没有任何封装——它只是一个简单的、静态分配的无符号数组。

现在我想添加一些启发式方法。我想尝试的第一件事是在展开后对展开的子状态进行排序(“以更好的顺序展开它们”)——就像我正在编写一个简单的最佳优先 DFS 一样。为此,我想加入队列(代表最近的扩展状态),并使用某种启发式方法对它们进行排序。我还可以以不同的顺序扩展状态(因此在这种情况下,如果我打破队列的 FIFO 属性并不重要)。

我的目标不是实现A* 或基于深度优先搜索的算法(我无法扩展所有状态,但如果我不这样做,我将开始遇到状态无限循环的问题空间,所以我不得不使用iterative deepening之类的东西。

【问题讨论】:

  • 这不是 O(n) 空间要求。这是一个 O(N) 额外的空间要求,其中 N 是您实际需要排序的数量。我猜 N 很小。
  • @tgamblin:我看到我选择了相反的大小写约定。我只能说:Bbzzzzzzt。就这样!
  • 有人可以帮我整理文章的格式吗?出于某种原因,十五个谜题的链接没有终止(但在预览中还可以,所以我不知道那里出了什么问题)。
  • @tgamblin - 你说得对,我应该选择不同的字母,而不是使用 n/N。
  • @Kim - 链接问题已修复。 SO 对 URL 真的很愚蠢,en.wikipedia.org/wiki/A*_search_algorithm URL 中的星号使它很适合,所以我把它的 URL 缩小了。

标签: c algorithm sorting circular-buffer


【解决方案1】:

这里的例子怎么样。这个例子可以轻松地对零件或任何您想要的东西进行排序,而无需重新定义大量额外的内存。 它只需要两个指针,一个状态位和一个用于 for 循环的计数器。

#define _PRINT_PROGRESS
#define N 10
BYTE buff[N]={4,5,2,1,3,5,8,6,4,3};
BYTE *a = buff;
BYTE *b = buff;
BYTE changed = 0;
int main(void)
{
    BYTE n=0;
    do
    {
        b++;
        changed = 0;
        for(n=0;n<(N-1);n++)
        {
            if(*a > *b)
            {
                *a ^= *b;
                *b ^= *a;
                *a ^= *b;
                changed = 1;
            }
            a++;
            b++;
        }
        a = buff;
        b = buff;
#ifdef _PRINT_PROGRESS
        for(n=0;n<N;n++)
            printf("%d",buff[n]);
        printf("\n");
    }
#endif
    while(changed);
    system( "pause" );
}

【讨论】:

    【解决方案2】:

    你知道优化规则吗?你可以谷歌他们(你会找到几个版本,但他们都说几乎一样的东西,不要)。

    听起来您正在优化而不进行测试。这是一个巨大的禁忌。另一方面,您使用的是直接 C,因此您可能处于需要一定程度注意速度的受限平台上,因此我希望您需要跳过前两条规则,因为我假设您别无选择:

    优化规则:

    1. 不要优化。

    2. 如果您知道自己在做什么,请参阅规则 #1

    你可以去更高级的规则:

    优化规则(续):

    1. 如果您的规范需要特定级别的性能,请编写未优化的代码并编写测试以查看它是否符合该规范。如果它满足它,你就完成了。在达到这一点之前,切勿编写考虑性能的代码。

    2. 如果您完成了第 3 步,但您的代码不符合规范,请重新编码,将原来的“最明显”代码保留为 cmets 并重新测试。如果不符合要求,就扔掉,使用未优化的代码。

    3. 如果您的改进使测试通过,请确保测试保留在代码库中并重新运行,并且您的原始代码作为 cmets 保留在那里。

    注意:那应该是 3. 4. 5. 有些事情搞砸了——我什至没有使用任何标记。

    好的,最后——我不是在说这个,因为我在某处读过它。我花了几天时间试图解开其他人编码的一些可怕的混乱,因为它是“优化的”——真正有趣的是,十分之九,编译器可以比他们优化得更好。

    我意识到有时您需要优化,我所说的只是在未优化的情况下编写、测试并重新编码。它真的不会花费您太多时间——甚至可能使编写优化代码更容易。

    我发布此内容的唯一原因是因为您编写的几乎每一行都与性能有关,而且我担心下一个看到您的代码的人会像我这样可怜。

    【讨论】:

    • 是的,你可能是对的——但我主要考虑的是“性能”。冒泡排序易于编码并不意味着不应“过早”使用“优化”算法(如快速排序、合并或堆排序)。
    • 我知道你的意思,但是性能重要吗?就此而言,当链表可以工作时,您是否需要使用循环缓冲区?它是否足够小以至于数组实际上也可以工作?你写过测试来证明这一点吗?
    • 更改算法以减少工作量并不是您批评的意义上的优化。低级优化应该留到最后,但是应该尽快将算法从 O(n^2) 转换为 O(n log n) 或类似的。
    • 我同意使用循环缓冲区听起来像是以牺牲简单性为代价的优化,因为“没有充分的理由”
    • 汤姆,总的来说,我有点同意你关于数量级的说法,但我不能说这是绝对的。很大一部分时间,一段代码运行多长时间是绝对没有意义的,但清晰的代码总是有价值的,所以要编码两次。
    【解决方案3】:

    我没有看到您在 c 中要求的确切解决方案。您可能会考虑以下想法之一:

    • 如果您有权访问您的libcqsort() 的源代码,您可以复制它并简单地将所有数组访问和索引代码替换为适当通用的等效代码。这为您提供了一些适度的保证,即下级排序是有效的并且几乎没有错误。当然,对于引入自己的错误的风险没有帮助。 Big O 喜欢系统 qsort,但可能乘数更差。

    • 如果要排序的区域与缓冲区的大小相比较小,您可以使用直线前进线性排序,使用测试换行保护调用并执行复制到线性-仅在需要时进行缓冲排序然后复制回例程。在触发警卫的情况下引入额外的O(n) 操作(对于n 要排序的区域的大小),这使得平均O(n^2/N) &lt; O(n)


    我发现 C++ 不适合您。 ::sigh:: 我会把它留在这里以防其他人可以使用它。

    • 如果 C++ 是一个选项,您可以(如果需要,子类化缓冲区并)重载 [] 运算符以使标准排序算法工作。同样,应该像标准排序一样使用乘数惩罚。

    【讨论】:

      【解决方案4】:

      您可以旋转循环队列,直到有问题的子集不再环绕。然后像往常一样将该子集传递给qsort。如果您需要频繁排序或数组元素大小非常大,这可能会很昂贵。但是,如果您的数组元素只是指向其他对象的指针,那么旋转队列可能就足够快了。事实上,如果它们只是指针,那么您的第一种方法也可能足够快:制作子集的单独线性副本,对其进行排序,然后将结果写回。

      【讨论】:

      • 这是一个 O(N) 操作,队列大小为 N。对于要排序的区域的大小,它出现在大约 n/N 种情况下。如果在糟糕的情况下你能忍受可能很大的话,那么平均值就是 O(n),这相当于 Kim 的选项 1。还不错。
      • 我同意,这是一个有趣的想法。另外,我没有考虑到“特殊”排序只发生在 n/N 轮中(缓冲区大到足以容纳很多区域)。
      【解决方案5】:

      我认为您需要从问题中退后一步并尝试将其作为一个整体来解决 - 很有可能半排序的循环缓冲区不是存储数据的最佳方式。如果是,那么您已经提交了,您将不得不编写缓冲区来对元素进行排序——这是否意味着偶尔使用外部库执行排序,或者在插入元素时进行排序,我不知道。 但归根结底,它会变得很丑陋,因为 FIFO 和排序缓冲区根本不同。


      上一个答案,假设您的排序库具有强大且功能丰富的 API(根据您的问题的要求,这不需要您编写自己的 mod 排序或任何东西 - 它取决于支持任意位置的库数据,通常通过回调函数。如果您的排序不支持链表,则无法处理):

      循环缓冲区已经使用 % (mod) 算法解决了这个问题。 QSort 等不关心内存中的位置——它们只需要一个方案来以线性方式寻址数据。

      它们适用于链表(内存中不是线性的),就像它们适用于“真正的”线性非循环数组一样。

      因此,如果您有一个包含 100 个条目的循环数组,并且您发现需要对前 10 个条目进行排序,而前 10 个恰好在顶部折叠成一半,那么您需要为排序提供以下两位信息:

      • 定位数组项的函数是(x % 100)
      • 要排序的项目在位置 95 到 105

      该函数会将排序使用的地址转换为实际数组中使用的索引,并且数组环绕的事实是隐藏的,尽管对超出其边界的数组进行排序可能看起来很奇怪,一个循环数组,定义,没有界限。 % 运算符会为您处理,您不妨将数组的部分称为 1295 到 1305

      拥有一个包含 2^n 个元素的数组的奖励积分。


      其他注意事项:

      在我看来,您使用的排序库无法对线性数组以外的任何内容进行排序 - 因此它无法对链表或具有简单排序以外的任何内容的数组进行排序。你真的只有三个选择:

      • 您可以重新编写库以使其更加灵活(即,当您调用它时,您会为其提供一组用于比较操作和数据访问操作的函数指针)
      • 您可以重新编写数组,使其适合您现有的库
      • 您可以为您的特定解决方案编写自定义排序。

      现在,就我而言,我将重新编写排序代码,使其更灵活(或复制它并编辑新副本,以便您拥有对线性数组快速的排序,以及对非线性数组)

      但现实情况是,现在您的排序库非常简单,您甚至无法告诉它如何访问非线性存储的数据。

      如果就这么简单,那么您应该毫不犹豫地根据您的特定需求调整库本身,或者调整您的缓冲区以适应库。

      尝试一个丑陋的组合,比如以某种方式将你的缓冲区变成一个线性数组,对其进行排序,然后将它放回去就是这样 - 一个丑陋的组合,你以后必须理解和维护。您将“闯入”您的 FIFO 并摆弄内脏。

      -亚当

      【讨论】:

      • 我知道——这就是为什么我写道我可以“编写自己的使用模运算的排序实现”——但这是我特别想避免的一件事。此外,您必须非常小心地进行比较(这对您没有帮助,您必须使用差异)。
      • 在这种情况下,您需要描述排序算法的库实现。您已经拥有的 API/接口是什么?如果它不支持任意映射,那么你甚至不能用它对链表进行排序,而且它是一个相当有限的库。
      • 我使用的是标准 C(不是 C++ 或 C#),没有其他库。我将编辑我的原始问题,以更详细地了解我正在尝试做的事情。
      • 你还没有回答我的问题。您声称您想将排序算法留在库中 - 但您没有告诉我们哪个库。 C 标准不包括排序,所以我们在这里有点不知所措 - 你试图硬塞进你的程序的排序 API 是什么?
      【解决方案6】:

      也许priority queue 可以用来解决您的问题。'

      【讨论】:

      • 一个简单的优先级队列有 O(n) 的插入时间,在我的情况下导致 O(n^2) (与插入排序相同)。我不确定是否使用更复杂的基于堆的队列 - 可能会出现问题,因为我仍然需要整个缓冲区作为队列,而不仅仅是前 N 个排序元素(队列数组?)
      • 好吧,当然,如果你使用带有线性搜索的链表,你会得到 O(n) 的插入时间。不要那样做——使用 real 优先级队列。实际上有一些非常简单的,特别是如果您对摊销的性能而不是最坏的情况感兴趣。
      • 您在原地重新订购商品似乎很好;没有理由不能遍历优先级队列结构来访问所有内容。如果 N 很小,您可以考虑将 N 个顶部元素的排序数组与剩余元素的堆组合。
      猜你喜欢
      • 2013-05-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-09-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多