Detailed analysis of stack and queue

Today I will talk about related technologies about stacks and queues, just as a review for myself.

Infix and postfix expressions (learn about learning)

The concept of infix and suffix:

Our common expressions are generally infix expressions , for example:

1+2*3-4

So what is a postfix expression ?

Because infix expressions cannot determine the precedence of operators very well, so there is the birth of suffix expressions, for example:

523*-

The operation rules are as follows:

When encountering operands, we skip first, and when encountering operators, we calculate . An operator is evaluated with the two nearest numbers to its left, and the result of the computation serves as the right operand of the next operator, and so on.

Here, first calculate 2*3 to get 6, 6 is calculated again as the right operand of the minus sign, that is, 5-6=-1, and the result of the above expression is -1.

We can realize the above ideas through the stack: (here just talk about the idea)

First create a stack: stack<int>st; we traverse from the beginning of the expression, when we encounter an operand, we push_back the operand to the stack, when we encounter an operator, we take the number on the top of the stack as the right operand, after pop Then take the number on the top of the stack as the operand, and the result of the operation is pushed_back to the stack again. When the expression traverses to the end, the last element on the top of the stack is the result of the expression.

The method of converting infix to suffix:

When we encounter an operand, we output it, and when we encounter an operator, we put the operator on the stack. For two adjacent operators in the stack, if the latter one is higher than the previous one, it cannot be operated and stored in the stack first, and the latter one is higher than the previous one. If one is low or equal, then the previous one can be popped out of the stack for operation.

The mock implementation of the stack:

Every container design will have its design pattern:

  1. adapter pattern . Convert what you already have into what you want.

  1. Iterator pattern . The underlying details are not exposed, and a unified access container is provided after encapsulation.

Here we want to simulate the realization that the stack can be encapsulated by existing containers such as vector or list. The code is as follows:

#pragma once
#include <iostream>
#include<vector>
#include<list>

using namespace std;

namespace kang
{
    template<class T, class container=vector<T>>
    class stack
    {
    public:
        void push(const T& val)
        {
            _con.push_back(val);
        }

        void pop()
        {
            _con.pop_back();
        }

        const T& top()
        {
            return _con.back();
        }

        bool empty()
        {
            return _con.empty();
        }

        size_t size()
        {
            return _con.size();
        }

    private:
        container _con;
    };
}

The template parameter container uses the adapter whose bottom layer is vector by default, and we can also use the linked list stack in our actual application:

int main()
{
    kang::stack<int,list<int>> st;
    st.push(1);
    st.push(2);
    st.push(3);
    st.push(4);
    st.push(5);
    while (!st.empty())
    {
        cout << st.top() << endl;
        st.pop();
    }
    return 0;
}

The mock implementation of the queue:

Just like implementing a stack, we also use other containers to adapt a queue: (similar to the above code, but different in details)

#pragma once
#include <iostream>
#include<vector>
#include<list>

using namespace std;

namespace kang
{
    template<class T, class container = list<T>>
    class queue
    {
    public:
        void push(const T& val)
        {
            _con.push_back(val);
        }

        void pop()    //因为是队列,所以pop出的是队头的数据
        {
            _con.pop_front();
        }

        const T& front()    //队列特有接口
        {
            return _con.front();
        }

        const T& back()    //队列特有接口
        {
            return _con.back();
        }

        bool empty()
        {
            return _con.empty();
        }

        size_t size()
        {
            return _con.size();
        }

    private:
        container _con;
    };
}

Here, the container adapter can only be a list by default. The reason: in the pop() interface, pop_front exists in the list, but the vector does not. The efficiency of vector header deletion is low, so there is no interface that supports vector header deletion. Vector implements header deletion through erase().

Stacks and queues implemented in the library:


Needless to say about the interface, our simulated implementation is basically the same as in the library, but the stack and queue in the library use the same container adapter deque. What kind of container is this, and why should we use it? Next we discuss in detail:

The respective defects of vector and list:

vector: Expansion is costly, and insertion and deletion in the middle of the head is inefficient. (only stack can be satisfied)

list: Random access is not supported, and the CPU cache hit rate is low. (Can satisfy stack and queue)

So can we design a container that has both the advantages of vector and list? At this time, deque is commonly known as double-ended queue.

Note : Double-ended queue is not a queue in essence! ! So it has no first-in, first-out nature! ! !

Double-ended queue deque:

It can not only support random access like vector, but also support head plug and delete operations like list.

list is equivalent to Zhuge Liang, vector is equivalent to Lu Bu, and deque is equivalent to Wei Yan. Although deque has the advantages of both sides, it is rarely used in scenarios because it does not maximize the advantages of both sides. You will understand when I finish talking about its underlying implementation.

The underlying implementation of deque:

Deque is composed of multiple buffer arrays and a central control pointer array.

