【问题标题】:Is there a sorting algorithm with linear time complexity and O(1) auxiliary space complexity?有没有线性时间复杂度和 O(1) 辅助空间复杂度的排序算法?
【发布时间】:2020-12-13 03:33:49
【问题描述】:

有没有线性时间复杂度和O(1)辅助空间复杂度的排序算法对正整数列表进行排序?我知道radix sortcounting sort 具有线性时间复杂度(如果我们以k 为常数,则分别为O(kn)O(n+k)),但它们都具有O(n+k) 辅助空间复杂度。一个排序是否有可能同时具有这两个属性?此类示例将不胜感激。

【问题讨论】:

  • 这取决于您所说的“排序算法”是什么意思。基数排序和计数排序比基于比较的排序算法更多地假设数组的内容,因此适用于较少的排序问题。例如,如果您只想对包含从 1 到 n 的数字的混洗数组进行排序,那么您可以在线性时间和恒定空间中执行此操作,但这算作排序吗?
  • 记住要记住,你在“线性时间复杂度”中计算的是什么 - 这通常是比较的次数 except 对于像基数排序和计数排序这样的东西 -计算您的特定数据可能不是真正正确的事情。例如,对于某些数据,比较可能比复制/移动/交换便宜得多 - 并且就地排序 - 这是您在 O(1) 辅助空间复杂度下所要求的 - 最终可能会花费您很多由于需要复制/移动/交换而需要更多时间...我的意思是,如果您计算一下,不仅在实践中,而且在理论上的复杂性...
  • @kaya3 我明白你的意思。现在让我们假设列表只包含正整数。
  • 对于基于比较的排序,您所要求的已被证明是不可能的。不过我不知道在哪里可以找到这些证明。
  • @MarkRansom 一个简单的证明是考虑到,对长度为 n 的所有可能输入(其中有 n!)进行排序;每次将n! 分成两半的比较排序至少需要log2(n!) 步骤才能正确(=正确排序所有可能的输入),即O(n log n)

标签: algorithm sorting time-complexity space-complexity


【解决方案1】:

我想在这里包含一个算法,它是对 Mathphile 的第一个答案的改进。在这种情况下,想法是从输入的未排序后缀中的每个数字中删除1(同时将排序的数字交换到前缀中)。每当未排序后缀中的数字达到 0 时,这意味着它小于未排序后缀中的任何其他数字(因为所有数字都以相同的速度减少)。

可能有一个重大改进:在不改变时间复杂度的情况下,我们可以减去比1 大得多的数字——事实上,我们可以减去一个等于最小的剩余未排序项的数字。这使得无论数组项的数字大小和浮点值如何,这种排序都能很好地运行!一个javascript实现:

let subtractSort = arr => {
  
  let sortedLen = 0;
  let lastMin = 0; // Could also be `Math.min(...arr)`
  let total = 0;
  while (sortedLen < arr.length) {
    
    let min = arr[sortedLen];
    for (let i = sortedLen; i < arr.length; i++) {
      
      if (arr[i]) {
        
        arr[i] -= lastMin;
        if (arr[i]) min = Math.min(min, arr[i]);
        
      } else {
        
        arr[i] = arr[sortedLen];
        arr[sortedLen] = total;
        sortedLen++;
        
      }
      
    }
    
    total += lastMin;
    lastMin = min;
    
  }
  return arr;
  
};

let examples = [
  [ 3, 2, 5, 4, 8, 5, 7, 1 ],
  [ 3000, 2000, 5000, 4000, 8000, 5000, 7000, 1000 ],
  [ 0.3, 0.2, 0.5, 0.4, 0.8, 0.5, 0.7, 0.1 ],
  [ 26573726573, 678687, 3490, 465684586 ]
];
for (let example of examples) {
  console.log(`Unsorted: ${example.join(', ')}`);
  console.log(`Sorted:   ${subtractSort(example).join(', ')}`);
  console.log('');
}

请注意,这种排序仅适用于正数。要处理负数,我们需要找到最负的项目,从数组中的每个项目中减去这个负值,对数组进行排序,最后将最负的值加回每个项目 - 总体而言,这不会增加时间复杂度.

