c语言实现数据结构---关于线性表包括栈和队列

期末考试之前复习以及总结一下数据结构

目录

关于线性表

线性表的基本操作

 顺序表的基本操作

顺序表的类型定义

对顺序表的初始化操作 

对顺序表进行判空操作

对顺序表进行求长度

关于函数参数 

 取出顺序表的第i个元素

查找元素e在顺序表中的位序

插入操作

删除操作 

单链表的基本操作

单链表的类型定义

 初始化操作

建立单链表

 头插法建立链表

判断一个单链表是否为空表

求单链表的长度

取单链表中的第i个元素

查找元素e的位序

删除操作

 双向链表的基本操作

双向链表的类型定义

插入操作 

删除操作

特殊的线性表

关于栈的基本操作

 栈的定义

初始化一个栈

判断一个顺序栈是否为空栈

求顺序栈的长度

入栈push

出栈pop

取栈顶元素

共享存储空间的顺序栈

 共享存储空间的顺序栈的类型定义

初始化操作

判空

入栈

出栈

取栈顶元素

关于队列的基本操作

 队列的顺序存储结构--循环队列

 关于队列“假溢出”现象

 循环队列类型定义

初始化循环队列

判断一个循环队列是否为空

求循环队列的长度

入队列

出队列

取队头元素

队列的链式存储结构--链队列

 链队列的类型定义

初始化

判断一个链队列是否为空

入队列

出队列

 取队头元素


     

关于线性表

  是一种应用十分广泛的数据结构,特点是结构中各数据元素满足线性关系,并且其长度可根据需要增加或减短。线性表在计算机中有两种存储方式:顺序存储和链式存储,主要操作时插入、删除和查找等。

线性表的基本操作

 顺序表的基本操作

顺序表的类型定义

顺序表存储一组地址连续的存储单元依次存储线性表的数据元素,即顺序存储直接将线性表的逻辑结构映射到存储结构上,即逻辑上相邻的数据在计算机中的存储位置也是相邻的。

线性表的存储结构:

 代码定义:


//定义一个顺序表
#define LInit 100  //顺序表的初始大小 
#define LInc 10   //顺序表的存储空间增量
typedef struct{
	int * base; //表的存储空间基地址,int可以换成别的类型
	int length; //表的当前长度
	int lsize; //顺序表的存储空间大小 
}SQ;           // 顺序表的类型
 

对顺序表的初始化操作 

初始化就是对所定义的顺序表L中的所有成员赋初始值。

void Init (SQ &l) {
	//创建一个初始大小为LInit大小的空顺序表
	l.base = (int*)malloc(LInit*sizeof(int));
	//申请存储空间
	if(!l.base)//申请空间失败
		return;
	l.length = 0; //设为空表
	l.lsize = LInit;
	return ;
}

对顺序表进行判空操作

这个操作特别的简单,判断一个顺序表是否为空表,只要判断length的值是否为零

bool LEmpty(SQ &l){
	if(l.length == 0)
		return true;
	return false;
} 

对顺序表进行求长度

这个更简单,只要获得顺序表的length值就可以了

int Llen(SQ l){
	return l.length;
} 

关于函数参数 

由于可以不用对顺序表本身进行操作只获得顺序表的数据,可以传地址也可以不用传地址,直接把数据复制到形参中。

形参要在内存中临时开辟另一段空间存储数据同样指针传递也是会用到内存空间,但是大部分情况下直接传递地址会快一些,另外就是更多次的使用那一块空间对数据的保护没有太多好处,我参考的教材上可以不对表本身操作的都没有直接传递地址,所以我大部分也是没有用到传递地址

 取出顺序表的第i个元素

我觉得这里可以联想一下数组,即拿到下标为i-1存储空间里的元素

void Get(SQ l, int i, int &p){
	//看看第i个是否在域内
	if(i < 1 || i > l.length )
		return ;
	p = l.base[i-1];
	return; 
}

查找元素e在顺序表中的位序

就是数组for或者while循环

