数据结构栈和队列

Posted 时间的光线

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构栈和队列相关的知识,希望对你有一定的参考价值。

栈和队列

栈的概念及结构

栈是一种特殊的线性表。其只允许在固定一端操作数据。进行插入数据和删除数据的一端叫做栈顶,另一端叫做栈底。删除数据操作称为出栈,插入数据称为入栈,压栈,进栈。其进行数据操作遵循LIFO(last in first out) 后进先出的原则。

栈的实现

根据栈的性质,构建栈的方式用两种:

  1. 链表,插入数据就是头插操作。
  2. 数组,插入数据就是尾插。

这两种方法都可以实现栈。但是,选择数组会跟好一些,为什么?当我们进行插入数据时,链表需要找到最后一个结点,需要遍历一遍,数组直接根据下标找到最后一个位置。

如果在链表中加一个尾指针指向最后一个位置,似乎可以解决这个问题。但是,怎么让尾指针指向上个结点呢?那就要遍历一遍了,这样就增加负担了。所以,使用数组是较好的选择。

那么接下来用数组实现栈

//Stack.h
#pragma once

#include<stdio.h>
#include<assert.h>
#include<stdbool.h>
#include<malloc.h>
#include<stdlib.h>

typedef int STDataType;
#define CAPACITY 4
//栈的结构有数组,有效元素和容量构成
typedef struct Stack 
	int* s;
	int top;
	int capacity;
ST;

void STInit(ST* ps);//栈的初始化
void STDestroy(ST* ps);//栈的销毁
bool STEmpty(ST* ps);//判断栈是否为空
void STPush(ST* ps, STDataType x);//进行插入数据
void STPop(ST* ps);//删除数据
STDataType STTop(ST* ps);//寻找栈顶的数据
int STSize(ST* ps);//查看栈有几个元素
//Stack.c
#define _CRT_SECURE_NO_WARNINGS

#include"Stack.h"

void STInit(ST* ps)

	assert(ps);//栈不能为空
	ps->s = (STDataType*)malloc(sizeof(STDataType) * CAPACITY);//malloc开辟空间
	if (ps->s == NULL)
	
		perror("malloc fail");//开辟失败报错
		return;
	
	ps->top = 0;//将top置为0,代表数据的个数。赋值为多少无所谓,看个人习惯
	ps->capacity = CAPACITY;//将容量先置为空


bool STEmpty(ST* ps)

	assert(ps);
	return ps->top == 0;


void STDestroy(ST* ps)

	assert(ps);
	free(ps->s);//释放空间
	ps->s = NULL;//同时将指针置为空
	ps->top = 0;
	ps->capacity = 0;


void STPush(ST* ps, STDataType x)

	assert(ps);
	//要先检查容量,判断为不为满,满了进行扩容
	//如果扩容步骤使用很多次,就可以写个扩容函数,只有一次就没必要
	if (ps->top == ps->capacity)
	
	//realloc开辟空间失败会返回NULL,所以使用一个新的指针变量
		STDataType* new = (STDataType*)realloc(ps->s, sizeof(STDataType) * ps->capacity * 2);
		if (new == NULL)
		
			perror("realloc fail");
			return;
		
		ps->s = new;
		ps->capacity *= 2;//开辟成功,容量变为之前的两倍
	
	ps->s[ps->top] = x;
	ps->top++;


void STPop(ST* ps)

	assert(ps);
	assert(!STEmpty(ps));//如果栈为空的话,就不能进行删除
	ps->top--;//直接将个数减一就行了


STDataType STTop(ST* ps)

	assert(ps);
	assert(!STEmpty(ps));
	return ps->s[ps->top - 1];//返回元素减一的数据


int STSize(ST* ps)

	assert(ps);
	return ps->top;//返回有效元素



//Test.c
#define _CRT_SECURE_NO_WARNINGS

#include"Stack.h"

int main()

	ST st;
	STInit(&st);
	STPush(&st, 1);
	STPop(&st);
	STDestroy(&st);
	return 0;

队列

队列的概念及结构

队列是一种特殊的线性结构。其只允许在一端插入数据,在另一端删除数据。插入数据操作称为入队列,该位置为队尾删除数据操作称为出队列,该位置为队头。队列的操作原则是FIFO(firtst in first out) 先进先出