The buffer pointers are stored in the central control pointer array, and all data is stored in each buffer. Such a continuous space not only has the advantages of vector random access and high CPU cache hit rate, but also has the advantages of list: insertion and deletion at the head and tail (no need to expand, just open the buffer array directly). The only thing that needs to be expanded is that the central control pointer array will be expanded when it is full.

Its flaws:

  1. Inserting data in the middle is inefficient and needs to move the data. Compared with deleting data in the middle of the list, it is not perfect enough, and it is not as fast as the list .

  1. vector下标随机访问是指针直接加减就行,而deque随机访问需要计算数据在第几个buffer中已经在这个buffer的第几个位置。因此它的随机访问有一定的消耗,没有vector快。

总结:

deque的应用虽不是很广泛,但作为栈和队列的容器适配器非常的合适!

优先级队列:

priority_queue底层是一个堆,它的容器适配器是vector。它默认是大的优先级高(大堆)

它的使用非常的简单接口如下:

优先级队列的底层实现:

优先级队列本质上是一个堆,让我们先写出它的构造函数

priority_queue(InputIterator first, InputIterator last)
            :_con(first,last)
        {
            //建堆
            for (int i = (_con.size() - 1-1)/2; i >= 0; --i)
            {
                adjust_down(i);
            }

        }
//_con默认是vector

建堆如果使用向下调整建堆它的时间复杂度是O(N)

建堆如果使用向上调整建堆它的时间复杂度是O(N*logN)

由此可见用向下调整的方法最优,所以在上面的代码我们使用adjust_down。但是需要注意的是:在我们向下调整的时候,不是最后一个数据做向下调整,而是他的父节点做第一次向下调整。通过先减1后除2一个找到一个子节点的父节点。

然后是push和pop操作:

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(_con[0]);
        }

push用的是向上调整,因为实现不了向下调整。

一个堆中的pop操作是头删,方法是先将第一个数据和最后一个位置互换,再删除最后一个数据,最后将第一个数据做向下调整。

讲了这么多最重要的还没讲,那就是向上调整向下调整的代码实现:

        //默认是大堆
        void adjust_up(size_t child)
        {
            size_t parent = (child-1) / 2;
            while (child > 0)
            {
                if (_con[parent] < _con[child])
                {
                    swap(_con[parent], _con[child]);
                    child = parent;
                    parent = (child - 1) / 2;
                }
                else
                {
                    break;
                }
            }
        }
//默认是大堆
        void adjust_down(size_t parent)
        {
            size_t child = parent * 2 + 1;
        
            while (child <_con.size())
            {
                 //默认认为左孩子大,右孩子大就++child

                if (child + 1 < _con.size() && _con[child] < _con[child + 1])
                {
                    ++child;
                }
                if (_con[child] > _con[parent])
                {
                    swap(_con[child], _con[parent]);
                    parent = child;
                    child=parent*2+1
                }
                else
                {
                    break;
                }
            }
            
        }

仿函数/函数对象:

仿函数是一个,它的要求是必须封装operator(),这样可以使类对象可以像函数一样使用。

例如我们实现两个数大小比较的仿函数:

namespace kang
{
    template<class T>
    class less
    {
    public:
        bool operator()(const T& x, const T& y)
        {
            return x < y;
        }

    };

    template<class T>
    class greater
    {
    public:
        bool operator()(const T& x, const T& y)
        {
            return x > y;
        }

    };

}

我们实现的类就可以称为是:仿函数

在这里我们调用了lessFunc(1,2),按照我们之前的理解,lessFunc是函数名或者是函数指针。但是在这里它却是一个类实例化出来的对象。一个对象像函数一样被使用,那我们称它为:函数对象

它的好处:

我们用冒泡排序举例子,如果我们想控制它的升序降序问题,我们需要在它的参数中传入一个函数指针,用函数指针控制降序升序:

但是传入函数指针的方法太麻烦了,有的函数指针甚至非常复杂,这时仿函数的优势就体现了出来:

template<class T,class Compare>
void bubble_sort(T arr[], int sz,Compare con)//传入数组和数组元素的个数sz
{

    for (int i = 0; i <= sz ; i++)//趟数
    {
        int exchange = 1;
        for (int j = 1; j <= sz - i; j++)//交换的个数
        {
            if (con(arr[j],arr[j-1]))//此处是精华!!!!
            {
                swap(arr[j - 1], arr[j]);
                exchange = 1;
            }
            if (exchange == 0)
            {
                break;
            }
        }
        
    }
}

con(arr[j],arr[j-1])这句代码我们不需要关注底层的实现细节,我们只要传入需要的仿函数对象来达到我们想要的目的。不仅可以这样调用,也可以这样调用:

第一个是有名对象,第二个是匿名对象。

这时有人会提出一个问题:为什么这里的con不加引用呢?

没错,这里不加引用就会进行拷贝,但拷贝的代价在这里不大!!!我们先思考一个仿函数对象是几个字节?一个仿函数是没有成员变量的类,所以它的大小为1个字节

