栈和队列详细解析

今天我来讲讲关于栈和队列的相关技术,就当是给自己做一个复习吧。

中缀和后缀表达式(了解学习)

中缀和后缀的概念:

我们常见的表达式一般都是中缀表达式,例如:

1+2*3-4

那么什么是后缀表达式呢?

因为中缀表达式不能很好的确定运算符的优先级,所以才有了后缀表达式的诞生,例如:

523*-

运算规则是这样的:

遇到操作数我们先跳过,遇到操作符我们计算。操作符与它左边最近的两个数进行计算,计算的结果充当下一个操作符的右操作数依次类推。

在这里先计算2*3得出6,6作为减号的右操作数再次计算,即5-6=-1,上面表达式的结果就为-1.

我们可以通过栈对以上思想进行实现:(这里只说一下思路)

先创建一个栈:stack<int>st;我们从表达式的开始遍历,遇到操作数我们将操作数push_back到栈中,遇到操作符时我们取栈顶的数作为右操作数,pop以后再去取栈顶的数作为取操作数,运算的结果再次push_back到栈中,当表达式遍历到结尾时,最终留下的栈顶元素就是表达式的结果了。

中缀转后缀的方法:

遇到操作数我们输出,遇到操作符我们就将操作符入栈,栈中相邻的两个运算符,后一个比前一个高就不能运算并且先在栈中储存着,后一个比前一个低或者相等,那么前一个就可以出栈进行运算。

栈的模拟实现:

每一个容器的设计都会它的设计模式:

  1. 适配器模式。用已有的东西封装转换成你想要的东西。

  1. 迭代器模式。不暴露底层的细节,封装后提供统一的访问容器。

在这里我们要模拟实现栈可以用vector或者list这些已有的容器进行封装实现,代码如下:

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

模板参数container默认使用的是底层为vector的适配器,在我们实际应用的时候也可以使用链表栈:

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

队列的模拟实现:

和实现栈一样,我们也用其他容器适配出一个队列:(和上面代码类似,但细节不同)

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

在这里容器适配器默认只能为list,原因:在pop()接口中,pop_front在list里面有,但是vector没有。vector头删的效率低,所以并没有支持vector头删的接口,vector是通过erase()来实现头删的。

库中实现的栈和队列:


接口不用多说,我们模拟实现的和库中基本一致,但是库里的栈和队列用了同一个容器适配器deque,这是个什么容器,为什么要使用它呢?接下来我们细谈:

vector和list各自的缺陷:

vector:扩容有消耗,头部中间插入删除效率低。(只能满足栈)

list:不支持随机访问,cpu高速缓存命中率低。(能满足栈和队列)

所以能不能设计出一个容器,兼具vector和list的优点呢?这时候deque俗称双端队列闪亮。

双端队列它的本质不是一个队列!!因此它没有先进先出的性质!!!

双端队列deque:

它既能像vector一样支持随机访问,又支持了像list一样头插头删的操作。

list相当于诸葛亮,vector相当于吕布,而deque相当于魏延。虽然deque兼具了双方的优点但是使用的场景很少,因为它并没有将双方的优势发挥到极致,等我讲完它的底层实现你就明白了。

deque的底层实现:

deque是由多个buffer数组和一个中控指针数组构成。

中控指针数组里面存放的是buffer的指针,所有的数据存放在一个一个buffer中。这样连续的空间兼具了vector随机访问、cpu高速缓存命中率高的优点,又具备了list的优势:头尾的插入删除(不需要扩容了,直接开buffer数组就行)。唯一需要扩容的就是当中控指针数组满的时候会扩容。

它的缺陷:

  1. 中间插入数据效率低,需要挪动数据,相比list中间删除数据不够极致,没有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;

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

猜你喜欢

转载自blog.csdn.net/m0_69005269/article/details/128795048
今日推荐