队列的实现

构建队列方式有两种:

  1. 动态数组。
  2. 链表。

使用链表较好。队列的性质是先进先出,数组如何实现先出?后面元素依次向前覆盖,时间复杂度很大,而链表只需要记住头指针就行,必要时加上尾指针。

//Queue.h
#pragma once

#include<stdio.h>
#include<assert.h>
#include<malloc.h>
#include<stdbool.h>

typedef int QDataType;

//这是结点的结构,但是队列是FIFO,所以要记录头指针和尾指针,方便尾入和头出
typedef struct QueueNode 
	struct QueueNode* next;
	QDataType data;
QNode;
//我们需要头指针和尾指针,尾指针进行插入数据时是需要的,就不用遍历了浪费时间。size记录元素个数很有必要,在一些函数有重大意义
//多个数据可以用结构体来构建
typedef struct Queue 
	QNode* head;//有多个数据就可以再使用一个结构体
	QNode* tail;
	int size;
Queue;

void QueueInit(Queue* pq);//队列的初始化
void QueueDestroy(Queue* pq);//队列的销毁
void QueuePush(Queue* pq, QDataType x);//队列的尾部插入数据
void QueuePop(Queue* pq);//队列的头部删除数据
bool QueueEmpty(Queue* pq);//判断队列为不为空
int QueueSize(Queue* pq);//知道队列有几个有效元素
QDataType QueueFront(Queue* pq);//返回队列的首个元素
QDataType QueueBack(Queue* pq);//返回队列的尾元素
//Queue.c
#define _CRT_SECURE_NO_WARNINGS

#include"Queue.h"

void QueueInit(Queue* pq)

	assert(pq);
	pq->head = pq->tail = NULL;//队列的初始化将两个指针置为空
	pq->size = 0;//有效元素为0


void QueueDestroy(Queue* pq)

	assert(pq);
	//将一个结点指针指向头节点,作为循环的条件
	QNode* cur = pq->head;
	while (cur)
	
		QNode* del = cur->next;//一个要删除的结点
		free(cur);
		//我这里写成了cur = cur->next,这是不对的,cur的内存已经释放了,还指向next,就是野指针。
		//因为这个细节,导致一个题目死活过不了。
		cur = del;//指向下一个结点
	
	//销毁时及时将两个指针置为空,避免野指针的出现
	pq->head = pq->tail = NULL;
	pq->size = 0;


void QueuePush(Queue* pq, QDataType x)

	assert(pq);
	QNode* newnode = (QNode*)malloc(sizeof(QNode));//malloc申请新的结点
	if (newnode == NULL)
	
		perror("malloc fail");//申请失败报错并返回
		return;
	
	//如果为空队列,那么就要将指针同时指向新的结点
	if (pq->head == NULL)
	
		pq->head = pq->tail = newnode;
	
	else//这里没加else语句,那么if执行了的话就会混乱
	
		pq->tail->next = newnode;
		pq->tail = newnode;//同时将尾指针指向新的结点
	
	//将尾结点的next指向新的结点
	newnode->data = x;//新的结点进行赋值
	newnode->next = NULL;
	pq->size++;//有效元素进行加1


void QueuePop(Queue* pq)

	assert(pq);
	assert(!QueueEmpty(pq));//空的队列就不能删除

 	QNode* del = pq->head;
	pq->head = pq->head->next;//将头指针指向下个结点
	free(del);//释放空间
	del = NULL;
	//这里要考虑只有一个节点的时候,tail可能会是野指针
	if (pq->head == NULL)
	
		pq->tail = NULL;
	
	pq->size--;//有效元素减1


bool QueueEmpty(Queue* pq)

	assert(pq);
	return pq->size == 0;//有效元素为0,那么就是空


int QueueSize(Queue* pq)

	assert(pq);
	return pq->size;//返回有效元素


QDataType QueueFront(Queue* pq)

	assert(pq);
	assert(!QueueEmpty(pq));
	return pq->head->data;//返回队头数据


QDataType QueueBack(Queue* pq)

	assert(pq);
	assert(!QueueEmpty(pq));
	return pq->tail->data;//返回队尾数据

