【问题标题】:Sort elements, but keep certain ones fixed对元素进行排序,但保持某些固定不变
【发布时间】:2015-11-08 08:13:26
【问题描述】:

功能

template <typename Container, typename Comparator, typename Predicate>
void sortButKeepSomeFixed (Container& c, const Comparator& comp, const Predicate& pred)

是根据排序标准comp对容器c进行排序,但满足pred的元素在排序后将保持固定在其原始位置(即不受排序影响)。

我试图调整快速排序以适应这种情况,但想不出。最后,我决定调整粗略的选择排序来完成工作:

#include <iostream>
#include <vector>

std::vector<int> numbers = {5,7,1,8,9,3,20,2,11};

template <typename Container, typename Comparator, typename Predicate>
void sortButKeepSomeFixed (Container& c, const Comparator& comp, const Predicate& pred) {  // O(n^2), but want O(nlogn) on average (like quick sort or merge sort)
    const std::size_t N = c.size();
    std::size_t i, j, minIndex;
    for (i = 0; i < N-1; i++) {
        if (pred(c[i]))
            continue;  // c[i] shall not swap with any element.
        minIndex = i;
        for (j = i + 1; j < N; j++) {
            if (pred(c[j]))
                continue;  // c[j] shall not swap with any element.
            if (comp(c[j], c[minIndex]))
                minIndex = j;
        }
        if (minIndex != i)
            std::swap(c[i], c[minIndex]);
    }
}

int main() {
    sortButKeepSomeFixed (numbers,
        std::greater<int>(),  // Ordering condition.
        [](int x) {return x % 2 == 0;});  // Those that shall remain fixed.
    for (int x : numbers) std::cout << x << ' ';  // 11 9 7 8 5 3 20 2 1
}

但时间复杂度是 O(N^2)(我认为)。有人可以将这里的时间复杂度提高到平均 O(NlogN) 吗?换句话说,找到一个整体更好的算法,使用递归或类似的东西?

或者更好的办法是取出满足pred 的元素,对剩下的std::sort 进行排序,然后将提取的元素放回原来的位置?这会更有效,还是只会让情况变得更糟?

更新: 这是基于 Beta 的建议(对不通过 pred 的迭代器进行排序)。但是虽然通过pred 的元素确实保持不变,但最后的排序是不正确的。

template <typename Container, typename Comparator, typename Predicate>
void sortButKeepSomeFixed (Container& c, const Comparator& comp, const Predicate& pred) {
    std::vector<typename Container::iterator> iterators;
    for (typename Container::iterator it = c.begin();  it != c.end();  ++it) {
        if (!pred(*it))
            iterators.emplace_back(it);
    }
    std::vector<typename Container::iterator> originalIterators = iterators;
    std::sort(iterators.begin(), iterators.end(),
        [comp](const typename Container::iterator& x, const typename Container::iterator& y)
        {return comp(*x, *y);});
    for (int i = 0; i < originalIterators.size(); i++)
        *originalIterators[i] = *iterators[i];
}

错误的输出是11 9 9 8 11 3 20 2 9,而应该是11 9 7 8 5 3 20 2 1

【问题讨论】:

  • 你的“更好的主意”会很好用,O(N logN)。
  • O(N logN) for std::sort 本身,但是删除元素然后将它们放回原来的位置所花费的时间增加了很多,不是吗?
  • 如果您使用 vector 之类的东西,但不自己处理插入/删除元素,插入/删除的复杂性已摊销到 O(N),所以没关系。
  • 其实没有,抱歉。如果你想在原地做,你需要复制一些东西,例如如果您不创建原始数组的副本。如果您逐个进行删除,则每次插入/删除将为O(N),因此总复杂度为 O(N^2)。我建议你为那些没有通过谓词的人创建一个新数组,如果运行性能是你的首要任务,内存复杂度是O(N)而不是O(1)
  • 有时应该问“为什么?”

标签: c++ sorting templates lambda


【解决方案1】:

这很有趣。我首先尝试使用仅跳过满足谓词的元素的custom iterator 编写 IMO 正确方法的代码。结果证明这是相当具有挑战性的,至少在我这样做的时候在手机上写。

基本上,这应该会导致类似于 Eric Niebler 的 ranges v3 中的代码。

但您还可以尝试使用上述更简单、更直接的方法。您的非工作解决方案的问题是,它正在更改(其余已排序的)迭代器在最后一个 for 循环中分配时指向的值。这个问题可以通过复制来避免,就像在我的代码中一样:

