有效地选择与列表的所有元素不同的整数

时间:2014-08-15 18:59:00

标签: c algorithm

我有一个对象的链表,每个对象都包含一个32位整数(可证明少于2个 32 这样的对象),我想有效地选择一个列表中不存在的整数,< strong>不使用任何额外的存储(因此将它们复制到数组,排序数组,并选择不在数组中的最小值将不是一个选项)。但是,列表元素的结构定义在我的控制之下,因此我可以(在合理范围内)为每个元素添加额外的存储,作为解决问题的一部分。例如,我可以添加一组额外的prev / next指针并对列表进行合并排序。这是最好的解决方案吗?或者有更简单或更有效的方法吗?

7 个答案:

答案 0 :(得分:8)

鉴于您在评论中列出的条件,特别是您对许多相同值的期望,您必须期望使用稀疏的已分配值。

因此,实际上最好只是随机猜测一个值,然后检查它是否与列表中的值一致。即使使用了一半的可用值范围(从您的评论中看起来极不可能),您也只能平均两次遍历列表。并且您可以通过同时检查一次通过的多个猜测来大幅减少此因素。如果做得恰当,因素应始终接近一。

这种概率方法的优点是你可以免受不良价值序列的影响。使用基于范围的方法始终可以使用此类序列:如果计算数据的最小值和最大值,则存在风险,即数据包含02^32-1。如果按顺序细分一个间隔,则存在始终在间隔中间获取值的风险,这可以在32个步骤中将其缩小为零。使用概率方法,这些序列不会伤害你。

我认为,对于非常小的列表,我会使用四次猜测,并且当列表的大小接近极限时,将其调整为大约16。高起始值是由于任何这样的算法将是存储器限制的事实,即。即你的CPU有足够的时间在等待下一个值从内存到达时检查一个值,所以你最好好好利用那段时间来减少所需的传递次数。

进一步的优化会立即用新的猜测替换已被破坏的猜测并跟踪替换发生的位置,这样您就可以避免完全通过数据。另外,将猜测的猜测移动到猜测列表的末尾,这样您只需要检查循环中第一个猜测的起始位置,以便尽早停止。

答案 1 :(得分:7)

如果你可以在每个对象中备用一个指针,那么你很容易得到一个O(n)最坏情况算法(标准分而治之):

  1. 平均分配可能的ID范围。
  2. 制作一个涵盖每个子范围的单链表。
  3. 如果一个子范围为空,请选择其中的任何ID。
  4. 否则重复使用最少元素的子范围元素。
  5. 每次迭代使用两个子范围的示例代码:

    unsigned getunusedid(element* h) {
        unsigned start = 0, stop = -1;
        for(;h;h = h->mainnext)
            h->next = h->mainnext;
        while(h) {
            element *l = 0, *r = 0;
            unsigned cl = 0, cr = 0;
            unsigned mid = start + (stop - start) / 2;
            while(h) {
                element* next = h->next;
                if(h->id < mid) {
                    h->next = l;
                    cl++;
                    l = h;
                } else {
                    h->next = r;
                    cr++;
                    r = h;
                }
                h = next;
            }
            if(cl < cr) {
                h = l;
                stop = mid - 1;
            } else {
                h = r;
                start = mid;
            }
        }
        return start;
    }
    

    更多评论:

    1. Beware of bugs in the above code; I have only proved it correct, not tried it.
    2. 使用更多的存储桶(最好保持2的幂,以便轻松高效地处理)每次迭代可能会更快,因为更好的数据位置(尽管只有尝试并测量它是否还不够快),如@MarkDickson rightly remarks
    3. 如果没有这些额外指针,您需要在每次迭代时完全扫描,将界限提高到O(n*lg n)
    4. 另一种选择是每个元素使用2+个额外指针来维护平衡树。这会加速id搜索,但会牺牲一些内存和插入/删除时间开销。

答案 2 :(得分:4)

我假设整数具有不受代码控制的随机值。

在列表类中添加两个无符号整数:

unsigned int rangeMinId = 0;
unsigned int rangeMaxId = 0xFFFFFFFF ;

或者,如果不可能更改List类,则将它们添加为全局变量。

当列表为空时,您将始终知道该范围是否空闲。在列表中添加新项目时,检查其ID是否在rangeMinId和rangeMaxId之间,如果是,则将它们中最近的一个更改为此ID。

可能会在很长一段时间后发生rangeMinId等于rangeMaxId-1,那么你需要一个简单的函数来遍历整个列表并搜索另一个自由范围。但这不会经常发生。

其他解决方案更复杂,涉及使用集合,二叉树或排序数组。

<强>更新

自由范围搜索功能可以在O(n * log(n))中完成。下面给出了这种功能的一个例子(我没有对它进行过广泛的测试)。该示例适用于整数数组,但很容易适用于列表。

int g_Calls = 0;

