检查数组是否已排序的最快方法

16

考虑有一个非常大的数组从一个函数返回。

如何最快速地测试数组是否已排序?

最简单的方法是:

/// <summary>
/// Determines if int array is sorted from 0 -> Max
/// </summary>
public static bool IsSorted(int[] arr)
{
for (int i = 1; i < arr.Length; i++)
{
    if (arr[i - 1] > arr[i])
    {
    return false;
    }
}
return true;
}

只有连续元素吗? - huseyin tugrul buyukisik
2
如果你想要比O(n)更好的算法,你就必须容忍一些错误。 - barak1412
另一种选择可能是将列表包装在流中(假设您知道数组已经打包,就像您示例中的int []一样),并每次读取sizeof(int)字节,与先前读取的字节进行比较。这将节省您递增索引计数器的时间,但不确定它对缓存会产生什么影响。 - user244343
9个回答

22

您需要访问数组的每个元素以查看是否有未排序的内容。

如果没有关于数组可能状态的特殊知识,那么您的O(n)方法就是最快的了。

您的代码专门测试数组是否已排序,并且较小的值在较低的索引中。如果这不是您的意图,那么您的if语句会稍微复杂一些。您的代码注释建议您追求这种情况。

如果您具有对可能状态的特殊知识(例如,您知道它通常已排序,但新数据可能添加到末尾),则可以优化访问数组元素的顺序,以便在数组未排序时更快地失败检查。

您可以利用硬件架构的知识通过对数组进行分区来并行检查多个数组部分,首先比较分区的边界(快速失败检查),然后在单独的线程上运行一个数组分区(每个CPU核心不超过1个线程)。请注意,如果数组分区比缓存行的大小小得多,则线程将相互竞争以访问包含数组的内存。对于相当大的数组,多线程才能非常高效。


5
更快的方法,平台目标:任何CPU,优先选择32位。
具有512个元素的排序数组:速度提升约25%。
static bool isSorted(int[] a)
{
    int j = a.Length - 1;
    if (j < 1) return true;
    int ai = a[0], i = 1;
    while (i <= j && ai <= (ai = a[i])) i++;
    return i > j;
}

目标:x64,同一数组:速度提升约40%。

static bool isSorted(int[] a)
{
    int i = a.Length - 1;
    if (i <= 0) return true;
    if ((i & 1) > 0) { if (a[i] < a[i - 1]) return false; i--; }
    for (int ai = a[i]; i > 0; i -= 2)
        if (ai < (ai = a[i - 1]) || ai < (ai = a[i - 2])) return false;
    return a[0] <= a[1];
}

我忘了一个,这个比我的第一段代码略慢。

static bool isSorted(int[] a)
{
    int i = a.Length - 1; if (i < 1) return true;
    int ai = a[i--]; while (i >= 0 && ai >= (ai = a[i])) i--;
    return i < 0;
}

测量它(见greybeard的评论)。
using System;                                  //  ????????? DEBUG ?????????
using sw = System.Diagnostics.Stopwatch;       //  static bool abc()    
class Program                                  //  {   // a <= b <= c ?  
{                                              //      int a=4,b=7,c=9;  
    static void Main()                         //      int i = 1;  
    {                                          //      if (a <= (a = b))  
        //abc();                               //      {  
        int i = 512;                           //          i++;  
        int[] a = new int[i--];                //          if (a <= (a = c))
        while (i > 0) a[i] = i--;              //          {    
        sw sw = sw.StartNew();                 //              i++;  
        for (i = 10000000; i > 0; i--)         //          }  
            isSorted(a);                       //      }  
        sw.Stop();                             //      return i > 2;  
        Console.Write(sw.ElapsedMilliseconds); //  }  
        Console.Read();                        //  static bool ABC();
    }                                          //  {
                                               //      int[]a={4,7,9};    
    static bool isSorted(int[] a) // OP Cannon //      int i=1,j=2,ai=a[0]; 
    {                                          //  L0: if(i<=j)    
        for (int i = 1; i < a.Length; i++)     //        if(ai<=(ai=a[i]))  
            if (a[i - 1] > a[i]) return false; //          {i++;goto L0;}  
        return true;                           //      return i > j;  
    }                                          //  }  
}

目标:x64架构。四核线程。 一个包含十万个元素的已排序数组:约为55%。

static readonly object _locker = new object();
static bool isSorted(int[] a)  // a.Length > 3
{
    bool b = true;
    Parallel.For(0, 4, k =>
    {
        int i = 0, j = a.Length, ai = 0;
        if (k == 0) { j /= 4; ai = a[0]; }                        // 0 1
        if (k == 1) { j /= 2; i = j / 2; ai = a[i]; }             // 1 2
        if (k == 2) { i = j - 1; ai = a[i]; j = j / 2 + j / 4; }  // 4 3
        if (k == 3) { i = j - j / 4; ai = a[i]; j = j / 2; }      // 3 2
        if (k < 2)
            while (b && i <= j)
            {
                if (ai <= (ai = a[i + 1]) && ai <= (ai = a[i + 2])) i += 2;
                else lock (_locker) b = false;
            }
        else
            while (b && i >= j)
            {
                if (ai >= (ai = a[i - 1]) && ai >= (ai = a[i - 2])) i -= 2;
                else lock (_locker) b = false;
            }
    });
    return b;
}

