数据结构从头开始
最近在重新学习数据结构与算法,对于常见的数据结构用c实现了一下,记性不好,我估计可能等不到这本书复习完我就忘了,所以整理到这方便用ipad复习,同时也记录一下过程中的一些想法、思路和笔记。
第一章 线性表
第一章讲的逻辑结构是——线性表。这一章分为顺序表和链表两块,分别对应了线性表的顺序实现与链式实现。在严老师的数据结构教材中,采用了一下一些类C++的语法,所以如果要实现课本中的代码,要把代码文件的后缀改成”.cpp”,不然是无法通过编译的。
1.1顺序表
顺序表风两种:
静态分配和动态分配
静态分配:
就是开辟一片固定大小的内存空间用来存数据,这片空间的大小一旦分配就不能再改变了,所以一般我们采用
数组
实现顺序表的静态分配。
**动态分配:**也是开辟一片连续的内存空间来存放数据同时定义一个指针指向这片区域的首地址(对于后续的地址的访问也可以通过指针实现,也就是指针的偏移),不同的是当这片空间的大小不够用的时候它可以重新开辟一片更大的空间去存放这些数据并把原来的空间给释放掉,从而实现扩容的效果(但实际上他这种效果用时间换空间,时间复杂度很高,所以这种方式并不理想)。
一般情况下,我们对于顺序表都采用静态分配,对于动态分配有所了解就好。
1.1.1静态实现(理解)
思路:我们首先定义一个定义了数组和表长的结构体,用静态的数组来存放顺序表的元素用表长来记录长度方便操作。
#include <stdio.h>
#define MaxSize 10 //事先定义了数组最大长度属于是静态分配了
typedef struct{ //这个结构体的大小为max+1:数组长度再加上一个length
int data[MaxSize];//用静态数组存放元素
int length; //顺序表的当前长度
}SqList;
//————————————————————关于操作的定义————————————————————————
//操作1:初始化一个顺序表
void InitList(SqList &L){//把&写道形参的位置是c++的语法,称为引用,这个时候操作L和主函数里边使用L等价的
/* for(int i=0;i<MaxSize;i++){//将所有数据元素初始化为0,如果不初始化内存中会有脏数据 ,正常访问不到可以不做
L.data[i]=0; //初始化表长为0
}*/
L.length=0; //将表长设置为0!必做
}
//操作2:插入一个元素
bool ListInsert(SqList &L,int i,int e){
if(i<1||i>L.length+1){
return false;//判断:插入的范围是第一个前面到最后一个后面
}
if(L.length>=MaxSize) {
return false;//判断:存满了 则返回false
}
for(int j=L.length;j>=i;j--){//依次后移
L.data[j] =L.data[j-1];
}
L.data[i-1]=e;
L.length++;
return true;
}
//操作3:删除一个元素并且返回删除的值
bool ListDelete(SqList L,int i,int &e) {
//e是引用形变量,引用了我们自己定义的全局变量,如果没有&符号
//那么函数就会在内部定义局部变量,改变也是改变的局部变量的值也就带不回来了
if(i<1||i>L.length) return false;//删除的范围也就是元素的范围
e=L.data[i-1];//第i个位置对应数组的i-1号下标
for(int j=i;j<L.length;j++) {
L.data[j-1]=L.data[j];
}
L.length--;
return true;
}
//操作4:按位查找
int GetElem(SqList L,int i){
if(i<1||i>L.length) printf("非法位置");
else return L.data[i-1];
}
//操作5:修改
bool GaiElem(SqList &L,int i,int e){
if(i<1||i>L.length) {
return false;
}
else {
L.data[i-1]=e;
}
return true;
}
//——————————主函数——————————————————————————
int main(){
SqList L; //声明一个顺序表
bool ret;// 用来接受返回值
int del;//用来存要删除的元素
InitList(L); //初始化顺序表 (也就是把表长设置为零)
L.data[0]=1;
L.data[1]=2;
L.data[2]=3;
L.length=3;
ListInsert(L,1,1); //插入元素 为什么这里传入的不是&L啊 为什么不带& 不用带回来吗??????????????为什么因为指针就是一个地址 定义要地址就给地址 c++封装好的不用加&了 -----c++就是这样定义的
//删除测试-----------------
/* int e=-1;//用变量e把删掉的元素带回来
if(ListDelete(L,3,e))
printf("已经删除第三个元素,值为=%d\n",e);
else
printf("位序不合法,删除失败!\n");
return 0;
*/
//插入测试----------------------
ret=ListInsert(L,2,2);
if(ret){
printf("插入成功!\n");
}
else{
printf("插入失败!\n");
}
//查找测试----------------------
int a=GetElem(L,1);
printf("GetElem的值为%d\n",a);
/*顺序表遍历 ---------------
for(int i=0;i<L.length;i++){
printf("data[%d]=%d\n",i,L.data[i]);
}
*/
return 0;
}
1.1.2动态分配(掌握)
#include <stdio.h>
#include <stdlib.h>
#define InitSize 10
typedef struct{
int *data; //指向了顺序表中第一个数据元素
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList; //顺序表的类型定义(动态分配的方式)
//操作1:初始化
void InitList(SeqList &L){
L.data=(int *)malloc(InitSize*sizeof(int));//调用malloc函数申请一片连续的空间,返回的指针强转为存放的数据类型(l.data指针指向一片连续的存储空间 ) 这片空间也是用数组表示?????????
//动态分配,data是个指针指向顺序表中的第一个数据元素 ,这里虽然data是个指针但是仍然可以用数组下标的方式访问里面的元素
L.length=0;
L.MaxSize=InitSize; //设置最大容量和初始值保持一致
}
//操作2:增加动态数组的长度
void IncreaseSize(SeqList &L,int len){
int *p=L.data; //让p指针也指向data指针指向的位置
L.data=(int *)malloc((L.MaxSize+len)*sizeof(int));//申请一片比原空间多len的大小的空间
for(int i=0;i<L.length;i++){
L.data[i]=p[i];
}
L.MaxSize=L.MaxSize+len;
free(p);//调用free函数 释放p指针指向的原来小的那一整片空间 而对于p,他是定义在函数中的局部变量当函数执行结束之后系统会将他的空间自动回收
}
//操作3:按位查找
int GetElem(SeqList L,int i){
return L.data[i-1];
}
//操作4:按值查找
int LocateElem(SeqList L,int e){
for(int i=0;i<L.length;i++)
if(L.data[i]==e) return i+1;
return 0;
}
//主函数
int main(){
SeqList L;
InitList(L);
IncreaseSize(L,5);
GetElem(L,3);
int a=LocateElem(L,9);
printf("%d",a);
/* for(int i=0;i<15;i++){
printf("data[%d]=%d\n",i,L.data[i]);//打印顺序表的初始值
} */
return 0;
}
1.2链表
链表分为带头结点的和不带头节点的,我们定义头节点可以方便我们对链表的前向元素进行操作,所以我们一般都采用带头结点的链表。对于链表我们实现的思路是:设计结构体分为数据域和指针域,不管有没有头节点,先定义一个头指针,头指针的数据域不存数据只起到指向第一个数据节点的作用 。
1.2.1 带头结点的单链表
#include <stdio.h>
#include <stdlib.h>
typedef struct LNode{
int data;
struct LNode *next;//定义指向下一个节点的指针 !!!注意这里不能使用别名定义,因为是从上到下编译此时别名还没定义
}LNode,*LinkList;
//操作1:初始化 (开辟节点,next置空)
bool InitList(LinkList &L){
//指向的下一节点是头节点不存储数据;加头节点是为了一些操作更加方便实现
L=(LNode *) malloc(sizeof(LNode));
if(L==NULL){
return false;//内存分配失败
}
L->next=NULL;
return true;
}
//头插法和尾插法的核心思想就是初始化操作和指定节点的后插操作
//操作2:尾插法建立单链表(要考)
LinkList List_TailInsert(LinkList &L){
int x;
L=(LinkList)malloc(sizeof(LNode));//初始化空表
L->next=NULL;
LNode *s,*r=L;// r为尾指针 此时两个指针都指向头节点
scanf("%d",&x);//插入节点的值
while(x!=9999){//当输入值为9999代表插入结束
s=(LNode *)malloc(sizeof(LNode));
s->data=x;
s->next=s;
r=s;//r指向新的表尾节点
scanf("%d",&x);
}
r->next=NULL;
return L;
}
//操作3:头插法建立单链表 逆置 !
LinkList List_HeadInsert(LinkList &L){
LNode *s;//s为要插入的新节点
int x;
L=(LinkList)malloc(sizeof(LNode));
L->next=NULL;
scanf("%d",&x);
while(x!=9999){
s=(LNode *)malloc(sizeof(LNode));
s->data=x;
s->next=L->next;
L->next=s;
scanf("%d",&x);//逆置时逻辑不变,但是取值不是scanf而是用指针循环扫描遍历出所有元素,再用头插法插入进链表
}
return L;
}
//操作4:前插和后插操作
bool InsertNextNode(LNode *p,int e){
if (p=NULL) return false;//??????????????????????????配合查找函数使用
LNode *s =(LNode *)malloc(sizeof(LNode));
//if(s==NULL) return false;内存分配失败
s->data;
s->next=p->next;
p->next=s;
return true;
}
//前插操作
//1.bool InsertPriorNode(LinkList L,LNOde *p,int e) 传入头指针 时间复杂度为n
//2.偷天换日
bool InsertPriorNode(LNode *p,int e){
if (p==NULL) return false;
LNode *s=(LNode *)malloc(sizeof(LNode));
//if (s==NULL) return false;
s->next=p->next;
p->next=s;
s->data=p->data;
p->data=e;
}
//操作5:按位查找
LNode *GetElem(LinkList L,int i){
if(i<0)
return NULL;//头节点的数据为null
LNode *p;
int j=0;
p=L;
while(p!=NULL&&j<1){
p=p->next;
j++;
}
return p;//如果超出最大存储也会返回null,所以只需要判断是否返回null就可知道是否查找成功
}
//操作6:按值查找 ,找到数据域==e的结点 时间复杂度为n(平均和最坏)
LNode *LocateElem (LinkList L,int e){
LNode *p=L->next;//从第一个结点(也就是头节点的下一个节点)开始查找数据域为e的节点
while(p!=NULL&&p->data != e)
p=p->next;
return p; //找到后返回该节点指针,否则返回null
}
//操作7:求表长
int length(LinkList L){
int len=0;
LNode *p=L;
while (p->next!=NULL){
p=p->next;
len++;
}
return len++;
}
//思路:
/*
先判断位置是否合法,如果不合法直接返回false
如果合法,就定义一个指针指向头节点,定义一个计数器记录这个节点目前指向的位置
使用while找到插入节点的前一个位置即第i-1个位置,让新指针指向这个位置,这一步while要判断位置是否合法若next指向null则不合法返回false
然后开辟节点,修改指针指向,注意先顺序。
*/
//操作8:插入(封装一下查找和后插《找到插入位置的前一个节点进行后插)
bool ListInsert(LinkList L,int i,int e){
LNode *p=GetElem(L,i-1); //找到第i-1个节点
/*if(i<1) return false;//i代表位序,位序从一开始 ,头节点当作零不存数据也就不能向零号中插入数据,一号节点作为表头
LNode *p;//p指针指向当前向前扫描到的节点
int j=0;//计数器 记录当前指针p指向的是第几个节点,当下正指向头节点
p=L;//L指向头节点,头节点是第0个节点(不存数据)
while(p!=NULL&&j<i-1){// 循环的目的是找到第i-1个节点
p=p->next;
j++;
}*/
return InsertNextNode(p,e);
/*----后插操作-----
if(p==NULL) return false;//i不合法 最后一个节点的next域是null
LNode *s =(LNode *)malloc(sizeof(LNode));
s->data=e;
s->next=p->next;
p->next=s;
return true;*/
}
//操作10:按位删除(修改前驱节点的next指针+释放资源)
bool ListDelete(LinkList &L,int i,int &e){
//--------------------------------------
if(i<1) return false;//i代表位序,位序从一开始 ,头节点当作零不存数据也就不能向零号中插入数据,一号节点作为表头
LNode *p;//p指针指向当前向前扫描到的节点
int j=0;//计数器 记录当前指针p指向的是第几个节点,当下正指向头节点
p=L;//L指向头节点,头节点是第0个节点(不存数据)
while(p!=NULL&&j<i-1){// 循环的目的是找到第i-1个节点
p=p->next;
j++;
}
if(p==NULL) return false;//i不合法
//------------------------------------
if(p->next==NULL) return false;//第i-1个之后已无其他节点
LNode *q=p->next;
e=q->data;
p->next=q->next;
free(q);
return true;
}
//操作11:删除指定的节点(偷天换日)
bool ListDelete (LNode *p){
if(p==NULL) return false;
LNode *q=p->next;
p->data=p->next->data;//存在bug,如果要删除的是最后一个节点就会出错,因为最后一个节点的next是null此时会报空指针的错误,此时只能用传入头节点的方法来解决问题
p->next=q->next; //体现了单链表的局限性,无法逆向检索,只能顺着找后面的不能反着找前面的
free(q);
return true;
}
//操作12:遍历链表
void PrintList(LinkList L) {
L=L->next;//使头指针指向第一个数据节点
while(L!=NULL){
printf("%d",L->data);
L=L->next;
}
}
int main(){
LinkList L;
List_HeadInsert(L);
// List_TailInsert(L);
PrintList(L);
}
1.2.2不带头结点的单链表(了解)
#include <stdio.h>
#include <stdlib.h>
typedef struct LNode{
int data;
struct LNode *next;
}LNode, *LinkList;
//LNode 是指结构体= typedef struct LNode LNode;
//*LinkList是指向LNode的指针 =typedef struct LNode *LinkList;
//操作一:初始化
bool InitList(LinkList L){
L=NULL;//这里的初始化是防止内存中有脏数据 指向的下一节点就是存放数据的节点
return true;
}
//操作二:判空
bool Empty(LinkList L){
return (L==NULL);
}
bool ListInsert(LinkList &L,int i,int e){
if(i<1) return false;
if(i==1){//插入第一个节点与其他节点的操作不同
LNode *s = (LNode *)malloc (sizeof(LNode));//开辟一个新的节点先存数据在插到链中
s->data=e;
s->next=L;
L=s;//头指针指向新节点
return true;
}
LNode *p;//指针p指向当前扫描到的节点
int j=1;//当前p指向的是第一个节点(没有头节点)
p=L;
while (p!=NULL&& j<i-1){
p=p->next;
j++;
}
if (p==NULL) return false;
LNode *s=(LNode *)malloc (sizeof(LNode));
s->data =e;
s->next =p->next;
p->next=s;
return true;
}
int main(){
LinkList L;//在这里只是定义了头节点,此处并没有创建一个节点 ,他只是一个指针并不是一个节点
InitList(L);
ListInsert(L,1,1);
}
1.2.3双链表
几个点:
1.双向链表的头结点的前向指针永远指向null
2.在查找操作上与单链表相同,在插入和删除操作上不同
#include <stdio.h>
#include <stdlib.h>
typedef struct DNode{
int data;
struct DNode *prior,*next;
}DNode,*DLinkList;
bool InitDLinkList(DLinkList &L){
L=(DNode *)malloc(sizeof(DNode));
if (L==NULL) return false;
L->prior=NULL;
L->next=NULL;
return true;
}
//插入(p之后插入s节点)
bool InsertNextDNode(DNode *p,DNode *s){
if(p==NULL||s==NULL) return false;
s->next=p->next;
if (p->next!=NULL) p->next->prior=s;
s->prior=p;
p->next=s;
return true;
}
//删除p节点的后继节点
bool DeleteNextDNode(DNode *p){
if(p==NULL) return false;
DNode *q =p->next;
if (q==NULL) return false;
p->next=q->next;
if (q->next!=NULL) q->next->prior=p;
free(q);
return true;
}
//销毁
void DestoryList(DLinkList &L){
while(L->next!=NULL)
DeleteNextDNode(L);
free(L);
L=NULL;
}
//遍历(按位查找,安置查找)
int main(){
DLinkList L;
InitDLinkList(L);
//bool b=InsertNextDNode(p,s)
}
1.2.4循环链表
#include <stdio.h>
#include <stdlib.h>
//1、循环单链表
typedef struct LNode{
int data;
struct LNode *next;
};LNode,*LinkList;
//初始化
bool InitList(LinkList &L){
L=(LNode *) malloc (sizeof(LNode));
if(L==NULL) return false;
L->next=L; //头节点指向自己
return true;
}
//2、循环双链表
typedef struct DNode {
int data;
struct DNode *prior *next ;
}DNode,*DLinklist;
//初始化
bool InitDlinkList(DLinklist &L){
if(L==NULL) return false;
L->prior=L;
L->next=L;
return true;
}
1.2.5静态链表
静态链表:分配一整片连续空间,各个节点集中安置 。静态链表的每个节点包含两块,数据元素和下一个节点的数组下标(游标)。
1.数组为零的节点充当头节点不存放数据元素。
2.最后一个节点的游标为-1。
3.可以用一个特殊的数值表示空闲的节点,如:-2.
就是用数组实现的单链表;
优点:增删不需要移动大量元素
缺点:不支持随机存取;容量固定。
使用场景:不支持指针的低级语言;数据元素固定不变;
顺序表与链表的对比
顺序表:
优点:1.支持随机存取、存储密度高
缺点:大片连续空间分配起来不方便,改变容量也不方便
使用场景:表长固定,查询操作多
链表:
优点:修改方便
缺点:存取密度低,不支持随机存取
使用场景:表长难以估计,增删操作多
第二章 栈和队列
2.1栈的实现
栈就是操作受限的线性表,栈式是线性表的特殊化。
2.1.1顺序栈
采用静态数组实现
//创销判空求表长增删改查
#include <stdio.h>
#include<stdlib.h>
#define MaxSize 10
typedef struct SqStack{
int data[MaxSize];
int top;//栈顶指针 记录的是数组的下标,从零开始
}SqStack;
//初始化
void InitStack(SqStack &S){
S.top=-1;
}
//销毁
bool DestoryStack(SqStack &S){
S.top=-1;
}
//判空
bool StackEmpty(SqStack &S){
if (S.top==-1)
return true;
else
return false;
}
//求表长
int GetLength(SqStack &S){
return S.top+1;
}
//增:进栈
bool Push(SqStack &S,int x){
if(S.top==MaxSize-1) return false;
S.top=S.top+1;//先加一再赋值
S.data[S.top]=x; //S.data[++S.top]=x
return true;
}
//删:出栈
bool Pop(SqStack &S,int &a){
if(S.top==-1) return false;
a=S.data[S.top--];//先取值再下移指针,只是在逻辑上删除了元素但数据仍残留在内存中
return true;
}
//改查:读取栈
bool GetTop(SqStack &S,int &y){
if(S.top==-1) return false;
y=S.data[S.top];
return true;
}
int main(){
SqStack S;
InitStack(S);
int a;//接收数据
int b;//接受栈顶元素
Push(S,0);
Push(S,1);
Push(S,2);
Push(S,3);
Push(S,4);
Push(S,5);
Push(S,6);
a=GetLength(S);
GetTop(S,b);
printf("%d\n",a);
printf("%d",b);
}
2.1.2共享栈
由于顺序栈采用静态数组实现,空间是一次开辟完成。为了正常使用通常会开辟一大块空间,但这样有时候会造成资源浪费,所以就出现了共享栈。共享栈相当于是两个栈共用一片存储空间,两端分别是栈顶,存储空间的中段是两个栈的栈底。
#define MaxSize 10
typedef struct{
int data[1000];
int top0;
int top1;
}SHStack;
void InitStack(ShStack &S){
S.top0=-1;
S.top1=MaxSize;
}
栈满条件:top0+1==top1
2.1.3 链栈
’‘’ 链栈本质上就是限制在头部进行插入和删除操作的单链表 ‘’‘
//链栈本质上就是限制在头部进行插入和删除操作的单链表
#include <stdio.h>
#include<stdlib.h>
typedef struct StackNode{
int data;
struct StackNode *next;
}*LinkStack;
//-------------------------此处实现不带头结点的版本-----------------------------------------
//初始化
bool InitStack(LinkStack &S){
S=NULL;
return true;
}
//销毁
bool DestoryStack(LinkStack &S){
S=NULL;
}
//判空
bool StackEmpty(LinkStack &S){
return (S==NULL);
}
//增:进栈
bool Push(LinkStack &S,int x){
StackNode *s = (StackNode *)malloc (sizeof(StackNode));//开辟一个新的节点先存数据在插到链中
s->data=x;
s->next=S;
S=s;
return true;
}
//删:出栈
bool Pop(LinkStack &S,int &a){
if(S==NULL) return false;
LinkStack p=NULL;
a=S->data;
p=S;
S=S->next;
free(p);
return true;
}
//改查:读取栈
int GetTop(LinkStack &S,int &y){
if(S==NULL) return -1;
y=S->data;
return y;
}
//遍历栈
void PopStack(LinkStack &S){
LinkStack P;
P=S;
while(P!=NULL){
printf("%d\n",P->data);
P=P->next;
}
}
int main(){
LinkStack S;
InitStack(S);
int a;//接收数据
int b;//接受栈顶元素
Push(S,0);
Push(S,1);
Push(S,2);
Push(S,3);
Push(S,4);
Push(S,5);
Push(S,6);
// b=GetTop(S,a);
// printf("%d\n",b);
PopStack(S);
}
2022.1.26
2.2 队列的实现
2.2.1队列的顺序实现——循环队列
#include <stdio.h>
#include <stdlib.h>
#define MaxSize 100
//创销增删改查
typedef struct {
int data[MaxSize];
int front,rear;//队头指向队头元素 队尾指针指向队尾元素的后一个位置也即是下一个元素的插入位置
}SqQueue;
//初始化
void InitQueue(SqQueue &Q){
Q.front=Q.rear=0; //让队头队尾指针都指向数组的第一个位置
}
//入栈
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;
}
//出队
bool DeQueue(SqQueue Q,int &x){
if((Q.rear+1)%MaxSize==Q.front) return false;
x =Q.data[Q.front];
Q.front++;
return true;
}
//遍历出队
int PopQueue(SqQueue &Q) {
int j=0;//计数器
for(int i=Q.front;i!=Q.rear;i=(i+1)%MaxSize)
{
printf("%d->",Q.data[i]);
}
printf("出队完成!\n");
return j;
}
//获取队头
int GetHead(SqQueue Q){
if(Q.rear!=Q.front)
return Q.data[Q.front];
}
//判空
bool QueueEmpty(SqQueue Q){
return (Q.rear==Q.front);
}
int main(){
SqQueue Q;
EnQueue(Q,11);
EnQueue(Q,22);
EnQueue(Q,33);
EnQueue(Q,44);
EnQueue(Q,55);
//int a=GetHead(Q);
//printf("%d",a);
int a=-1;
DeQueue(Q,a);
DeQueue(Q,a);
DeQueue(Q,a);
//printf("%d",a);
PopQueue(Q);
}
注:
- 取模构成循环队列的原因是解决队头指针出队后上移动导致前面空出无法入队的空间造成假溢出的问题
- 由于队空队满时候的判断条件相同,所以为了区别他们两个的状态我们有以下三种方法,
一、设置状态记录位当满时为1非满时候为0
二、设置队长记录位
三、空出一位不写入数据 【条件:队尾指针的下一个位置是队头即(Q.rear+1)%MaxSize==Q.front)】
3.计算队列中元素的个数:(rear+MaxSize-front)%MaxSize ——选择题常考
2.2.2队列的链式实现
//带头节点的队列
#include <stdio.h>
#include <stdlib.h>
typedef struct QNode{
int data;
struct QNode *next;
}QNode,*QueuePtr;
typedef struct {
QueuePtr front;
QueuePtr rear;
}LinkQueue;
//初始化
bool InitQueue(LinkQueue &Q){
Q.front=Q.rear=(QueuePtr)malloc(sizeof(QNode));//k开辟一块一个节点大小的内存空间并用头尾保存此节点的内存地址
if(!Q.front) return false;
Q.front->next=NULL;
return true;
}
//销毁
bool Destory(LinkQueue &Q){
QNode *P;
while(Q.front){
P=Q.front->next;
free(Q.front);
Q.front=P;
}
return true;
}
//入队
void EnQueue(LinkQueue &Q,int a){
QueuePtr P=(QueuePtr)malloc(sizeof(QNode));
P->data=a;
P->next=NULL;
Q.rear->next=P;
Q.rear=P;
}
//出队
bool DeQueue(LinkQueue &Q,int &b){
if(Q.front==Q.rear)
return false;
QueuePtr P=Q.front->next;
b=P->data;
Q.front->next =P->next;
if(Q.rear==P)
Q.rear=Q.front;//这里比较特殊,意思是我们是采用了新的指针P暂存待删除元素的地址,但当是最后一个地址的时候已经无法再将节点向后移所以在最后一个时候要调整rear指针
free(P);
return true;
}
//队头元素
int GetHead(LinkQueue Q){
return Q.front->next->data;
}
int main(){
int a,b;
LinkQueue Q;
InitQueue(Q);
EnQueue(Q,22);
EnQueue(Q,33);
DeQueue(Q,a);
b=GetHead(Q);
printf("%d",b);
}
双端队列
只允许从两端插入和删除的线性表
变体:输入受限和输出受限的双端队列
考题:判断输出顺序是否合法
卡特兰数.
2.3 栈和队列的应用
2.3.1 栈的应用——括号匹配LIFO
算法实现:
2.3.2 栈的应用——表达式求值
表达式由
操作数、运算符、界限符
组成。
中缀表达式就是日常使用的形式:运算符在两个表达式中间,例如a+b
后缀表达式应用场景多考察较多:运算符在两个表达式后面,例如ab+
前缀表达式称为波兰表达式(后缀表达式称为逆波兰表达式)例如+ab
手算时,一个中缀表达式对应的后缀表达式不唯一
为了算法的唯一性,机算采用左优先原则(只要左边的运算符能先计算就先算左边的),保证机算和手算的结果相同
实现:
中缀转前缀
前缀表达式
2.3.3 栈的应用——递归
函数调用的特点:LIFO
在任何一段代码执行前,系统都会在内存中开辟一块函数调用栈,用这个栈来保持各个函数在调用过程中需要保存的地址如调用返回地址、实参、局部变量。
递归包括:递归表达式和边界条件,如斐波那契和汉诺塔
递归的复杂度很高如果对时间要求不较高一般不采用递归算法
2.3.4 队列的应用——树、图的广度优先遍历、操作系统资源分配策略
2.4.特殊矩阵的压缩存储
2.4.1 普通矩阵
二维数组
2.4.2 对称矩阵
存储:只存储主对角线和下三角区
//映射函数
if(i>j)
算出这个元素前面有多少元素
if(i<j)
使用:通过实现一个映射函数将矩阵下标映射成数组下标然后使用(考察点!)
2.4.3 三角矩阵
2.4.4 三对角矩阵
稀疏矩阵
三元组存储
十字链表发