数据结构二之栈和队列、字符串、数组和广义表

 

数据结构线性表的学习,让我们基本了解了数据结构的基本知识,数据结构就是对数据的操作,是独立于C语言的一门独立学科,我们这里只不过以C语言为基础进行学习数据结构,大家在学习其它语言,数据结构实现基本思想是一致的。

栈和队列,严格意义上来说,也属于线性表,因为它们也都用于存储逻辑关系为 "一对一" 的数据。

同顺序表和链表一样,栈也是用来存储逻辑关系为 "一对一" 数据的线性存储结构,如图 1 所示。

图 1 栈存储结构示意图

从图 1 我们看到,栈存储结构与之前所学的线性存储结构有所差异,这缘于栈对数据 "存" 和 "取" 的过程有特殊的要求:

  1. 栈只能从表的一端存取数据,另一端是封闭的,如图 1 所示;
  2. 在栈中,无论是存数据还是取数据,都必须遵循"先进后出"的原则,即最先进栈的元素最后出栈。拿图 1 的栈来说,从图中数据的存储状态可判断出,元素 1 是最先进的栈。因此,当需要从栈中取出元素 1 时,根据"先进后出"的原则,需提前将元素 3 和元素 2 从栈中取出,然后才能成功取出元素 1。

因此,我们可以给栈下一个定义,即栈是一种只能从表的一端存取数据且遵循 "先进后出" 原则的线性存储结构。

通常,栈的开口端被称为栈顶;相应地,封口端被称为栈底。因此,栈顶元素指的就是距离栈顶最近的元素,拿图 2 来说,栈顶元素为元素 4;同理,栈底元素指的是位于栈最底部的元素,图 2 中的栈底元素为元素 1。

图 2 栈顶和栈底

进栈和出栈

基于栈结构的特点,在实际应用中,通常只会对栈执行以下两种操作:

  • 向栈中添加元素,此过程被称为"进栈"(入栈或压栈);
  • 从栈中提取出指定元素,此过程被称为"出栈"(或弹栈);

栈的具体实现

栈是一种 "特殊" 的线性存储结构,因此栈的具体实现有以下两种方式:

  1. 顺序栈:采用顺序存储结构可以模拟栈存储数据的特点,从而实现栈存储结构;
  2. 链栈:采用链式存储结构实现栈结构;

两种实现方式的区别,仅限于数据元素在实际物理空间上存放的相对位置,顺序栈底层采用的是数组,链栈底层采用的是链表。有关顺序栈和链栈的具体实现会在后续章节中作详细讲解。

栈的应用

基于栈结构对数据存取采用 "先进后出" 原则的特点,它可以用于实现很多功能。

例如,我们经常使用浏览器在各种网站上查找信息。假设先浏览的页面 A,然后关闭了页面 A 跳转到页面 B,随后又关闭页面 B 跳转到了页面 C。而此时,我们如果想重新回到页面 A,有两个选择:

  • 重新搜索找到页面 A;
  • 使用浏览器的"回退"功能。浏览器会先回退到页面 B,而后再回退到页面 A。

浏览器 "回退" 功能的实现,底层使用的就是栈存储结构。当你关闭页面 A 时,浏览器会将页面 A 入栈;同样,当你关闭页面 B 时,浏览器也会将 B入栈。因此,当你执行回退操作时,才会首先看到的是页面 B,然后是页面 A,这是栈中数据依次出栈的效果。

不仅如此,栈存储结构还可以帮我们检测代码中的括号匹配问题。多数编程语言都会用到括号(小括号、中括号和大括号),括号的错误使用(通常是丢右括号)会导致程序编译错误,而很多开发工具中都有检测代码是否有编辑错误的功能,其中就包含检测代码中的括号匹配问题,此功能的底层实现使用的就是栈结构。

同时,栈结构还可以实现数值的进制转换功能。例如,编写程序实现从十进制数自动转换成二进制数,就可以使用栈存储结构来实现。

以上也仅是栈应用领域的冰山一角,这里不再过多举例。在后续章节的学习中,我们会大量使用到栈结构。接下来,我们学习如何实现顺序栈和链栈,以及对栈中元素进行入栈和出栈的操作。

顺序栈及基本操作(入栈和出栈)

顺序栈,即用顺序表实现栈存储结构。通过前面的学习我们知道,使用栈存储结构操作数据元素必须遵守 "先进后出" 的原则,本节就 "如何使用顺序表模拟栈以及实现对栈中数据的基本操作(出栈和入栈)" 给大家做详细介绍。

