从零开始学习 --数据结构(一)

根据B站上自考数据结构课程讲解PPt (勉强吧) 手抄
方便后续复习和重点难点理解
转载需注明
线性表的链式存储结构(二)
单链表上的基本运算
单链表的建立

动态建立单链表的常用方法有两种:一个头插法一个尾插法。
头插法建表,从一个空表开始,重复读入数据,生成新的结点,将读入的数据放到新的结点的数据域中,然后将新的结点插入到当前链表的表头上,直到读入结束标志为止。
LinkList CreateListF()
{
LinkList head;
listNode p;
char ch;
head = NUll;
ch = getchar();
while(ch!="\n")
{
P=(ListNode
)malloc(sizeof(ListNode));
p->data = ch;
p->next =head;
head = p;
ch =getchar();
}
return head;
}

顺序表删除
void DeleteList(SeqList *L,int i)
{
//删除表元素表的第i个元素
int j;DataType x
if(i<1||i>L->length)//判断i值是否合法?当i<1或i>length时,i值不合法。
{
printf(“position error”)
exit(0);//不合法退出
}
DataType x= L->data[i];//保存被删除的元素
for(j =i ;j<=length-1;j++)
L->data[j - 1]=L->data[j];//元素前移
L->length - -;//实际表长减一
return x;//返回被删除的元素
}

单链表的概念
节点的结构:data:next
单链表:head -> 开始节点 -> a2 —>终端节点
链表不同于顺序表,顺序表占用的存储空间是连续的,而链表的各节点占用的存储空间之间可以是连续的,也可以是不连续的;但每个节点的内部占用的存储单元地址必须是连续的,因此链表中节点的逻辑次序和物理次序不一定相同,通过指针来反映节点之间的逻辑关系。

2.单链表数据类型的定义
typedef struct node //节点类型定义
{
DataType data; //节点的数据域
struct node *next; //节点指针域
}ListNode;
typeof ListNode *LinkList;
ListNode *p; //定义一个指向单链表的头指针
ListList head; //定义一个单链表的头指针
p -> data若出现在表达式中,它表示由P所指的链节点的数据域的内容;否则,标识由p所指的那个系欸但的数据域(位置);
p->data =x; p->data = p->data+25;
位置 位置 内容

p->next若出现在表达式中,它表示由p所指的链接点的指针域内容,也就是p所致的链接点的下一个结点的下一个链接点的存储地址;否则,表示由P所指的那个链节点的指针域(位置)。
对于表达是中的p->next->data。表示p所指的链节点的数据域中的信息。

尾插法:将新的结点插入在当前链表的表尾上,因此需要增设一个尾指针rear,使其始终指向链表的尾结点。
LineList CreateListR()
{
LinkList head,rear;
ListNode p;
char ch;
head = NULL, rear =NUll;
ch = getchar();
while(ch!="\n")
{
p = (ListNode
) malloc(Sizeof(ListNode))
p ->data = ch;
if(head = = NULL)
head = p;
else rear->next = p;
rear = p;
ch = getchar();

}

}

带头节点的单链表:作用是可以简化单链表的算法
head - >头节点->a2 开始结点-> a3->anNULL 终端结点

在引入头结点之后,尾插法建立单链表的算法可简化为:
LinkList CreateListRl()//尾插法建立带头结点的单链表算法
{
LinkList head = (ListNode *)malloc(Sizeof(ListNode));
ListNode *p,*r;
DataType ch;
r = head;
while((ch=getchar()) != “\n”)
{
p=(ListNode *)malloc(Sizeof(ListNode));
p->data = ch;
r->next = p;
r = p;
}
r ->next =NULL;
return head;
}

在单链表中查找值为k的结点
算法描述
ListNode * LockateNodek(Linklist head,DataType k)
{
//head 为带头结点的单链表的头指针,k为要查找的结点值
ListNode *p = head ->next;
while(p&&p->next = =k )
p=p->next;
return p;
}

插入运算
将值为X的新结点插入到表的第i个结点的位置上,即插入到ai-1与ai之间。
算法思想:先使p执行ai-1的位置,然后生产一个数据域值为x的新结点*s,再进行插入操作。
head->头结点 ->a1开始结点—>a2->ai-1§->s X—>ai

