数据结构实战开发教程(六)栈的概念及实现、队列的概念及实现、两个有趣的问题

2023-05-16

三十四、栈的概念及实现(上)

1、栈的定义

  • 栈是一种特殊的线性表
  • 栈仅能在线性表的一端进行操作
    • 栈顶(Top):允许操作的一端
    • 栈底(Bottom):不允许操作的一端

2、栈的特性

  • 后进先出( Last In First Out )

3、栈的操作

  • 创建栈             Stack()
  • 销毁栈           ~Stack()
  • 清空栈           clear()
  • 进栈              push()
  • 出栈              pop()
  • 获取栈顶元素 top()
  • 获取栈的大小 size()

4、栈的实现

5、栈的顺序实现

6、StaticStack 设计要点

  • 类模板
    • 使用原生数组作为栈的存储空间
    • 使用模板参数决定栈的最大容量

        

7、编程实验:基于顺序存储结构的栈

StaticStack.h

#ifndef STATICSTACK_H
#define STATICSTACK_H

#include "Stack.h"
#include "Exception.h"

namespace DTLib
{

template < typename T, int N >
class StaticStack : public Stack<T>
{
protected:
    T m_space[N];       
    int m_top;
    int m_size;
public:
    StaticStack()    // O(1)
    {
        m_top = -1;
        m_size = 0;
    }

    int capacity() const  // O(1)
    {
        return N;
    }

    void push(const T& e)  // O(1)
    {
        if( m_size < N )
        {
            m_space[m_top + 1] = e;     // 为了异常安全,先复制后改动标识,因为是类型T可能存在异常
            m_top++;
            m_size++;
        }
        else
        {
            THROW_EXCEPTION(InvalidOperationException, "No space in current stack ...");
        }
    }

    void pop()   // O(1)
    {
        if( m_size > 0 )
        {
            m_top--;
            m_size--;
        }
        else
        {
            THROW_EXCEPTION(InvalidOperationException, "No element in current stack ...");
        }
    }

    T top() const   // O(1)
    {
        if( m_size > 0 )
        {
            return m_space[m_top];
        }
        else
        {
            THROW_EXCEPTION(InvalidOperationException, "No element in current stack ...");
        }
    }

    void clear()   // O(1)
    {
        m_top = -1;
        m_size = 0;
    }

    int size() const  // O(1)
    {
        return m_size;
    }
};

}

#endif // STATICSTACK_H

8、小结

  • 栈是一种特殊的线性表
  • 栈只允许在线性表的一端进行操作
  • StaticStack使用原生数组作为内部存储空间
  • StaticStack的最大容量由模板参数决定

三十五、栈的概念及实现(下)

  • 当存储的元素为类类型时,StaticStack的对象在创建时,会多次调用元素类型的构造函数,影响效率。
  • 是的,所以我们需要实现链式栈。

        

创建栈对象时,调用泛指类型的构造函数。

1、链式栈的存储实现

2、链式栈的设计要点

  • 类模板,抽象父类Stack的直接子类
  • 在内部组合使用LinkList类,实现栈的链式存储
  • 只在单链表成员对象的头部进行操作

3、编程实验:基于链式存储结构的栈

LinkStack.h

#ifndef LINKSTACK_H
#define LINKSTACK_H

#include "Stack.h"
#include "LinkList.h"

namespace DTLib
{

template < typename T >
class LinkStack : public Stack<T>
{
protected:
    LinkList<T> m_list; // 组合使用单链表类
public:
    void push(const T& e)  // O(1)
    {
        m_list.insert(0, e);
    }

    void pop()   // O(1)
    {
        if( m_list.length() > 0 )
        {
            m_list.remove(0);
        }
        else
        {
            THROW_EXCEPTION(InvalidOperationException, "No element in current stack ...");
        }
    }

    T top() const  // O(1)
    {
        if( m_list.length() > 0 )
        {
            return m_list.get(0);
        }
        else
        {
            THROW_EXCEPTION(InvalidOperationException, "No element in current stack ...");
        }
    }

    void clear()  // O(n)
    {
        m_list.clear();
    }

    int size() const  // O(1)
    {
        return m_list.length();
    }
};

}

#endif // LINKSTACK_H