100万个物品?

if (k < 2)
    while (b && i < j)
        if (ai <= (ai = a[i + 1]) && ai <= (ai = a[i + 2]) &&
            ai <= (ai = a[i + 3]) && ai <= (ai = a[i + 4])) i += 4;
        else lock (_locker) b = false;
else
    while (b && i > j)
        if (ai >= (ai = a[i - 1]) && ai >= (ai = a[i - 2]) &&
            ai >= (ai = a[i - 3]) && ai >= (ai = a[i - 4])) i -= 4;
        else lock (_locker) b = false;

让我们忘记百分比。
原始数据:每个项目0.77纳秒,现在:每个项目0.22纳秒。
200万个项目?四个核心:速度提高4倍。


1
这个能检测到倒置吗?(我_认为_ 1)它可以 2)不太明显)。请披露一下您测量的方法。 - greybeard
这个程序能检测到逆序吗?好问题!维基百科上说: 设(A(1),...,A(n))是一个由n个不同数字组成的序列。 如果i < j且A(i) > A(j),那么对(A)来说,(i,j)就是一个逆序对。
  • 如果该序列有重复数字,则并非所有数字都是不同的。
  • 如果一对(i,j)被isSorted使用,其中j = i + 1,并且A(i) > A(j), 则该序列未排序,是的,在一个不一定包含不同数字的序列中,检测到了一个逆序对,我们就完成了。 因此它不能检测到多个逆序,但如果它检测到一个逆序,那么我们就知道该序列未排序。 我添加了一个代码块“测量它”。
- P_P
(不要评论要求额外信息或澄清的评论:编辑您的帖子。使我发表评论/我还没有找到惯用语的部分:ai <= (ai = a[i])(以及类似的内容)。) - greybeard
2
你确定这些时间是在没有附加调试器的 Release 构建中记录的吗?如果调试器已经附加(即在 Visual Studio 中按下 F5),那么你的时间不反映代码在生产环境中的运行情况。如果你想要真实的时间,请从“调试”菜单中选择“无调试运行”。 - Jim Mischel
1
是的,我很确定。我的时间记录是使用Release构建(优化代码“on”)完成的。我刚在一台较慢的笔记本电脑上检查了它。对于具有512个元素的排序数组的结果:任何CPU,首选32位:24%,x64:35%。简而言之:在您的系统上运行它,检查我的方法是否更快(或不是)。 - P_P
你不需要锁,volatileThread.MemoryBarrier 就足够了。实际上,在某个线程已经将 false 设置后读取 true 不会导致算法给出错误的结果,并且这可能是不需要特定顺序的良好权衡。 - Steves

1

Linq解决方案。

public static bool IsSorted<T>(IEnumerable<T> list) where T:IComparable<T>
{
    var y = list.First();
    return list.Skip(1).All(x =>
    {
        bool b = y.CompareTo(x) < 0;
        y = x;
        return b;
    });
}

3
原文:The OP asked What will be the fastest approach to test if the array is sorted?. This solution is at best as fast as the OP's solution, and is probably a little slower. Linq may be the new black, but it's not the best solution all of the time.翻译:楼主问:“如何最快地测试数组是否已排序?” 这个解决方案最快与楼主的方案速度相当,甚至可能稍微慢一些。Linq也许是新宠,但它并不总是最佳解决方案。 - Eric J.
3
如果传递的“Enumerable”是一个只能迭代一次的集合,实际上这种方法可能会失败。例如,如果有人写了“x = IsSorted(File.ReadLines("filename.txt"))”,那么该方法将失败,因为它需要对“Enumerable”进行多次枚举。 - Jim Mischel

1

这是我的函数IsSorted的版本

public static bool IsSorted(int[] arr)
{               
    int last = arr.Length - 1;
    if (last < 1) return true;

    int i = 0;

    while(i < last && arr[i] <= arr[i + 1])
        i++;

    return i == last;
}

虽然这个函数比问题中的函数稍微快一些,但它将执行的赋值和比较操作比已发布的任何内容都要少。在最坏的情况下,它执行2n+1次比较。如果您可以对数据的性质做出合理的假设,例如最小数据大小或数组包含偶数个元素,则仍然可以进行改进。


0
我能想到的唯一改进是同时检查数组的两端,这个小改变将使其时间减半...
public static bool IsSorted(int[] arr)
{
int l = arr.Length;
for (int i = 1; i < l/2 + 1 ; i++)
{
    if (arr[i - 1] > arr[i] || arr[l-i] < arr[l-i-1])
    {
    return false;
    }
}
return true;
}