仿函数优化优先级队列:

以下是优先级队列的完整代码:

#pragma once
#include <iostream>
#include <vector>
using namespace std;

namespace kang
{
    template<class T>
    class less
    {
    public:
        bool operator()(const T& x, const T& y)
        {
            return x < y;
        }

    };

    template<class T>
    class greater
    {
    public:
        bool operator()(const T& x, const T& y)
        {
            return x > y;
        }

    };

    template<class T,class Container= vector<T>,class Compare=less<T>>
    class priority_queue
    {
    public:
        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);
            }

        }
        

        //默认是大堆
        void adjust_up(size_t child)
        {
            Compare com;  //显示实例化
            size_t parent = (child-1) / 2;
            while (child > 0)
            {
                if (com(_con[parent] , _con[child]))
                {
                    swap(_con[parent], _con[child]);
                    child = parent;
                    parent = (child - 1) / 2;
                }
                else
                {
                    break;
                }
            }
        }

        //默认是大堆
        void adjust_down(size_t parent)
        {
            Compare com;
            size_t child = parent * 2 + 1;
            //默认认为左孩子大
            while (child <_con.size())
            {
                if (child + 1 < _con.size() && com(_con[child] , _con[child + 1]))
                {
                    ++child;
                }
                if (com(_con[parent] , _con[child]))
                {
                    swap(_con[child], _con[parent]);
                    parent = child;
                    child = parent * 2 + 1;
                }
                else
                {
                    break;
                }
            }
            
        }


        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);
        }

        const T& top() const
        {
            return _con[0];
        }

        bool empty() const
        {
            return _con.empty();
        }

        size_t size() const
        {
            return _con.size();
        }
    private:
        Container _con;
    };
}

仿函数的高级用法:

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
        : _year(year)
        , _month(month)
        , _day(day)
    {}

    bool operator<(const Date& d)const
    {
        return (_year < d._year) ||
            (_year == d._year && _month < d._month) ||
            (_year == d._year && _month == d._month && _day < d._day);
    }

    bool operator>(const Date& d)const
    {
        return (_year > d._year) ||
            (_year == d._year && _month > d._month) ||
            (_year == d._year && _month == d._month && _day > d._day);
    }

    friend ostream& operator<<(ostream& _cout, const Date& d)
    {
        _cout << d._year << "-" << d._month << "-" << d._day;
        return _cout;
    }

private:
    int _year;
    int _month;
    int _day;
};





void TestPriorityQueue()
{
    // 大堆,需要用户在自定义类型中提供<的重载
    kang::priority_queue<Date> q1;
    q1.push(Date(2018, 10, 29));
    q1.push(Date(2018, 10, 28));
    q1.push(Date(2018, 10, 30));
    cout << q1.top() << endl;

    // 如果要创建小堆,需要用户提供>的重载
    kang::priority_queue<Date, vector<Date>, greater<Date>> q2;
    q2.push(Date(2018, 10, 29));
    q2.push(Date(2018, 10, 28));
    q2.push(Date(2018, 10, 30));
    cout << q2.top() << endl;


    // 大堆
    kang::priority_queue<Date*> q3;
    q3.push(new Date(2018, 10, 29));
    q3.push(new Date(2018, 10, 28));
    q3.push(new Date(2018, 10, 30));
    cout << *q3.top() << endl;

    // 小堆
    kang::priority_queue<Date*> q4;
    q4.push(new Date(2018, 10, 29));
    q4.push(new Date(2018, 10, 28));
    q4.push(new Date(2018, 10, 30));
    cout << *q4.top() << endl;
}


int main()
{
    TestPriorityQueue();

    return 0;
}

在上面代码运行的结果中,我们发现前两个结果正确,后两个结果错误并且结果每次各不相同。原因就是每次new出来的指针大小是不固定的。在后面两个代码中我们想要比较的是日期类的大小,而不是指针的大小,所以我们可以用仿函数来解决:

struct PDateLess
{
    bool operator()(const Date* d1, const Date* d2)
    {
        return *d1 < *d2;
    }
};

struct PDateGreater
{
    bool operator()(const Date* d1, const Date* d2)
    {
        return *d1 > *d2;
    }
};

最后再将仿函数传进参数中:(因为没有用模板,所以不用加类型)

priority_queue<Date*, vector<Date*>, PDateLess> q3;
priority_queue<Date*, vector<Date*>, PDateGreater> q4;

这里的应用就是在特定的场景下我们可以实现一个特定的仿函数来满足我们的需求,因为库中的仿函数可能满足不了我们的需求,所以我们要自己写。好了到这里栈和队列的所有内容就结束了,创作不易,感谢阅读,希望大佬了能够多多支持我。

Guess you like

Origin blog.csdn.net/m0_69005269/article/details/128795048