《数据结构基础》讲义1.docx
《《数据结构基础》讲义1.docx》由会员分享,可在线阅读,更多相关《《数据结构基础》讲义1.docx(37页珍藏版)》请在冰豆网上搜索。
![《数据结构基础》讲义1.docx](https://file1.bdocx.com/fileroot1/2023-1/7/35c9b0ca-299c-4809-9181-524df8b0c669/35c9b0ca-299c-4809-9181-524df8b0c6691.gif)
《数据结构基础》讲义1
《数据结构基础》讲义
第一章绪论
1.1基本概念和术语
数据:
是信息的载体。
凡是能够被计算机进行处理的对象都是数据。
例如:
整数、字符、声音、图象等都是数据。
数据有三个层次:
数据、数据元素、数据项。
数据元素是数据的基本单位,它有完整独立的意义。
在《数据结构》课程中有时也把数据元素称做结点、记录、顶点。
一个数据元素可由一个或多个数据项组成。
数据项是具有独立含义的数据最小可命名单位。
有时也把数据项称做域、字段等。
例如在学生档案管理中,一个学生的信息就是数据元素,而学号、姓名、年龄等就是数据项。
什么是数据结构?
数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
《数据结构》是一门什么样的课程呢?
数据的逻辑结构:
只是抽象地描述数据元素间的逻辑关系,而不管其在计算机中的存储表示方式。
分为线性结构和非线性结构。
在《数据结构》中数据的逻辑结构有4种:
线性表、树、图、集合。
线性结构的特点:
数据一个接一个的排列,如果有前趋或后继就只能有一个。
非线性结构就不具备上述特点。
数据的物理结构:
是数据的逻辑结构在计算机存储器中的实现。
也称做存储结构。
在《数据结构》中数据的物理结构有4种:
顺序存储、链式存储、索引存储、散列存储。
数据的运算:
是定义在数据的逻辑结构上的,但运算的具体实现要在存储结构上进行。
数据的运算是数据结构的一个重要方面。
1.2算法的描述和分析
一个算法应该具有下列特性:
(1)有穷性
(2)确定性(3)可行性(4)输入(5)输出
判断一个算法的好坏,主要有以下几个标准:
1)正确性2)可读性3)健壮4)效率
在算法正确的前提下,执行时间和所占空间的分析,是对算法进行评估和选择的重要依据。
常见的时间复杂度,按数量级递增排列有:
常数阶O
(1)、对数阶O(log2n)、线性阶O(n)、线性对数阶O(nlog2n)、平方阶O(n2)、立方阶O(n3)、…、k次方阶O(nk)、指数阶O(n2)。
以空间复杂度作为算法所需存储空间的耗费的度量,记为S(n)=O(f(n))。
具体实际例题可看4-5页的例1-5。
第二章线性表
线性表是最简单、最常用的一种数据结构。
主要的存储结构有两种:
顺序和链式。
2.1线性表的定义和运算
2.1.1线性表的定义
线性表是n(n≥0)个数据元素的有限序列。
n是线性表的长度。
线性表通常记做
(a1,a2,……,an),其中a1是第一个元素,an是最后一个元素。
用图表示:
an
从图中可以看出,a1只有后继没有前驱,an只有前驱没有后继,ai既有前驱又有后继。
有前驱和有后继的只能有一个。
这是线性表的最重要的特点。
26个英文字母,是线性表,学生的成绩单也是线性表。
2.1.2线性表的运算
在线性表上常用的运算有:
(1)存取
(2)插入(3)删除(4)查找(5)合并(6)分拆(7)排序(8)求表长
2.2线性表的顺序存储结构
2.2.1顺序表
线性表最简单的存储方法,是用一组地址连续的存储单元依次存放线性表中数据元素,简称顺序表。
线性表中第i个元素的存储地址为:
LOC(ai)=LOC(a1)+(i-1)这是一个数据占一个单元。
LOC(ai)=LOC(a1)+(i-1)×m这是一个数据占m个单元。
顺序存储结构的特点是:
逻辑上相邻的数据元素,物理存储时也相邻。
从上面的公式可以看出,要访问第i个元素,就可以直接算出ai的地址LOC(ai)。
所以顺序表能随机存取表中的任一元素。
就是说,数据元素在顺序表中的位置取决于该元素在线性表中的顺序号。
在C语言中可以用以下方式定义一个顺序表:
#defineMAX100
datatypedata[MAX];
intn;
其中datatype表示数据元素的类型。
data[MAX]是一维数组,用来存放顺序表中的n个数据元素。
n是顺序表中数据元素的个数。
这种定义方式有缺点,因为数组data[MAX]与n是孤立的,没有反映出数组data[MAX]与n的内在联系。
可以用以下方式进行定义:
#defineMAX100
typedefstruct{datatypedata[MAX];
intn;}Slist;
Slisty1,y2,y3;
Y1.data[2]=15;y1.n=20;
注意:
以后我们定义的许多算法都作为函数来处理。
在C语言中,函数调用参数的传递是值的单向传递。
函数的计算结果是不能带回来的。
因为顺序表就是数组,为了将函数的计算结果带回来,应该将传递的参数改为指针。
例如将voidf(SLists,…)改成以下的语句定义形式:
voidf(SList*p)
在主函数中,采用以下语句调用f函数:
SLists1;f(&s1);
2.2.2插入
线性表的插入运算,是指在第i个数据元素之前插入一个新的数据元素x,长度为n的线性表变为长度为n+1的线性表。
除了将x插入到表尾,否则应该将第n个到第i个数据元素依次往后移动一个位置。
共需移动n-i+1个数据元素。
最后将存入a[i-1]中。
算法如下:
intinsertlist(SList*p,intx,inti)/*插入函数,SList*p是指向线性表的指针,x是要插入的数据元素,i是要插入位置*/
{intj;
if(p->n==MAX)
{printf(“overflow”);return(0);}
if(i<1||i>p->n+1)
{printf(“error”);return(0);}
for(j=p->n;j>=i;j--)
p->m[j+1]=p->m[j];/*m[]是存放线性表的一维数组*/
p->m[j+1]=x;/*将x插入到线性表中*/
p->n++;/*数据元素个数加1*/
return
(1);}
2.2.3删除
线性表的删除是指将表中第i个数据元素删去。
长度为n的线性表变为长度为n-1的线性表。
除了将表尾元素删除,否则应该将第n个到第i+1个数据元素依次向前移动一个位置。
共需移动n-i个数据元素。
intdeletelist(SList*p,inti)/*删除函数*/
{intj;
if(i<1||i>p->n+1)
{printf(“error”);return(0);}
for(j=i+1;jn;j++)
p->m[j]=p->m[j+1];
p->n--;
return
(1);}
2.2.4查找
线性表的查找是指找出数据元素x在表中的位置。
intsearch(SListp,intx)/*查找函数*/
{intj;
for(j=0;j
if(x==p.m[j])return(j);
return(0);
}
2.2.5插入、删除运算的时间分析
在等概率的情况下,对顺序存储的线性表:
插入时所需要移动数据元素的平均次数为:
n/2。
删除时所需要移动数据元素的平均次数为:
(n-1)/2。
两者平均时间复杂度都是O(n)。
2.3线性表的链式存储结构
顺序存储的线性表优点是随机访问表中的元素很方便。
缺点是进行插入和删除时要进行大量的数据移动,并且一维数组的长度也很难估计。
为了克服以上缺点,引入线性表的链式存储结构。
2.3.1线性链表
线性表的链式存储结构是用一组任意的存储单元(也可以不连续)存放线性表的数据元素。
为了表示数据之间的逻辑关系,除了存储数据元素本身外(称为数据域),还必须存放一个指针或叫链(称为指针域),指向直接后继元素的位置。
这两部分组成一个结点。
头指针
2158∧
例如:
为了确定线性表的第一个数据元素的位置,需要有一个指针指向链表的表头,称此指针为头指针。
线性表的最后一个结点的指针为空,用∧或NULL表示。
这样的表称为单链表。
有时为了操作方便,在单链表的第一个结点之前添加一个表头结点。
表头结点的结构与表中结点的结构相同,但不存放线性表的元素。
例如:
表头
单链表中结点类型定义:
typedefstructnode{DataTypedata;
structnode*link;}Lnode;
有如下定义:
Lnode*h,*p;h是头指针,p是指向链表中某结点的指针
(*p).data或p->data表示由p所指向的结点的数据域。
(*p).link或p->link表示由p所指向的结点的指针域。
当p被定义后,并没有指向某个结点,当需要增加一个新结点时,需按结点类型向系统申请一个新结点的存储空间。
可以用C语言中的库函数malloc(m),调用方式如下:
p=(LNode*)malloc(sizeof(LNode));
当我们不需要p结点时,要把这个结点的存储空间归还给系统。
可以用C语言中的库函数free(p)来实现。
正是基于这两个函数。
使得链表可以在程序执行的过程中动态地生成或消失。
它不是预先分配存储空间,而是可以由系统应需求即时生成。
关于无指针的高级语言的链表的描述(略)。
2.3.2单链表的基本运算
1.结点产生函数
LNode*creat-item()
{LNode*s;
s=(Lnode*)malloc(sizeof(Lnode));
if(!
s)
printf(“内存空间不足”);
elses->link=NULL;
returns;
}
2.初始化单链表
voidinitial(Lnode*h)
{(List->h=creat-item();
List->t=List->h;
List->n=0;
}
3.查找(按值查找)查找可以按值查找,也可以按序号查找。
LNode*locarenode(Lnode*h,intx)
{LNode*p;
p=h;
while(p!
=NULL&&P->data!
=x)
p=p->link;
return(p);
}
4.插入
voidinsertlist(LNode*p,intx)/*新结点s插入到p结点后面*/
{LNode*s;
s=(LNode*)malloc(sizeof(LNode));
s->data=x;
s->link=p->link;
p->link=s;
}
5.删除
voiddeletelist(LNode*p)/*删除p的后继结点*/
{LNode*q;
if(p->link!
=NULL)
{q=p->link;
p->link=q->link;free(q);
}
6.前插法建立单链表
LNode*createfirst()
{LNode*s,*h;
intx,tag;
printf(“输入结束标志”);
scanf(“%d”,&tag);
h=NULL;
printf(“输入数据x:
”);
scanf(“%d”,&x);
while(x!
=tag)
{s=(LNode*)malloc(sizeof(LNode));
s->data=x;
s->link=h;
h=s;
scanf(“%d”,&x);
}
returnh;
}
7.尾插法建立单链表
LNode*createtail()
{LNode*s,*h,*r;
intx,tag;
printf(“输入结束标志”);
scanf(“%d”,&tag);
h=(LNode*)malloc(size(LNode));
h->data=tag;
r=h;
printf(“输入数据x:
”);
scanf(“%d”,&x);
while(x!
=tag)
{s=(LNode*)malloc(sizeof(LNode));
s->data=x;
r->link=s;
r=s;
scanf(“%d”,&x);
}
r->link=NULL;
returnh;
}
8.在带表头结点的单链表中删除所有数据域值为X的结点。
voiddeletex(LNode*h,intx)
{LNode*s;
while(h->link!
==NULL)
{if(h->link->data==x)
{s=h->link;
h->link=s->link;
free(s);
elseh=h->link;
}}
2.3.3单链表的其他运算示例
1.在头指针为h的带表头结点的单链表中,把结点b插入到结点a之前,若不存在结点a就把结点b插入到表尾。
解决此问题需要先找到结点a,再插入结点b。
qp
a∧
b
s
设s指向结点b,插入结点b的语句序列为:
q->link=s;s->link=p;
因此,需要设pqs三个指针。
p指向当前搜索结点,q指向当前结点的前驱结点。
s是新生成结点的地址。
算法如下:
voidinsertb(LNode*h,inta,intb)
s=(LNode*)malloc(size(LNode));
s->data=b;
p=h->link;q=h;
while(p->data!
=a&&p->link!
=NULL)
{q=p;p=p->link;}
if(p->data==a){q->link=s;s->link=p;}
else{p->link=s;s->link=NULL;}
}
2.设线性表的n个元素依次存放在一维数组a[n]中,请建立一个带表头结点的单链表,h为新建链表的头指针。
首先生成一个h结点作为表头结点,其链域为空。
再生成一个s结点,是单链表中的第一个结点,其链域为空。
这个s结点也是链表的最后一个结点。
以后顺序生成新的若干个s结点,每生成一个新的s结点都插入到链表的最前面。
如图:
h
SnS2S1∧
算法如下:
LNode*createarray1(inta[],intn)
LNode*s,*h;
inti;
h=(LNode*)malloc(size(LNode));
h0;h->link=NULL;
for(i=n;i>0;i--)
{s=(LNode*)malloc(size(LNode));
s->data=a[i-1];
s->link=h->link;
h->link=s;
}
return(h);
}
3.设h是带表头结点的单链表的头指针,请设计一个逆置这个单链表的算法法。
原表
hS1S2S3S4∧
逆表
hS4S3S2S1∧
建立逆表的方法是:
顺序扫描原表,依次将原表中的结点逐个插入到表中第一个结点之前。
设s指向当前逆转结点,p指向s结点的直接后继结点。
算法如下:
voidinvert(LNode*h)
{LNode*s,*p;
p=h->link;
h->link=NULL;
while(p!
=NULL)
{s=p;
p=p->link;
s->link=h->link;
h->link=s;
}
}
4.按升序打印h为头指针的单链表中各数据域值,并将打印完的结点从表中删除。
需要先寻找表中最小元素的结点,然后打印元素值,再删除这个结点。
查找时要设定两个指针p和q,设p指向表中最小元素的结点,q指向p的直接前驱结点。
寻找表中最小元素的结点的方法是:
从第一个结点开始,逐个比较结点的数据域的值,并令p指向当前最小元素的结点,q指向p的直接前驱结点,直到表尾。
查找过程中另设两个指针p1和q1,分别指向当前搜索结点及其直接前驱结点。
应当注意的是:
若p结点是表中第一个结点时,必须先修改头指针h使其指向p结点的直接后继,然后再删除p结点。
算法如下:
voiddeleteprint(LNode*h)
{LNode*p,*q,*p1,*q1;
while(h!
=NULL)
{p=q=q1=h;
p1=p->link;
while(p1!
=NULL)
{if(p1->datadata)
{p=p1;q=q1;}
q1=p1;
p1=p1->link;
}
if(p==h)h=h->link;
elseq->link=p->link;
free(p);
}
}
5设pa,pb分别为两个按升序排列的单链表的头指针,请设计一个算法把这两个单链表合并为一个按按升序排列的单链表pc。
合并的方法是:
从两表的第一个结点开始顺链逐个将对应元素进行比较,复制小者并入表尾。
当两表有一个已经到表尾,则复制另一个链表的剩余部分,插到pc。
为了减少程序中的判断,pc表增设了表头结点,运算结束时再把它删除。
函数值返回指向pc表的头指针。
设pa,pb分别指向两表搜索结点,p指向pc表的当前表尾结点。
算法如下:
{LNode*mergelist(LNode*pa,LNode*pb)
LNode*p,*q,*pc;
pc=(LNode*)malloc(size(LNode));
p=pc;
while(pa!
=NULL&&pb!
=NULL)
{q=(LNode*)malloc(size(LNode));
if(pb->datadata)
{q->data=pb->data;
pb=pb->link;}
else{q->data=pa->data;
if(pa->data==pb->data)pb=pb->link;}
pa=pa->link;
p->link=q;
p=q;
}
if(pa=NULL)pa=pb;
while(pa!
=NULL)
{q=(LNode*)malloc(size(LNode));
q->data=pa->data;
pa=pa->link;
p=q;
}
p->link=NULL;
p=pc;
pc=p->link;
free(p);
return(pc);
}
2.4栈
2.4.1栈的定义和运算
栈是限定只能在表的一端进行插入和删除的线性表。
允许插入和删除的一端叫做栈顶,不允许插入和删除的另一端叫做栈底。
我们称栈为先进先出表。
栈的主要运算是删除和插入。
栈的其他运算有:
设置一个空栈,判断栈空否,取栈顶元素。
栈可以使用顺序存储结构,称为顺序栈。
栈也可以使用链式存储结构,称为链栈。
2.4.2顺序栈和主要运算的实现
可用一维数组data[MAX]存放栈的元素。
MAX是栈的最大容量。
由于栈顶的位置经常变动,故设一个简单变量top来指示位置栈顶,top称为栈顶指针。
顺序栈的类型定义:
#defineMAX100
typedefintDataType
typedefstruct{DataTypedata[MAX];
inttop;}SeqStack;
栈在初始状态下top=0,为空栈,这时再出栈为“下溢”。
当top=MAX为栈满,这时再进栈为“上溢”。
顺序栈的基本运算算法:
1.创建一个空栈
SeqStack*createemptystacks()/*创建一个空栈*/
{SeqStack*s;
s=(SeqStack*)malloc(sizeof(Seq));
s->top=0;
returns;}
2.判栈空
intstackemptys(SeqStack*s)/*判栈空*/
{returns->top==0;}
3.判栈满
intstackfulls(SeqStack*s)/*判栈满*/
{returnp->top==MAX-1;}
4.进栈
intpashs(SeqStack*p,DataTypex)/*入栈*/
{if(stackfulls(&s)){printf(“over\n”);return0;}
else{s->data[s->top++]=x;return1;}
}
5.退栈
voidpops(SeqStack*s)/*出栈*/
{if(stackemptys(&s)){printf(“under\n”);return0;}
else(s->top--;return1;}
}
6.当栈非空时取栈顶元素
DataTypegettop(SeqStack*s)
{returns->data[s->top-1];)
两个栈共享存储空间(一维数组)的方法。
祥见24-25页。
2.4.3链栈
当栈的最大容量事先不能估计时,也可采用链表作存储结构,简称链栈。
其结点的类型定义与单链表类似:
typedefstructnode{intdata;
structnode*link;}Lnode;
2.5栈与递归
栈的一个重要应用是可以用来实现递归函数。
递归最简单的例子是阶乘:
这种用自身的简单情况来定义自己的方式,称为“递归定义”。
一个递归定义必须一步比一步简单,最后是有终结的,决不能无限循环下去。
程序如下:
intfact(intn)
{if(n==0||n==1)reyurn
(1);
elsereturn(n*fact(n-1));
}
main()
{intn,y;
scanf(“%d”,&n)
y=fact(n)
printf(“%d!
=%d”,n,y);
}
函数直接或间接调用自身称为递归调用。
实现递归调用的关键是建立一个栈。
这个栈是由系统提供的运行工作栈。
函数调用时,系统先将工作记录(函数的实参,局部变量,及返回地址等)进栈,然后程序控制转到被调函数;当被调函数返回时,则先退栈,然后程序控制转到退栈记录的返回地址处继续执行。
在函数递归调用时,前次递归调用和本次递归调用是两次进栈的不同的工作记录,这就保证了递归调用的正确性。
由此可见,离开了栈,函数递归调用是不能实现的。
2.6队列
2.6.1队列的定义
队列是限定只能在表的一端进行插入运算,在表的另一端进行删除运算的线性表。
允许插入的一端叫队尾,允许删除的一端叫队首。
队列又叫先进先出表。
2.6.2队列的顺序存储结构
队列的顺序存储结构是用一组连续的存储单元依次存放队列中的元素。
可用一维数组q[MAX]存放队列,其中MAX表示队列允许的最大容量。
需要引进两个指针f和r,设f为队首指针,指向实际队首的前一个位置,r为队尾指针,指向实际队尾元素所在的位置。
f和r应对应数组元素的下标。
在程序中是整型变量,而不是指针型变量。
初始状态f=r=1,设f=r时队列为空。
进队时r加1,出队时f加1。
队列“假溢出”的问题:
当进队时r加1,当r=MAX-1时无法再继续进对,此时队列不一定是满的,这种现象叫“假溢出”。
(图2-22)
解决“假溢出”有两种方法:
一是每次删除一个元素,就将队列里的元素都向前移动一个位置,二是采用循环队列的方法。
把一维数组想象成首尾相接的循环数组,设想data[0]在data[MAX]之后,(图2-23)称这种方法为循环队列。
循环队列队空的标志为:
r=f
循环队列队满的标志为:
(