数据结构(六) -- C语言版 -- 栈和队列 - 栈的设计与实现

零、读前说明

  • 本文中没有涉及到很多的相关理论知识,也没有做深入的了解,所以,您如果是想要系统的学习、想要多学习关于理论的知识等,那么本文可能并不合适您。
  • 本文中所有设计的代码均通过测试,并且在功能性方面均实现应有的功能。
  • 设计的代码并非全部公开,部分无关紧要代码并没有贴出来。
  • 如果你也对此感兴趣、也想测试源码的话,可以私聊我,非常欢迎一起探讨学习。
  • 由于时间、水平、精力有限,文中难免会出现不准确、甚至错误的地方,也很欢迎大佬看见的话批评指正。
  • 嘻嘻。。。。 。。。。。。。。收!

一、栈的概述

  • 从数据结构角度看,栈和队列也是线性表,其特殊性在于栈和队列的基本操作是线性表操作的子集,它们是操作受限的线性表,因此,可称为限定性的数据结构。
  • 栈是限定仅仅在表尾进行插入和删除的线性表。
  • 一般情况下,把允许插入和删除的一端成为栈顶,另一端成为栈底,不含有任何数据元素的栈成为空栈。
  • 栈一般先进后出、后进先出的线性表。
  • 栈的插入操作称为进栈
  • 栈的删除操作称为出栈

二、栈的操作

  栈的常用操作基本包括下面这些。

  • 栈的创建
  • 栈的销毁
  • 栈的清空
  • 进栈
  • 出栈
  • 获取栈顶元素
  • 获取栈的大小

  其中,需要注意的就是

  • top方法是获取栈的元素,但是没有弹出
  • pop是弹出栈的元素

三、栈的模型概述

  栈既然只允许一端进行操作,那么就会有哪一端操作的问题出现,小眼一闭大致一想,无外乎四种开口的方向,也就是上下左右,所以,可以用下面的一个图图来简单的描述一下下。
在这里插入图片描述

四、栈的顺序存储结构及实现

4.1、顺序存储结构的栈的模型

  说到底,顺序存储结构就是数组存储么,那么根据栈的特性来说,就会有左右哪一端操作的选择问题,假设从左边作为开口进行操作,那么简单的示意图可以为下图的这样。
在这里插入图片描述
  由此可见,在进行入栈和出栈的后,会大量的移动数组中的元素,那么这种事情明显感觉就不是很对劲是不是。
  那么再看一下右边开口的情况,那简单的示意图可以为下图的这样。
在这里插入图片描述
  哎,有那个感觉了哈,这种情况下就不需要移动元素了,而且随来随走,简直不要太方便了哈哈!

4.2、顺序存储结构栈的代码实现

4.2.1、传统栈的简易实现与测试

  那么按照惯例先要来个最简单的、最传统的栈的实现的代码来热热身啦,一般嘛,前戏还是很有必要的啦,就像上课,你你来点开场什么的,怎么能够让学生很快进入状态呢是不是。
  好,那既然这样了,最实际当然就是直接献祭代码,那么来了。

#include <stdio.h>
#include <stdlib.h>

#define CAPACITY 8
typedef int data_t;
typedef struct seqstack
{
	data_t a[CAPACITY];
	int top; //栈顶 ----> last
} seqstack_t;


//开辟结构体空间(空的栈)
seqstack_t *seqstack_create()
{
	seqstack_t *stack = (seqstack_t *)malloc(sizeof(seqstack_t));
	if (stack == NULL)
		return NULL;

	stack->top = -1;

	return stack;
}

//满return 1;不满return 0;
int seqstack_is_full(seqstack_t *stack)
{
	return stack->top == CAPACITY - 1 ? 1 : 0;
}

//空return 1;非空return 0;
int seqstack_is_empty(seqstack_t *stack)
{
	return stack->top == -1 ? 1 : 0;
}

//入栈: 成功returnn 0;已满失败return -1;
int seqstack_push(seqstack_t *stack, data_t value)
{
	if (seqstack_is_full(stack))
		return -1;

	stack->top++;
	stack->a[stack->top] = value;

	return 0;
}

