数据结构学习
第一周:线性表
1.1 线性表的定义和特点
线性表是具有相同特性的数据元素的一个有限序列
(a1 ,a2,…,ai-1,ai+1,…,an)
线性表(Linear List):
由n(n>=0)个数据元素(结点)a1 ,a2,…an组成的有限序列。
- 其中元素的个数n定义为表的长度
- 当n=0时称为空表
- 将非空的线性表(n>0)记作:(a1 ,a2,…an)
- 这里的数据元素ai(1<=i<=n)只是一个抽象的符号,其具体含义在不同的情况下可以不同。
同一线性表中的元素必定具有相同特性,数据元素的关系时线性关系
线性表的逻辑特征:
从以上例子可看出线性表的逻辑特征是:
- 从非空的线性表,有且仅有一个开始结点a1,它没有直接前趋,而仅有一个直接后继a2;
- 有且仅有一个终端结点an,它没有直接后继而仅有一个直接前趋an-1;
- 其余的内部结点ai(2,<=i<=n-1)都有且仅有一个直接前趋ai-1和一个直接后继ai+1。
线性表是一种典型的线性结构
1.2 案例引入
顺序存储结构存在问题:
- 存储空间分配不灵活
- 运算的空间复杂度
改用链式存储结构
总结:
- 线性表中数据元素的类型可以为简单类型,也可以为复杂类型。
- 从具体应用中抽象出共性的逻辑结构和基本操作(抽象数据类型),然后实现其存储结构和基本操作。
1.3 线性表的类型定义
抽象数据类型线性表的定义如下:
ADT List{
数据对象:D = {a~i~|a~i~属于Elemset,(i=1,2,…,n,n>=0)}
数据关系:R = {<a~i-1~a~i~>|a~i-1~,a~i~属于D,(i=2,3…,n)}
基本操作:
lnitList(&L); DestoryList(&L);
Listlnsert(&L,i,e); ListDelete(&L,i,&e);
……等等
}ADT List
基本操作(一)
lnitList(&L) (lnitialization List)
- 操作结果:构造一个空的线性表L。
DestroyList(&L)
- 初始条件:线性表L已经存在。
- 操作结果:销毁线性表L。
ClearList(&L)
- 初始条件:线性表L已经存在
- 操作结果:将线性表L重置为空表。
基本操作(二)
ListEmpty(L)
- 初始条件:线性表L已经存在。
- 操作结果:若线性表L为空表。则返回TRUE,否则返回FALSE。
ListLength(L)
- 初始条件:线性表L已经存在。
- 操作结果:返回线性表L中的数据元素个数。
基本操作(三)
GetElem(L,i,&e)
- 初始条件:线性表L已经存在,1<=i<=ListLength(L).
- 操作结果:用e返回线性表L中第i个数据元素的值。
LocateElem(L,e,compare())
- 初始条件:线性表L已经存在,compare()是数据元素判定函数。
- 操作结果:返回L中第1个与e满足compare()的数据元素的位序。若这样的数据元素不存在则返回值为0.
基本操作(四)
PriorElem(L,cur_e,&pre_e)
- 初始条件:线性表L已经存在。
- 操作结果:若cur_e是L的数据元素,且不是第一个,则用pre——而返回它的前驱。否则操作失败,pre_e无意义。
NextElem(L,cur_e,&next_e)
- 初始条件:线性表L已经存在
- 操作结果:若cur_e是L的数据元素,且不是第最后个,则用next_e返回它的后继,否则操作失败,next_e无意义。
基本操作(五)
Listlnsert(&L,i,e)
- 初始条件:线性表L已经存在,i<=i<=ListLength(L)+1。
- 操作结果:在L的第i个位置之前插入新的数据元素e,L的长度加以。
插入元素e之前(长度为n):(a1 ,a2,ai-1,ai,…,an)
插入元素e之后(长度为n+1):(a1 ,a2,ai-1,e,ai,…,an)
基本操作(六)
ListDelete(&L,i,&e)
- 初始条件:线性表L已经存在,1<=i<=ListLength(L)
- 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减一。
- 删除前(长度为n):(a1 ,a2,ai-1,ai,…,an)
- 删除后(长度为n-1):(a1 ,a2,ai-1,ai+1,…,an)
ListTraverse(&L,visited())
- 初始条件:线性表L已经存在。
- 操作结果:依次对线性表中的每个元素调用visited()
1.4 线性表的顺序表示和实现
线性表的存储结构
在计算机内,线性表有两种基本的存储结构
顺序存储结构和链式存储结构
线性表的顺序表示又称为顺数存储结构或顺序映像。
顺序存储定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构。
简言之,逻辑上相邻,物理上也相邻
线性表的第1个数据元素a1的存储位置,称线性表的起始位置或基地址
顺序存储结构
例如:线性表(1,2,3,4,5,6)的存储结构:
是一个典型的线性表顺序存储结构。
依次存储,地址连续—中间没有空出存储单元。
存储结构:
不是一个线性表顺序存储结构。
地址不连续—中间存在空的存储单元
线性表顺序存储结构占用一片连续的存储空间。知道某个元素的存储位置就可以计算其他元素的存储位置
顺序表中元素存储位置的计算
如果每个元素占用8个存储单元,ai存储位置是2000单元,则ai+存储位置是?
2008单元
假设线性表的每个元素需占l个存储单元,则第i+1个数据元素的存储位置和第i个数据元素的存储之间满足关系:
由此,所有数据的存储位置均可由第一个数据元素的存储位置得到:
线性表顺序存储结构的图示
顺序表的特点:以物理位置相邻表示逻辑关系。任一元素均可随机存取(优点)
顺序表的顺序存储表示
线性表长可变(删除)
数组长度不可动态定义
(一维数组的定义方式:类型说明符 数组名[常量表达式])
说明:常量表达式中可以包含常量和符号常量,不能包含变量。即C语言中不允许对数组的大小作动态定义。
#define LIST_INIT_SIZE //线性表存储空间的初始分配量
typedef struct{
ElemType elem[LIST_INIT_SIZE]
int length //当前长度
}SqList;
注意:逻辑位序和物理位序相差1
多项式的顺序存储结构类型的定义
#define MAXSIZE 1000
typedf struct{
float p;
int e;
}Polynomial;
typedef struct{
Polynomial *elem;
int length;
}SqList;
顺序表示意图
最后一个数据存放表中的个数的
补充:操作算法中用到的预定义常量和类型
// 函数结果状态代码
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
//Status 是函数的类型,其值是函数结果状态码
typedef int Status;
typedef char ElemType;
顺序表基本操作的实现
【算法1.1】 线性表L的初始化(参数用引用)
Status IlitList_Sq(SqList &L){ //构造一个空的顺序表L
L.elem=new ElemType[MAXSIZE]; //为顺序表分配空间
if(!L.elem) exit(OVERFLOW); //存储分配失败
L.length=0; //空表长度为0
return OK;
}
【算法1.2】销毁线性表L
void DestroyList(SqList &L){
if(L.elem) delete L.elem; //释放存储空间
}
【算法1.3】清空线性表L
void ClearList(SqList &L){
L.length=0; //将线性表的长度置为0
}
【算法1.4】求线性表L的长度
int GetLength(SqList L){
return (L.length);
}
【算法1.5】判断线性表L是否为空
int lsEmpty(SqList L){
if(L.length==0) return 1;
else return 0;
}
【算法1.6】顺序表的取值(根据位置i获取相应位置数据元素的内容)
int GetElem(SqList L,int i,ElemType &e){
if(i<1||i>L.length) return ERROR;
//判断i值是否合理,若不合理,返回ERROR
e=L.elem[i-1]; //第i-1的单元存储着第i个数据
return OK;
}
这种机制是随机存取;
【算法1.7】顺序表的查找
- 在线性表L中查找与指定值e相同的数据元素的位置
- 从表的一端开始,逐个进行记录的关键字和给定值的比较。找到,返回该元素的位置序号,未找到,返回0.
int LocateElem(SqList L,ElemType e){
//在线性表L中查找值为e的数据元素,返回其序号(是第几个元素)
for (i=0;i<L.length;i++)
if(L.elem[i]==e) return i+1; //查找成功,返回序号
return 0;//查找失败,返回0
}
顺序表的查找算法分析:
基本操作:L.elem[i]==e
平均查找长度ASL(Average Search Length):
因为查找算法的基本操作为:将记录的关键字通给定值进行比较的关键字的个数的期望值叫做查找算法的平均查找长度
顺序查找的平均查找长度:
假设每个记录的查找概率相等:
则:
顺序表的删除
线性表的删除元素是指将表的第i(1<=i<=n)个结点删除,使长度为n的线性表(a1,…,ai-1,ai,ai+1,…,an),变成长度为n-1的线性表(a1,…,ai-1,ai+1,…,an)
算法思想:
1.判断删除位置i是否合法(合法值为1<=i<=n)
2.将欲删除的元素保留在e中
3.将第i+1至第n位的元素依次向前移动一个位置
4.表长减1,删除成功返回OK
删除算法演示:
- 删除位置在最后
- 删除位置在中间
- 删除位置在最前面
【算法1.8】顺序表的删除
Status ListDelete Sq(SqList &L,int i)
{
if((i<1||(i>L.length)) return ERROR; //i值不合法
for(j=i;j<=L.length-1;j++)
L.elem[j-1]=L.elem[j]; //被删除元素之后的元素前移
L.length--; //表长减1
return OK;
}
算法分析:
算法时间主要耗费在移动元素的操作上
- 若删除尾结点,则根本无需移动(特别快);
- 若删除首结点,则表中n-1个元素全部前移(特别慢)
顺序表删除算法的平均时间复杂度为O(n)
1.5 类C语言有关的补充
1.补充:元素类型说明
顺序表类型定义
typedef struct{
ElemType data[];
int length;
}SqList; //顺序表类型
ElemType:代表线性表元素的类型,这个元素是什么类型,就可以换成是什么类型
例:typedef char ElemType
typedef int ElemType
typedef struct{
float p;
int e;
}Polynomial;
typedef struct{
Polynomial *elem;
int length;
}SqList;
2.补充:数组定义
动态分配函数来分配内存:
SqList L;
L.data=(ElemType*)malloc(sizeorf(ElemType)*MaxSize);
3.补充:C语言的内存动态分配
SqList L;
L.data=(ElemType*)malloc(sizeorf(ElemType)*MaxSize);
- malloc(m)函数,开辟m字节长度的地址空间,并返回这段空间的首地址
- sizeof(x)运算,计算变量x的长度
- free§,释放指针p所指变量的存储空间,即彻底删除一个变量
需要加载头文件:<stdlib.h>
4.补充C++的动态分配内存
5.补充:C++中的参数传递
- 函数调用时传送给形参的实参必须与形参三个一致
类型、个数、顺序 - 传值方式(参数为整型、实型、字符型等)
- 传地址
1.参数为指针变量
2.参数为引用类型
3.参数为数组名
1.6 顺序表小结
顺序表(线性表的顺序存储结构)的特点
(1)利用数据元素的存储位置表示线性表中相邻数据元素之间的前后关系,即线性表的逻辑结构与存储结构一致
(2)在访问线性表时,可以快速地计算出任何一个数据元素的存储地址。因此可以粗略地认为,访问每个元素所花时间相等
这种存取元素地方法被称为随机存取法
顺序表的操作算法分析
- 时间复杂度
查找、插入、删除算法的平均时间复杂度为O(n) - 空间复杂度
显然,顺序表操作算法的空间复杂度S(n)=O(1)(没有占用辅助空间)
顺序表优缺点
优点:
- 存储密度大(结点本身所占存储量/结点结构所占存储量)
- 可以随机取表中任一元素
缺点:
- 在插入、删除某一元素时,需要移动大量元素
- 浪费存储空间
- 属于静态存储形式,数据元素的个数不能自由扩充
为了克服这一缺点 ——> 链表
1.7 线性表的链式表示和实现
1.链表的概念
- 链式存储结构
结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻 - 线性表的链式表示又称为非顺序映像或链式映像
用一组物理位置任意的存储单元来存放线性表的数据元素。
这组存储单元既可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。
链表中元素的逻辑次序和物理次序不一定相同
与链式存储有关的术语
1、结点:数据元素的存储映像。由数据域和指针域两部分组成
2、链表:n个结点由指针链组成一个链表。它是线性表的链式存储映像,称为线性表的链式存储结构
3、单链表、双链表、循环链表:
- 结点只有一个指针域的链表,称为单链表或线性链表
- 结点有两个指针域的链表,称为双链表
- 首尾相接的链表称为循环链表
4、头指针、头结点和首元结点
头指针:是指向链表中第一个结点的指针
首元结点:是指链表中存储第一个数据元素a1的结点
头节点:是在链表的首元结点之前附设的一个结点
前面的例子中的链表的存储结构示意图有以下两种形式:
讨论1:如何表示空表?
1.无头结点时,头指针为空时表示空集
2.有头结点时,当头结点的指针域为空时表示空表
讨论2:在链表中设置头结点有什么好处?
1.便于首元结点的处理
首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其他位置一致,无须进行特殊处理;
2.便于空表和非空表的统一处理
无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了
讨论3:头结点的数据域内装的是什么?
头结点的数据域可以为空,也可以存放线性表长度等附加信息,但此结点不能计入链表长度值。
链表(链式存储结构)的特点
(1)结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻
(2)访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个节点和最后一个结点所花费的时间不等
这种存取元素的方法被称为顺序存取法
2.单链表的定义
带头结点的单链表
单链表是表头唯一确定,因此单链表可以用头指针的名字来命名,若头指针名是L,则把链表称为表L
单链表的存储结构
typedef struct Lnode{ //声明结点的类型和指针结点的指针类型
ElemType data; //结点的数据域
struct Lnode *next; //结点的指针域
}Lnode,*LinkList; //LinkList为指向结构体Lnode的指针类型
定义链表:LinkList L;
定义指针结点p:LNode *p <-> LinkList p;
3.单链表的基本操作1 - 初始化和判断空表
单链表的初始化(带头结点的单链表)
即构造一个如图的空表
【算法步骤】
(1)生成新结点作头结点,用头指针L指向头结点。
(2)将头结点的指针域置空
【算法描述】
Status lnitList_L(LinkList &L){
L = new LNode; //或L = (LinkList)malloc(sizeof(LNode));
L -> next = NULL;
return OK;
}
补充单链表的几个常用算法
【补充算法1】——判断链表是否为空:
空表:链表中无元素,称为空链表(头指针和头结点仍然在)
【算法思路】判断头结点指针域是否为空
int ListEmpty(LinkList L){ //若L为空表,则返回1,否则返回0
if(L->next) //非空
return 0;
else
return 1;
}
4.单链表的基本操作 - 销毁单链表
【补充算法2】——单链表的销毁:链表销毁后不存在
【算法思路】从头指针开始,依次释放所有结点
【算法】销毁单链表L
Status DestroyList_L(LinkList &L){ //销毁单链表L
Lnode *p; //或LinkList p;
while(L){
p=L;
L=L->next;
delete p;
}
return OK;
}
5.单链表的基本操作 - 清空单链表
【补充算法3】—— 清空链表:
链表仍存在,但链表中无元素,成为空链表(头指针和头结点仍然在)
【算法思路】依次释放所有结点,并将头结点指针域设置为空
【算法】清空链表L:
Status ClearList(LinkList &L){ //将L重置为空表
Lnode *p,*q; //或LinkList p,q;
p=L->next;
while(p){ //没有表尾
q=p->next;
delete p;
p=q;
}
L->next=NULL; //头结点指针域为空
return OK;
}
6.单链表基本操作 - 求单链表的表长
【补充算法4】——求单链表的表长
【算法思路】从首元结点开始,依次计数所有结点
【算法】求单链表L的表长
int ListLength_L(LinkList L){ //返回L中数据元素个数
LinkList p;
p=L->next; //p指向第一个结点
i=0;
while(p){ //遍历单链表,统计结点数
i++;
p=p->next;
}
return i;
}
7.单链表的基本操作 - 取第i个元素值
取值——取单链表中第i个元素
【算法思路】分别取出表中第3个元素和第15个元素
从链表的头指针出发,顺着链域next逐个结点往下搜索,直至搜索到第i个结点为止。因此,链表不是随机存取结构
【算法步骤】
1.从第1个结点(L -> next)顺链扫描,用指针p指向当前扫描到的结点,p初值p = L -> next。
2.j做计数器,累计当前扫描过的结点数,j初值为。.
3.当p指向扫描到下一结点时,计数器j加1。
4.当 j == i 时,p所指向的结点就是要找的第i个结点。
【算法描述】
Status GetElem_L(LinkList,int i,ElemType &e){ //获取线性表L中的某个数据元素的内容,通过变量e返回
p=L->next;j=1; //初始化
while(p&&j<i){
p=p->next;++j;
}
if(!p||j。i) return ERROR;//第i个元素不存在
e=p->data; //取第i个元素
return OK;
}//GetElem_L
8.单链表的基本操作 - 按值查找
【算法】按值查找——根据指定数据获取该数据所在的位置(地址)
【算法步骤】
1.从第一个结点起,依次和e相比较。
2.如果找到一个其值与e相等的数据元素,则返回其在链表中的“位置”或地址;
3.如果查遍整个链表都没有找到其值和e相等的元素,则返回0或“NULL”。
【算法描述】
Lnode *LocateElem_L(LinkList L,Elemtype e){
//在线性表L中查找值为e的数据元素
//找到,则返回L中值为e的数据元素的地址,查找失败返回NULL
p=L->next;
while(p&&p->data!=e)
p=p->next;
return p;
}
【算法】按值查找——根据指定数据获取该数据位置序号
【算法描述】
//在线性表L中查找值为e的数据元素的位置序号
int LocateElem_L(LinkList L,Elemtype e){
//返回L中值为e的数据元素的位置序号,查找失败返回0
p=L->next;j=1;
while(p&&p->data!=e)
{p=p->next;j++;}
if(p) return j;
else return 0;
9.单链表基本操作 - 插入节点
【算法】插入——在第i个结点前插入值为e的新结点
【算法步骤】
1、首先找到ai-1的存储位置p。
2、生成一个数据域为e的新结点s。
3、插入新结点:
- 新结点的指针域指向结点ai
- 结点ai-1的指针域指向新结点
1.s - > next = p ->next;
2.p - > next = s;
思考;步骤1和2能互换吗?先执行2.后执行1,可以吗?
答:不可以!会丢失ai的地址
【算法描述】
//在L中第i个元素之前插入数据元素e
Status Listlnsert_L(LinkList &l,int i,ElemType e){
p=L;j=0;
while(p&&j<i-1) {p=p->next;++j;} //寻找第i-1个结点,p指向i-1结点
if(!p||j>i-1) return ERROR; //i大于表长+1或者小于1,插入位置非法
S=new LNdoe; s->data=e; //生成新结点s,将结点s的数据域置为e
s->next=p->next; //将结点s插入L中
p->next=s;
return OK;
}//Listlnsert_L
10.单链表的基础操作 - 删除节点
【算法】删除——删除第i个结点
【算法步骤】
1、首先找到ai-1的存储位置p,保存要删除的ai的值
2、令p -> next 指向ai+1
【算法描述】
//将线性表L中第i个数据元素删除
Status ListDelete_L(LinkList &L,int i,ElemType &e){
p=L;j=0;
while(p->next &&j<i-1){ p=p->next;++j;)
//寻找第i个结点,并令p指向其前驱
if(!(p->next)||j>i-1) return ERROR; //删除位置不合理
q=p->next; //临时保存被删结点的地址以备释放
p->next=q->next; //改变删除结点前驱结点的指针域
e=q->data; //保存删除结点的数据域
delete q; //释放删除结点的空间
return OK;
}//ListDelete_L
11.单链表的基本操作 - 查找插入删除算法分析
单链表的查找、插入、删除算法时间效率分析
1.查找:
- 因线性链表只能顺序存取,即在查找时要从头指针找起,查找的时间复杂度为O(n)。
2.插入和删除:
- 因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度为O(1)。
- 但是,如果要在单链表中进行前插入或删除操作,由于要从头查找前驱结点,所耗费时间复杂度为O(n)。
12.单链表的基本操作 - 头插法建立链表
【算法】建立单链表:头插法——元素插入在链表头部,也叫前插法
1.从一个空表开始,重复读入数据;
2.生成新结点,将读入数据存放到新结点的数据域中
3.从最后一个结点开始,依次将各结点插入到链表的前端
例如,建立链表L(a,b,c,d,e)
【算法描述】
void CreateList_H(LinkList &L,int n){
L=new LNode;
L->next=NULL;//先建立一个带头结点的单链表
for(i=n;i>0;--i){
p=new LNode; //生成新结点p=(LNode*)malloc(sizeof(LNode));
cin>>p->data; //输入元素值scanf(&p->data);
p->next=L->next; //插入到表头
L->next=p;
}
}//CreateList_H
13.单链表的基本操作 - 尾插法建立链表
【算法】建立单链表:尾插法——元素插入在链表尾部,也叫后插法
- 从一个空表L开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的尾结点。
- 初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点。
【算法描述】//正位序输入n个元素的值,建立带表结点的单链表L
void CreateList_R(LinkList &L,int n){
L=new LNode; L->next=NULL;
r=L; //尾指针r指向头结点
for(i=0;i<n;++i){
p=new LNode;cin>>p->data; //生成新结点,输入元素值
p->next=NULL;
r->next=p; //插入到表尾
r=p; //r指向新的尾结点
}
}//CreateList_R
1.8 循环链表
循环链表:是一种头尾相接的链表(即:表中最后一个结点的指针域指向头结点,整个链表形成一个环)。
优点:从表中任一结点出发均可找到表中其他结点。
注意:由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或p->next是否为空,而是判断它们是否等于头指针。
循环条件:
1.循环链表 - 两个链表合并
带尾指针循环链表的合并(将Tb合并在Ta之后)
分析有哪些操作?
带尾指针循环链表的合并
【算法描述】
LinkList Connect(LinkList Ta,LinkList Tb){
//假设Ta、Tb都是非空的单循环链表
p=Ta->next; //p存表头结点
Ta->next=Tb->next->next; //Tb表头连结Ta表尾
delete Tb->next; //释放Tb表头结点
Tb->next=p; //修改指针
return Tb;
} 时间复杂度O(1)
1.9 双向链表
为什么要讨论双向链表:
单链表的结点—>有指示后继的指针域—>找后继结点方便;
即:查找某结点的后继结点的执行时间为O(1)
—>无指示前驱的指针域—>找前驱结点难:从表头出发查找。
即:查找某结点的前驱结点的执行时间为O(n)。
(可用双向链表来克服单链表的这种缺点)
双向链表:在单链表的每个结点里再增加一个指向其直接前驱的指针域prior,这样链表中就形成了有两个不同的链,故称为双向链表。
双向链表的结构可定义如下:
typedef struct DuLNode{
Elemtype data;
struct DuLNode *prior,*next;
}DuLNode,*DuLinkList;
和单链表的循环表类似,双向链表也可以有循环表
- 让头结点的前驱指针指向链表的最后一个结点
- 让最后一个结点的后继指针指向头结点
双向链表结构的对称性(设指针p指向某一结点):
在双向链表中有些操作(如:ListLength、GetElem等),因仅涉及一个方向的指针,故它们的算法与线性链表的相同。但在插入、删除时,则需同时修改两个方向上的指针,两者的操作的时间复杂度均为O(n)。
1.双向链表的插入操作
【算法】双向链表的插入
1.s->prior=p->prior;
2.p->prior->next=s;
3.s->next=p;
4.p->prior=s;
void Listlnsert_DuL(DuLinkList &L,int i,ElemType e){
//在带头结点的双向循环链表L中第i个位置之前插入元素e
if(!(p=GetElemP_DuL(L,i))) return ERROR;
s=new DuLNode; s->data=e;
s->prior=p->prior; p->prior->next=s;
s->next=p; p->prior=s;
return OK;
}//Listlnsert_DuL
2.双向链表的删除操作
【算法】双向链表的删除
1.p->prior->next=p->next;
2.p->next->prior=p->prior;
【算法】双向链表的删除
void ListDelete_DuL(DuLink &L,int i,ElemType &e){
//删除带头结点的双向循环链表L的第i个元素,并用e返回
if(!(p=GetElemP_DuL(L,i))) return ERROR;
e=p->data;
p->prior->next=p->next;
p->next->prior=p->prior;
free(p)
return OK;
}//ListDelete_DuL
1.10 单链表、循环链表、双向链表的比较
1.11 顺序表和链表的比较
- 链式存储结构的优点:
结点空间可以动态申请和释放;
数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素。 - 链式存储结构的缺点:
存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大。
链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该结点,这增加了算法的复杂度。
存储密度
存储密度是指结点数据本身所占的存储量和整个结点结构中所占的存储量之比,即:
一般地,存储密度越大,存储空间地利用率就越高。显然,顺序表地存储密度为1(100%),而链表地存储密度小于1。
顺序表和链表的比较
2.1 栈和队列的定义和特点
1.栈和队列介绍
- 栈和队列是两种常用的、重要的数据结构
- 栈和队列是限定插入和删除只能在表的“端点”进行的线性表
由于栈的操作具有后进先出的固有特性,使得栈成为程序设计中的有用工具。另外,如果问题求解的过程具有“后进先出”的天然特性的话,则求解的算法中也必然需要利用“栈”。
由于队列的操作具有先进先出的特性,使得队列成为程序设计中解决类似排队问题的有用工具。
2.栈的定义和特点
- 栈(stack)是一个特殊的线性表,是限定仅在一端(通常是表尾)进行插入和删除操作的线性表。
- 又称为后进先出(Last In First Out)的线性表,简称LIFO结构。
栈的相关概念
栈是仅在表尾进行插入、删除操作的线性表。
表尾(即an端)称为栈顶Top;表头(即a1端)称为栈底Base
插入元素到栈顶(即表尾)的操作,称为入栈。
从栈顶(即表尾)删除最后一个元素的操作,称为出栈。
入栈的操作示图:
出栈的操作示图:
栈与一般线性表有什么不同
栈与一般线性表的区别:仅在于运算规则不同。
3.队列的定义和特点
队列(queue)是一种先进先出(First In First Out ----FIFO)的线性表。在表一端插入(表尾),在另一端(表头)删除
队列的相关概念
2.2 栈的表示和操作的实现
1.栈的抽象数据类型定义
ADT Stack{
数据对象:
D={ai|ai∈ElemSet,i=1,2,...,n,n>=0}
数据关系:
R1={<ai-1,ai>|ai-1,ai∈D,i=2,...,n}
约定an端为栈顶,a1端为栈底。
基本操作:初始化、进栈、出栈、取栈顶元素等
}ADT Stack
-
InitStack(&S) 初始化操作
操作结果:构造一个空栈S。 -
DestroyStack(&S) 销毁栈操作
初始条件:栈S已存在。
操作结果:栈S被销毁。 -
StackEmpty(S) 判定S是否为空栈
初始条件:栈S已存在
操作结果:若栈S为空栈,则返回TRUE,否则FASLE。 -
StackLength(S) 求栈的长度
初始条件:栈S已存在。
操作结果:返回S的元素个数,即栈的长度。 -
GetTop(S,&e) 取栈顶元素
初始条件:栈S已存在且非空
操作结果:用e返回S的栈顶元素。 -
ClearStack(&S) 栈置空操作
初始条件:栈S已存在
操作结果:将S清为空栈 -
Push(&S,e) 入栈操作
初始条件:栈S已存在
操作结果:插入元素e为新的栈顶元素。 -
Pop(&S,&e) 出栈操作
初始条件:栈S已存在且非空
操作结果:删除S的栈顶元素an,并且e返回其值。
2. 栈的顺序表示
- 由于栈本身就是线性表,于是栈也有顺序存储和链式存储两种实现方式。
栈的顺序存储—顺序栈
站的链式存储—链栈
存储方式:同一般线性表的顺序存储结构完全相同,
利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素。栈底一般在低地址端。
- 附设top指针,指示栈顶元素在顺序栈中的位置。
- 另设base指针,指示栈底元素在顺序栈中的位置。
- 另外,用stacksize表示栈可使用的最大容量
但是,为了方便操作,通常top指示真正的栈顶元素之上的下标地址
空栈:base==top是栈空标志
栈满 top - base == stacksize
栈满时的处理方法:
1、报错,返回操作系统。
2、分配更大的空间,作为栈的存储空间,将原栈的内容移入新栈
使用数组作为顺序栈存储方式的特点:
简单、方便、但易产生溢出(数组大小固定)
- 上溢(overflow):栈已经满,又要压入元素
- 下溢(underflow):栈已经空,还要弹出元素
注:上溢是一种错误,使问题的处理无法进行;而下溢一般认为是一种结束条件,即问题结束处理。
3.顺序栈操作1
#define MAXSIZE 100
typedef struct{
SElemType *base; //栈底指针
SElemType *top; //栈顶指针
int stacksize; //栈可用最大容量
}SqStack;
【算法2.1】顺序栈的初始化
Status InitStack(SqStack &S){ //构造一个空栈
S.base = new SElemType[MAXSIZE];
//或S.base = (SElemType*)malloc(MAXSIZE*sizeof)SElemType));
if(!S.base) exit(OVERFLOW); //存储分配失败
S.top = S.base; //栈顶指针等于栈底指针
S.stacksize = MAXSIZE;
return OK;
}
4.顺序栈操作2
【算法补充】顺序栈判断栈是否为空
Status StackEmpty(SqStack S){
//若栈为空,返回TRUE;否则返回FALSE
if(S.top == S.base)
return TRUE;
else
return FALSE;
}
【算法补充】清空顺序栈
Status ClearStack(SqStack S){
if(S.base) S.top = S.base;
return OK;
【算法补充】销毁顺序表
Status DestroyStack(SqStack &S){
if(S.base){
delete S.base;
S.stacksize = 0;
S.base = S.top =NULL;
}
return OK;
}
【算法2.2】顺序栈的入栈
(1)判断是否栈满,若满则出错(上溢)
(2)元素e压入栈顶
(3)栈顶指针加1
Status Push(SqStack &S,SElemType e){
if(S.top - S.base == S.stacksize) //栈满
return ERROR;
*S.top=e;
S.top++; //*S.top++=e;
return OK;
}
5.顺序栈操作3
【算法2.3】顺序栈的出栈
(1)判断是否栈空,若空栈则出错(下溢)
(2)获取栈顶元素e
(3)栈顶指针1
Status Pop(SqStack &S,SElemType &e){
//若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR
if(S.top == S.base)//等价于if(StackEmpty(S))
return ERROR;
e = *--S.top;
//--S.top;
//e=*S.top;
return OK;
}
6.链栈表示和实现
链栈的表示
链栈是运算受限的单链表,只能在链表头部进行操作
typedef struct StackNode{
SElemType data;
struct StackNode *next;
}StackNode,*LinkStack;
LinkStack S;
注意:链栈中指针的方向
- 链表的头指针就是栈顶
- 不需要头结点
- 基本不存在栈满的情况
- 空栈相当于头指针指向空
- 插入和删除仅在栈顶处执行
【补充算法】判断链栈是否为空
Stack StackEmpty(LinkStack S){
if(S==NULL) return TRUE;
else return FALSE;
}
【算法2.6】栈链的入栈
Status Push(LinkStack &S,SElemType e){
p=new StackNode; //生成新结点p
p->data=e; //将新结点数据域为e
p->next=S; //将新结点插入栈顶
S=p; //修改栈顶指针
return OK;
}
【算法2.7】栈链的出栈
Status Pop(LinkStack &S,SElemType &e){
if(S==NULL) return ERROR;
e = S->dta;
p = s;
S = S->next;
delete p;
return OK;
}
2.3 栈和递归
递归的定义
- 若一个对象部分地包含自己,或用它自己给自己定义,则称这个对象是递归的;
- 若一个过程直接地或间接地调用自己,则称这个过程是递归地过程。
以下三种情况常常用到递归方法 - 递归定义的数学函数
- 具有递归特性的数据结构
- 可递归求解的问题
递归问题——用分治法求解
分治法:对于一个较为复杂的问题,能够分解成几个相对简单的且解法相同或类似的子问题来求解
必备的三个条件
1、能将一个问题转变成一个新问题,而新问题与原问题的解法相同或类同,不同的仅是处理的对象,且这些处理对象是变化有规律的
2、可以通过上述而使问题简化
3、必须有一个明确的递归出口,或称递归的边界
函数调用过程
调用前,系统完成:
(1)将实参,返回地址等传递给被调用函数
(2)为被调用函数的局部变量分配存储区
(3)将控制转移到调用函数的入口
调用后,系统完成:
(1)保存被调用函数的计算结果
(2)释放被调用函数的数据区
(3)依照被调用函数保存的返回地址将控制转移到调用函数
当多个函数构成嵌套调用时:
遵循后调用的先返回
递归函数调用的实现
递归的优缺点
优点:结构清晰,程序易读
缺点:每次调用要生成工作记录,保存状态信息,入栈;返回时要出栈,恢复状态信息。时间开销大。
递归->非递归
方法1:尾递归、单向递归->循环结构
方法2:自用栈模拟系统的运行时栈
单向递归->循环结构
虽然有一处以上的递归调用语句,但各次递归调用语句的参数只和主调函数有关,相互之间参数无关,并且这些递归调用语句处于算法的最后。
2.4 队列的表示和操作的实现
相关术语
- 队列(Queue)是仅在表尾进行插入操作,在表头进行删除操作的线性表。
- 表尾即an,称为队尾;表头即a1,称为队头。
- 它是一种先进先出(FIFO)的线性表。
插入元素称为入队;删除元素称为出队。
队列的存储结构为链队或顺序队(常用循环顺序队)
队列的常见应用
- 脱机打印输出:按申请的先后顺序依次输出
- 多用户系统,多个用户排成队,分时地循环使用CPU和主存
- 按用户地优先级排成多个队,每个优先级一个队列
- 实时控制系统中,信号按接收地先后顺序依次处理
- 网络电文传输,按到达的时间顺序依次进行
队列的抽象数据类型定义
队列的物理存储可以用顺序存储结构,也可以用链式存储结构。相应地,队列的存储方式也分为两种,即顺序队列和链式队列。
队列的顺序表示—用一维数组base[MAXQSIZE]
#define MAXQSIZE 100 //最大队列长度
Typedef struct{
QElemType *base; //初始化的动态分配存储空间
int front; //头指针
int rear; //尾指针
}SqQueue;
解决假上溢的方法
1、将队中元素依次向队头方向移动。
缺点:浪费时间。每移动一次,队中元素都要移动。
2、将队空间设想成一个循环的表,即分配给队列的m个存储单元可以循环使用,当rear为maxqsize时,若向量的开始端空着,又可从头使用空着的空间。当front为maxqsize时,也是一样。
解决假上溢的方法—引入循环队列
base[0]接在base[MAXQSIZE - 1]之后,若rear+1==M,则令rear=0;
实现方法:利用模(mod,C语言中:%)运算。
插入元素:
Q.base[Q.rear]=x;
Q.rear=(Q.rear+1)%MAXQSIZE;
删除元素:
x=Q.base[s.front]
Q.front=(Q.front+1)%MAXQSIZE
循环队列;循环使用为队列分配的存储空间。
解决方法:
1.另外设一个标志以区别队空、队满
2.另设一个变量,记录元素个数
3.少用一个元素空间
循环队列解决队满时判断方法—少用一个元素空间:
循环队列的类型定义
#define MAXQSIZE 100 //最大队列长度
typedef struct{
QElemType *base; //动态分配存储空间
int front; //头指针,若队列不空,指向队列头元素
int rear; //尾指针,若队列不空,指向队列尾元素的下一个位置
}SqQueue;
循环队列的操作—求队列的长度
int QueueLength(SqQueue Q){
return((Q.rear-Q.front+MAXQSIZE)%MAXQSIZE);
}
循环队列的操作—循环队列入队
Status EnQueue(SqQueue &Q,QElemType e){
if((Q.rear=1)%MAXQSIZE==Q.front) return ERROR;//队满
Q.base[Q.rear]=e; //新元素加入队尾
Q.rear=(Q.rear+1)%MAXQSIZE; //队尾指针+1
return OK;
}
循环队列的操作—取队头元素
SElemType GetHead(SqQuere Q){
if(Q.front!=Q.rear) //队列不为空
return Q.base[Q.front]; //返回头指针元素的指,队头指针不变
队列的链式表示和实现
若用户无法估计所用队列的长度,则宜采用链队列
链队列的类型定义
#define MAXQSIZE 100 //最大队列长度
typedef struct Qnode{
QElemType data;
struct Qnode *next;
}QNode,*QuenePtr;
typedef struct{
QuenePtr front; //队头指针
QuenePtr rear; //队尾指针
}LinkQueue;
链队列运算指针变化状况
栈队列的操作—链队列初始化
Status lnitQueue(LinkQueue &Q){
Q.front=Q.rear=(QueuePtr)malloc(sizeof(QNode));
if(!Q.front) exit(OVERFLOW);
Q.front->next=NULL;
return OK;
}
链队列的操作—销毁链队列(补充)
算法思想:从队头结点开始,依次释放所有结点
Status DestroyQueue(LinkQueue &Q){
while(Q.front){
p=Q.front->next;free(Q.front);Q.front=p;
}
//Q.rear=Q.front->next;free(Q.front);Q.front=Q.rear;
return OK;
}
链队列的操作—将元素e入队
Status EnQueue(LinkQueue &Q,QElemType e){
p=(QueuePtr)malloc(sizeof(QNode));
if(!p) exit(OVERFLOW);
p->data=e;p->next=NULL;
Q.rear->next=p;
Q.rear=p;
return OK;
}
链队列的操作—链队列出队
Status DeQueue(LinkQueue &Q,QElemType &e){
if(Q.front==Q.rear) return ERROR;
p=Q.front->next;
e=p->data;`
Q.front->next=p->next;
if(Q.rear==p) Q.rear=Q.front;
delete p;
return OK;
}
链队列的操作—求队列的队头元素
Status GetHead(LinkQueue Q,QElemType &e){
if(Q.front==Q.rear) return ERROR;
e=Q.front->next->data;
return OK;
}
3.1 串、数组和广义表
1.串
串的定义
串(String)—零个或多个任意字符组成的有限序列
字串:一个串中任意个连续字符组成的子序列(含空串)称为该串的子串
例如:“abcde”的字串有:
“”、“a”、”ab“、”abc“、”abcd“和”abcde“等;
真子串是指不包含自身的所有字串。
串的定义—几个术语
- 字串:串中任意个连续字符组成的子序列称为该串的字串
- 主串:包含字串的串相应地称为主串
- 字符位置:字符在序列中的序号为该字符在串中的位置
- 字串位置:字串第一个字符在主串中的位置
- 空格穿:由一个或多个空格组成的串,与空串不同
串相等:当且仅当两个串的长度相等并且各个对应位置上的字符都相同时,这两个串才是相等的。
所有空串是相等的。
串的类型定义、存储结构及其运算
ADT String{
基本操作:
串中元素逻辑关系与线性表的相同,串可以采用与线性表相同的存储结构。
串的顺序存储结构
#define MAXLEN 255
typedef struct{
char ch[MAXLEN+1]; //存储串的一维数组
int length; //串的当前长度
}SString;
串的链式存储结构
串的链式存储结构—块链结构
#define CHUNKSIZE 80 //块的大小可由用户定义
typedef struct Chunk{
char ch[CHUNKSIZE];
struct Chunk *next;
}Chunk;
typedef struct{
Chunk *head,*tail; //串的头指针和尾指针
int curlen; //串的当前长度
}LString; //字符串的块链结构
2.串的模式匹配算法
算法目的:
确定主串中所含字串(模式串)第一次出现的位置(定位)
算法应用:
搜索引擎、拼写检查、语言翻译、数据压缩
算法种类:
- BF算法(Burte-Force,又称古典的、经典的、朴素的、穷举的)
- KMP算法(特点:速度快)
Burte-Force简称为BF算法,亦称为简单匹配算法。采用穷举法的思路。
算法的思路是从S的每一个字符开始依次与T的字符进行匹配。
例如:
设目标串S=“aaaaab",模式串T=”aaab“。
S的长度为n(n=6),T的长度为m(m=4)
BF算法的匹配过程如下:
匹配失败:
i=i-j+2=2(回溯)
j=1(从头开始)
匹配成功:
i=7,j=5 返回i-t.length=3
算法设计思想
Index(S,T,pos)
将主串的第pos个字符和模式串的第一个字符比较
- 若相等,继续逐个比较后续字符;
- 若不等,从主串的下一字符起,重新与模式串的第一个字符比较。直到主串的一个连续字符序列与模式串相等。返回值为S中与T匹配的子序列第一个字符的序号,即匹配成功。否则,匹配失败,返回值0
【算法3.1】
int Index_BF(SString S,SString T){
int i=1,j=1;
while(i<=S.length && j<=T.length){
if(s.ch[i]==t.ch[j]){++i;++j;}//主串和子串依次匹配下一个字符
else {i=i-j+2;j=1;}//主串、字串指针回溯重新开始下一次匹配
}
if(j>=T.length) return i-T.length;//返回匹配的第一个字符的下标
else return 0; //模式匹配不成功
}
BF算法事件复杂度
若n为主串长度,m为字串长度,最坏情况是
- 主串前面n-m个位置都部分匹配到字串的最后一位,即这n-m位各比较了m次
- 最后m位也各比较了1次
总次数为:(n-m)*m+m=(n-m+1)m
若m<<n,则算法复杂度O(nm)
KMP算法
3.2 数组
数组:按一定格式排列起来的,具有相同类型的数据元素的集合。
一维数组:若线性表中的数据元素为非结构的简单元素,则称为一维数组。
一维数组的逻辑结构:线性结构。定性的线性表。
声明格式:数据类型 变量名称[长度];
例:int num[5] = {0,1,2,3,4};
二维数组
二维数组:若一维数组中的数据元素又是一维数组结构,则称为二维数组。
声明格式:数据类型 变量名称[行数] [列数] ;
在C语言中,一个二维数组类型也可以定义为一维数组类型(其分量类型为一维数组类型),即:
typedef elemtype array2[m][n];
等价于:
typedef elemtype array1[n];
typedef array1 array2[m];
三维数组
三维数组:若二维数组中的元素又是一个一维数组,则称作三维数组。
n维数组:若n-1维数组中的元素又是一个一维数组结构,则称作n维数组。
数组特点:结构固定——定义后,维数和维界不再改变。
数组基本操作:除了结构的初始化和销毁之外,只有取元素和修改元素值的操作。
数组的抽象数据类型定义
n为数组的维数
bi为数组第i维的长度
ji为数组元素第i维的下标
例:二维数组的抽象数据类型的数据对象和数据关系的定义
n=2(维数为2,二维数组)
b1:第1维长度(行数) b2:第2维长度(列数)
aj1j2:第1维下标为j1,第2维下标为j2
数组的顺序存储
因为:
- 数组特点:结构固定—维数和维界不变
- 数组基本操作:初始化、销毁、取元素、修改元素值。一般不做插入和删除操作。
所以:一般都是采用顺序存储结构来表示数组。
注意:数组可以是多维的,但存储数据元素的内存单元地址是一维的,因此,在存储数组结构之前,需要解决多维关系映射到一维关系的问题。
存储单元是一维结构,而数组是个多维结构,则用一组连续存储单元存放数组的数据元素就有个次序约定问题。
- 以行序为主序
- 以列序为主序
二维数组的行序优先表示
以行序为主序:
设数组开始存储位置LOC(0,0),存储每个元素需要L个存储单元
数组元素a[i][j]的存储位置是:LOC(i,j)= LOC(0,0)+(n*i+j)*L
三维数组
按页/行/列存放,页优先的顺序存储
a[m1][m2][m3]各维元素个数为m1,m2,m3
下标为i1,i2,i3的数组元素的存储位置:
n维数组
各维元素个数维m1,m2,m3,…,mn
下标为i1,i2,i3,…,in的数组元素的存储位置:
例:设有一个二维数组A[m][n]按行优先顺序存储,假设A[0][0]存放位置在644(10),A[2][2]存放在676(10),每个元素占一个空间,问A[3][3](10)存放在什么位置?(脚注(10)表示用10进制表示。)
答:设数组元素A[i][j]存放在起始地址为Loc(i,j)的存储单元中
3.3 广义表
广义表(又称列表Lists)是n>=0个元素a0,a1,…,an-1的有限序列,其中一个ai或者是原子,或者是一个广义表。
-
广义表通常记作:LS=(a1,a2,…,an)
其中:LS为表名,n为表的长度,每一个ai为表的元素
习惯上,一般用大写字母表示广义表,小写字母表示原子。 -
表头:若LS非空(n>=1),则其中第一个元素a1就是表头。
记作head(LS) = a1。注:表头可以是原子,也可以是子表。 -
表尾:除表头之外的其他元素组成的表
记作tail(LS) = (a2,…,an)。
注:表尾不是最后一个元素,而是一个子表
广义表的性质
(1)广义表中的数据元素有相对次序;一个直接前驱和一个直接后继
(2)广义表的长度定义为最外层所包含元素的个数;
如:C=(a,(b,c))是长度为2的广义表。
(3)广义表的深度定义为该广义表展开后所含括号的重数;
A = (b,c)的深度为1,B=(A,d)的深度为2,C=(f,B,h)的深度为3。
注意:“原子”的深度为0;“空表”的深度为1。
(4)广义表可以为其他广义表共享;如:广义表B就共享表A。在B中不必列出A的值,而是通过名称来引用,B=(A)。
(5)广义表可以是一个递归的表。如:F=(a,F)=(a,(a,(a,…)))
注意:递归表的深度是无穷值,长度是有限值。
(6)广义表是多层次结构,广义表的元素可以是单元素,也可以是子表,而子表的元素还可以是子表,…。
可以用图形象地表示。
广义表与线性表地区别
广义表可以看成是线性表的推广,线性表是广义表的特例。
广义表的结构相当灵活,在某种前提下,它可以兼容线性表、数组、树和有向图等各种常用的数据结构。
当二维数组的每行(或每列)作为子表处理时,二维数组即为一个广义表。
另外,树和有向图可以用广义表来表示。
由于广义表不仅集中了线性表、数组、树和有向图等常见数据结构的特点,而且可有效地利用存储空间,因此在计算机的许多应用领域都有成功使用广义表的实例。
广义表的基本运算
(1)求表头GetHead(L):非空广义表的第一个元素,可以是一个单一的元素,也可以是一个子表
(2)求表尾GetTail(L):非空广义表除去表头元素以外其他元素所构成的表。表尾一定是一个表
4.1 树和二叉树的定义
树的定义
树(Tree)是n(n>=0)个结点的有限集。
若n = 0,称为空树;
若n > 0,则它满足如下两个条件:
(1)有且仅有一个特定的称为根(Root)的结点;
(2)其余结点可分为m(m>=0)个互不相交的有限集T1,T2,T3,…,Tm,其中每一个集合本身又是一棵树,并称为根的子树(SubTree)。
树的基本术语
树的深度:树中结点的最大层次
森林:是m(m>=0)棵互不相交的树的集合。
把根结点删除树就变成了森林。
一棵树可以看成是一个特殊的森林
给森林中的各子树加上一个双亲结点,森林就变成了树。
树一定是森林,森林不一定是树。
树结构和线性结构的比较
二叉树的定义
为何要重点研究每结点最多只有两个“叉”的树?
- 二叉树的结构最简单,规律性最强
- 可以证明,所有树都能转为唯一对应的二叉树,不失一般性
普通树(多叉树)若不转化为二叉树,则运算很难实现
二叉树在树结构的应用中起着非常重要的作用,因为对二叉的许多操作算法简单,而任何树都可以与二叉树相互转换,这样就解决课树的存储结构及其运算中存在的复杂性。
二叉树是n(n>=0)个结点的有限集,它或者是空集(n=0),或者由一个根节点及两棵互不相交的分别称作这个根的左子树和右子树的二叉树组成。
特点:
1、每个结点最多有俩孩子(二叉树中不存在度大于2的结点)。
2、子树有左右之分,其次序不能颠倒。
3、二叉树可以是空集合,根可以有空的左子树或空的右子树。
注:二叉树不是树的特殊情况,它们是两个概念。
二叉树结点的子树要区别左子树和右子树,即使只有一棵子树也要进行区分,说明它是左子树,还是右子树。
树的结点只有一个孩子时,就无须区分它是左还是右次序。因此二者是不同的。这是二叉树与树的最主要的差别
(也就是二叉树每个结点位置或者说次序都是固定的,可以是空,但是不可以说它没有位置,而树的结点位置时=是相对于别的结点来说的,没有别的结点时,它就无所谓左右了)
思考
具有三个结点的二叉树可能有几种不同形态?普通树呢?
二叉树有五种形态:
树有两种形态:
二叉树的5种基本形态
注:虽然二叉树与树概念不同,但有关树的基本术语对二叉树都适用。
树和二叉树的抽象数据类型定义
二叉树抽象数据类型定义
ADT BinaryTree{
数据对象D: D是具有相同特性的数据元素的集合
数据关系R: 若D = ∅,则R = ∅;
若D ≠ ∅,则R = {H}; H是如下二元关系:
1. root唯一 //关于根的说明
2. D~j~∩D~k~=∅ //关于子树不相交的说明
3. …… //关于数据元素的说明
4. …… //关于左子树和右子树的说明
基本操作P://至少有20个
}ADT BinaryTree
CreateBiTree(&T,definition)
初始条件:definition给出二叉树T的定义
操作结果:按definition构造二叉树T。
PreOrderTraverse(T)
初始条件:二叉树T存在
操作结果:先序遍历T,对每个结点访问一次。
InOrderTraverse(T)
初始条件:二叉树T存在
操作结果:中序遍历T,对每个结点访问一次。
PostOrderTraverse(T)
初始条件:二叉树T存在
操作结果:后序遍历T,对每个结点访问一次。
二叉树的性质和存储结构
- 性质1:在二叉树的第i层上至多有2i-1个结点(i>=1)
提问:第i层上至少有1个结点
- 性质2:深度为k的二叉树至多有2k-1个结点(k>=1)。
提问:深度为k时至少有k个结点 - 性质3:对任何一棵二叉树T,如果其叶子数为n0,度为2的结点数为n2,则n0 = n2 + 1
两种特殊形式的二叉树
- 满二叉树
- 完全二叉树
为什么要研究这两种特殊形式?
因为它们在顺序存储方式下可以复原!
满二叉树
一颗深度为k且有2k-1个结点的二叉树称为满二叉树。
特点:
1.每一层上的结点数都是最大结点数(即每层都满)
2.叶子节点全部在最底层
对满二叉树结点位置进行编号
- 编号规则:从根节点开始,自上而下,自左而右。
- 每一结点位置都有元素。
思考:下图中的二叉树时满二叉树吗?
不是满二叉树,叶子不在同一层上。且最后一层结点个数不满。
满二叉树在同样深度的二叉树中结点个数最多
满二叉树在同样深度的二叉树中叶子结点个数最多
完全二叉树
深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点——对应时,称之为完全二叉树。
注:在满二叉树中,从最后一个结点开始,连续去掉任意个结点,即是一颗完全二叉树。
一定是连续的去掉!!!
特点:
1.叶子只可能分布在层次最大的两层上。
2.对任一结点,如果其右子树的最大层次为i,则其左子树的最大层次必为i或i+1。
满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。