一、任务需求
1)算法效率的度量(时间复杂度,空间复杂度)
2)线性表,栈和队列的各种基本算法的操作(初始化,取值,查找,插入,删除);
3)树和二叉树基本概念,遍历二叉树(先序遍历,中序遍历和后序遍历),哈夫曼树的构造和设计哈夫曼编码;
4)图的存储结构(邻接矩阵,邻接表),图的遍历(深度和广度优先搜索),最小生成树;
5)查找的基本概念(平均查找长度),线性表的查找(顺序查找,折半查找(判定树)),树表的查找(二叉排序树,平衡二叉树);
6)排序方法(插入排序,交换排序,选择排序)。
二、算法的复杂度
2.1时间复杂度
时间复杂度可简单理解为执行语句的条数,用来度量算法执行时间的长短。
大O渐进表示法:
1)找出执行语句的条数。如果有循环和递归,则忽略简单语句,直接算循环和递归的语句执行次数;如果算法中有包含嵌套的循环,则执行次数通常是将两个循环次数相乘,如果算法中包含并列的循环,则将并且的相加;
2)将语句执行次数的数量级放入大Ο记号中,算法中某个基本语句重复执行的次数是问题规模为n的某个函数,则时间复杂度为。
2.2空间复杂度
空间复杂度可简单理解为执行时创建的变量(包括临时变量)个数。算法的空间复杂度并不是计算实际占用的空间,而是计算整个算法的辅助空间单元的个数。空间复杂度记为。
三、顺序表和链表的比较
3.1线性表
3.1.1顺序存储:顺序表
在逻辑上相邻的数据元素,物理存储位置也相邻,并且顺序表的存储空间需要预先分配。
优点:
a) 顺序表具有按元素序号随机存取表中任意结点;
b) 不用为表示节点间的逻辑关系而增加额外的存储空间;
c) 方法简单且容易实现,各种高级语言中都有数组。
缺点:
a) 在顺序表中做插入、删除操作时,平均移动表中的一半元素,因此n较大的顺序表运算效率很低;
b) 静态分配,程序执行之前必须明确规定存储规模预先分配足够大的存储空间,估计过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出,不便于其动态分配。
完整程序:
#include<iostream.h>
//#include<fstream>
//#include<string>
#include<iomanip> //exit()函数需要;
#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef int status; //status 是函数返回值类型,其值是函数结果状态代码。
typedef int ElemType; //ElemType 为可定义的数据类型,此设为int类型
//顺序表的存储结构
#define MAXSIZE 100 //顺序表可能达到的最大长度
typedef struct{
ElemType *elem; //存储空间的基地址
int length; //当前长度
}Sqlist;
status InitList(Sqlist &L) //算法2.1 顺序表的初始化:构造一个空的顺序表L
{
L.elem = new ElemType[MAXSIZE]; //为顺序表分配一个大小为MAXSIZE的数组空间
if(!L.elem) exit(OVERFLOW); //存储分配失败退出
L.length = 0; //空表长度为0
return OK;
}
status GetElem(Sqlist L, int i, ElemType &e) //算法2.2 顺序表的取值
{
if (i<1||i>L.length)
return ERROR; //若i值不合理,返回ERROR
e=L.elem[i-1]; // e为第i个数据元素的值
return OK;
}
int LocateElem(Sqlist L, ElemType e) //算法2.3 顺序表的查找
{
int i;
for(i=0;i< L.length;i++)
if(L.elem[i]==e) return i+1;
return 0;
}
status ListInsert(Sqlist &L, int i, ElemType e)//算法2.4 顺序表的插入
{
int j;
if((i<1) || (i>L.length+1))
return ERROR;
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;
++L.length;
return OK;
}
status ListDelete(Sqlist &L, int i)//算法2.5 顺序表的删除
{
int j;
if ((i<1) || (i>L.length)) return ERROR;
for (j=i;j<=L.length-1;j++)
L.elem[j-1]=L.elem[j];
--L.length;
return OK;
}
int main()
{
Sqlist L; //将L定义为Sqlist类型的变量,
int choose;
cout << "1. 建立\n";
cout << "2. 输入\n";
cout << "3. 取值\n";
cout << "4. 查找\n";
cout << "5. 插入\n";
cout << "6. 删除\n";
cout << "7. 输出\n";
cout << "0. 退出\n\n";
choose = -1;
while (choose != 0) {
cout << "请选择:";
cin >> choose;
int array[] = {23,33,21,46,67,98};
switch (choose) {
case 1://创建顺序表
if (InitList(L))
cout << "成功建立顺序表\n\n";
else
cout << "顺序表建立失败\n\n";
break;
case 2: //顺序表信息输入,将数组array中的值放入顺序表中
int i;
for (i=0;i<=5;i++)
L.elem[i]=array[i];
L.length=6;
cout<<"已成功输入"<<L.length<<"个数据"<<endl;
break;
case 3://顺序表的取值
int getnumber,locate;
cout << "请输入要取值的位置: ";
cin >> locate;
GetElem(L, locate, getnumber);
cout<<"线性表第"<<locate<<"个位置的值为: "<<getnumber<<endl;
break;
case 4: //顺序表的查找
int looknumber,temp;
cout << "请输入要查找的数据: ";
cin >> looknumber;
temp = LocateElem(L, looknumber);
if (temp != 0) {
cout << "查找成功\n";
cout <<"这个数在线性表中的位置是:" << temp << endl;
}
else
cout <<"查找失败,没有找到这个数" <<endl;
break;
case 5://顺序表的插入
int site;
ElemType aa;
cout << "请输入插入表的位置: ";
cin >> site;
cout << "请输入插入的值: ";
cin >> aa;
cout << "未插入数据前顺序表长度为:"<<L.length<<endl;
ListInsert(L, site, aa);
cout << "插入数据后顺序表长度为:"<<L.length<<endl;
break;
case 6: //顺序表的删除
int delsite;
cout << "请输入要删除的位置: ";
cin >> delsite;
cout <<"未删除数据前顺序表的长度为:"<<L.length<<endl;
ListDelete(L,delsite);
cout <<"删除数据后顺序表的长度为:"<<L.length<<endl;
break;
case 7: //顺序表的输出,输出顺序表的长度,并输出表中的数据
int j;
cout <<"当前线性表的长度为:" <<L.length<<endl;
for (j=0; j<=L.length-1;j++)
cout << L.elem[j] << endl;
break;
}
}
return 0;
}
3.1.2链式存储:单链表 循环链表
逻辑上相邻的数据元素,物理存储位置不一定相邻,它使用指针实现元素之间的逻辑关系。并且,链表的存储空间是动态分配的。
优点:
a)插入和删除只需要改变指针即可,运算方便;
b)存储空间易于扩充,便于其动态分配。
缺点:
a)需要占用额外的存储空间存储元素之间的逻辑关系,存储密度比顺序表低。
(存储密度是指一个节点中数据元素所占的存储单元和整个节点所占的存储单元之比);
b)链表不是一种随机存储结构,不能随机存取元素。
完整程序:
#include<iostream.h>
//#include<fstream>
//#include<string>
#include<iomanip> //exit()函数需要;
#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef int status; //status 是函数返回值类型,其值是函数结果状态代码。
typedef int ElemType; //ElemType 为可定义的数据类型,此设为int类型
#define MAXSIZE 100
//单链表的存储结构
typedef struct LNode
{
ElemType data; //结点的数据域
struct LNode *next; //结点的指针域
}LNode, *LinkList;
status InitList(LinkList &L) //单链表的初始化:构造一个空的单链表L
{
L=new LNode; //生成新结点作为头结点,用头指针L指向头节点
L->next=NULL; //头结点的指针域置空
return OK;
}
void CreateList(LinkList &L, int n)//单链表数据的录入
{
LNode *p;
int i;
int array[]={23,33,21,45,67,98};//或array[]={98,67,45,21,33,23}
for (i=n-1;i>=0;i--) //或for (i=0;i<=n;i++)
{
p=new LNode;
p->data=array[i];
p->next=L->next;
L->next=p;
}
}
status length(LinkList &L)//单链表的长度
{
LinkList p;
p=new LNode; //或不定义头结点;
int len=0;
p=L; //无头结点时:p=L->next;
while(p->next) //即p的next指向的地址不为空
{
len++;
p=p->next;
}
//无头结点时:len++;
return len;
}
void output(LinkList &L)//单链表数据的输出
{
LinkList p;
p=L->next;
cout<<"单链表中的元素为:"<<endl;
while(p!=NULL) //即p指向的地址不为空
{
cout<<p->data<<endl;
p=p->next;
}
}
void find(LinkList L, int m) //单链表取第m个元素值
{
LinkList p;
int i=1;
p=L->next;
while(i!=m)
{
p=p->next;
i++;
}
cout<<"线性表第"<<m<<"个位置的值为: "<<p->data<<endl;
}
status ListInsert(LinkList &L, int i, ElemType e) //单链表中元素的插入
{
LinkList p,s;
int j=0;
p=L->next;
while(p && (j<i-1))
{
p=p->next;
++j;
}
if (!p || j>i-1) return ERROR;
s=new LNode;
s->data=e;
s->next=p->next;
p->next=s;
return OK;
}
status ListDelete(LinkList &L, int i)//单链表中元素的删除
{
LinkList p,q;
int j=0;
p=L;
while ((p->next)&&(j<i-1))
{
p=p->next;
j++;
}
if(!(p->next)||(j>i-1)) return ERROR;
q=p->next;
p->next=q->next;
delete q;
return OK;
}
int main()
{
LinkList L; //将L定义为LinkList类型的变量,
int choose;
cout << "1. 建立空表\n";
cout << "2. 录入数据\n";
cout << "3. 求表长\n";
cout << "4. 输出数据\n";
cout << "5. 取值\n";
cout << "6. 插入\n";
cout << "7. 删除\n";
cout << "0. 退出\n\n";
choose = -1;
while (choose != 0) {
cout << "请选择:";
cin >> choose;
int array[] = {22,33,21,46,67,98};
switch (choose) {
case 1://创建单链表
if (InitList(L))
cout << "成功建立单链表\n\n";
else
cout << "单链表建立失败\n\n";
break;
case 2: //录入数据
CreateList(L, 6);
cout<<"已成功输入数据"<<endl;
break;
case 3: //单链表的表长
cout<<"单链表的表长为"<<length(L)<<endl;
break;
case 4://单链表的输出
output(L);
break;
case 5://单链表的取值
int site;
cout << "请输入要取值的位置: ";
cin >> site;
find(L, site);
break;
case 6: //单链表的插入
int number,temp;
cout << "请输入要插入的位置: ";
cin >> temp;
cout << "请输入要插入的数据: ";
cin >> number;
ListInsert(L,temp, number);
break;
case 7://单链表的删除
int weizhi;
cout << "请输入删除表的位置: ";
cin >> weizhi;
ListDelete(L, weizhi);
break;
}
}
return 0;
}
3.1.3顺序表与链表的区别
顺序表和链表都具有增、删、查、改的相同功能,但算法复杂度却不相同,如下表所示:
顺序表 | 链表 | |
---|---|---|
增 |
指定位置,不覆盖的添加一个值,后面的值要往后移动,算法复杂度为O(n)。 | 指定位置添加一个节点,需要从表头遍历到指定位置,算法复杂度为O(n)。如果带有索引的节点,算法复杂度为O(1)。 |
删 |
指定位置,删除一个值时,需要将后面的值向前移动,算法复杂度为O(n)。 | 指定一个位置,删除一个时,如果没有对指定节点进行索引,需要从表头遍历到指定位置,然后将指定节点删除,算法复杂度为O(n)。如果对指定节点做索引,删除节点的算法复杂度为O(1)。 |
改 |
增和删表数量规模比较大时,平均移动一半的元素,效率不高。 | 对于有索引的链表,添加和删除只需要O(1)算法复杂度,效率高。因此,链表用于频繁的添加和删除数据时,有优势。 |
查 |
直接查询指定位置值算法复杂度为O(1)。 |
| 需要遍历节点到指定位置,算法复杂度为O(n)。如果节点指定位置节点有索引,算法复杂度为O(1)。
|
|
内存 | 由数组组成的线性表,数组是一组地址连续的单元存储块,分配于栈区,可以自动释放。 | 由不连续的地址节点组成的线性表,每个节点可以是一个单元的地址块或连续地址块,分配于堆区,节点必须手动释放,内存管理比较不方便。 |
四、栈(先进后出)
4.1.1顺序存储:顺序栈
栈的顺序存储结构是利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,指针top是栈顶元素在顺序栈中的位置。栈底位置固定不变,栈顶位置随着入栈和出栈操作而变化。初始时顺序栈必须确定一个固定的长度,所以有存储个数的限制和空间浪费的问题。
4.1.2链式存储:链栈(单链)
链栈是一种特殊的线性链表,和所有链表一样,是动态存储结构,无需预先分配存储空间。而链栈没有栈满的问题,只有当内存没有空间可用时才会出现栈满,但是每个元素都需要一个指针域,从而产生了结构性开销。当栈的使用过程中元素个数变化较大时,用链栈是适宜的;反之,应该使用顺序栈。
完整程序:
//问题:
//1.当出栈元素的个数已经超过栈长时,输出结果出现空白;
//2.不能正确理解*S.top++=e与e=*--S.top的含义,可能对这种表示形式不太熟悉;
/*3.实现出栈过程中开始使用的是输入出栈的元素,然后出栈。但是如果输入的不是栈中元素怎么出栈,
或者输入的元素不遵循“先进后出”的原则又怎么出栈,运行之后果然出现了错误结果;*/
//4.入栈过程中是每次输入一个想要入栈的数据,但是如果想要一次性将多个数据元素入栈操作起来就很麻烦;
//解决:
//1.出栈时应当判断是否栈已空,同理入栈时也应当判断是否栈满;
/*2.通过查看相关资料知道,当S为对象top为指针时,入栈时*S.top++=e与*S.top=e,S.top++是等价的。
出栈时e=*--S.top与S.top--,e=*S.top是等价的;*/
//3.当已录入栈中元素时,每次出栈的都是栈顶元素,同时可以利用循环结构,输入想要出栈的个数,就可以得到最后栈中的元素;
//4.通过尝试改进,运行结果出现了多处错误,需要重新修改前面定义的算法,所以仍使用的原代码。
#include<iostream.h>
//#include<fstream>
//#include<string>
#include<iomanip> //exit()函数需要;
#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef int status; //status 是函数返回值类型,其值是函数结果状态代码。
typedef int SElemType; //ElemType 为可定义的数据类型,此设为int类型
//顺序栈的存储结构
#define MAXSIZE 100;
typedef struct
{
SElemType *base; //栈底指针,在栈构造之前和销毁之后,base的值为NULL
SElemType *top; //栈顶指针
int stacksize; //栈可用的最大容量,当前已分配的存储空间,以元素为单位
}SqStack;
status InitStack(SqStack &S) //为顺序栈动态分配一个最大容量为MAXSIZEL的数组空间
{
S.base=new SElemType[]; //存储分配失败
if(!S.base) exit(OVERFLOW); //头结点的指针域置空
S.top=S.base; //top初始为base,空栈
S.stacksize=MAXSIZE; //stacksize置为栈的最大容量MAXSIZE
return OK;
}
status input(SqStack &S, int array[])//录入数据元素
{
int i;
if(S.top-S.base==S.stacksize) return ERROR; //栈满
for (i=0;i<5;i++)
{
*S.top++=array[i];
}
return OK;
}
status length(SqStack &S)//顺序栈的长度
{
int len=0;
len=S.top-S.base;
return len;
}
status output(SqStack &S) //显示栈中数据元素
{
int i;
if (S.top)
{
for (i=0;i<(S.top-S.base);i++)
{
cout<<*(S.base+i)<<" ";
}
cout<<endl;
}
return OK;
}
SElemType push(SqStack &S,int &e) //实现入栈
{
if(S.top-S.base==S.stacksize) return ERROR;
*S.top++ = e;
return OK;
}
SElemType gettop(SqStack S) //获取栈顶元素
{
if(S.top != S.base)
return *(S.top-1);
}
SElemType pop(SqStack &S,int &e) //实现出栈
{
if (S.top==S.base) return ERROR;
e=*--S.top;
return e;
}
int main()
{
SqStack S;
int choose;
cout << "1. 栈的初始化\n";
cout << "2. 录入数据\n";
cout << "3. 求栈长\n";
cout << "4. 实现入栈\n";
cout << "5. 实现出栈\n";
cout << "0. 退出\n\n";
choose = -1;
while (choose != 0) {
cout << "请选择:";
cin >> choose;
int array[]={11,13,15,17,19};
switch (choose) {
case 1://创建空栈
if (InitStack(S))
cout << "成功建立空栈\n\n";
else
cout << "空栈建立失败\n\n";
break;
case 2: //录入数据
input(S,array);
output(S);
break;
case 3: //栈长
int changdu;
changdu=length(S);
cout<<"栈长为"<<changdu<<endl;
changdu=0;
break;
case 4://实现入栈
int aa;
cout<<"请输入入栈数据:"<<endl;
cin>>aa;
push(S,aa);
output(S);
break;
case 5://实现出栈
int n,bb;
cout<<"请输入出栈元素的个数:"<<endl;
cin>>n;
if(n<length(S))
{
cout<<"出栈后栈中元素依次为:"<<endl;
for(int i=0;i<n;i++)
{
gettop(S);
bb=gettop(S);
pop(S,bb);
}
output(S);
}
else
{
cout<<"栈已空"<<endl;
}
break;
}
}
return 0;
}
五、队列(先进先出)
5.1.1顺序存储:循环队列
1)“假溢出”:队列尚有空间而出现的溢出情况。当front端有元素出队时,front向后移动;当rear端有元素入队时,rear向后移动,若rear已指到队列中下标最大的位置,此时虽然front前面有空间,但再有元素入队也会出现溢出,叫作“假溢出”。当(d)中插入元素出现“假溢出”。
2)改进方案:为充分利用数组的存储空间,将顺序队列的头尾相连,构成一个循环队列,循环队列一般都是用数组来实现的,可将循环队列假想为一个环状的空间。
“假溢出”
循环队列
在循环队列中,由于不能以头、尾指针的值是否相同来判断队列空间是“空”还是“满”,所以约定少用一个元素空间,即队列空间大小为m时,有m-1个元素就认为队满。
队空条件:头、尾指针的值相同
Q.front==Q.rear
队满条件:队尾标识的rear在队头标识front的上一个位置
(Q.rear + 1)%MAXSIZE == front
队列长度:
(Q.rear + MAXSIZE - front) % MAXSIZE
5.1.2链式存储:链队(单链)
主要是通过移动对头指针和队尾指针来进行进队的操作,当对头指针和队尾指针同时指向头结点时,队列为空,其余各个功能的实现和单链表相似。初始时循环队列必须确定一个固定长度,所以会有元素个数的限制和浪费空间的问题,链队列不会有溢出的问题,但是每个元素都需要一个指针域,从而也会产生结构性开销。所以当队列元素变化较大时,优先考虑链队列,反之采用循环队列。
完整程序:
//问题:
//1.在录入队中数据元素时参照栈编写Q.base[Q.rear]++=array[i];的错误代码,没有认真思考其中的含义;
//2.循环队列出队时判断是否为空队的方法与栈类似:Q.front == Q.rear,但入队判断是否队满时没能正确写出代码;
//3.编写了output方法后,可以运行,但此方法无任何作用,后面又多次的修改以实现队中数据的输出;
//4.忘记或者错误的使用标点符号,导致运行结果出现了很多错误,逻辑思维还不够严谨;
//5.有时遇到错误时花很多的时间去解决,即使能够成功实现相关步骤,但效率不高。
//解决:
//1.在理解其中的意思后,然后进行了修改Q.base[Q.rear]=array[i]; Q.rear++;
//2.通过查看电子版教科书,循环队列队满时应该为(Q.rear + 1) % MAXQSIZE == Q.front;
//3.由于入队的元素个数远小于队列可能达到的最大长度,所以将队头指针移动,分别获取队中元素值Q.base即可;
//4.修改相关的错误符号,尽量减少出现类似的错误;
//5.记住书本上的重要知识点,同时也要动手操作才行。
#include<iomanip>
#include<iostream.h>
#define OK 1
#define ERROR 0
#define OVERFLOW -1
typedef int Status;
typedef int QElemType;
#define MAXQSIZE 100 //队列可能达到的最大长度
typedef struct
{
QElemType *base; //初始化的动态分配存储空间
int front; //头指针,若队列不空,指向队列头元素
int rear; //尾指针,队列不空时,指向队列尾元素的下一个元素
} SqQueue;
Status InitQueue(SqQueue &Q) //队列的初始化
{
Q.base = new QElemType[];
if(!Q.base) exit(OVERFLOW);
Q.front = Q.rear = 0;
return OK;
}
Status input(SqQueue &Q, int array[]) //录入队中数据元素
{
int i;
if((Q.rear + 1) % MAXQSIZE == Q.front) return ERROR;
for (i=0;i<5;i++)
{
Q.base[Q.rear]=array[i];
Q.rear++;
}
return OK;
}
Status output(SqQueue &Q) //显示队中数据元素
{
int i;
for (i=Q.front;i<Q.rear;i++)
{
cout<<Q.base[i]<<" ";
}
cout<<endl;
return OK;
}
QElemType length(SqQueue &Q) //队列的长度
{
int len=0;
len= (Q.rear - Q.front + MAXQSIZE) % MAXQSIZE;
return len;
}
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;
return OK;
}
QElemType GetHead(SqQueue &Q) //取循环队列的对头元素
{
if(Q.front != Q.rear)
return Q.base[Q.front];
}
Status DeQueue(SqQueue &Q, QElemType &e) //实现出队
{
if(Q.front == Q.rear) return ERROR;
e = Q.base[Q.front];
Q.front = (Q.front + 1) % MAXQSIZE;
return OK;
}
Status main()
{
SqQueue Q;
int array[]={11,13,15,17,19};
int choose;
cout << "1. 队的初始化\n";
cout << "2. 录入数据\n";
cout << "3. 求队长\n";
cout << "4. 实现入队\n";
cout << "5. 实现出队\n";
cout << "0. 退出\n\n";
choose = -1;
while(choose != 0)
{
cout << "请选择操作内容:";
cin >> choose;
switch (choose)
{
case 1: //创建空队列
if (InitQueue(Q))
cout <<"成功建立队列"<<endl;
else
cout <<"队列建立失败"<<endl;
break;
case 2: //录入数据
input(Q,array);
output(Q);
cout<<"已成功将元素入队"<<endl;
break;
case 3: //队长
int changdu;
changdu = length(Q);
cout <<"队长为"<<changdu<<endl;
changdu=0;
break;
case 4: //实现入队
int aa;
cout <<"请输入要入队的数据: "<<endl;
cin >> aa;
EnQueue(Q, aa);
output(Q);
break;
case 5: //实现出队
int n,bb;
cout<<"请输入出队元素的个数:"<<endl;
cin>>n;
if(n<length(Q))
{
cout<<"出队后队中元素依次为:"<<endl;
for(int i=0;i<n;i++)
{
GetHead(Q);
bb=GetHead(Q);
DeQueue(Q, bb);
}
output(Q);
}
else
{
cout<<"队已空"<<endl;
}
break;
}
}
return 0;
}
六、树和二叉树
6.1树
由n(n>=0)个结点组成的一个具有层次关系的有限集合,若n=0,则是一棵空树。在一棵树中,度不为0的结点称为分支结点,度为0的结点称为叶子结点,也就是没有后继结点的结点就是叶子结点。
6.2 二叉树
二叉树是树的一种,每个结点最多只能有两个结点。二叉树是有序树,左子树和右子树次序不能颠倒,即使树中某个结点只有一棵子树,也要区别是左子树还是右子树。二叉树还可分为非完全二叉树,完全二叉树,满二叉树。
6.2.1二叉树的重要特性
(1)在二叉树的第i层上至多有2i-1个结点(i>0);
(2)深度为k的二叉树至多有2k-1个结点(k>0);
(3)具有n个结点的完全二叉树,它的深度必为+1;
(4)对于任何一棵二叉树,若度为2的结点数有n2个,则叶子数(n0)必定为n2+1(即n0=n2+1);
(5)若对完全二叉树中的结点从上至下、从左至右编号,则编号为i(1<=i<=n)的结点,其左孩子编号必为2i,其右孩子编号必为2i+1;其双亲编号必为i/2(i = 1时除外)。
6.2.2二叉树的遍历
(1)先序遍历 VLR:先根再左再右;
(2)中序遍历 LVR:先左再根再右;
(3)后序遍历 LRV:先左再右再根。
完整程序:
//问题
//1.把函数createbitree(T)放在if语句里面;
//2.int data; char ch;T->data=ch;导致遍历结果出现错误;
//3. 各种遍历函数中的语句次序有误;
//4. 创建二叉树时,不能理解&T的含义;
//解决
//1. 函数返回值不是真或假,不能直接放在if语句里面;
//2. 将data的类型修改成char类型,否则会自动转换成为int数据类型;
//3. 理解书本上的概念,调整语句次序;
//4. BiTree &T表示传递BiTree T的地址。
#include<iostream.h>
//#include<fstream>
//#include<string>
#include<iomanip> //exit()函数需要;
#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef int status; //status 是函数返回值类型,其值是函数结果状态代码。
typedef int TElemType; //TElemType 为可定义的数据类型,此设为int类型
#define MAXTSIZE 100 //二叉树的最大结点树
typedef TElemType SqBiTree[100]; //0号单元存储根节点
SqBiTree bt;
//二叉链表的存储结构
typedef struct BiTNode{
char data; //结点数据域
struct BiTNode *lchild,*rchild; //左右孩子指针
}BiTNode,*BiTree;
//二叉树的建立
void createbitree(BiTree &T)
{
char ch;
cin>>ch;
if(ch=='#') T=NULL;
else
{
T=new BiTNode;
T->data=ch;
createbitree(T->lchild);
createbitree(T->rchild);
}
}
//前序遍历
void preordertraverse(BiTree T)
{
if(T)
{
cout<< T->data<<" ";
preordertraverse(T->lchild);
preordertraverse(T->rchild);
}
}
//中序遍历
void inordertraverse(BiTree &T)
{
if(T)
{
inordertraverse(T->lchild);
cout<< T->data<<" ";
inordertraverse(T->rchild);
}
}
//后序遍历
void posordertraverse(BiTree &T)
{
if(T)
{
posordertraverse(T->lchild);
posordertraverse(T->rchild);
cout<< T->data<<" ";
}
}
//深度
int depth(BiTree T)
{
int m,n;
if(T==NULL) return 0;
else
{
m=depth(T->lchild);
n=depth(T->rchild);
if (m>n) return(m+1);
else return(n+1);
}
}
//结点总个数
int nodecount(BiTree T)
{
if(T==NULL) return 0;
else return nodecount(T->lchild)+nodecount(T->rchild)+1;
}
//叶子结点
void leaf(BiTree T)
{
if (T!=NULL)
{
if (T->lchild==NULL && T->rchild==NULL)
cout<< T->data<<" ";
leaf(T->lchild);
leaf(T->rchild);
}
}
//叶子结点的个数
int leafcount(BiTree T)
{
int leafnum;
if(T==NULL)
leafnum=0;
else
if((T->lchild==NULL)&&(T->rchild==NULL))
leafnum=1;
else
leafnum=leafcount(T->lchild)+leafcount(T->rchild);
return leafnum;
}
//度为1的结点个数
int count1(BiTree T)
{
int i=0;
if(T!=NULL)
{
if((T->lchild!=NULL && T->rchild==NULL)||(T->lchild==NULL && T->rchild!=NULL))
{
i=count1(T->lchild)+count1(T->rchild)+1;
}
else
{
i=count1(T->lchild)+count1(T->rchild);
}
}
return i;
}
//每个叶子结点到根节点的路径
void prepath(BiTree T,TElemType path[],int pathlen)//先序遍历方法输出每个叶子结点到根结点的路径序列
{
if(T!=NULL)
{ if (T->lchild==NULL && T->rchild==NULL) //输出栈中所有叶子结点值
{
printf("%c->",T->data);
for (int i=pathlen-1;i>0;i--)
printf("%c->",path[i]);
printf("%c\n",path[0]);
}
else
{ path[pathlen++]=T->data;
prepath(T->lchild, path, pathlen);
prepath(T->rchild, path, pathlen);
}
}
}
int main()
{
BiTree T;
int choose;
cout << "1. 建立二叉树\n";
cout << "2. 前序遍历\n";
cout << "3. 中序遍历\n";
cout << "4. 后序遍历\n";
cout << "5. 深度\n";
cout << "6. 结点个数\n";
cout << "7. 叶节点个数\n";
cout << "8. 度为1的结点个数\n";
cout << "9. 每个叶结点到根节点路径\n";
cout << "0. 退出\n\n";
choose = -1;
while (choose != 0) {
cout << "请选择:";
cin >> choose;
switch (choose) {
case 1://创建二叉树
createbitree(T);
cout<<"二叉树创建完成!"<<endl;
break;
case 2://前序遍历
cout<<"前序遍历结果: ";
preordertraverse(T);
cout<<endl;
break;
case 3://中序遍历
cout<<"中序遍历结果: ";
inordertraverse(T);
cout<<endl;
break;
case 4://后序遍历
cout<<"后序遍历结果: ";
posordertraverse(T);
cout<<endl;
break;
case 5://深度
cout<<"树的深度为: "<<depth(T)<<endl;
break;
case 6://结点个数
cout<<"树的结点个数为: "<<nodecount(T)<<endl;
break;
case 7://叶子结点个数
cout<<"叶子结点为: ";
leaf(T);
cout<<"叶子结点的个数为: "<<leafcount(T)<<endl;
break;
case 8://度为1的结点个数
cout<<"度为1的结点个数为: "<<count1(T)<<endl;
break;
case 9://每个叶子结点到根节点的路径
int path[3];
printf("prepath:\n");
prepath(T,path,0);
break;
}
}
return 0;
}
6.3哈夫曼树和哈夫曼编码
假设有个权值,n个叶子结点所构成的所有二叉树中,带权路径长度最小的二叉树称为哈夫曼树,树中所有叶子结点的带权路径长度之和记为。
设需要编码的字符集为{a1,a2,a3,…,an},各个字符出现的频率为,以字符出现的频率作为结点字符的权值来构造霍夫曼树。规定左权分支为0,右权分支为1,则从根结点到叶子结点经过的分支所组成的0和1串便是对应字符的编码,称为霍夫曼编码。
七、图
7.1图的存储结构
7.1.1邻接矩阵:用一个nn的矩阵来表示一张图,矩阵的横纵坐标均表示图的点,若矩阵第i行第j列数字为n,在无向图中表示点i与点j之间则有n条连线,若矩阵第i行第j列数字为m,在有向图中表示在图中有m条由i指向j的边。
7.1.2邻接表:用链表的形式来表示图,若表头结点所对应的顶点存在相邻顶点,则把相邻顶点依次存放于表头结点所指向的单向链表中。
7.2图的遍历
7.2.1深度优先搜索:
(1)从图中某个顶点v0出发并访问v0;
(2)访问结点v0的第一个邻接点,以这个邻接点vt作为一个新节点,访问vt所有邻接点。直到以vt出发的所有节点都被访问到,回溯到v0的下一个未被访问过的邻接点,以这个邻结点为新节点,重复上述步骤。直到图中所有与v0相通的所有节点都被访问到;
(3)若此时图中仍有未被访问的结点,则另选图中的一个未被访问的顶点作为起始点。重复深度优先搜索过程,直到图中的所有节点均被访问过。
7.2.2广度优先搜索
(1)从图中某个顶点v0出发并访问v0;
(2)依次访问v0的各个未被访问的邻接点;
(3)依次从上述邻接点出发,访问它们的各个未被访问的邻接点;
(4)若此时图中仍有未被访问的结点,则另选图中的一个未被访问的顶点 作为起始点。重复广度优先搜索过程,直到图中的所有节点均被访问过。
*7.3最小生成树
从图中的一个起点v1开始,把v1加入集合U,然后寻找从与v1有关联的边,权重最小的那条边并且该边的终点v3在顶点集合V中,再把v3加入到集合U中,并且输出边(v1,v3)的信息,这样我们的集合U为{v1,v3}。然后寻找与v1关联和v3关联的边中,权重最小的那条边并且该边的终点在集合V中,我们把v6加入到集合U中,并且输出对应的那条边的信息,这样我们的集合U为{v1,v3,v6}这三个元素了,依次类推直到所有顶点都加入到了集合U。
从而得到最小生成树。
八、查找
8.1平均查找长度
即平均查找长度,在查找运算中,由于所费时间在关键字的比较上,所以把平均需要和待查找值比较的关键字次数成为平均查找长度。其中n为查找表中元素个数,Pi为查找第i个元素的概率,通常假设每个元素查找概率相同,Pi=1/n,Ci是找到第i个元素的比较次数。一个算法的ASL越大,说明时间性能差,反之时间性能好。
8.2线性表的查找
顺序查找:查找方式为从头扫到尾,找到待查找元素即查找成功,若到尾部没有找到,说明查找失败。
折半查找:以有序表的中间记录作为比较对象,并以中间记录将表分割为两个子表,对子表继续上述操作。所以对表中每个记录的查找过程,可用二叉树来描述。二叉树中的每个结点对应有序表中的一个记录,结点中的值为该记录在表中的位置,这样的二叉树为折半查找判定树。判定树每个结点的值均大于其左子树上所有结点的值,小于其右子树上所有结点的值。
8.3树表的查找
二叉排序树:二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:若左子树不空,则左子树上所有节点的值均小于它的根节点的值;若右子树不空,则右子树上所有节点的值均大于它的根节点的值;左、右子树也分别为二叉排序树。
二叉排序树构造过程如下图所示:
平衡二叉树:平衡二叉树,它是一棵空树,或者是具有以下性质:它的左右两个子树的深度差的绝对值不超过1;它的左右两个子树也分别是平衡二叉树。二叉树节点的左子树的深度减去它的右子树的深度称为平衡因子,则平衡二叉树上所有节点的平衡因子只可能是-1、0和1,如下图所示: