俗话说得好,线性表(尤其是链表)是一切数据结构和算法的基础,很多复杂甚至是高级的数据结构和算法,细节处,除去数学和计算机程序基础的知识,大量的都在应用线性表。

一、栈

其实本质还是线性表:限定仅在表尾进行插入或删除操作。 俗称:后进先出 (LIFO=last in first out结构),也可说是先进后出(FILO)。

同样的,栈也分为顺序和链式两大类。其实和线性表大同小异,只不过限制在表尾进行操作的线性表的特殊表现形式。

1、顺序栈:利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,同时附设指针 top 指示栈顶元素在顺序栈中的位置,附设指针 base 指示栈底的位置。 同样,应该采用可以动态增长存储容量的结构。且注意,如果栈已经空了,再继续出栈操作,则发生元素下溢,如果栈满了,再继续入栈操作,则发生元素上溢。栈底指针 base 初始为空,说明栈不存在,栈顶指针 top 初始指向 base,则说明栈空,元素入栈,则 top++,元素出栈,则 top--,故,栈顶指针指示的位置其实是栈顶元素的下一位(不是栈顶元素的位置)。

 #ifndef _____ADT__
#define _____ADT__
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#define STACK_SIZE 50
#define STACK_INCREMENT 10 typedef struct{
int stackSize;//栈容量
char *base;//栈底指针
char *top;//栈顶指针
} SqStack; //初始化
//本质还是使用动态数组
void initStack(SqStack *s)
{
s->base = (char *)malloc(STACK_SIZE * sizeof(char));
//分配成功
if (s->base != NULL) {
//空栈
s->top = s->base;
s->stackSize = STACK_SIZE;
}
else
{
puts("分配失败!");
}
} //判空
bool isEmpty(SqStack s)
{
return s.top == s.base ? true : false;
} //判满
bool isFull(SqStack s)
{
return (s.top - s.base) >= STACK_SIZE ? true : false;
} //求当前长度
int getLength(SqStack s)
{
int i = ;
char *q = s.top; while (q != s.base) {
q--;
i++;
} return i;
} //求栈顶元素
char getTop(SqStack s, char topElement)
{
if (isEmpty(s)) {
puts("栈空!");
} topElement = *(s.top - );
return topElement;
} //入栈
void push(SqStack *s, char topElement)
{
char *q = NULL; if (isFull(*s)) {
q = (char *)realloc(s->base, STACK_INCREMENT * sizeof(char)); if (NULL == q) {
exit();
} s->base = q;
s->stackSize = s->stackSize + STACK_INCREMENT;
}
//进栈
*s->top++ = topElement;
} //出栈
void pop(SqStack *s, char *topElement)
{
if (isEmpty(*s)) {
exit();
} s->top--;
*topElement = *s->top;
} //遍历
void traversal(SqStack s)
{
for (int i = ; i < getLength(s); i++) {
printf("栈中元素遍历:%c \n", s.base[i]);
}
} //清空
void cleanStack(SqStack *s)
{
if (!isEmpty(*s)) {
s->top = s->base;
puts("栈已经清空!");
}
} //销毁
void destroyStack(SqStack *s)
{
if (s->base != NULL) {
free(s->base);
s->base = NULL;
s->top = NULL;
s->stackSize = ;
puts("栈成功销毁!");
}
} #endif /* defined(_____ADT__) */

函数: void exit(int status);    所在头文件:stdlib.h

功 能: 关闭所有文件,终止正在执行的进程。

exit(1)表示异常退出.这个1是返回给操作系统的。

exit(x)(x不为0)都表示异常退出

exit(0)表示正常退出

exit()的参数会被传递给一些操作系统,包括UNIX,Linux,和MS DOS,以供其他程序使用。

exit()和return的区别:

按照ANSI C,在最初调用的main()中使用return和exit()的效果相同。 但要注意这里所说的是“最初调用”。如果main()在一个递归程序中,exit()仍然会终止程序;但return将控制权移交给递归的前一级,直到最初的那一级,此时return才会终止程序。

return和exit()的另一个区别在于,即使在除main()之外的函数中调用exit(),它也将终止程序。

_exit()与exit的区别:

头文件不同:

exit:#include<stdlib.h>

_exit:#include<unistd.h>

_exit()函数:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;

exit()函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序。比如系统调用之前exit()要检查文件的打开情况,把文件缓冲区中的内容写回文件。

 #include "ADT.h"

 int main(void) {
char temp = '';
SqStack stack;
initStack(&stack); printf("%d\n", getLength(stack)); push(&stack, 'b');
push(&stack, 'k');
push(&stack, 'y'); printf("%d\n", getLength(stack));
// 函数使用temp之前必须初始化
temp = getTop(stack, temp);
printf("%c\n", temp); traversal(stack); pop(&stack, &temp);
printf("%d\n", getLength(stack)); traversal(stack); cleanStack(&stack); destroyStack(&stack); return ;
}

