- 注意:类C语言使用引用传参(&),是借用C++语法中的内容,C语言实现时使用指针(*)传参,与类C代码有很大不同
2.线性表
案例:稀疏多项式的运算
- 创建一个新数组c
- 分别从头遍历比较a和b的每一项
- 指数相同:对应系数相加,若其和不为零,则在c中增加一个新项
- 指数不相同:则将较小项复制到c中
- 当其中一个多项式遍历完毕,将另一个剩余项复制到c中即可
线性表的基本操作:
-
InitList(&L)
- 线性表的初始化
- 操作结果:构建一个空的线性表L
-
DestroyList(&L)
- 初始条件:线性表L已经存在
- 操作结果:销毁线性表L(表本身消失)
-
ClearList(&L)
- 初始条件:线性表L已经存在
- 操作结果:将线性表L重置为空表(表仍存在)
-
ListEmpty(L)
- 初始条件:线性表L已经存在
- 操作结果:若线性表为空,则返回TURE;否则返回FALSE
-
ListLength(L)
- 初始条件:线性表L已经存在
- 操作结果:返回线性表中元素个数
-
GetElem(L,i,&e)
- 初始条件:线性表L已经存在,1<=i<=ListLength(L)
- 操作结果:用e返回线性表L中第i个元素的值
-
LocateElem(L,e,compare())
- 初始条件:线性表L已经存在,compare()是数据元素判定函数
- 操作结果:返回L中第一个与e满足compare()的数据元素的位序,若不存在,则返回0
-
PriorElem(L,cur_e,&pre_e)
- 初始条件:线性表L已经存在
- 操作结果:若cur_e,是L的数据元素,且不是第一个,则用pre_e返回他的前驱,否则操作失败,pre_e无意义
-
NextElem(L,cur_e,&next_e)
-
初始条件:线性表L已经存在
-
操作结果:若cur_e,是L的数据元素,且不是最后一个,则用next_e返回他的前驱,否则操作失败,next_e无意义
-
-
ListInsert(&L,i,e)
- 初始条件:线性表L已经存在,i<=i<=ListLength(L)+1
- 操作结果:在L的第i个位置前插入新的元素e,L的长度加一
-
ListDelete(&L,i,&e)
- 初始条件:线性表L已经存在,i<=i<=ListLength(L)
- 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减一
-
ListTraverse(&L,visited())
- 初始条件:线性表L已经存在
- 操作结果:依次对线性表L中每个元素调用visited()
算法中用到的预定义常量和类型
-
//函数结果状态代码 #define TURE 1 #define FALSE 0 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 //Status 是函数的类型,其值是函数结果状态代码 typedef int Status; typedef char ElemType;
顺序表
1.顺序表中元素存储位置的计算
- 假设线性表的每个元素需占用i个存储单元,则第i+j个元素的存储位置和第i个元素的存储位置之间满足关系:
- LOC(a(n+1))=LOC(a(n))+i
2.顺序表的特点
- 以物理位置相邻表示逻辑关系,任一元素均可随机存取
3.顺序表
- 地址连续
- 依次存放
- 随机存取
- 类型相同
4.顺序表的类C语言代码实现
-
#define LIST_INIT_SIZE 100//线性表存储空间的初始分配量 typedef struct { ElemType elem[LIST_INIT_SIZE];//方案1:静态数组方式 ElemType *elem;//方案2:动态内存分配方式 int length;//当前长度 }SqList; SqList L;//定义变量L,L是SqList这种类型的变量,L是个顺序表 L.elem=(ElemType*)malloc(sizeof(ElemType)*MAXSIZE);//动态内存分配
-
多项式的顺序存储结构类型定义
-
#define MAXSIZE 1000 //多项式可能达到的最大长度 typedef struct //多项式非零项的定义 { float p;//系数 int e;//指数 }Polynomial;//多项式 typedef struct { Polynomial *elem;//存储空间的首地址,以自定义数据类型建立的数组 int length;//多项式中当前项的个数 }SqList;//多项式的顺序存储结构类型为SqList
-
-
图书表的顺序存储结构类型定义
-
#define MAXSIZE 10000 //图书表可能达到的最大长度 typedef stcuct //单本图书信息定义 { char no[20]; //图书ISBN char name[50]; //图书名字 float price; //图书价格 }Book; typedef struct { Book *elem; //存储空间的基地址 int length; //图书表中的当前图书个数 }SqList; //图书表的顺序存储结构为SqList
-
-
操作算法中用到的预定常量和类型
-
//函数结果状态代码 #define TURE 1 #define FALSE 0 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 //Status 是函数的类型,其值的函数结果代码 typedef int Status typedef char ElemType
-
5.顺序表基本操作的实现(类C语言代码)
算法1:线性表L的初始化(参数用引用)
Status InitList_Sq(SqList &L) //构造一个空的顺序表L
{
L.elem=new ElemType[MAXSIZE]; //为顺序表分配空间,C++类型??
if(!L.elem)
exit(OverFLOW); //存储分配失败,异常处理
L.length=0; //空表长度为0
return 0;
}
算法2:销毁线性表L
void DestroyList(SqList &L)
{
if(L.elem)
delete L.elem; //释放空间,C++语法,具体C语言代码实现不是这样
}
算法3:清空线性表L
void ClearList(SqList &L)
{
L.length=0; //将线性表长度置为0
}
算法4:求线性表长度
int GetLength(SqList L)
{
return (L.length);
}
算法5:判断线性表L是否为空
int IsEmpty(SqList L)
{
if(L.length==0)
return 1; //若为空,则返回1
return 0;
}
算法6:顺序表取值(根须位置i获取相应位置数据元素的值,用e返回)
Status 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;
}
//随机存取,时间复杂度为O(1)
算法7:顺序表的查找
//在线性表L中查找与指定值e相同的数据元素的位置
//从表的一端开始,逐个进行记录的关键字和给定值的比较。找到,返回该元素的位置序号,未找到,则返回0
int LocateElem(SqList L,ElemType e)
{
for(i=0;i<L.length;i++)
{
if(L.length[i]==e)
return i+1;//eg:数组a[0]的位置序号是1
}
return 0;//未找到,返回0
}
算法8:顺序表的插入
/*算法思想:
1.判断插入位置i是否合法(i<1||i>L.length+1)
2.判断顺序表的存储空间是否已满,若已满则返回ERROR
3.将第n至第i位的元素依次向后移动一个位置,空出第i个位置
4.将要插入的新元素放入第i个位置
*/
Status ListInsert_Sq(SqList &L,int i,ElemType e)
{
if(i<1||i>L.length+1)
return ERROR; //i值不合法
if(L.length==MAXSIZE)
return ERROR; //当前存储空间已满
for(j=L.length-1;j>=i-1;j++)
L.elem[j+1]=L.elem[j]; //插入位置及之后元素后移
L.elem[i-1]=e; //将新元素e放入第i个位置
L.length++; //表长加一
return OK;
}
算法9:顺序表的删除
/*算法思想:
1.判断删除位置是否合法(1<i<n)
2.将第i+1至n位的元素依次向前移动一个位置
3.表长减一,返回OK
*/
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.在链表中设置头结点有什么好处?
- 便于首元结点的处理
- 首元结点的地址保存在头结点的指针域中,所以在链表第一个位置上的操作和其他位置一致,无需进行特殊处理
- 便于空表和非空表的统一处理
- 无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了
2.单链表结点的定义:
-
typedef struct Lnode { ElemType data; //结点的数据域 struct Lnode *next; //结点的指针域 }Lnode,*LinkList; //LinkList为指向结构体Lnode的指针类型 //定义一个结构体类型指针的两种方式 Lnode *p; LinkList L; //一般区分使用: //定义链表L LinkList L; //定义结点指针p: Lnode *p;
3.单链表结构的定义和表示
-
eg:存储学生学号、姓名、成绩的单链表结点类型定义
-
方式a:不常用,因为存在多个数据域
-
typedef struct student { char num[8];//学号 char name[8];//姓名 int score;//成绩 struct student *next; //指针域 }Lnode,*LinkList; LinkList L;
-
-
方式b:常用,将多个数据的内容先定义成一个结构体
-
typedef struct { char num[8];//学号 char name[8];//姓名 int score;//成绩 }ElemType; typedef struct Lnode { ElemType data; struct Lnode *next; }Lnode,*LinkList;
-
-
4.单链表基本操作的类C语言实现
算法1:单链表的初始化
-
/*算法步骤: 1.生成新节点作为头结点,用头指针L指向头结点 2.将头结点的指针域置空 */ Status InitList_L(LinkList &L) { L=(LinkList)malloc(sizeof(Lnode)); L->next=NULL; return OK; }
算法2:判断链表是否为空
-
//空表:链表中无元素,但头指针和头结点仍存在 //算法思路:判断头结点指针域是否为空 int ListEmpty(LinkList L) //若为空返回1,否则返回0 { if(L->next) return 0; else return 1; }
算法3:单链表的销毁,销毁后不存在
-
//算法思路:从头指针开始,依次释放所有结点 Status DestroyList_L(LinkList &L) { Lnode *p; while(L) { p=L; L=L->next; free(p); } return OK; }
算法4:清空单链表
-
//依次释放所有结点,并将头结点指针域设置为空 Status ClearList_L(LinkList &L) { Lnode *p,*q; p=L->next; while(p) { q=p->next; free(p); p=q; } L->next=NULL; return 0; }
算法5:求单链表表长
-
//算法思路:从首元结点开始,依次计数所有结点 int ListLength_L(LinkList L) { Lnode *p; p=L->next;//p指向第一个结点 i=0; while(p) { i++; p->next; } return i; }
算法6:取第i个元素的值
-
按位查找:
-
Status GetElem_L(LinkList L,int i,ElemType &e) { p=L-next; //p指向首元结点 j=1; while(p&&j<i) //向后扫描直到p指向第i个元素或p为空 { p=p->next; ++j; } if(!p||j>i) return ERROR; //第i个元素不存在 e=p->data; return OK; }
-
-
按值查找:根据指定数据获取该数据所在的位置(地址或序号)
-
返回地址
Lnode *LocateElem_L(LinkList L,ElemType e) { p=L->next; while(p&&p->data!=e) { p=p->next; } return p; }
-
返回序号
int LocateElem_L(LinkList L,ElemType e) { p=L->next; j=1; while(p&&p->data!=e) { p=p->next; j++; }//如果没找到,则p为空 if(p) return j; return 0;//查找失败返回0 }
-
算法7:插入结点(在第i个结点前插入值为e的新结点)
-
Status ListInsert_L(LinkList &L,int i,ElemType e) { p=L; j=0; while(p && j<i-1) //寻找第i-1个结点,p指向i-1结点 { p=p->next; j++; } if(!p||j>i-1) return ERROR; //i大于表长+1或小于1,插入位置非法 s=(*Lnode)malloc(sizeof(Lnode)); //新建一个s结点(可能有问题) s->data=e; //生成新结点,将结点的数据域置为e s->next=p->next; p->next=s; //顺序不可颠倒 return OK; }
算法8:删除第i个结点(删除第i个元素)
-
Status ListDelete_L(LinkList &L,int i,ElemType &e) { p=L; //指向头结点 j=0; while(p->next&&j<i-1) //寻找第i-1个结点,且第i个结点不能为空 { p=p->next; j++; } if(!(p->next)||j>i-1) return ERROR; q=p->next; //临时保存被删结点的地址以备释放 p->next=q->next; e=q->data; //存放删除结点的数据 free(q);//释放删除结点的空间 return OK; }
算法9:头插法建立单链表
-
void CreateList_H(LinkList &L,int n) //n 为插入的元素个数 { L=(*Lnode)malloc(sizeof(Lnode)); //头结点 L->next=NULL; for(i=n;i>0;i--) {0 p=(*Lnode)malloc(sizeof(Lnode)); //新结点 //输入元素,存在p->data; p->next=L->next; L->next=p; } } //时间复杂度O(n)
算法10:尾插法建立单链表
-
/* 1.从一个空表L开始,将新结点逐个插入到链表尾部,尾指针r指向链表尾部 2.初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点 */ void CreateList_R(LinkList &L,int n) { L=(*Lnode)malloc(sizeof(Lnode)); //头结点 L->next=NULL; r=L; //尾指针指向头结点 for(i=0;i<n;i++) { p=(*Lnode)malloc(sizeof(Lnode)); //新结点 //输入元素,存在p->data; p->next=NULL; r->next=p; r=p; //尾指针指向新的尾结点 } } //时间复杂度O(n)
5.循环链表:头尾相接的链表(最后一个结点的指针域指向头结点,整个链表形成一个环)
-
优点:从表中任一结点出发均可找到表中其他结点
-
注意:循环链表中没有NULL指针,故涉及遍历操作时,终止条件为是否等于头指针
-
eg:带尾指针循环链表的合并(尾指针Ta和Tb)
-
操作步骤:
- p存Ta的表头结点:p=Ta->next
- Tb表头连接到Ta表尾:Ta->next=Tb->next->next
- 释放Tb表头结点:free(Tb-next)
- Tb指向Ta的头结点:Tb->next=p
-
LinkList Connect(LinkList Ta,LinkList Tb) { p=Ta->next; Ta->next=Tb->next->next; free(Tb-next); Tb->next=p; return Tb; }
-
6.双向链表:在单链表的每个结点里再增加一个指向其直接前驱的指针域prior
-
双向链表的结构定义:
-
typedef struct DuLnode { ElemType data; struct DuLnode *prior,*next; }DuLnode,*DuLinkList;
-
-
双向链表的对称性:
- p->prior->next=p=p->next->prior
-
双向链表中,插入、删除操作需要同时修改两个方向上的指针
-
双向链表的插入操作:
-
void ListInsert_DuL(DuLinkList &L;int i;ElemType e) //在带头结点的双向链表L的第i个位置前插入元素e { if(!(p=GetElemP_DuL(L,i))) //找到第i个元素 return ERROR; s=(*DuLnode)malloc(sizeof(DuLnode)); s->data=e; s->prior=p->prior; //将新结点的前驱改为p的前驱 p->prior->next=s; //将p的前驱的后继改为新结点 p->prior=s; //p的前驱改为新结点s s->next=p; //新结点s的后继改为p }
-
-
双向链表的删除:
-
void ListDelete_DuL(DuLinkList &L;int i;ElemType &e) //删除第i个元素,并用e返回 { if(!(p=GetElemP_DuL(L,i))) //找到第i个元素 return ERROR; e=p->data; p->prior->next=p->next; //p的前驱的后继改为p的后继 p->next->prior=p->prior; //p的后继的前驱改为p的前驱 free(p); }
-
-
7.单链表、循环链表和双向链表的时间效率比较
-
查找首元结点 查找表尾结点 查找结点*p的前驱结点 带头结点的单链表L L->next 时间复杂度O(1) 从L->next依次向后遍历,时间复杂度O(n) 通过p->next无法找到前驱 带头结点仅设头指针L的循环单链表 L->next 时间复杂度O(1) 从L->next依次向后遍历,时间复杂度O(n) 通过p->next可以找到前驱,时间复杂度O(n) 带头结点仅设尾指针R的循环单链表 R->next->next 时间复杂度O(1) R,时间复杂度O(1) 通过p->next可以找到前驱,时间复杂度O(n) 带头指针的双向循环链表L L->next 时间复杂度O(1) L->prior,时间复杂度O(1) p->prior,时间复杂度O(1)顺序表和链表的比较
8常见问题:
-
为什么单链表初始化时用二级指针,而插入操作时用一级指针?
-
在初始化过程中,需要修改头指针,因此要用到二级指针传递头指针的地址,这样才能修改头指针。这与普通变量类似,当需要修改普通变量的值,需传递其地址。使用二级指针,很方便就修改了传入的结点一级指针的值。 如果用一级指针,则只能通过指针修改指针所指内容,却无法修改指针的值,也就是指针所指的内存块。
-
在使用带头结点的单链表时
1、初始化链表头部指针需要用二级指针
2、销毁链表需要用到二级指针
3、插入、删除、遍历、清空结点用一级指针即可注意:
如果是不带头结点的单链表,插入、删除和清空结点也需要二级指针(比如往空链表中插入一个节点时,新插入的节点就是链表的头指针,此时会改动头指针。同理,删除第一个结点和清空结点都会改动头指针)。
-
顺序表的链表的比较
- 链式存储结构:
- 优点:
- 节点空间可以动态申请和释放
- 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素
- 缺点:
- 存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大(存储密度=结点数据所占空间/结点所占的空间总量)
- 链式存储结构时非随机存取结构》对任意结点的操作都要从头指针依指针链查找到该结点
- 优点:
- 空间:
- 存储空间:
- 顺序表:预先分配,会导致空闲限制或溢出
- 链表:动态分配,不会出现存储空间闲置或溢出
- 存储密度:
- 顺序表:不用为表示结点间的逻辑关系而额外增加存储开销,存储密度为1
- 链表:需要借助指针来体现元素间的逻辑关系,存储密度小于1
- 存储空间:
- 时间:
- 存取元素:
- 顺序表:随机存取,按位置访问元素时间复杂度为O(1)
- 链表:顺序存取,按位置访问元素时间复杂度为O(n)
- 插入、删除:
- 顺序表:平均移动表中一半元素,时间复杂度为O(n)
- 链表:不需要移动元素,确定位置后,时间复杂度为O(1)
- 存取元素:
- 使用情况:
- 顺序表:
- 表长变化不大,且能事先确定变化的范围
- 很少进行插入或删除的操作,经常按元素位置序号访问数据元素
- 链表:
- 长度变化很大
- 频繁进行插入或删除操作
- 顺序表:
线性表的应用
1.线性表的合并
-
算法步骤:
- 依次取出Lb中的每个元素,执行以下操作:
- 在La中查找该元素
- 如果找不到,则将其插入到La的最后
- 依次取出Lb中的每个元素,执行以下操作:
-
void union(List &La,List Lb)//仅表示思路 { La_len=ListLength(La);//求线性表La的长度 Lb_len=ListLength(Lb); for(i=1;i<Lb_len;i++)//遍历Lb中元素 { GetElem(Lb,i,e); if(!(LocateElem(La,e)))//执行条件为,在La中找不到元素e ListInsert(La,++La_len,e);//插入到第表长加一个位置 } }
2.有序表的合并(顺序表)
-
已知线性表La和Lb中的数据元素非递减有序排列,现要求将两表归并为一个新的非递减有序排列
-
算法步骤:
- 创建一个空表c
- 依次从La和Lb中摘取元素值较小的结点插入到Lc表的最后,直至其中一个表为空
- 继续将没有空的那个表的剩余结点插入到Lc表的最后
-
void MergeList_Sq(SqList LA,SqList LB,SqList &LC) { pa=LA.elem; pb=LA.elem;//指针pa、pb分别指向两个表的第一个元素 LC.length=LA.length+LB.length; LC.elem=(*ElemType)malloc(LC.length*sizeof(Elemtype)); pc=LC.elem;// pa_last=LA.elem+LA.length-1;//指向LA表最后一个元素,用来记录是否遍历到了表尾 pb_last=LB.elem+LB.length-1; while(pa<=pa_last&&pb<=pb_last)//两表都不为空时 { if(*pa<=*pb) { *pc=*pa; pc++;//指针右移 pa++; //或直接写成:*pc++=*pa++; } else { *pc=*pb; pc++;//指针右移 pb++; //或直接写成:*pc++=*pb++; } while(pa<=pa_last)//此时LB表已遍历完,将LA中剩余元素加入LC *pc++=*pa++; while(pb<=pb_last)//此时LA表已遍历完,将LB中剩余元素加入LC *pc++=*pb++; } }
3.有序表的合并(链表)
-
void MergeList_L(LinkList &La,LinkList &Lb,LinkList &Lc) { pa=La->next;//pa指针指向La的首元结点 pb=Lb->next; pc=Lc=La; while(pa && pb)//两表都不为空时 { if(pa->data<pb->data) { pc->next=pa;//将pa接在pc之后 pc=pa;//pc移动到pa pa=pa->next;//pa后移 } else { pc->next=pb; pc=pb; pb=pb->next; } } pc->next=pa?pa:pb;//pa不为空则指向pa free(Lb);//释放Lb的头结点 }
案例分析和实现
案例一:一元多项式的运算
- 实现两个多项式加、减、乘运算
案例二:稀疏多项式的运算
顺序表实现
-
算法思路:
- 创建一个新数组c
- 分别从头遍历比较a和b的每一项
- 指数相同:对应系数相加,若其和不为零,则在c中增加一个新项
- 指数不相同:则将较小项复制到c中
- 当其中一个多项式遍历完毕,将另一个剩余项复制到c中即可
-
问题分析:
- 顺序存储结构存在问题:
- 存储空间分配不灵活
- 运算空间复杂度高
- 顺序存储结构存在问题:
-
算法实现:
-
链表实现
-
算法思路:
- 创建一个只有头结点的空链表
- 根据多项式的个数n,循环n次执行以下操作:
- 生成一个新结点s
- 输入多项式当前项的系数和指数赋值给新结点s的数据域
- 设置一个前驱指针pre,用于指向待找到的第一个大于输出项指数的结点的前驱,pre初值指向头结点
- 指针q初始化,指向首元结点
- 循链向下逐个比较链表中当前结点与输入项指数,找到第一个大于输出项的结点q
- 将输入项结点s插入到结点q之前
-
算法描述:
-
多项式结点的定义:
-
typedef struct PNode { float coef;//系数 int expn;//指数 struct PNode *next;//指针域 }PNode,*polynomial;
-
-
多项式创建:
-
void Createpolyn(Polynomial &P,int n) { P=(*PNode)malloc(sizeof(PNode));//先建立一个带头结点的单链表 P->next=NULL; for(i=1;i<=n;i++) { s=(*PNode)malloc(sizeof(PNode));//生成新结点 //输入系数和指数 pre=P;//pre用于保存q的前驱,初值为头结点 q=P->next;//q指向首元结点 while(q && q->expn<s->expn) { pre=q; q=q->next; } s->next=q;//将输入项s插入到q和其前驱结点pre之间 pre->next=s; } }
-
-
多项式相加:
- 算法步骤:
- 指针p1和p2初始化,分别指向Pa和Pb的首元结点
- p3指向和多项式的当前节点,初值为Pa的头结点
- 当指针p1和p2均未到达相应表尾时,循环比较p1和p2所指结点对应的指数(p1->expn和p2->expn),有三种情况
- 当p1->expn==p2->expn时,则将两个结点中的系数相加
- 若和不为零,则修改p1所对应的结点的系数值,同时删除p2所指结点
- 若和为零,则删除p1和p2所指结点
- 当p1->expnexpn时,则摘取p1所指结点到“和多项式”链表中
- 当p1->expn>p2->expn时,则摘取p2所指结点到“和多项式”链表中
- 当p1->expn==p2->expn时,则将两个结点中的系数相加
- 将非空多项式剩余段插入p3所指结点之后
- 释放Pb的头结点
- 算法步骤:
-
案例三:图书信息管理
-
结构类型定义
-
struct Book { char id[20];//ISBN char name[50];//书名 int price;//定价 } typedef struct { Book *elem; int length; }SqList;//顺序表定义 typedef struct LNode { Book data; struct LNode *next; }LNode,*LinkList;//链表定义
-
3.栈和队列
栈和队列的定义和特点
-
栈和队列是限定插入和删除只能在表的端点进行的线性表
-
栈(stack)是一个特殊的线性表,是限定在一端进行插入和删除的线性表,后进先出(Last In First Out),简称(LIFO)
- 栈仅在表尾(栈顶)进行插入、删除
- 表尾(an)称为栈顶(Top),表头(a1)称为(Base)
- eg:栈 s=(a1,a2,…an);
- 插入到栈顶称为入栈,从栈顶删除叫出栈
-
队列(queue),是一种先进先出(First In First Out)的线性表(FIFO),在表一端(表尾)插入,在另一端(表头)删除
栈的应用
-
“先进后出,后进先出”
-
数值转换
-
表达式求值
-
括号匹配的检验
-
八皇后问题
-
行编辑程序
-
函数调用
-
迷宫求解
-
递归调用的实现
-
队列的应用
- “先进先出,后进后出”,类似排队问题
- 脱机打印:按申请的先后顺序依次输出
- 多用户系统中,多个用户排队,分别循环使用CPU和主存
- 按用户的优先级排成队,每个优先级一个队列
- 实时控制系统,信号按接收的先后顺序依次处理
- 网络电文传输,按到达的时间先后顺序依次处理
案例引入
案例一:进制转换
案例二:括号匹配的检验
案例三:表达式求值
案例四:舞伴问题
栈的表示和实现
栈的抽象数据类型定义
-
ADT Stack{ 数据对象: D={ai|ai∈ElemSet,i=1,2,3,......,n,n>=0}; 数据关系 R1={<ai-1,ai>|ai-1,ai∈D,i=2,...,n}; 基本操作:初始化、进栈、出栈、取栈顶元素等; }ADT Stack
栈的基本操作
- InitStack(&S)
- 栈的初始化
- 操作结果:构造一个空栈S
- DestroyStack(&S)
- 栈的销毁
- 初始条件:栈S已经存在
- 操作结果:栈S被销毁
- StackEmpty(S)
- 判定栈S是否为空栈
- 初始条件:栈S已经存在
- 操作结果:若栈S为空栈,则返回TURE;否则返回FALSE
- StackLength(S)
- 求栈的长度
- 初始条件:栈S已经存在
- 操作结果:返回S的元素个数,即栈的长度
- GetTop(S,&e)
- 取栈顶元素
- 初始条件:栈S已经存在,且非空
- 操作结果:用e返回S的栈顶元素
- ClearStack(&S)
- 清空栈
- 初始条件:栈S已经存在
- 操作结果:将栈S清空
- Push(&S,e)
- 入栈
- 初始条件:栈S已经存在
- 操作结果:插入元素e为新的栈顶元素
- Pop(&S,&e)
- 出栈
- 初始条件:栈S已经存在
- 操作结果:删除栈顶元素an,并用e返回其值
顺序栈的表示和实现
存储方式:
-
同一般线性表的顺序存储结构完全相同
-
利用一组地址连续的存储单元依次存放自栈底到栈顶的元素,栈底一般在低地址处
-
附设top指针,指示栈顶元素在顺序栈中的位置
-
另设base指针,指示栈底元素在顺序栈中的位置
-
但是为了方便操作,通常top指示真正的栈顶元素之上的下标地址
-
另外,用stacksize表示栈可使用的最大容量
-
空栈标志:top==base
-
栈满标志:top-base==stacksize
- 栈满时的处理方法:
- 报错,返回操作系统
- 分配更大空间作为栈的存储空间,将原栈的内容移入新栈
- 栈满时的处理方法:
-
上溢(overflow):栈已满,还要压入元素
-
下溢(underflow):栈已空,还要弹出元素
-
注意:上溢是一种错误,使问题无法继续执行;而下溢一般认为是一种结束条件,使问题处理结束
-
顺序栈的表示(类C)
-
#define MAXSIZE 100 typedef struct { SElemType *base;//栈底指针 SElemType *top;//栈顶指针 int stacksize;//栈可用最大容量 }SqStack;
顺序栈的基本操作实现
算法1:顺序栈的初始化
-
Status InitStack(SqStack &S) { S.base=(SElemType*)malloc(MAXSIZE*sizeof(SElemType)); if(!S.base) exit(OVERFLOW);//存储分配失败 S.top=S.base;//栈顶指针等于栈底指针 S.stacksize=MAXSIZE; return OK; }
算法2:顺序栈判断是否为空
-
Status StackEmpty(SqStack S) //若栈为空,返回TURE;若不为空,返回FALSE { if(S.top == S.base) return TURE; return FALSE; }
算法3:求顺序栈长度
-
int StackLength(SqStack S) { return S.top-S.base;//涉及指针减指针这一特殊情况 }
算法4:清空栈
-
Status ClearStack(SqStack S) { if(S.base) S.top=S.base;//直接将栈顶指针指向栈底指针 return OK; }
算法5:销毁栈
-
Status DestroyStack(SqStack &S) { if(S.base) { free(S.base); S.stacksize=0 ; S.base=S.top=NULL; } return OK; }
算法6:顺序栈的入栈
-
/* 1.判断是否栈满,若满则返回上溢 2.元素e压入栈顶 3.栈顶指针加一 */ Status Push(SqStack &S,SElemType e) { if(S.top-S.base == stacksize) return ERROR;//或OVERFLOW *S.top=e; S.top++; return OK; }
算法7:顺序栈的出栈
-
/* 1.判断是否栈空,若空则下溢 2.获取栈顶元素e 3.栈顶指针减一 */ Status Pop(SqStack &S,SElemType &e) { if(S.top == S.base) return ERROR; --S.top; e=*S.top; return OK; }
链栈的表示和实现
链栈的表示
-
链栈是操作受限的单链表,只能在链表头部进行操作
-
typedef struct StackNode { SElemType data; struct StackNode *next; }StackNode,*LinkStack; LinkNode S;//
-
链表的头指针就是栈顶
-
不需要头结点
-
基本不存在栈满的情况
-
空栈相当于头指针指向空
-
插入和删除仅在栈顶处执行
链栈的基本操作实现
算法1:链栈的初始化
-
void InitStack(LinkStack &S) { //构建一个空栈,栈顶指针置空 S=NULL; return OK; }
算法2:判断链栈是否为空
-
Status StackEmpty(LinkStack S) { if(S==NULL) return TURE; return FALSE; }
算法3:链栈的入栈
-
Status StackPush(LinkStack &S,SElemType e) { p=(StackNode*)malloc(sizeof(StackNode));//生成新结点 p->data=e;//将新结点数据域置为e p-next=S;//将新结点插入栈顶 S=p;//修改栈顶指针 return OK; }
算法4:链栈的出栈
-
Status StackPop(LinkStack &S,SElemType &e) { if(S == NULL) return ERROR; e=S->data; p=S;//新建一个结点存放要释放的栈顶结点 S=S->next;//栈顶指针下移 free(p);//释放要删除的结点 return OK; }
算法5:取栈顶元素
-
SElemType GetTop(LinkStack S) { if(S != NULL) return S->data; }
栈和递归
递归的定义
- 若一个对象部分的包含他自己,或用他自己给自己定义,则称这个对象是递归的
- 若一个过程之间或间接地调用自己,则称这个过程是递归的过程
- ed:递归求n的阶乘
常用递归方法的情况
- 递归定义的数学函数
- 阶乘函数
- 斐波那契数列
- 具有递归特性的数据结构
- 二叉树
- 广义表
- 可递归求解的问题
- 迷宫问题
- 汉诺塔问题
递归问题
- 用分治法求解
- 分治法:对于一个较复杂的问题,能够分解成几个相对简单的且解法相同或类似的子问题来求解
- 必备的三个条件:
- 能够将一个问题转变成一个新问题,且新问题与原问题的解法相同或类同,不同的仅是处理的对象,且这些处理对象是有变化规律的
- 可以通过上述转化使问题简化
- 必须有一个明确的递归出口,或称为递归的边界
函数调用过程
- 调用前,系统完成:
- 将实参、返回地址等传递给被调用函数
- 为被调函数的局部变量分配存储区域
- 将控制转移到被调函数的入口
- 调用后,系统完成:
- 保存被调函数的计算结果
- 释放被调函数的数据区
- 依照被调函数保存的返回地址将控制转移到被调函数
队列的表示和实现
队列的抽象数据类型:
顺序队列的基本操作实现
队列的顺序表示
-
用一维数组base[MAXSIZE]
-
循环队列的结构
-
#define MAXSIZE 100//最大队列长度 typedef struct { QElemType *base;//初始化动态分配存储空间 int front;//头指针(实际是队头元素下标) int rear;//尾指针(实际是队尾元素下标) }SqQueue;
-
-
初始:
-
cfront=rear=0
-
-
入队
-
base[rear]=x; rear++;
-
-
出队
-
x=base[front]; front++; //空队标志 front==rear;
-
-
真溢出
-
front=0; rear=MAXSIZE;
-
-
假溢出
-
front!=0; rear=MAXSIZE;
-
解决假上溢的方法
-
将队中元素依次向队头方向移动
- 缺点:浪费时间,每移动一次,队中元素都要移动
-
将队空间想象成一个循环的表,即分配给队列的m个存储单元可以循环使用,当rear为MAXSIZE时,若向量的开始端空着,则又可以开始从头使用空着的空间,当front为MAXSIZE时,也是一样
-
引入循环队列:base[0]接在base[MAXSIZE-1]之后,若rear+1==M,则令rear=0;
-
实现方法:利用模运算
-
插入元素:
-
Q.base[Q.rear]=x; Q.rear=(Q.rear+1)%MAXSIZE;
-
-
删除元素:
-
x=Q.base[Q.front]; Q.front=(Q.front+1)%MAXSIZE;
-
-
循环队列队空队满标志冲突
- front==rear
- 解决方案:
- 另外设一个标志以区别队空队满
- 另设一个变量,记录元素个数
- 少用一个元素空间(采用)
- 队空:front==rear
- 队满:(rear+1)%MAXSIZE==front
-
-
循环队列的操作
算法1:队列的初始化
-
Status InitQueue(SqQueue &Q) { Q.base=(QElemType*)malloc(MAXSIZE*sizeof(QElemType));//分配数组空间 if(!Q.base) exit(OVERFLOW);//存储分配失败 Q.front=Q.rear=0;//头指针尾指针都置为零,队列为空 return OK; }
算法2:求队列长度
-
int QueueLength(SqQueue Q) { return (Q.rear-Q.front+MAXSIZE)%MAXSIZE;//解决了循环队列中尾指针到头指针前面的情况 }
算法3:循环队列入队
-
Status EnQueue(SqQueue &Q,QElemType e) { if((Q.rear+1)%MAXSIZE==front) return ERROR;//判断队满 Q.base[Q.rear]=e;//新元素加入队尾 Q.rear=(Q.rear+1)%MAXSIZE; return OK; }
算法4:循环队列出队
-
Status OutQueue(SqQueue &Q,QElemType &e) { if(Q.rear==Q.front) return ERROR;//判断队空 e=Q.base[front];//保存队头元素 Q.front=(Q.front+1)%MAXSIZE;//队头指针加一(但不会超过MAXSIZE-1) return OK; }
算法5:取队头元素
-
SElemType GetHead(SqQueue Q) { if(Q.front==Q.rear) return ERROR; return Q.base[Q.front];//返回队头元素的值,队头指针不动 }
链式队列的表示和实现
- 若用户无法估计所用队列的长度,则采用链队列
链队列的类型定义
-
tepedef struct QNode { QElemType data; struct QNode *next; }QNode,*QueuePtr;//链队列结点,链队列指针 typedef struct { QueuePtr front;//队头指针 QueuePtr rear;//队尾指针 }LinkQueue;//链队列
链队列的基本操作
算法1:链队列初始化
-
Status InitQueue(LinkQueue &Q) { Q.front=Q.rear=(QNode*)malloc(sizeof(QNode)); if(!Q.front) exit(OVERFLOW); Q.front->next=NULL;//队头指针置空 return OK; }
算法2:销毁链队列
-
//从队头结点开始依次释放所有结点 Status DestroyQueue(LinkQueue &Q) { while(Q.front) { p=Q.front->next;//存放下一个结点,否则直接删除Q.front会找不到下一结点 free(Q.front); Q.front=p;//移动队头结点 } return OK; }
算法3:链队列的入队
-
Status EnQueue(LinkQueue &Q,QElemType e) { p=(QNode*)malloc(sizeof(QNode)); if(!p) exit(OVERFLOW); if((Q.rear+1)%MAXSIZE == Q.front) return ERROR; p->data=e; p->next=NULL; Q.rear->next=p; Q.rear=p; return OK; }
算法4:链队列的出队
-
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; free(p); return OK; }
算法5:求链队列的队头元素
-
Status GetHead(LinkQueue Q,QElemType &e) { if(Q.front == Q.rear) return ERROR;//判断队空 e=Q.front->next->data; return OK; }
4.串、数组和广义表
串
串的定义:
- 零个或多个任意字符组成的有限序列
- eg:S=“abcdefg” (n>=0)
- S为串名
- abcdefg为串值
- n为串长;若n为零,则称为空串
- 几个术语:
- 空格串:是只包含空格的串,可以有一个或多个空格
- 注意它与空串的区别,空格串是有内容有长度的,而且可以不止一个空格
- 子串与主串:串中任意个数的连续字符组成的子序列称为该串的子串,相应地,包含子串的串称为主串
- 真字串:不包含自身的所有子串
- 字符位置:字符在序列中的序号为该字符在串中的位置
- 子串位置:子串的第一个字符在主串中的序号
- 串相等:当且仅当两个串的长度相等且对应位置上的字符都相同时,这两个串才相等
- 所有的空串都相等
- 空格串:是只包含空格的串,可以有一个或多个空格
案例引入
- 案例一:病毒感染检测
- 案例实现:
- 对于每一个待检测的任务,假设病毒DNA序列的长度是m,因为病毒DNA序列是环状的,为了线性取到每个可行的长度为m的字符串,可将存储病毒DNA序列的字符串长度扩大到2m,将病毒DNA序列连续存储两次
- 然后循环m次,依次取得每个长度为m的环状字符串,将此字符作为模式串,将人的DNA序列作为主串,调用BF算法进行模式匹配
- 只要匹配成功,即可终止循环,表明该人感染了病毒;否则,循环m次结束后,可通过BF算法的返回值判断该人是否感染了对应的病毒
- 案例实现:
串的抽象数据类型定义
-
ADT String { 数据对象:D={ai|ai∈CharacterSet,i=1,2,3,...,n,n>=0} 数据关系: 基本操作: 1.StrAssign(&T,chars)//串赋值 2.StrCompare(S,T)//串比较 3.StrLength(S)//求串长 4.Concat(&T,S1,S2)//串连结 5.SubString(&Sub,pos,len)//求子串 6.StrCopy(&T,S)//串拷贝 7.StrEmpty(S)//串判空 8.ClearString(&S)//清空串 9.lndex(S,T,pos)//子串的位置 10.Replace(&S,T,V)//串替换 11.StrInsert(&S,pos,T)//子串插入 12.StrDelete(&S,pos,len)//子串删除 13.DestroyString(&S)//串销毁 }ADT String
顺序串
顺序串的存储结构
-
用的更多,因为字符串操作很少需要插入删除
-
#define MAXSIZE 255 typedef struct { char ch[MAXSIZE+1];//存储串的一维数组,下标为0的位置闲置(在某些算法中会带来简便),从下标为1的位置开始存放 int length;//串的当前长度 }SString;//第一个S表示“顺序”
链串
链串的存储结构—块链结构
-
#define CHUNKSIZE 80 //块的大小由用户定义 typedef struct Chunk { char ch[CHUNKSIZE]; struct Chunk *next; }Chunk; typedef struct { Chunk *head,*tail; //串的头指针和尾指针 int curlen; //串的当前长度 }LString; //字符串的块链结构
- 优点:操作方便
- 缺点:存储密度较低
- 可以将多个字符存放在一个结点内,以克服存储密度较低的缺点
串的模式匹配算法(BF和KMP)
-
算法目的:
- 确定主串中所含子串第一个出现的位置(定位)
-
算法应用:
- 搜索引擎、拼写检查、语言翻译、数据压缩
-
算法种类:
-
BF算法(Brute-Force,又称古典的、经典的、朴素的、穷举的)
-
又称简单匹配算法,采用穷举的思路
-
算法思路:从S(主串:正文串)的每一个字符开始依次与T(子串:模式)的字符进行匹配
- lndex(S,T,pos)
- 将主串的第pos个字符与模式串的第一个字符比较
- 若相等,继续逐个比较后续字符
- 若不相等,从主串下一个字符起,重新与模式串的第一个字符比较
- 直到主串的一个连续子串字符序列与模式串相等,返回值为S与T中匹配的子序列第一个字符的序号,即匹配成功
- 否则,匹配失败,返回0
- 将主串的第pos个字符与模式串的第一个字符比较
- lndex(S,T,pos)
-
int lndex_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;//i-(j-1)+1 j=1; } } if(j >= T.length) return i-T.length;//返回匹配的第一个字符下标 return 0;//未匹配成功 }
-
BF算法的时间复杂度:
- 若主串长度为n,子串长度为n,最坏情况下:主串前面n-m个位置都匹配到了子串的最后一位,即n-m个位置各比较了m次,最后m位也各比较了1次
- 总次数为:(n-m)*m+m
- 若m<<n,则算法的时间复杂度为Q(n*m)
- 若主串长度为n,子串长度为n,最坏情况下:主串前面n-m个位置都匹配到了子串的最后一位,即n-m个位置各比较了m次,最后m位也各比较了1次
-
-
KMP算法(特点:速度快)
-
较BF算法有较大改进,主串S的指针i不再回溯,可提速至O(m+n)
-
需要定义next[j]函数,表明当模式中第i个字符与主串中相应字符不匹配时,在模式中需重新和主串中该字符进行比较的字符位置
- next[j] =
-
max{ k | 1<k<j ,且 p1p2p3p4…pk-1 = pj-k-1…pj-1}
-
0 当j=1时
-
1 其他情况
-
- next[j] =
-
int lndex_KMP(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 //主串、子串指针回溯,重新开始下一次匹配 { j=next[j]; } } if(j >= T.length) return i-T.length;//返回匹配的第一个字符下标 return 0;//未匹配成功 } void get_next(SString T,int &next[]) { i=0; next[1]=0; j=0; while(i<T.length) { if(j==0 || T.ch[i]==T.ch[j]) { ++i; ++j; next[i]=j; } else j=next[j]; } }
-
-
数组
-
结论:线性表结构是数组元素的一个特例,而数组结构又是线性表结构的拓展
-
数组特点:结构固定,定义后维数和维界不再改变
-
数组基本操作:
- 除了结构的初始化和销毁之外,只有去元素和修改元素值的操作
n维数组的抽象数据类型
-
ADT Array { 数据对象: 数据关系: 基本操作: 1.InitArray(&A,n,bound1,...,boundn) //构建数组A 2.DestroyArray(&A) //销毁数组A 3.Value(A,&e,index1,...,indexn) //去数组元素值 4.Assign(A,&e,index1,...,indexn) //给数组元素赋值 }ADT Array
数组的顺序存储
- 一般都采用顺序存储结构来表示数组
- 数组可以是多维的,但存储数据元素的内存单元地址的一维的,因此在存储数据结构之前,需要解决多维关系映射到一维关系的问题
- 二维数组两种顺序存储方式:
- 以行序为主序(低下标优先):BASIC、COBOL、PASCAL、Java、C
数组元素a[i][j]的存储位置是:LOC(i,j)=LOC(0,0)+(n*i+j)*L
- 以行序为主序(高下标优先):FORTRAN
- 以行序为主序(低下标优先):BASIC、COBOL、PASCAL、Java、C
特殊矩阵的压缩存储
- 不适宜常规存储的矩阵:值相同的元素且呈某种规律分布;零元素多
- 矩阵的压缩存储:为多个相同的非零元素只分配一个存储空间;对零元素不分配空间
- 什么样的矩阵能压缩存储:
- 对称矩阵
- 特点:a(ij)=a(ji) (沿对角线对称)
- 存储方法:只存储下三角(或上三角)(包括主对角线)的元素。共占用n(n+1)/2个元素空间
- 存储结构:以行序为主序将元素存在一个一维数组sa[n(n+1)/2]中
- 对角矩阵
- 特点:在n×n的方阵中,所有元素都集中在以主对角线为中心的带状区域中,区域外的值全为0
- 常见的有:三对角矩阵、五对角矩阵。七对角矩阵
- 存储方法:以对角线的顺序存储
- 特点:在n×n的方阵中,所有元素都集中在以主对角线为中心的带状区域中,区域外的值全为0
- 三角矩阵
- 特点:对角线以上(或以下)的数据元素(不包括对角线)全部为常数C
- 存储方法:重复元素共享一个存储空间,共占用n(n+1)/2+1个元素空间
- 存储结构:以行序为主序将元素存在一个一维数组sa[n(n+1)/2+1]中
- 下三角矩阵的元素位序k=
- i*(i-1)/2+j i>=j(对角线及下半区)
- n(n+1)/2+1 i<j(上半区)
- 上三角矩阵的元素位序k=
- (i-1)*(2n-i)/2+j-i+1 i<=j (对角线及上半区) ***未推导
- n(n+1)/2+1 i<j(下半区)
- 稀疏矩阵:矩阵中非零元素少于百分之五
- 方法一:三元组顺序表(又称有序的双下标法)
- 压缩存储原则:存各非零元的值,行列位置和矩阵的行列数
- 注意:为更可靠描述,通常再加一个总体信息,即总行数、总列数、总元素数
- 优点:非零元在表中按行序有序储存,因此便于进行依行顺序处理的矩阵运算
- 缺点:不能随机存取,若按行号存取某一行中的非零元,则需从头开始查找
- 方法二:十字链表
- 优点:能够灵活地插入因运算产生的的非零元素,删除运算产生的新零元素
- 在十字链表中,矩阵的每一个非零元素用一个结点表示,该结点除了(行,列,数值),还要有两个域:
- right:用于链接同一行的下一个非零元素
- down:用于链接同一列的下一个非零元素
- 方法一:三元组顺序表(又称有序的双下标法)
- 对称矩阵
广义表
广义表的定义
- 广义表(又称列表Lists),是n>=0个元素a0,a1,a2,…,an-1的有限序列,其中每个元素是一个原子或一个广义表
- 广义表通常记为 LS=(a1,a2,a3,…,an)
- LS为表名,n为长度,每一个ai为表的元素
- 表头:若LS非空,则第一个元素a1为表头
- 记作:head(LS)=a1 (注意:表头可以是原子,也可以是子表)
- 表尾:除表头的其他元素组成的表
- 记作:tail(LS)=(a2,a3,…,an) (注意:表尾不是最后一个元素,而是一个子表)
广义表的性质
- 广义表中的数据元素有相对次序,一个直接前驱和一个直接后继
- 广义表的长度定义为最外层所包含元素的个数
- 广义表的深度定义为该广义表展开后所包含的括号重数
- 注:原子的深度为0,空表的深度为1
- 广义表可以和其他广义表共享
- 广义表可以是一个递归的表
- 广义表是一个多层次的结构,广义表的元素可以是单元素、也可以是子表,子表的元素还可以是子表
广义表和线性表的区别
- 广义表可以看成是线性表的推广,线性表是广义表的特例
广义表的运算
- 求表头运算GetHead(L):非空广义表的第一个元素可以是一个原子,也可以是一个子表
- 求表尾运算GetTail(L):非空广义表除去表头元素以外其他元素所构成的表,表尾一定是一个表
5.树和二叉树
树和二叉树的定义
树的定义
- 树是(Tree)n(n>=0)个结点的有限集(树的定义是一个递归的定义)
- 若n=0,称为空树
- n>0,则它满足两个条件:
- 有且仅有一个特定的称为根(Root)的结点
- 其余结点可分为m个互不相交的有限集T1,T2,T3…,Tm,其中每个集合本身又是一棵树,并称为根的子树(SubTree)
树的基本术语
- 根结点:非空树中无直接前驱的结点
- 结点的度:结点拥有的子树数
- 树的度:树内各结点的度的最大值
- 叶子结点(终端结点):度为零的结点
- 分支结点/非终端结点:度!=0的结点
- 内部节点:根节点之外的分支节点
- 结点的子树的根称为该结点的孩子,该结点称为孩子的双亲
- 兄弟结点:有共同双亲的结点
- 堂兄弟结点:双亲在同一层的结点
- 结点的祖先:从根到该节点所经分支上的任一结点
- 结点的子孙:以该结点为根的子树中的任一结点
- 树的深度:树中结点的最大层次
- 有序树:树中结点的各子树从左至右有次序(最左边为第一个孩子)
- 无序树:树中结点的各子树无次序
- 森林:m(m>=0)棵互不相交的树的集合
- 把树的结点删除,树就变成了森林
- 给森林中的各子树加上一个双亲结点,森林就变成了树
- 一棵树可以看作是一个特殊的森林
- 树一定是森林,森林不一定是树
二叉树的定义
-
二叉树是n(n>=0)个结点的有限集,它由空集或由一个根结点及两颗互不相交的分别称为左子树和右子树的二叉树组成
- 特点:
- 每个结点最多有两个孩子(二叉树中不存在在度大于2的结点)
- 子树有左右之分,次序不能颠倒
- 二叉树可以是空集,根可以有空的左子树或空的右子树
- 注意:二叉树不是树的特殊情况,它们是两个概念
- 二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也要说明它是左子树还是右子树
- 树当结点只有一个孩子是,无须区分它是左是右
- ed:具有三个结点的二叉树有5种不同形态,普通树则只有两种不同形态
- 特点:
-
为什么要重点研究二叉树?
- 普通树若不转化为二叉树,则运算很难实现
- 二叉树结构最简单,规律性最强
- 所有的树都可以转化成唯一对应的二叉树,不失一般性
案例引入
案例一:数据压缩问题
- 将数据文件转换成由0、1组成的二进制编码
案例二:利用二叉树求解表达式的值
树和二叉树的抽象数据类型定义
二叉树的抽象数据类型定义
-
ADT BinaryTree { 数据对象:D是具有相同特性的数据元素的集合 数据关系:若D=Ø,则R=Ø; 若D!=Ø,则R={H};H是如下二元关系: 1.//关于树的说明 2.//关于子树不相交的说明 3.//关于数据元素的说明 4.//关于左子树和右子树的说明 基本操作P: CreateBiTree(&T,definition);//建立二叉树 PreOrderTraverse(T);//先序遍历 InOrderTraverse(T);//中序遍历 PostOrderTraverse(T);//后序遍历 ...... }ADT BinaryTree
二叉树的性质和存储结构
二叉树的性质(1,2,3)
- 在二叉树的第i层上至多有2的i-1次方个结点(第1层:2的0次方;第2层:2的1次方)
- 深度为k的结点至多有2的k次方减1个结点(等比数列),至少有k个结点
- 对任何一颗二叉树T,如果其叶子节点为n0,度为2的结点个数为n2,则n0=n2+1 (???)
两种特殊的二叉树
满二叉树
- 定义:一棵深度为k,且有2的k次方减1个结点的二叉树
- 特点:
- 每一层上的结点数都是最大结点数
- 叶子结点全部在最底层
- 对满二叉树结点位置进行编号:
- 编号规则:从根节点开始,自上而下,自左至右
- 每一个结点位置都有元素
- 满二叉树在同样深度的二叉树中结点个数最多,叶子结点个数也最多
完全二叉树
- 深度为k的具有n个结点的二叉树,当且仅当其每个结点都与深度为k的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树
- 注:在满二叉树中,从最后一个结点开始,连续去掉任意个结点,得到的就是一棵完全二叉树
- 特点:
- 叶子结点只可能分布在层次最大的两层上
- 对任一结点,如果其右子树的最大层次为i,则其左子树最大层次必为i或i-1
- 满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树
完全二叉树的性质(4,5)
- 具有n个结点的完全二叉树的深度为:(不大于logn的最大整数+1)
- 完全二叉树中双亲结点编号与孩子结点的编号之间的关系,如果对一棵有n个结点的完全二叉树的结点按层序编号,则对任一结点:
- 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点:(不大于i/2的 最大整数)
- 如果2i>n,则结点i是叶子结点,无左孩子;否则,其左孩子是结点:2i
- 如果2i+1>n,则结点i无右孩子;否则,其右孩子是结点2i+1
二叉树的顺序存储
-
实现:按二叉树的结点层次编号,依次存放二叉树中的数据元素
-
#define MAXSIZE 100 typedef TElemType SqBiTree[MAXSIZE]; SqBiTree bt;
-
-
二叉树顺序存储缺点:
- 最坏情况:深度为k且只有k个结点的单支树,需要2的k次方减1的一维数组
-
特点:结点关系蕴含在其存储位置中,浪费空间,适合满二叉树和完全二叉树
二叉树的链式存储结构
1. 二叉链表
-
二叉链表中空指针域的数量(假设n个结点)
- 具有n个结点的二叉链表中,一共有2n个指针域;n个结点一共有n-1个孩子,即有n-1个指针用来指向结点的左右孩子,其余n+1个指针域为空
-
//二叉链表 typedef struct BiNode { TElemType data; struct BiNode *lchild,*rchild;//左右孩子指针 }BiNode,*BiTree0
2.三叉链表
-
//三叉链表 typedef struct TriTNode { TElemType data; struct TriTNode *lchild,*parent *rchild; }TriTNode,*TriTree;
遍历二叉树和线索二叉树
遍历的定义:
- 顺着某一条搜索路径巡防二叉树中的结点,使得每个结点均被访问一次,而且仅访问一次
遍历的目的:
- 得到树中所有结点的一个线性排列
遍历的用途:
- 树结构的插入、删除、修改、查找和排序的前提,是二叉树一切运算的基础和核心
遍历二叉树算法描述:
- 根结点(D)
- 左子树(L)
- 右子树(R)
1.DLR—先序遍历
-
若二叉树为空,则空操作;否则:
- 访问根节点
- 先序遍历左子树
- 先序遍历右子树
-
先序遍历算法(递归实现)
-
Status PreOrderTraverse(BiTree T) { if(T == NULL) return OK;//空二叉树 visit(T);//访问根节点,可以是输出或其他操作 PreOrderTraverse(T->lchild);//递归遍历左子树 PreOrderTraverse(T->rchild);//递归遍历右子树 }
-
2.LDR—中序遍历
-
若二叉树为空,则空操作;否则:
- 中序遍历左子树
- 访问根节点
- 中序遍历右子树
-
中序遍历算法(递归实现)
-
Status InOrderTraverse(BiTree T) { if(T == NULL) return OK;//空二叉树 InOrderTraverse(T->lchild);//递归遍历左子树 visit(T);//访问根节点,可以是输出或其他操作 InOrderTraverse(T->rchild);//递归遍历右子树 }
-
-
中序遍历算法(非递归算法)
-
基本思想:
- 建立一个栈
- 根结点进栈,遍历左子树
- 根结点出栈,输出根结点,遍历右子树
-
Status InorderTraverse(BiTree T) { BiTree p,q; InitStack(S); p=T;//p指向根结点 while(p || !StackEmpty(S)) //树不为空,或栈不为空,终止条件为树和栈都空 { if(p)//根结点不为空 { Push(S,p);//根结点入栈 p=p->lchild;//指针指向左子树根结点,开始遍历左子树 } else//根结点为空 { Pop(S,q);//根结点出栈 visit(q);//访问根结点 p=q->rchild;//指针指向右子树根结点 } } return OK; }
-
3.LRD—后序遍历
-
若二叉树为空,则空操作;否则:
- 后序遍历左子树
- 后序遍历右子树
- 访问根节点
-
后序遍历算法(递归实现)
-
Status PostOrderTraverse(BiTree T) { if(T == NULL) return OK;//空二叉树 PostOrderTraverse(T->lchild);//递归遍历左子树 PostOrderTraverse(T->rchild);//递归遍历右子树 visit(T);//访问根节点,可以是输出或其他操作 }
-
4.层次遍历
-
对于一棵二叉树,从根节点开始,从上到下,从左到右的顺序访问每一个结点,且每个结点仅访问一次
-
算法设计思路:
- 将根结点入队
- 队不空时循环:从队列中出列一个结点*p,访问它:
- 若它有左孩子结点,将左孩子结点入队
- 若它有右孩子结点,将右孩子入队
-
//队列类型定义 typedef struct { BTNode data[MAXSIZE]; int front,rear; }SqQueue; //层次遍历算法 void LevelOrder(BTNode *b) { BTNode *p; SqQueue *b; InitQueue(qu);//初始化队列 enQueue(qu,b);//根结点指针入队 while(!QueueEmpty(qu)) { deQueue(qu,p);//根结点出队 visit(p); if(p->lchild != NULL) enQueue(qu,p->lchild);//有左孩子时将其入队 if(p->rchild != NULL) enQueue(qu,p->rchild);//有右孩子时将其入队 } }
5.二叉树的建立(按先序序列建立)
-
步骤:
- 键盘输入二叉树的结点信息,建立二叉树的存储结构
- 在建立二叉树的过程中按照二叉树先序的方式建立
-
Status CreateBiTree(BiTree &T) { scanf(&ch); if(ch == "#") { T=NULL; } else { T=(BiTNode*)malloc(sizeof(BiTNode)); if(!T) exit(OVERFLOW); T->data=ch; CreateBiTree(T->lchild);//创建左子树 CreateBiTree(T->rchild);//创建右子树 } return OK; }
6.复制二叉树
-
如果是空树,递归结束;否则:
- 申请新结点空间
- 复制根结点
- 递归复制左子树
- 递归复制右子树
-
int Copy(BiTree T,BiTree &NewT) { if(T == NULL) { NewT=NULL; return 0; } else { NewT=(BiTNode*)malloc(sizeof(BiTNode)); NewT->data=T->data; Copy(T->lchild,NewT->lchild); Copy(T->rchild,NewT->rchild); } return OK; }
7.计算二叉树的深度
-
算法思想:
- 如果是空树,则深度为零
- 否则,递归计算左子树的深度m,递归计算右子树的深度记为n,二叉树的深度则为m与n的较大值加一
-
int Depth(BiTree T) { if(T == NULL) return 0; m=Depth(T->lchild);//遍历左子树 n=Depth(T->rchild);//遍历右子树 if(m>n) return m+1; return n+1; }
8.计算二叉树结点总数
-
算法思想:
- 如果是空树,则结点个数为零
- 否则,结点个数为:左子树结点个数+右子树结点个数+1
-
int NodeCount(BiTree T) { if(T == NULL) return 0; return NodeCount(T->lchild)+NodeCount(T->rchild)+1;//加一加的是作为当前根结点的结点 }
9.计算二叉树叶子结点总数
-
算法思想:
- 如果是空树,则叶子结点个数为0
- 否则,叶子结点总数为:左子树叶子结点个数+右子树叶子结点个数
-
int LeafCount(BiTree T) { if(T == NULL) return 0; if(T->lchild==NULL && T->rchild==NULL) return 1;//是叶子节点 return LeafCount(T->lchild)+LeafCount(T->rchild);//不是叶子节点,则分别统计左右子树叶子结点个数 }
根据遍历序列确定二叉树
- 若二叉树中各结点的值均不相同,则二叉树结点的先序序列、中序序列和后序序列都是唯一的。由二叉树的先序序列和中序序列,或由二叉树的后序序列和中序序列可以唯一确定一棵二叉树(但由先序序列和后序序列不可以)
- 先序和中序
- 由先序确定根,由中序确定左右子树
- 后序和中序
- 由后序确定根,由中序确定左右子树
- 后序遍历,根结点必在后序序列尾部
- 先序和中序
线索二叉树
-
为什么要研究线索二叉树?
- 当使用二叉链表作为二叉树的存储结构时,可以很方便地找到某个结点的左右孩子;但一般情况下,无法直接找到该结点在某种遍历序列中的前驱和后继结点
-
如何寻找特定遍历序列中二叉树结点的前驱和后继?
- 解决方法:
- 通过遍历寻找(费时间)
- 再增设前驱、后继指针域(费空间)(增加了存储负担)
- 利用二叉链表中的空指针域:
- 如果某个结点左孩子为空,则将空的左孩子域改为指向其前驱;
- 如果某个结点右孩子为空,则将空的右孩子域改为指向其后继;
- 注:这种改变指向的指针称为“线索”,这种加了线索的二叉树称为线索二叉树,对二叉树某种遍历次序使其变为线索二叉树的过程叫线索化
- 为区分lchild和rchild 指针到底是指向孩子的指针,还是指向前驱和后继的指针,对二叉链表中每个结点增设两个标志域ltag和rtag,并约定:
- ltag=0:lchild指向该结点的左孩子
- ltag=1:lchild指向该结点的前驱
- rtag=0:rchild指向该结点的右孩子
- rtag=1:rhild指向该结点的后继
- 解决方法:
-
线索二叉树结点结构:
-
typedef struct BiThrNode { int data; int ltag,rtag; struct BiThrNode *lchild,*rchild; }BiThrNode,*BiThrTree;
-
为避免悬空态,增设了一个头结点
- ltag=0,lchild指向根结点
- rtag=1,rchild指向遍历序列的最后一个结点
- 遍历序列中第一个结点的lc域和最后一个结点的rc域都指向头结点
-
树和森林
1.树的存储结构
-
双亲表示法
-
实现:
- 定义数组,存放树的结点,每个结点含两个域:
- 数据域:存放结点本身信息
- 双亲域:指示本结点的双亲结点在数组中的位置
- 定义数组,存放树的结点,每个结点含两个域:
-
特点:找双亲容易,找孩子难
-
结点结构类型:
-
typedef struct PTNode { TElemType data; int parent;//双亲位置域(数组下标) }PTNode;
-
-
树结构:
-
#define MAX_TREE_SIZE 100 typedef struct { PTNode nodes[MAX_TREE_SIZE]; int r,n;//根结点位置,结点个数 }PTree;
-
-
-
孩子链表
-
把每个结点的孩子结点排列起来,看成一共线性表,用单链表存储,则n个结点有n个孩子链表(叶子的孩子链表为空表),而n个头指针又组成一共线性表,用顺序表(含n个元素的结构数组)存储
-
特点:找孩子容易,找双亲难
-
孩子结点结构:
-
typedef struct CTNode { int child; struct CTNode *next; }*ChildPtr;
-
-
双亲结点结构:
-
typedef struct { TElemType data; ChildPtr firstchild;//孩子链表头指针 }CTBox;
-
-
树结构:
-
typedef struct { CTBox nodes[MAX_TREE_SIZE]; int n,r;结点数和根结点的位置 }CTree;
-
-
-
孩子兄弟表示法
-
又叫二叉树表示法,二叉链表表示法
-
实现:用二叉链表作为树的结构,链表中每个结点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点
-
结构:
-
typedef struct CSNode { ElemType data; struct CSNode *firstchild,*nextsibling; }CSNode,*CSTree;
-
-
2. 树和森林的转换
-
将树转化为二叉树进行处理,利用二叉树的算法实现对树的操作
-
由于树和二叉树都可以用二叉链表作存储结构,则以二叉链表作媒介则可以导出树与二叉树之间的对应关系
-
给定一棵树,可以找到唯一的一棵二叉树与之对应
将树转换成二叉树:
- 兄弟相连留长子
1. 加线:在兄弟之间加一连线
2. 抹线:对每个结点,除了其左孩子外,去除与其余孩子的关系
3. 旋转:以树的根结点为轴心,将整棵树顺时针旋转45°
将二叉树转换成树:
- 左孩右右连双亲,去掉原来右孩线
- 加线:若p结点是双亲的左孩子,则将p的右孩子,右孩子的右孩子…沿分支找到所有右孩子,都与p的双亲用线连起来
- 抹线:抹掉原二叉树中双亲与右孩子之间的连线
- 调整:将结点按层次排列,形成树结构
森林转换成二叉树:
- 树变二叉根相连
- 将各棵树分别转换成二叉树
- 将每棵树的根结点用线相连
- 以第一棵树根结点作为二叉树的根,再以根结点为轴心,顺时针旋转,构成二叉树型结构
二叉树转换成森林:
- 去掉全部右孩线,孤立二叉再还原
- 抹线:将二叉树中根结点与右孩子的连线,及沿右分支搜索到的所有右孩子间的连线全部抹掉,使之变成孤立的二叉树
- 还原:将孤立的二叉树还原成树
3. 树和森林的遍历
树的遍历(三种方式)
1.先根遍历
- 若树不为空,则先访问根结点,然后依次先根遍历各棵子树
2.后根遍历
- 若树不为空,则先依次后根遍历各棵子树,然后访问根结点
3.层次根遍历
- 若树不为空,则自上而下自左至右访问树中每个结点
森林的遍历
-
将森林看成三部分构成:
- 森林中的第一棵树的根结点
- 森林中第一棵树的子森林
- 森林中其他树的构成的森林
1.先序遍历:
- 若树不为空,则:
- 访问森林中第一棵树的根结点
- 先序遍历森林中第一棵树的子树森林
- 先序遍历森林中(除第一棵树之外),其余树构成的森林
- 即:依次从左到右对森林中每一棵树进行先根遍历
2.中序遍历:
- 若树不为空,则:
- 中序遍历森林中第一棵树的子树森林
- 访问森林中第一棵树的根结点
- 中序遍历森林中(除第一棵树之外),其余树构成的森林
- 即:依次从左到右对森林中每一棵树进行后根遍历
哈夫曼树及其应用
哈夫曼树的基本概念
- 路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径
- 结点的路径长度:两结点间路径上的分支数
- 树的路径长度:从树根到每一个结点的路径长度之和,记作:TL
- 结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树
- 权:将树中结点赋给一个有着某种含义的值,这个数值称为该结点的权
- 结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积
- 树的带权结点路径长度:树中所有叶子结点的带权路径长度之和,记作WPL
- 哈夫曼树:
- 最优树:带权路径长度(WPL)最短的树
- 最优二叉树:带权路径长度(WPL)最短的二叉树
- 特点:
- 满二叉树不一定是哈夫曼树
- 哈夫曼树中权越大的叶子离根越近
- 具有相同带权结点的哈夫曼树不唯一
哈夫曼树的构造算法
-
贪心算法:构造哈夫曼树时首先选择权值最小的叶子结点
-
哈夫曼算法:
- 根据n个给定的权值构成n棵二叉树的森林,森林中的每棵树都只有一个对应的带权根结点(构造森林全是根)
- 在F中选取两棵根结点的权值最小的树作为左右子树,构建一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和(选用两小造新树)
- 在森林中删除这两棵树,同时将新得到的树加入森林中(删除两小添新树)
- 重复2、3,直到森林中只有一棵树,这棵树即为哈夫曼树(重复二三剩单树)
-
口诀:
- 构造森林全是根
- 选用两小造新树
- 删除两小添新树
- 重复二三剩单树
-
特点:
- 哈夫曼树的结点的度为0或2,没有度为1的结点
- 包含n个叶子结点的哈夫曼树中共有2n-1个结点(包含n棵树的森林要经过n-1次合并才能形成哈夫曼树,共产生n-1个结点)
-
算法实现:
-
采用顺序存储结构(一维数组)
-
结点类型定义:
-
typedef struct { int weight;//权重 int parent,lch,rch; }HTNode,*HuffmanTree;
-
-
算法步骤:
-
void CreatHuffmanTree(HuffmanTree HT,int n) { if(n <= 1) return; m=2*n-1;//数组共2n-1个元素 HT=(HTNode*)malloc((m+1)*sizeof(HTNode));//0号元素不用 for(i=1;i<=m;++i) { HT[i].lch=0; HT[i].rch=0; HT[i].parent=0; } for(i=1;i<=n;++i) //输入前n个元素的weight值,HT[i].weight }
-