7
你如何表达这个算法可以在一半的时间内执行?实际上,你需要做相同数量的比较(l/2 * 2即l)。 - Senthil Babu
好的,我明白你的意思。无论如何,如果有什么问题,它可能会更快地发现它,特别是当错误出现在数组的第一或第四个季度时。 - saul672
5
如果未排序的部分在中间,那么稍后你将会发现它。这样做增加了复杂性但没有增加价值。而且,从两端检查降低了访问数据时CPU缓存命中的可能性(取决于数组大小和底层架构)。 - Eric J.
1
更不用说由于缓存行为的影响,这很可能会变得更慢。 - Andrej Bauer
@AndrejBauer,您能详细说明一下缓存行为吗? - Natalie Perret
嗯,我不是CPU缓存方面的专家,但通常以良好有序的方式获取内存更好,因为CPU有很多启发式算法来猜测哪些内存块应该被预取。线性获取内存可能会有更好的行为。无论如何,进行一些实验应该不难。(不是由我来做,我对此没有利益关系。) - Andrej Bauer

0

这是我想出来的,而且发现在处理更大的数组时效果更好。该函数是递归的,并且将在第一次调用时被调用,比如在一个 while 循环中像这样:

while( isSorted( yourArray, 0 )

if语句检查数组的边界是否已经到达。

else if语句将在条件变为false时递归调用自身并中断。

 public static bool IsSorted(int[] arr, int index)
    {
        if (index >= arr.Length - 1)
        {
            return true;
        }
        else if ((arr[index] <= arr[ index + 1]) && IsSorted(arr, index + 1))
        {
            return true;
        }
        else
        {
            return false;
        }
    }

这是我发现更好的方法。a)比什么更好?(比问题中提供的代码更好吗?)b)在哪方面更好:我该如何重现你的发现? - greybeard
抱歉,Michael。但是:1)你测试过你的解决方案吗?int[] arr = new int[]{0,0},isSorted(arr,0)返回false(将“<”改为“<=”)。2)在我的电脑上,它比OP的解决方案(要慢得多)。3)使用一个大数组(“int[] arr = new int[1000000]”)会导致堆栈溢出。 - P_P
嗨@greybeard,是的,我指的是问题中提出的代码,特别是关于更大数组的执行时间。我已经尝试了一些随机数据的样本。如果您发现有其他情况,请告诉我。 - Michael Dera
@P_P 谢谢你提醒我,我不知道相邻元素相等的情况。我已经测试了这个解决方案,并发现它在处理大数据集时更快,但我会进一步测试它。 - Michael Dera

0

如果顺序不重要(降序或升序)。

private bool IsSorted<T>(T[] values) where T:IComparable<T>
{
    if (values == null || values.Length == 0) return true;

    int sortOrder = 0;

    for (int i = 0; i < values.Length - 1; i++)
    {
        int newSortOrder = values[i].CompareTo(values[i + 1]);

        if (sortOrder == 0) sortOrder = newSortOrder;

        if (newSortOrder != 0 && sortOrder != newSortOrder) return false;
    }

    return true;
}

-1

这可能不是最快的解决方案,但它是完整的解决方案。每个具有小于i的索引的值都与在i处的当前值进行比较。这是用php编写的,但可以轻松地转换为c#或javascript。

for ($i = 1; $i < $tot; $i++) {
        for ($j = 0; $j <= $i; $j++) {
            //Check all previous values with indexes lower than $i
            if ($chekASCSort[$i - $j] > $chekASCSort[$i]) {
                return false;
            }
        }
    }

1
请尝试估算随着tot的增长,比较次数的渐近增长阶。利用顺序关系具有传递性的机会是否存在? - greybeard
我同意当tot很大时比较次数会增加,但是我们不能假设序列是可传递的,因为我们不知道序列顺序是什么,如果它是可传递的(根据定义),则假定它已经处于某种类型的顺序中。 - atomCode
1
如果您不能假设序列顺序是传递的,那么首先无法进行排序。从一般意义上讲,排序意味着传递性。 - Jim Mischel

-1
我脑海中浮现的问题是“为什么”?
是为了避免重新排序已经排序好的列表吗?如果是,只需使用Timsort(Python和Java中的标准)。它非常擅长利用数组/列表已经排序或几乎排序的情况。尽管Timsort在这方面非常出色,但最好不要在循环内部进行排序。
另一种选择是使用本质上已排序的数据结构,例如treap、红黑树或AVL树。这些都是在循环内部排序的良好替代品。

1
天生排序的数据结构仍然需要花费成本来进行排序。这只是在各种添加/插入/删除调用之间分散开来。这可能是好事,也可能不是,具体取决于使用情况。 - Eric J.
1
除了初始种群外,我会说你不需要对treap进行排序。在循环中更新treap并不是排序 - 它是保持排序的状态,这比Timsort还要便宜。 - user1277476

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接