目录
1.基础回顾
回归本质,数组和链表最基础的就是实现他们的增删查改,那么我们就来说下他们各自的实现形式吧。
数组顺序表:确保数据的连续性,在修改的时候需要往前移动数据,若为表尾则不需要。
链表:通过节点链接和断开进行增删,查改直接遍历。
栈
1.栈的定义
栈(Stack):是只允许在一端进行插入或删除的线性表。首先栈是一种线性表,但限定这种线性表只能在某一端进行插入和删除操作。
栈顶(Top):线性表允许进行插入删除的那一端。
栈底(Bottom):固定的,不允许进行插入和删除的另一端。
空栈:不含任何元素的空表。
栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构
typedef int Stdatatype; //typedef栈数据,方便修改
typedef struct stack
{
Stdatatype* arr;
int top;
int capacity;
}ST;
(1)顺序表和顺序栈的区别
解析: 顺序表可以在任意位置对数据进行操作,我们只需要知道所要数据的下标即可,而顺序栈是利用顺序表构建的栈,依靠栈顶top指针对栈顶元素进行操作,实现后进先出。我们可以设定固定大小的栈,或者动态增长的栈。栈一定要有栈顶指针top。
base == top 是栈空标志,top 指示真正的栈顶元素之上的下标地址。
栈满时的处理方法:
1、报错,返回操作系统。
2、分配更大的空间,作为栈的存储空间,将原栈的内容移入新栈。
2、栈的常用基本方式(函数实现于下文,&是引用的意思)
(1)StackInit(&S):初始化一个空栈S。
(2)StackEmpty(S):判断一个栈是否为空,若栈为空则返回true,否则返回false。
(3)StackPush(&S, x):进栈(栈的插入操作),若栈S未满,则将x加入使之成为新栈顶。
(4)StackPop(&S):栈为非空,出栈(栈的删除操作)。
(5)StackTop(S, &x):读栈顶元素,若栈S非空,则用x返回栈顶元素。
(6)StackDestroy(&S):栈销毁,并释放S占用的存储空间。
栈顶有两种表达方式:
区别:当栈为空的时候栈顶的指向不同,指向0或者-1,同时进栈的方式也不同,栈顶top为指向顶部元素时是top先加一位再赋值,栈顶top指向顶部的下一位时是赋值后top再后移一位。
各有各的优点,按个人习惯为主或具体问题方便优先。
3.栈函数基本操作
(1)栈初始化(动态开辟空间)
//初始化
void StackInit(ST* ps)
{
assert(ps);//判断是否为空,ps为空无法进行
Stdatatype* temp = (Stdatatype*)malloc(0 * sizeof(Stdatatype));
if (temp == NULL)
{
printf("Init:fail\n");
return;
}
ps->arr = temp;
ps->capacity = 0;
ps->top = 0;//top定义为0的话那么就对应输入值的下一位,即先输入再++,
//若是-1为初始化,就是先++再输入
}
(2)栈判空
bool StackEmpty(ST* ps)
{
assert(ps);
//直接返回
return ps->top == 0;
}
(3)取栈顶元素
Stdatatype Stacktop(ST* ps)
{
assert(ps);
assert(ps->top > 0);
return ps->arr[ps->top-1];//top指向最后一个元素的下一个,故-1
}
(4)入栈(动态增长版本)
void StackPush(ST* ps, Stdatatype x)
{
assert(ps);
if (ps->capacity == ps->top)
{
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
Stdatatype* temp = (Stdatatype*)realloc(ps->arr,newcapacity * sizeof(Stdatatype));
if (!temp)
{
printf("realloc:fail\n");
exit(-1);//错误就无法进行了,直接退出
}
ps->capacity = newcapacity;
ps->arr = temp;
}
ps->arr[ps->top] = x;
ps->top++;
}
(5)出栈
void StackPop(ST* ps)
{
assert(ps);
assert(ps->top > 0);//等于0就说明没有数据了。不能删了
ps->top--;
}
(6)栈销毁
//栈销毁
void StackDestroy(ST* ps)
{
assert(ps);
free(ps->arr);
ps->capacity = 0;
ps->top = 0;
//free(ps);//一般在内部释放外部的栈,不确定栈是否是malloc的
//外部创建外部释放
}
4.栈实操题目
(1)括号匹配问题
问题:给定一个只包括
'('
,')'
,'{'
,'}'
,'['
,']'
的字符串s
,判断字符串是否有效。1.输入:s = "()" 输出:true 2.输入:s = "(]" 输出:false
思想:利用栈后进先出的特点,依次遍历字符串,遇到左括号进栈,遇到右括号用其与栈顶元素对比,若左右括号匹配,就pop掉栈顶左括号,接着匹配下一对括号。
代码实现:
bool isValid(char * s){
assert(s);
ST ps = {0};
StackInit(&ps);//初始化栈
while(*s)
{
if((*s =='(')||(*s=='{')||(*s=='['))
{
StackPush(&ps,*s);
s++;
}
else
{
if(StackEmpty(&ps)) //如果再这为空说明该开始是右括号或者为空
return false;
char top = Stacktop(&ps);//*s只包含括号,所以只需要考虑括号的比就行
StackPop(&ps);
if((*s==']'&&top!='[')||(*s=='}'&&top!='{')||(*s==')'&&top!='('))
return false;
else
{
s++;
}
}
}
if(StackEmpty(&ps))//此时栈为空则正确,不为空可能s只含有左括号
return true;
return false;
}
(2)中缀表达式实现
问题:用栈实现中缀表达式的识别运算 (中缀表达式:运算符在数据中间,1+2*3-6这种。
输入:1+2*3-6 输出:1
输入:9+4*(2+3) 输出:33
思想:创建一个数据栈OPND,一个符号栈OPTR,设OPTR栈头默认为 ’‘#“ ,字符串尾也添加 ’‘#“ ,用以表示结束(当两个#相碰撞时),接着遍历字符串为数据时,push数据进入OPND栈中,当遍历字符串为符号时,与符号栈OPTR栈顶元素比较,优先级比栈顶大就进栈。优先级比栈顶小就拿出数据栈中前两个数据与符号栈OPTR栈顶的符号进行运算。(下面有优先级对比解释)运算结果压栈到数据栈中。然后继续遍历字符串直到尾部#。
总结符号优先级(四则运算)
1:符号栈OPTR栈顶的运算符
2:表达式中当前的cur指向的运算符
1 > 2时,说明这时候栈顶优先级符号比较大,我们要先算数据。
1 > 2时,说明栈顶符号比较小,可以之后算,先算乘除后算加减。而且 1是左括号( 时,他的优先级最小,先把后面的压进来先, 1是右括号时要优先算括号中的数据,故先要用之前的符号也就是符号栈OPTR栈顶符号与数据栈中前两个运算。
1 = 2时,说明遇到了一对()或者一对#,一对括号就说明括号中间的运算结束了,把这两括号去掉即可,即pop掉符号栈OPTR栈顶的(,与cur指向下一位。一对#即可结束。
具体实现图解:
实现代码:
char infix()//两个# 代表结束
{
ST OPTR,OPND;
StackInit(&OPTR);
StackInit(&OPND);
char a, b,theta;
StackPush(&OPTR, '#');
char c = getchar();
while (c != '#' || StackTop(&OPTR) != '#')//两个同时为# 才是结束,这两个挨在一起
{
if (isdigit(c))
{
StackPush(&OPND, c);
c = getchar();
}
else if (!in(c))
{
printf("出现非法字符\n");
exit(-1);
}
else
{
switch (judge(StackTop(&OPTR),c))
{
case '>':
//OPTR栈顶符号大,说明前面的数要先算
a = StackPop(&OPND);
b = StackPop(&OPND);
theta = StackPop(&OPTR);
StackPush(&OPND, calculate(b, theta, a));
break; //这时候c还没有用上,不用再取
case '=':
StackPop(&OPTR);
c = getchar();
break;
case '<':
//后者大,就把符号位压进来
StackPush(&OPTR, c);
c = getchar();
break;
}
}
}//while
return StackTop(&OPND);
}
判断是否非法符号函数:
bool in(char c)
{
char a[] = { '+','-','*','/','(',')','#' ,'\0'};
char* pa = a;
while (*pa != '\0')
{
if (c == *pa)
return true;
else
pa++;
}
return false;
}
比较符号优先级函数:
char judge(char a, char b)
{
if ((a == '#' && b == '#') || a == '(' && b == ')')
return '=';
else if (a == '#' || a == '(' || b == '(' ||
((a == '+' || a == '-') && (b == '*' || b == '/'))) //a是开括号就都要压栈
return '<';
else
return '>';
}
运算函数:
char calculate(char a, char theta, char b)
{
char temp = 0;
switch (theta)
{
case '+':
temp = (a - '0') + (b - '0') + 48;
break;
case '*':
temp = (a - '0') * (b - '0') + 48;
break;
case '/':
temp = (a - '0') / (b - '0') + 48;
break;
case '-':
temp = (a - '0') - (b - '0') + 48;
break;
}
return temp;
}
队列
1.队列的定义
队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。
队头(Front):允许删除的一端,又称队首。
队尾(Rear):允许插入的一端。
空队列:不包含任何元素的空表。
typedef int Qdatatype; //typedef队列数据元素,方便之后更改
typedef struct QNode //链式结构,先定义节点
{
Qdatatype val;
struct QNode* next;
}QNode;
typedef struct Queue //定义队列结构,首尾指针
{
QNode* head; //fornt,前
QNode* tail; //rear,后
}Queue;
2、队列的常用基本方式(函数实现于下文,&是引用的意思)
QueueInit(&Queue):初始化队列,构造一个空队列Queue。
QueueEmpty(&Queue):判队列空,若队列Queue为空返回true,否则返回false。
QueuePush(&Queue,& x):入队,若队列Queue未满,将x加入,使之成为新的队尾。
QueuePop(&Queue,& x):出队,若队列Queue非空,删除队头元素,并用x返回。
QueueFront(&Queue):读队头元素,若队列Queue非空,则将队头元素赋值给x。QueueDestroy(&Queue):销毁所创造的队列
图解:
队列可以用数组或者链表实现:但是都需要有头指针 front 和尾指针 rear 。依次进入,从头fornt开始出数据,链式队列就不需要考虑如上图的空间问题,还得导一下回到空余的空间(这便是下文的循环队列)。故队列优先考虑链式结构,栈优先考虑顺序结构,之后有二者的比较。
3.队列函数基本操作
(1)队列初始化
void QueueInit(Queue* ps)
{
ps->head = NULL;
ps->tail = ps->head;
}
(2)队列判空
bool QueueEmpty(Queue* ps)
{
assert(ps);
return ps->head == NULL;//head为空就是true
}
(3)入队列
void QueuePush(Queue* ps, Qdatatype x)
{
assert(ps);//判断的是这个结构体是否为空
QNode* newNode = (QNode*)malloc(sizeof(QNode));
if (!newNode)
{
printf("push:fail\n");
return;
}
newNode->val = x;
newNode->next = NULL;
if (!ps->head)//head为空就直接赋值
{
ps->head = newNode;
ps->tail = ps->head;
}
else //不为空就链接
{
ps->tail->next = newNode;
ps->tail = ps->tail->next;
}
}
(4)出队列
void QueuePop(Queue* ps)
{
assert(ps);
assert(ps->head && ps->tail);//不能无元素
QNode* next = ps->head->next;
Qdatatype temp = ps->head->val;
free(ps->head);
ps->head = next;
if (!ps->head)//如果head为空,那么就是到结尾了,tail也要置空
ps->tail = NULL;
}
(5)取队列首元素
Qdatatype QueueFront(Queue* ps)
{
assert(ps);
assert(ps->head);
return ps->head->val;
}
(6)队列销毁
void QueueDestroy(Queue* ps)
{
assert(ps);
QNode* cur = ps->head;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
ps->head = ps->tail = NULL;
}
队列实操题目
力扣622:设计循环队列
思想:循环利用之前出队列的空间,需要考虑的是如何表示空队列和满队列,若是整个队列放慢,我们就无法分别到底是队列空还是满。如图:
建立多一个空间,如N个,那么最多存储N-1个数据,那么当front(head)指针等于rear(tial)指针时为空,rear(tail)指针的下一位是front(head)指针时候为满队列
代码实现:(数组法)
//数组
typedef struct {
int* arr; //存放数据的数组
int head;
int tail;
int k;//记录数组总长度,但是实际大小要比k大1
} MyCircularQueue;
//循环队列初始化
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* ps = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
if (ps == NULL)
return NULL;
ps->arr = (int*)malloc((k + 1) * sizeof(int));
ps->head = ps->tail = 0;
ps->k = k;
return ps;
}
//循环队列判满
bool myCircularQueueIsFull(MyCircularQueue* obj) {
if (obj->tail == obj->k)
return obj->head == 0;
else
return obj->head == obj->tail + 1;
}
//循环队列判空
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->head == obj->tail;
}
//push元素
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
//入队
if (myCircularQueueIsFull(obj))
return false; //队列满了就无法入队了
//没有满直接添加
obj->arr[obj->tail] = value;
if (obj->tail != obj->k)
{
obj->tail++;
}
else
{
obj->tail = 0;
}
return true;
}
//pop元素
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
//出队列
if (myCircularQueueIsEmpty(obj))
return false;
//不是空就出数据
if (obj->head != obj->k)
{
obj->head++;
}
else
obj->head = 0;
return true;
}
//返回队列首元素
int myCircularQueueFront(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj))
return -1;
return obj->arr[obj->head];
}
//返回队列尾元素
int myCircularQueueRear(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj))
return -1;
if (obj->tail == 0)
{
return obj->arr[obj->k];
}
else
return obj->arr[obj->tail - 1];
}
//销毁队列
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->arr);
free(obj);
obj == NULL;
}
数组循环队列指针一旦到了顶部就需要循环到起始0位置,这样方可达到循环的目的。
栈和队列的结构对比(数组和链表)
数组:在内存上给出了连续的空间.
链表:内存地址上可以是不连续的,是通过上一个节点来找到下一个节点的。
链表和数组的本质差异
1 、 在访问方式上 :数组可以随机访问其中的元素 ,链表则必须是顺序访问,不能随机访问
2 、 空间的使用上 :链表可以随意扩大数组则不能
(1)数组与链表:
1.数组内的数据可随机访问.但链表不具备随机访问性..数组在内存里是连续的空间.可以通过下标随机访问
2.数组高速缓存命中率更高,这里涉及计组内存问题,不展开讲。这是由于数组是连续的空间。链表是一块一块内存中的空间建联的,访问起来速度相对于没有数组那么快。
3.内存空间创建问题,数组是一次性开辟的空间,是有限定的(除了动态开辟,但是空间利用也没有链表好),链表是创建一个利用一个,链表节点会附加上一块或两块下一个节点的信息.但是数组在建立时就固定了.所以也有可能会因为建立的数组过大或不足引起内存上的问题.。
4.插入,删除,更改数据问题,数组在尾部插入数据时比较快的,这一点链表也一样,但是在头部插入数据链表较快,因为数组需要一个个移动。更改数据时候,数组可以随机直接访问,它更改速度比较快,而链表需要一个个从头开始遍历才能找到要更改的数据,删除分任意位置删除和尾删头删,数组在除了尾部的删除都是比较麻烦,因为要保持不缺空间,要一个个往回填。链表删除比较简单,但是需要获得目标节点的前一个节点才行。
(2)栈和队列使用的结构
栈和队列都可以使用顺序表(数组)和链表,但是具体哪种比较方便还需要仔细对比,但是并不是说另外一种不行,只是相对来说。
栈:栈一般使用顺序表,首先栈是后进先出,数组肯定是从前到后插入数据的,因为不确定具体数据量,所以是前面进后面出,而以数组的优势来说就是方便尾删尾插,而链式栈的尾删需要更前一位的节点才可以删,这样就需要遍历才能找到,或者用多一个指针一直指向尾节点的前一个节点,但这样就操作麻烦了一点点,相对没有顺序栈那么优。
队列:队列一般是使用链表,队列是先进先出,对于数组来说就需要尾插头删,那么每次删除都需要往前面移动元素来保持顺序结构。而链式结构不用,他也是尾插头删,尾插和数组一样方便时间复杂度为O(1),但是头删没有数组麻烦,链表只需要移动一下头节点即可。故链式队列相对较好。