数据结构(三)——栈和队列


这是两种受限操作的线性表。

1 栈

1.1 栈的概念

  • 栈(stack):限制在一端进行插入和删除操作的线性表。LIFO(last in first out)线性表。
  • 栈顶:进行插入和删除操作的一端。使用栈顶指针top指示栈顶元素。
  • 栈底:固定端,又称表头。
  • 空栈:当表中没有元素时称为空栈。
    顺序栈示意图

1.2 栈的顺序存储表示

顺序栈用一维数组存储栈,根据数组是否可以根据需求扩大分为:

  • 静态顺序栈:实现简单,但不能根据需要增大栈的存储空间。
  • 动态顺序栈:实现稍复杂,但可以根据需要增大栈的存储空间。

1.2.1 栈的动态顺序存储表示

1.2.1.1 实现要点:
  • bottom为栈底指针,栈底固定不变;栈顶指针top(指示当前栈顶位置)随着进栈和出栈操作而变化。
  • 栈空条件top=bottom
  • 节点进栈:首先将数据元素保存到top所指向的位置,然后执行top+1,其指向栈顶的下一个存储位置。
  • 节点出栈:首先将top减一(此时指向栈顶元素的位置),然后将栈顶元素取出。

在这里插入图片描述

1.2.1.2 实现code
栈的类型定义
#define STACK_SIZE 100 		// 初始大小
#define STACKCREMENT 10 	// 存储空间增量大小
typedef int ElemenType;
typedef struct sqstack{
	ElemType *bottom;	// 栈不存在时为NULL
	ElemType *bottom;	// 栈顶指针
	int stacksize;	// 当前已分配空间
}SqStack;
栈的初始化
bool Init_Stack(void)
{
	SqStack S;
	S.bottom = (ElemType *)malloc(STACK_SIZE*sizeof(ElemType));
	if(!S.bottom)
		return false;
	S.top = S.bottom;	// 栈空
	S.stacksize = STACK_SIZE;
	return true;
}
元素进栈
bool push(SqStack S, ElemType e)
{
	if(S.top-S.bottom >= S.stacksize-1)	// 栈满
	{
		S.bottom = (ElemType *)realloc((STACK_SIZE+STACKCREMENT)*sizeof(ElemType));	// 追加存储空间
		if(!S.bottom)
			return false;
		S.top = S.bottom + S.stacksize;
		S.stacksize += STACKCREMENT;
	}
	*S.top = e;	// e称为新的栈顶
	S.top++;	// 栈顶增加1
	return true;
}
元素出栈
bool pop(SqStack S, ElemType *e)
{
	if(S.top==S.bottom)	// 空栈,无法出栈操作
		return false;
	S.top--;
	*e = *(S.top);
	return true;
}

1.2.2 栈的静态顺序存储表示

1.2.1.1 实现要点:
  • 栈底固定不变;栈顶指针top(指示当前栈顶位置)随着进栈和出栈操作而变化。
  • 栈空条件top=0表时栈空的初始状态
  • 节点进栈:首先执行top++top指向栈顶位置,然后将数据保存到栈顶(top指向的当前位置)。
  • 节点出栈:首先将栈顶元素取出,然后top--,指向新的栈顶位置。
  • 栈满条件top==Maxsize-1

在这里插入图片描述

1.2.2.2 实现code
栈的类型定义
#define MAX_SIZE 100 		// stack大小
typedef int ElemenType;
typedef struct sqstack{
	ElemType stack_array[MAX_SIZE];		
	int top;	
}SqStack;
栈的初始化
SqStack Init_Stack(void)
{
	SqStack S;
	S.top = 0;
	return S;
}
元素进栈
bool push(SqStack S, ElemType e)
{
	if(S.top==MAX_SIZE-1)	// 栈满,返回错误,上溢出,设法避免
		return false;
	S.top++;		// 栈顶指针加1
	S.stack_array[S.top] = e;	// e称为新的栈顶
	return true;
}

栈满时进栈运算会产生空间溢出(上溢出),是一种出错状态,应设法避免。

元素出栈
bool pop(SqStack S, ElemType *e)
{
	if(S.top==0)	// 空栈,会产生下溢出错误
		return false;
	*e = S.stack_array[S.top];
	S.top--;
	return true;
}

栈空时做出栈会产生溢出(下溢出)。下溢出可能是正常现象,因为在栈使用时,其初态或终态都是空栈,所以下溢出常用做控制转移的条件。

1.3 栈的链式存储表示

又称链栈,是运算受限的单链表。其插入和删除操作都只能在表头位置上进行。
在这里插入图片描述
由上图可以看出,链栈没必要设置单链表那样的附加头节点,栈顶指针top就是链表的头指针。

1.3.1 节点表示

typedef struct Stack_Node
{
	ElemType data;
	struct Stack_Node *next;
}Stack_Node;

1.3.2 链栈的基本操作实现

