从向量中删除最小的非唯一值

时间:2015-06-17 16:19:16

标签: c++ algorithm c++11 unique

我有一个未分类的双精度矢量(实际上是带有双成员的对象,在这种情况下使用)。从这个向量我需要删除最小的非唯一值。但是,不保证存在非唯一值。允许对范围进行排序。

一如既往,我开始寻找std :: algorithm并找到了std :: unique。在我的第一个想法中,我将结合使用std :: sort将所有非唯一值移动到向量的末尾,然后在非唯一值上使用min_element。但是,std :: unique会将非唯一值保留在未指定状态的末尾。事实上,我失去了所有非POD成员。

有没有人有建议如何有效地做到这一点?重要的是要有效地执行它,因为代码被用在程序的瓶颈中(已经有点太慢)。

5 个答案:

答案 0 :(得分:7)

好吧,如果你可以对范围进行排序,那么这很容易。按升序对其进行排序,然后迭代直到遇到两个等效的相邻元素。完成。

这样的事情:

T findSmallestNonunique(std::vector<T> v)
{
   std::sort(std::begin(v), std::end(v));
   auto it = std::adjacent_find(std::begin(v), std::end(v));
   if (it == std::end(v))
      throw std::runtime_error("No such element found");
   return *it;
}

这是a demonstration

#include <vector>
#include <algorithm>
#include <stdexcept>
#include <iostream>

template <typename Container>
typename Container::value_type findSmallestNonunique(Container c)
{
   std::sort(std::begin(c), std::end(c));
   auto it = std::adjacent_find(std::begin(c), std::end(c));

   if (it == std::end(c))
      throw std::runtime_error("No such element found");

   return *it;
}

int main(int argc, char** argv)
{
    std::vector<int> v;
    for (int i = 1; i < argc; i++)
        v.push_back(std::stoi(argv[i]));

    std::cout << findSmallestNonunique(v) << std::endl;
}

// g++ -std=c++14 -O2 -Wall -pedantic -pthread main.cpp \
// && ./a.out 1 2 2 3 4 5 5 6 7 \
// && ./a.out 5 2 8 3 9 3 0 1 4 \
// && ./a.out 5 8 9 2 0 1 3 4 7
// 
// 2
// 3
// terminate called after throwing an instance of 'std::runtime_error'
//   what():  No such element found

请注意,这里我没有就地进行搜索,但我可以通过引用来获取容器。 (这取决于您是否可以对原始输入进行排序。)

由于排序操作,这可能与 O(N×log(N))一样“差”,但它简单易维护,不需要任何分配/拷贝(除了整个数据集的单个副本,如上所述,您可以轻易地完全避免)。如果您的输入很大,或者您希望在大多数情况下匹配失败,则可能需要使用其他内容。一如既往:个人资料

答案 1 :(得分:7)

您可以在(预期)线性时间内执行此操作。

  • 使用unordered_map来计算元素。这是(预期)值的数量的线性。

  • 使用朴素循环查找非唯一身份中的最小项目。

这是一个可能的实现:

#include <unordered_map>
#include <iostream>
#include <vector>

using namespace std;

int main()
{
    const vector<double> elems{1, 3.2, 3.2, 2};
    unordered_map<double, size_t> c;
    for(const double &d: elems)
        ++c[d];
    bool has = false;
    double min_;
    for(const auto &e: c)
        if(e.second > 1)
        {
            min_ = has? min(e.first, min_): e.first;
            has = true;
        }
    cout << boolalpha << has << " " << min_ << endl;
    return 0;
}

编辑作为Howard Hinnant&amp;在轨道上的轻盈竞赛已经指出,这包含分配&amp;哈希值。因此它将是线性的但具有相对大的因子。其他基于排序的解决方案可能适用于小尺寸。在进行概要分析时,使用好的分配器很重要,例如Google's tcmalloc

答案 2 :(得分:6)

