将已排序数组快速就地分区为两个已排序的子数组

时间:2012-04-26 10:37:02

标签: c# algorithm

编辑 - 我删除了所有不必要的上下文解释 - 过于冗长,最终与问题无关。总而言之,我在构建平衡KD树的过程中对坐标数组进行了分区(see wikipedia article, Construction section以获得更多。我实际上有n个项目的k个并行数组,每个项目必须通过相同的比较进行分区)< / em>的

这不是家庭作业 - 我已经写过这样的问题,就是确保传达所有细微差别。

给定排序数组:

 int[] ints =  { 0, 1, 2, 3, 4, 5, 6 };
 //this one is important - my current solution fails on this
 int[] ints2 = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

请注意,由于同事要求澄清,所有关于这些数组的保证是element[n]将小于或等于element[n+1]。< /强>

对这些操作的成功操作会将它们分成两个子数组LR(如下所示):

/*ints == */  { 1, 3, 5, 0, 2, 4, 6 }
              /*|> L <|  |>   R  <|*/

/*ints2 == */ { 1, 3, 5, 7, 9, 0, 2, 4, 6, 8 }
              /*|>    L    <|  |>    R    <|*/

L包含奇数的整数,R包含偶数的整数,同时保留这些子数组中这些元素的原始排序顺序。

理想情况下,该函数 NOT 需要重新排序元素(已经提前执行了冗长的排序操作),并且它不会使用临时数组。我相信这意味着我正在寻找O(N)复杂度和O(1)内存。

该函数可以提供每个子数组的开始和结束元素 - 即调用者可以预先知道有多少项将落在左/右侧(可能通过预先扫描数组为奇数/偶数) 。 编辑 实际上它就像一个数组一样开始;所以一个可以在没有这些值的情况下工作的解决方案是好的,因为否则如果需要初始传递,完整的解决方案实际上只能达到O(2n)复杂度。

这是我目前正在尝试的地方 - 我已经对其进行了更新,并根据原帖中的内容对其进行了评论。

public void DivideSubArray(int[] array, int leftStart, int leftCount, 
  int rightStart, int rightCount)
{
  int currentLeft = leftStart, currentRight = rightStart;
  int leftCounter = leftCount;
  int temp;
  int readahead;
  while (leftCounter != 0) {
    if ((array[currentLeft] % 2) == 0)
    {
      //remember the element we swap out
      temp = array[currentRight];
      //Set as next item on the right. We know this is the next lowest-sorted 
      //right-hand item because we are iterating through an already-sorted array
      array[currentRight++] = array[currentLeft];
      // * read ahead to see if there are any further elements to be placed
      // * on the left - move them back one by one till there are no more.
      readahead = currentLeft + 1;
      while ((array[readahead] % 2) != 0)
      {
        array[currentLeft++] = array[readahead++];
        leftCounter--;
      }
      //Now write the swapped-out item in, but don't increment our currentLeft.  
      //The next loop will check if the item is in the correct place.
      array[currentLeft] = temp;
    }
    else //this item is already in the correct place
    {
      currentLeft++;
      leftCounter--;
    }
  }
}

如下调用:

int numOdd = ints.Count(i => (i % 2) == 1);
DivideSubArray(ints, 0, numOdd, numOdd, ints.Length - numOdd);

它为ints(以及许多其他数组)生成预期的数组,但不生成ints2

{ 1, 5, 3, 7, 9, 0, 2, 6, 4, 8 }

因此它正确分区 - 但交换3,56,4。我理解 为什么 :因为在第一个循环5被交换到左侧,然后传播2,因为算法说5 1}}很奇怪,应该留下来。我写了一个决策树来修复它,但是跟着它做了几个循环,它推断出解决方案是递归的。

我很难看到如何在子阵列中运行更多排序操作或创建临时列表/数组作为工作空间来解决这个问题。当然,但是,一种类型可能会增加复杂性但保持内存要求;如果它是最快的解决方案,那么使用它是有意义的。

根据我的回答,您可以看到我当前最快(在运行时间)和最佳内存解决方案。作为衡量标准 - 上述尝试不仅会产生不正确的结果,而且还需要3倍于我答案中的代码。