//出栈: 成功return value;已空失败return -1;
data_t seqstack_pop(seqstack_t *stack)
{
	if (seqstack_is_empty(stack))
		return -1;

	data_t value = 0;
	value = stack->a[stack->top];
	stack->top--;

	return value;
}

int main(int argc, const char *argv[])
{
	int i;
	seqstack_t *stack = seqstack_create();
	if(stack == NULL) return -1;
	
	printf("入栈数据一次为:");
	for (i = 0; i < 8; i++)
	{
		printf("%d\t", i + 1);
		seqstack_push(stack, i + 1); //入栈顺序  1  2  3
	}

	printf("\n出栈数据一次为:");
	for (i = 0; i < 8; i++)
	{
		printf("%d\t", seqstack_pop(stack)); //出栈顺序  8  7  6  5
	}
	putchar(10);
	
	return 0;
}

  那么代码都来了,直接就啪啪啪一通,结果就这么出来了。
在这里插入图片描述
  说完和测试完成简单的传统的栈的测试,那么再来看看通用的实现与测试咯。当当当。。。。。
  因为之前已经实现了线性表的顺序存储的代码,那么在这个地方就直接调用原先实现的功能函数,详细的可以查看电梯 数据结构(二) – C语言版 – 线性表的顺序存储

4.2.2、测试工程的目录结构

  为了兼容unixwindows系统以及方便进行工程管理,特意使用Cmake工具进行编译等,目前测试工程的目录结构如下所示。

seqstack/
├── CMakeLists.txt
├── README.md
├── build
├── main
│   └── main.c
├── runtime
└── src
    ├── seqlist
    │   ├── seqlist.c
    │   └── seqlist.h
    └── seqstack
        ├── seqstack.c
        └── seqstack.h

6 directories, 7 files
4.2.3、seqstack.h文件

  此文件为栈的顺序存储结构的实现的函数以及相关定义的文件,主要内容如下。

#ifndef __SEQSTACK_H__
#define __SEQSTACK_H__

#define null NULL
typedef void SeqStack;
typedef void SeqStackNode;

typedef struct func_SeqStack
{
    SeqStack *(*create)();
    int (*destroy)(SeqStack *);
    int (*clear)(SeqStack *);
    int (*length)(SeqStack *);
    int (*capacity)(SeqStack *);
    int (*push)(SeqStack *, SeqStackNode *);
    SeqStackNode *(*top)(SeqStack * );
    SeqStackNode *(*pop)(SeqStack * );
} func_SeqStack;

#endif

4.2.4、seqstack.c文件

  seqstack.c文件主要内容如下。

#include "seqstack.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "../seqlist/seqlist.h"

#ifndef null
define null NULL
#endif

extern funSeqList SeqListfunc;

/**
 * 功 能:
 *      创建一个栈
 * 参 数:
 *      capacity:栈的最大容量
 * 返回值:
 *      成功:操作句柄
 *      失败:NULL
 **/
SeqStack *SeqStack_Create(int capacity)
{
    return SeqListfunc.create(capacity);
}
/**
 * 功 能:
 *      销毁一个栈
 * 参 数:
 *      Stack:要操作的栈
 * 返回值:
 *      成功:0
 *      失败:-1
 **/
int SeqStack_Destory(SeqStack *Stack)
{
    return SeqListfunc.destory(Stack);
}
/**
 * 功 能:
 *      清空一个栈
 * 参 数:
 *      Stack:要操作的栈
 * 返回值:
 *      成功:0
 *      失败:-1
 **/
int SeqStack_Clear(SeqStack *Stack)
{
    return SeqListfunc.clear(Stack);
}

/**
 * 功 能:
 *      获取栈的长度
 * 参 数:
 *      Stack:要操作的栈
 * 返回值:
 *      成功:栈的长度
 *      失败:-1
 **/
int SeqStack_Length(SeqStack *Stack)
{
    return SeqListfunc.length(Stack);
}

/**
 * 功 能:
 *      获取栈的最大容量
 * 参 数:
 *      Stack:要操作的栈
 * 返回值:
 *      成功:长得容量
 *      失败:-1
 **/
int SeqStack_Capacity(SeqStack *Stack)
{
    return SeqListfunc.capacity(Stack);
}