嗯,这是一个实际删除最小的非唯一项目的算法(而不是打印它)。

template <typename Container>
void
removeSmallestNonunique(Container& c)
{
    using value_type = typename Container::value_type;
    if (c.size() > 1)
    {
        std::make_heap(c.begin(), c.end(), std::greater<value_type>{});
        std::pop_heap(c.begin(), c.end(), std::greater<value_type>{});
        for (auto e = std::prev(c.end()); e != c.begin(); --e)
        {
            std::pop_heap(c.begin(), e, std::greater<value_type>{});
            if (*e == e[-1])
            {
                c.erase(e);
                break;
            }
        }
    }
}

我选择此算法主要是因为Lightness Races in Orbit没有。我不知道这是否会比sort/adjacent_find更快。答案几乎肯定取决于输入。

例如,如果没有重复项,则此算法肯定比sort/adjacent_find慢。如果输入非常非常大,并且最小唯一可能在排序范围的早期,则此算法可能比sort/adjacent_find更快。

我上面所说的一切都只是在猜测。在对实际问题的统计可能输入执行所需测量之前,我要退出。

也许Omid可以在他的测试中包含这个并提供摘要答案。 : - )

7小时后......时间

我使用Omid's代码,纠正了其中的一个小错误,更正了其他两个算法以实际擦除元素,并更改了测试工具以更广泛地改变大小和重复数量。

以下是我在-O3使用clang / libc ++测试的代码:

#include <unordered_map>
#include <iostream>
#include <vector>
#include <algorithm>
#include <random>
#include <chrono>
#include <cassert>

template <typename Container>
void
erase_using_hashTable(Container& vec)
{
    using T = typename Container::value_type;
    std::unordered_map<T, int> c;
    for (const auto& elem : vec){
        ++c[elem];
    }
    bool has = false;
    T min_;
    for (const auto& e : c)
    {
        if (e.second > 1)
        {
            min_ = has ? std::min(e.first, min_) : e.first;
            has = true;
        }
    }
    if (has)
        vec.erase(std::find(vec.begin(), vec.end(), min_));
}

template <typename Container>
void 
eraseSmallestNonunique(Container& c)
{
   std::sort(std::begin(c), std::end(c));
   auto it = std::adjacent_find(std::begin(c), std::end(c));

   if (it != std::end(c))
       c.erase(it);
}

template <typename Container>
void
removeSmallestNonunique(Container& c)
{
    using value_type = typename Container::value_type;
    if (c.size() > 1)
    {
        std::make_heap(c.begin(), c.end(), std::greater<value_type>{});
        std::pop_heap(c.begin(), c.end(), std::greater<value_type>{});
        for (auto e = std::prev(c.end()); e != c.begin(); --e)
        {
            std::pop_heap(c.begin(), e, std::greater<value_type>{});
            if (*e == e[-1])
            {
                c.erase(e);
                break;
            }
        }
    }
}

template<typename iterator>
iterator partition_and_find_smallest_duplicate(iterator begin, iterator end)
{
    using std::swap;
    if (begin == end)
        return end; // empty sequence

    // The range begin,end is split in four partitions:
    // 1. equal to the pivot
    // 2. smaller than the pivot
    // 3. unclassified
    // 4. greater than the pivot

    // pick pivot (TODO: randomize pivot?)
    iterator pivot = begin;
    iterator first = next(begin);
    iterator last = end;

    while (first != last) {
        if (*first > *pivot) {
            --last;
            swap(*first, *last);
        } else if (*first < *pivot) {
            ++first;
        } else {
            ++pivot;
            swap(*pivot, *first);
            ++first;
        }
    }

    // look for duplicates in the elements smaller than the pivot
    auto res = partition_and_find_smallest_duplicate(next(pivot), first);
    if (res != first)
        return res;

    // if we have more than just one equal to the pivot, it is the smallest duplicate
    if (pivot != begin)
        return pivot;

    // neither, look for duplicates in the elements greater than the pivot
    return partition_and_find_smallest_duplicate(last, end);
}