4、栈的应用实践

  • 符号匹配问题
    • 在C语言中有一些成对匹配出现的符号
      • 括号:(), [], { }, <>
      • 引号:' ' , " "

5、问题:如何实现编译器中的符号成对检测?

6、算法思路

  • 从第一个字符开始扫描
    • 当遇见普通字符时忽略
    • 当遇见左符号时压入栈中
    • 当遇见右符号时弹出栈顶符号,并进行匹配
  • 结束
    • 成功:所有字符扫描完毕,且栈为空
    • 失败:匹配失败或所有字符扫描完毕但栈非空

7、编程实验:符号匹配问题

main.cpp

8、小结

  • 链式栈的实现组合使用了单链表对象
  • 在单链表的头部进行操作能够实现高效的入栈和出栈操作
  • 后进先出 的特性适用于检测成对出现的符号
  • 栈非常适合于需要 就近匹配 的场合

9、开放性问题

使用单链表对象实现链式栈时,为什么选择在单链表的头部进行操作?如果选择在尾部进行操作是否也能实现栈的功能?

三十六、队列的概念及实现(上)

1、队列的本质

  • 队列是一种特殊的线性表
  • 队列仅能在线性表的两端进行操作
    • 队头( Front ):取出数据元素的一端
    • 队尾( Rear ):插入数据元素的一端

2、队列的特性

先进先出( First In First Out )

3、队列的操作

  • 创建队列             Queue()
  • 销毁队列            ~Queue()
  • 清空队列             clear()
  • 进队列                 add( )
  • 出队列                      remove()
  • 获取队头元素       front()
  • 获取队列的长度     length()

4、队列的实现

5、队列的顺序实现

6、StaticQueue设计要点

  • 类模板
    • 使用原生数组作为队列的存储空间
    • 使用模板参数决定队列的最大容量

        

7、StaticQueue 实现要点(循环计数法)

  • 关键操作

  • 队列的状态

8、编程实验:基于顺序存储结构的队列

StaticQueue.h

#ifndef STATICQUEUE_H
#define STATICQUEUE_H

#include "Queue.h"
#include "Exception.h"

namespace DTLib
{

template < typename T, int N >
class StaticQueue : public Queue<T>
{
protected:
    T m_space[N];
    int m_front;
    int m_rear;
    int m_length;
public:
    StaticQueue()
    {
        m_front = 0;
        m_rear = 0;
        m_length = 0;
    }

    int capacity() const   // O(1)
    {
        return N;
    }

    void add(const T& e)    // O(1)
    {
        if( m_length < N )
        {
            m_space[m_rear] = e;// 为了异常安全,先复制后改动标识,因为是类型T可能存在异常
            m_rear = (m_rear + 1) % N;
            m_length++;
        }
        else
        {
            THROW_EXCEPTION(InvalidOperationException, "No space in current queue ...");
        }
    }

    void remove()    // O(1)
    {
        if( m_length > 0 )
        {
            m_front = (m_front + 1) % N;
            m_length--;
        }
        else
        {
            THROW_EXCEPTION(InvalidOperationException, "No element in current queue ...");
        }
    }

    T front() const   //  O(1)
    {
        if( m_length > 0 )
        {
            return m_space[m_front];
        }
        else
        {
            THROW_EXCEPTION(InvalidOperationException, "No element in current queue ...");
        }
    }

    void clear()   // O(1)
    {
        m_front = 0;
        m_rear = 0;
        m_length = 0;
    }

    int length() const   // O(1)
    {
        return m_length;
    }
};

}

#endif // STATICQUEUE_H

9、小结

  • 队列是一种特殊的线性表,具有先进先出的特性
  • 队列只允许在线性表的两端进行操作,一端进,一端出
  • StaticQueue使用原生数组作为内部存储空间
  • StaticQueue的最大容量由模板参数决定
  • StaticQueue采用循环计数法提高队列操作的效率

三十七、队列的概念及实现(下)

  • 当数据元素为类类型,StaticQueue 的对象在创建时,会多次调用元素类型的构造函数,影响效率。
  • 是的,所以我们需要实现链式队列。

1、队列的链式存储实现

2、链式队列的设计要点

  • 类模板,抽象父类Queue的直接子类
  • 在内部使用链式结构实现元素的存储
  • 只在链表的头部尾部进行操作