测试结果:

0

3

y

栈中元素遍历:

栈中元素遍历:

栈中元素遍历:

2

栈中元素遍历:

栈中元素遍历:

栈已经清空!

栈成功销毁!

Program ended with exit code: 0


顺序栈的小结:

1)、尽量使用指向结构的指针做函数参数,这样的操作比结构体变量作函数参数效率高,因为无需传递各个成员的值,只需传递一个结构的地址,且函数中的结构体成员并不占据新的内存单元,而与主调函数中的成员共享存储单元。这种方式还可通过修改形参所指成员影响实参所对应的成员值。

2)、栈清空,一定是栈顶指向栈底,不可颠倒,否则析构出错!

3)、再次注意二级指针和一级指针做函数参数的不同

4)、使用一个指针变量之前,必须初始化,不为指针分配内存,即指针没有指向一块合法的内存,那么指针无法使用,强行运行出错,这就是野指针的危害。类似其他类型变量都是如此,(这也是为什么建议声明变量的同时就立即初始化,哪怕是一个不相干的数值),就怕程序写的复杂的话,一时忘记变量是否初始化,导致出错。

5)、为什么顺序栈(包括顺序表)在初始化函数的传入参数里不用二级指针传参?

个人理解:

  首先清楚函数传参的类型,值传递,引用(c++)传递,和指针传递,且函数修改的是实参的一份拷贝,并不是直接去修改实参。

问题是,在之前的链表里,定义结点结构( Node)和指向结点的指针 p,有 struct Node *p;为了给结点分配内存,把这个指针(p本身占据一份内存空间,在栈区操作系统分配,但是 p 的指向是没有初始化的)p 传入初始化函数内,等于是传入的指向结点Node的一个指针 p 的拷贝 _p,而这个拷贝 _p 本身(假设指针变量p自己在内存的地址是0x1111)和拷贝 _p 存储的(指针 p指向的 内存区域)内容是不一样的,此时给 拷贝 _p 使用malloc函数分配内存,看似是修改了 _p ,实际上是把 _p 指向的内存区域改变了, p 本身在内存的地址(0x1111)没有被改变,故函数执行完毕,栈分配的内存被操作系统回收,然后参数返回给主调函数,那份拷贝 _p 还是以前传入的那份拷贝 _p, 高矮胖瘦那是纹丝未动,故不会对实参起到修改的作用,完全类似值传递,在值传递,就是把实参的本身的值传入函数,如果函数内部对其拷贝进行修改,其实传入的参数本身并没有被改变,虽然函数体内,他的值被修改了,但是一旦函数执行完,栈的内存被系统回收,修改就变得徒劳。

顺序栈里,一般是定义的整个表List结构,struct List p;变量p 就是一个实例化的栈结构,注意 p 已经分配了内存(主调函数 main ,操作系统在栈区给p分配),这和主调函数里链表的指针 p 不一样,链表的指针 p 在 main 函数,只是给指针本身分配了内存空间,但是对 其指向的表结点没有分配,需要在初始化函数初始化!故到了顺序栈里,当给函数传入&p(因为开始main 已经给栈结构分配了内存空间,而&p 是栈结构 p 在内存的地址0x1111,也就是栈本身的首地址,也是 base指向的栈的基址),同样是传入一份拷贝 _&p ,且不是给 _&p  malloc 内存,而是给 _&p 指向的内容分配空间—— (&p)->base(表的基地址)分配内存,也就是说,这里堆 p 修改也是没用的,但是对 p 的指向修改,也就是 base,是有用的。而 base 本身就是一个指针,p 其实和 base 值相等,只不过变量的类型不一样,故不需要再传入二级指针。

6)、初始化那里,其实写的不好,主调函数里 s 分配了内存,如果没有对这个结构体初始化,就不代表 s 的成员 base 或者 top 等就是有指向的,更别说 NULL 了。很大程度是指向是垃圾值,不确定的内存区域,故这里的判断

if (s->base != NULL)

在这个前提下,是没有用处的语句。故还是声明变量的同时,最好是初始化,哪怕是0或者 NULL。

2、链栈

其实就是链表的特殊情形,一个链表,带头结点,栈顶在表头,插入和删除(出栈和入栈)都在表头进行,也就是头插法建表和头删除元素的算法。显然,链栈还是插入删除的效率较高,且能共享存储空间。