【讨论】:

    【解决方案2】:

    这是一个排序算法,它具有线性时间复杂度和 O(1) 辅助空间复杂度。我称之为减法排序。这是 C 中的代码(可运行 here)。

    // Subtract Sort
    #include<stdio.h>
    int print_arr(int arr[], int n)
    {
      int z;
      for(z=0 ; z<n ; z++)
      {
        printf("%d ", arr[z]);
      }
    }
    void subtract_sort(int arr[], int arr_size)
    {
      int j=0;
      int val=1;
      int all_zero=0;
      while(!all_zero)
      {
        int m;
        all_zero=1;
        int i;
        for(i=j ; i<arr_size ; i++)
        {
          arr[i]--;
          if(arr[i]==0)
          {
            arr[i]=arr[j];
            arr[j]=val;
            j++;
          }
          all_zero=0;
        }
        val++;
      }
    }
    int main()
    {
      int arr[12]={2,10,3,7,9,8,54,3,9,38,8};
      int size=11;
      subtract_sort(arr, size);
      printf("\n--------------------------\n");
      print_arr(arr, size);
      return 0;
    }
     
    

    该算法的最坏情况时间复杂度为O(kn),其中k 是数组的最大元素。该算法对于包含小值的大数组很有效(优于快速排序),但对于包含大值的小数组非常低效。时间复杂度也正好等于sum(arr),它是数组中所有元素的总和。

    对于那些说这个算法没有线性时间的人,给我找一个超过我计算的最坏情况时间复杂度O(kn)的数组。如果找到这样的反例,我很乐意同意你的看法。

    也许this example 的最坏情况有助于理解时间复杂度。

    【讨论】:

    【解决方案3】:

    如果我们只对整数进行排序,我们可以使用计数排序的原位变体,其空间复杂度为O(k),与变量n 无关。换句话说,当我们将k 视为常数时,空间复杂度为O(1)

    或者,我们可以使用in place radix sortlg k 的二元分区阶段和O(lg k) 空间复杂度(由于递归)。甚至更少的阶段使用计数排序来确定 n 路分区的桶边界。这些解决方案的时间复杂度为 O(lg k * n),仅以变量 n 表示时为 O(n)(当 k 被视为常数时)。

    k 被认为是常数时,获得O(n) 步复杂度和O(1) 空间复杂度的另一种可能的方法是使用可以称为减法排序的东西,正如OP 在他们的own answer 中所描述的那样,或elsewhere。它具有步复杂度O(sum(input)),优于O(kn)(对于某些特定的输入,它甚至优于二进制基数排序的O(lg k * n),例如对于[k, 0, 0, ... 0] 形式的所有输入)和空间复杂度O(1) .

    另一种解决方案是使用bingo sort,其步长复杂度为O(vn),其中v &lt;= k 是输入中唯一值的数量,空间复杂度为O(1)

    请注意,这些排序解决方案都不是稳定的,如果我们排序的不仅仅是整数(一些具有整数键的任意对象),这很重要。

    paper 中还描述了一种尖端的稳定分区算法,空间复杂度为O(1)。将其与基数排序相结合,可以构造出一种具有恒定空间的稳定线性排序算法——O(lg k * n)步复杂度和O(1)空间复杂度。


    编辑:

    根据评论的要求,我试图找到计数排序的“原位”变体的来源,但没有找到任何可以链接到的优质内容(真的很奇怪对于这样的基本算法,没有容易获得的描述)。因此,我在这里发布算法:

    常规计数排序(来自维基百科)

    count = array of k+1 zeros
    for x in input do
        count[key(x)] += 1
    
    total = 0
    for i in 0, 1, ... k do
        count[i], total = total, count[i] + total
    
    output = array of the same length as input
    for x in input do
        output[count[key(x)]] = x
        count[key(x)] += 1 
    
    return output
    

    假设输入由一些对象组成,这些对象可以由0k - 1 范围内的整数键标识。它使用O(n + k) 额外空间。

    整数的简单原位变体

    这个变体要求输入是纯整数,而不是带有整数键的任意对象。它只是从计数数组中重构输入数组。

    count = array of k zeros
    for x in input do
        count[x] += 1
    
    i = 0
    for x in 0, 1, ... k - 1 do
        for j in 1, 2, ... count[x] do
            input[i], i = x, i + 1
    
    return input
    

    它使用O(k) 额外空间。

    具有整数键的任意对象的完整原位变体

    这个变体类似于常规变体接受任意对象。它使用交换将对象放置在适当的位置。在前两个循环中计算 count 数组后,它使其保持不变,并使用另一个名为 done 的数组来跟踪有多少具有给定键的对象已经放置在正确的位置。

    count = array of k+1 zeros
    for x in input do
        count[key(x)] += 1
    
    total = 0
    for i in 0, 1, ... k do
        count[i], total = total, count[i] + total
    
    done = array of k zeros
    for i in 0, 1, ... k - 1 do
        current = count[i] + done[i]
        while done[i] < count[i + 1] - count[i] do
            x = input[current]
            destination = count[key(x)] + done[key(x)]
            if destination = current then
                current += 1
            else
                swap(input[current], input[destination])
            done[key(x)] += 1 
    
    return input
    

    此变体不稳定,因此不能用作基数排序中的子程序。它使用O(2k) = O(k) 额外空间。

    【讨论】:

    • “考虑k常量”是作弊。这样,你可以做任何事情 O(1)。目前尚不清楚k 究竟是什么,但在我看来,OP 非常清楚这不是一个常数。
    • 是的,这里的 k 不是常数,会根据输入而变化。因此我不会考虑这个 O(1) 时间。
    • @anatolyg 实际上,经典的 big-Oh 表示法仅针对一个变量定义(请参阅 cs.stackexchange.com/questions/3149/what-is-the-meaning-of-omn/…),关于多变量 big-Oh 的真正含义,我们进行了完整的讨论。跨度>
    • @Mathphile 好的,我明白了。那么,大概的答案就是没有这样的算法,但是现在还不能证明。
    • 另外,请注意我的回答提供了一些实用的见解:例如我们可以在线性时间和常数空间中对 64 位整数进行排序,因为所有可能的 64 位整数中只有一个常数。
    【解决方案4】:

    这是另一个排序算法的例子,它具有线性时间复杂度(如果k 被视为常数),O(1) 辅助空间复杂度,并且也是稳定的。这是我写的用户 ciamej 在他的回答中提到的“二进制基数排序”的实现。我在互联网上找不到任何满足所有 3 个属性的算法实现,这就是为什么我觉得在这里添加它是个好主意。你可以试试here

    // Binary Radix Sort
    #include<stdio.h>
    #include<math.h>
    int print_arr(int arr[], int n)
    {
      int z;
      for(z=0 ; z<n ; z++)
      {
        printf("%d ", arr[z]);
      }
      printf("\n");
    }
    int getMax(int arr[], int n) 
    { 
      int mx = arr[0]; 
      for (int i = 1; i < n; i++) 
          if (arr[i] > mx) 
              mx = arr[i]; 
      return mx; 
    } 
    void BinaryRadixSort(int arr[], int arr_size)
    {
      int biggest_int_len = log2(getMax(arr, arr_size))+1;
      int i;
      int digit;
      for(i=1 ; i<=biggest_int_len ; i++)
      {
        digit=i;
        int j;
        int bit;
        int pos=0;
        int min=-1;
        int min2=-1;
        int min_val;
        for(j=0 ; j<arr_size ; j++)
        {
          int len=(int) (log2(arr[j])+1);
          if(i>len)
          {
            bit=0;
          }
          else
          {
            bit=(arr[j] & (1 << (digit - 1)));
          }
          if(bit==0)
          {
            min_val=arr[j];
            min=j;
            min2=j;
            break;
          }
        }
        while(min!=-1)
        {
          while(min>pos)
          {
            arr[min]=arr[min-1];
            min--;
          }
          arr[pos]=min_val;
          pos++;
          int k;
          min=-1;
          for(k=min2+1 ; k<arr_size ; k++)
          {
            int len=(int) (log2(arr[k])+1);
            if(i>len)
            {
              bit=0;
            }
            else
            {
              bit= arr[k] & (1 << (digit-1));
            }
            if(bit==0)
            {
              min_val=arr[k];
              min=k;
              min2=k;
              break;
            }
          }
        }
      }
    }
    int main()
    {
      int arr[16]={10,43,73,14,64,2,6,1,5,3,6,3,5,8,4,5};
      int size=16;
      BinaryRadixSort(arr, size);
      printf("\n--------------------------\n");
      print_arr(arr, size);
      return 0;
    }
    

    该算法的时间复杂度为O(log2(k).n),其中k是列表中最大的数字,n是列表中元素的个数。

    【讨论】:

      猜你喜欢
      • 2015-05-25
      • 1970-01-01
      • 2019-10-08
      • 1970-01-01
      • 1970-01-01
      • 2012-08-14
      • 2020-06-24
      • 1970-01-01
      • 2021-01-23
      相关资源
      最近更新 更多