bool _findFreeRange(const int* value, int n, int& left, int& right)
{
    g_Calls ++ ;

    int l=left, r=right,l2,r2;
    int m = (right + left) / 2 ;
    int nl=0, nr=0;
    for(int k = 0; k < n; k++)
    {
        const int& i = value[k] ;

        if(i > l && i < r)
        {
            if(i-l < r-i)
                l = i;
            else
                r = i;
        }

        if(i < m)
            nl ++ ;
        else
            nr ++ ;

    }


    if ( (r - l) > 1 )
    {
        left = l;
        right = r;
        return true ;
    }

    if( nl < nr)
    {
        // check first left then right
        l2 = left;
        r2 = m;
        if(r2-l2 > 1 && _findFreeRange(value, n, l2, r2))
        {
            left = l2 ;
            right = r2 ;
            return true;
        }

        l2 = m;
        r2 = right;
        if(r2-l2 > 1 && _findFreeRange(value, n, l2, r2))
        {
            left = l2 ;
            right = r2 ;
            return true;
        }

    }

    else
    {
        // check first right then left
        l2 = m;
        r2 = right;
        if(r2-l2 > 1 && _findFreeRange(value, n, l2, r2))
        {
            left = l2 ;
            right = r2 ;
            return true;
        }

        l2 = left;
        r2 = m;
        if(r2-l2 > 1  && _findFreeRange(value, n, l2, r2))
        {
            left = l2 ;
            right = r2 ;
            return true;
        }
    }

    return false;
}

bool findFreeRange(const int* value, int n, int& left, int& right, int maxx)
{
    g_Calls = 1;
    left = 0; 
    right = maxx;

    if(!_findFreeRange(value, n, left, right))
        return false ;

    left++;
    right--;

    return (right - left) >= 0 ;
}

如果它返回 false 列表已填充并且没有空闲范围(极少数可能), maxm 是该范围内的最大限制,在这种情况下为0xFFFFFFFF。

这个想法是首先搜索列表的最大范围,然后如果没有找到任何空洞来递归搜索子范围中可能在第一次传递期间留下的空洞。如果列表被稀疏地填充,那么函数将被多次调用是非常可能的。然而,当列表几乎完全填满时,范围搜索可能会花费更长时间。因此,在这种最糟糕的情况下,当列表关闭以填充时,最好开始将所有空闲范围保留在列表中。

答案 3 :(得分:4)

如果您不介意对列表中的每个更改进行O(n)扫描,并且每个元素有两个额外位,则无论何时插入或移除元素,都要扫描并使用这两个位来表示是否为整数(元素+ 1)或(元素-1)存在于列表中。

例如,插入元素2,列表中每个31的额外位将更新为显示3-1(在这种情况下) 3)和1+1(在1的情况下)现在已存在于列表中。

通过将每个元素的指针添加到具有相同整数的下一个元素,可以减少插入/删除时间。

答案 4 :(得分:3)

这让我想起了这本书Programming Pearls,特别是第一栏"Cracking the Oyster"。你想要解决的真正问题是什么?

如果您的列表很小,那么查找最大/最小值的简单线性搜索将起作用,并且可以快速运行。

当您的列表变大并且线性搜索变得难以处理时,您可以构建位图以表示未使用的数字,而不是在每个节点添加2个额外指针在链表中。事实上,与链接列表相比,它只有2 ^(32-8)= 16KB的RAM可能> 10GB。

然后,为了找到一个未使用的数字,你可以一次遍历位图一个机器字,检查它是否为非零。如果是,则该32位或64位块中至少有一个数字未被使用,您可以检查该字以确切地找出设置的位。在向列表中添加数字时,您所要做的就是清除位图中的相应位。

答案 5 :(得分:2)

一种可能的解决方案是使用简单的O(n)次迭代获取列表的最小值和最大值,然后在maxmin + (1 << 32)之间选择一个数字。这很容易做到,因为对无符号整数很好地定义了溢出/下溢行为:

uint32_t min, max;
// TODO: compute min and max here

// exclude max from choice space (min will be an exclusive upper bound)
max++;

uint32_t choice = rand32() % (min - max) + max; // where rand32 is a random unsigned 32-bit integer

当然,如果它不需要是随机的,那么你可以使用超过列表最大值的一个。

注意:唯一失败的情况是min为0且maxUINT32_MAX(又名为4294967295)。

答案 6 :(得分:0)

确定。这是一个非常简单的解决方案。一些答案对于优化而言变得过于理论化和复杂化。如果您需要快速解决方案,请执行以下操作:

1.在你的列表中添加一个成员:

unsigned int NextFreeId = 1;
  1. 还添加了一个std :: set&lt; unsigned int&gt; IDS

  2. 在列表中添加项目时,还要添加集合中的整数并跟踪NextFreeId:

  3. int insert(unsigned int id)     {        ids.insert(ID);

    if (NextFreeId == id) //will not happen too frequently
    {
        unsigned int TheFreeId ;
        unsigned int nextid = id+1, previd = id-1;
        while(true )
        {
            if(nextid < 0xFFFFFFF && !ids.count(nextid))
            {
                NextFreeId = nextid ;
                break ;
            }
    
            if(previd > 0 && !ids.count(previd))
            {
                NextFreeId = previd ;
                break ;
            }
    
            if(prevId == 0 && nextid  == 0xFFFFFFF)
              break;  // all the range is filled, there is no free id
    
            nextid++ ;
            previd -- ;
        }
    }
    
    return 1;
    

    }

    设置是非常有效的,以检查是否包含值,因此复杂性将为O(log(N))。它很快实现。也不是每次都搜索set,而是仅在填充NextFreeId时搜索。列表不会被遍历。