简而言之,就是顺序表和链表在满足栈和队列出入数据的特性的基础上衍生出的新的数据结构,也就是栈和队列,他们都有两种实现方式,顺序表实现和链表实现。
栈是只允许在栈顶进行插入删除操作的线性结构,也看作特殊的线性表。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出***LIFO(Last In First Out)***的原则。
按存储结构,栈分为顺序栈和链栈。栈的顺序存储–顺序栈。栈的链式存储–链栈
栈用来解决先进后出的问题,常见的有进制转换、括号匹配问题、行编辑程序、迷宫求解、表达式求值、八皇后问题、函数递归调用。这些都在本文末尾一一讲解
LIFO是栈的特点,栈接口函数数据插入和删除操作都是在栈顶实现:
逻辑结构如下:
顺序栈实现算法概述:利用顺序表,通过改变出入数据的方式,达到后进先出的效果。
静态顺序表就是实现静态栈,动态顺序表实现的则是动态栈。我们一般用动态栈。
下面是动态栈实现的代码:
#pragma once
#include
#include
#include
#include
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top; //栈顶,也可以用指针,但是没必要
int capacity; //容量
}Stack;
//初始化栈
void StackInit(Stack* ps);
//销毁栈
void StackDestory(Stack* ps);
//入栈
void StackPush(Stack* ps, STDataType x);
//出栈
void StackPop(Stack* ps);
//获取栈顶元素
STDataType StackTop(Stack* ps);
//获取栈中有效的元素个数
int StackSize(Stack* ps);
//判断栈是否为空,空则返回非零,不为空则返回0
bool StackEmpty(Stack * ps);
#include"Stack.h"
void StackInit(Stack* ps)//初始化
{
assert(ps);//断言判断是否为空
//凡是传进来指针,就断言判断。凡是申请指针空间,就要判断是否成功!
ps->a = (STDataType*)malloc(4 * sizeof(STDataType));
if (ps->a == NULL)
{
printf("malloc failed\n");
exit(-1);
}
ps->capacity = 4;
ps->top = 0;
}
void StackDestory(Stack* ps)//销毁栈
{
assert(ps);//断言判断是否为空
free(ps->a);
ps->a = NULL;//释放完指针要置空,否则就是野指针
ps->top = ps->capacity = 0;
}
void StackPush(Stack* ps, STDataType x)//入栈
{
assert(ps);//断言判断是否为空
if (ps->top == ps->capacity)
{
STDataType* tmp = (STDataType*)realloc(ps->a, 2 * ps->capacity * sizeof(STDataType));
//凡是传进来指针,就断言判断。凡是申请指针空间,就要判断是否成功!
if (tmp == NULL)
{
printf("realloc failed\n");
exit(-1);//exit(0)表示正常退出,exit(x)x非零表示非正常退出
}
else
{
ps->a = tmp;
ps->capacity *= 2;
}
}
ps->a[ps->top] = x;
ps->top++;
}
void StackPop(Stack* ps)//出栈
{
assert(ps);//断言判断是否为空
assert(ps->top);//栈空
//ps->a[ps->top]=0;是不可取的!
ps->top--;
}
STDataType StackTop(Stack* ps)//获取栈顶元素
{
assert(ps);//断言判断是否为空
assert(ps->top > 0);//栈空
return ps->a[ps->top - 1];
}
//获取栈中有效的元素个数
int StackSize(Stack* ps)
{
assert(ps);//断言判断是否为空
return ps->top; //0~(ps->top-1)
}
bool StackEmpty(Stack* ps) //判断栈是否为空,空则返回1,不为空则返回0
{
assert(ps);
return ps->top == 0;//代码简洁
}
#include"Stack.h"
void Stacktest()
{
Stack st;
StackInit(&st);
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
StackPush(&st, 4);
StackPush(&st, 5);
while (!StackEmpty(&st))
{
printf("%d\n", StackTop(&st));
StackPop(&st);
} //符合栈的性质,相对而言的后进先出(相对于栈里的数据)
StackDestory(&st);
}
int main()
{
Stacktest();
}
入栈顺序:12345,出栈顺序:54321
逻辑结构如下:
用一个头指针指向栈顶的链表,对应只提供头插和头删两个方法实现。模拟实现栈的先进后出。很简单就不在此赘述。
若一个对象部分地包含自己,或用自己给自己定义,则称这个对象是递归的。
若一个过程直接的或间接的调用自己,则称这个过程是递归过程。如函数的递归调用、数的遍历、斐波那契数列等
著名的迷宫求解问题和Hanoi塔问题都能用递归求解
递归问题-用分治法求解
分治法,对于一个较为复杂的问题,能够分解成几个相对简单的且解法相同或类似的子问题来求解。
分治求解递归问题算法的一般形式
通常,一个函数在运行期间调用另一个函数时,在运行被调用函数之前,系统需先完成3件事:
而从被调用函数返回调用函数之前,系统也应完成3件事
为了保证递归正确执行,系统会建立一个递归工作栈,记录整个递归函数运行期间使用的数据存储区。每一层递归所需信息构成工作记录,包括实参,所有的局部变量,以及上一层的返回地址,每进入一层递归就产生一个新的工作记录压入栈顶,每退出一层递归,就从栈顶弹出一个工作记录,则当前执行层的工作记录必是递归工作栈栈顶的工作记录,称这个记录为活动记录。
递归阶乘n!的过程
int Fact(int x) { if(x==0)return 1; else return x*Fact(x-1); }
- 1
- 2
- 3
- 4
- 5
20. 有效的括号 - 力扣(Leetcode)这是相应的括号匹配问题力扣题
括号匹配就是“( )”“{ }”“[ ]”这些括号满足一一对应关系。可以嵌套定义,如:({()})。而
括号匹配问题就是:
给定一个只包括
'('
,')'
,'{'
,'}'
,'['
,']'
的字符串s
,判断字符串是否有效。有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号
设计思路:
这里使用C++来实现算法
class Solution {
public:
bool isValid(string s) {
stack<char>storage;
for (int i = 0; i < s.size(); i++)
{
if (s[i] == '(' || s[i] == '[' || s[i] == '{')
storage.push(s[i]);
else
{
if (s[i] == ')')
{
if (!storage.empty() && storage.top() == '(')storage.pop();
else return false;
}
else if (s[i] == '}')
{
if (!storage.empty() && storage.top() == '{')storage.pop();
else return false;
}
else if (s[i] == ']')
{
if (!storage.empty() && storage.top() == '[')storage.pop();
else return false;
}
}
}
return storage.empty();
}
};
所谓表达式,就是由变量、常量以及运算符组合而成的式子。其中,常用的运算符无非 !(阶乘运算符)、^(指数 运算符)、+、-、*、/ 、( ) 这几种,比如
3!+4*2/(1-5)^2
就是一个表达式。 那么,如何用栈结构求一个表达式的值呢。用后缀表达式法。
什么是后缀表达式?就是将表达式中所有运算符放在它的运算项后面
这里以
3!+4*2/(1-5)^2
为例:6+8/16
!
运算符对应的运算项为3
,转换后得到3!
+
运算符对应的运算项是3!
和4*2/(1-5)^2
,转换之后得到:3! 4*2/(1-5)^2 +
*
运算符对应的运算项是 4 和 2,转换之后得到4 2 *
/
运算符对应的运算项是4 2 *
和(1-5)^2
,转换后得到4 2 * (1-5)^2 /
-
运算符对应的运算项是 1 和 5,转换后得到1 5 -
^
运算符对应的运算项是1 5 -
和2
,转换后得到1 5 - 2 ^
。整合之后,整个普通表达式就转换成了
3 ! 4 2 * 1 5 - 2 ^ / +
这就是其对应的后缀表达式。
得到的后缀表达式,如何计算?首先创建一个栈。接着按照从左到右的顺序扫描后缀表达式,遇到运算项就入栈。遇到n元运算符,就出栈顶元素n个计算并将计算结果压回栈中。代码实现应注意的是:从栈出来的先后顺序,对应原来运算的哪一个运算项!
如:遇到阶乘(一元运算符),出栈顶计算。遇到乘法(二元运算符),出栈顶两元素计算并压回栈中。循环上述操作,最后栈中最后一个元素,即为此运算项即为整个表达式的值。
double calculate(char* out)
{
int index = 0;
stack<double>result;
while (out[index] != '\0')
{
char c = out[index];
switch (c)
{
case '!':
{
double i = result.top();
result.pop();
double end = 1;
while (i != 1) end *= i-- ;
result.push(end);
break;
}
case '*':
{
double right = result.top();
result.pop();
double left = result.top();
result.pop();
result.push(left * right);
break;
}
case '/':
{
double right = result.top();//被除数
result.pop();
double left = result.top();//除数
result.pop();
if (!right)
{
cout << "分母为零,错误" << endl;
exit(-1);
}
else
{
result.push(left / right);
break;
}
}
case '+':
{
double right = result.top();
result.pop();
double left = result.top();
result.pop();
result.push(left + right);
break;
}
case '-':
{
double right = result.top();//被减数
result.pop();
double left = result.top();//减数
result.pop();
result.push(left - right);
break;
}
case '^':
{
double exp = result.top();//指数
result.pop();
double base = result.top();//底数
result.pop();
if (!base)
{
cout << "底数为零" << endl;
exit(-1);
}
else
{
double end = 1;
for (int i = 0; i < exp; i++)
{
end *= base;
}
result.push(end);
break;
}
}
default:
{
result.push(c - 48);
}
}
index++;
}
return result.top();
}
上面讲过了如何求解表达式:表达式–后缀表达式–利用栈运算,那么如何获得后缀表达式?
首先看规则:
普通表达式转换为后缀表达式需要用到一个空栈(假设名为Optr)和一个空数组(数组名)
依照以上处理过程,直到将普通表达式遍历完毕,如果 Optr 栈中仍有运算符,依次将它们出栈并添加到 postex数组尾部。最终,postexp 数组内存储的表达式就是转换后的后缀表达式。
总结一句:运算项直接放数组中,运算符压入栈中,只有遇到比栈顶运算符优先级低的或栈空才出栈放入数组中。括号单独考虑。
如此一来运算符优先级高的就紧随运算项,先运算。运算符优先级低的往往在后缀表达式最右边。
下面是表达式3 ! 4 2 * 1 5 - 2 ^ / +
转换为后缀表达式的过程:(按序号看)
代码实现
void transform(char* expr, char* out)
{
stack<char>temp;
int index = 0;//作为输出数组的下标
int i = 0;//表达式的下标
while(expr[i]!='\0')
{
char c = expr[i];
switch (c)
{
case '!':
{
while (!temp.empty())
{
if (temp.top() == '!')
{
char ch = temp.top();
temp.pop();
out[index++] = ch;
}
else//说明优先级变高了,跳出循环直接入栈
{
break;
}
}
temp.push('!');
i++;
break;
}case '*':
{
while (!temp.empty())
{
if (temp.top() == '!' || temp.top() == '^' || temp.top() == '*' || temp.top() == '/')
{
char ch = temp.top();
temp.pop();
out[index++] = ch;
}
else//说明优先级变高了,跳出循环直接入栈
{
break;
}
}
temp.push('*');
i++;
break;
}case '/':
{
while (!temp.empty())
{
if (temp.top() == '!' || temp.top() == '^' || temp.top() == '*' || temp.top() == '/')
{
char ch = temp.top();
temp.pop();
out[index++] = ch;
}
else//说明优先级变高了,跳出循环直接入栈
{
break;
}
}
temp.push('/');
i++;
break;
}case '+':
{
while (!temp.empty())
{
if (temp.top() == '!' || temp.top() == '^' || temp.top() == '*'
|| temp.top() == '/' || temp.top() == '+' || temp.top() == '-')
{
char ch = temp.top();
temp.pop();
out[index++] = ch;
}
else//说明优先级变高了,跳出循环直接入栈
{
break;
}
}
temp.push('+');
i++;
break;
}case '-':
{
while (!temp.empty())
{
if (temp.top() == '!' || temp.top() == '^' || temp.top() == '*'
|| temp.top() == '/' || temp.top() == '+' || temp.top() == '-')
{
char ch = temp.top();
temp.pop();
out[index++] = ch;
}
else//说明优先级变高了,跳出循环直接入栈
{
break;
}
}
temp.push('-');
i++;
break;
}case '(':
{
temp.push('(');
i++;
break;
}case ')':
{
while (temp.top() != '(')
{
char ch = temp.top();
temp.pop();
out[index++] = ch;
}
temp.pop();//此时栈顶为(
i++;
break;
}case '^':
{
while (!temp.empty())
{
if (temp.top() == '!'||temp.top()=='^')
{
char ch = temp.top();
temp.pop();
out[index++] = ch;
}
else//说明优先级变高了,跳出循环直接入栈
{
break;
}
}
temp.push('^');
i++;
break;
}default :
{
out[index++] = c;
i++;
break;
}
}
}
//此时将栈中多有的数据逐一出栈
while (!temp.empty())
{
out[index++] = temp.top();
temp.pop();
}
out[index] = '\0';//最后给后缀表达式加上尾\0
}
int main() {
char* s = (char*)malloc(15 * sizeof(char));
char* out = (char*)malloc(13 * sizeof(char));
char temp[] = "3!+4*2/(1-5)^2";
//cout << strlen(s);
for (int i = 0; i < 15; i++)
{
s[i] = temp[i];
}
transform(s,out);
cout << "后缀表达式为:"<< out << endl;
cout <<"表达式运算结果为:"<< calculate(out);
}
队列:只允许一端插入数据,另一端删除数据作的线性结构。队列中数据遵守先进先出FIFO(First In First Out)原则。
按存储结构,队列分为顺序队和链队。队列的顺序存储–顺序队,队列的链式存储–链队。
队列用于解决先进先出的问题,如脱机打印输出、多用户系统中多个用户排成队,分时地循环使用CPU和主存、网络电文传输,按到达时间先后顺序依次进行。
顺序队相比于链队,顺序队每次出数据都需要向前移动整个队列数据,相对浪费时间。相比之下链队更加优秀。用两个指针分别指向队列的队头和队尾(初始状态如图a)入队出队的过程其实就是无头单向链表的实现(头插和尾删),具体请看:(99条消息) C/C++【数据结构笔记】(顺序表&链表)_Answer-2296的博客-CSDN博客)
入队
出队
队列头文件
#pragma once
#include
#include
#include
#include
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QueueNode;//无头单向链表的结点
typedef struct Queue
{
//用这个结构体来代表队列
QueueNode* head;//指向队列的头
QueueNode* tail;//指向队列的尾
}Queue;
//队列初始化
void QueueInit(Queue* qp);
//销毁队列
void QueueDestory(Queue* qp);
//入队(队尾入队)
void QueuePush(Queue* qp, QDataType x);
//出队(队头出队)
void QueuePop(Queue* qp);
// 获取队列头部元素
QDataType QueueFront(Queue* qp);
// 获取队列队尾元素
QDataType QueueBack(Queue* qp);
// 获取队列中有效元素个数
int QueueSize(Queue* qp);
// 检测队列是否为空,如果为空返回1,如果非空返回0
bool QueueEmpty(Queue* qp);
#pragma once
#include"Queue.h"
void QueueInit(Queue* qp)//队列初始化
{
assert(qp);
qp->head = qp->tail = NULL;//初始化都置成空!
}
void QueueDestory(Queue* qp)//销毁队列
{
assert(qp);
QueueNode* cur = qp->head;
while (cur)//最后一个结点的next值位空
{
QueueNode* next = cur->next;
free(cur);
cur = next;
}//逐个销毁
qp->head = qp->tail = NULL;//没用的指针都置成空,避免野指针
}
void QueuePush(Queue* qp, QDataType x)//入队(队尾入队)
{
assert(qp);
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
if (!newnode)
{
printf("malloc failed\n");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;//构建好数据结点
if (qp->tail == NULL)
{
qp->head = qp->tail = newnode;
//如果是空队列,队尾队头都要变
}
else
{
qp->tail->next = newnode;
qp->tail = newnode;//重新改变队尾
}
}
void QueuePop(Queue* qp)//出队(队头出队)
{
assert(qp);
assert(qp->head);//队列非空才能出队
if (qp->head->next == NULL)
{
free(qp->head);
qp->head = qp->tail = NULL;
}//队列只有一个结点,则置空队列(qp->tail=NULL)如果没单独考虑,qp->tail会成为野指针。
else
{
QueueNode* next = qp->head->next;
free(qp->head);
qp->head = next;
}
}
QDataType QueueFront(Queue* qp)// 获取队列头部元素
{
assert(qp);
assert(qp->head);
return qp->head->data;
}
QDataType QueueBack(Queue* qp)// 获取队列队尾元素
{
assert(qp);
assert(qp->head);
return qp->tail->data;
}
int QueueSize(Queue* qp)// 获取队列中有效元素个数
{
assert(qp);
int size = 0;
QueueNode* cur = qp->head;
while (cur)
{
size++;
cur = cur->next;
}
return size;
}
bool QueueEmpty(Queue* qp)// 检测队列是否为空,如果为空返回1,如果非空返回0
{
assert(qp);
return qp->head == NULL;//简洁!
}
#include"Queue.h"
void Queuetest()
{
Queue q;
QueueInit(&q);
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
QueuePush(&q, 4);
QueuePush(&q, 5);
while (!QueueEmpty(&q))
{
printf("%d\n",QueueFront(&q));
QueuePop(&q);//输出完删除,模拟栈出过程
}
}
int main()
{
Queuetest();
}
队列入队顺序12345,出队顺序12345
用顺序表头删和尾插分别模拟顺序队的出队和入队,相比于链队,顺序队每次出队都需要将顺序队中所有的元素前移,浪费时间。
而队列的顺序存储也有静态顺序存储和动态顺序存储之分
实际中我们有时还会使用一种队列叫循环队列。如操作系统课程讲解生产者消费者模型时可以就会使用循环队列。环形队列可以使用数组,实现,也可以使用链表实现。
- 内存上并没有环形的结构,因此环形队列实际上是数组的线性空间来实现的。
- 当数据到了尾部该如何处理呢?它将转回到原来位置进行处理,通过取模操作来实现
这里用数组实现的环形队列为例。
Q.front表示队头,初始值为0。
Q.rear表示队列最后一个元素的后一个位置(预留一个空间,作为约定,方便判断队列是否为空)。
maxSize: 数组的最大容量
我们用求模运算来实现循环。一般来说队头位置固定为0,每次出队需要将队列中所有的元素前移,十分浪费空间。对此我们不固定队头,出队,只需要前移队头!
//插入元素
Q.base[Q.rear]=x;
Q.rear=(Q.rear+1)%maxSize;
//删除元素(出队)
x=Q.base[s.front];//是否删除前返回数据因人而异
Q.front=(Q.front+1)%maxSize;
队列的两种状态
队空:front == rear
队满:(rear+1)%maxSize == front//队头前始终空一个位置
环形队列实现代码
#include
using namespace std;
#define MAXSIZE 11
typedef int QueueDataType;
struct Queue{
QueueDataType* base;
int front;
int rear;
};
//初始化环形队列
void QueueInit(Queue& Q)
{
Q.base = new QueueDataType[MAXSIZE];
//C语言写法:Q.base=(QueueDataType*)malloc(sizeof(QueueDataType)*MAXSIZE);
if (!Q.base)exit(-1);
Q.front = Q.rear = 0;
}
//入队
void QueuePush(Queue& Q, QueueDataType data)
{
if ((Q.rear + 1) % MAXSIZE == Q.front)
{
cout << "入队失败,队列已满" << endl;
exit(-1);
}
Q.base[Q.rear] = data;
Q.rear = (Q.rear + 1) % MAXSIZE;
}
//出队
void QueuePop(Queue& Q)
{
if (Q.rear == Q.front)
{
cout << "对空,出队失败" << endl;
exit(-1);
}
Q.front = (Q.front + 1) % MAXSIZE;
}
//取队头元素
QueueDataType QueueTop(Queue Q)
{
if (Q.rear == Q.front)
{
cout << "队空,取队头失败" << endl;
exit(-1);
}
else
{
return Q.base[Q.front];
}
}
int main()
{
Queue q;
QueueInit(q);
for (int i = 0; i < 10; i++)
{
QueuePush(q, i);
}
for (int i = 0; i < 10; i++)
{
int temp = QueueTop(q);
QueuePop(q);
cout << temp<<" ";
}
}
栈中的数据元素遵守后进先出***LIFO(Last In First Out)***的原则。并且是相对而言,多为后进先出是指,栈现有的数据是后进先出,所以可以出现先进栈的数据比后进栈的数据先出(比如数据进栈立马出栈,这时它就比后面数据先出来,所以说栈的后进先出是相对而言的)
队列中数据遵守先进先出FIFO(First In First Out)原则。并且是绝对而言,只要是先进队列的一定是先出来的。
分清数据在哪里发生变化!Stack中的指针,top,capacity在哪里变化,怎么变?对于指针申请空间,我们用函数包装好,内部做判断,capacity和和空间绑定在一块,所以capacity也是在函数内部来变化!top作为待插入数据的位置坐标,是在入栈出栈的时候做出改变。
在出队函数实现时尤为需注意野指针的诞生,因为一般队列足够长时,出队只需要操作对头,并释放对应数据。但如果队列只有一个数据,这时释放完空间,却没有对队尾指针修改,此时尾指针指向的是一个未知空间!
(224条消息) 【数据结构拓展笔记】_栈和队列的相互转换_Answer-2296的博客-CSDN博客
由于篇幅过长,转战。