int Locate(SQ l, int p){
	int i = 0;
	//时间复杂度O(n) 
	while(i < l.length && l.base[i] != p)
		i++;//满足循环条件就是没有找到匹配的值
	//跳出循环一共两个原因找到p或者遍历完了也没找到 
	if(i < l.length)//跳出循环的原因是找到了,因为没有满足遍历完这个条件
		return i + 1;//下标是i就是第i+1个因为位序是从1开始记的 
	return 0;//零不存在于位序之中	 
}

插入操作

在第i个位置插入一个元素

插入位置之前的元素不变,插入位置之后的元素全部后移

void Insert(SQ &l, int i, int e)
{
	//此时对表本身进行操作
	//首先判断i是否合法
	if(i < 1 || i >= l.length) {
		return ;
	}
	if(l.length >= l.lsize )//存储空间已满,需扩充
	{
		int *newbase = (int*)realloc(l.base, (l.lsize + LInc)*sizeof(int));
		if(!newbase)
			return;
		l.base = newbase;
		//因为用的realloc,申请出来的空间和原来的基地址是同一个,即直接进行赋值就行 
		l.lsize += LInc;
	 } 
	 for(int j = l.length-1; j >= i-1; j--){
	 	l.base[j+1] = l.base[j];
	 }//简单的后移操作 
	 l.base[i-1] = e;
	 l.length++;
	 return ;
}

删除操作 

删除一个元素和插入差不多,但是不用害怕空间不够用,相比之前还要简单一些。

void del(SQ &l, int i, int &p){
	if(i < 1 || i > l.length){
		return ;
	}
	p = l.base[i-1];
	for(int j = i; j < l.length; j++){
		l.base[j-1] = l.base[j];
	}
	l.length --;
	return ;
}

顺序表的基本操作所有的函数 

#define LInit 100  //顺序表的初始大小 
#define LInc 10   //顺序表的存储空间增量
typedef struct{
	int * base; //表的存储空间基地址
	int length; //表的当前长度
	int lsize; //顺序表的存储空间大小 
}SQ;           // 顺序表的类型
void Init (SQ &l) {
	//创建一个初始大小为LInit大小的空顺序表
	l.base = (int*)malloc(LInit*sizeof(int));
	//申请存储空间
	if(!l.base)//申请空间失败
		return;
	l.length = 0; //设为空表
	l.lsize = LInit;
	return ;
}
//判空操作
bool LEmpty(SQ &l){
	if(l.length == 0)
		return true;
	return false;
} 
//求长度 
int Llen(SQ l){
	return l.length;
} 
void Get(SQ l, int i, int &p){
	//看看第i个是否在域内
	if(i < 1 || i > l.length )
		return ;
	p = l.base[i-1];
	return; 
}
int Locate(SQ l, int p){
	int i = 0;
	//时间复杂度O(n) 
	while(i < l.length && l.base[i] != p)
		i++;//满足循环条件就是没有找到匹配的值
	//跳出循环一共两个原因找到p或者遍历完了也没找到 
	if(i < l.length)//跳出循环的原因是找到了,因为没有满足遍历完这个条件
		return i + 1;//下标是i就是第i+1个因为位序是从1开始记的 
	return 0;//零不存在于位序之中	 
}
void Insert(SQ &l, int i, int e)
{
	//此时对表本身进行操作
	//首先判断i是否合法
	if(i < 1 || i >= l.length) {
		return ;
	}
	if(l.length >= l.lsize )//存储空间已满,需扩充
	{
		int *newbase = (int*)realloc(l.base, (l.lsize + LInc)*sizeof(int));
		if(!newbase)
			return;
		l.base = newbase;
		//因为用的realloc,申请出来的空间和原来的基地址是同一个,即直接进行赋值就行 
		l.lsize += LInc;
	 } 
	 for(int j = l.length-1; j >= i-1; j--){
	 	l.base[j+1] = l.base[j];
	 }//简单的后移操作 
	 l.base[i-1] = e;
	 l.length++;
	 return ;
}
void del(SQ &l, int i, int &p){
	if(i < 1 || i > l.length){
		return ;
	}
	p = l.base[i-1];
	for(int j = i; j < l.length; j++){
		l.base[j-1] = l.base[j];
	}
	l.length --;
	return ;
}

单链表的基本操作

