【C语言数据结构5】--栈的实现

简介: 栈是一种特殊的线性表,我们可以认为栈是一种阉割版的线性表。它的插入、删除操作只能在栈顶进行。因此造就了它后进先出(LIFO)的特征。

一、什么是栈

栈是一种特殊的线性表,我们可以认为栈是一种阉割版的线性表。它的插入、删除操作只能在栈顶进行。因此造就了它后进先出(LIFO)的特征。

在生活中有很多栈的例子,比如平时吃的冰糖葫芦,我们需要先吃掉上面的才能吃下面的(虽然不是这样,但是希望大家配合一下)。又比如我们脱衣服,要把外面的衣服脱了才能脱里面的衣服。

网络异常,图片无法展示
|

我们可以看一下下面的图片:

网络异常,图片无法展示
|

中间部分就是一个栈,而栈最顶端的部分就是栈顶。栈最常用的两个操作就是进栈(入栈)和出栈操作。这组操作就是插入删除操作,它们只允许在栈顶进行。

进栈操作就是将新数据放入栈顶的上方(逻辑上),然后变成栈的新栈顶。而出栈操作则是将栈顶元素删除,然后让栈顶下方的元素作为栈顶。

二、栈的表示

我们可以用顺序存储和链式存储两种结构来实现栈,下面我们分别看看用两种存储结构如何表示一个栈。

(1)顺序存储结构

这里和顺序表的表示是类似的,但是我们需要设置一个栈顶指针。

#define MAXSIZE 20
typedef int ElemType;
typedef struct{
    ElemType data[MAXSIZE];   //静态数组,用于存放栈的元素
    int top;           //栈顶指针
}SqStack;
复制代码

在结构体中定义了一个静态数组用于存放栈中的数据,另外还需要定义一个栈顶指针。栈顶指针会一直指向数组中最后一个元素的下标(栈顶元素)。

(2)链式存储结构

链式存储结构实现的栈和单链表是类似的,结构体如下:

typedef struct SNode{
    ElemType data;      //数据域
    struct SNode *next;   //指针域
}*LinkedStack;
复制代码

这里就不详细解释了。

因为栈的操作是限制在栈顶的,因此不存在线性表中大量移动数据的问题,因此我们选择用顺序存储结构来实现栈的各个操作。

三、栈的实现

(1)栈的初始化

顺序存储我们同样可以使用两种方式来实现,一种是通过静态数组的方式,另一种是通过malloc函数动态申请内存的方式。

对于前者我们的初始化只需要初始化栈顶,对于后者我们还需要使用malloc分配内存。两种差别不大,这里我们选择用前者实现。

/**
* 用于初始化栈S
*/
int InitSqStack(SqStack *S){
    //将栈顶指向-1
    S->top = -1;
    return 1;
}
复制代码

在前面我们说过,栈顶指针会一直指向栈顶元素的下标。这里之所以不设置为0是因为,如果栈顶为0,那就表示栈中又一个元素。因此初始化时我们设置为-1。

(2)入栈

入栈的图示如下:

网络异常,图片无法展示
|

入栈操作我们需要判断栈是否满了,如果满了则返回0,如果没满则需要移动栈顶指针,再将元素入栈。

int Push(SqStack *S, ElemType elem){
    //如果栈满了,则top == MAXSIZE-1,此时不进行入栈操作,返回0
    if(S->top == MAXSIZE-1){
        return 0;
    } 
    //移动栈顶指针
    S->top++;
    //将入栈元素放置在栈顶
    S->data[S->top] = elem;
    return 1;
}
复制代码

我们还可以将移动指针和元素放入栈顶的操作合并成一句:

//先进行++S->top移动指针,再将元素赋值到新top的位置
S->data[++S->top] = elem;
复制代码

不过鉴于可读性差了许多。作者认为,在代码不会影响执行效率的情况下,还是不要为了追求代码简短而放弃可读性。

(3)出栈

