数据结构Chap 3 / 栈和队列

一、栈

1.1 栈的基本概念:

        栈是一种特殊的线性表,其特殊性体现在元素插入和删除运算上,它的插入和删除运算仅限定在表的某一端进行,不能在表中间和另一端进行。允许进行插入和删除的一端称为栈顶(top),另一端称为栈底(bottom)。栈的插入操作称为进栈(Push),删除操作称为出栈(pop)。处于栈顶位置的数据元素称为栈顶元素。不含任何数据元素的栈称为空栈。

1.2 基本操作

        (1)进栈:插入[ Push(x) ]

        (2)出栈:删除[ Pop( ) ]

        (3)取栈顶元素  Top( )

        (4)判断栈是否为空/满   IsEmpty( )   IsFull( )

【创销,增删查(改)】

注:这些操作都可以在常数时间内被完成。

        先进后出,后进先出。

1.3 应用

        (1)函数调用、递归

        (2)编辑器的撤回操作。

        (3)检查括号是否平衡(有左括号必须得有右括号)

二、栈的实现

2.1 用数组实现一个栈

#include <iostream>
using namespace std;
#define MAX_SIZE 101
int A[MAX_SIZE];//全局变量
int top = -1;//索引变量,类似于表明位置的指针
void Push(int x)
{
	if (top == MAX_SIZE - 1)//top到最后一个位置了,在加就要溢出了
	{
		cout << "Error:stack is full of elements!" << endl;
		return;
	}
	A[++top] = x;
}
bool Pop(int &x)
{
	if (top == -1)//已经是空栈了
	{
		cout << "Error:No element to pop!" << endl;
		return false;
	}
	x = A[top];
	top--;
	return true;
}//x将会得到取出来的值
void Print()
{
	int i;
	for (i = 0; i <= top; i++)//注意这里是小于等于,因为top也是从0开始算的
		cout << A[i] << endl;
}
int Top()
{
	return A[top];//返回栈顶元素值
}
int main()
{
	Push(2);
	Push(5);
	Push(7);
	Push(4);
	int x;
	int TestVictory = Pop(x);
	cout << "TestVictory= " << TestVictory << " the got element is "<<x<< endl;
	Print();
	return 0;
}

数组栈的缺点:栈的大小不可变,有可能出现栈溢出或者栈浪费 

共享栈:

(1)定义:两个栈共享同一片内存空间,两个栈从两边往中间增长。

(2)初始化:0号栈的栈顶指针top0=-1;1号栈的栈顶指针top1=MaxSize;【放元素时都是先动指针,后存入值】

(3)栈满条件:top0+1==top1;

2.2 用单链表实现一个栈

(1)在链表尾插入/删除,时间复杂度为O(n)

(2)在链表头部插入/删除,时间复杂度仅为O(1)

        其实就是头插法头删法。

#include <iostream>
using namespace std;
typedef struct Node
{
	int data;
	Node* link;
}Node;
Node* top = NULL;//就是之前的“头指针”
void Push(int x)
{
	Node* temp = new Node;
	temp->data = x;
	temp->link = top;//之前的值在你后面
	top = temp;//你作为链表的头结点,同时为栈顶
}
void Pop()
{
	if (top == NULL)//空栈
	{
		cout << "Error:No element to pop!\n";
		return;
	}
	Node* temp = top;//新建一个指向头结点的temp来记录当前的头结点(因为马上要释放它了)
	top = top->link;//让头指针和第二个结点建立链接
	free(temp);//释放被删除的原头结点
}
void Print()
{
	Node* temp = top;
	cout << "stack is:\n";
	while (temp != NULL)
	{
		cout << temp->data << endl;
		temp = temp->link;
	}
}
void IsEmpty();
void Top();
int main()
{
	Push(1);//stack:1
	Push(4);//stack:4 1
	Push(7);//stack:7 4 1
	Print();
	Pop();//stack:4 1
	Print();
	return 0;
}

用单链表表示栈的优点:一般不考虑溢出的问题,除非用光计算机的全部内存。

合法输出数目为卡特兰数:即 \frac{1}{n+1}C_{2n}^{n}

 2.3 栈的应用

(1)反转

a.反转字符串

#pragma once
#include "iostream"//不用加.h
#include <string>
using namespace std;
#include<stack>
void Reverse(char* C, int n)
{
	stack<char> S;//声明一个可以传入char类型的栈
	for (int i = 0; i < n; i++)//将C数组中元素从首元素压入char类型的栈底下
	{
		S.push(C[i]);//注意stack标准库中的push是小写(自定义的函数才使用首字母大写)
	}
	for (int i = 0; i < n; i++)
	{
		C[i] = S.top();//将栈顶上的元素放前面,实现数组倒序
		S.pop();//赋值完,需要把栈顶元素丢掉
	}
}
int main()
{
	char C[51];
	cout << "Please input a string:\n";
	cin>>(C);
	Reverse(C, strlen(C));//标明string类才能使用strlen
	cout << "Output is:";
	for (int i = 0; i < strlen(C); i++)
		//这里不是字符串string,所以没法采用直接cout<<C的命令
		//C中采用%s
		cout << C[i];
	return 0;
}