相比于顺序表都是连续的存储空间,链表的存储空间可以连续也可以不联系,链式存储即用链表将存储空间链起来, 即不需要空间连续就能找到后继,甚至前驱。

 链表通过一个或者两个指针域来定位后继或者前驱。

单链表就是有一个指针域,指向当前节点的后继,只能单向移动,即你无法去到当前结点的前面,就像现在知道了a2的地址,你不能知道a1的地址,只能知道a2之后的元素的地址。

单链表的类型定义

包括数据域和指针域

typedef struct lNode{
	int date;
	//我用的int型,也可以是别的类型包括一些结构体甚至c++容器 
	struct lNode *next;
}lNode, *linklist;
//lNode是结点类型, linklist是结构体类型的指针类型
//linklist就相当于lNode*,只是名字不同
 

 初始化操作

对单链表进行初始化,就是构造一个空链表,即只有一个头节点,指针域为空

void Init(linklist &l){
	l = (linklist)malloc(sizeof(lNode));
	if(!l)
		return ;
	l->next = NULL;
} 

建立单链表

建立一个单链表有两种方法:头插和尾插。

尾插比较好理解,就是把需要插入的元素插入到链表的最后就是尾端,可以看一下这个图,就是尾插。

//尾插法建立链表
void Tail(linklist &l, int n){
	//输入n个元素用尾插发建立一个带有头节点的单链表l
	l = (linklist)malloc(sizeof(lNode));//申请头节点
	linklist pre = (linklist)malloc(sizeof(lNode));
	pre = l; //pre保存头节点的地址
//	linklist p = (linklist)malloc(sizeof(lNode));
	for(int i = 0; i < n; i++){
		linklist p = (linklist)malloc(sizeof(lNode));
		scanf("%d", &p->date);
		pre->next = p;
		pre = p;
	} 
	pre->next = NULL;
	return ;
} 

 头插法建立链表

每读入一个元素,将包含该元素的结点作为首元结点插到链表中,直到所有元素读取结束。首先向系统申请一个头结点,将其赋给头指针l,使l->next=NULL,然后申请新结点p,使p->date=x,p->next=l->next,l->next=p,这个步骤直到所有元素插完为止。


void head(linklist &l, int n){
	l =(linklist)malloc(sizeof(lNode));
	l->next = NULL;
	for(int i = 0; i < n; i++){
		linklist p = (linklist)malloc(sizeof(lNode));
		scanf("%d", &p->date);
		p->next = l->next;
		l->next = p;
	}
    return;
} 

判断一个单链表是否为空表

如果头结点的next的值为空,为空的话链表就是空的,否则就不是空表。

bool Empty(linklist l){
	if(l->next == NULL)
		return true;
	return false;
}

求单链表的长度

单链表就是只能往后走,求长度就是从头结点开始遍历到最后一个元素

int len(linklist l){
	int count = 0;
	linklist p = l->next;
	while(p)
		count++, p = p->next;
	return count;
}

取单链表中的第i个元素

顺着头结点找呗,比顺序表麻烦点

void Get(linklist l, int i, int &e){
	int j = 1;
	linklist p =l->next;
	while(p && j < i){
	//两种情况,找到头没有到达第i个结点说明给的这个i是无效的,j=i说明找到了,刚好没有满足循环条件 
		j++;
		p = p->next ; 
	}
	if(j > i || !p){
		return ;
	}
	e = p->date ;
	return;
}

查找元素e的位序

思想就是设置一个计数器变量,然后遍历。

void Insert(linklist &l, int i, int e){
	int j = 0;
	linklist p = l;
	while(p && j < i-1){
		j++;
		p = p->next;
	}
	if(!p || j> i-1)
		return ;
	linklist s = (linklist)malloc(sizeof(lNode));
	s->date = e;
	s->next = p->next;
	p->next = s;
	return ;
}

删除操作

在链表里面删除和插入一个结点除了一些指针的改变几乎没有元素的移动,相比起顺序表O(n)的时间复杂度,要高效很多。

删除某个结点时找到前一个元素结点,保存指针信息,并且相应赋值