/**
 * 功 能:
 *      插入元素
 * 参 数:
 *      Stack:要操作的栈
 *      node : 要插入的元素
 * 返回值:
 *      成功:0
 *      失败:-1
 **/
int SeqStack_Push(SeqStack *Stack, SeqStackNode *node)
{
    return SeqListfunc.insert(Stack, node, SeqListfunc.length(Stack));
}
/**
 * 功 能:
 *      获取栈的元素
 * 参 数:
 *      Stack:要操作的栈
 * 返回值:
 *      成功:操作句柄
 *      失败:NULL
 **/
SeqStackNode* SeqStack_Get(SeqStack *Stack)
{
    return SeqListfunc.get(Stack, SeqListfunc.length(Stack) - 1);
}

/**
 * 功 能:
 *      删除栈
 * 参 数:
 *      Stack:要操作的栈
 * 返回值:
 *      成功:删除后的元素
 *      失败:NULL
 **/
SeqStackNode* SeqStack_Pop(SeqStack *Stack)
{
    return SeqListfunc.delete(Stack, SeqListfunc.length(Stack) - 1);
}

func_SeqStack fun_SeqStack = {
    SeqStack_Create,
    SeqStack_Destory,
    SeqStack_Clear,
    SeqStack_Length,
    SeqStack_Capacity,
    SeqStack_Push,
    SeqStack_Get,
    SeqStack_Pop
};

4.3、顺序存储结构栈的测试案例

  测试案例就是简单的测试一下api函数的功能实现的情况啦,那么笨测试案例就可以写成这样。

#include "../src/seqstack/seqstack.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* 声明底层链表的函数库 */
extern func_SeqStack fun_SeqStack;

#define CAPACITY 10

int main(int argc, const char *argv[])
{
    int i = 0;
    int a[CAPACITY] = {0};
    SeqStack *stack = fun_SeqStack.create(CAPACITY);

    if (stack == NULL)
        return -1;

    // 压栈
    for (i = 0; i < 5; i++)
    {
        a[i] = i + 1;
        fun_SeqStack.push(stack, &a[i]);
    }

    printf("最大容量为 : %d\n", fun_SeqStack.capacity(stack));
    printf("目前长度为 : %d\n", fun_SeqStack.length(stack));
    printf("栈顶数据为 : %d\n", *((int *)fun_SeqStack.top(stack)));
    printf("\n");

    while (fun_SeqStack.length(stack) > 0)
    {
        printf("出栈的数据为 :%d\n", *((int *)fun_SeqStack.pop(stack)));
    }

    printf("\n");

    return 0;
}

4.4、测试案例构建编译

  构建过程使用cmake工具进行编译,使用如下指令即可。本次测试是在windows环境下进行,其他系统等详细说明在README.md中查看。不多说了直接上图啦。
在这里插入图片描述

4.5、测试结果

  经过cmake编译之后,配置cmake可执行文件放在固定目录runtime下,可以使用在当前目录下使用指令 ./../runtime/linkList.exe来运行可执行程序,也可以进入到目录runtime中,然后使用指令 ./linkList.exe ,即可运行测试程序。实际测试的结果如下图所示。
在这里插入图片描述
  至此,顺序存储结构栈的东东先告一段落了。

五、栈的链式存储结构及实现

  我们已经知道链表是什么样式的,如果还不知道那么详情可以参考:电梯,点我直达 — 数据结构(三) – C语言版 – 线性表的链式存储 - 单链表。

5.1、链式存储结构的栈的模型

5.1.1、链式存储结构的栈的模型选择

  因为线性表是单向的,所以在链表进行模拟栈的时候也存在这个模型选择的问题。可以用下面简单的两个图片进行一个比较说明。
  首先可以简单模拟一下,如果使用右边开口的情况下进行栈的模拟,那么简单的示意图可以为下图这样。
在这里插入图片描述
  一般情况链表我们都是记录头节点 ,然后通过头节点进行后续的遍历等操作,那么从上面的图例中可以看出来,在进行压栈和出栈的时候,其实相当于进行链表的尾插入法和尾删除法,所以都需要进行整个链表的遍历,那么这个情况在操作的过程中是比较耗费时间的。虽然我们可以选择,当时我们还想找到更好的,不是吗?!
  那我们可以反过来试试看看,就算我们一半是咸鱼,但也要翻过来,把另一半也弄成咸鱼,是咸鱼那就要做全身最咸的那个。那再请看下面这个图图儿。