template<typename container>
void remove_smallest_duplicate(container& c)
{
    using std::swap;
    auto it = partition_and_find_smallest_duplicate(c.begin(), c.end());
    if (it != c.end())
    {
        swap(*it, c.back());
        c.pop_back();
    }
}

int  main()
{
    const int MaxArraySize = 5000000;
    const int minArraySize = 5;
    const int numberOfTests = 3;

    //std::ofstream file;
    //file.open("test.txt");
    std::mt19937 generator;

    for (int t = minArraySize; t <= MaxArraySize; t *= 10)
    {
        const int range = 3*t/2;
        std::uniform_int_distribution<int> distribution(0,range);

        std::cout << "Array size = " << t << "  range = " << range << '\n';

        std::chrono::duration<double> avg{},avg2{}, avg3{}, avg4{};
        for (int n = 0; n < numberOfTests; n++)
        {
            std::vector<int> save_vec;
            save_vec.reserve(t);
            for (int i = 0; i < t; i++){//por kardan array ba anasor random
                save_vec.push_back(distribution(generator));
            }
            //method1
            auto vec = save_vec;
            auto start = std::chrono::steady_clock::now();
            erase_using_hashTable(vec);
            auto end = std::chrono::steady_clock::now();
            avg += end - start;
            auto answer1 = vec;
            std::sort(answer1.begin(), answer1.end());

            //method2
            vec = save_vec;
            start = std::chrono::steady_clock::now();
            eraseSmallestNonunique(vec);
            end = std::chrono::steady_clock::now();
            avg2 += end - start;
            auto answer2 = vec;
            std::sort(answer2.begin(), answer2.end());
            assert(answer2 == answer1);

            //method3
            vec = save_vec;
            start = std::chrono::steady_clock::now();
            removeSmallestNonunique(vec);
            end = std::chrono::steady_clock::now();
            avg3 += end - start;
            auto answer3 = vec;
            std::sort(answer3.begin(), answer3.end());
            assert(answer3 == answer2);

            //method4
            vec = save_vec;
            start = std::chrono::steady_clock::now();
            remove_smallest_duplicate(vec);
            end = std::chrono::steady_clock::now();
            avg4 += end - start;
            auto answer4 = vec;
            std::sort(answer4.begin(), answer4.end());
            assert(answer4 == answer3);
        }
        //file << avg/numberOfTests <<" "<<avg2/numberOfTests<<'\n';
        //file << "__\n";
        std::cout <<   "Method1 : " << (avg  / numberOfTests).count() << 's'
                  << "\nMethod2 : " << (avg2 / numberOfTests).count() << 's'
                  << "\nMethod3 : " << (avg3 / numberOfTests).count() << 's'
                  << "\nMethod4 : " << (avg4 / numberOfTests).count() << 's'
                  << "\n\n";
    }

}

以下是我的结果:

Array size = 5  range = 7
Method1 : 8.61967e-06s
Method2 : 1.49667e-07s
Method3 : 2.69e-07s
Method4 : 2.47667e-07s

Array size = 50  range = 75
Method1 : 2.0749e-05s
Method2 : 1.404e-06s
Method3 : 9.23e-07s
Method4 : 8.37e-07s

Array size = 500  range = 750
Method1 : 0.000163868s
Method2 : 1.6899e-05s
Method3 : 4.39767e-06s
Method4 : 3.78733e-06s

Array size = 5000  range = 7500
Method1 : 0.00124788s
Method2 : 0.000258637s
Method3 : 3.32683e-05s
Method4 : 4.70797e-05s

Array size = 50000  range = 75000
Method1 : 0.0131954s
Method2 : 0.00344415s
Method3 : 0.000346838s
Method4 : 0.000183092s