void del(linklist &l, int i, int &e){
	int j = 0;
	linklist p = l->next;
	while(p->next && j < i-1)//查找第i-1个结点
		j++, p = p->next ; 
	while(!p->next && j < i-1)//i值非法 
		return;
	linklist s = p->next;//s记住第i个点
	p->next = l->next ;//将p结点的指针域只想s结点的后继
	e = s->date;
	free(s); 
	return; 
}

 双向链表的基本操作

双向链表的类型定义

双向链表包含两个指针域,分别指向前驱和后继。

typedef struct dlNode{
	int date;
	struct dlNode *pri;
	struct dlNode *next;
}dlNode, *dlinklist;

插入操作 

和单链表是类似的但是由于在双向链表中,这个结点的前驱和后继都存储有这个结点的地址信息,所以和单链表只有这一点不同

void insert(dlinklist &l, int i, int e){
	dlinklist p = l;
	int j = 0;
	while(p && j < i-1){//查找第i-1个结点 
		p = p->next ;
		j++; 
	}
	if(!p || j > i-1)//i值非法 
	return;
	dlinklist s = (dlinklist)malloc(sizeof(dlNode)) ;
	s->date = e;
	s->next = p->next ;
	s->pri = p;
	return;
}

删除操作

可以直接记录前驱和后继的地址

void del(dlinklist &l, int i, int &e){
	dlinklist p = l;
	int j = 0;
	while(p && j < i){//查找第i个结点 
		p = p->next;
		j++;
	}
	if(!p && j > i){
		return;
	}
	//dlinklist 
	e = p->date;
	p->pri->next = p->next;
	if(p->next)//不为空
		p->next->pri = p->pri;
	free(p); 
}

特殊的线性表

栈和队列是两种操作受限的线性表,逻辑结构和线性表类似,对于插入和删除附加了一定的限制

关于栈的基本操作

栈这类数据结构只可以从出和入。

 栈的定义

typedef struct{		
	int *base;		//顺序栈的存储空间基地址 
	int top;		//栈顶指针 
	int stacksize;  //存储空间大小 
}Stack;		//顺序栈的类型 

初始化一个栈

void init(Stack &s){
	s.base = (int*)malloc(Stacksize * sizeof(int));
	if(!s.base)//申请存储空间失败 
		return;
	s.top = 0;//栈顶指针初值为0 
	s.size = Stacksize;//空间大小初始值 
	return; 
} 

判断一个顺序栈是否为空栈

判断一个栈是否为空栈,直接判断栈顶指针的值是否为0

bool empty(Stack s){
	if(s.top == 0)
		return true;
	return false;
}

求顺序栈的长度

在顺序栈中栈顶指针指向栈顶元素的下一个位置,即顺序栈S的长度就是top的值

int len(Stack s){
	return s.top;
}

入栈push

入栈是在顶部入栈

void push(Stack &s, int e){
	if(s.top >= s.size){//存储空间已满,需扩充
		s.base = (int*)realloc(s.base, (s.size + Stackc) * sizeof(int));
		if(!s.base)
			return; 
		s.size += Stackc;
	}
	s.base[s.top++] = e;
	return ;
}

出栈pop

void pop(Stack &s, int &e){
	// 
	if(s.top == 0)//空栈直接返回 
		return ;
	e = s.base[--s.top];
	return ;
}

取栈顶元素

这个操作和pop不同, 只拿到这个值而不改变top指针的值,并不改变栈的结构也不缩短栈的长度。

void gettop(Stack s, int &e){
	if(s.top == 0)
		return ;
	e = s.base[s.top-1];//栈顶指针并不改变
	return;
}

共享存储空间的顺序栈

问题:在一个应用中,如果需要使用两个相同数据类型的栈,最直接的方法就是为每一个栈开辟连续的存储空间,但是这样的话,会可能可能出现一种情况,一个栈空间已满,一个还有大量的存储空间

为了节省存储空间,可以设置一段连续的存储空间存放两个栈,一个栈的栈底为该存储空间的始端,另一个栈的栈底为该存储空间的末端,每个栈从各自的端点向中间延申。

 共享存储空间的顺序栈的类型定义

#define Stacksize 100//初始空间大小 
typedef struct {
	int date[Stacksize];
	int top1;
	int top2;
}dStack;

