有没有线性时间复杂度和 O(1) 辅助空间复杂度的排序算法?

Posted

技术标签:

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

有没有线性时间复杂度和O(1)辅助空间复杂度的排序算法对正整数列表进行排序?我知道radix sort 和counting 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) 【参考方案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 的最坏情况有助于理解时间复杂度。

【讨论】:

评论不用于扩展讨论;这个对话是moved to chat。【参考方案3】:

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

或者,我们可以使用in place radix sort 和lg 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是列表中元素的个数。

【讨论】:

以上是关于有没有线性时间复杂度和 O(1) 辅助空间复杂度的排序算法?的主要内容,如果未能解决你的问题,请参考以下文章

算法的时间复杂度和空间复杂度

时间复杂度和空间复杂度计算

1.线性表

交换与选择类排序

常用的排序算法的时间复杂度和空间复杂度

各个排序算法的时间复杂度和空间复杂度