感觉必须有一种简单的方法来利用单个&#39;备用&#39;变量交换项目 - 我只是无法看到它 - 我希望SO集体大脑会:)

当然,如果答案是“不”。那就这样吧。

7 个答案:

答案 0 :(得分:0)

我认为你可以通过以下方式简化你的任务:首先修改数组,首先在数组的开头按升序写入奇数,然后在数组的开头按顺序写入偶数。结束。对于你的例子{0,1,... 6},这看起来像{1,3,5,6,4,2,0}。执行此操作后,执行另一个线性传递以反转数组的第二部分(这非常简单直接)。

为什么我认为这应该更容易?好吧,因为你在第一步中应该做的就是常规的qsort算法会做什么(有点奇怪的比较运算符)。您可以搜索互联网以查看qsort分区是如何完成的(例如,有一个示例here)。我真的相信,如果你在这两个步骤上分解问题,实施解决方案对你来说会更容易。另请注意,整体复杂性没有改变。

希望这会对你有所帮助。

编辑:以下是我相信你可以做我的建议的第一部分:

public void DivideSubArray(int[] array, int leftStart, 
              int leftCount, int rightStart, int rightCount)
{
    int currentRight = rightStart + rightCount - 1;

    int current = leftStart;
    while (current < currentRight) {
        if ((array[current] % 2) == 0)
        {
            int temp = array[current];
            array[current] = array[currentRight];
            array[currentRight] = temp;
            currentRight--;
        } else {
            current++;
        }
    } 
}

我没有提供反转偶数部分的代码,因为我认为这很简单,我还想强调这种方法简化代码的程度。

答案 1 :(得分:0)

我设法得到了一个没有使用临时阵列的解决方案 - 对于大N来说,这个速度非常慢;我甚至都不会为它发布代码,就是那么糟糕!

编辑 - 根据我的原始解决方案进行了改进。复杂性在技术上是O(2n)(因为List.CopyTo方法使用Array.Copy,根据框架文档是O(n)),内存是O(n)。

是的,解决方案只是采用数组并动态进行拆分,而不是事先依赖奇数/偶数拆分的知识。这意味着(当回归到我的实际代码时)不需要初始传递 - 所以它更可取。

这个解决方案很简单:它扫描数组,将赔率移回阵列的开头(或者如果它们已经在正确的位置,则将它们留在原处)并将平均值添加到列表中。循环完成后,列表将复制到数组的其余部分。它以牺牲内存为代价来满足我的复杂性要求 - 最糟糕的是O(n) - 并且是对我已经使用的代码的一个很大的改进(它比双列表解决方案快两倍)。它也不需要初始传递来获得奇数/偶数分裂。

public void DivideSubArray(int[] array)
{       
    int currentOdd=0;
    List<int> even = new List<int>(array.Length / 2);
    for (int i = 0; i < array.Length; i++)
    {
        if ((array[i] % 2) != 0)
        {
            even.Add(array[i]);
        }
        else
        {
            if (currentOdd != i)
                array[currentOdd++] = array[i];
            else
                currentOdd++;
        }
    }
    even.CopyTo(array, currentOdd);
}

注意列表的初始容量 - 正如Mooing Duck在下面的评论中提到的那样,我可以通过利用一些概率并选择稍高的值来进一步改进(假设平均会观察到大致均匀的分裂)。

也就是说,算法在偶数分割时执行速度最慢 - 如果有更多奇数项,那么它只是一堆交换。如果有更多的平均值,那么,是的,需要更多Add次操作,但它只会是一个列表调整大小会导致性能下降。

我的最后一次尝试是看看我是否能够实现izomorphius所建议的 - 以正确的顺序构建赔率,并且在没有额外数组的情况下反向或任何顺序均衡。如果那是可能的那么那个解决方案将是O(1)内存,但是O(n +(排序的复杂性)) - 如果它的性能,实际上,甚至是上述解决方案的一半,我可能会采用它。

答案 2 :(得分:0)

我认为没有任何'直接'的方法来分割列表而没有一端或另一端被扰乱,但是仍然可以有一个线性时间恒定空间解决方案。 izomorphius提供的分区方法将导致结束的右侧以相反的顺序(在线性时间内容易纠正),另一端以某种可预测的方式进行加扰,右侧的那些元素混合在一起,以相反的顺序,来自左边的那些。人们可以很容易地在恒定时间内识别给定元素是否来自右侧(只是将其与左侧的最后一项进行比较),然后可以很容易地线性时间反转已经从中移动的元素的序列。右侧到左侧。