//Test.c
#define _CRT_SECURE_NO_WARNINGS

#include"Queue.h"

int main()

	Queue q;
	QueueInit(&q);
	QueuePush(&q, 1);
	QueuePush(&q, 2);
	QueuePush(&q, 3);
	QueuePush(&q, 4);
	//根据队列的性质,只能取队头打印,然后删除数据,才能打印下个队头
	printf("%d  ", q.head->data);
	QueuePop(&q);
	printf("%d  ", q.head->data);
	QueuePop(&q);
	printf("%d  ", q.head->data);
	QueuePop(&q);
	printf("%d  ", q.head->data);
	QueuePop(&q);
	QueueDestroy(&q);
	return 0;

实现了链表和顺序表的基础上,再来实现栈和队列就简单多了,

栈和队列

栈和队列:

栈和队列是非常重要的两种数据结构,在软件设计中应用很多。栈和队列也是线性结构,线性表、栈和队列这三种数据结构的数据元素以及数据元素间的逻辑关系完全相同,差别是线性表的操作不受限制,而栈和队列的操作受到限制。栈的操作只能在表的一端进行,队列的插入操作在表的一端进行而其它操作在表的另一端进行,所以,把栈和队列称为操作受限的线性表。

 

栈(Stack)是操作限定在表的尾端进行的线性表。表尾由于要进行插入、删除等操作,所以,它具有特殊的含义,把表尾称为栈顶( Top),另一端是固定的,叫栈底( Bottom)。当栈中没有数据元素时叫空栈(Empty Stack)。
栈通常记为: S= (a1,a2,…,an),S是英文单词stack的第 1 个字母。a1为栈底元素,an为栈顶元素。这n个数据元素按照a1,a2,…,an的顺序依次入栈,而出栈的次序相反,an第一个出栈,a1最后一个出栈。所以,栈的操作是按照后进先出(Last In First Out,简称LIFO)或先进后出(First In Last Out,简称FILO)的原则进行的,因此,栈又称为LIFO表或FILO表。栈的操作示意图如图所示。

 

BCL中的栈:

C#2.0 一下版本只提供了非泛型的Stack类(存储object类型)

C#2.0 提供了泛型的Stack<T>类

重要的方法如下
1,Push()入栈(添加数据)
2,Pop()出栈(删除数据,返回被删除的数据)
3,Peek()取得栈顶的数据,不删除
4,Clear()清空所有数据

4,Count取得栈中数据的个数

 

 

栈的接口定义:

public interface IStack<T> {
        int Count{get;}
int GetLength(); //求栈的长度
bool IsEmpty(); //判断栈是否为空
void Clear(); //清空操作
void Push(T item); //入栈操作
T Pop(); //出栈操作
T Peek(); //取栈顶元素
}

 

 

栈的存储和代码实现-1.1,顺序栈:

用一片连续的存储空间来存储栈中的数据元素(使用数组),这样的栈称为顺序栈(Sequence Stack)。类似于顺序表,用一维数组来存放顺序栈中的数据元素。栈顶指示器 top 设在数组下标为 0 的端, top 随着插入和删除而变化,当栈为空时,top=-1。下图是顺序栈的栈顶指示器 top 与栈中数据元素的关系图。

 

栈的存储和代码实现-1.2,顺序栈:

class SeqStack<T>:IStack<T>

    {

        private T[] data;

        private int top;



        public SeqStack(int size)

        {

            data = new T[size];

            top = -1;

        }



        //默认构造数组的最大容量为10

        public SeqStack():this(10)

        {

        } 



        public int GetLength()

        {

            return top + 1;

        }



        public int Count

        {

            get

            {

                return GetLength();

            }

        }



        public bool IsEmpty()

        {

            return top <= -1;

        }



        public void Clear()

        {

            top = -1;

            Array.Clear(data,0,data.Length);

        }



        public void Push(T item)

        {

            data[top + 1] = item;

            top++;

        }

        /// <summary>

        /// 出栈

        /// </summary>

        /// <returns></returns>

        public T Pop()

        {

            T temp = data[top];

            top--;

            return temp;

        }



        public T Peek()

        {

            return data[top];

        }

    }

 

 

栈的存储和代码实现-2.1,链栈:

