版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1、第七章 动态内存分配与数据结构,本章首先介绍程序运行时动态内存分配(dynamic memory allocation)的概念与方法。进一步讨论复制构造函数,然后学习更多有关数据结构的基本知识,包括链表,栈,队,二叉树等的基本算法和应用。模板是标准C+实现代码复用的有力工具,特别是有关数据结构的算法,本章继续使用,7.1自由存储区内存分配,7.4二叉树 (选读,7.3 栈与队列的基本操作及其应用,7.2 链表与链表的基本操作,第七章 动态内存分配与数据结构,7.1自由存储区内存分配,7.1.1自由存储区内存 的分配与释放,7.1.2自由存储区对象 与构造函数,7.1.3 浅复制与深复制,静态存
2、储分配: 通常定义变量(或对象),编译器在编译时都可以根据该变量(或对象)的类型知道所需内存空间的大小,从而系统在适当的时候为它们分配确定的存储空间。 动态存储分配: 有些操作对象只有在程序运行时才能确定,这样编译器在编译时就无法为他们预定存储空间,只能在程序运行时,系统根据运行时的要求进行内存分配,称为动态存储分配。动态分配都在自由存储区中进行,7.1.1自由存储区内存的分配与释放,当程序运行到需要一个动态分配的变量或对象时,必须向系统申请取得自由存储区中的一块所需大小的存贮空间,用于存贮该变量或对象。当不再使用该变量或对象时,也就是它的生命结束时,要显式释放它所占用的存贮空间,这样系统就能
3、进行再次分配,做到重复使用有限的资源,申请和释放自由存储区中分配的存贮空间,分别使用new和delete的两个运算符来完成,其使用的格式如下: 指针变量名=new 类型名(初始化式); delete 指针名; new运算符返回的是一个指向所分配类型变量(对象)的指针。对所创建的变量或对象,都是通过该指针来间接操作的,而动态创建的对象本身没有名字,动态分配与释放,7.1.1自由存储区内存的分配与释放,一般定义变量和对象时要用标识符命名,称命名对象,而动态的称无名对象(请注意与栈区中的临时对象的区别,两者完全不同:生命期不同,操作方法不同,临时变量对程序员是透明的)。自由存储区是不会自动在分配时做
4、初始化的(包括清零),所以必须用初始化式(initializer)来显式初始化,new表达式的操作: 从自由存储区分配对象,然后用括号中的值初始化该对象。 分配对象时,new表达式调用库操作符new(): int *pi=new int(0); pi现在所指向的变量的存储空间是由库操作符new()分配的,位于程序的自由存储区中,并且该对象未命名,无名对象,7.1.1自由存储区内存的分配与释放,自由存储区,i,演示: 用初始化式(initializer)来显式初始化 int *pi=new int(0); 当pi生命周期结束时, 必须释放pi所指向的目标: delete pi; 注意这时释放了p
5、i所指的目标的内存空间,也就是撤销了该目标,称动态内存释放(dynamic memory deallocation),但指针pi本身并没有撤销,该指针所占内存空间并未释放,7.1.1自由存储区内存的分配与释放,数组动态分配格式: 指针变量名=new 类型名下标表达式; Delete 指向该数组的指针变量名; 说明: 两式中的方括号必须配对使用。 如果delete语句中少了方括号,因编译器认为该指针是指向数组第一个元素的指针,会产生回收不彻底的问题(只回收了第一个元素所占空间),加了方括号后就转化为指向数组的指针,回收整个数组。 delete 的方括号中不需要填数组元素数,系统自知。即使写了,编
6、译器也忽略。 请注意“下标表达式”不是常量表达式,即它的值不必在编译时确定,可以在运行时确定,7.1.1自由存储区内存的分配与释放,动态分配数组与标准字符串类,标准字符串类string就是采用动态建立数组的方式解决数组溢出的问题的,例7.1所做的动态内存分配与释放,在string类对象中都是自动完成的,在程序中不需要也不允许再显式地为string进行动态内存的分配与释放,例7.1】动态数组的建立与撤销,7.1.1自由存储区内存的分配与释放,动态分配数组的特点,1. 变量n在编译时没有确定的值,而是在运行中输入,按运行时所需分配空间,这一点是动态分配的优点,可克服数组“大开小用”的弊端,在表、排
7、序与查找中的算法,若用动态数组,通用性更佳。delete pc是将n个字符的空间释放,而用delete pc则只释放了一个字符的空间,2. 如果有char *pc1,令pc1=pc,同样可用delete pc1 来释放该空间。尽管C+不对数组作边界检查,但在自由 存储区 空间分配时,对数组分配空间大小是纪录在案的,3. 没有初始化式(initializer),不可对数组初始化,7.1.1自由存储区内存的分配与释放(选读,多维数组动态分配: new 类型名下标表达式1 下标表达式2; 建立一个动态三维数组 float (*cp)3020 ; /指向一个30行20列数组的指针 cp=new flo
8、at 15 30 20; /建立由15个30*20数组组成的数组; 注意cp等效于三维数组名,但没有指出其边界,即最高维的元素数量,就像指向字符的指针即等效一个字符串,不要把指向字符的指针,说成指向字符串的指针。这与数组的嵌套定义相一致,7.1.1自由存储区内存的分配与释放(选读,比较: float(*cp) 30 20; /三级指针 float (*bp) 20; /二级指针 cp=new float 1 30 20; bp=new float 30 20; 两个数组都是由600个浮点数组成,前者是只有一个元素的三维数组,每个元素为30行20列的二维数组,而另一个是有30个元素的二维数组,每
9、个元素为20个元素的一维数组。 删除这两个动态数组可用下式: delete cp; /删除(释放)三维数组 delete bp; /删除(释放)二维数组,例7.2】 动态创建和删除一个m*n个元素的数组,7.1.1自由存储区的分配与释放,指针使用的要点: 1. 动态分配失败。返回一个空指针(NULL),表示发生了异常,堆资源不足,分配失败,指针删除与自由存储区空间释放。删除一个指针p(delete p;)实际意思是删除了p所指的目标(变量或对象等),释放了它所占的自由存储区空间,而不是删除本身,释放自由存储区空间后,成了空悬指针。空悬指针是程序错误的一个根源)。建议这时将置空(NULL,3.
10、new()和delete()是可以重载的,它们都是类的静态成员函数。程序员无需显式声明它为静态的,系统自动定义为静态的。本教材不讨论new()和delete()的重载。未重载时,调用全局库操作符new(,7.1.1自由存储区内存的分配与释放,4内存泄漏(memory leak)和重复释放。new与delete 是配对使用的, delete只能释放自由存储区空间。如果new返回的指针值丢失,则所分配的自由存储区空间无法回收,称内存泄漏,同一空间重复释放也是危险的,因为该空间可能已另分配,所以必须妥善保存new返回的指针,以保证不发生内存泄漏,也必须保证不会重复释放自由存储区内存空间,5动态分配的
11、变量或对象的生命期。无名对象的生命期并不依赖于建立它的作用域,比如在函数中建立的动态对象在函数返回后仍可使用。但必须记住释放该对象所占自由存储区空间,并只能释放一次,在函数内建立,而在函数外释放是一件很容易失控的事,往往会出错,7.1.2自由存储区对象与构造函数,类对象动态建立与删除过程: 通过new建立的对象要调用构造函数,通过delete删除对象也要调用析构函数。 CGoods *pc; pc=new CGoods; /分配自由存储区空间,并构造一个无名的CGoods对象; . delete pc; /先析构,然后将内存空间返回给自由存储区,自由存储区对象的生命期并不依赖于建立它的作用域,
12、所以除非程序结束,自由存储区对象(无名对象)的生命期不会到期,并且需要显式地用delete语句析构该类对象,上例执行delete语句时,C+自动调用商品类的析构函数,7.1.2自由存储区对象与构造函数,由自由存储区创建对象数组,只能调用默认的构造函数,不能调用其他任何构造函数。如果没有默认的构造函数,则不能创建对象数组,类对象初始化: new后面类(class)类型可以有参数。这些参数即构造函数的参数。但对创建数组,则无参数,只能调用默认的构造函数,例7.3】演示自由存储区对象分配和释放,7.1.3浅复制与深复制,浅复制:默认复制构造函数,可用一个类对象初始化另一个类对象,称为默认的按成员复制
13、,而不是对整个类对象的按位复制。这称为浅复制,图7.1 浅复制,如果类中有一个数据成员为指针,该类的一个对象obj1中的这个指针p,指向了动态分配的一个自由存储区对象,(参见图7.1复制前),如果用obj1按成员复制了一个对象obj2,这时obj2.p也指向同一个自由存储区对象,obj1,obj1,obj2,7.1.3浅复制与深复制复制,当浅复制析构时,如用默认的析构函数,则动态分配的自由存储区对象不能回收。如果在析构函数中有“delete p;”语句,则如果先析构函数obj1时,自由存储区对象已经释放,以后再析构obj2时出现了二次释放的问题,自由存储区对象,P,P,自由存储区对象,图7.2
14、 深复制,深复制:重新定义复制的构造函数,给每个对象独立分配一个自由存储区对象,称深复制。这时先复制对象主体,再为obj2分配一个自由存储区对象,最后用obj1的自由存储区对象复制obj2的自由存储区对象,obj1,obj2,7.1.3浅复制与深复制,例7.4】定义复制构造函数 (copy structor)和复制赋值操作符(copy Assignment Operator)实现深复制,学生类定义: class student char *pName; /为了演示深复制,不用string类 public: student(); /默认构造函数 student(char *pname); /带参
15、数构造函数 student(student /复制赋值操作符,检验主函数和运行结果,7.1.3浅复制与深复制,提示: 自由存储区内存是最常见的需要自定义复制构造函数的资源,但不是唯一的,还有打开文件等也需要自定义复制构造函数。 如果类对象需要动态分配资源应该由构造函数完成,而释放资源由析构函数完成,这时类也需要一个自定义的复制构造函数,实现对象的深复制。由此可见,构造函数并非仅做初始化工作,7.1.3浅复制与深复制,思考: 深入地考虑【例7.4】,如果数据域还有很多其他数据,甚至有好几个是动态建立的C字符串,深复制是不是太复杂了?如果使用C+标准字符串string作为成员对象(聚合)是否就不需
16、要考虑深复制了,的确是这样的,准确地说,string类的内部包含动态建立字符数组的操作,其复制构造函数是深复制。如果在student类中使用string类而不是C字符串,就不要再考虑深复制问题了。也就是说,动态内存分配和深复制应该放在一个适当的层面上,一个更单纯的类定义中,如string类。在使用中,把它作为一个成员对象,就像使用string类对象那样,7.1.3浅复制与深复制,探讨: 最后进一步讨论类的封装。封装的更高境界是在该类对象中一切都是完备的、自给自足的,不仅有数据和对数据的操作,还包括资源的动态安排和释放。在需要时可以无条件地安全使用。标准string类模板就是典型的例子。这样的类
17、对象,作为另一个类的成员对象使用时,就不会出任何问题。这表明聚合实现了完善的封装,7.2 链表与链表的基本操作,线性表是最简单,最常用的一种数据结构。线性表的逻辑结构是n个数据元素的有限序列(a1,a2,an)。而线性表的物理结构包括:顺序表,链表,7.2.1 单链表基本算法,7.2.3 双向链表(选读,7.2.2单链表类型模板,7.2.1 单链表基本算法,单链表(Singly Linked list): 每个数据元素占用一个节点(Node)。一个节点包含两个域,一个域存放数据元素info,其数据类型由应用问题决定;另一个存放指向该链表中下一个节点的指针link,节点定义如下: typedef
18、 int Datatype; /数据为整型 struct node Datatype info; node *link;,在C/C+中允许结构(或对象)成员是结构自身的指针类型,通过指针引用自身这种类型的结构。但结构成员决不能是结构自身类型,即结构不能自己定义自己,这会导致一个无穷递归的定义,7.2.1 单链表基本算法,单链表的第一个结点的地址可通过链表的表头指针head找到, head在使用中千万不可丢失,否则链表整个丢失,内存也发生泄漏,infon-1,info2,info1,info0,head,图7.3 单链表结构,单链表的插入与删除: 只要改变链中结点指针的值,无需移动表中的元素,就
19、能实现插入和删除操作。 插入算法有三种情况,我们希望在单链表中包含数据infoi的结点之前插入一个新元素,则infoi可在第一个结点,或在中间结点,如未找到,则把新结点插在链尾结点之后,7.2.1 单链表基本算法,newnode,infox,info0,info1,head,插在链首: 首先新结点的link指针指向info0所在结点,然后,head指向新结点。即: newnodelink=head;/注意:链表操作次序非常重要 head=newnode,7.2.1 单链表基本算法,infox,infoi-1,infoi,p,newnode,插在中间: 首先用工作指针p找到指定结点,而让指针q指
20、向紧跟其后的结点,令infoi-1所在结点的link指针指向新结点,而后让新结点的link指向infoi所在结点。即: newnodelink=p; /或newnodelink=qlink;可用于插入某结点之后 qlink=newnode,7.2.1 单链表基本算法,infox,newnode,p,infon-1,插在队尾: 只要工作指针p找到队尾,即可链在其后: plink=newnode; newnode.link=NULL,7.2.1 单链表基本算法,带表头结构的链表: 研究以上算法,插在链表第一个结点之前与其他结点之前的算法有所不同。要使算法中没有特殊者,可以给每一个链表加上一个表头结
21、点,如下图所示,空表如下,下面分别介绍带表头结构的链表的生成链表算法、链表查找算法、插入一个结点的算法和删除一个结点的算法,7.2.1 单链表基本算法,head,tail,p,info1,tail,p,tail,1. 向后生成链表算法: node *createdown() Datatype data; Node*head,*tail,*p; head=new node; /建立链表头结点 tail=head; while(cindata) /回车结束 p=new(node); /每输入一个数申请一个结点 p-info=data; /添入数据 tail-link= p; /新结点接到链尾 ta
22、il=p; /尾指针到链尾 tail-link=NULL; /链尾加空指针,表示链结束 return head; /返回头指针,7.2.1 单链表基本算法,head,info0,P,P,info1,2. 向前生成链表算法: node *createup() node *head,*p; Datatype data; head=new node; /建立头结点 head-link=NULL; while(cindata) /建立的总是第一个结点 p=new node; p-info=data; p-link= head-link ; /新结点放在原链表前方 head-link=p; /头结点放新
23、结点之前 return head,7.2.1 单链表基本算法,3.链表查找算法(按关键字)查找: node *traversal(node *head,Datatype data) node *p=head-link; while(p!=NULL,7.2.1 单链表基本算法,5. 删除单链表节点*p后面节点: void del (node *p) node *q; q=p-link; p-link=q-link; delete q; /如果要把该节点移入另一个链中,则可将q返回。,7.2.2 单链表类型模板,例7.5_h】单链表类模板。 定义结点类: templateclass List; t
24、emplateclass Node T info; /数据域 Node *link; /指针域,注意结点类格式,尖括号中是参数名表,类模板实例化为类 public: Node(); /生成头结点的构造函数 Node(const T,定义链表类: templateclass List Node *head,*tail; /链表头指针和尾指针 public: List(); /构造函数,生成头结点(空链表) List(); /析构函数 void MakeEmpty(); /清空链表,只余表头结点 Node* Find(T data); /搜索数据域与data相同的结点,返回该结点的地址 int L
25、ength(); /计算单链表长度 void PrintList(); /打印链表的数据域 void InsertFront(Node* p); /可用来向前生成链表 void InsertRear(Node* p); /可用来向后生成链表 void InsertOrder(Node *p); /按升序生成链表 Node*CreatNode(T data); /创建结点(孤立结点) Node*DeleteNode(Node* p); ; /删除指定结点,7.2.2 单链表类型模板,7.2.2 单链表类型模板,例7.5】由键盘输入16个整数,以这些整数作为结点数据,生成两个链表,一个向前生成,一
26、个向后生成,输出两个表。然后给出一个整数在一个链表中查找,找到后删除它,再输出该表。清空该表,再按升序生成链表并输出。 在本例中程序只需调用类模板中的成员函数就可以完成所有链表操作。 【例7.6】以学生类作为链表的数据类,完成学生档案的管理,7.2.2 单链表类型模板,讨论复制构造函数: 在本例中没有给出Node类的复制构造函数,并非可以使用默认的复制构造函数,而是避开了所有使用它的情况,本例中函数的参数和返回值仅使用指向Node的指针,类定义中也没有用复制方式初始化。定义复制构造函数与类的实际意义和使用方式有关,并非只是深复制和浅复制的问题。 通常对Node类复制的结果应是一个孤立结点: t
27、emplate Node:Node(Node link域值为NULL。该函数与Node的有参构造函数功能基本相同。考虑到函数的参数和返回值仅使用指向Node的指针,定义复制构造函数已经没有实际意义 单链表的复制构造函数和复制运算符是一个复杂的算法,与前文所编的复制构造函数和复制运算符构造相去甚远了,7.2.3 双向链表(选读,双向链表引入: 考虑单链表只能找后继。如要找前驱,必须从表头开始搜索。为了克服这一缺点,可采用双向链表(Double Linked List)。双向链表的结点有三个域:左链接指针(llink),数据域(info),右链接指针域(rlink)。双向链表经常采用带头结点的循环
28、链表方式,7.2.3 双向链表(选读,双向链表的访问: 假设指针p指向双向循环链表的某一个结点,那么,p-llink指示P所指结点的前驱结点,p-rlink指示后继结点。p-llink-rlink指示本结点的前驱结点的后继结点,即本结点,间接访问符-可以连续使用,例7.7】双向链表类模板和结点类模板,7.3 栈与队列的基本操作及其应用,栈和队都是特殊的线性表,限制存取位置的线性结构,可以由顺序表实现,也可以由链表实现,7.3.1 栈,7.3.3队列,7.3.2 栈的应用(选读,7.3.1 栈,栈的基本概念: 栈定义为只允许在表的一端进行插入和删除的线性表。允许进行插入和删除的一端叫做栈顶(to
29、p),而另一端叫栈底(bottom)。 栈中没有任何元素时,称为空栈。 进栈时最先进栈的在最下面,最后的在最上面,后来居上。而出栈时顺序相反,最后进栈的最先出栈,而最先进栈的a0最后出栈。所以栈又称作后进先出(LIFO:Last In First Out)的线性表。 栈可以用顺序表实现,称顺序栈;也可以用链表实现,称链栈,7.3.1 栈,栈的基本操作: 参见下图,设给定栈s=(a0,a1,an-1),称a0为栈底,an-1为栈顶。进栈时最先进栈的a0在最下面,an-1在最上面。而出栈时顺序相反,最后进栈的an-1最先出栈,而最先进栈的a0最后出栈,a0,an-2,a1,an-1,bottom,
30、进栈,top,top,top,top,top,出栈,图示为顺序栈。其中栈底bottom是指向栈数据区的下一单元,这样判断是否为空栈会更方便,只需top与bottom相同就是空栈。通常只有栈顶与操作有关,7.3.1 栈,templateclass Stack int top; /栈顶指针(下标) T *elements; /动态建立的元素 int maxSize; /栈最大容纳的元素个数 public: Stack(int=20); /构造函数,栈如不指定大小,设为20元素 Stack()delete elements; void Push(const T /输出栈内所有数据,例7.8】顺序栈的
31、类模板,7.3.1 栈,void main() int i,a10=0,1,2,3,4,5,6,7,8,9,b10; Stack istack(10); for(i=0;i10;i+) istack.Push(ai); if(istack.IsFull() cout栈满endl; istack.PrintStack(); for(i=0;i10;i+) bi=istack.Pop(); if(istack.IsEmpty() cout栈空endl; for(i=0;i10;i+) coutbit; /注意先进后出 coutendl; istack.Pop(); /下溢出,7.3.1 栈,例7.
32、9_h】 链栈的结点类模板: templateclass Node /链栈结点类模板 T info; Node *link; public: Node(T data=0,Node *next=NULL) info=data; link=next; friend class Stack;,链栈,7.3.1 栈,链栈类模板(无头结点链表): templateclass Stack /链栈类模板 Node *top; /栈顶指针 public: Stack()top=NULL; Stack(); /析构函数 void Push(const T,7.3.2 栈的应用(选读,顺序栈和链栈逻辑功能是一样,
33、尽管这里两个栈模板的成员函数功能选择稍有出入,因为顺序栈可以随机访问其中的元素,而链栈只能顺序访问,但逻辑上完全可以做到一样(物理结构不同)。顺序栈必须先开一定大小内存空间,执行起来简单,速度快,可能溢出。链栈内存空间随用随开,不会溢出,但执行复杂(不断地动态分配),速度慢,顺序栈和链栈比较,7.3.2 栈的应用(选读,栈可用于表达式的计算。现考虑最简单的+、-、*、/四个运算符和结束符组成的算术表达式,只有两个优先级,先*/,后+-。 为实现运算符的优先级,采用两个栈:一个数栈,一个运算符栈。数栈暂存操作数,运算符栈暂存运算符,栈的使用(表达式运算),b*c-t1,d/e-t2,t1-t2-
34、t3,a+t3-t4,N:数栈 O:运算符,a) (b) (c) (d) (e,设有:a+b*c-d/e=;参见上图。从左向右扫描算术表达式,遇到操作数,压入数栈;遇到运算符,则与运算符栈栈顶的运算符比较优先级,若新的运算符优先级高,或运算符栈空,则压栈。否则将栈顶运算符出栈,与数字栈出栈的两个数据进行运算,结果压入数栈,再将新运算符压栈。继续扫描,直到遇到号,扫描结束。栈中数据继续按前面规则出栈,7.3.2 栈的应用(选读,例7.9】模拟简单计算器,该计算器只认+ - * / 四个运算符,输入为整数。表达式结束符使用号,清空栈用c字符。使用z字符表示结束。 简易计算器类定义,class Ca
35、lculator Stack Nstack; /使用链栈 Stack Ostack; public: Calculator(void); void Cal(void); /计算器运算程序 void GetTwoNum(int /清除,7.3.3 队列,队列的基本概念: 队列(Queue)也是一种限定存取位置的线性表。它只允许在表的一端插入,而在另一端删除。允许插入的一端称为队尾(rear),允许删除的一端叫做队头(front)。每次在队尾加入新元素,加入称为进队,删除称为出队。队列的这种特性正好与栈相反,叫做先进先出FIFO(First In First Out,图7.15队列,图7.15所示
36、队列随队尾加入元素,队尾(rear)不断向后移;而随队头元素的出队,则队头(front)也不断后移,即位置在变(如要位置不变,移动元素工作量也太大,7.3.2 队列,图7.16 顺序队列的插入和删除,由图可见:空队时指针(下标)front和rear在一起都指向队前方,当有元素进队,则rear后移;有元素出队,则front后移,最后分配给队的前端不再被利用,顺序表队列的缺点,7.3.2 队列,顺序表队列的改进:逻辑上的循环队列,注意,空队时rear=front,满队时必须空一个位置,7.3.2 队列,循环队列浪费一个位置好像太可惜,特别在该位置中存放一个很大的对象时。实际上对象很大时,总是由索引
37、(指针)来排队。若想利用这个空间,必然加一个标志来表示队空/队满,进队出队都要判断,使用上更不方便。 用链表实现队列无此问题,例7.10】顺序存储方式的循环队列类模板,例7.11】链队类模板。链首出队,链尾入队。无链表头结点方式,7.4二叉树(选读,树形结构是一类重要的非线性数据,树和二叉树是常用的树形结构,7.4.2 二叉树的遍历,7.4.1二叉树的概念,7.4.3 排序二叉树,7.4.1二叉树的概念 (选读,树(Tree)是由n(n0)个结点组成的有限集合。如n=0,称为空树。非空树有一个特定的结点,它只有直接后继,没有直接前驱,称之为根(root)。除根以外的其它结点划分为m(m0)个互
38、不相交的有限集合T0,T1,Tm-1,每个集合又是一棵树,称为根的子树(subtree)。每棵子树的根结点有且仅有一个直接前驱,但可以有0个或多个直接后继。这是一个递归方法定义的数据结构,树的概念,7.4.1二叉树的概念(选读,图7.18 树的示意图,结点,包括数据项和多个指针项,指针项数目并不固定,且无次序。 结点的度,结点所拥有的子树数量。 叶结点,度为0的结点,如G,I,J,K,L,M,N,O结点。 分支结点,度1的结点。 孩子结点,若结点x有子树,则子树根结点即为x的孩子结点,树的术语,7.4.1二叉树的概念(选读,图7.18 树的示意图,双亲结点,若结点x有孩子,它即为孩子的双亲。
39、兄弟结点,同一双亲的结点互称为兄弟。 结点的层次,从根到该结点所经路径上的分支条数。 树的深度,树中结点的层次数。 树的度,树中结点度的最大值,树的术语,7.4.1二叉树的概念(选读,二叉树(Binary Tree)是另一种独立的树形结构。二叉树是结点的一个有限集合,该集合或为空,或是由一个根结点及两棵树分别称为左子树和右子树的(注意有左右之分)互不相交的二叉树组成,其中左右子树分别可以为空子树或均为空树。这也是一个递归的定义。二叉树的特点是:每个结点最多两个孩子,并且子树有左右之分,二叉树的基本性质: 1二叉树的第i层上最多有2i-1(i=1)个结点; 2深度为h的二叉树中最多有2h-1个结
40、点; 3在任一棵二叉树中,有n0个叶子结点,有n2个度为2的 结点,则有n0=n2+1,树的概念,7.4.1二叉树的概念(选读,例7.12】画出有三个结点的所有二叉树。 解:结果见图7.19,共5种,图7.19 5种不同的三结点二叉树,7.4.1二叉树的概念(选读,满二叉树和完全二叉树: 分别如图7.20和图7.21,完全二叉树已有的结点排序与满二叉树相同,图7.20 满二叉树,图7.21 完全二叉树,7.4.1二叉树的概念(选读,下面给出链式储存方式的二叉树。每个结点有三个域:数据域、左孩子指针和右孩子指针,见图7.22,图7.22 二叉树结点,7.4.1二叉树的概念(选读,二叉树类结点类模
41、板定义: templateclass BinaryTree; templateclass Node Node *lchild,*rchild; T info; public: Node() lchild=NULL; rchild=NULL; Node(T data,Node *left=NULL , Node *right=NULL) info=data; lchild=left; rchild=right;,7.4.1二叉树的概念(选读,T Getinfo()return info; /取得结点数据 void setinfo(const T /二叉树类说明为友元类,7.4.2 二叉树的遍历(
42、选读,二叉树的遍历(binary tree traversal): 遵从某种次序,查巡二叉树的所有结点,每个结点都被访问一次,而且仅访问一次。所谓“访问”指对结点施行某些操作,但不破坏它原来的数据结构。 遍历二叉树有不同次序,规定先左后右,令L,R,V分别代表遍历一个结点的左右子树和访问该结点的操作,有三种方式: 前序遍历(VLR) 中序遍历(LVR) 后序遍历(LRV,7.4.2 二叉树的遍历(选读,遍历实例:前序遍历访问次序为ABDEGCFH,图7.23 二叉树遍历,中序遍历结果为 D B G E A F H C。 后序遍历结果为 D G E B H F C A,7.4.2 二叉树的遍历(
43、选读,例7.13】二叉树类模板(其中二叉树生成借用二叉排序树,见下节)。特别注意插入结点时,第二参数为指针的引用!否则不能建立树。为什么?请读者自己思考。本例采用简单的接口函数,而把较复杂的算法作为私有函数。 templateclass BinaryTree Node *root; /二叉树的根指针 void InOrder(Node *Current); /中序遍历 void PreOrder(Node *Current); /前序遍历 void PostOrder(Node *Current); /后序遍历 void Insert(const T /删除树,7.4.2 二叉树的遍历(选读,
44、public: BinaryTree()root=NULL;/空树构造函数 BinaryTree()Destory(root);/析构函数 void Creat(T* data,int n); /建立(排序)二叉树 void InOrder()InOrder(root); /中序遍历,公有函数为接口 void PreOrder()PreOrder(root); /前序遍历,公有函数为接口 void PostOrder()PostOrder(root); /后序遍历,公有函数为接口,检验主函数,7.4.2 二叉树的遍历(选读,例7.14】某二叉树先序遍历为ABCEFDGHIJK,中序遍历为ECF
45、BDGAIHJK绘出该二叉树,图7.24 例7.14二叉树,按同样方法推出A的右子树。结果如图7.23。可以证明已知先序和中序访问次序可以唯一确定一棵二叉树,解:由先序知A为根结点,而由中序知E F B D G为左子树,I H J K为右子树。由先序中的B C E F D G知B为左子树根结点,由中序中的E C F B D G知E C F为其左子树,而DG为右子树,再由先序C E F知C为左子树根结点,由中序E C F知E为C左子树,F为的右子树。再由先序D G知,D为B的右子树根结点,由中序D G知G为D的右子树,7.4.3 排序二叉树 (选读,二叉排序树(Binary Sorting Tr
46、ee)又称二叉搜索树(Binary Search Tree),是一种特殊结构的二叉数,作为一种排序和查找的手段,对应有序表的对半查找,通常亦被称为数表。其定义也是递归的,二叉排序树的定义: 二叉排序树或者是空树或者是具有下述性质的二叉数,其左子树上所有结点的数据值均小于根结点的数据值;右子树上所有结点的数据值均大于等于根结点的数据值,左子树和右子树又各是一棵二叉排序树,7.4.3 排序二叉树(选读,二叉排序树用中序遍历就可以得到由小到大的有序序列,如图7.24可得有序序列8,10,11,12,15,16,18,22,24,26,29,图7.24 二叉排序树例,二叉排序树的遍历,二叉排序树生成算
47、法: 对任意一组数据元素序列a0,a1,a2,an-1。要生成二叉排序树的过程为: 1. 令a0为二叉树的根结点。 2.若a1a0,令a1为a0左子树的根结点,否则a1为a0右子树的根结点。 3. 如ai小于根结点,则去左子树,否则去右子树,按此法查找,直到成为空树,则安放此位置。这步就是插入算法,7.4.3 排序二叉树(选读,7.4.3 排序二叉树(选读,templateNode* BinaryTree:Find(const T,例7.15】排序二叉树查找函数。 算法:用中序遍历即可,但递归慢,下面为迭代查找算法,第七章 动态内存分配与数据结构,完,谢谢,7.1.1自由存储区内存的分配与释放
48、【例7.1,例7.1】动态数组的建立与撤销 #include #include using namespace std; int main() int n; char *pc; coutn; /在运行时确定,可输入25 pc=new charn; strcpy(pc,“自由存储区内存的动态分配); coutpcendl; delete pc;/释放pc所指向的n个字符的内存空间 return 0,7.1.1自由存储区内存的分配与释放【例7.2,例7.2】 动态创建和删除一个m*n个元素的数组。采用指针数组方式来完成二维数组的动态创建,int m=4; /行数 int n=6; /列数 二维数组
49、的动态创建: int main() double *data; int i,j; data = new double*m; /设置行 if (data ) = 0) cout Could not allocate. Bye .; return -1; for(j=0;jm;j+) dataj = new doublen; /设置列 if (dataj = 0) cout Could not allocate. Bye .; return -1;,7.1.1自由存储区内存的分配与释放【例7.2,for (i=0;im;i+) for (j=0;jn;j+) dataij=i*n+j; /初始化数
50、组元素 display(data); de_allocate(data); return 0; 二维数组的撤销与内存释放: void de_allocate(double *data) for (int i=0;im;i+) delete datai; /注意撤销次序,先列后行,与设置相反 delete data;,7.1.2自由存储区对象与构造函数【例7.3,类说明: class CGoods string Name; int Amount; float Price; float Total value; public: CGoods()cout调用默认构造函数endl; CGoods(st
51、ring name,int amount ,float price) cout调用三参数构造函数endl; Name=name; Amount=amount; Price=price; Total_value=price*amount; CGoods() cout调用析构函数endl;,例7.3】演示自由存储区对象分配和释放,7.1.2自由存储区对象与构造函数【例7.3,使用: int main() int n; CGoods *pc,*pc1,*pc2; pc=new CGoods(“夏利2000”,10,118000); /调用三参数构造函数 pc1=new CGoods(); /调用默认
52、构造函数 coutn; pc2=new CGoodsn; /动态建立数组,不能初始化,调用n次默认构造函数 delete pc; delete pc1; delete pc2; return 0,例7.4 实现深复制,默认构造函数: student:student() coutConstructor; pName=NULL; cout默认“endl;,带参数构造函数: student:student(char *pname) coutConstructor; if(pName=new charstrlen(pname)+1) strcpy(pName,pname); coutpNameendl
53、;,例7.4 实现深复制,复制构造函数: student:student(student,析构函数: student:student() coutDestructorpNameendl; if(pName) pName0=0; delete pName; /释放字符串,例7.4 实现深复制,复制赋值操作符: student,int main(void) student s1(范英明),s2(沈俊),s3; student s4=s1; s3=s2;return 0,例7.4 实现深复制,例7.5_h 单链表类型模板,template Node:Node()link=NULL; template
54、 Node:Node(const T,例7.5_h 单链表类型模板,链表类成员函数: templateList:List() head=tail=new Node(); templateList:List() MakeEmpty();delete head; templatevoid List:MakeEmpty()/清空链表 Node *tempP; while(head-link!=NULL) tempP=head-link; head-link=tempP-link; /把头结点后的第一个结点从链中脱离 delete tempP; /删除(释放)脱离下来的结点 tail=head; /表
55、头指针与表尾指针均指向表头结点,表示空链,例7.5_h 单链表类型模板,链表类成员函数: template Node* List:Find(T data) Node *tempP=head-link; while(tempP!=NULL,例7.5_h 单链表类型模板,链表类成员函数: templatevoid List:PrintList()/显示链表 Node* tempP=head-link; while(tempP!=NULL) coutinfolink; coutvoid List:InsertFront(Node *p) p-link=head-link; head-link=p;
56、if(tail=head) tail=p; templatevoid List:InsertRear(Node *p) p-link=tail-link; tail-link=p; tail=p,链表类成员函数: template void List:InsertOrder(Node *p) Node *tempP=head-link,*tempQ=head; /tempQ指向tempP前面的一个结点 while(tempP!=NULL) if(p-infoinfo)break; /找第一个比插入结点大的结点,由tempP指向 tempQ=tempP; tempP=tempP-link; te
57、mpQ-InsertAfter(p); /插在tempP指向结点之前,tempQ之后 if(tail=tempQ) tail=tempQ-link; template Node* List:CreatNode(T data) Node*tempP=new Node(data); return tempP,例7.5_h 单链表类型模板,链表类成员函数: template Node* List:DeleteNode(Node* p) Node* tempP=head; while(tempP-link!=NULL /本函数所用方法可省一个工作指针,与InsertOrder比较,例7.5_h 单链表
58、类型模板,例7.5 单链表类型模板主函数,void main() Node * P1; List list1,list2; int a16,i,j; coutai; /随机输入16个整数 for(i=0;i16;i+) P1=list1.CreatNode(ai); list1.InsertFront(P1); /向前生成list1 P1=list2.CreatNode(ai); list2.InsertRear(P1); /向后生成list2 list1.PrintList(); coutlist1长度:list1.Length()endl; list2.PrintList(,例7.5 单链
59、表类型模板主函数,coutj; P1=list1.Find(j); if(P1!=NULL) P1=list1.DeleteNode(P1); delete P1; list1.PrintList(); coutlist1长度:list1.Length()endl; else cout未找到endl; list1.MakeEmpty();/清空list1 for(i=0;i16;i+) P1=list1.CreatNode(ai); list1.InsertOrder(P1); /升序创建list1 list1.PrintList(); return 0,例7.6 学生档案的管理,include Ex7_6.h class student int id ;string name;char sex; int age;string address; float eng, phy, math, electron; /英语,物理,数学和电子学成绩 public: student(int=0,string=,char=0,int=0,string=,
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- GB/T 20870.4-2024半导体器件第16-4部分:微波集成电路开关
- 发展规划部总经理岗位职责说明
- 高中地理 第三章 自然资源的利用与保护 3.2 非可再生资源合理开发利用对策教案 新人教版选修6
- 八年级历史下册 第五单元 第15课《独立自主的和平外交》教学设计含教后反思 新人教版
- 河北省涞水波峰中学七年级地理上册 3.4 世界的气候说课稿 新人教版
- 2023四年级数学上册 七 三位数除以两位数的除法说课稿 西师大版
- 2024-2025学年高二地理第3周教学设计
- 租奶牛合同(2篇)
- 综合班组合同(2篇)
- 房屋租赁合同(2篇)
- 高血压危象课件2
- 部编版七年级上册语文 第三单元 周周清(一)
- JD-BQ(M)电动执行器技术规范书(隔爆)
- 小学《道德与法治》课堂教学生活化的研究课题实施方案
- 光伏并网逆变器调试报告(正式版)
- 303093 池国华 《内部控制与风险管理(第3版)》思考题和案例分析答案
- 化工安全隐患大排查内容
- 中英文版送货单
- 主持人大赛评分表
- 广东省建设工程造价咨询服务收费项目和收费标准表[粤价函(2011)742号]
- (自己编)丝网除沫器计算
评论
0/150
提交评论