如果你仔细观察顺序表(底层实现是数组)和栈结构就会发现,它们存储数据的方式高度相似,只不过栈对数据的存取过程有特殊的限制,而顺序表没有。

例如,我们先使用顺序表(a 数组)存储 {1,2,3,4},存储状态如图 1 所示:

图 1 顺序表存储 {1,2,3,4}

同样,使用栈存储结构存储 {1,2,3,4},其存储状态如图 2 所示:

图 2 栈结构存储 {1,2,3,4}

通过图 1 和图 2 的对比不难看出,使用顺序表模拟栈结构很简单,只需要将数据从 a 数组下标为 0 的位置依次存储即可。

从数组下标为 0 的模拟栈存储数据是常用的方法,从其他数组下标处存储数据也完全可以,这里只是为了方便初学者理解。

了解了顺序表模拟栈存储数据后,接下来看如何模拟栈中元素出栈的操作。由于栈对存储元素出栈的次序有"先进后出"的要求,如果想将图 1 中存储的元素 1 从栈中取出,需先将元素 4、元素 3 和元素 2 依次从栈中取出。

这里给出使用顺序表模拟栈存储结构常用的实现思路,即在顺序表中设定一个实时指向栈顶元素的变量(一般命名为 top),top 初始值为 -1,表示栈中没有存储任何数据元素,及栈是"空栈"。一旦有数据元素进栈,则 top 就做 +1 操作;反之,如果数据元素出栈,top 就做 -1 操作。

顺序栈元素"入栈"

比如,还是模拟栈存储 {1,2,3,4} 的过程。最初,栈是"空栈",即数组是空的,top 值为初始值 -1,如图 3 所示:

图 3 空栈示意图

首先向栈中添加元素 1,我们默认数组下标为 0 一端表示栈底,因此,元素 1 被存储在数组 a[1] 处,同时 top 值 +1,如图 4 所示:

图 4 模拟栈存储元素 1

采用以上的方式,依次存储元素 2、3 和 4,最终,top 值变为 3,如图 5 所示:

图 5 模拟栈存储{1,2,3,4}因此,C 语言实现代码为:

    //元素elem进栈,a为数组,top值为当前栈的栈顶位置
    int push(int* a,int top,int elem){
        a[++top]=elem;
        return top;
    }

代码中的 a[++top]=elem,等价于先执行 ++top,再执行 a[top]=elem。

顺序栈元素"出栈"

其实,top 变量的设置对模拟数据的 "入栈" 操作没有实际的帮助,它是为实现数据的 "出栈" 操作做准备的。

比如,将图 5 中的元素 2 出栈,则需要先将元素 4 和元素 3 依次出栈。需要注意的是,当有数据出栈时,要将 top 做 -1 操作。因此,元素 4 和元素 3 出栈的过程分别如图 6a) 和 6b) 所示:

图 6 数据元素出栈

注意,图 6 数组中元素的消失仅是为了方便初学者学习,其实,这里只需要对 top 值做 -1 操作即可,因为 top 值本身就表示栈的栈顶位置,因此 top-1 就等同于栈顶元素出栈。并且后期向栈中添加元素时,新元素会存储在类似元素 4 这样的旧元素位置上,将旧元素覆盖。

元素 4 和元素 3 全部出栈后,元素 2 才能出栈。因此,使用顺序表模拟数据出栈操作的 C 语言实现代码为:
 

    //数据元素出栈
    int pop(int * a,int top){
        if (top==-1) {
            printf("空栈");
            return -1;
        }
        printf("弹栈元素:%d\n",a[top]);
        top--;
        return top;
    }

代码中的 if 语句是为了防止用户做 "栈中已无数据却还要数据出栈" 的错误操作。代码中,关于对栈中元素出栈操作的实现,只需要 top 值 -1 即可。

总结

通过学习顺序表模拟栈中数据入栈和出栈的操作,初学者完成了对顺序栈的学习,这里给出顺序栈及对数据基本操作的 C 语言完整代码:


    #include <stdio.h>
    //元素elem进栈
    int push(int* a,int top,int elem){
        a[++top]=elem;
        return top;
    }
    //数据元素出栈
    int pop(int * a,int top){
        if (top==-1) {
            printf("空栈");
            return -1;
        }
        printf("弹栈元素:%d\n",a[top]);
        top--;
        return top;
    }
    int main() {
        int a[100];
        int top=-1;
        top=push(a, top, 1);
        top=push(a, top, 2);
        top=push(a, top, 3);
        top=push(a, top, 4);
        top=pop(a, top);
        top=pop(a, top);
        top=pop(a, top);
        top=pop(a, top);
        top=pop(a, top);
        return 0;
    }