算法描述
void InsertList(LinkList head,int i,dataType X)
{
ListNode *p,*s;int j;
p = head; j= 0;
while(p!=NUll && j {
p=p->next; ++j;
}
if (p=NUll)
{
printf(“error \n”);return;
}
}
else

删除运算
将链表的第i个结点从表中删去
算法思想:先使P指向ai-1的位置,然后执行p->next指向第i+1个结点,再将i个结点释放掉。

算法描述
DataTypeDeleteList(LinkList head,int i)
{
ListNode *p,*s;
dataType x;int j;
p =head;j =0;
while(p!=NUll&&j<=i-1)
{
p=p->next;
}
if(p=NULL){printf(“位置错误”);exit(0);
}
else{
s = p->next;
p->next =s->next;
x=s->data;
free(s);return x;
}
}

LinkList mynote(LinkList L)
{
if(L&&L->next)
{
q=l;L=l->next;p=l;
S1:while(p->next);p=p->next;
s2:p->next = q;
q->next = NULL;
}
return L;
}

试写一个算法,将一个头结点指针为阿德带头结点的单链表A分解成两个单链表A和B,其中头结点指针分配为a和b,使得A链表中含有 原A链表中的序号奇数的元素,而B链表中含有原链表中序号为偶数的元素,并保持原来的相对顺序。

分析;遍历整个链表A,当遇到序号为奇数的结点时将其连接到A表上,序号为偶数结点时,将其链接到B表中,一直到遍历结束。
void split(LinkList a,LinkList b)
{
ListNode *p,*r,*s;
p = a->next;
r = a ;
s = b;
while(p!= NULL)
{
r->next = p;
r = p;
p = p->next;
if§
{
s->next =p;
s =p;
p =p ->next;
}
}
}

假设头指针为La和Lb的单链表分别为线性表A和B的存储结构,两个链表都是按结点数据值递增有序的。试写一个算法,将这两个单链表合并成为一个有序的链表L.
分析:简历三个指针 Pa、pb、pc,其中pa和pb 分别指向La和Lb表中当前待比较插入的结点,而pc则指向Lc表示当前的最后一个结点,若pa->data<=pb->data,则将 pa所指向的结点链接到pc所指的结点之后,否则将Pb所指向的结点链接到PC所指的结点之后。

linkList MergeL. ist(LinkList La,LinkList Lb)
{
ListNode *pa,*pb,*Pc;
LinkList Lc;
pa=La->next;
pb=Lb->next;
Lc=pc=la;
while(pa!=NULl&&pb!=NULl)
if(pa->data<=pa->data)
pc->next=pa;
pc= pa;
pa = pa->next;
else
pc ->next=pb;
pc = pb;
pb = pb-next;

pc->netx =pa!=NULL?pa:pb;
free(Lb);
return Lc;
}

线性表的链式存储结构(四)
三、循环链表
循环链表与单链表的区别仅在于其尾结点的链域值不是NULl ,而是一个指向头结点的指针。
循环链表的主要优点是:从表中任意一结点出发都能通过后移操作而遍历整个循环链表。
在循环链表中,将头指针改设为尾指针(rear)后,物流是找头结点、开始结点还是终端点都很方便,
它们存储位置分别是rear->next/rear->next->next/rear.这样可以简化基本的操作。

已知有一个结点的数据域为整型,且按从大到小顺序排列的头结点指针L的非空单循环链表,试写一算法
插入一个结点(其数据域为X)直至循环链表的适当位置,使之保持链表的有序性。

分析:要解决这个算法问题,首先就是要解决查找插入位置,方法:使用指针q扫描循环链表,循环条件应该是
(q->data>x&&q!=L),同时使用指针p基类q的直接前驱,已方便进行插入操作。
voidInsertList(LinkList L,int x)
{
ListNode *s,*p,*q;
s=(ListNode)malloc(sizeof(ListNode));
s->data =x;p=l;
q=p->next;
while(p->data>x&&q!=L)
{
p=p->next;
q=p->next;
}
p->data=s;
s-next=q;
}

顺序表和链表的比较
一、顺序表的优缺点
优点:1.空间利用率高 2.可以实现随机存取。
缺点:1.插入删除操作需要大量的移动结点,时间性能差
2.需要事先确定少顺序表的大小,估计小了会发生溢出现象,估计打了又将造成空间浪费。

二、链表的优缺点
有点:1.插入、删除操作中无需移动结点 ,时间性能好。
2.因为动态分配存储空间,空闲空间就不会发生溢出现象
缺点:每个结点都额外设置指针域,空间利用率低

顺序表和链表比较
通过上述比较,可以看出算法的时间性能与空间性能往往是一对矛盾,时间性能的改善往往要以牺牲空间性能为代价,反之亦然。因此在实际中,要根据具体情况来确定采用哪种存储结构。
1.对线性表的操作时经常性的查找运算,以顺序表形式存储为宜。
2.如果经常的运算是插入和删除,以链式存储为宜。
3.对于线性表的结点存储密度问题,也是选择存储结构的一个重要依据。所谓存储密度就是结点空间的利用率。
它的计算公式是:
存储密度=结点数据域所占空间/整个结点所占空间
结点存储密度越大,存储空间的利用率越高。


一、栈的定义及运算
1、栈的定义
栈(stock):是限定在表的一端进行插入和删除运算的线性表,通常将插入、删除的一端称为栈顶(top),另一端称为栈底(bottom)。不含元素的空表称为空栈。
栈的修改是按后进先出的原则进行的,因此栈又称为后进先出(Last in first out)的线性表,简称Lifo表。

2.栈的基本运算
1.置空栈initStack(&S):构造一个空栈S.
2.判栈空StackEmpty(S):若栈S为空栈。则返回tRUE,否则返回FAlSE.
3.判栈满StackFull(S):若栈S为满栈,则返回True,否则返回false。
4.进栈(又称入栈或插入):push(&S,x)将元素X插入s栈栈顶。
5.退栈又称为出栈或删除,Pop(&S):若栈S为非空,则将S的栈顶元素删除。并返回栈顶元素。
6.取栈顶元素GetTop(S):若S栈为非空,则返回栈顶元素,但不改变栈的状态。

二、栈的存储表示和实现
1、栈的顺序存储结构
(1) 顺序栈的概念
栈的顺序存储结构称为顺序栈。顺序栈也是用数组实现的,栈底位置是固定不变的,将栈底位置设置在数组的最低端(即下标为0);栈顶位置是随着进栈和退栈操作而变化的,一个整型量top来只是当前栈顶位置,通常称为top为栈顶指针。

(2)顺序栈的类型描述
#define stackSize 100 //栈空间的大小应该根据实际需要来定义,这里假设为100
typedef char DataType; //datatype的类型可根据实际情况来定义,这里假设为char
typedef Stuct
{
DataType data[stackSize]; //数组data 用来存放表结点
int top; //表示栈顶指针
}SeqStack; //栈的数据类型
Seqstack s; //s为栈类型的变量

2.栈的顺序实现
(1)、置空栈
void InitStack(SeqStack *S)
{
//置空顺序栈。由于C语言数组下标是从O开始,所以栈中元素亦从0开始
//存储,因此空栈时栈顶指针不能是0,而只能是-1
S->top =-1;
}

(2)、判栈空
void StackEmpty(stack *S)
{
return S->top = = -1;//如果栈空则S->top = = -1,返回1,反之,返回0
}

(3)、判栈满
int StackFull(*S)
{
return S->top= = StackSize-1;
//如果栈满则S->top = = StackSize-1的值为1,返回1,反之返回0;
}
(4)、进栈(入栈)
void Push(SeqStack *S,DataType x)
{
if(StackFull(S))
printf(“stack overflow”); //判断函数满
else
{
S->top = S->top +1; //将栈顶指针加1
S->data[S->top]=x; //将x入栈
}
}

(5)退栈(出栈)
DataType Pop(SeqStack *S)
{
if(StackEmpty(S))
{
printf(“Stack underflow”);
exit(0);
}
else
{
return S->data[S->top- -];//返回栈顶元,栈顶指针减1
}
}

(6)取栈顶元素(不改变栈顶指针)
DataType GetTop(SeqStack *S)
{
if(StackEmpty(S))
{
printf(“Stack empty”);
exit(0);
}
else
{
return S->data[S->top];
}
}

另外,同时使用多个栈时,多个栈分配在同一个顺序存储空间内,即让多个栈共享存储空间,则可以相互进行调节,既节约了空间,又可降低发生溢出的频率。当程序中使用两个栈时,可以将两个栈的栈底分别设置顺序存储空间的两端,让两个栈顶各种向中间延伸。

3.栈的链式存储结构
(1)链栈的概念
栈的链式存储结构称为链栈,它是运算受限的单链表,其插入和删除操作仅限制在表头位置上(栈顶)进行,因此不必设置头结点,将单链表的头指针head改为栈顶指针top即可。
(2)链栈的类型定义
typedef stuck stackNode
{
DataType data;
stuck stackNode *next;
}StackNode;定义结点
typedef StackNode *LinkStackl
linkStack top; //定义栈顶指针top

4.链栈的基本运算
判栈空
(1)int StackEmpty()
{
return top = = NULL; //栈空 top = =NUlL的值为1,返回1,否则返回0;
}
(2)进栈(入栈)
LinkStack Push(LinkStack top,DataType x) //将X插入栈顶
{ //无需判满
StackNode *P;
p = (StackNode )malloc(Sizeof(StackNode)); //申请新的结点存储空间
p->data=x; //申请新的结点
p->next =top; //新结点
P插入栈顶
top =p; //更新栈顶指针top
}
第二节 栈的应用举例
一、圆括号匹配的检验
对于输入的一个算是表达式字符串,试写一算法判断其中圆括号是否匹配,若匹配则返回TURE,否则则返回FAlSE.
[分析]利用栈的操作来实现:循环读入表达式中的字符,如遇左括号“(”就进栈;遇右括号“)”则判断栈是否为空,若为空,则返回FALSE,否则退栈;循环结束后再判断栈是否为空,若栈空则说明括号匹配,否则说明不匹配。
算法描述
int Expr()
{
SeqStack S;
DataType ch,x;
InitStack(&S);
ch=getChar();
while(ch!="\n")
{
if(ch=="(")
Push(&S,ch);//遇左括号进栈
else
if(ch==")")
{
if(StackEmpty(&S))
return 0;
else
x=Pop(&S);
ch= getchar();//读入下一个字符
} //end of while
}
if(StackEmpty(&S)) return 1;
else return 0;
}
二、字符串回文的判断
利用顺序栈的基本运算,试设计一个算法,判断一个输入字符串是否具有中心对称,例如ababbaba,abcba都是中心对称的字符串。
分析:从中间向两头进行比较,若完全相同,则该字符串是中心对称的,否则不是。这就要首先球场字符串串的长度,然后将前一半字符入栈。再利用退栈操作将其后一半字符进行比较。

算法描述
int symmetry(char str[])
{
SeqStack S;
int j,k,i= 0;
InitStack(&S);
while(Str[i]!="\0") i++; //求串长度
for(j=0,j {
Push(&s,str[j]); //前一半字符入栈
}
k=(i+!)/2; //后一半字符在串中的起始位置
//特别注意这条命令怎么处理奇偶数个字符的
for(j=k;j if(str[j]!=Pop(&S)
return 0; //有不相同的字符,即不对称
return 1; //完全相同,即对称
}

三、数制转换
将一个非负的十进制整数N转换成d进制
【分析】讲一个非负的十进制整数N转换成d进制的方法:N除以d,求出每一步所得的余数,然后将所有余数逆序书写就是一个十进制整数N对应的d进制数。
void conversion(int N,int d)
{
SeqStack S;
InitStack(&S);
while(N)
{
Push(&S,N%d); //余数入栈
N=N/d;
}
while(!StackEmpty(&S))
{
i =Pop(&S);
printf("%d",i); //余数倒叙出栈
}
}

四、栈与递归
栈还有一个非常重要的应用就是在程序设计语言中实现递归。一个直接调用自己或间接调用自己的函数,称为递归函数。
递归是一个程序设计中强有力的工具,递归算法有两个关键条件:一是又一个递归公式;二是有终止条件。
例如:求n的阶乘可递归地定义为
1 n=0
n! =
n(n-1)! n>0
2阶的Fibinocci树列:
0 n=0
Fib(n)= 1 n=1
fib(n-1)+fib(n-2) n>1

试分析求阶乘的递归函数
long int fact(int n)
{
int temp;
if(n= =0)
return 1;
else
temp = n*fact(n-1);
r12:return temp;
}
void main()
{
long int n;
n =fact(5);
r11:printf(“5!=%1d”,n);
}
n+1 n<1
已知函数 fu(n)= fu(n/2)*f(n/4) n>=2
float fu(int n)
{
if(n<2)
return(n+1); //递归终止条件
else
return fu(n/2)*fu(n/4); //递归公式
}

第三节 队列(一)
一、队列的定义及其运算
1、队列的定义
队列(Queue)是一种操作受限的线性表,它只允许在表的一端进行元素插入,而在另一端进行元素删除。允许插入的一端称为队尾(rear),允许删除的一端称为队头(front)。
新插入的结点只能添加到队尾,被删除的只能是排在队头的结点,因此,队列又称为先进先出(First In First Out)的线性表,简称FIFO表。
某队列初始为空,若它的输入序列为(a,b,c,d),它的输出序列应为(A)。
A. a,b,c,d B.d,c,b,a
C. a,c,b,d D.d,a,c,b

2、队列的基本运算
(1)置空队列InitQueue(Q),构造一个空队列。
(2)判队空QueueEmpty(Q),若Q为空队列,则返回TRUE,否则返回FALSE.
(3)入队列EnQueue(Q,x)),若队列不满,则将返回数据x插入到Q的队尾
(4)出队列DeQueue(Q),若队列不空,则删除队头元素,并返回该元素
(5)取队头GetFront(Q),若队列不空,则返回队头元素

二、顺序循环队列
1、顺序队列的概念
队列的顺序存储结构称为顺序队列。队列的顺序存储结构是利用一块连续的存储单元存放队列中的元素的,设置两个指针front和rear分别指示队头和队尾元素在表中的位置。
设有一顺序队里Q,容量为5,初始状态Q.front=Q.rear=0,画出做完下列操作后队列及其头尾指针的状态变化情况,若不能入队,请简述其理由。
(1)d,e,b入队
(2)d,e出队
(3)i,j 入队
(4) b 出队
(5)n,o,p入队
j j
i i
b b b
e
d

第五步无法进行,因队列已满,再有元素入队,就会溢出,发生假溢。

2.顺序循环队
为了解决顺序队的“假溢”问题采用循环队。约定循环队的队头指针指向队头元素在数组中实际位置的前一个位置,队尾指针指示队尾元素在数组中的实际位置。其次再约定队头指针指示的结点不用于存储队列元素,只起标志作用。当这样队尾指针“绕一圈”后干啥队头指针时视为队满。
例 设Q是一个有11个元素存储空间的顺序循环队列,初始状态Q.front = Q.rear = 0,写出协力操作后头、尾指针的变化情况,若不能入队,请说明理由。
d,e,b,g,h 入队;d,e 出队
i,j,k,l,m入队;b出队;
n,o,p,q,r入队
解答
0 1 2 3 4 5 6 7 8 9 10
初始状态
d,e,b,g,h 入队;d,e 出队 b g h
i,j,k,l,m入队;b出队 g h i j k l m
n,o,p,q,r入队 o p g h i j k l m n
3、循环队的顺序存储的类型定义
#define QueueSize 100
typedef char DataType;
type struct
{
DataType data[QueueSize];
int front rear;
}CirQueue;

4、循环队列基本运算的各算法
(1)置空队列
void IntQueue(CirQueue *Q)
{
Q->front = Q->rear = 0;
}
(2)判队空
int QueueEmpty(CirQueue *Q)
{
return Q->front = = Q->rear;
}
(3)判队满
int QueueFull(CirQueue Q)
{
return (Q->rear+1)%QueueSize= = Q->ftont;
}
(4)入队列
int EnQueue(CirQueue Q,DataType x)
{
if(QueueFull(Q))
printf(“overflow”);
else
{
Q->data[Q->rear]=x;
Q->rear=(Q->rear+1)%QueueSieze; //循环意义下的加1
}
}
(5)取队头元素
DataType GetFront(Ci rQueue
Q)
{
//获取Q的队头元素值
if(QueueEmpty(Q))
{
printf(“Queue empty”);
exit(0);
}
else
{
return Q->data[Q->front];
}
}
(6)出队列
DataType DeQueue(CirQueue
Q)
{
DataType x;
if(QueueEmpty(Q))
{
printf(“Queue empty”);
}
}

例:
设栈S={1,2,3,4,5,6,7},其中7为栈顶元素,请写出调用函数Algo(S)后栈S的状态。其函数为:
void Algo(SeqStack S)
{
int i = 1;
CirQueue Q;SeqStack T; //定义一个循环队列和一个栈
InitQueue(&Q);Initstack(&T); //初始化队列和栈
while(!StackEmpty(&S))
{
if(i% 2 != 0)
Push(&T,Pop(&S)); //栈中序列号为奇数的元素入T栈中
else //7、5、3/1顺序入栈T
EnQueue(&Q,Pop(&S));
i++; //序号为偶数的元素入队列Q中
} //6、4、2顺序入队Q
while(!QueueEmpty(&Q))
push(&S,DeQueue(&Q));
while(!StackEmpty(&T)) //队Q中6、4、2顺序出队并入栈S
Push(&S,Pop(&T)); //队栈T中按1、3/5/7顺序出栈并入栈S
}
S中元素(6,4,2,1,3,5,7)

三、链队
1、链队的定义
队列的链式存储结构称为链队列。它是一种限制在表头删除和表尾插入操作的单链表。
2、链队的数据类型定义
type struct qnode
{
DataType data;
struct qnode *next;
}QueueNode;
typedef struct
{
QueueNode *front; //队头指针
QueueNode *rear; //队尾指针
}LinkQueue;
LinkQueue Q;
Q.front Q->头结点 -> a1队头结点-> a2 ->… -> 队尾结点an^
Q.rear -------------------------------------->

3.链队的基本运算实现
(1)构造空队列
Q->front Q–>头结点^
Q->rear —>
void InitQueue(LinkQueue Q)
{
Q->front = (QueueNode
)malloc(sizeof(QueueNode)) //申请头结点
Q->rear=Q->front; //尾指针指向头结点
Q->rear->next =NUll;
}

(2)判队空
int QueueEmpty(LinkQueue *Q)
{
return Q->rear = = Q->front; //头尾指针相等队列为空
}
(3)入队列
Q->front Q->头结点 ->队头结点a1-> a2—>…an__> p
Q->rear - - - - - - - - - - - - - - - - - - - -X^
void EnQueue()

(5)出队列
链队列出队列操作有两种不同情况要分别考虑。
1、当队列的长度大于1时,则出队操作只需要修改头结点指针域即可,尾指针不变,类似于单链表删除首结点,操作步骤:
S=Q->front ->next;
Q->front ->next =s->next;
x=s->data;
free(s);return x; //释放队头结点,并返回其值

2若队列长度等于1,则出队时不仅要修改头及诶点指针域,而且还需要修改尾指针。
Q->front Q->头结点->ai^ Q->front Q->头结点^
Q->rear ----- - --> Q->rear————>
出队前 出队后
s = Q->front->next;
Q->front->next=NULL; //修改队头指针
Q->rear=Q->front; //修改队尾指针
x=s->data;
free(s);return x; //释放队头结点,并返回其值

为了方便,直接删除头结点,将原队头结点当头结点使,这样算法就简单了。
Q.front Q—>a2头结点–>队头结点---->…—>队尾结点 a2^
Q.rear - - - - - - - - - - - - - - - - - - - -->
链队列的出队算法描述
DataType DeQueue(LinkQueue *Q)
{ //删除链队的头结点,并返回头结点的元素值
QueueNode *P;
if(QueueEmpty(Q))
{
printf(“Queue underflow”);
exit(0); //出错退出处理
}
else
{
p=Q->front;
Q->front = Q->front ->next;//P指向头结点
free§; //删除是否的原头结点
return(Q->front->data); //返回原队头结点的数据值
}
}

真题选择,算法f31的功能是清空带头结点的链队列Q,请在空缺处填入合适内容,使其成为一个完整的算法。
typedef struct node
{
DataType data;
struct node *next;
}QueueNode;
typedef struct
{
QueueNode *front;
QueueNode *rear;
}LinkQueue;
void f31(LInkQueue *Q)
{
QueueNode *P,*S;
p =(1);
while(P!=NULL)
{
s=p;
p=p->next;
free(s);
}
----2---- =null;
Q->rear= --3—;
}
(1) Q->front ->next (2)Q->front->next (3)Q->front
4、循环队列
<--------- – - – - - — - - - - - - - - – -
头结点—>a1队头结点—>a2->…—>an队尾结点
rear---->

循环链队的类型定义
typedef struct queuenode
{
Datatype data;
struct queuenode *next;
}QueueNode;
QueueNode *rear;
(1)初始化空队列
<-- - - -
rear-----> 头结点————>

QueueNode *InitQueue(QueueNode *rear)
{
tear=(QueueNode *)malloc(sizeof(QueueNode));
rear ->next =rear;
return rear;
}

11.24
(2)入队列
< —— – -- ---------- - - - - - - - - s
头结点 -->a1队头结点—>a2—>…an队尾结点—>x
rear- - >
void EnQueue(QueueNode *rear DataType x)
{
QueueNode *s = (QueueNode *)malloc(sizeof(QueueNode));
s->data=x;
s->next = rear->next;
rear->next=s;
rear = s;
}
(3)出队列
<----- - - - - - - - - – - - - - -
s->头结点 ->t->a1 新头结点 —>a2---->…—> an 队尾结点
rear—>
DataType DeQueue(QueueNode *rear)
{
QueueNode *s,*t;
DataType x;
if(rear->next = = rear) //判队空
{
printf(“Queue Empty”);
exit(0);
}
else
{
s=rear->next; //s指向头结点
rear ->next =s->next; //删除头结点
t =s ->next; //t指向原队头结点
x =t->data; //保存原对头结点
free(s); //释放被删除的原头结点
return x; //返回出队结点的数据
}
}

第三节 队列(二)
四、栈和队列的应用实例
1、中缀表达式和后缀表达式
中缀表达式:9-(2+47)/5+3- - - - 符合人的习惯
后缀表达式:9247
+5/-3+ - – - 适合计算机操作的特点
后缀表达式特点:
(1)从左向有数字得顺序与中缀表达式完全相同
(2)从左向右运算符的顺序与中缀表达式的计算顺序完全相同;
(3)后缀表达式没有括号,运算规则简单
2.中缀表达式到后缀表达式的转换
中缀表达式转换为后缀表达式的算法思想:
顺序扫描中缀算术表达式:
(1)当读到数字时,直接将其送至输出队列中;
(2)当读到运算符时,将栈中所有优先级高于或等于该运算符的运算符弹出了,送至输出队列中,再将当前运算符入栈;
(3)当读入左括号时,即入栈;
(4)当读到右括号时,将靠近栈顶的第一个左括号上面的运算符全部依次弹出,送至输出队列中,再删除栈中的左括号。

中缀表达式:
9-(2+4*7)/5+3转换后缀表达式的过程:
从零开始学习 --数据结构(一)_第1张图片

【算法描述】
int Priorty(DataType op)
{
switch(op)
{
case ‘(’:
case ‘#’:return 0;
case ‘-’:
case ‘+’:return 1;
case ‘*’:
case ‘/’:return 2;
}
return -1;
}
void CTPostExp(CirQueue Q)
{
SeqStack S;
char c,t;
InitStack(&S);
Push(&S,"#");
do
{
c=getchar();
switch©
{
case’ ‘:break; //去除空格
case ‘0’:
case ‘1’:
case ‘2’:
case ‘3’:
case ‘4’:
case ‘5’:
case ‘6’:
case ‘7’:
case ‘8’:
case ‘9’:
case ‘(’:
case ‘)’:
case ‘#’: //右括号和#
do
{
t=Pop(&S);//出栈
if(t!=’(’&&!=’#’)EnQueue(Q,t); //如果是运算符则入队
}while(t!=’(’&&S.top!=-1); break;
//直到左括号出栈或栈空循环结束
case’+’:
case’-’:
case’
’:
case’/’:
while(Prinority©<=Priority(GetTop(s))) //当前运算符优先级小于等于栈顶运算符优先级 {
t=top(&S);EnQueue(Q,t); //运算符出栈,入队
}
Push(&S,c);break; //当前运算符入栈
} //ENDswitch
}
while(c!=’#’); //以’#'号结束表达式扫描
}

从零开始学习 --数据结构(一)_第2张图片

【算法描述】
int CPostExp(SeqQueue Q) //后缀表达式再队列Q中
{
SeqStack1 S; // S是顺序栈
char ch;int x,yl
Initstack1(&S); //初始化栈S
while(!QueueEmpty(Q))
{
ch=DeQueue(&Q); //出队
if(ch>=‘0’&&ch<=‘9’) //是1~9
Push1(&S,ch-‘0’); //入栈
else
{
y=Pop1(&S);
x=Pop1(&S);
switch(ch)
{
case’+’;Push1(&S,x+y);break;
case’-’:Push1(&S,x-y);break;
case’’:Push1(&S,xy);break;
case’/’:Push1(&S,x/y);break;
}
}
}
return GetTopl(S);
}

真题选择 阅读下列算法,并回答问题:
(1)假设栈S=(3,8,6,2,5),其中5为栈顶元素,写出执行函数f31(&S)后的S;
(2)简述函数f31的功能。
void f31(Stack *S)
{
Queue Q;InitQueue(&Q);
while(!StackEmpty(S))
EnQueue(&Q,Pop(&S));
while(!QueueEmpty(Q))
Push(&S,DeQueue(&Q));
}
(1)S(5,2,6,8,3)其中3为栈顶元素。
(2)函数f31的功能是将栈S中的元素倒置。

本章小结
本章重点介绍了顺序栈、链栈、循环队列及链队列的存储结构及基本运算,要求考生能熟练掌握。本章的难点是理解递归算法执行过程中栈的状态变化过程以及循环队列对边界条件的处理问题。
在本章中介绍了多个利用队列或栈设计算法解决简单应用问题的实例,特别是表达式求值问题的例子,它是一个综合的应用实例,几乎用到栈和队列的各种算法。通过这些例子,考试可以更好地理解栈和队列的特点和应用,增强分析问题解决问题的能力。
从历年考试情况来看,本章主要考顺序栈,链栈,循环队列及链队列的存储结构及其基本运算符,以选择题、填空题为主,有时有简单题或算法阅读理解题。

第一节 多维数组和运算符
一、多维数组的定义
1、一维数组
是一种元素个数固定的线性表。
2、多维数组
是一种复杂的数据结构,可以看成是线性表的推广,一个n维数组可以视为其数据元素为n-1维数组的线性表。

二、数组的顺序存储
通常采用顺序存储结构来存放数组。对二维数组可能有两种存储算法:一种是以行序为主序的存储方式,另一种是以列序为主序的存储方式。在C语言中,采用以行序为主序的存储。
(1)对于C语言的二维数组A[m][n],下标从0开始,假设一个数组元素占d个存储单元,则二维数组中任一元素a[i][j]的存储位置可由下式确定:
loc(A[i][j])=loc(A[0][0])+(i*n+j)*d

真题选解
例题.单选题 二维数组A[4][5]按行优选顺序存储,若每个元素占2个存储单元,且第一个元素A[0][0]的存储地址为1000,则数组元素A[3][2]的存储地址为(c)
A.1012 b.1017 c.1034 d.1036

(2)对于C语言的三维数组A[m][n][p],下标从0开始,假设一个数组元素占d个存储单元,则三维数组中任一元素a[i][j][k]的存储位置可由下式确定:
loc(A[i][j][k])=loc(A[0][0][0])+(inp+j*p+k)*d

例题.单选
三维数组A[4][5][6]按行优先存储方法存储在内存中,若每个元素占2个存储单元,且数组中第一个元素的存储地址为120,则元素
A[3][4][5]的存储地址为(b)
A.256 b.358 c.360 d.362
解析:A[4][5][6]表示它公有4片,每片有5行,每行有6个元素。元素A[3][4][5]处在3片4行5列上,是三维数组的最后一个元素。按行优先存储方法存储时,它前面有三个完成的片,每片有56个元素。3片有356个元素…
loc(A[3][4][5])=loc(A[0][0][0])+(3
56+46+5)*2=120+238=358

三、数组运算举例
设计一个算法,实现矩阵Amn的转置矩阵Bnm.
分析 对于一个mn的矩阵A,其转置矩阵是一个nm的矩阵B,而且B[i][j]=A[j][i],0<=i<=n-1,0<=j<=m-1.假设m=5,n=8.
算法描述
void trsmat(int a[][8],int b[][5],int m,int n)
{
int i,j;
for(j=0;j for(i=0;i b[i][j]=a[j][i]
}

例:如果矩阵A中存在这样的一个元素A[i][j],满足:A[i][j]是第i行元素中最小值,且又是第j列元素中最大值,则称此元素为该矩阵的一个马鞍点。假设以二维数组存储矩阵Am*n,试编写求出矩阵中所有马鞍点的算法。
分析 算法思想:先求出每行中的最小值元素,存入数组Min[m]之中,再求出每列的最大值元素,存入数组Max[n]之中。若某元素即在Min[i]中又在Max[j]中,则该元素A[i][j]就是马鞍点,找出所有这样的元素。
算法秒杀
void MaxMin(int A[4][5],int m,int n)
{
int i,j;
int Max[5],Min[4];
for(i=0;i {
Min[]=A[i][0]; //先假设第i行第一个元素最小,然后再与后面的元素比较
for(j=1;j if(A[i][j] Min[i]=A[i][j];
}
for(j=0;j {
Max[j]=A[0][j]; //假设第j列第一个元素最大,然后再与后面的元素比较
for(i=1;i if(A[i][j]>Max[j])
Max[j]=A[i][j];
}
for(i=0;i for(j=0;j if(Min[i]= =Max[j]) //判断是否为马鞍点
printf("%d,%d",i,j); //显示马鞍点
}

真题选解
例题.算法阅读题:
void f30(int A[],int n)
{
int i,j,m;
for(i=1;i for(j=0;j {
m=A[in + j]; //A[in+j]与A[jn+i]值的交换
A[i
n+j]=A[jn+i];
A[j
n + i]= m;
}
}
回答下列问题:
1 2 3
(1)已知矩阵B= 4 5 6 ,将其按行优先存于一维数组A中,给出执行函数调用f30(A,3)后矩阵B的值;
7 8 9
(2)简述函数f30的功能。

(1) 解析 数组A 1 2 3 4 5 6 7 8 9
循环过程
外层循环 内层循环 操作
i = 1 j=0 A[3]=A[1]
i = 2 j=0 A[6]=A[2]
j=1 A[7]=A[5]
执行后的数组A
数组A 1 2 3 4 5 6 7 8 9
A[0] A[1] A[2] A[3] A[4] A[5] A[6] A[7] A[8]
答案
1 4 7
B= 2 5 8
3 6 9
(2)函数f30的功能是求解矩阵的转置

第二节 矩阵的压缩存储
一、特殊矩阵
特殊矩阵:是相同值的元素或者零元素在矩阵中的分布有一定规律的矩阵。

1、对称矩阵
若n阶方阵A中的元素满足下述性质:
aij=aji(0<=i,j<=n-1)
则称A为n阶的对称矩阵。
对于一个n阶的对称矩阵,可只存储其下三角矩阵:
a00
a10 a11
a20 a21 a22
… … …
ano an-11 … an-1n-1
将元素压缩存储到1+2+3+…+n=n(n+1)/2个元素的存储空间中,假设以一维数组sa[n(n+1)/2] 作为n阶对称矩阵A的存储结构,以行序为主序存储其下三角(包括对角线)中的元素,数组M[k]和aij的对应关系:
1+2+3+…+i+j=i(i+1)/2+j 当i>=j;
k
j(j+1)/2+i 当i

真题选解
设有一个10阶的对称矩阵A,采用行优先压缩存储方式,a11为第一个元素,其存储地址为1,每个元素占一个字节空间,则a85的地址为(33)

压缩矩阵的实现
算法描述
void matrlxmult(int a[],intb[],int[c][20],int n)
{ //n为A,B矩阵的下三角元素个数,a,b分别是一维数组
//存放矩阵a和b的下三角元素值,c存放a和b的乘积
for(i=0;i<20;i++)
for(j=0;j<20;j++)
{
s=0;
for(k=0;k {
if(i>=k) //表示元素为下三角的元素,计算在a数组中的下标
L1=i*(i+1)/2+k;
else //表示元素为上三角的元素,计算下标
L1=k*(k+1)/2+i;
if(k>=j) //表示元素为下三角的元素,计算在b数组中的下表
L2=K*(k+1)/2+j;
else
L2=j*(j+1)/2+k;
s=s+a[L1]*b[L2]; //计算矩阵乘积
}
c[i][j]=s;
}
}

2.三角矩阵
a00 c c c c a00 a01 a02 … a0n-1
a10 a12 c c c c a11 a12 … a1n-1
a20 a21 … …c c c c a22 … a2n-1
2… … … … c c … … … …
an-10 an-11 an-12 … an-1n-1 c … … c an-1n-1

下三角矩阵 上三角矩阵

下三角矩阵的主对角线上的方均为常数c或零;上三角矩阵是指矩阵的下三角(不包括对角线)中的元素均为常数c或是零的n阶方阵。
一般情况下,三角矩阵的常数c均为零。
三角矩阵可压缩存储到数组M[n(n+2)/2+1]中。
上三角矩阵中,主对角线上的第i行有n-i+1个元素,以行序为主序存放,M[K]和aij的对应关系是:
n+n-1+n-2… = i(2n-i+2)/2+j-i 当i<=j
k =
n(n+1)/2 当 i>j

下三角矩阵中,以行序为主存放,与对称矩阵类是,M[k]和aij的对应关系是:
i(i+1)/2 当i>=j
k =
n(n+1)/2 当i

真题选择
1、假设一个10阶的上三角矩阵A按行优先顺序压缩存储在一维数组B中,若矩阵中的第一个元素a11在B中的存储位置k=0,则元素a55在B中的存储位置K _______34 .

11.25
二、稀疏矩阵
1、稀疏矩阵的定义
含有大量的零元素且零元素分布没有规律矩阵称为稀疏矩阵。
2、三元组表
(1)三元组表的含义:一个稀疏矩阵可用一个三元组线性表表示,每个三元组元素对应稀疏矩阵中的一个非零元素,包含有该元素的行号、列号和元素值。每个三元组表中是按照行号值的升序为主序、列号值的升序为辅序(即行号值相同再按列号值顺序)排列的。

画出下列稀疏矩阵对应的三元组表
0 0 1 0
0 5 0 0
0 0 0 0
-4 0 0 -7

解析 根据签名的三元组的含义很容易画出该矩阵的三元组表
i j v
0 2 1
1 1 5
3 0 -4
3 3 -7

(2)三元组表的类型定义
#define Maxsize 1000 //假设非零元素个数最大为1000个
typedef struct
{
int i,j; //非零元素的行号、列号(下标)
DataType v; //非零元素值
}TriTupleNode;
typedef struct
{
TriTupleNode data[Maxsize]; //存储三元组的数组
int m,n,t; //矩阵的行数、列数和非零元素个数
}TSmatrix; //稀疏矩阵类型

例 试写一个算法,建立顺序存储稀疏矩阵的三元组表。
假设A为一个稀疏矩阵,其数据存储在二维数组a中,b为一个存放对应于A矩阵生成的三元组表。在这个算法中,要进行二重循环来判断每个矩阵元素是否为零,若不为零,则将其行,列下标及其值存入b中。

算法描述
void CreateTriTable(TSMatrix *b,inta[][5],int m,int n)
{//建立稀疏矩阵的三元组表
int i,j,k=0;
for(i=0;i for(j=0;j if(a[i][j]!=0)
{
b->data[k].i=i;
b->data[k].j=j;
b->data[k].v=a[i][j];
k++;
}
b->m=m;b->n=n; //记录矩阵的行数
b->t=k; //保存非零个数
}

例 试写一个算法,实现以三元组表结构存储的稀疏矩阵的转置运算。
对于一个mn的矩阵M,它的转置矩阵T是一个nm的矩阵,而且Mij=Tji(0<=i

0	3	0	5	0				0	0	1	0
0	0	-2	0	0				3	0	0	0

M 1 0 0 0 -6 T 0 -2 0 8
0 0 8 0 0 5 0 0 0
0 0 6 0
稀疏矩阵M和它的转置矩阵T
i j v
0 0 1 3
1 0 3 5
2 1 2 -2
3 2 0 1
4 2 4 6
5 3 2 8

i	j	v

0 0 2 1
1 1 0 3
2 2 1 -2
3 2 3 8
4 3 0 5
5 4 2 6

(1)一般的转置算法
算法思想 对M中的每一列col(0<=col<=a.n-1)从头至尾依次扫描三元组表,行号和列号互换后再依次存入b->data中就可以得到T的按行优先的三元组表。
算法描述
void TransMatrix(TSMatrix a,TSMatrix b)
{
//a和
b是矩阵M/T的三元组表表示,求稀疏矩阵M的转置T
int p,q,col;
b->m=a.n;b->n=a.m; //M和T行列数互换
b->t=a.t; //赋值非零元素个数
if(b->t<=0)
printf(“M中无非零元素!”)
else
{
q=0;
for(col=0;col for(p=0;p if(a.data[p].j==col) //找与col相等的三元组表
{
b->data[q].i=a.data[p].j;
b->data[q].j=a.data[p].i;
b->data[q].v=a.data[p].v;
++q;
}
}
}
算法分析 该算法的时间复杂度为O(nt),即与稀疏矩阵M的历史和非零元素个数的乘积成正比。一般的矩阵转置算法的时间复杂度为O(mn).该算法仅适用于非零元素个数t远小于矩阵元素个数m*n的情况。

(2)快速转置算法
算法思想 创建两个数组num和rownext。num[j]存放矩阵第j列商非零元素个数,rownext[i]代表转置矩阵第i行的下一个非零元素在b中的位置。
算法描述
void FaStTran(TSMatfix a,TSMatrix *b)
{
int col,p,t,q;
int *num,*rownextl
num=(int *)calloc(a.n+1,4); //分配n+1个长度为4的连续空间
rownext=(int *)calloc(a.m+1,4); //分配m+1个长度为4的连续空间
b->m=a.n;b->n=a.m;b->t=a.t;
if(b->t)
{
for(col=0;col num[col]=0; //初始化
for(t=0;t ++num[a.data[t].j]; //计算每列非零元素数
rownext[0]=0;
for(col=1;col rownext[col]=rownext[col-1]+num[col-1];
for(p=0;p {
col=a.data[p].j;
q=rownext[col];
b->data[q].i=a.data[p].j;
b->data[q].j=a.data[p].i;
b->data[q].v=a.data[p].v;
++rownext[col]; //下次再有该行元素,起始点就比商一个加了1
}
}
}
算法分析 算法的时间复杂度为0(t)

3、带行表的三元组表
带行表的三元组表:又称为行逻辑连接的顺序表。在按行优先存储的三元组表中,增加一个存储每一行的第一个非零元素在三元组表中位置的数组。
类型描述
typdef struct
{
TriTupleNode data[MaxSize];
int RowTab[MaxRow]; //每行第一个非零元素的位置表
int m,n,t;
}RLSMatrix;
带行表的三元组表的特点:
1.对于任给行号i(0<=i<=m-1),能迅速地确定该行的第一个非零元在三元组表中的存储位置为RowTab[i]
2.RowTabi表示第i行之前的所有行的非零元素
3.第i行商的非零元数目为RowTab[i+1]-RowTabi
4.最后一行即第m-1行的非零元数目为t-RowTabm-1

注意
若在行表中令RowTab[m]=t(要求MaxRow>m)会更方便些,且t可省略
带行表的三元组表可改进矩阵的转置算法,具体参阅其他参考书。

真题选解
1、对于下列稀疏矩阵(注矩阵元素的行列下标均从1开始)
0 0 0 0 0
0 7 -1 0 0
-8 0 5 0 0
0 0 0 0 0
0 0 6 -2 9

(1)画出三元组表
(2)画出三元组表的行表
答案
(1)三元组表
i j v
0 2 2 7
1 2 3 -1
2 3 1 -8
3 3 3 5
4 5 3 6
5 4 4 -2
6 5 5 9

(2)三元组行表
0 0 2 2 4

第三节 广义表基础
一、广义表的定义
广义表是线性表的推广。即广义表中放松对表元素的原子限制,容许它们具有其自身结构。
1、广义表定义
广义表是n(n>=0)个元素a1,a2…ai,…an的有限序列。
其中;
(1)ai- - 或是原子或是一个广义表
(2)广义表通常记作:
Ls=(a1,a2,a3…ai,…an).
(3)Ls是广义表的名字,n为它的长度
(4)若ai是广义表,则称它为Ls字表
(5)一个表展开后所含括号的层数称为广义表的深度
注意:
(1)广义表通常用圆括号括起来,用逗号分隔其中的元素
(2)为了区分原子和广义表,书写时用大写字母表示广义表,用小写字母表示原子
(3)若广义表Ls非空(n>-1),则a1是Ls的表头(head),其余元素组成的表(a1,a2…an)称为Ls的表尾(tail).
(4)广义表是递归定义的


(1)A=()–A是一个空表,其长度为零,深度为1.
(2)B=(a)- - B是一个只有一个原子的广义表,其长度为1深度为1
(3)C=(a,(b,c))- - C是一个长度为2的广义表,第一个元素是原子,第二个元素是字表,深度是2
(4)D=(A,B,C)=((),(a),(a,(b,c)))- - D是一个长度为三深度为三的广义表、
(5)E=(C,d)=((a,(b,c)),d)- - E是一个长度为2的广义表,第一个元素是子表,第二个元素是原子,深度为3
(6)F=(e,F)=(e,(e,(e,…)))- - 是一个递归的表,它的长度是2,第一个元素是原子,第二个元素是表自身,展开后它是一个无限循环的广义表,深度为正无穷。

2、广义表的几个重要性质:
(1)广义表的元素可以是字表,而字表又可以含有字表,因此广义表是一个多层次结构的表,它可以用图形象的表示。
从零开始学习 --数据结构(一)_第3张图片

(2)广义表具有递归和共享的性质,例如,表F就是一个递归的广义表,D表示共享的表,在表D中可以不必李处子表的值,而是通过字表的名字来引用。

二、广义表的基本运算
广义表是对线性表和树的推广,广义表的大部分运算与这些数据结构上的运算类似。因此,只讨论广义表的两个特殊的基本运算:
取表头head(Ls)和取表尾tail(Ls).任何一个非空的广义表其表头可能是原子,也可能是字表,二其表尾一定是子表。
例已知有下列的广义表,试求出下面广义表的表头head()/表尾tail()、表长length()和深度depth()。
(1)A=(a,(b,c,d),e,(f,g)) (2)B=((a));
(3)C=(y,(z,w),(x,(z,w),a)); (4)D=(x,((y),b),D)。
(1)head=a tail= length=4 depth=2
(2)head=(a) tail= length=1 depth=2
(3)head=y tail= length=3 depth=3
(4)head=x tail=D length=3 depth=无穷
在这里插入图片描述


取出广义表A=((x,y,z),(a,b,c,d))中原子b的函数是 ______.tail(head(tail(A)))

三、广义表的存储结构
1、结点结构
tag data/slink link
说明:(1)tag标志位,tag=1,该结点是字表,第二个域为slink,用以存放字表的地址;当tag=0时,该结点是原子结点,第二个域为data。用以存放元素值。
(2)link域是用来存放与本元素同一层的下一个元素对应结点的地址,当该元素是所在层的最后一个元素是,link的值为NULL.

A=()
A=NULL
B=(a)
B----->0 a ^
c=(a,(b,c))
D(A,B,C)=((),(a),(a))

在这里插入图片描述

本章小结
前两张讨论的是线性表、栈和队列都是典型的线性结构,结构中数据元素都是不能分解的非结构的原子类型。他们的逻辑特征是:每个数据元素至多有一个直接前趋和直接后续。而多维数组和广义表是一种复杂的线性结构,他们的逻辑特征是;一个数据元素可能有多个直接前驱和直接后继。
多维数组可以看成是线性表的推广。因为一旦确定数组是按行或案列优先顺序存储之后,每个数组元素之间的关系就同一位数组一样变成线性的了。
因此,只要弄清楚多维数组按行优先顺序存储结构之后,它的运算就同线性表运算类似。
本章主要介绍数组的逻辑结构特征及其存储方法、特殊矩阵和稀疏矩阵的压缩存储方法,压缩存储特殊矩阵和稀疏聚餐的各种运算及应用。以及广义表的逻辑结构,表的表头及表尾的求解。

你可能感兴趣的:(队列,链表,stack)