[C++] priority_queue use and simulation implementation - functor


First of all, I would like to recommend a C++ query documentation website that I am using. Here is the usage documentation for priority_queue . If there is something that is not clearly explained in this article, you can refer to the content of this website.

1. The use of priority_queue

1. Introduction to priority_queue

priority_queue (priority queue), is a container adapter included in the <queue> header file. The following is an introduction to priority_queue on the cplusplus website.

image-20230427143323115

As you can see, priority_queue is a container adapter , and the default container is vector . It is similar to the queue in use, except that the rules for exiting the queue are different. The queue exits the queue in the order in which it entered the queue, and the priority_queue exits the queue according to the priority. The priority here is determined according to the third parameter of the class template. , the third parameter here is a functor , and the concept of functor will be explained in detail below.

2. The structure of priority_queue

priority_queue is a heap in the logic of the bottom layer, and each pop is the heaped data. For the explanation of the heap, you can read the [data structure] tree and binary tree written by the blogger before , which is the data structure of the heap. The explanation is still relatively clear.

3. Main interface

priority_queue is a container adapter , so the interfaces are basically the same. Let's take a look at the documentation:

image-20230427145626883

It can be seen that the interface is basically the same as stack and queue. Here are some key function interfaces:

functional interface Interface Description
priority_queue() Construct an empty priority queue
priority_queue(first, last) Construct a priority queue according to the iterator range
empty() Determine whether the priority queue is empty, return bool type
top() Returns the top element of the heap (modified by costnt)
push(x) insert a data
pop() delete heap top data

4. Example of use

After understanding some of the above interfaces, it is always necessary to practice. Next, we use the following structure to test it:

//这里放一下测试代码,读者可以自行拷贝下去测试
void Test1()
{
    
    
	priority_queue<int> heap;
	heap.push(5);
	heap.push(3);
	heap.push(7);
	heap.push(10);
	heap.push(1);
	heap.push(9);
	while (!heap.empty())
	{
    
    
		cout << heap.top() << " ";
		heap.pop();
	}
	cout << endl;
}

image-20230427150918002

It can be seen that by default, the maximum value in the current heap is popped out, so it can be seen that the default heap is a large heap .

❓So what do you need to do if you want to turn the heap into a small heap?
✅It should be noted here that at the beginning of the article, we talked about the third parameter of the class template. You can see that the default parameter is less, which means that a large heap is created by default. If you want to build a small heap, the functor that needs to be passed is greater .
image-20230427152520257

Next, let's use the priority_queue we just learned to do an OJ problem: 215. The Kth largest element in the array - LeetCode .

I am a problem solver, click me

2. Functors

1. The concept of functor

In the above, we saw that there are three template parameters in the priority_queue class template, the third of which is the functor .

What exactly are functors?

Functors , also called function objects , are part of the six major components of STL. Here we can't finish it all at once, so let's introduce it based on the implementation of priority_queue.

In fact, in terms of implementation, the name function object is more appropriate: an object with the characteristics of a function. However, functors seem to describe his behavior more closely. So here we use the name functor.

Before learning STL, we have already understood the concept of generic programming. C++ introduced templates to allow our programming to control data types at will. Now we have introduced the concept of functors, allowing us to control logic.

So now let's meet functors:

image-20230428000620364

image-20230428000825943

These two are the functors we may use in the parameter list of priority_queue.

image-20230428002052512

It can be seen that when used, it is actually similar to a function call, so it is called a functor

2. Try to implement a functor

The essence of the functor is an operator overloading operator(), which is the operator of the function call. Since the functor itself is a class template, our implementation is as follows

template<class T>
class less
{
    
    
    public:
    bool operator()(const T& x, const T& y)//less和greater需要实现的就是一个比较,所以这里的返回值是bool类型
    {
    
    
        return x < y;
    }
};
template<class T>
class greater
{
    
    
    public:
    bool operator()(const T& x, const T& y)
    {
    
    
        return x > y;
    }
};

In this way, the functors of less and greater are implemented.

