是否存在具有线性时间复杂度和O1辅助空间复杂度的排序算法?

问题描述

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

解决方法

如果仅排序整数,则可以使用计数排序的原位变量,该变量具有O(k)空间复杂度,独立于变量n。换句话说,当我们将k视为常量时,空间复杂度为O(1)

或者,我们可以将in place radix sort与二进制分区的lg k个阶段配合使用,并具有O(lg k)的空间复杂度(由于递归)。甚至更少的阶段使用计数排序来确定n路分区的存储桶边界。这些解决方案具有O(lg k * n)的时间复杂度,当仅用变量n表示时,则为O(n)(当k被认为是常数时)。

O(n)被认为是常数时,获得O(1)步复杂度和k空间复杂度的另一种可能的方法是使用一种称为减法排序的方法,如OP所述。在他们的own answerelsewhere中。它的步复杂度O(sum(input))O(kn)好(并且对于某些特定输入,它甚至比二进制基数排序的O(lg k * n)好,例如,对于所有形式为{{1}的输入})和空间复杂度[k,... 0]

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

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

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


编辑:

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

常规计数排序(来自Wikipedia)

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 0范围内的整数键来标识。它使用k - 1多余的空间。

简单的整数变体

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

O(n + k)

它使用count = array of k zeros for x in input do count[x] += 1 i = 0 for x in 0,... k - 1 do for j in 1,2,... count[x] do input[i],i = x,i + 1 return input 的额外空间。

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

与常规变量类似,此变量接受任意对象。它使用交换将对象放置在适当的位置。在前两个循环中计算了O(k)数组后,它保持不变,并使用另一个名为count的数组来跟踪已经将多少个具有给定键的对象放置在正确的位置。 / p>

done

该变量不稳定,因此不能用作基数排序的子例程。它使用count = array of k+1 zeros for x in input do count[key(x)] += 1 total = 0 for i in 0,count[i] + total done = array of k zeros for i in 0,... 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 多余的空间。

,

我想在这里包括一个算法,它是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,5,4,8,7,1 ],[ 3000,2000,5000,4000,8000,7000,1000 ],[ 0.3,0.2,0.5,0.4,0.8,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('');
}

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

,

这是排序算法的另一个示例,该算法具有线性时间复杂度(如果将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,6,3,5};
  int size=16;
  BinaryRadixSort(arr,size);
  printf("\n--------------------------\n");
  print_arr(arr,size);
  return 0;
}

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

,

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

// Subtract Sort
#include<stdio.h>
int print_arr(int arr[],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,9,54,38,8};
  int size=11;
  subtract_sort(arr,size);
  return 0;
}
 

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

对于所有说这种算法没有线性时间的人来说,发现我的数组超出了我计算出的最坏情况下的时间复杂度O(kn)。如果找到这样的反例,我会很乐意与您同意。如果没有,那么至少不要对这个答案不满意。

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