Array size = 500000  range = 750000
Method1 : 0.25375s
Method2 : 0.0400779s
Method3 : 0.00331022s
Method4 : 0.00343761s

Array size = 5000000  range = 7500000
Method1 : 3.82532s
Method2 : 0.466848s
Method3 : 0.0426554s
Method4 : 0.0278986s

<强>更新

我已使用Ulrich Eckhardt's algorithm更新了上述结果。他的算法相当竞争力。好工作Ulrich!

我应该向读者警告这个答案,Ulrich的算法很容易受到快速排序O(N ^ 2)问题的影响。其中对于特定输入,算法可以严重退化。一般算法是可修复的,Ulrich显然已经意识到这个漏洞,这个评论证明了这一点:

// pick pivot (TODO: randomize pivot?)

这是针对O(N ^ 2)问题的一种防御,还有其他问题,例如检测不合理的递归/迭代以及切换到中间流的另一算法(例如方法3或方法2)。如上所述,当给定有序序列时,方法4受到严重影响,并且当给出逆序序列时,方法4是灾难性的。在我的平台上,对于这些情况,方法3对于方法2也是次优的,尽管不如方法4差。

找到理想的技术来解决类似快速排序的算法的O(N ^ 2)问题,这在某种程度上是黑色的,但非常值得花时间。我肯定会认为方法4是工具箱中的一个有价值的工具。

答案 3 :(得分:6)

首先,关于删除元素的任务,最难的是找到它,但实际上删除它很容易(与最后一个元素交换然后pop_back())。因此,我只会解决这个问题。另外,你提到对序列进行排序是可以接受的,但我从中得出的不仅仅是排序,而且任何类型的重新排序都是可以接受的。

看看快速排序算法。它选择一个随机元素,然后将序列分为左右两侧。如果您编写分区以便区分“less”和“not less”,则可以对序列进行部分排序并在运行中找到最小的副本。

以下步骤应该完成工作:

  • 首先,选择一个随机数据块并对序列进行分区。同时,您可以检测到检查枢轴是否重复。请注意,如果您在此处找到重复内容,则可以丢弃(!)更大的内容,因此您甚至不必为两个分区投入存储容量和带宽。
  • 然后,递归到较小元素的序列。
  • 如果较小的分区中有一组重复项,则这些是您的解决方案。
  • 如果第一个支点重复,那就是您的解决方案。
  • 否则,请转发给寻找重复项的较大元素。

优于常规排序的优点是,如果您在较低的数字中找到重复项,则不会对整个序列进行排序。在一系列唯一数字上,您将完全对它们进行排序。与使用散列映射建议的元素计数相比,这确实具有更高的渐近复杂度。它是否表现更好取决于您的实现和输入数据。

请注意,这需要对元素进行排序和比较。你提到你使用double值,当你在那里有NaN时排序是非常糟糕的。我可以想象标准容器中的散列算法可以使用NaNs,因此还有一点可以用哈希映射进行计数。

以下代码实现了上述算法。它使用一个递归函数对输入进行分区并查找重复项,从第二个函数调用,然后最终删除副本:

#include <vector>
#include <algorithm>
#include <iostream>

template<typename iterator>
iterator partition_and_find_smallest_duplicate(iterator begin, iterator end)
{
    using std::swap;

    std::cout << "find_duplicate(";
    for (iterator it=begin; it!=end; ++it)
        std::cout << *it << ", ";
    std::cout << ")\n";
    if (begin == end)
        return end; // empty sequence

    // The range begin,end is split in four partitions:
    // 1. equal to the pivot
    // 2. smaller than the pivot
    // 3. unclassified
    // 4. greater than the pivot

    // pick pivot (TODO: randomize pivot?)
    iterator pivot = begin;
    std::cout << "picking pivot: " << *pivot << '\n';

    iterator first = next(begin);
    iterator last = end;

    while (first != last) {
        if (*first > *pivot) {
            --last;
            swap(*first, *last);
        } else if (*first < *pivot) {
            ++first;
        } else {
            ++pivot;
            swap(*pivot, *first);
            ++first;
            std::cout << "found duplicate of pivot\n";
        }
    }

    // look for duplicates in the elements smaller than the pivot
    auto res = partition_and_find_smallest_duplicate(next(pivot), first);
    if (res != first)
        return res;

    // if we have more than just one equal to the pivot, it is the smallest duplicate
    if (pivot != begin)
        return pivot;

    // neither, look for duplicates in the elements greater than the pivot
    return partition_and_find_smallest_duplicate(last, end);
}