出栈操作和入栈操作是相反的。图示如下:

网络异常,图片无法展示
|

我们先判断是否栈空,如果栈中有数据,则先获取数据,再移动栈顶指针。

int Pop(SqStack *S, ElemType *elem){
    //如果栈为空,则top==-1,此时不进行出栈操作
    if(S->top == -1){
        return 0;
    }
    //获取栈顶元素
    *elem = S->data[S->top];
    //移动栈顶指针
    S->top--;
    return 1;
}
复制代码

因为我们要获取出栈的数据,因此这里传入的elem是一个指针。

(4)清空栈

清空栈的操作我们只需要完成逻辑上的清空即可,即将栈的top赋值为-1,代码如下:

void ClearStack(SqStack *S){
    //将栈顶指向-1
    S->top = -1;
}
复制代码

(5)栈判空

栈判空我们只需要判断top是否指向-1即可:

int EmptyStack(SqStack S){
    //如果S.top==-1则返回1,如果S.top!=-1则返回0
    return S.top == -1 ? 1 : 0;
}
复制代码

上面使用了三元运算符,我们可以把上面的代码翻译成如下:

int EmptyStack(SqStack S){
    if(S.top == -1){
        return 1;
    }else{
        return 0;
    }
}
复制代码

(6)获取栈顶元素

除了出栈,我们还可以提供一个获取栈顶元素的操作。它和出栈的区别就是它不会移动栈顶指针(不会删除栈中元素),因此它的操作要比出栈简单:

int GetTop(SqStack S, ElemType *elem){
    if(S.top == -1){
        return 0;
    }
    *elem = S.data[S.top];
    return 1;
}
复制代码

除了上面的操作,我们还可以实现很多其它操作。这里就不再细说了。

四、栈的应用

栈有很多应用,可能刚开始接触栈会觉得这种结构非常多余。但是其实操作系统很多功能都是基于栈这种结构的,像是数值运算,程序递归,括号匹配等都可以用栈来实现。下面我们来实现几个简单的例子。

(1)逆序输出

现在我们要求实现输入一个序列,然后按照与这个序列输入相反的顺序输出。

这里我们利用栈先进后出的特性。我们将序列的数据依次入栈,再依次出栈输出即可达到逆序输出效果,代码如下:

void ReversePrint(int n){
    ElemType temp;
    SqStack S;
    InitSqStack(&S);
    //将输入元素依次入栈
    for(int i = 0; i < n; ++i){
        scanf("%d", &temp);
        Push(&S, temp);
    }
    //将元素出栈并输出
    for(int i = 0; i < n; ++i){
        Pop(&S, &temp);
        printf("%d\t", temp);
    }
}
复制代码

(2)进制转换

在进制转换中,栈同样扮演者逆序输出的作用。

将10进制数N转换成d进制数的原理如下:

image.png

其中%为取模操作。我们先对十进制数进行除操作,然后将除的结果取模。最终N的d进制数为:

image.png

我们实际操作一下,这里N取1348,d取8:

N N/d N%d
1348 168 4
168 21 0
21 2 5
2 0 2

最后得出10进制数1348的8进制为2504。下面我们用代码来实现一下:

void conversion(){
    int n;
    //用于获取出栈元素
    ElemType e;
    SqStack S;
    InitSqStack(&S);
    scanf("%d", &n);
    while (n){
        //把式中s放入栈
        Push(&S, n % 8);
        n = n / 8;
    }
    while (!EmptyStack(S)){
        //逆序输出
        Pop(&S, &e);
        printf("%d\t", e);
    }
}
复制代码

(3)括号匹配

括号匹配的大致步骤如下:

  1. 依次判断字符串内容
  2. 如果是左括号则直接入栈
  3. 如果是右括号则匹配栈顶是否是对应的右括号
  4. 如果匹配成功则入栈继续匹配,如果匹配失败则返回0
  5. 遍历完字符后,如果栈为空,则匹配成功。如果栈非空,则匹配失败

