数据结构篇(二):
这里我们直接开讲。
文章目录
1.在开始本章内容之前,我们需要搞懂如下概念:
什么是线性表?
由同类型数据元素构成有序序列的线性结构,
表中的元素个数称为线性表的长度。
线性表没有元素时为空表。
表的起始位置为表头,结束位置为表尾。
线性表的基本操作有:
初始化一个空的线性表
根据序K,找到对应元素
在线性表中找到元素第一次出现的位置
在序列i前插入一个元素
删除指定位序的元素
返回线性表长度
什么是链表?
有一系列结点(地址)组成,通过结点间的相互连接,形成的链式结构,叫做链表。
其中结点包括两个部分:
(1)存储数据元素的数据域(内存空间)
(2)存储指向下一个结点地址的指针域。
2.顺序存储结构直接表示
比如我们有上图这样一个一元多项式,那么我们应该如何用代码表示它呢?
首先我们要抓住多项式的三个关键要素:项数、系数、指数
用顺序存储结构表示的话,我们可用a[i]表示x^i的系数,i表示X的指数。
举个例子:
图示中,我们用下标i = 0表示x^0的系数,下标i = 2表示x^2的系数,以此类推。
但是这个方法虽然简单,但是当x的指数越大时,这个方法所需要的数组规模也就越大,空间复杂度就会越高,所以这个方法不适合大规模的计算。
但是对于上述问题,请大家思考,我们有必要将数组里面的0项等无用项都表示出来浪费空间吗?我们该如何改进?
结构数组的方法:
就像上图中,此时我们只需要表示非零项,就无须考虑那么多无用项了。
同理我们也可用于表示两个多项式的相加:
如上图,我们有两个多项式P1、P2,那我们接下来该怎么操作呢?
先将P1、P2的第一项做对比,显然我们看指数,19>12所以,我们指数大的那一项排在前面,做为和结果的第一项输出:
再接着:
比较P1第一项和P2第二项的指数大小,同样的,我们将指数大的一个做为输出,补在第一次输出的后面:
以此类推,最终结果为:
显然,这样的表示方法又省空间又省时间。
但是我们实现上述步骤,还有一种更简单有效的方法——链表。
3.链表
链表结构如下:
typedef struct PolyNode *Polynomial;
struct PolyNode
{
int coef; //系数
int expon; //指数
Polynomial link;//指向下一个节点
}
对于前面的两个多项式,用链表表示如下:
怎么样?是不是比之前的方法更简单明了。
4.线性表实现多项式相加
1.定义线性表的结构:
typedef struct LNode *List;
struct LNode
{
ElementType Data[Maxsize];
int Last;
};
struct LNode L;
List PtrL;
线性表的长度为L.Last+1或L->Last+1
ElementType默认int型,可修改。
2.初始化线性表
List MakeEmpty()
{
List PtrL;
PtrL = (List)malloc(sizeof(struct LNode)) //分配节点空间
PtrL->last = -1; //长度
return Ptrl;
}
3.查找
int Find(ElementType X,List Ptrl)
{
int i = 0;
while(i<=Ptrl->Last&&Ptrl->Data[i]!=X)
i++;
if(i>Ptrl->Last) return -1; //没找到返回-1
else return i; //找到返回i
}
4.插入
想要在线性表的i-1位置插入某个元素时,我们需要先将i-1位置后方的所有元素后移一位,再进行插入。
注意:此时要从最后一个开始往后挪,具体道理大家想想都懂。
Void Insert(Element x,int i,List Ptrl)
{
int j = 0;
if(Ptrl->Last == Maxsize-1) //判断线性表是否满
{
printf("表满,不可加入");
return;
}
if(i<0||i>Ptrl->last+2<i) //判断i是否超出线性表
{
printf("输入不合法");
return;
}
for(j = Ptrl->Last;j>=i-1;j--)
{
Ptrl->Data[j+1] = Ptrl->Data[j];
}
Ptrl->Data[i-1] = X;
Ptrl->Last++;
return;
}
5.删除
先删除,再移动后面的元素。
void Delete( int i, List PtrL )
{
int j;
if(i<1||i>Ptrl->Last+1) //检查空表和合法性
{
printf("不存在第%d个元素",i);
return;
}
for(j = i;j<=Ptrl->Last;j++)
{
Ptrl->Data[j-1] = Ptrl->Data[j];
}
Ptrl->Last--; //元素个数减一
return;
}
6.链表操作的具体流程:
1.定义结点
typedef struct LNode*List;
struct LNode
{
ElementType Data;
List Nest;
}
Struct Lnode L;
List Ptrl;
2.求表长
int Length(List Ptrl)
{
List P = Ptrl; //指向第一个结点
int j = 0;
while(P)
{
P = P->Nest; //进入下一个结点
j++; //结点数加一
}
return j++;
}
3.查找
按序号:
List FindKth(int K,List Ptrl)
{
List P = Ptrl;
int i = 1;
while(P!=NULL&&i<k)
{
P = P->Next; //逐个查找
i++;
}
if(i==k) return P; //找到返回
else return NULL; //没找到返回空
}
按值:
List Findth(int x,List Ptrl)
{
List P = Ptrl;
while(P!=Null&&P->data!=x) //判断是否为空和相等
{
P = P->Next;
}
return P;
}
4.插入
因为链表的内存是动态的,所以我们在添加一个结点的同时,还要为它创建一个空间,用malloc实现。
List insert(ElementType x,int i,List Ptrl)
{
List s,p;
if(i == 1) //如果加在指针头部
{
s = (list)malloc(sizeof(struct LNode)); //分配空间
s->Data = x;
s->Next = Ptrl;
return s;
}
p = Findth(i-1,Ptrl); //找到对应的结点
if(p == NULL)
{
printf("参数出错");
return NULL;
}else
{
s = (list)malloc(sizeof(struct LNode));
s->Data = x;
s->Nest = p->Next;
p->Nest = s;
return Ptrl;
/*
将p指向的的下个结点改编成s,s指向的下个结点转变成p原本指向的那个结点
*/
}
}
5.删除操作
比如你想删除第2个结点,你需要将1结点指向的下个结点,指向3.
List Delete(int i,List PtrL)
{
List p,s;
if(i == 1)
{
s = PtrL;
if(PtrL!=NULL) PtrL = PtrL->Next;
else return NULL;
free(s);
return PtrL;
}
/*
这里要注意删除的结点为第一个结点时,应该判断后续是否有结点,如果没有结点则直接释放空间,如果有结点直接转向下一个结点
*/
p = FindKth(i-1,PtrL);
if(p == NULL)
{
printf("第%d个结点不存在",i-1);
return NULL;
}else if(p->Next == NULL)
{
printf("第%d个结点不存在",i);
return NULL;
}else
{
s = P->Next;
p->Next = s->Next;
free(s);
return PtrL;
}
}
7.广义表与多重链表
除了遇到一元多项式的问题,我们还经常遇到多元问题,如下:
此时的变量就不只是x了,而是包含x、y的二元多项式,这种情况我们还能用解决一元的方式来解决吗?
当然可以
我们可以将上式转换如下:
这样形成x的“一元方程”,随后将系数转换,不再是常量而是应该是一个指针,指向y形成的“一元方程”做为x的系数。
此时就可以用解决一元的方式解决二元的问题。
typedef struct GNode *GList;
struct GNode
{
int Tag;
union
{
ElementType Data;
GList SubList;
}URegion;
/*
这里注意union可存放不同类型的数据
*/
Glist Next;
};
其中,Tag是标志域,0表示结点是单元数,1表示是广义表。
多重链表:
如图,如果用二维数组表示矩阵,则会造成很大的浪费,0项很多,所以我们这里只表示非零项,并且每个结点通过两个指针域把行列串起来,有行指针和列指针。
由图,两个红色的循环结点,而红框中的结点,即属于某一行也属于某一列,则称为十字链表,黄色的是列循环链表的头结点,蓝色是行循环列表的头结点,紫色框中的结点作用,相当于Term,是稀疏矩阵的入口,其中4代表行数,5代表列数,7代表7个非零项,通过这个指针可以找到所有行列的头结点。
8.堆栈
首先让我们来讲讲什么叫后缀表达式:
即运算符号在两个数的后面,我们平常用到的表达式为中缀表达式,即运输符号在两个数当中
由图,第一行为中缀,后一行为后缀。
(后缀表达式的读法,从计算符号往前看)。
这里用:
来举个例子。
首先,堆栈时先入后出,后入先出,
就比如上式中,先把计算式按+×24-3/26(上述计算式反过来,从右端输入,输入第一个数为6),一个一个元素加入堆中,再一个一个拿出来计算(例:输入/26后算出3再存回原处,再与后面33-的表达式进行计算,算出结果为0再存储回去,不断更新值),抱住后缀表达式的正确性。
接收到/号时:
接收到-号时:
再输入后面的计算式。
堆栈中,插入数据——入栈。
推出数据——出栈。
数组实现:
#define Maxsize
typedef struct SNode*Stack
struct SNode
{
ElementType Data[Maxsize];
int Top; //元素个数
};
(1).入栈
void Push(Stack Ptrs,ElementType item)
{
if(Ptrs->Top>Maxsize-1)
{
printf("堆栈满");
return;
}else
{
Ptrs->Data[++(ptrs->Top)] = item;
return;
}
}
(2)出栈
ElementType Pop(Stack Ptrs)
{
if(ptrs->Top == -1)
{
printf("堆栈空");
return ERROR;
}else
{
return(Ptrs->Data[(ptrs->Top)--]);
//出栈以后,Top减一
}
}
下面让我们考虑一个问题,用一个数组实现两个堆栈,要求最大的利用数组空间,使数组有空间入栈就可以成功。
显然,如果只是简单对半分组,显然不能有效的利用数组空间,可能存在后一个堆栈满了,而前一个堆栈还剩许多空间的状况。
面对这种情况,我们就要想个比较聪明的方法,从两边往中间堆,当两个堆栈Top相遇时,则数组就满。
操作如下:
#define Maxsize
struct Dstack
{
ElementType Data[MaxSize];
int Top1;
int Top2;
}s;
//其中,s.Top1 = -1 s.Top2 = Maxsize
push操作:
void push(struct Dstack *Ptrs,ElementType item,int Tag)
{
if(Ptrs->Top2-Ptrs->Top1 == 1)
{
printf("堆栈满");
return;
}
if(Tag == 1)//判断用第几个堆栈
{
Ptrs->Data[++(Ptrs->Top1)] = item;
}else
{
Ptrs->Data[--(Ptrs->TOP2)] = item;
}
}
pop操作:
ElementType Pop(struct Dstack *Ptrs,int Tag)
{
if(Tag == 1)
{
if(ptrs->Top1 == -1)
{
printf("堆栈1空");
return NULL;
}else return Ptrs->Data[(Ptrs->Top1)--];
}else
{
if(ptrs->Top2 == Maxsize)
{
printf("堆栈2空");
return NULL;
}else
return Ptrs->Data[(Ptrs->Top2)++];
}
}
(3)单向链表
正如我们所知,堆栈也可用单向链表实现,但是由于堆栈的特点,我们的插入和删除只能在链表的一头操作,并且要在链表的头部操作,在链表的尾部操作时,进行删除操作会找不到前一个结点,所以不可以用链尾操作。
定义结点结构:
typedef struct SNode *Stack;
struct SNode
{
ElementType Data;
struct SNode *Next;
}
建立堆栈头结点:
Stack CreatStack()
{
Stack S;
S = (Stack)malloc(sizeof(struct SNode));
S->Next = NULL;
return S;
}
判断堆栈是否为空:
int ISEmpty(Stack S)
{
return (S->Next == NULL);
}
push操作:
void Push(ElementType item,Stack S)
{
Struct SNode *TmpCell;
TmpCell = (struct SNode *)malloc(sizeof(struct SNode));
TmpCell->Element = item;
TmpCell->Next = S->Next;
s->Next = TmpCell;
}
pop操作:
ElementType Pop(Stack S)
{
struct SNode *FirstCell;
ElementType TopElem;
if(IsEmpty(S))
{
printf("堆栈空");
return NULL;
}
else
{
FirstCell = S->Nest; //去一个变量存储要删除的结点
S->Next = FirstCell->Next; //更改指向的结点为删除结点的Next
TopElem = FirstCell->Element;
free(FirstCell);
return TopElem;
}
}
因为链表空间使动态的,所以不用判断满不满。
中缀表达式:
这里实在打的有点累了,就说明下怎么操作吧。
如图遇到a b c d运算值时进行输出,遇到算术符号时进行堆栈,如图,先是将一个个算术符号进行堆栈存储,再逐步确认优先级,上图中,下一个输入的是“)”,则可以确定到“(”的所有算术符号都可以pop出去,如下:
随后输入的“/”号与“×”号的优先级采取同级左边的优先,则×号也出去,如下:
最后再把/号pop出去计算就好。
最终中缀转后缀操作如下:
1.运算数:直接输出。
2.左括号:压入堆栈。
3.右括号:将栈顶的运算符弹出并输出,直到遇到左括号(出栈,不输出)。
4.运算符:
若优先级大于栈顶运算符时,则把它压栈;
若优先级小于等于栈顶运算符时,将栈顶运算符弹出并输出;再比较新的栈顶运算符,直到该运算符大于栈顶运算符优先级为止,然后将该运算符压栈;
5.若各对象处理完毕,则把堆栈中存留的运算符一并输出。
9.队列(Queue)
队列用通俗的话来讲就相当于我们刷身份证过动车安检,我们排只能从队伍的后方开始排,出去只能从队伍的前方出去。
插入和删除操作:只能在一端插入,而在另一端别除。
Queue CreatQueue( int MaxSize )://生成空队列
int IsFullQ( Queue Q, int MaxSize )://判断队列是否满
void AddQ( Queue Q, ElementType item )://将item加到队列中
int IsEmptyQ(Queue Q )://判断队列是否为空
ElementType DeleteQ( Queue Q )://将队头数据删除
因为队列在两头发生,所有要有一个front和一个rear
定义队列:
#define Maxsize
struct QNode
{
ElementType Data[Maxsize]; //队列数组
int rear; //队尾
int front; //队头
};
typedef struct QNode *Queue;
每次增加一次工作则,Rear++,Front你不变。删除了一个工作以后,Front++,Rear不变。
当遇到如下情况时,我们肯定会想既然是排队,那你再排到队尾去不就好了吗?
所以这就形成了我们所熟知的顺环队列。
如上就是顺环队列,其中也有不少问题让我们思考:
当遇到如上情况时,如果我们再加入一个job,就会使Rear==front跟数列为空的状态冲突了,此时我们就无法分辨队列到底是满了还是空了。
为什么会造成这样的错误呢?
因为,如果我们队列的大小为n时,队列的状态应该有n+1种,但是front和rear的差距只有n种情况,我们用n种情况去衡量n+1种状态怎么可能不出错呢?就等于给你一个标志位i,他只有0 1两种电平,你却要他表示3种状态,这样肯定会出错的。
那我们如何解决这一问题呢?
1.使用额外的标志位表示第n+1种状态,一般是记录操作时插入还是删除,如果rear = front时对应的操作时插入,则就是队列满,反之则是队列空。
2.只使用n-1个数组空间,比如有6个空位我只放5个位,第6位舍弃不用。
当然我们也可以用链表实现队列:
那么问题来了,相比你还记得之前我们遇到的问题,堆栈时,我们用单向链表操作时要选择队头操作还是队尾操作,队列也不例外。
此时,队列也是跟堆栈一样也是只能用队头,不能用队尾,原理相同。
定义结点:
struct Node
{
ElementType Data;
struct Node *Next;
};
定义队列:
struct QNode
{
struct Node *rear;
struct Node *front;
};
typedef struct QNode *Queue;
Queue PtrQ;
出队操作:
ElementType DeleteQ(Queue PtrQ)
{
struct Node *FrontCell;
ElementType FrontElem;
if(PtrQ->front == NULL)
{
printf("队伍空");
return ERROR;
}
FrontCell = PtrQ->front;
if(PtrQ->front==PtrQ->rear)
PtrQ->front = PtrQ->rear = NULL;
else
PtrQ->front = PtrQ->front->Next;
FrontElem = FrontCell->Data;
free(FrontCell);
return FrontElem;
}
目标的坚定是性格中最必要的力量源泉之一,也是成功的利器之一。没有它,天才也会在矛盾无定的迷径中徒劳无功。
数据结构篇(一):_风声向寂的博客-CSDN博客
(2条消息) 数据结构篇(三):_风声向寂的博客-CSDN博客
数据结构篇(四):_风声向寂的博客-CSDN博客
数据结构篇(五):_风声向寂的博客-CSDN博客