template<typename container>
void remove_smallest_duplicate(container& c)
{
    using std::swap;
    auto it = partition_and_find_smallest_duplicate(c.begin(), c.end());
    if (it != c.end())
    {
        std::cout << "removing duplicate: " << *it << std::endl;

        // swap with the last last element before popping
        // to avoid copying the elements in between
        swap(*it, c.back());
        c.pop_back();
    }
}

int main()
{
    std::vector<int> data = {66, 3, 11, 7, 75, 62, 62, 52, 9, 24, 58, 72, 37, 2, 9, 28, 15, 58, 3, 60, 2, 14};

    remove_smallest_duplicate(data);
}

答案 4 :(得分:4)

(我添加了一个额外的答案,因为1)第一个答案的焦点是使用现成的STL组件,2)Howard Hinnant提出了一些有趣的观点。)

感谢Howard Hinnant对不同方法进行基准测试的原则(以及一个非常独特的解决方案)!它带来了一些我个人觉得有趣的东西(并且完全不了解)。

然而,恕我直言,执行基准测试有些问题。

问题表明问题是

  

...具有双成员的对象在这种情况下使用...由于代码在程序的瓶颈中使用,因此有效地执行它很重要

然而,

测试:

  • int上执行操作,这对基于排序的机制有利;虽然double比较和散列都比int s&#39;更贵,但比较次数是 Theta(n log(n)),而散列次数是多少是 O(n)

  • 取出main函数的主体,并将其包装在函数(而不是类对象)中,并且不使用池分配器。坦率地说,我认为这是一个使结果毫无意义的缺陷,因为它基本上建立了众所周知的事实,即动态分配+大量容器的不必要的重新初始化是昂贵的。

  • 依赖于排序算法只能返回它们所操作的vector这一事实(对于原始问题无法做到)。在下文中,我让这一点滑动,因为vector double的问题本身很有趣,但是OP应该注意到这可能会改变一些事情。

因此,为了处理第二个问题,我最初使用了我自己的gcc libstdc++ pb_ds extension中基于探测的哈希表。这本身将解决方案#1的运行时间缩短到解决方案#2(sort + adjacent_find)的运行时间,但它仍然比#3(make_heap)更昂贵。

为了进一步减少这种情况,我使用了最简单的&#34;哈希表&#34;这似乎很重要。

template<typename T, class Hash=std::hash<T>>
class smallest_dup_remover
{
public:
    explicit smallest_dup_remover(std::size_t max_size) 
    {
        while(m_mask < max_size)
            m_mask *= 2;

        m_status.resize(m_mask);
        m_vals.resize(m_mask);

        --m_mask;
    }