一旦完成了这个,就会留下一个分区问题,这个问题非常像原版,但只有一半;唯一的区别是分裂标准是基于节点的值是大于还是小于“原始”最后一个元素,而不是它是偶数还是奇数。因此,可以基本上将原始算法应用于较小的数据集。由于可以预先确定分割的哪一侧将具有更多项目,因此可以放置分割以使剩余边不超过原始尺寸的一半。实际效果是,对大小为2N的数组进行分区所需的时间是分区大小为N的数组的O(1)倍。由于单个元素数组可以在恒定时间内完成(显然可以),这意味着< i>将一个任意大小的数组分割成两个任意混合的排序数据运行,分成两个不相交的排序数据,可以使用常量空间在线性时间内完成。

顺便提一下,虽然对整数无关紧要,但重要的是要注意上述算法依赖于比较两个元素并知道第一个属于第二个元素的左侧还是右侧的能力。因此,它不能用作稳定排序算法的基础。

答案 3 :(得分:0)

我可以试试这个帖子吗?我可以看到你说的是C#。我不懂语言,但我认为这对任务来说并不重要。

问题描述中缺少某些内容 - 排序数组来自哪里。可能我应该发表评论要求澄清,但我决定我会写一个答案,涵盖我能想到的所有可能性。希望这样,答案将在未来为更多人服务。