b.反转链表

void Reverse()
{
	if (head == NULL)
		return;
	stack<Node*>S;//定义一个存结点地址的栈,这里的Node*,类似于int,放的数据类型
	Node* temp = head;//先把head指向head,方便后面压入栈中
	while (temp != NULL)
	{
		S.push(temp);//压进去
		temp = temp->next;//往下走,这里原链表没有消失
	}
	temp = S.top();//针对栈中第一个元素需要特殊对待,因为涉及到头指针
	head = temp;//栈顶元素当头指针【这里不能让head直接等于S.top(),因为也需要改变temp的值】
	S.pop();//把栈顶元素T走
	while (!S.empty())//栈不为空
	{
		temp->next = S.top();//排一个新链条
		S.pop();//弹出
		temp->next = temp;//预备排到后面
	}
}

 (2)检查括号匹配

typedef struct Node
{
	int data;
	Node* next;
}Node;
typedef struct
{
	Node* front, *rear;
}SqStack;
//初始化栈
void InitStack(SqStack& S);
//判断栈是否为空
bool IsEmpty(SqStack S);
//新元素入栈
bool Push(SqStack& S, char x);
//出栈,并用x储存栈顶元素
bool Pop(SqStack& S,char x);
/*利用栈判断括号是否匹配*/
bool BracketCheck(char str[], int length)
{
	SqStack S;
	for (int i = 0; i < length;i++)
	{
		if (str[i] == '(' || str[i] == '[' || str[i] == '{')
			Push(S, str[i]);
		else
		{
			if (!IsEmpty(S))
				return false;
			char topElem;
			Pop(S, topElem);//弹出来最后栈顶元素,并用topElem储存
			if (str[i] == '(' && topElem != ')')
				return false;
			else if
				(str[i] == '[' && topElem != ']')
				return false;
			else if
				(str[i] == '{' && topElem != '}')
				return false;//这里最好不要合在一起写,因为逻辑判断顺序可能会乱,要么就是加额外的括号,更乱
		}
	}
	return IsEmpty(S);//上述判断完还不能说明一定成功,最后还需对栈判空决定是否匹配成功
}

ps:

步骤:依次扫描所有字符,遇到左括号入栈,遇右括号则弹出栈顶元素检查是否匹配。

匹配失败情况:

1.右括号单身(右括号进来,发现栈空) 

2.左右括号不匹配(进来的右括号发现栈顶元素不是对应的左括号)

3.左括号单身(最后判空,发现栈中含有多的左括号)

 (3)栈在表达式求值中的应用【通过栈分别实现机转、机算】

a.中缀转后缀:【手转】(一般是习惯由内而外地写)

1.确定中缀表达式中各个运算符的运算顺序

2.选择下一运算符,按照【左操作数 右操作数 运算符】 的方式组合成一个新操作数

3.如果还有运算符没被处理,就重复步骤2.

由于运算顺序不唯一,因此手算对应的后缀表达式也不唯一。

算法的确定性:机算结果确定,因为有左优先原则,只要左边的运算符能先计算就先左。

 b.中缀转后缀的机算方法【机转】(一般是习惯由前而后地写)

/*需要一个栈(存取暂时无法确定运算顺序的运算符或界限符)和一个字符串数组(存放后缀表达式)*/ 

1.从左到右扫描

       a.如果是操作数,则直接存入后缀表达式

       b.如果是界限符【括号】,则判断,若是左括号就直接入运算符栈。如果是右括号,则依次弹出运算符栈中运算符,直到弹出第一个左括号为止

        c.如果是运算符【+-*/】,则依次弹出栈中高于或等于自己的运算符【若比我低,你凭什么先进后缀表达式先计算,给我在栈里呆着;若不比我低,则按照左优先原则,你是大哥你先算】,并依次弹出放入后缀表达式,若碰到左括号或者栈空则停止(碰到左括号证明需要优先考虑括号里的,别弹了,你也进来呆着吧)再把当前运算符压入栈。

2.最后判断栈中是否为空,有的话依次弹出

         机理其实和我们眼睛看一样,从左往右看,碰到优先级高的【人:最内层小括号】【机器:符号先出现的】就先运算这两个数(注意先压入栈的一定是左操作数),并把结果先放到这。

        这样转化后只是改变了运算符的位置,便可以得到无括号的运算式【但是操作数的相对位置没有改变】

 后缀的机器求值方法:【机算】