我们可以用几个例子模拟一下:

([)]
()()()
([][]{})
([][]{}))
复制代码

这里就不详细分析了,下面是具体代码:

int match(char str[], int len){
    ElemType elem;
    SqStack S;
    InitSqStack(&S);
    for(int i = 0; i < len; i++){
        //如果是左括号则直接入栈
        if(str[i] == '(' || str[i] == '[' || str[i] == '{'){
            Push(&S, str[i]);
        //如果是右括号,则进行匹配
        }else if(str[i] == ')'){
            //匹配成功则出栈
            if(GetTop(S, &elem) && elem == '('){
                Pop(&S, &elem);
            }else{
                return 0;
            }
        }else if(str[i] == ']'){
            if(GetTop(S, &elem) && elem == '['){
                Pop(&S, &elem);
            }else{
                return 0;
            }
        }else if(str[i] == '}'){
            if(GetTop(S, &elem) && elem == '{'){
                Pop(&S, &elem);
            }else{
                return 0;
            }
        }
    }
    if(EmptyStack(S)){
        return 1;
    }else{
        return 0;
    }
}
复制代码

另外大家可以用栈实现一些其它算法。



目录
相关文章
|
2月前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
58 1
|
2月前
|
存储 算法 搜索推荐
【趣学C语言和数据结构100例】91-95
本文涵盖多个经典算法问题的C语言实现,包括堆排序、归并排序、从长整型变量中提取偶数位数、工人信息排序及无向图是否为树的判断。通过这些问题,读者可以深入了解排序算法、数据处理方法和图论基础知识,提升编程能力和算法理解。
57 4
|
2月前
|
存储 机器学习/深度学习 搜索推荐
【趣学C语言和数据结构100例】86-90
本文介绍并用C语言实现了五种经典排序算法:直接插入排序、折半插入排序、冒泡排序、快速排序和简单选择排序。每种算法都有其特点和适用场景,如直接插入排序适合小规模或基本有序的数据,快速排序则适用于大规模数据集,具有较高的效率。通过学习这些算法,读者可以加深对数据结构和算法设计的理解,提升解决实际问题的能力。
51 4
|
2月前
|
存储 算法 数据处理
【趣学C语言和数据结构100例】81-85
本文介绍了五个经典算法问题及其C语言实现,涵盖图论与树结构的基础知识。包括使用BFS求解单源最短路径、统计有向图中入度或出度为0的点数、统计无向无权图各顶点的度、折半查找及二叉排序树的查找。这些算法不仅理论意义重大,且在实际应用中极为广泛,有助于提升编程能力和数据结构理解。
54 4
|
2月前
|
算法 数据可视化 数据建模
【趣学C语言和数据结构100例】76-80
本文介绍了五种图论算法的C语言实现,涵盖二叉树的层次遍历及广度优先搜索(BFS)和深度优先搜索(DFS)的邻接表与邻接矩阵实现。层次遍历使用队列按层访问二叉树节点;BFS利用队列从源节点逐层遍历图节点,适用于最短路径等问题;DFS通过递归或栈深入图的分支,适合拓扑排序等场景。这些算法是数据结构和算法学习的基础,对提升编程能力和解决实际问题至关重要。
56 4
|
1天前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
110 75
|
1天前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
24 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
1天前
|
存储 C语言 C++
【C++数据结构——栈与队列】链栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现链栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储整数,最大
24 9
|
1天前
|
C++
【C++数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】
【数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】(1)遇到左括号:进栈Push()(2)遇到右括号:若栈顶元素为左括号,则出栈Pop();否则返回false。(3)当遍历表达式结束,且栈为空时,则返回true,否则返回false。本关任务:编写一个程序利用栈判断左、右圆括号是否配对。为了完成本关任务,你需要掌握:栈对括号的处理。(1)遇到左括号:进栈Push()开始你的任务吧,祝你成功!测试输入:(()))
21 7
|
2月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
76 5