是栈顶在表头!栈顶指针指向表头结点。栈底是链表的尾部,base 就是尾指针。还有,理论上,链式结构没有满这一说,但是理论上是这样的,也要结合具体的内存,操作系统等环境因素。

 #ifndef _____ADT__
#define _____ADT__
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h> typedef struct Node{
char data;
struct Node *next; //next 指针
} Node, *ListStack; //初始化头结点,top 指针指向头结点
void initStack(ListStack *top)
{
//top就是头指针!也就是栈顶指针
*top = (ListStack)malloc(sizeof(Node));
//就一个结点,也就是空栈,其实和链表一样,没啥意思
(*top)->next = NULL;
//内容初始化
(*top)->data = '';
} //判空
bool isEmpty(ListStack top)
{
//栈顶指针的next==NULL 就是空栈,没有满的判断,但是也要悠着点。小心内存受不了。
return top->next == NULL ? true : false;
} //入栈
void push(ListStack top, char topElement)
{
ListStack q = NULL;
q = (ListStack)malloc(sizeof(Node)); if (NULL == q) {
exit();
}
//类似头插法建表,进栈
q->next = top->next;
top->next = q;
//赋值
top->data = topElement;
//栈底永远是表尾指针
} //出栈
void pop(ListStack top, char *topElement)
{
ListStack p = NULL; if (isEmpty(top)) {
exit();
}
//栈顶元素出栈,记住,栈顶指针永远是指向栈顶元素的下一位,p 指向栈顶元素
p = top->next;
*topElement = p->data;
//删除这个元素
top->next = p->next;
free(p);
} //求当前长度
int getLength(ListStack top)
{
int i = ;
ListStack q = top->next; while (q != NULL) {
i++;
q = q->next;
} return i;
} //求栈顶元素
char getTop(ListStack top, char topElement)
{
if (isEmpty(top)) {
puts("栈空!");
} topElement = top->next->data;
return topElement;
} //遍历
void traversal(ListStack top)
{
ListStack p = top->next; for (int i = ; i < getLength(top); i++) {
printf("栈中元素遍历:%c \n", p->data);
p = p->next;
}
} //销毁
void destroyLinkStack(ListStack *top)
{
ListStack p = *top;
ListStack pn = (*top)->next; while (pn != NULL)
{
free(p);
p = pn;
pn = pn->next;
}
//销毁最后一个
free(p);
p = NULL;
puts("栈成功销毁!");
} #endif /* defined(_____ADT__) */

main 函数

 #include "ADT.h"

 int main(void) {
ListStack stack = NULL;
initStack(&stack); printf("栈长度 = %d\n", getLength(stack)); push(stack, 'a');
push(stack, 'b');
push(stack, 'c');
push(stack, 'd'); printf("栈长度 = %d\n", getLength(stack)); traversal(stack); char temp = '';
printf("栈顶元素 = %c\n", getTop(stack, temp));
pop(stack, &temp); printf("栈长度 = %d\n",getLength(stack)); traversal(stack); destroyLinkStack(&stack); return ;
}

其实链栈和链表是一样的,没什么新鲜的东西。可见,线性表,尤其是链表,是数据结构和算法里的重中之重,后续很多复杂高级的数据结构和算法,都会无数次的用到链表的相关知识和概念。

欢迎关注

dashuai的博客是终身学习践行者,大厂程序员,且专注于工作经验、学习笔记的分享和日常吐槽,包括但不限于互联网行业,附带分享一些PDF电子书,资料,帮忙内推,欢迎拍砖!