/*需要一个栈:存取暂时无法确定运算顺序的操作数*/

1.从左往右扫描下一个元素,直到处理完所有元素。

2.若扫描到数则压入栈中,并回到步骤1;否则执行步骤3

3.若扫描到运算符,则弹出两个栈顶元素,执行该运算,运算结果压回栈顶,并回到步骤1

中缀的机器求值方法:【机算】

/*需要一个运算符栈(存取暂时无法确定运算顺序的操作符或界限符)和一个操作数栈(存放不确定运算顺序的操作数)*/ 

1.从左到右扫描

       a.如果是操作数,则直接存入后缀表达式

       b.如果是运算符或者界限符,则按照“中缀转后缀”的逻辑压入运算符栈(期间也会弹出运算符,若弹出来一个,就得在操作栈弹两个的栈顶元素进行运算,运算结果再压入操作数栈)

2.最后判断运算符栈中是否为空,有的话依次弹出、计算、压回(操作数栈)

三、队列

3.1 定义

只允许在一端插入,在另一端删除的线性表。【一种操作受限的线性表】

3.2 基本操作

创销、增删查改【入队-增、出队-删】

(1)队列的顺序表(数组)实现

方案一:仅利用代码逻辑判断,定义序号指针front和rear 【牺牲一个存储位置】

注:这里rear指向是队尾元素的后一个位置,rear在前面走,front在后面跟 

#include <iostream>
using namespace std;
#define MaxSize 10
typedef struct
{
	int data[MaxSize];//使用静态数组存放队列元素
	int front, rear;//定义两个“指针”来表明序号位置,注意rear指向的位置是即将插入的位置
//即rear指向队尾后一个元素
}SqQueue;//顺序队列 
void InitQueue(SqQueue &Q)
{
	Q.front = Q.rear = 0;//队头队尾都指向0,这是初始状态
}
void testQueue()
{
	SqQueue Q;
	InitQueue(Q);
}
bool QueueEmpty(SqQueue Q)
{
	if (Q.front = Q.rear)//队空条件
		return true;
	else
		return false;
}
bool EnQueue(SqQueue& Q, int x)//入队操作,不需要取出x,所以不用引用符号
{
	if ((Q.rear + 1) % MaxSize == Q.front)//rear的后一位为front时为满队列,否则还可以插入
		return false;
	else
		Q.data[Q.rear] = x;//Q.rear是一个整体,表示序号(类似指针)
	Q.rear = (Q.rear + 1) % MaxSize;//先填入,再让rear指针后移。
//【+1表示后移,但是不是简单的后移】这一句让rear的排列升华了,不是线性队列而是“循环队列”。
//后面站不下了,可以站到前面来,直到全部占满
}
bool DeQueue(SqQueue& Q, int &x)//出队操作,需要让参数x改变,所以要加引用符号
{
	if (Q.rear == Q.front)
		return false;//队空
	x = Q.data[Q.front];//[出队头,入队尾]
    Q.front=(Q.front+1)%MaxSize;//每出队一个元素,front指针往后移一位
	return true;
}

队列元素个数: (rear+MaxSize-front)%MaxSize

 方案二:利用辅助变量size,记录元素个数

入队一个元素时,size++;出队一个元素时,size--;

在front和rear相等的情况下,可以分Size==MaxSize和Size==0的情况。

 

 方案三:增加辅助变量tag,记录刚刚的动作是插入还是删除

队满条件:front==rear&&tag==1;//tag=1表示刚刚插入了数据

队空条件:front==rear&&tag==0;//tag=0表示刚刚删除了数据

ps:如果rear指向的不是队尾元素的后一个位置,而直接指向的队尾元素呢?

1.入队操作:(先移后填)

让front指向0,而rear指向n-1的位置【如10 个元素,则front在0,rear在9】

先后移rear,再填入。【填入第一个元素时,rear又指向了0,再填】

2.出队操作:

        这个没有变化,还是先出后移

3.判空:

        (Q.rear+1)%MaxSize==Q.front

4.判满:

        a:牺牲最后一个存储单元【不能填入9这个单元,反之如果可以填入9,则和队空条件一致,无法区分】

        b:增加辅助变量(size或者tag)

 (2)队列的链式实现

优点:永远不用怕存满【除非把计算机内存用完,基本不会,所以入队不需要判满

基本信息:

#include <iostream>
using namespace std;
typedef struct Node
{
	int data;
	Node* next;
}Node;//每个结点类型
typedef struct
{
	Node* front, *rear;//定义链式队列中的两个指针,分别指向头结点和尾结点
//【注意rear前面也要有*】
}LinkQueue;
void InitQueue(LinkQueue& Q)//链式不用判满,只会判空,所以这里不用bool 
{
	Q.front = Q.rear = new Node;//让头指针和尾指针都先指向头结点
	Q.front->next = NULL;//头结点的后面为空
}
void testQueue()
{
	LinkQueue Q;//声明一个链式队列【未分配内存】
	InitQueue(Q);//初始化后,给Q分配了内存
	//后续操作...
}
方案一:带头结点
bool IsEmpty(LinkQueue Q)
{
	if (Q.front == Q.rear)
		//if(Q.front==NULL)时,队列也为空
		return true;
	else
		return false;
}
void EnQueue(LinkQueue& Q, int x)
{
	    Node* temp = new Node;
	//malloc用法:Node* temp=(Node*)malloc(sizeof(Node ))
	temp->data = x;
	temp->next = NULL;
	Q.rear->next = temp;//你作为我的最后一个结点
	Q.rear = temp;//rear指针指向你
}
bool DeQueue(LinkQueue& Q, int& x)//出队操作有可能失败,用bool类型
{
	if (Q.front = Q.rear)
		return false;
	Node* temp = Q.front->next;
	x = temp->data;
	Q.front->next = temp->next;
	if (Q.rear == temp)
		Q.rear = Q.front;
        free(temp);//别忘了释放不用的结点空间
	return true;
}
方案二:不带头结点
void EnQueue(LinkQueue &Q,int x)
{
	Node* temp = new Node;
	temp->data = x;
	temp->next = NULL;
	if (Q.front == NULL)//插入第一个结点时要特别注意
	{
		Q.front = temp;
		Q.rear = temp;
	}
	else//如果插入之前有结点,则跟之前一样
	{
		Q.rear->next = temp;
		Q.rear = temp;
	}
}

3.3 队列的应用

(1)树的层次遍历

后面入队加children,前面出队parent

(2) 图的广度优先遍历

入队相邻的,出队已有的

(3)队列在操作系统中的应用

先来先服务(FCFS,First Come First Service),一个CPU处理多个程序

三、特殊矩阵的压缩存储

1.一维数组 int a[i],

 则 location(a[i])=location(a[0])+i*sizeof(int);

2.二维数组int b[i][j]

若采取行优先存储,则 location(b[i][j])=location{a[0][0]}+(i*M+j)*sizeof(int);【M为每排元素个数】

若采取列优先存储,则 location(b[i][j])=location{a[0][0]}+(j*M+i)*sizeof(int);【M为每排元素个数】

3.特殊矩阵【可以压缩存储空间】

(1)对称矩阵

1.基础知识:n阶矩阵中aij=aji;所以只需储存主对角线和上三角数据。

2.数据总大小:1+2+3+...+n,所以一共有(n+1)*n/2个数据。数组下标从0至(n+1)*n/2-1

3.如何应用(访问):实现一个“映射”函数,实现从矩阵下标【aij】到一位数组下标【Bk】

按照行优先原则:现在是(1+2+3...+(i-1)+j)元素,即k=(i*(i-1)+j)-1

(2)三角矩阵

1.基础知识:上三角矩阵是指除了主对角线和上三角区(乱的),其他元素都相同(常数c)

2.数据总大小:比上一个内存多1,即数组下标从0至(n+1)*n/2,因为还需要一个空间存c

3.以下三角为例:

若i<j , 则k=(i*(i-1)+j)-1,和上面一样不变

若i>j , 则k=(n+1)*n/2,因为常数的永远存在最后一个

上三角:

若i<j,则k=(i-1)*(2n-i+2)/2

若i>j,则k=(n+1)*n/2,因为常数的永远存在最后一个

(3)三对角矩阵

1.又称带状矩阵

当|i-j|>1时,有aij=0   (0\leq i,j\leq n)【外面的全为0】

2.数据总大小:3n-2【第一行和最后一行少个元素,其余都是3个元素】

3.已知行列号aij,如何得到数组下标k:

(1)当i=1时,aij是第j-i+1个元素

(2)当i>1时,aij的前i-1行有3(i-1)-1【先假设全满,再减去一个】

aij是第i行的第j-i+2个元素

所以aij是第2i+j-2个元素

k=2i+j-3

已知数组下标k,如何得到行列号aij:

数组下标k,则是第k+1个元素

前(i-1)行一共有3(i-1)-1;

前i行一共有3i-1;

若该元素刚好在第i行,则有(3i-4)<=k+1<(3i-1)

(4)稀疏矩阵(0太多了,有效元素少)

1.顺序存储——用三元组(struct)存“行、列、值”

2.十字链表法——利用非零结点定义结点形成链表

typedef struct Node
{
    int row;
    int line;
    int value;
    Node* down;
    Node* right;
}Node;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值