3、链式队列的设计要点

4、编程实验:基于LinkList的队列

LinkQueue.h

5、问题

使用LinkList类实现链式队列是否合适,是否有更好的方案?

时间复杂度较高

6、队列链式存储实现的优化

7、链式队列的设计优化

8、编程实验:基于Linux内核链表的队列

LinkQueue.h

#ifndef LINKQUEUE_H
#define LINKQUEUE_H

#include "Queue.h"
#include "LinuxList.h"
#include "Exception.h"

namespace DTLib
{

template < typename T >
class LinkQueue : public Queue<T>
{
protected:
    struct Node : public Object                                     // 结点类型
    {
        list_head head;                                             // Linux内核链表结点的结构体成员
        T value;                                                    // 数据域
    };

    list_head m_header;// 头结点
    int m_length;//当前队列长度
public:
    LinkQueue()   // O(1)
    {
        m_length = 0;

        INIT_LIST_HEAD(&m_header); // Linux链表头结点初始化
    }

    void add(const T& e)     // O(1)
    {
        Node* node = new Node();

        if( node != NULL )
        {
            node->value = e;

            list_add_tail(&node->head, &m_header);   // O(1)插入到链表的尾部

            m_length++;
        }
        else
        {
            THROW_EXCEPTION(NoEnoughMemoryException, "No memory to add new element ...");
        }
    }

    void remove()      // O(1)
    {
        if( m_length > 0 )
        {
            list_head* toDel = m_header.next;

            list_del(toDel);

            m_length--;

            delete list_entry(toDel, Node, head);
        }
        else
        {
            THROW_EXCEPTION(InvalidOperationException, "No element in current queue ...");
        }
    }

    T front() const    // O(1)
    {
        if( m_length > 0 )
        {
            return list_entry(m_header.next, Node, head)->value;
        }
        else
        {
            THROW_EXCEPTION(InvalidOperationException, "No element in current queue ...");
        }
    }

    void clear()    // O(n)
    {
        while( m_length > 0 )
        {
            remove();
        }
    }

    int length() const    // O(1)
    {
        return m_length;
    }


    ~LinkQueue()   // O(n)
    {
        clear();
    }
};

}

#endif // LINKQUEUE_H

9、小结

  • StaticQueue在初始化时可能多次调用元素类型的构造函数
  • LinkList的组合使用能够实现队列的功能但是不够高效
  • LinkQueue的最终实现组合使用Linux内核链表
  • LinkQueue中入队和出队操作可以在常量时间内完成

三十八、两个有趣的问题

1、问题

栈和队列在实现上非常类似,是否可以用栈实现队列?

2、问题分析

用栈实现队列等价于用后进先出的特性实现先进先出的特性!

3、解决方案设计

4、实现思路

  • 准备两个栈用于实现队列:stack_instack_out
    • 当有新元素入队时:将其压入stack_in
    • 当需要出队时:
      • stack_out.size() == 0 :
        • stack_in 中的元素逐一弹出并压入stack_out
        • stack_out的栈顶元素弹出
      • stack_out.size() > 0:
        • stack_out的栈顶元素弹出

5、编程实验:用栈实现队列

StackToQueue.h

6、问题

反之,是否可以用队列实现栈?

7、问题分析

本质为,用队列先进先出的特性实现栈后进先出的特性!

8、解决方案设计

9、实现思路

  • 准备两个队列用于实现栈:queue_1 [in]queue_2 [out]
    • 当有新元素入栈时:将其加入队列[in]
    • 当需要出栈时:
      • 将队列 [in] 中的n-1个元素出队列,并进入队列 [out]
      • 将队列 [in] 中的最后一个元素出队列(出栈)
      • 交换两个队列的角色:queue_1 [out]queue_2 [in]

10、编程实验:用队列实现栈

QueueToStack.h

11、小结

  • 栈和队列在实现上非常类似,可以相互转化实现
  • 两个栈 后进先出 叠加得到 先进先出 的特性
  • 两个队列 先进先出相互配合得到后进先出 的特性
  • 栈和队列相互转化的学习有助于强化本质的理解
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

数据结构实战开发教程(六)栈的概念及实现、队列的概念及实现、两个有趣的问题 的相关文章

随机推荐