栈的存储结构和常见操作(c 语言实现)的更多相关文章

  1. 队列的存储结构和常见操作(c 语言实现)

    一.队列(queue) 队列和栈一样,在实际程序的算法设计和计算机一些其他分支里,都有很多重要的应用,比如计算机操作系统对进程 or 作业的优先级调度算法,对离散事件的模拟算法,还有计算机主机和外部设 ...

  2. 动态单链表的传统存储方式和10种常见操作-C语言实现

    顺序线性表的优点:方便存取(随机的),特点是物理位置和逻辑为主都是连续的(相邻).但是也有不足,比如:前面的插入和删除算法,需要移动大量元素,浪费时间,那么链式线性表 (简称链表) 就能解决这个问题. ...

  3. C#创建安全的栈(Stack)存储结构

    在C#中,用于存储的结构较多,如:DataTable,DataSet,List,Dictionary,Stack等结构,各种结构采用的存储的方式存在差异,效率也必然各有优缺点.现在介绍一种后进先出的数 ...

  4. 栈的存储结构的实现(C/C++实现)

    存档 #include "iostream.h" #include <stdlib.h> #define max 20 typedef char elemtype; # ...

  5. C语言解释器的实现--存储结构(一)

    目录: 1. 内存池 2. 栈 3. Hash表 1.内存池  在一些小的程序里,没什么必要添加内存管理模块在里面.但是对于比较复杂的代码,如果需要很多的内存操作,那么加入自己的内存管理是有必要的.至 ...

  6. 数据结构11: 栈(Stack)的概念和应用及C语言实现

    栈,线性表的一种特殊的存储结构.与学习过的线性表的不同之处在于栈只能从表的固定一端对数据进行插入和删除操作,另一端是封死的. 图1 栈结构示意图 由于栈只有一边开口存取数据,称开口的那一端为“栈顶”, ...

  7. C++编程练习(4)----“实现简单的栈的链式存储结构“

    如果栈的使用过程中元素数目变化不可预测,有时很小,有时很大,则最好使用链栈:反之,如果它的变化在可控范围内,使用顺序栈会好一些. 简单的栈的链式存储结构代码如下: /*LinkStack.h*/ #i ...

  8. C++中栈结构建立和操作

    什么是栈结构 栈结构是从数据的运算来分类的,也就是说栈结构具有特殊的运算规则,即:后进先出. 我们可以把栈理解成一个大仓库,放在仓库门口(栈顶)的货物会优先被取出,然后再取出里面的货物. 而从数据的逻 ...

  9. Java栈之链式栈存储结构实现

    一.链栈 采用单链表来保存栈中所有元素,这种链式结构的栈称为链栈. 二.栈的链式存储结构实现 package com.ietree.basic.datastructure.stack; /** * 链 ...

随机推荐

  1. java入门(1)

    今天开始了java的学习,是同学给的一个说是外面培训的视频,因为我个人的专业是硬件,对软件开发其实也蛮有兴趣,不过确实有跟风的嫌疑哈,因为大家都说现在的主流就是java,所以就跟软件的人要了它的资料, ...

  2. HDU1575Tr A(矩阵相乘与快速幂)

    Tr A hdu1575 就是一个快速幂的应用: 只要知道怎么求矩阵相乘!!(比赛就知道会超时,就是没想到快速幂!!!) #include<iostream> #include<st ...

  3. UNET学习笔记1 - 总览

    UNET为两类人设计: (1)使用Unity开发简单多人在线游戏.这类用户可以从NetworkManager或者the High Level API开始: (2)开发复杂多人在线游戏或者开发网络服务. ...

  4. 重制AdvanceWars第一步 -- 搞定地图

    首先来聊下高级战争吧Advance Wars,由任天堂旗下的Intelligent Systems开发的战棋游戏.初作诞生于GBA上,后来继续跟进了高战2黑洞崛,而后在下一代掌机DS上也出了三代续作高 ...

  5. 打造Ubuntu下Java开发环境

    一.了解JDK 不同的java软件和类库对jdk有不同要求,在了解如何安装Java之前,让我们快速地了解JRE.OpenJDK和Oracle JDK之间的不同之处. JRE(Java Runtime ...

  6. 推荐几款自己写博客使用的Ubuntu软件

    使用Ubuntu桌面有段时间,到现在也写过几篇博客了,期间用到的几款好用的软件推荐给大家.1. 图片简单编辑软件gthumbubuntu默认提供shotwell查看图片,类似与windows的图片查看 ...

  7. 开始VS 2012中LightSwitch系列的第4部分:太多信息了!使用查询来排序和筛选数据

    [原文发表地址]  Beginning LightSwitch in VS 2012 Part 4: Too much information! Sorting and Filtering Data ...

  8. [C#进阶系列]专题二:你知道Dictionary查找速度为什么快吗?

    一.前言 在之前有一次面试中,被问到你了解Dictionary的内部实现机制吗?当时只是简单的了问答了:Dictionary的内部结构是哈希表,从而可以快速进行查找.但是对于更深一步了解就不清楚了.所 ...

  9. STC12C5A60S2笔记6(中断)

    1. 基本特性 1) 中断源 STC12C5A60S2共有十个中断源,每个中断源可设置4类优先级:当相同优先级下各中断优先级由高到低依次如下: 1.1)INT0(外部中断0) 中断向量地址 0003H ...

  10. WPF,Silverlight与XAML读书笔记第四十四 - 外观效果之样式

    说明:本系列基本上是<WPF揭秘>的读书笔记.在结构安排与文章内容上参照<WPF揭秘>的编排,对内容进行了总结并加入一些个人理解. 如果你有Web编程的经验,你会知道使用Sty ...