文章目录
目录
前言
提示:这里可以添加本文要记录的大概内容:
例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。
提示:以下是本篇文章正文内容,下面案例可供参考
一、思维导图
二、栈
1.栈的基本概念
(1)栈的定义
栈是只允许在表的一端进行插入和删除操作的线性表,
(2)栈的特点
卡特兰数:n个不同的元素进栈时,出栈元素不同排列的个数为
2.栈的基本操作
这些基本操作可直接在答案中调用
3.栈的存储结构
(1)顺序栈
顺序栈是栈的重点,而链栈基本上与链表保持一致,所以这里重点讨论顺序栈;实际应用中更多的也是顺序栈。
顺序栈的存储结构与顺序表一样,一般也有两种表示方式:静态分配,动态分配;
静态分配
(参考王道版本)
//顺序栈的存储结构——静态分配
#define maxsize 100
typedef struct{
int Data[maxsize];//开一个长度为100的数组,用作栈
int top //定义top指针;
}SqStack;
初学者可能会在这里产生一点疑惑,为什么在这里定义一个int型的变量可以将其称之为指针呢?这里的指针并不是真正意义上的指针,我们知道,真正的指针存放的是一个地址,在这个栈(数组)中其实地址就是0,1,2,3,4,5......0号地址第一个元素,1号地址存第二个元素......,所以用top变量用来存放栈顶元素在数组当中的哪个位置。
此外,用静态分配的方式来表示一个栈时,我们只用了一个栈顶指针,没有用栈底指针,栈底指针是固定在栈底的,所以有没有都一样。我们需要重点关注的时栈顶指针的指向,在这里,栈顶指针一般是两种指向方式,两种指向方式的不同会导致一些具体操作的不同,为方便表示,我用手写的方式呈现:
下面基本操作仅以s.top=-1表示,s.top=0类比,很简单的
初始化
void InitSqStack(SqStack &S){
s.top=-1;
}
判断空
bool SqStackEmpty(SqStack &S){
if(s.top==-1){
return true;
}
else{
return false;
}
}
入栈
//入栈
bool Push(Sqstack &S,int x){
//首先判断空是否满
if(s.top==maxsize-1){
return false;
}
s.Data[++S.top]=x;
/*
s.Data[++S.top]=x;
这一行代码就是
s.top=s.top+1;
s Data[s.top]=x;
*/
return true;
}
出栈
//出栈
bool Pop(Sqstack &s,int &x){
//首先需要判断栈是否为空
if(s.top==-1){
return false;
}
x=s.Data[s.top--];
/*
x=s.Data[s.top--];
这行代码等效于
x=s.Data[s.top];
s.top=s.top-1;
*/
return true
}
读取栈顶元素
//读栈顶元素
int GetTop(SqStack S,int &x){
//首先需要判空
if(s.top==-1){
return false;
}
x=s.Data[s.top];
//这里将栈顶元素,赋值给变量x,因为这里参数传来的是&x(可以理解为x的地址)
//在这个函数内部修改x的值(也就是将栈顶元素赋值给x)在函数外,x的值同样被修改
//所以直接返回bool型即可,此时x的值已经被“返回”了;
return true;
}
动态分配
所谓的顺序栈的动态分配就是利用malloc函数在内存中动态的分配内存空间用来存放数据。下面我说明一下这个所谓的动态分配,首先看malloc函数,malloc函数的作用就是在内存中开辟一段地址连续的空间(更详细的内容请看c语言库函数)然后在这段地址连续的空间内进行操作,当空间存满时,就会动态分配,这时候一般使用realloc函数比如(int *)realloc(sizeof(int )*n),(n一定是大于原来malloc中的长度)此时realloc函数会在内存中寻找一片更大的地址连续的空间,然后将原来的数据复制到这一段更大的空间中。
存储结构
//顺序栈——动态分配
//存储结构
typedef struct{
int *top;
int *base;
int stackszie;
}SqStack;
与静态分配不同,这里定义了栈底指针。也就是动态分配的原因使得这个栈底指针需要定义
初始化
//初始化
bool InitStack(SqStack &s){
s.base=(int *)malloc(sizeof(int)*length);
if(s.base==NULL){
return false
}
s.top=s.base;
s.stacksize=length;
return true;
}
取栈顶元素
//取栈顶元素
bool InitStack(SqStack &s int &x){
if(s.top==s.base){
return false;
}
x=*(s.top-1);
return true;
}
入栈
//入栈
bool Push(SqStack &s int x){
if(s.top-s.base>=s.stacksize){
s.base=(int *)realloc(s.base,(s.stacksize+length1)*sizeof(int));
if(s.base==NULL){
return false;
}
s.top=s.base+stacksize;
s.stacksize+=length1;
}
*s.top++=e;
return true;
}
出栈
//出栈
bool Pop(SqStack &s int &x){
if(s.top==s.base){
return false;
}
x=*--s.top;
return true;
}
(2)链栈
链栈基本上与链表一致,只不过是限定在一段进行操作的链表,所以各种操作基本与链表一致
4.栈的应用
1.括号匹配
所谓括号匹配就是利用栈实现检查括号是否成对出现
规则:依次扫描所有字符,遇到左括号就入栈,遇到右括号就弹出栈顶元素,与当前括号检查是否匹配。
代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#define MaxSize 100 //定义栈中元素最大个数
typedef struct{
char data[MaxSize];
int top;
}SqStack;
//初始化栈
void InitStack(SqStack *S){
S->top = -1;
}
//判断栈是否为空
bool IsEmpty(SqStack S){
if(S.top == -1){
return true;
}
return false;
}
//新元素入栈
void Push(SqStack *S,char x){
if(S->top == MaxSize-1){
printf("栈已满"); //栈已满
return;
}
S->top += 1;
S->data[S->top] = x;
}
//栈顶元素出栈,用x返回
void Pop(SqStack *S,char *x){
if(S->top == -1){
printf("栈已满");
return;
}
*x = S->data[S->top];
S->top -= 1;
}
//匹配算法
bool bracketCheck(char str[],int length){
SqStack S;
InitStack(&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); //栈顶元素出栈
if(str[i]==')'&&topElem!='('){
return false;
}
if(str[i]=='}'&&topElem!='{'){
return false;
}
if(str[i]==']'&&topElem!='['){
return false;
}
}
}
return IsEmpty(S);
}
int main(){
char s[MaxSize];
printf("请输入需要判断的括号序列:\n");
scanf("%s",s);
int len = strlen(s);
if(bracketCheck(s,len)){
printf("匹配成功!");
}else{
printf("匹配失败!");
}
return 0;
}
2.表达式求值
这个部分内容不太好用语言讲述明白,建议观看王道的视频,这里就用思维导图的形式完整梳理下这个考点
3.栈与递归
发生递归时,计算机会在内存中开辟一段空间,这一段空间就是栈,栈的作用是保存每一层递归的信息,当满足递归终止条件时,递归调用栈会返回每一层的数据,即栈顶元素出栈,完成递归。
三、队列
1.队列的基本概念
(1)队列的定义
队列也是一种操作受限的线性表,只允许在表的一端进行插入,在表的另一端进行删除。操作特性时先进先出。
2.队列的顺序存储结构
(1)存储结构
队列的顺序存储是分配一块连续的存储单元来存放队列中的元素,设两个"指针",队头指针front,队尾指针rear。
//队列的顺序存储
#define maxsize 100
typedef struct{
int data[maxsize];
int front,rear;
}SqQueue;
初始时Q.front=Q.rear;
进队:队不满时,先入队,队尾指针+1;
出队:队不空时,先出队,队头指针+1;
此时需要思考一个问题,当队空时,判空条件是Q.front==Q.rear;但如果队满时,判满条件会是Q.rear==maxsize吗?显然队列的特性是从一端插入,从另一端删除,我们假设这样一个情景,在向队列插入元素的同时,从另一端删除一些元素,这样rear不停的+1,直到Q.rear==maxsize,而在这个过程中我们在另一端也删除了元素,这时如果以Q.rear==maxsize作为判满条件是不对的,此时队列并不满。所以我们引入了循环队列
(2)循环队列
循环队列的定义
循环队列即将队列想像成为一个环状空间(但实际上该怎么存还怎么存),从逻辑上将队列视为环
初始时:Q.front=Q.rear=0;
队首指针+1:Q.front=(Q.front+1)%maxsize;
队尾指针+1:Q.rear=(Q.rear+1)%maxsize;
判空和判满条件
三种方式处理(之所以会有不同的处理方式,其目的都是为了解决循环队列判空和判满条件可能会冲突的问题,也就是判空和判满条件是一样的)
第一种:牺牲一个存储单元
队空条件:Q.front==Q.rear;
队满条件:(Q.rear+1)%maxsize==Q.front;
上图即为牺牲一个存储单元来区分队空和队满,这里需要格外说明一点:这里队列中两个指针的指向,即front(队头指针)指向队头元素,rear(队尾指针)指向的是队尾元素的下一位置。
所以此时判空条件为Q.front==Q.rear;
判满条件为:(Q.rear+1)%maxsize==Q.front;
第二种:增加size数据成员
size用来表示队列中的元素个数,入队一个元素size+1,出队一个元素size-1;
此时判空:Q.size==0;
判满条件:Q.size==maxsize;
第三种:增加tag数据成员,
用tag=0,表示出队成功,tag=1表示入队成功
此时,判空条件Q.front==Q.rear&&Q.tag==0;
判满条件:Q.front==Q.rear&&Q.tag==1;
基本操作
1.初始化
void InitQueue(SqQueue &Q){
Q.front=Q.rear=0;
}
2.判断队列是否为空
bool IsEmpty(SqQueue Q){
if(Q.rear==Q.front){
return true;
}
else{
return false;
}
}
3.入队
bool EnQueue(SqQueue &Q,int x){
//入队首先需要判断队是否已满,这里采用牺牲一个存储单元的方式
if((Q.rear+1)%maxsize==Q.front){
return false;
}
Q.Data[Q.rear]=x;
Q.rear=(Q.rear+1)%maxsize;
return true;
}
4.出队
bool DeQueue(SqQueue &Q,int &x){
//首先需要判断队列是否为空
if(Q.front==Q.rear){
return false;
}
x=Q.Data[Q.front];
Q.front=(Q.front+1)%maxsize;
return true;
}
在入队操作中,rear指向的是当前元素的下一位置,学习一定是要学懂的,如果在这里假设rear指针指向的是当前元素,那么该如何操作呢
1.入队操作改变,尾指针先+1再入队
2.初始化时,front指针和rear指针指向的位置改变,front指针仍然指向0号元素,rear指针指向n-1的位置,如图:
3.判空判满条件发生变化(Q.rear+1)%maxszie==Q.front,判满条件Q.front==Q.rear,
3.队列的链式存储结构
(1)存储结构
队列的链式存储就是一个带有头指针和尾指针的单链表,头指针指向头结点,尾指针指向尾结点
//队列的链式存储
typedef struct Linknode{
int data;
struct Linknode *next;
}Linknode;
typedef struct{
Linknode *front,*rear;
}*LinkQueue;
一般来说,链队列都是带头结点的,不带头结点的链队列在操作上是比较麻烦的,所以下边的基本操作都以带头结点进行
(2)基本操作
初始化
//链队列的初始化
void InitQueue(LinkQueue &Q){
Q.front=Q.rear=(Linknode *)malloc(sizeof(linknode));
//生成一个头结点,队头指针和队尾指针均指向头结点;
Q.front->next=NULL;
//将头结点的指针域置为空
}
判队空
//判断链队列是否为空
bool IsEmpty(LinkQueue Q){
if(Q.front==Q.rear){
return true;
}
else{
return false;
}
}
入队
//入队
bool EnQueue(LinkQueue &Q,int x){
Linknode *s=(Linknode *)malloc(sizeof(Linknode));
s->data=x;
s->next=NULL;
Q.rear->next=s;
Q.rear=s;
}
出队
//出队
bool DeQueue(LinkQueue &Q,int &x){
if(Q.front==Q.rear){
return false;
}
Linknode *p=Q.front->next;
x=p->data;
Q.front->next=p->next;
if(Q.rear==p){ //当队列中除头结点外只有一个结点时,作特殊处理,删除后,尾指针指向头指针(头结点)
Q.rear=Q.front;
}
free(p);
return true;
}
4.双端队列
所谓双端队列就是队列的两端都可以进行入队和出队的操作。这部分内容多数以选择题的形式考察输出序列。
5.队列的的应用
队列的应用有两个方面:
(1)队列在层次遍历中的应用
队列在层次遍历中的应用主要应用于树
(2)队列在计算机系统中的应用
这部分内容主要应用在操作系统和CPU内,其实很简单的,在操作系统中会有一个缓冲区,这个缓冲区就是用队列实现的,主要解决双方速度不匹配的问题。
总结
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。