Here is just a little mention of the concept of functors. After all, functors are one of the six major components of STL, and there are many things contained in them. Our learning of functors is still on the way.

3. Simulation implementation of priority_queue

With the above-mentioned foreshadowing, now we have the ability to simulate priority_queue, so just do it.

1. The structure of priority_queue

First of all, for the design of the function template, we matched it with the library and gave three parameters, which respectively represent the parameter type of the parameter stored in the container, the container type, and the functor. The default functor is less, and a large pile is built.

template<class T, class Container = std::vector<T>, class Compare = less<T>>
class priority_queue
{
    
    
public:
    //...
private:
    Container _con;
}; 

2. Interface implementation

According to our previous experience in implementing heaps in [data structure] trees and binary trees, the two interfaces that must be implemented are upward adjustment and downward adjustment. This is also the core interface of the entire heap, so we will focus on implementing these two functions first . Interface, after the implementation of these two interfaces, other structural interfaces are easy to implement.

1. Adjust the algorithm downward

image-20230428004532849

Here, we take the small heap as an example (being lazy here: the picture of the small heap has been drawn before), what we need to do is to find the smaller of the two children, and then compare the size with the parent node, if the parent If the node is larger than the child node, the exchange is performed, and then the original child node becomes the new parent node, and the above steps are repeated. until it fits the structure of the heap

void adjust_down(size_t parent)
{
    
    
    Compare cmp;//实例化仿函数
    int minchild = 2 * parent + 1;
    while (minchild < _con.size())
    {
    
    
        if (minchild + 1 < _cin.size() && cmp(_con[minchild], con[minchild + 1]))//使用仿函数找到符合条件的子节点
        {
    
    
            minchild++;
        }
        if (cmp(_con[parent], _con[minchild]))//判断是否满足堆结构
        {
    
    
            std::swap(_con[parent], _con[minchild]);
            //父子节点迭代
            parent = minchild;
            minchild = 2 * parent + 1;
        }
    }
}

2. Adjust the algorithm upwards

image-20230428010029365

The upward adjustment operation here starts from the specified child node position, compares it with the parent node, and judges whether the heap structure is satisfied. If not, exchange the parent-child node, and then the original parent node programs the child node, and performs the above operation again until it is satisfied. up to the heap structure.

void adjust_up(size_t child)
{
    
    
    Compare cmp;//实例化仿函数
    size_t parent = (child - 1) / 2;
    while (child > 0)
    {
    
    
        if (cmp(_con[parent], _con[child]))
        {
    
    
            std::swap(_con[parent], _con[child]);
            //父子迭代
            child = parent;
            parent = (child - 1) / 2;
        }
    }
}

3. Constructor

Constructors are divided into no-argument construction and iterator interval construction

//无参的构造函数
priority_queue() {
    
    }
//迭代器区间构造
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last)
    _con(first, last)
{
    
    
    //从最后一个父节点的位置开始向下调整建堆,效率最高
    for (int i = (_con.size() - 1 - 1) / 2; i <= 0; --i)
    {
    
    
        adjust_down(i);
    }
}

4. Modify data

The data modification of priority_queue only has two cases of push and pop

The push data is directly inserted at the end, and then the heap is adjusted upward until the heap structure is satisfied. The pop data is exchanged with the top data and the last data of the heap, and then the container is pop_back, and then adjusted downward until the remaining data meets the heap structure.

void push(const T& val)
{
    
    
    _con.push_back(val);//尾插数据
    adjust_up(_con.size() - 1);//向上调整堆结构
}
void pop()
{
    
    
    swap(_con[0], _con[_con.size() - 1]);//交换堆顶和堆内最后一个元素
    _con.pop_back;//容器尾删
    adjust_down(0);//向下调整堆结构
}

5. Get data

The rest of the interface can directly reuse the interface provided by the container

bool empty() const
{
    
    
    return _con.empty();
}
size_t size() const
{
    
    
    return _con.size();
}
const T& top() const
{
    
    
    return _con[0];
}

Guess you like

Origin blog.csdn.net/weixin_63249832/article/details/130418247