在这里插入图片描述
  虽然咋一看貌似和上面的没什么太大的区别,但是你细看,头节点就在栈顶的位置,栈顶指针和链表的头指针合二为一了,何乐而不为呢。所以,压栈和出栈的操作都在链表的头部进行操作,头插入法,头删除法。那这样的话就和我们心里的预期差不多了哈。
  这个东东就像是找女朋友,虽然不求最好的,但也想求更好的,但归根结底还是选择最合适的是不是,那么对于栈的链式存储,我觉得这种方式甚好,甚好。

5.1.2、栈传统栈的简易实现与测试

  惯例热身。代码如下。

#include <stdio.h>
#include <stdlib.h>

typedef int data_t;

//数据类型
typedef struct node
{
	data_t data;
	struct node *next;
} linknode_t;

//创建空栈
linknode_t *linkstack_create()
{
	linknode_t *stack = (linknode_t *)malloc(sizeof(linknode_t));
	if (stack == NULL)
		return NULL;

	stack->next = NULL;

	return stack;
}

//判断是否为空
int linkstack_is_empty(linknode_t *stack)
{
	if (stack == NULL)
		return -1;

	return stack->next == NULL ? 1 : 0;
}

//入栈(头插法)
int linkstack_push(linknode_t *stack, data_t value)
{
	if (stack == NULL)
		return -1;

	linknode_t *node = (linknode_t *)malloc(sizeof(linknode_t));
	if (node == NULL)
		return -1;

	node->data = value;
	node->next = stack->next;
	stack->next = node;

	return 0;
}

//出栈(头删法)
data_t linkstack_pop(linknode_t *stack)
{
	if (stack == NULL)
		return -1;

	if (linkstack_is_empty(stack))
		return -1;

	linknode_t *temp = stack->next;
	data_t value = temp->data;

	stack->next = temp->next;
	free(temp);
	temp = NULL;

	return value;
}

int main(int argc, const char *argv[])
{
	int i;
	linknode_t *stack = linkstack_create();
	if (stack == NULL)
		return -1;

	printf("入栈数据一次为:");
	for (i = 0; i < 10; i++)
	{
		printf("%d\t", i + 1);
		linkstack_push(stack, i + 10);
	}

	printf("\n出栈数据一次为:");
	for (i = 0; i < 10; i++)
	{
		printf("%d\t", linkstack_pop(stack));
	}
	putchar(10);

	return 0;
}


  编译运行,完事儿。
在这里插入图片描述
  是的呀,就是这么简单的事情,不过下面开始就用大量的篇幅来说明我今天带你看的不一样的栈。
  关好车门系好安全带,我们走起。。。。。

5.1.3、链式存储结构的栈的业务节点转换

  按照原来线性表的测试案例,如果想要把业务节点插入到链表中,那么就要业务节点去包含链表节点,那么现在还有一个问题就是如何将栈的业务节点转化成一个链表的业务节点呢。
  我们已经在 数据结构(三) – C语言版 – 线性表的链式存储 - 单链表4.7、调用测试用例 章节已经说明了,通用链表并不关心业务节点是什么样式的,只是让业务节点去包含链表节点即可形成链表,那么现在在进行栈的操作中我们也需要将栈的业务节点转换成链表的业务节点。其实,就像前文中的 “老师节点”、“学生节点” 一样 ,我们也需要将链表节点包含到栈的节点中即可,那么,本文中的节点的定义可以为这样。
  定义存在于文件 linkstack.h中。

typedef void LinkStack;
typedef void LinkStackNode;

typedef struct _tag_LinkStackNode
{
    LinkListNode node;   // 链表的业务节点
    LinkStackNode *item; // 栈的业务节点
} TLinkStackNode;

  而原本的链表的业务节点的定义为这样的。
  定义存在于文件 linklist.h中。

typedef void Linklist;
#define null NULL

typedef struct _tag_linklistNode
{
    struct _tag_linklistNode *next;
} LinkListNode;

