对元素进行排序,但保留某些固定的元素

Sort elements, but keep certain ones fixed

本文关键字:元素 保留 排序      更新时间:2023-10-16

功能

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)(我认为)。有人能把时间复杂度提高到平均0 (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 7 8 5 3 20 2 1,错误输出为11 9 9 8 11 3 20 2 9

这个很有趣。我首先尝试编写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;
}

这里有一个实例。修改前忘记分叉了,抱歉

(对于使用堆分配数据的类型,可能应该使用move,而不是最后使用副本。虽然我不确定你是否可以分配一个移动的对象)

使用…相当奇怪……包装器类而不是std::reference_wrapper使实现过滤排序成为可能,而不必使用值类型为

的元素(复制或移动)的向量
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内部的矢量来提供一个排序的"视图"的数据。

基于Beta使用迭代器排序的想法,尽管我不确定时间复杂度是多少。它也不适用于所有的容器,例如std::set, std::map。

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的错误是"assign of read-only location"。有人知道如何将其推广到集合和映射中吗?

更新:所以我建议设置,地图等…,只需删除那些满足pred的元素,然后创建一个新的set/map/…使用Compare作为它们的key_compare类型。像下面。但它只适用于set。如何将其推广到其他具有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