zoukankan      html  css  js  c++  java
  • [知识点] 7.1 栈,队列与链表

    总目录 > 7 数据结构 > 7.1 栈,队列与链表

    前言

    马上要考数据结构了,停更一个星期后决定先把数据结构这一块复习一遍。

    子目录列表

    1、数据结构简介

    2、栈

    3、队列

    4、链表

    7.1 栈,队列与链表

    1、数据结构简介

    数据结构,顾名思义,计算机存储数据的结构。最简单的,变量,数组,都是数据结构。一般情况下对于数据结构的学习中,这些就直接略过而归入语言基础了,而数据结构专题往往从栈,队列与链表这三大基本结构开始介绍。

    程序的运行离不开数据结构,而不同的数据结构各有优劣,对于不同的问题,选取合适的数据结构能够大幅提升算法的效率

    2、栈

    ① 概念

    栈(stack)是最常见的线性数据结构之一,并且栈这个概念在计算机中经常被提及——汇编语言里有堆栈操作程序,平常编程时会提到栈溢出……

    那么栈到底是什么?从中文里似乎不能直接想到,那么从英文入手 —— stack,其本义为堆叠(n. & v.),想象一下,新学期开学了,学校下发了 3 本新教材,你想把它们整理好并堆成一叠放在寝室——先把数据结构放在桌上,再把汇编语言放在数据结构上面,再把计算机组成放在汇编语言上面,叠好了。这个过程是从下到上堆叠的。而现在开始上数据结构课了,如果你需要从里面取出教材,在不考虑直接从下面抽取的情况,则必须要从最上方的计算机组成开始取,然后再拿出汇编语言,最后才能拿到数据结构。

    这种结构的最大特点是——后加入的数据必须最先取出,简称为后进先出(LIFO, last in first out)

    我们将栈的最上方称为栈顶,最下方称为栈底,每次加入或弹出数据都只能从栈顶进行。

    汉诺塔问题也是个比较典型的栈——每次在堆叠的时候,我们必须将最上方那块最小的先移动到其他位置,才能移动下方的,以此类推——而上次介绍汉诺塔问题是在递归部分(请参见:2.2 递归与分治)。

    说到递归,我们发现它似乎和栈有异曲同工之妙——递归过程不也是对于最后进入的层必须最先处理吗?没错,递归其实本质也是栈结构,这就是为什么在介绍 DFS 的时候提到,DFS 是通过栈这个数据结构实现的(请参见:3.1 DFS / BFS 搜索

    汇编语言里提到的堆栈操作程序同理,将寄存器中的数据 PUSH 进去,最先 POP 出来的则是最后 PUSH 进去的。

    ② 常用操作

    那么对于这样一个数据结构,可以进行哪些操作?我们还是模拟一下取书的过程:

    > 首先肯定可以把书放入;

    > 然后可以看到最上方的书是什么(下方的书不能看到);

    > 然后可以取出最上方的书;

    > 可以对书本数量进行计数;

    > 可以判断是否还有书。

    那么这五个过程,可以分别对应栈的五种操作:

    > push:将元素加入栈顶;

    > top:获取栈顶元素;

    > pop:将栈顶元素取出;

    > size:获取栈内元素个数;

    > empty:判断栈是否为空。

    ③ 代码实现

    先给出一段手搓的栈的实现代码。

     1 #include <bits/stdc++.h>
     2 using namespace std;
     3 
     4 #define MAXN 1005
     5 
     6 class Stack {
     7     int stk[MAXN], tot;
     8 public:
     9     Stack() : tot(0) {}
    10     void push(int o) {
    11         tot++, stk[tot] = o;
    12     }
    13     int top() {
    14         return stk[tot];
    15     }
    16     void pop() {
    17         tot--;
    18     }
    19     int size() {
    20         return tot;
    21     }
    22     bool empty() {
    23         return tot;
    24     }
    25 };
    26 
    27 int main() {
    28     Stack s;
    29     s.push(1), s.push(2), s.push(3);
    30     cout << s.size() << ' ' << s.top() << endl;
    31     s.pop(), s.pop();
    32     cout << s.size() << ' ' << s.top() << endl;
    33     s.pop();
    34     cout << s.empty();
    35     return 0;    
    36 }

    五个操作应该都不用过多解释。

    如果使用的是 C++,那么学过 STL(请参见:1.3.1 STL 简介)应该就知道,可以使用 STL 中的 stack 来实现栈,其构造方式为:

    stack <Typename T, Container> s;

    其中 T 表示数据类型,除了 int, double 等基本类型之外,同样支持诸如类等更高级的类型。Container 为容器,可省略,省略则默认为 deque,同时标准容器 vector, list 也可以使用。关于容器的使用,暂时不介绍。

    STL 内的栈支持的五种方法同上,也就是说将上述代码主程序创建的栈从手工栈改成 STL 栈,对结果没有影响。感兴趣的可以去 <stack> 库看看实现方式。

    ④ 应用举例

    栈的应用和递归思想的应用基本是一致的,因为递归思想本身就是通过栈实现。常提的几种题型包括:

    (1) 进制转换

    (2) 括号匹配

    (3) 行编辑程序问题

    (4) 表达式求值

    (5) 汉诺塔问题

    等等。

    2、队列

    ① 概念

    和栈通常一起提及的另一种数据结构——队列(queue),这个概念就很好理解了,因为生活中随处可见排队的现象,对于一列排队买包子的人,必然是先开始排队的先买到包子,则和栈结构恰好相反,队列结构的最大特点是——先加入的数据最先取出,简称为先进先出(FIFO, first in first out)

    我们将队列的最前方称为队首,最尾端称为队尾,每次加入数据都只能从队尾加入,而每次弹出数据只能从队首弹出。

    ② 常用操作

    对于队列,可以进行的操作和栈类似:

    > push:将元素加入队尾;

    > front:获取队首元素;

    > back:获取队尾元素;

    > pop:将队首元素取出;

    > size:获取队列中元素个数;

    > empty:判断队列是否为空。

    ③ 代码实现

    由于栈只需要对栈顶元素进行操作,所以只需要一个变量 size,就可以同时完成加入,访问,取出,获取元素,判断是否为空 5 种操作。队列则因为加入和弹出分别在队首和队尾,则需要两个变量 head 和 tail

     1 #include <bits/stdc++.h>
     2 using namespace std;
     3 
     4 #define MAXN 1005
     5 
     6 class Queue {
     7     int q[MAXN], head, tail;
     8 public:
     9     Queue() : head(1), tail(0) {}
    10     void push(int o) {
    11         tail++, q[tail] = o;
    12     }
    13     int back() {
    14         return q[tail];
    15     } 
    16     int front() {
    17         return q[head];
    18     }
    19     void pop() {
    20         head++;
    21     }
    22     int size() {
    23         return tail - head + 1;
    24     }
    25     bool empty() {
    26         return tail - head > 0;
    27     }
    28 };
    29 
    30 int main() {
    31     Queue q;
    32     q.push(1), q.push(2), q.push(3);
    33     cout << q.size() << endl;
    34     cout << q.front() << ' ' << q.back() << endl;
    35     q.pop(), q.pop();
    36     cout << q.size() << endl;
    37     q.pop();
    38     cout << q.empty();
    39     return 0;    
    40 }

    上述代码将队列封装在一个类中了,目的是规范化,其实通常可以不用这样规矩,简单一点的框架伪代码可以为:

    1 int head = 1, tail = 2;
    2 while (head != tail) {
    3     int o = q[head];
    4     for (...) {
    5         if (...)
    6             tail++, q[tail] = v;
    7     }
    8     head++;
    9 }

    同样地,如果使用的是 C++,可以使用 STL 队列。其构造方式为:

    queue <Typename T, Container> q;

    和 stack 一样,它们都是容器适配器,允许加入容器 Container。

    STL 队列支持的六种方法同上。

    ④ 应用举例

    DFS 是用栈实现的,而 BFS(请参见:3.1 DFS / BFS 搜索)是用队列实现的。在清晰认识到队列的原理后,对 BFS 的理解应该就轻松多了。

    ⑤ 其他队列

    > 循环队列

    在数据量较大的情况下,为了防止溢出,可以使队列首尾相连进行循环,即 x 的后继为 (x + 1) % size,但这样可能会覆盖有效数据。

    > 双端队列

    队首队尾均支持插入和弹出元素操作。

    > 优先队列

    请参见:7.4 堆与优先队列

    3、链表

    ① 链表与数组

    常见的数组,和上述的栈和队列,它们的共同特点是都属于线性结构,在数据结构中称之为线性表。除此之外,和数组类似的还有一种线性表——链表(linked-list)

    数组的数据是相互独立的,对于任意一个元素,它只和它在数组中的位置有对应关系,所以在访问数据时时间复杂度为 O(1),而插入、删除数据为 O(n),因为一个元素的增加或减少要影响接下来所有的元素。

    链表则相反——它的结构类似于链子,元素本身没有固定的位置,而是靠元素和元素之间的位置关系来维持结构,下图体现了数组和链表的区别:

    对于链表,访问数据时间复杂度为 O(n)插入、删除数据为 O(1)。上图展示的是单向链表,每个元素有一个后继指针,同时还有双向链表,即每个元素有前驱指针和后继指针两个。下面先介绍单向链表。

    ② 构建

    class Node {
        int v;
        Node *nxt;
    };

    这是采用指针形式单向链表构建方式。Node 为结点,其包含两个数据成员:v 为该结点的值,*nxt 为其后继结点的指针,用以链接元素。当然也可以不使用指针而直接定义一个变量,但就需要更多的空间了。

    ③ 常用操作

    相比栈和队列,链表可操作性更强,操作的具体流程也更为复杂。这里先列举最常规的操作:

    > insert:在任意位置插入一个元素

    给出一个具体位置(指针 p),直接实现 O(1) 插入,举例:

    假设指针 p 指向 5,则表示在 5 后面插入元素。先建立一个新结点,将 9 存入该结点,再将 5 的后继指针赋值给 9 的后继指针,而后将 5 的后继指针指向 9,插入就完成了。这是从中间插入的过程,由于是插入到给定指针的元素后面,如果要在链表头插入数据,则需要特判,同时,要维护链表的头指针,如果从头插入,则头指针需要同时修改。

    代码实现见下方。

    > erase:删除任意位置的一个元素

    单向链表的删除操作相对难理解一些。因为没有前驱指针,我们只能通过将被删除元素的后继元素覆盖到被删除的元素上,如图:

    图中可以看出,虽然我们确实将 5 删除了,但其实是把 7 赋值到原来 5 所在的结点,再通过修改指针将 7 所在的那个结点删除了,通过这种方式间接删去了 5。

    > size:获取链表内元素个数

    > empty:判断链表是否为空

    ④ 代码实现

     1 class List {
     2     int tot;
     3     Node *head;
     4 public:
     5     void insert(int o) {
     6         Node *node = new Node;
     7         node -> v = o;
     8         node -> nxt = head;
     9         head -> pre = node;
    10         head = node;
    11         tot++;
    12     }
    13     void insert(int o, Node *p) {
    14         Node *node = new Node;
    15         node -> v = o;
    16         node -> nxt = p -> nxt;
    17         node -> pre = p;
    18         p -> nxt -> pre = node; 
    19         p -> nxt = node;
    20         tot++;
    21     }
    22     void erase(Node *p) {
    23         p -> pre -> nxt = p -> nxt;
    24         p -> nxt -> pre = p -> pre;
    25         delete p;
    26         tot--;
    27     }
    28     int size() {
    29         return tot;
    30     }
    31     bool empty() {
    32         return tot;
    33     }
    34 };

    ⑤ 双向链表与 list

    双向链表很好理解,新增加一个前驱指针后,可以进行反向遍历,对应的操作的具体实现会有所不同,比如删除操作就不再需要围魏救赵了。下面直接给出代码。

     1 class List {
     2     int tot;
     3     Node *head;
     4 public:
     5     void insert(int o) {
     6         Node *node = new Node;
     7         node -> v = o;
     8         node -> nxt = head;
     9         head = node;
    10         tot++;
    11     }
    12     void insert(int o, Node *p) {
    13         Node *node = new Node;
    14         node -> v = o;
    15         node -> nxt = p -> nxt;
    16         p -> nxt = node;
    17         tot++;
    18     }
    19     void erase(Node *p) {
    20         p -> v = p -> nxt -> v;
    21         Node *node = p -> nxt;
    22         p -> nxt = p -> nxt -> nxt;
    23         tot--;
    24     }
    25     int size() {
    26         return tot;
    27     }
    28     bool empty() {
    29         return tot;
    30     }
    31 };

    C++ 的 STL 中提供的是双向链表 list,功能相当丰富,提供的成员方法多达三十多种,除了上述几种操作之外,还有诸如:clear 清空链表,remove 删除特定值元素,sort 链表排序,unique 元素去重等等。

    不同于 stack 和 queue 是容器适配器,list 就是容器,需要通过迭代器 iterator 访问元素,上面给出的单向和双向链表中其实都略过了查询操作,关于 STL 容器的介绍请参见:<施工中>,此处不深入解释。

    ⑤ 循环链表

    链表同样可以循环,即链表头和链表尾相连。

    ⑥ 应用举例

    > 图的存储结构之一——邻接链表,请参见:8.2 图的存储与遍历

    > 解决哈希冲突的链地址法,请参见:7.2 哈希表

  • 相关阅读:
    [OpenGL ES 071]光照原理
    [OpenGL ES 03]3D变换:模型,视图,投影与Viewport
    [日志]当今最流行的网络生僻字,很火
    [日志]关于茶的基础知识
    [健康]快速除牙痛的八个小验方
    [日志]我们生活中的潜规则
    [日志]做事要方,做人要圆
    [日志]家居装修花钱看你怎么省
    [日志]非常宝贵的工作经验
    [日志]你用的着的一些家装尺寸数据
  • 原文地址:https://www.cnblogs.com/jinkun113/p/12979964.html
Copyright © 2011-2022 走看看