程序输出结果为:

链栈及基本操作(入栈和出栈)

链栈,即用链表实现栈存储结构。

链栈的实现思路同顺序栈类似,顺序栈是将数顺序表(数组)的一端作为栈底,另一端为栈顶;链栈也如此,通常我们将链表的头部作为栈顶,尾部作为栈底,如图 1 所示:

图 1 链栈示意图

将链表头部作为栈顶的一端,可以避免在实现数据 "入栈" 和 "出栈" 操作时做大量遍历链表的耗时操作。

链表的头部作为栈顶,意味着:

  • 在实现数据"入栈"操作时,需要将数据从链表的头部插入;
  • 在实现数据"出栈"操作时,需要删除链表头部的首元节点;

因此,链栈实际上就是一个只能采用头插法插入或删除数据的链表。

链栈元素入栈

例如,将元素 1、2、3、4 依次入栈,等价于将各元素采用头插法依次添加到链表中,每个数据元素的添加过程如图 2 所示:

图 2 链栈元素依次入栈过程示意图

C语言实现代码为:

    //链表中的节点结构
    typedef struct lineStack{
        int data;
        struct lineStack * next;
    }lineStack;
    //stack为当前的链栈,a表示入栈元素
    lineStack* push(lineStack * stack,int a){
        //创建存储新元素的节点
        lineStack * line=(lineStack*)malloc(sizeof(lineStack));
        line->data=a;
        //新节点与头节点建立逻辑关系
        line->next=stack;
        //更新头指针的指向
        stack=line;
        return stack;
    }

链栈元素出栈

例如,图 2e) 所示的链栈中,若要将元素 3 出栈,根据"先进后出"的原则,要先将元素 4 出栈,也就是从链表中摘除,然后元素 3 才能出栈,整个操作过程如图 3 所示:

图 3 链栈元素出栈示意图因此,实现栈顶元素出链栈的 C 语言实现代码为:

    //栈顶元素出链栈的实现函数
    lineStack * pop(lineStack * stack){
        if (stack) {
            //声明一个新指针指向栈顶节点
            lineStack * p=stack;
            //更新头指针
            stack=stack->next;
            printf("出栈元素:%d ",p->data);
            if (stack) {
                printf("新栈顶元素:%d\n",stack->data);
            }else{
                printf("栈已空\n");
            }
            free(p);
        }else{
            printf("栈内没有元素");
            return stack;
        }
        return stack;
    }

代码中通过使用 if 判断语句,避免了用户执行"栈已空却还要数据出栈"错误操作。

总结

本节,通过采用头插法操作数据的单链表实现了链栈结构,这里给出链栈及基本操作的C语言完整代码:
 

    #include <stdio.h>
    #include <stdlib.h>
    typedef struct lineStack{
        int data;
        struct lineStack * next;
    }lineStack;
    lineStack* push(lineStack * stack,int a){
        lineStack * line=(lineStack*)malloc(sizeof(lineStack));
        line->data=a;
        line->next=stack;
        stack=line;
        return stack;
    }
    lineStack * pop(lineStack * stack){
        if (stack) {
            lineStack * p=stack;
            stack=stack->next;
            printf("弹栈元素:%d ",p->data);
            if (stack) {
                printf("栈顶元素:%d\n",stack->data);
            }else{
                printf("栈已空\n");
            }
            free(p);
        }else{
            printf("栈内没有元素");
            return stack;
        }
        return stack;
    }
    int main() {
        lineStack * stack=NULL;
        stack=push(stack, 1);
        stack=push(stack, 2);
        stack=push(stack, 3);
        stack=push(stack, 4);
        stack=pop(stack);
        stack=pop(stack);
        stack=pop(stack);
        stack=pop(stack);
        stack=pop(stack);
        return 0;
    }

程序运行结果为:

队列

队列,和栈一样,也是一种对数据的"存"和"取"有严格要求的线性存储结构。

与栈结构不同的是,队列的两端都"开口",要求数据只能从一端进,从另一端出,如图 1 所示:

图 1 队列存储结构

通常,称进数据的一端为 "队尾",出数据的一端为 "队头",数据元素进队列的过程称为 "入队",出队列的过程称为 "出队"。

不仅如此,队列中数据的进出要遵循 "先进先出" 的原则,即最先进队列的数据元素,同样要最先出队列。拿图 1 中的队列来说,从数据在队列中的存储状态可以分析出,元素 1 最先进队,其次是元素 2,最后是元素 3。此时如果将元素 3 出队,根据队列 "先进先出" 的特点,元素 1 要先出队列,元素 2 再出队列,最后才轮到元素 3 出队列。