    void operator()(std::vector<T> &vals)
    {
        std::fill(std::begin(m_status), std::end(m_status), 0);
        bool has = false;
        T min_;
        std::vector<T> spillover;
        spillover.reserve(vals.size());
        for(auto v: vals)
        {
            const std::size_t pos = m_hash(v) & m_mask;
            char &status = m_status[pos];
            switch(status)
            {
            case 0:
                status = 1;
                m_vals[pos] = v;
                break;
            case 1:
                if(m_vals[pos] == v)
                {
                    status = 2;
                    min_ = has? std::min(min_, v): v;
                    has = true;
                }
                else
                    spillover.push_back(v);
                break;
            case 2:
               if(m_vals[pos] != v)
                    spillover.push_back(v);
            }
        }
        std::sort(std::begin(spillover), std::end(spillover));
        auto it = std::adjacent_find(std::begin(spillover), std::end(spillover));
        if(has && it == std::end(spillover))
            remove_min(vals, min_);
        else if(has && it != std::end(spillover))
            remove_min(vals, std::min(min_, *it));
        else if(!has && it != std::end(spillover))
            remove_min(vals, *it);
    }

private:
    void remove_min(std::vector<T> &vals, T t)
    {
        vals.erase(std::find(vals.begin(), vals.end(), t)); 
    }

private:
    size_t m_mask = 1;
    std::vector<char> m_status;
    std::vector<T> m_vals;
    Hash m_hash;
};

数据结构包含三个vector s:

  • a&#34; status&#34; vector,包含0,1和&#34;许多&#34;

  • 的代码
  • a&#34;值&#34; vector,包含&#34;哈希值&#34;

  • a&#34;溢出&#34;矢量,用于碰撞

带有&#34;很多&#34;的对象状态在飞行中进行最小化比较。碰撞对象(即与其他对象发生碰撞的对象)被推送到&#34;溢出&#34; vector。使用#2中的方法仔细检查溢出vector的最低重复。这与'&#34;许多&#34;中的最低发现值进行比较。值。

Here是重新测试此基准测试的基准测试代码,而here是生成以下图表的代码。

提醒#1是基于散列的,#2是基于快速排序的,#3是基于堆的。)

从之前由Howard Hinnant执行的测试开始(值从值的长度1.5范围内随机生成值),结果如下:

enter image description here

所以他的优秀的基于堆的算法确实在这种情况下表现最佳,但它看起来与以前完全不同。特别是,在分析其内存分配时,基于散列的算法并不那么糟糕。

但是,假设我们将范围更改为完全随机范围。然后结果改为:

enter image description here

在这种情况下,基于哈希的解决方案效果最好,接下来是基于排序的解决方案,而基于堆的解决方案效果最差。

为了验证原因,这里还有两个测试。

这是一个完全随机值+两个零值的测试(即最低重复值为零):

enter image description here

最后,这里是所有值都是从100个可能的值生成的情况(无论长度如何):

enter image description here

发生的事情如下。基于堆的解决方案是三个最依赖于分布的解决方案。 MakeHeap算法是线性时间,如果在此之后几乎立即遇到重复,则结果是线性算法(但没有任何散列)。相反,采取另一种极端,根本没有重复。实质上,该算法然后变为heapsort。从理论上来说,heapsort与quicksort的劣势在实践中得到了理解,并且在实践中得到了很多验证。

因此,基于堆的算法实际上是一种令人惊讶且不错的算法。它确实有很大的差异,可能是在实践中避免它的考虑因素。

一些观察结果:

  • 图表似乎没有意义: n log(n)行为在哪里,至少对于解决方案#2?

  • 为什么Hinnant测试与随机+低重复测试的工作方式类似? 1.5 X范围,并且考虑到这与Bootstrap Resampling非常相似,已知重复率为37%,我只是没有看到它。

  • 正如Howard Hinnant所说,这实际上取决于分布情况。但是,情况与之前的基准相差甚远。

  • 一些实用点:

    • OP,您可能希望使用真实的分布以及将原始结构矢量两次复制到排序解决方案中的+来重新计算原始问题。

    • 我已经考虑过如何并行化这个问题,没有任何有趣的东西。一种方法(可能提出更多问题而不是答案)是在一个线程上运行Howard Hinnant的解决方案,在另一个线程上运行另一个,并使用找到的第一个结果。鉴于它对某些发行版来说要快得多,而对其他发行版来说要慢得多,它可能会覆盖你的基础。