基本上,任务就是把我们放在一个盒子里:“你有一个数组,现在把它分开了”。但是,我想对这个数组的起源进行一些说明:

  • 案例1:从某处读取数组并在代码中排序(内存中)。如果是这种情况,那么从赔率中分割出平均值有一个优雅的解决方案,这不会带来任何开销:
    1. 确定赔率和平均数(通过数组O(n)单次传递)。
    2. 确定数组中的最大和最小数字。让我们称他们为MAXMMINM。这可以在第一遍中完成,以确定偶数和奇数。
    3. 再次通过数组,将MAXM - MINM + 1添加到每个奇数。目标是确保所有奇数都变得比平均值大。这是时间线性O(n)
    4. 使用kth_element算法拆分数组(基本上是快速排序关键分离的单次传递)。将奇怪的事实从奇怪的利用中分离出来,你已经知道每个人有多少,并且所有的赔率都大于所有的平均值。该算法以线性时间O(n)运行,但遗憾的是我只引用了C++ library implementation(没有C#)。
    5. 通过与奇数对应的所有数组插槽,并从每个数字中减去MAXM - MINM + 1以获得原始奇数。这也是时间线性O(n)
    6. 最后分别对比分和赔率进行排序。这不会增加整体分类的复杂性,但是你将把部分彼此分开。
  • 案例2:您已经读取了已经从某个持久存储中排序的数组,比如说硬盘上的文件,并且事先知道了赔率和平均数。
    1. 在这种情况下,您只需要在数组中输入数字:一个用于下一个跟随偶数,一个用于下一个跟随奇数。这个解决方案应该是显而易见的,它根本不会影响性能。
  • 案例3:您已经读取了已经从某个持久存储中排序的数组,比如硬盘上的文件,但事先并不知道赔率和赔率的数量。
    1. 从数组的开头和数组末尾的几率开始填充均衡。这样,最后两个序列将在中间相遇。
    2. 因此,您将以赔率形式分割,但奇数将按降序排列,而不是增加。你只需要在奇数部分(也是线性部分)的内部反向进行,你就拥有了所需的数组。

希望至少有一个所描述的场景适合您,您将能够使用其中的想法解决您的问题。

答案 4 :(得分:0)

我认为,可能存在O(n)时间和O(1)空间算法。但这对我们来说可能太复杂了。

我会通过以下方式说服你:

  1. 向您展示原始问题的特例,我们称之为A.
  2. 考虑问题A的反向,我们将其称为问题B.并且表明如果我们为其中任何一个得到O(n)时间和O(1)空间解,那么我们可以修改它以解决另一个问题。
  3. 我会告诉你问题B可以在O(n)时间和O(1)空间中解决,但解决方案非常复杂,需要大量数学。
  4. 所以这意味着,它不太可能轻松解决您的问题,否则我们可以轻松解决问题B.

    1.考虑一个特例:

    在这个例子中,设A [] = {1,2,3,4,5,6,7,8},从1到2n,n = 4。所以你想把它改成{1,3,5,7,2,4,6,8},对吧?我们将其称为问题A.一般来说,这意味着你有一个大小为2n的数组A,从A [1]到A [2n],你想要将它重新调整为A [1],A [3],A [ 5] ...,A [2N-1],A [2],A [4],A [6],A [2 N]。这是您的问题的一个特例。如果您能找到问题的解决方案,那么解决问题A就很容易了。

    2.问题A的反面。

    让我们考虑一个相关的问题。设B = {1,2,3,4,5,6,7,8},我们想将其改为{1,5,2,6,3,7,4,8}。这就像你有一副纸牌,你想做一个完美的洗牌,将它们分成两个相等的部分并交替合并它们。所以一般来说,你有一个大小为2n的数组B,从B [1]到B [2n]。你想把它重新调整为B [1],B [n + 1],B [2],B [n + 2],...... B [n],B [2n]。

    然后您将意识到问题A和问题B是反向操作。也就是说,对于一个大小为2n的数组,如果你用操作B做,然后用操作A做,那么它将成为原始数组,如果我们先做A然后A,它将是相同的。

    如果你对排列知识有所了解,你会知道如果我们得到A的算法,那么我们可以改变它以使其适用于B.如果你不熟悉这个,我可以稍后详细说明。 / p>

    3.问题B不容易解决。

    对于问题B,它是否存在O(n)时间和O(1)空间算法。确实如此,您可以在Computing the Cycles in the Perfect Shuffle Permutation查看它。这是一篇12页的论文,这意味着你不太可能在面试中提出这个解决方案。我已经阅读了它,它在数论中确实需要很多数学。而且它更像是一种理论解决方案。

    结论:

    似乎没有一个简单的(这意味着不需要10页纸)O(n)时间O(1)空间解决您的问题,即使对于问题A中的特殊情况。 否则我们可以对其进行修改以解决问题B.我不确定是否存在O(n)时间O(1)空间解决方案来解决您的广义问题。

    如果你真的对这个问题感兴趣的话。你可以看看Knuth的计算机编程艺术。有一章讨论原位置换。

    理解我的想法可能并不容易,所以如果您有任何疑问,请发表评论。

答案 5 :(得分:0)

// stable_partition.cpp
// example general inplace stable partition.

#include <algorithm>
#include <functional>
#include <iterator>
#include <iostream>
#include <vector>

template<typename Fwd, typename Pred>
  Fwd
  inplace_stable_partition(Fwd first, Fwd last, Pred pred)
  {
    ptrdiff_t nmemb = std::distance(first, last);

    if (nmemb == 1)
      return pred(*first) ? last : first;
    if (nmemb != 0)
      {
        Fwd split = first;
        std::advance(split, nmemb/2);

        first = inplace_stable_partition(first, split, pred);
        last = inplace_stable_partition(split, last, pred);

        std::rotate(first, split, last);
        std::advance(first, std::distance(split, last));
      }
    return first;
  }

int
main(int argc, char* argv[])
{
  using namespace std;

  vector<int> iv;
  for ( int i = 0; i < 10; i++ )
    iv.push_back(i);

  copy(iv.begin(), iv.end(), ostream_iterator<int>(cout, " "));
  cout << endl;

  inplace_stable_partition(iv.begin(), iv.end(), bind2nd(modulus<int>(), 2));

  copy(iv.begin(), iv.end(), ostream_iterator<int>(cout, " "));
  cout << endl;
  return 0;
}

答案 6 :(得分:-1)

看起来您正在寻找一种稳定的就地排序算法,该算法具有特殊的元素关系顺序(任何奇数小于任何偶数)。

鉴于此,我认为你不能比O(n ln n)更好。

我会去进行就地合并排序。

如果您不需要保留具有相同值的元素的顺序,请进行快速排序,这对于就地处理来说要简单得多(但是,对于数十亿个元素,这可能不太好适合)。