线性时间和就地排序

Posted

技术标签:

【中文标题】线性时间和就地排序【英文标题】:Sorting in linear time and in place 【发布时间】:2013-03-18 21:45:18 【问题描述】:

假设 n 条记录的键在 1 到 k 的范围内。

编写一个算法,在 O(n+k) 时间内对记录进行排序。 您可以在输入数组之外使用 O(k) 存储。 你的算法稳定吗?

如果我们使用计数排序,我们可以在 O(n+k) 时间内完成,并且稳定但不到位。 如果 k=2,它可以就地完成,但不稳定(使用两个变量来维护数组中 k=0 和 k=1 的索引) 但是对于 k>2,我想不出任何好的算法

【问题讨论】:

参见***条目中的Variant algorithms 部分(最后一段)。 "You may use O(k) storage outside the input array" - 听起来像是一种常规的计数排序,它可能属于“就地”的一些扭曲定义。您还可以使用递归和计数的负值(假设 k 我们需要在常规计数排序中存储 O(n+k)没有信息怎么做! 我想不出一个在 O(n+k) 中运行的稳定的就地排序。链接的 Wikipedia 文章中提到的变体算法依赖于使用临时值来“停放”每个序列元素。引用的文本(Java/C++/?? 中的算法)表明可以在线性时间内以这种方式对序列进行稳定排序,但随后提供了一种非渐近线性的算法。文本中的就地排列依赖于每个元素的最终位置已经预先计算,这显然需要 O(n) 额外的空间。 【参考方案1】:

首先,让我们重新讨论计数排序的工作原理:

计算每个键在要排序的数组中出现的频率。这些计数被写入大小为k 的数组。 计算键计数的部分总和。这给出了排序数组中每个相等键的起始位置。 将数组中的项目移动到它们的最终位置,增加每个项目的相应 bin 的起始位置。

现在的问题是如何就地执行最后一步。就地排列的标准方法是选择第一个元素并将其与占据正确位置的元素交换。对交换的元素重复此步骤,直到我们找到属于第一个位置的元素(一个循环已完成)。然后对第二、第三等位置的元素重复整个过程,直到处理完整个数组。

计数排序的问题在于最终位置不是现成的,而是通过在最终循环中递增每个 bin 的起始位置来计算的。为了永远不会将元素的起始位置增加两次,我们必须找到一种方法来确定某个位置的元素是否已经被移动到那里。这可以通过跟踪每个箱的原始起始位置来完成。如果某个元素位于原始起始位置和 bin 下一个元素的位置之间,则该元素已经被触摸过。

这是 C99 中的一个实现,它在 O(n+k) 中运行,并且只需要两个大小为 k 的数组作为额外的存储空间。最后的置换步骤不稳定。

#include <stdlib.h>

void in_place_counting_sort(int *a, int n, int k)

    int *start = (int *)calloc(k + 1, sizeof(int));
    int *end   = (int *)malloc(k * sizeof(int));

    // Count.
    for (int i = 0; i < n; ++i) 
        ++start[a[i]];
    

    // Compute partial sums.
    for (int bin = 0, sum = 0; bin < k; ++bin) 
        int tmp = start[bin];
        start[bin] = sum;
        end[bin]   = sum;
        sum += tmp;
    
    start[k] = n;

    // Move elements.
    for (int i = 0, cur_bin = 0; i < n; ++i) 
        while (i >= start[cur_bin+1])  ++cur_bin; 
        if (i < end[cur_bin]) 
            // Element has already been processed.
            continue;
        

        int bin = a[i];
        while (bin != cur_bin) 
            int j = end[bin]++;
            // Swap bin and a[j]
            int tmp = a[j];
            a[j] = bin;
            bin = tmp;
        
        a[i] = bin;
        ++end[cur_bin];
    

    free(start);
    free(end);

编辑:这是另一个版本,根据 Mohit Bhura 的方法,仅使用一个大小为 k 的数组。

#include <stdlib.h>

void in_place_counting_sort(int *a, int n, int k)

    int *counts = (int *)calloc(k, sizeof(int));

    // Count.
    for (int i = 0; i < n; ++i) 
        ++counts[a[i]];
    

    // Compute partial sums.
    for (int val = 0, sum = 0; val < k; ++val) 
        int tmp = counts[val];
        counts[val] = sum;
        sum += tmp;
    

    // Move elements.
    for (int i = n - 1; i >= 0; --i) 
        int val = a[i];
        int j   = counts[val];

        if (j < i) 
            // Process a fresh cycle. Since the index 'i' moves
            // downward and the counts move upward, it is
            // guaranteed that a value is never moved twice.

            do 
                ++counts[val];

                // Swap val and a[j].
                int tmp = val;
                val  = a[j];
                a[j] = tmp;

                j = counts[val];
             while (j < i);

            // Move final value into place.
            a[i] = val;
        
    

    free(counts);

【讨论】:

我相信后一种算法是哈登循环排序。【参考方案2】:

这是我在 O(n+k) 时间内运行的代码,并且只使用了 1 个额外的大小为 k 的数组(除了大小为 n 的主数组)

#include <stdio.h>
#include <string.h>

#include <stdlib.h>


int main(int argc, char const *argv[])

int n = atoi(argv[1]);
int k = atoi(argv[2]);

printf("%d\t%d",n,k);

int *a,*c;
int num,index,tmp,i;
a = (int*)malloc(n*sizeof(int));
c = (int*)calloc(k,sizeof(int));

srand(time(NULL));

for(i=0;i<n;i++)

    num =  (rand() % (k));
    a[i] = num;
    c[num]++;


printf("\n\nArray is : \n");
for(i=0;i<n;i++)

    printf("\t%d",a[i]);
    if(i%8==7)
        printf("\n");


printf("\n\nCount Array is : \n");
for(i=0;i<k;i++)

    printf("\t%d(%d)",c[i],i);
    if(i%8==7)
        printf("\n");


//Indexing count Array
c[0]--;
for(i=1;i<k;i++)

    c[i] = c[i-1] + c[i];       


printf("\n\nCount Array After Indexing is : \n");
for(i=0;i<k;i++)

    printf("\t%d(%d)",c[i],i);
    if(i%8==7)
        printf("\n");
 

// Swapping Elements in Array
for(i=0;i<n;i++)

    index = c[a[i]];
    //printf("\na[%d] = %d, going to position %d",i,a[i],index);
    c[a[i]]--;
    if(index > i)
    
        tmp = a[i];
        a[i] = a[index];
        a[index] = tmp;
        i--;
    


printf("\n\n\tFinal Sorted Array is : \n\n");
for(i=0;i<n;i++)

    printf("\t%d",a[i]);
    if(i%8==7)
        printf("\n");


printf("\n\n");

return 0;

即使这个算法也不稳定。所有元素的顺序相反。

P.s : 键的范围是 0 到 (k-1)

【讨论】:

我认为c[a[i]]--; 行属于以下if 语句。否则,这似乎是比我的方法更好的解决方案。 排序后的元素似乎没有按相反的顺序排列。 确实如此。假设x = a[i],当xis第一次遇到时,它去c[x],然后c[x]减1。所以当下次遇到x时,第二个x将去将一个放在第一个之前。 运行测试 - 它没有【参考方案3】:

整数值序列的示例。排序不稳定。虽然它不像 Mohit 提供的答案那么简洁,但通过跳过已经在正确 bin 中的元素(时间渐近相同),它的速度略快(对于 k

def sort_inplace(seq):
    min_ = min(seq)
    max_ = max(seq)
    k = max_ - min_ + 1
    stop = [0] * k
    for i in seq:
        stop[i - min_] += 1
    for j in range(1, k):
        stop[j] += stop[j - 1]
    insert = [0] + stop[:k - 1]
    for j in range(k):
        while insert[j] < stop[j] and seq[insert[j]] == j + min_:
            insert[j] += 1
    tmp = None
    for j in range(k):
        while insert[j] < stop[j]:
            tmp, seq[insert[j]] = seq[insert[j]], tmp
            while tmp is not None:
                bin_ = tmp - min_
                tmp, seq[insert[bin_]] = seq[insert[bin_]], tmp
                while insert[bin_] < stop[bin_] and seq[insert[bin_]] == bin_ + min_:
                    insert[bin_] += 1

使用更紧密的循环但仍会跳过已重定位的元素:

def dave_sort(seq):
    min_ = min(seq)
    max_ = max(seq)
    k = max_ - min_ + 1
    stop = [0] * k

    for i in seq:
        stop[i - min_] += 1

    for i in range(1, k):
        stop[i] += stop[i-1]
    insert = [0] + stop[:k - 1]

    for meh in range(0, k - 1):
        i = insert[meh]
        while i < stop[meh]:
            bin_ = seq[i] - min_
            if insert[bin_] > i:
                tmp = seq[insert[bin_]]
                seq[insert[bin_]] = seq[i]
                seq[i] = tmp
                insert[bin_] += 1
            else:
                i += 1

编辑:Mohit 在 Python 中的方法带有额外的位来验证对排序稳定性的影响。

from collections import namedtuple
from random import randrange

KV = namedtuple("KV", "k v")

def mohit_sort(seq, key):
    f = lambda v: getattr(v, key)
    keys = map(f, seq)
    min_ = min(keys)
    max_ = max(keys)
    k = max_ - min_ + 1
    insert = [0] * k

    for i in keys:
        insert[i - min_] += 1

    insert[0] -= 1
    for i in range(1, k):
        insert[i] += insert[i-1]

    i = 0
    n = len(seq)
    while i < n:
        bin_ = f(seq[i])
        if insert[bin_] > i:
            seq[i], seq[insert[bin_]] = seq[insert[bin_]], seq[i]
            i -= 1
        insert[bin_] -= 1
        i += 1


def test(n, k):
    seq = []
    vals = [0] * k
    for _ in range(n):
        key = randrange(k)
        seq.append(KV(key, vals[key]))
        vals[key] += 1
    print(seq)
    mohit_sort(seq, "k")
    print(seq)


if __name__ == "__main__":
    test(20, 3)

【讨论】:

【参考方案4】:

我真的不知道为什么这里发布的所有源代码都如此不必要地复杂化。 这是一个python解决方案:

def inplaceCtsort(A):
    b, e = min(A), max(A)
    nelems = e - b + 1
    CtsBeforeOrIn = [0]*nelems
    for i in A:
        CtsBeforeOrIn[i-b] += 1
    for i in range(1, nelems):
        CtsBeforeOrIn[i] += CtsBeforeOrIn[i-1]
    for i in range(0, len(A)):
        while i < CtsBeforeOrIn[A[i]-b] - 1:
            validPosition = CtsBeforeOrIn[A[i]-b] - 1
            A[i], A[validPosition] = A[validPosition], A[i]
            CtsBeforeOrIn[A[i]-b] -= 1

【讨论】:

以上是关于线性时间和就地排序的主要内容,如果未能解决你的问题,请参考以下文章

线性时间排序

线性时间排序

线性时间排序(python)

线性时间排序

算法导论笔记 第8章 线性时间排序

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