typedef struct _tag_Linklist
{
    LinkListNode header;
    int length;
} TLinklist;

  通过这样的包含关系,那么栈的节点已经包含了链表的节点。

5.2、链式存储结构栈的代码实现

5.2.1、测试工程的目录结构

  是的,Cmake,基本一样的工程结构,只是新增了相对应的链表的文件夹和文件,目前测试工程的目录结构如下所示。

linkstack/
├── CMakeLists.txt
├── README.md
├── build
├── main
│   └── main.c
├── runtime
└── src
    ├── linklist
    │   ├── linklist.c
    │   └── linklist.h
    └── linkstack
        ├── linkstack.c
        └── linkstack.h

6 directories, 7 files
5.2.2、linkstack.h文件

  linkstack.h文件为栈的主要变量等的定义和申明文件,文件内容如下。

#ifndef __LINKSTACK_H__
#define __LINKSTACK_H__

#include "../linklist/linklist.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"

#define null NULL

typedef void LinkStack;
typedef void LinkStackNode;

typedef struct _tag_LinkStackNode
{
    LinkListNode node;   // 链表的业务节点
    LinkStackNode *item; // 栈的业务节点
} TLinkStackNode;

typedef struct func_LinkStack
{
    LinkStack *(*create)();
    int (*destroy)(LinkStack *);
    int (*clear)(LinkStack *);
    int (*length)(LinkStack *);
    int (*push)(LinkStack *, LinkStackNode *);
    LinkStackNode *(*top)(LinkStack *);
    LinkStackNode *(*pop)(LinkStack *);
} func_LinkStack;

#endif

5.2.3、linkstack.c文件

  linkstack.c文件为栈的主要实现的文件,文件内容如下。

#include "LinkStack.h"

extern func_linklist fun_linklist;

#ifndef null
define null NULL
#endif

/**
 * 功 能:
 *      创建一个栈
 * 参 数:
 *      无
 * 返回值:
 *      成功:操作句柄
 *      失败:NULL
 **/
LinkStack *LinkStack_Create(void)
{
    return fun_linklist.create();
}

/**
 * 功 能:
 *      插入元素,压栈
 * 参 数:
 *      Stack:要操作的栈
 *      node : 要插入的元素,栈的业务节点
 * 返回值:
 *      成功:0
 *      失败:-1
 **/
int LinkStack_Push(LinkStack *Stack, LinkStackNode *node)
{
    if (Stack == NULL || node == NULL)
        return -1;
    TLinkStackNode *temp = NULL;
    int ret = 0;
    temp = (TLinkStackNode *)malloc(sizeof(TLinkStackNode));
    if (temp == NULL)
        return -1;

    memset(temp, 0, sizeof(TLinkStackNode));

    temp->item = node;

    ret = fun_linklist.insert(Stack, (LinkListNode *)temp, 0);
    if (ret != 0 && temp != NULL)
        free(temp);

    return ret;
}

/**
 * 功 能:
 *      弹出栈元素
 * 参 数:
 *      Stack:要操作的栈
 * 返回值:
 *      成功:删除后的元素
 *      失败:NULL
 **/
LinkStackNode *LinkStack_Pop(LinkStack *Stack)
{
    if (Stack == NULL)
        return NULL;
    LinkStackNode *item = NULL;
    TLinkStackNode *temp = NULL;
    temp = (TLinkStackNode *)fun_linklist.delete(Stack, 0);
    if (temp == NULL)
        return NULL;

    // 吧线性表的业务节点转换成栈的业务节点
    item = temp->item;
    // 在 insert 的时候malloc的内存需要free掉
    free(temp);

    return item;
}

/**
 * 功 能:
 *      清空一个栈
 * 参 数:
 *      Stack:要操作的栈
 * 返回值:
 *      成功:0
 *      失败:-1
 **/
int LinkStack_Clear(LinkStack *Stack)
{
    if (Stack == NULL)
        return -1;

    while (fun_linklist.length(Stack) > 0)
    {
        LinkStackNode *temp = LinkStack_Pop(Stack);
        if (temp == NULL)
            return -1;
    }
    return 0;
}