栈的另外一种存储方式是链式存储,这样的栈称为链栈(Linked Stack)。链栈通常用单链表来表示,它的实现是单链表的简化。所以,链栈结点的结构与单链表结点的结构一样,如图 3.3 所示。由于链栈的操作只是在一端进行,为了操作方便,把栈顶设在链表的头部,并且不需要头结点。

 

2.2,链栈-链栈结点实现:

public class Node<T>
{
    private T data; //数据域
    private Node<T> next; //引用域
//构造器
    public Node(T val, Node<T> p)
    {
        data = val;
        next = p;
    }

//构造器
    public Node(Node<T> p)
    {
        next = p;
    }

//构造器
    public Node(T val)
    {
        data = val;
        next = null;
    }

//构造器
    public Node()
    {
        data = default(T);
        next = null;
    }

//数据域属
    public T Data
    {
        get { return data; }
        set { data = value; }
    }
//引用域属性
    public Node<T> Next
    {
        get { return next; }
        set { next = value; }
    }
}

 

 

2.3,链栈-代码实现:

把链栈看作一个泛型类,类名为 LinkStack<T>。 LinkStack<T>类中有一个字段 top 表示栈顶指示器。由于栈只能访问栈顶的数据元素,而链栈的栈顶指示器又不能指示栈的数据元素的个数。所以,求链栈的长度时,必须把栈中的数据元素一个个出栈,每出栈一个数据元素,计数器就增加 1,但这样会破坏栈的结构。为保留栈中的数据元素,需把出栈的数据元素先压入另外一个栈,计算完长度后,再把数据元素压入原来的栈。但这种算法的空间复杂度和时间复杂度都很高,所以,以上两种算法都不是理想的解决方法。理想的解决方法是 LinkStack<T>类增设一个字段 num 表示链栈中结点的个数。

 

队列:

队列(Queue)是插入操作限定在表的尾部而其它操作限定在表的头部进行的线性表。把进行插入操作的表尾称为队尾(Rear),把进行其它操作的头部称为队头(Front)。当队列中没有数据元素时称为空队列(Empty Queue)。
队列通常记为: Q= (a1,a2,…,an),Q是英文单词queue的第 1 个字母。a1为队头元素,an为队尾元素。这n个元素是按照a1,a2,…,an的次序依次入队的,出对的次序与入队相同,a1第一个出队,an最后一个出队。所以,对列的操作是按照先进先出(First In First Out)或后进后出( Last In Last Out)的原则进行的,因此,队列又称为FIFO表或LILO表。队列Q的操作示意图如图所示。
在实际生活中有许多类似于队列的例子。比如,排队取钱,先来的先取,后来的排在队尾。
队列的操作是线性表操作的一个子集。队列的操作主要包括在队尾插入元素、在队头删除元素、取队头元素和判断队列是否为空等。与栈一样,队列的运算是定义在逻辑结构层次上的,而运算的具体实现是建立在物理存储结构层次上的。因此,把队列的操作作为逻辑结构的一部分,每个操作的具体实现只有在确定了队列的存储结构之后才能完成。队列的基本运算不
是它的全部运算,而是一些常用的基本运算。

 

BCL 中的队列:

C#2.0 以下版本提供了非泛型的Queue类

C#2.0 提供了泛型Queue<T>类
方法
1,Enqueue()入队(放在队尾)
2,Dequeue()出队(移除队首元素,并返回被移除的元素)
3,Peek()取得队首的元素,不移除
4,Clear()清空元素
属性
5,Count获取队列中元素的个数

 

 

队列接口定义:

public interface IQueue<T> {
        int Count{get;}//取得队列长度的属性
int GetLength(); //求队列的长度
bool IsEmpty(); //判断对列是否为空
void Clear(); //清空队列
void Enqueue(T item); //入队
T Dequque(); //出队
T Peek(); //取队头元素
}

 

 

1.1 顺序队列:

用一片连续的存储空间来存储队列中的数据元素,这样的队列称为顺序队列(Sequence Queue)。类似于顺序栈,用一维数组来存放顺序队列中的数据元素。队头位置设在数组下标为 0 的端,用 front 表示;队尾位置设在数组的另一端,用 rear 表示。 front 和 rear 随着插入和删除而变化。当队列为空时, front=rear=-1。