初始化操作

两个栈顶就在一段连续存储空间两端

void init(dStack &s){
	s.top1 = 0;
	s.top2 = Stacksize - 1;
	return;
}

判空

bool empty(dStack s){
	if(s.top1 == 0 && s.top2 == Stacksize)
    //两个栈都为空,返回真
		return true;
	return false;
}

入栈

void push(dStack &s, int i, int e){
	if(s.top1 - s.top2 == 1)//存储空间已满,在这个数据结构中无法扩充 
		return;
	if(i ==1)
		s.date[s.top1++] = e;//将元素入第一个栈中 
	else if (i == 2)
		s.date[s.top2--] = e;//将元素入第二栈中 
}

出栈

void pop (dStack &s, int i, int &e){
	if(i == 1){
		if(s.top1 == 0)//第一个栈空 
			return;
		e = s.date[--s.top1];
	}
	else if(i == 2){
		if(s.top2 == Stacksize-1)//第二个栈空 
			return ;
		e = s.date[++s.top2]; 
	}
	return ;
} 

取栈顶元素

void get(dStack s, int i, int &e){
	//取出第i个栈的栈顶元素,并用变量e返回其值 
	if(i == 1){
		if(s.top1 == 0)
			return ;
		e = s.date[s.top1-1];
	}
	else if(i == 2){
		if(s.top2 == Stacksize-1)
			return;
		e = s.date[s.top2+1];
	} 
	return ;
}
#define Stacksize 100//初始空间大小 
typedef struct {
	int date[Stacksize];
	int top1;
	int top2;
}dStack;
void init(dStack &s){
	s.top1 = 0;
	s.top2 = Stacksize - 1;
	return;
}
bool empty(dStack s){
	if(s.top1 == 0 && s.top2 == Stacksize)
		return true;
	return false;
}
void push(dStack &s, int i, int e){
	if(s.top1 - s.top2 == 1)//存储空间已满,在这个数据结构中无法扩充 
		return;
	if(i ==1)
		s.date[s.top1++] = e;//将元素入第一个栈中 
	else if (i == 2)
		s.date[s.top2--] = e;//将元素入第二栈中 
}
void pop (dStack &s, int i, int &e){
	if(i == 1){
		if(s.top1 == 0)//第一个栈空 
			return;
		e = s.date[--s.top1];
	}
	else if(i == 2){
		if(s.top2 == Stacksize-1)//第二个栈空 
			return ;
		e = s.date[++s.top2]; 
	}
	return ;
} 
void get(dStack s, int i, int &e){
	//取出第i个栈的栈顶元素,并用变量e返回其值 
	if(i == 1){
		if(s.top1 == 0)
			return ;
		e = s.date[s.top1-1];
	}
	else if(i == 2){
		if(s.top2 == Stacksize-1)
			return;
		e = s.date[s.top2+1];
	} 
}

关于队列的基本操作

队列是限定在一端进行插入,而允许在另一端进行删除的线性表,允许删除的一段称为队头,允许插入的一端称为队尾。

 队列的顺序存储结构--循环队列

队列是特殊的线性表,所以线性表的顺序存储结构同样适用于队列,即可以用一组地址连续的存储单元依次存储队列中的数据元素。队列的顺序存储结构称为顺序队列。在队列的顺序存储结构中,为了体现队列的特点,需要设置两个指针front和rear,分别指示队头元素和队列元素的位置。

初始化一个队列是,令rear=front=0.每当有元素入队列成为新的队尾元素时,尾指针(rear)增1;每当队头元素出队列时,头指针(front)增1.在非空队列中,头指针总是指向队头元素,而尾指针总是指向队尾元素的下一个位置。

 关于队列“假溢出”现象

当队列处于(d)所示状态时,就不能执行入队列操作了,因此进行入队操作,会超出队列所占的存储空间,但是如图所示,这一块连续的存储空间并没有满,尚有空闲,但是不能被利用,这就是假溢出

解决的方法

1.使Q.front的值总为0

当元素出队列时,将队列中剩余的元素依次向队头位置移动

使用这个方法,在不断进行出队列操作时要大量移动元素,降低了算法效率

