问题描述
是否存在具有线性时间复杂度和O(1)
辅助空间复杂度的排序算法来对正整数列表进行排序?我知道radix sort和counting 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 answer或elsewhere中。它的步复杂度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最坏的情况将有助于理解时间复杂度。