初始化
Stack_Node *Init_Link_Stack(void)
{
	Stack_Node *top;
	top = (Stack_Node *)malloc(sizeof(Stack_Node));
	top->next = NULL;
	return top;
}
入栈
bool push(Stack_Node *top, ElemType e)
{
	Stack_Node *p = (Stack_Node *)malloc(sizeof(Stack_Node));
	if(!p)
		return false;
	p->data = e;
	p->next = top->next;
	top->next = p;
	return true;
}
出栈
bool push(Stack_Node *top, ElemType *e)
{
	Stack_Node *p ;
	if(top->next==NULL)	// 栈空
		return false;
	p = top->next;
	*e = p->data;
	top->next = p->next;
	free(p);
	p = NULL;
	return true;
}

1.4 栈的应用

数制转换

十进制整数N向其它进制数d的转换是计算机实现计算的基本问题。
转换法则:
n = (n div d)*d + n mod d
其中:div为整除运算;mod为求余运算。

/* 将十进制整数n转换成d进制数 */
void conversion(int n, int d)
{
	SqStack S;
	int k, *e;
	S = Init_Stack();
	while(n > 0)
	{
		k = n %d;
		push(S,k);
		n = n/d;
	}
	// 求出所有余数
	while(S.top!=0)	// 栈不空时出栈
	{
		pop(S,e);
		printf("%ld", *e);
	}
}

括号匹配

思想:从左至右扫描一个字符串或表达式,每个右括号将与最近的那个左括号相匹配。可以将枣苗过程中遇到的左括号存放到堆栈中。每当遇到一个右括号时,就将它与栈顶的左括号(如果存在)相匹配,同时从栈删除该左括号。

SqStack S;
S = Init_Stcak();
int Match_Brackets()
{
	char ch,x;
	scanf("%c", &ch);
	while(asc(ch)!=13)
	{
		if((ch=='('||ch=='['))
			push(S,ch);
		else if(ch==']')
		{
			x = pop(S);
			if(x!='[')
			{
				printf("'['括号不匹配");
				return false;
			}
		}
		else if(ch==')')
		{
			x = pop(S);
			if(x!='(')
			{
				printf("'('括号不匹配");
				return false;
			}
		}
		if(S.top!=0)
		{
			printf("括号数量不匹配");
			return false;
		}
		else
			return true;
	}
}

栈与递归调用

栈在程序设计语言中实现递归调用。为了保证递归调用正确的进行,系统设立一个“递归工作栈”,作为整个递归调用过程期间使用的数据存储区。
每一层递归包含的信息如:参数、局部变量、上一层返回地址构成一个 “工作记录” 。每进入一层递归,就会产生一个新的工作记录压入栈顶;每退出一层递归,就从栈顶弹出一个工作记录。

从被调函数返回调用函数的一般步骤为:

  1. 若栈为空,则执行正常返回
  2. 从栈顶弹出一个工作记录
  3. 将“工作记录”中的参数值、局部变量赋给相应的变量;读取返回地址
  4. 将函数值赋给相应的变量
  5. 转移到返回地址

2 队列

允许在一端(队尾)进行插入,另一端(队首)进行删除的线性表表。具有FIFO(First in first out)的特点。

2.1 队列的顺序表示实现

使用一维数组依次存放队首到队尾各个元素。

#define MAX_SIZE 100
typedef struct queue
{
	ElemType Queue_array[MAX_SIZE];
	int front;
	int rear;
}SqQueue;

2.1.1 队列顺序存储结构下的一些概念

  • 初始化:front=rear=0
  • 入队: 将新元素插入rear所指向的位置,然后rear加1
  • 出队:删去front所指元素,然后加1并返回被删的元素
  • 对列为空:front=rear
  • 队满:rear=MAX_SIZE-1front=rear

在非空队列中,队首元素始终指向队头元素,而队尾指针始终指向队尾元素下一位置。

这种存储结构存在一个问题——假溢出。因为在入队和出队的过程中,头、尾指针只增加不减小,致使被删除元素的空间永远无法重新利用。尽管队列中实际元素个数可能远小于数组大小,但是由于队尾指针已超过向量空间的上界而不能做入队操作。

2.1.2 循环队列

为了充分利用向量空间,避免出现假溢出问题,可以采取循环队列的方式解决。
操作与之前相同,不过当队首、队尾指针指向向量上界(MAX_SIZE-1)时,其加一结果是指向响亮的下界0。
用模运算可以表示为:

i=(i+1)%MAX_SIZE;

入队时,尾指针向前追赶头指针
出队时,头指针头指针向前追赶尾指针
约定入队前,测试尾指针在循环意义下加1是否等与头指针,如相等认为队满。

  • rear所指向的单元始终为空
  • 循环队列为空front=rear
  • 循环队列满(rear+1)%MAX_SIZE=front

2.1.3 循环队列的基本操作