2.将队列的存储空间假设成一个环形空间

利用这种方法实现的顺序存储的队列称为循环队列,这种方法即可以解决假溢出又可以避免元素移动。

入队时rear的变化情况:q.rear = (q.rear+1)%MAXSIZE

出队时front的变化情况:q.front = (q.front+1)%MAXSIZE

 循环队列类型定义

#define QSIZE 100//初始空间大小 
typedef struct {
	int *base;
	int front;//队头
	int rear;//队尾 
}CQ;

初始化循环队列

 创建一个空的循环队列

void init(CQ &q){
	q.base = (int*)malloc(QSIZE * sizeof(int));
	if(!q.base)
		return;//申请空间失败直接返回退出函数
	q.front = q.rear = 0;//队头指针与队尾指针都为0 
}

判断一个循环队列是否为空

判断头指针和尾指针是否相等即可

bool emoty(CQ q){
	if(q.front == q.rear)
		return true;
	return false;
} 

求循环队列的长度

循环队列的长度,从队头指针front到尾指针rear的前一位置的连续环形空间元素的个数 

int len(CQ q){
	return (q.rear - q.front + QSIZE)%QSIZE;
	//循环队列的长度,从队头指针front到尾指针rear的前一位置的连续环形空间元素的个数 
}

入队列

void en(CQ &q, int e){
	if((q.rear + 1)%QSIZE == q.front)//存储空间已满 
		return;
	q.base[q.rear] = e;
	q.rear = (q.rear + 1)%QSIZE;
	return; 
} 

出队列

出队列操作就是在一个非空队列中,让队头元素出队列,并通过变量e返回其值

void del(CQ &q, int &e){
	if(q.front == q.rear)
		return;
	e = q.base[q.front];
	q.front = (q.front + 1)%QSIZE;
	return ;
	
} 

取队头元素

取出队头元素,对原队列不进行修改

void get(CQ q, int &e){
	if(q.front == q.rear)
		return ;//空队列
	e = q.base[q.front];
	return ; 
}

队列的链式存储结构--链队列

采用链式存储,队列的插入与删除操作实在队列的两端进行的,所以为了出队入队操作,在队列两端设置两个指针

 

 链队列的类型定义

typedef struct QNode{
	int date;
	struct QNode *next;
}QNode, *Qptr; 
typedef struct {
	Qptr front;
	Qptr rear;
}LQ;

初始化

头结点的指针域为NULL,而头指针和尾指针都指向头结点

void Init(LQ &q){
	q.front = q.rear = (Qptr)malloc(sizeof(QNode));
	if(!q.front )
		return ; 
	q.front->next = NULL;
	return;
}

判断一个链队列是否为空

头指针和尾指针都指向头结点就说明队列为空

bool empty(LQ q){
	if(q.front == q.rear )
		return true;
	return false;
}

入队列

入队列操作只要尾部加一个结点,尾指针指向这个新加入的结点,这个新结点的前一个结点存储在尾指针

void en(LQ &q, int e){
	Qptr s = (Qptr)malloc(sizeof(QNode));
	s->date = e;
	s->next = NULL;
	q.rear->next = s;//插入结点,现在这个尾指针就相当于原来的最后一个元素 
	q.rear = s;//修改尾指针 
	return;
}

出队列

在队头出队列,首先设置指针变量s指向首元结点,然后s指向的next指针赋值给头指针front的next,然后释放s,注意队列的长度,如果只有一个结点,出队结束后还要修改rear的指向

 

void Del(LQ &q, int &e){
	if(q.front == q.rear)
		return ;//队列为空
	Qptr s = q.front->next;
	q.front->next = s->next;
	e = s->date;
	if(q.rear == s)
		q.rear = q.front; //队列只有一个元素
	 free(s);
	 return ;
}

注意定义的指针就用"->",定义的结构体普通变量就用"."

 取队头元素

void get(LQ q, int &e){
	if(q.front == q.rear)
		return ;//队列为空
	e = q.front->next->date;
	return ;
}

一万多字,对于写习惯了c++和python的我来说,这个c语言虽然学过但真的麻烦,之前用这些都是直接用STL的库,写这个是真的好多小细节

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值