有没有线性时间复杂度和 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 <= 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
假设输入由一些对象组成,这些对象可以由0
到k - 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) 辅助空间复杂度的排序算法?的主要内容,如果未能解决你的问题,请参考以下文章