int main(int, char **) {
 vector<int> input {1,2,3,4,5,6,7,8,9};
 vector<reference_wrapper<int>> filtered{begin(input), end(input)};
 filtered.erase(remove_if(begin(filtered), end(filtered),
         [](auto e) {return e%2==0;}), end(filtered));
 vector<int> sorted{begin(filtered), end(filtered)};
 // change that to contain reference wrappers to see the issue
 sort(begin(sorted), end(sorted),
      greater<int>{});
 transform(begin(filtered), end(filtered),
    begin(sorted),
    begin(filtered),
    [](auto to, auto from) {
      to.get() = from; return to;});
 copy(begin(input), end(input),
      ostream_iterator<int>{cout, ", "});
 return 0;
}

Live example here. 修改前忘记fork了,抱歉。

(对于使用堆分配数据的类型,最好不要使用副本move。虽然我不确定您是否可以分配给已移动的对象。)

使用 ... 相当奇怪的 ... 包装类而不是 std::reference_wrapper 使得 possible to achieve the filtered sorting without 不得不使用带有(复制或移动)值类型元素的向量:

template <class T>
class copyable_ref {
public:
  copyable_ref(T& ref) noexcept
  : _ptr(std::addressof(ref)), _copied(false) {}
  copyable_ref(T&&) = delete;
  copyable_ref(const copyable_ref& x) noexcept
  : _ptr (new int(*x._ptr)), _copied (true) {
  }
  ~copyable_ref() {
    if (_copied) {
      delete _ptr;
    }
  }
  copyable_ref& operator=(const copyable_ref& x) noexcept {
    *_ptr = *x._ptr;
  }
  operator T& () const noexcept { return *_ptr; }
  T& get() const noexcept { return *_ptr; }
private:
  T* _ptr;
  bool _copied;
};

在构造时,这个类存储一个指向它的参数的指针,当使用复制赋值运算符时,它也会被修改。但是,当一个实例被复制构造时,就会生成一个被引用(由另一个)值的堆分配副本。这样,可以使用类似于

的代码交换两个引用的值
Value a, b;
copyable_ref<Value> ref_a{a}, ref_b{b};
copyable_ref<Value> temp{ref_a};
ref_a = ref_b;
ref_b = temp;
// a and b are swapped

这是必要的,因为 std::sort 似乎没有使用 swap(通过 ADL 或 std::swap 找到),而是与上述代码等效的代码。

现在可以通过用奇怪的包装类的(非复制构造)实例填充向量并对该向量进行排序来对过滤的“视图”进行排序。正如示例中的输出所示,最多有一个值类型的堆分配副本。不计算包装器内部指针所需的大小,此类启用具有恒定空间开销的过滤排序:

 vector<int> input {1,2,3,4,5,6,7,8,9};

 vector<copyable_ref<int>> sorted;
 sorted.reserve(input.size());
 for (auto & e : input) {
    if (e % 2 != 0) {
      sorted.emplace_back(e);
    }
 }
 sort(begin(sorted), end(sorted),
      greater<int>{});
 copy(begin(input), end(input),
      ostream_iterator<int>{cout, ", "});
 cout << endl;
 // 9 2 7 4 5 6 3 8 1

最后,虽然这很好用,但我可能不会在生产代码中使用它。我特别惊讶 std::sort 没有使用我自己的 swap 实现,这导致了这个冒险的复制构造函数。


您无法概括您的代码以适用于集合和地图:它们是按设计排序的,它们需要固定的顺序才能正常运行。无序变体是无序的,因此无法维持顺序。但是您始终可以(只要您不修改容器)在向量内使用std::reference_wrappers 来提供数据的排序“视图”。

【讨论】:

  • @Daniel Jour 感谢您修复我最初的迭代器尝试。至于您的自定义迭代器尝试,您认为可以修复吗?它看起来很有趣,如果您认为它可以修复,我会尝试自己修复它(这将是“最酷”的解决方案)。
  • 据我所知,您已经在自己的答案中做到了。这基本上就是我所做的。有什么困扰我的,我希望有一份必要的副本。这不应该是这样的,我正在努力解决这个问题。
  • 我知道,我还没有准备好。不幸的是,我没有分叉,所以前一个暂时丢失了。
  • @Daniel Jour 您可能对我提出的问题的进一步概括感兴趣。基本上,那些满足 pred 的人不仅会留在他们的位置上,而且会被其他一些比较器依次排序。然后进一步推广到任意数量的预测和比较器。我自己解决了这个问题,但也许所有元素的复制都会让你厌烦到想出一个更优雅的解决方案:ideone.com/ve91PU
【解决方案2】:

基于 Beta 使用迭代器进行排序的想法,但我不确定时间复杂度是多少。它也不适用于所有容器,例如标准::设置,标准::地图。

template <typename Container, typename Comparator, typename Predicate>
void sortButKeepSomeFixed (Container& c, const Comparator& comp, const Predicate& pred) {
    std::vector<typename Container::value_type> toSort;
    std::vector<typename Container::iterator> iterators;
    for (typename Container::iterator it = c.begin();  it != c.end();  ++it) {
        if (!pred(*it)) {
            toSort.emplace_back(*it);
            iterators.emplace_back(it);
        }
    }
    std::sort(toSort.begin(), toSort.end(), comp);
    for (std::size_t i = 0; i < toSort.size(); i++)
        *iterators[i] = toSort[i];
}

std::vector<int> vector   = {5,7,1,8,9,3,20,2,11};
std::array<int, 9> array = {5,7,1,8,9,3,20,2,11};
std::list<int> list       = {5,7,1,8,9,3,20,2,11};
std::set<int> set         = {5,7,1,8,9,3,20,2,11};
std::map<double, int> map = { {1.5,5}, {1.2,7}, {3.5,1}, {0.5,8}, {5.2,9}, {7.5,3}, {0.1,20}, {1.8,2}, {2.4,11} };

template <typename Container>
void test (Container& container) {
    sortButKeepSomeFixed (container,
        std::greater<int>(),  // Ordering condition.
        [](int x) {return x % 2 == 0;});  // Those that shall remain fixed.
    for (int x : container) std::cout << x << ' ';
    std::cout << '\n';
}

int main() {
    test(vector);  // 11 9 7 8 5 3 20 2 1
    test(array);  // 11 9 7 8 5 3 20 2 1
    test(list);  // 11 9 7 8 5 3 20 2 1
    test(set);  // Does not compile.
    sortButKeepSomeFixed (map,
        [](const std::pair<double, int>& x, const std::pair<double, int>& y) {return x.second > y.second;},
        [](const std::pair<double, int>& x) {return x.second % 2 == 0;});
    for (const std::pair<double, int>& x : map)
        std::cout << "(" << x.first << "," << x.second << ") ";  // Does not compile.
}

set 和 map 的错误是“分配只读位置”。 任何人都知道如何将其推广到集合和地图?

更新:所以我建议对于 set、map 等...,只需删除那些满足 pred 的元素并创建一个新的 set/map/... 并使用 Compare 作为它们的 key_compare 类型。如下所示。但这只是为了设置。如何将其推广到其他具有 key_compare 类型的容器?

template <typename Container, typename Comparator, typename Predicate>
std::set<typename Container::value_type, Comparator, typename Container::allocator_type>
        sortButRemoveSomeElements (Container& c, const Comparator&, const Predicate& pred) {
    std::set<typename Container::value_type, Comparator, typename Container::allocator_type> set;
    std::vector<typename Container::value_type> keep;
    for (typename Container::iterator it = c.begin();  it != c.end();  ++it) {
        if (!pred(*it))
            keep.emplace_back(*it);
    }
    for (typename Container::value_type x : keep)
        set.emplace(x);  // Sorted by Comparator automatically due to std::set's insertion property.
    return set;
}

测试:

struct GreaterThan { bool operator()(int x, int y) const {return x > y;} };
std::set<int, GreaterThan> newSet = sortButRemoveSomeElements (set,
    GreaterThan{},  // Ordering condition.
    [](int x) {return x % 2 == 0;});  // Those that shall be removed.
for (int x : newSet) std::cout << x << ' ';  // 11 9 7 5 3 1

【讨论】:

  • 旁白:这个实现有利于大多数元素不受影响的情况。其他替代方法是复制回来,重新评估 Pred 以查找需要跳过哪些元素(我的直觉说这是最好的通用版本),或者将迭代器保留在需要跳过的地方(这有利于Pred 很贵的情况)。
  • 对地图和集合进行排序确实没有意义。因为它们已经在集合的情况下按元素本身排序,在地图的情况下按键排序。这就是我们使用地图和集合的原因,始终保持它们的排序,因此无论何时我们需要查找/插入/删除一个元素,它始终是O(logN)
  • 好的,那么我建议使用 set、maps 等...,只需删除那些满足 pred 的元素并创建一个新的 set/map/...,并将 Compare 作为它们的 key_compare 类型。我已经写了一个函数,在这方面适用于集合,但是泛化?
猜你喜欢
  • 1970-01-01
  • 2013-03-25
  • 1970-01-01
  • 2018-01-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多