图是顺序队列的两个指示器与队列中数据元素的关系图。

 

 

 

1.2 顺序队列-(循环顺序队列):

如果再有一个数据元素入队就会出现溢出。但事实上队列中并未满,还有空闲空间,把这种现象称为“假溢出”。这是由于队列“队尾入队头出”的操作原则造成的。解决假溢出的方法是将顺序队列看成是首尾相接的循环结构,头尾指示器的关系不变,这种队列叫循环顺序队列(Circular sequence Queue)。循环队列如图所示。

 

 

1.3 顺序队列-代码实现:

把循环顺序队列看作是一个泛型类,类名叫 CSeqStack<T>,“ C”是英文单词 circular 的第 1 个字母。 CSeqStack<T>类实现了接口 IQueue<T>。用数组来存储循环顺序队列中的元素,在 CSeqStack<T>类中用字段 data 来表示。用字段maxsize 表示循环顺序队列的容量, maxsize 的值可以根据实际需要修改,这通过CSeqStack<T>类的构造器中的参数 size 来实现,循环顺序队列中的元素由 data[0]开始依次顺序存放。字段 front 表示队头, front 的范围是 0 到 maxsize-1。字段 rear表示队尾,rear 的范围也是 0 到 maxsize-1。如果循环顺序队列为空,front=rear=-1。当执行入队列操作时需要判断循环顺序队列是否已满,如果循环顺序队列已满,(rear + 1) % maxsize==front , 循 环 顺 序 队 列 已 满 不 能 插 入 元 素 。 所 以 ,CSeqStack<T>类除了要实现接口 IQueue<T>中的方法外,还需要实现判断循环顺序队列是否已满的成员方法。

 

2.1链队列:

队列的另外一种存储方式是链式存储,这样的队列称为链队列(Linked Queue)。同链栈一样,链队列通常用单链表来表示,它的实现是单链表的简化。所以,链队列的结点的结构与单链表一样,如图所示。由于链队列的操作只是在一端进行,为了操作方便,把队头设在链表的头部,并且不需要头结点。

 

2.2链队列-链队列结点类:

 

public class Node<T>
{
private T data; //数据域
private Node<T> next; //引用域
//构造器
public Node(T val, Node<T> p)
{
    data = val;
    next = p;
}
//构造器
public Node(Node<T> p)
{
    next = p;
}
//构造器
public Node(T val)
{
    data = val;
    next = null;
}
//构造器
public Node()
{
    data = default(T);
    next = null;
}
//数据域属性
public T Data
{
get
{
return data;
}
set
{
data = value;
}
}
//引用域属性
public Node<T> Next
{
get
{
return next;
}
set
{
next = value;
}
}
}

 

 

2.3链队列-代码实现:

把链队列看作一个泛型类,类名为 LinkQueue<T>。 LinkQueue<T>类中有两个字段 front 和 rear,表示队头指示器和队尾指示器。由于队列只能访问队头的数据元素,而链队列的队头指示器和队尾指示器又不能指示队列的元素个数,所以,与链栈一样,在 LinkQueue<T>类增设一个字段 num 表示链队列中结点的个数。

 

栈和队列的应用举例:

编程判断一个字符串是否是回文。回文是指一个字符序列以中间字符为基准两边字符完全相同,如字符序列“ ACBDEDBCA”是回文。

算法思想:判断一个字符序列是否是回文,就是把第一个字符与最后一个字符相比较,第二个字符与倒数第二个字符比较,依次类推,第 i 个字符与第 n-i个字符比较。如果每次比较都相等,则为回文,如果某次比较不相等,就不是回文。因此,可以把字符序列分别入队列和栈,然后逐个出队列和出栈并比较出队列的字符和出栈的字符是否相等,若全部相等则该字符序列就是回文,否则就不是回文。

 

以上是关于数据结构栈和队列的主要内容,如果未能解决你的问题,请参考以下文章

《线性表的插入和删除算法实现》以及《栈和队列的插入和删除算法实现》的c语言代码

数据结构系列-栈和队列

✨✨C语言版数据结构基础---栈和队列

数据结构(C语言) 栈和队列

c 语言数据结构栈和队列的相关操作

20-数据结构