栈和队列不要混淆,栈结构是一端封口,特点是"先进后出";而队列的两端全是开口,特点是"先进先出"。

因此,数据从表的一端进,从另一端出,且遵循 "先进先出" 原则的线性存储结构就是队列。

队列存储结构的实现有以下两种方式:

  1. 顺序队列:在顺序表的基础上实现的队列结构;
  2. 链队列:在链表的基础上实现的队列结构;

两者的区别仅是顺序表和链表的区别,即在实际的物理空间中,数据集中存储的队列是顺序队列,分散存储的队列是链队列。

实际生活中,队列的应用随处可见,比如排队买 XXX、医院的挂号系统等,采用的都是队列的结构。

拿排队买票来说,所有的人排成一队,先到者排的就靠前,后到者只能从队尾排队等待,队中的每个人都必须等到自己前面的所有人全部买票成功并从队头出队后,才轮到自己买票。这就不是典型的队列结构吗?

顺序队列

顺序队列,即采用顺序表模拟实现的队列结构。

我们知道,队列具有以下两个特点:

  1. 数据从队列的一端进,另一端出;
  2. 数据的入队和出队遵循"先进先出"的原则;

因此,只要使用顺序表按以上两个要求操作数据,即可实现顺序队列。首先来学习一种最简单的实现方法。

顺序队列简单实现

由于顺序队列的底层使用的是数组,因此需预先申请一块足够大的内存空间初始化顺序队列。除此之外,为了满足顺序队列中数据从队尾进,队头出且先进先出的要求,我们还需要定义两个指针(top 和 rear)分别用于指向顺序队列中的队头元素和队尾元素,如图 1 所示:

图 1 顺序队列实现示意图

由于顺序队列初始状态没有存储任何元素,因此 top 指针和 rear 指针重合,且由于顺序队列底层实现靠的是数组,因此 top 和 rear 实际上是两个变量,它的值分别是队头元素和队尾元素所在数组位置的下标。

在图 1 的基础上,当有数据元素进队列时,对应的实现操作是将其存储在指针 rear 指向的数组位置,然后 rear+1;当需要队头元素出队时,仅需做 top+1 操作。

例如,在图 1 基础上将 {1,2,3,4} 用顺序队列存储的实现操作如图 2 所示:

图 2 数据进顺序队列的过程实现示意图

在图 2 基础上,顺序队列中数据出队列的实现过程如图 3 所示:

图 3 数据出顺序队列的过程示意图

因此,使用顺序表实现顺序队列最简单方法的 C 语言实现代码为:
 

    #include <stdio.h>
    int enQueue(int *a,int rear,int data){
        a[rear]=data;
        rear++;
        return rear;
    }
    void deQueue(int *a,int front,int rear){
        //如果 front==rear,表示队列为空
        while (front!=rear) {
            printf("出队元素:%d\n",a[front]);
            front++;
        }
    }
    int main() {
        int a[100];
        int front,rear;
        //设置队头指针和队尾指针,当队列中没有元素时,队头和队尾指向同一块地址
        front=rear=0;
        //入队
        rear=enQueue(a, rear, 1);
        rear=enQueue(a, rear, 2);
        rear=enQueue(a, rear, 3);
        rear=enQueue(a, rear, 4);
        //出队
        deQueue(a, front, rear);
        return 0;
    }

程序输出结果:

此方法存在的问题

先来分析以下图 2b) 和图 3b)。图 2b) 是所有数据进队成功的示意图,而图 3b) 是所有数据全部出队后的示意图。通过对比两张图,你会发现,指针 top 和 rear 重合位置指向了  a[4] 而不再是 a[0]。也就是说,整个顺序队列在数据不断地进队出队过程中,在顺序表中的位置不断后移。

顺序队列整体后移造成的影响是:

  • 顺序队列之前的数组存储空间将无法再被使用,造成了空间浪费;
  • 如果顺序表申请的空间不足够大,则直接造成程序中数组 a 溢出,产生溢出错误;

为了避免以上两点,我建议初学者使用下面的方法实现顺序队列。

顺序队列另一种实现方法

既然明白了上面这种方法的弊端,那么我们可以试着在它的基础上对其改良。

为了解决以上两个问题,可以使用巧妙的方法将顺序表打造成一个环状表,如图 4 所示:

图 4 环状顺序队列

图 4 只是一个想象图,在真正的实现时&#

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值