/**
 * 功 能:
 *      销毁一个栈
 * 参 数:
 *      Stack:要操作的栈
 * 返回值:
 *      成功:0
 *      失败:-1
 **/
int LinkStack_Destory(LinkStack *Stack)
{
    LinkStack_Clear(Stack);
    return fun_linklist.destory(&Stack);
}

/**
 * 功 能:
 *      获取栈的长度
 * 参 数:
 *      Stack:要操作的栈
 * 返回值:
 *      成功:栈的长度
 *      失败:-1
 **/
int LinkStack_Length(LinkStack *Stack)
{
    return fun_linklist.length(Stack);
}

/**
 * 功 能:
 *      获取栈顶的元素
 * 参 数:
 *      Stack:要操作的栈
 * 返回值:
 *      成功:操作句柄
 *      失败:NULL
 **/
LinkStackNode *LinkStack_Get(LinkStack *Stack)
{
    if (Stack == NULL)
        return NULL;
    TLinkStackNode *temp = NULL;
    temp = (TLinkStackNode *)fun_linklist.get(Stack, 0);

    if (temp == NULL)
        return NULL;

    return temp->item;
}

func_LinkStack fun_LinkStack = {
    LinkStack_Create,
    LinkStack_Destory,
    LinkStack_Clear,
    LinkStack_Length,
    LinkStack_Push,
    LinkStack_Get,
    LinkStack_Pop
};

5.3、链式存储结构栈的测试案例

  main.c文件主要用于测试相关功能函数的实现是否正常等,那么可以这样写。

#include "../src/linkstack/linkstack.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* 声明底层栈的函数库 */
extern func_LinkStack fun_LinkStack;

#define CAPACITY 10

int main(int argc, const char *argv[])
{
    int i = 0;
    int a[CAPACITY] = {0};
    LinkStack *stack = fun_LinkStack.create();

    if (stack == NULL)
        return -1;

    // 压栈
    for (i = 0; i < CAPACITY; i++)
    {
        a[i] = i + 1;
        fun_LinkStack.push(stack, &a[i]);
    }

    printf("目前长度为 : %d\n", fun_LinkStack.length(stack));
    printf("栈顶数据为 : %d\n", *((int *)fun_LinkStack.top(stack)));
    printf("\n");

    while (fun_LinkStack.length(stack) > 0)
    {
        LinkStackNode *tmp = fun_LinkStack.pop(stack);
        printf("出栈的数据为 :%d\n", *((int *)tmp));
    }

    printf("\n");
	
	fun_LinkStack.destroy(stack);
	
    return 0;
}

5.4、测试案例构建编译

  是的,老掉牙的Cmake,还是老掉牙的指令,一样的编译过程,甚至一样的工程结构,实在是没有兴趣再说一遍了。
在这里插入图片描述

5.5、测试结果

  屁话少说,直接运行看效果。
在这里插入图片描述

六、强行来个总结

  • 可以用线性表的顺序存储结构、线性表的链式存储结构分别来模拟栈
  • 栈的先进后出、后进先出应用十分广泛,比如CTRL+Z、CTRL+Y、…
  • 从数据结构角度看,栈也是一种线性表(废话但是真的很重要)
  • 从数据类型角度看,栈和线性表是两种不同的抽象数据类型
  • 顺序结构栈可以有空、满的判断,但是本文中没有使用,其实就是长度的获取
  • 好像能想起来的能看见就这么多了,欢迎补充

七、勉强加个说明

  • 可以看出来,这片文章有点水了,因为目前使用到最主要的原理和代码均为前几天已经说明的顺序表和链表
  • 还是一样,好多的源码并没有公开,但可以私信免费提供
  • 所有功能均测试完成,贴图均为真实实验效果截图
  • 马赛克的原因就是,其实这些文件是在好几个电脑上实现测试的,所以为了效果,将主要的用户名马赛克了
  • 如果您觉得有用,可以尝试点个赞
  • 嘻嘻嘻嘻嘻。。。。。收!

上一篇:数据结构(五) – C语言版 – 线性表的链式存储 - 双向链表、双向循环链表
下一篇:数据结构(七) – C语言版 – 栈和队列 - 栈的应用解析

猜你喜欢

转载自blog.csdn.net/zhemingbuhao/article/details/104486929