初始化
SqQueue Init_CirQueue(void)
{
	SqQueue Q;
	Q.front=Q.rear=0;
	return Q;
}
入队操作
bool Insert_CirQueue(SqQueue Q, ElemType e)
{
	if((Q.rear+1)%MAX_SIZE==Q.front)
		return false;
	Q.Queue_array[Q.rear] = e;		// 元素e入队
	Q.rear = (Q.rear+1)%MAX_SIZE;	// 队尾指针向前移动
	return true;
}
出队操作
bool Delete_CirQueue(SqQueue Q, ElemType *x)
{
	if(Q.front+1==Q.rear)	// 队空
		return false;
	*x = Q.Queue_array[Q.front];		
	Q.front = (Q.front+1)%MAX_SIZE;		// 队首指针向前移动
	return true;
}

2.2 队列的链式存储表示与实现

2.2.1 队列的链式存储结构

队列的链式存储结构建成链队列,限制对表头进行删除操作和表尾进行插入操作的链表。
需要用到两种不同的节点:数据元素节点、队首指针和队尾指针的节点。

数据元素节点

typedef struct Qnode
{
	ElemType data;
	struct Qnode *next;
};

指针节点

typedef struct link_queue
{
	Qnode *front, *rear;
}Link_Queue;

在这里插入图片描述

2.2.2 链表的接本操作

链队列初始化
LinkQueue *Init_LinkQueue(void)
{
	LinkQueue *Q;
	QNode *p;
	p = (QNode *)malloc(sizepf(QNode));		// 开辟头节点
	p->next = NULL;
	Q = (LinkQueue *)malloc(sizeof(LinkQueue));	// 链队的指针节点
	Q.front=Q.rear=p;
	return Q;
}
链队列的入队
bool Insert_CirQueue(LinkQueue *Q, ElemType e)
{
	QNode *p = (QNode *)malloc(sizeof(QNode));
	if(!p)
		return false;
	p->data = e;
	p->next = NULL;
	Q.rear->next=p;
	Q.rear = p;	// 新节点插入到队尾
	return true;
}
链队列的出队
bool Del_CirQueue(LinkQueue *Q, ElemType *e)
{
	QNode *p;
	if(Q.front==Q.rear)
		return false;
	p = Q.front->next;	// 取队首节点
	*e = p->data;
	Q.front->next = p->next;	// 修改队首指针
	if(p==Q.rear)	// 当队列只有一个节点时应防止丢失队尾指针
		Q.rear = Q.front;
	free(p);
	p=NULL;
	return true;
}
链队列的撤销
void Destory_LinkQueue(LinkQueue *Q)
{
	while(Q.front!=NULL)
	{
		Q.rear=Q.front->next;	// 令队尾指针指向队列的第一个节点
		free(Q.front);			// 每次释放一个节点 ,第一次是头节点,以后是元素节点
		Q.front=Q.rear;			
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. 什么是二叉树? 二叉树是一种树形结构,其中每个节点最多有两个子节点。一个节点的左子节点比该节点小,右子节点比该节点大。二叉树通常用于搜索和排序。 2. 二叉树的遍历方法有哪些? 二叉树的遍历方法包括前序遍历、中序遍历和后序遍历。前序遍历是从根节点开始遍历,先访问根节点,再访问左子树,最后访问右子树。中序遍历是从根节点开始遍历,先访问左子树,再访问根节点,最后访问右子树。后序遍历是从根节点开始遍历,先访问左子树,再访问右子树,最后访问根节点。 3. 二叉树的查找方法有哪些? 二叉树的查找方法包括递归查找和非递归查找。递归查找是从根节点开始查找,如果当前节点的值等于要查找的值,则返回当前节点。如果要查找的值比当前节点小,则继续在左子树中查找;如果要查找的值比当前节点大,则继续在右子树中查找。非递归查找可以使用队列实现,从根节点开始,每次将当前节点的左右子节点入/队列,直到找到要查找的值或者/队列为空。 4. 二叉树的插入与删除操作如何实现? 二叉树的插入操作是将要插入的节点与当前节点的值进行比较,如果小于当前节点的值,则继续在左子树中插入;如果大于当前节点的值,则继续在右子树中插入。当找到一个空节点时,就将要插入的节点作为该空节点的子节点。删除操作需要分为三种情况:删除叶子节点、删除只有一个子节点的节点和删除有两个子节点的节点。删除叶子节点很简单,只需要将其父节点的对应子节点置为空即可。删除只有一个子节点的节点,需要将其子节点替换为该节点的位置。删除有两个子节点的节点,则可以找到该节点的后继节点(即右子树中最小的节点),将其替换为该节点,然后删除后继节点。 5. 什么是平衡二叉树? 平衡二叉树是一种特殊的二叉树,它保证左右子树的高度差不超过1。这种平衡可以确保二叉树的查找、插入和删除操作的时间复杂度都是O(logn)。常见的平衡二叉树包括红黑树和AVL树。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值