1.1数据结构在程序设计中的作用
数据结构+算法=程序。
1.2本书讨论的主要内容
计算机能够求解的问题一般可以分为数值问题和非数值问题。数值问题抽象出的数据模型通常是数学方程,非数值问题抽象出的数学模型通常是线性表、树、图等数据结构。本书讨论非数值问题的数据组织和处理,主要内容有如下4点:
一、数据的逻辑结构:线性表、树、图等数据结构,其核心是如何组织待处理的数据以及数据之间的关系。
二、数据的存储结构:如何将线性表、树、图等数据结构存储到计算机的存储器中,其核心是如何有效地处理数据。
三、算法:如何基于数据的某种存储结构实现插入、删除、查找等基本操作,其核心是如何有效地处理数据。
四、常用数据处理技术:包括查找技术、排序技术、索引技术等。
1.3 数据结构的基本概念
1.3.1数据结构
1、数据:信息的载体,在计算机科学中是指所有能输入到计算机中并能被计算机程序识别和处理的符号集合。
2、数据元素:数据的基本单位,在计算机程序中通常作为一个整体进行考虑和处理。
3、数据项:构成数据元素的不可分割的最小单位。
4、数据的逻辑结构:数据元素之间逻辑关系的整体, 可分为四类:集合、线性结构、树结构、图结构。
5、数据的存储结构(物理结构):是数据及其逻辑结构在计算机中的表示。
通常有两种:
顺序存储结构的基本思想是:用一组连续的存储单元依次存储数据元素,数据元素之间的逻辑关系由元素的存储位置来表示。
链式存储结构的基本思想是:用一组任意的存储单元存储数据元素,数据元素之间的逻辑关系用指针来表示。
数据的逻辑结构是从具体的问题抽象出来的数据模型,是面向问题的,反映了数据元素之间的关联方式或邻接关系。数据的存储结构是面向计算机的,其基本目标是将数据及其逻辑关系存储到计算机的内存中。为了区别于数据的存储结构,常常将数据的逻辑结构称为数据结构。
1.3.2抽象数据类型
1.数据类型:一组值的集合及定义与这个值集上的一组操作的总称,规定了该类型数据的取值范围和对这些数据所能采取的操作。
2.抽象:抽出问题本质特征而忽略其非本质的细节,是对具体事物的一个概括。
3.抽象数据类型:一个数据结构及定义在该结构上的一组操作的总称。
数据类型和ADT的区别在于:数据类型指高级程序设计语言支持的基本数据类型,ADT指自定义的数据类型。
1.4 算法及算法分析
1.4.1算法及其描述方法:
算法:对特定问题求解步骤的一种描述,是指令的有限序列。算法必须满足下列5个重要特性:输入、输出、有穷性、确定性和可行性。
好算法要满足算法的五大特性,此外具备 :正确性、健壮性、简单性、抽象分级和高效性。
算法的描述方法:自然语言、流程图、程序设计语言、伪代码。
1.4.2算法分析
度量算法效率的方法:我们通常会采用另一种方法:事前分析估算的方法----渐进复杂度。
算法的时间复杂度:当问题规模充分大时,算法中基本语句的执行次数在渐进意义下的阶,称作算法的渐进时间复杂度,简称时间复杂度,通常用大O(读作“大欧”)记号表示。
最好、最坏和平均情况
算法的空间复杂度: 算法的空间复杂度是指在算法的执行过程中,需要的辅助空间数量。辅助空间是除算法本身和输入输出数据所占据的空间外,算法临时开辟的存储空间。
算法分析举例:分析算法时间复杂度:找出所有语句中执行次数最大的那条语句作为基本语句,计算基本语句的执行次数,取其数量级放入大O中即可。
2.1线性表的逻辑结构
2.1.1线性表的定义
n个有相同类型的数据结构元素的有限序列。
线性表是最基本、最简单、也是最常用的一种数据结构。线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的。线性表的逻辑结构简单,便于实现和操作。因此,线性表这种数据结构在实际应用中是广泛采用的一种数据结构。
长度:线性表中数据元素的个数。
2.1.2线性表的抽象数据类型定义
对于不同的应用,线性表的基本操作不同
线性表的基本操作:
1.InitList(&L) 初始化:构造一个空的线性表L。
2.DestroyList(&L) 销毁:销毁一个业已存在的线性表L。
3.ClearList(&L) 清空:将一业已存在的线性表L重置为空表。
4.ListEmpty(L) 判表空:若L为空表,则返回TRUE;否则返回FALSE 。
5.ListLength(L) 求长度:对给定的线性表L,返回线性表L的数据元素的个数。
6.GetElem(L,i,&e) 对给定的线性表L,取第i个数据元素。0≤i≤Length(L)-1),用e返回L中第i个数据元素的值。
或GetElem(L,I) , 1≤i≤Length(L),正确返回值,否则出错。
7.LocateElem(L,e) e为线性表中的同型元素,定位返回L中第一个与e满足相等关系数据元素的位序, 若这种数据元素不存在, 则返回0 。
8.PriorElem(L,cur_e,&pre_e) 求前驱:若cur_e是L的数据元素, 且不是第一个, 则用pre_e返回它的前驱, 否则操作失败, pre_e无定义。
或PriorElem(L,e) 求前驱: e是L表中同质元素,求出e的前驱元素并用e返回其值。若有值,函数返回真,否则返回假。
9.NextElem(L,cur_e,&next_e)求后继 若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继,否则操作失败, next_e无定义。或用NextElem(L,e) 求后继: e是L表中同质元素求出e的后继元素并用e返回其值。若有值,函数返回真,否则返回假。
10.ListInsert(&L,i,e) 插入 在L中第i个位置之前插入新的数据元素e,L的长度加1 。
11.ListDelete(&L,i,&e) 删除 删除L的第i个数据元素,并用e返回其值,L的长度减1 。(i的选择要合法)
12.ListTraverse(L,visit()) 遍历 对给定的线性表L,依次输出L的每一个数据元素。(不允许重复)
13.Copy(L,C) 复制 将给定的线性表L复制到线性表C中。
14.Merge(A,B,C) 合并 将给定的线性表A和B合并为线性表C。}ADT List
2.2 线性表的顺序存储结构及实现
2.2.1线性表的顺序结构——顺序表
线性表有两种实现,一种是顺序实现,一种是链式实现。上述中我们已经定义了顺序表SqList,那么我们现在先用顺序实现。
顺序实现的几个例子:注意:在上述定义顺序表SqList时,我们用了两种方法(非指针和指针),那么在实现顺序结构时,也用两种方法进行实现,分别对应两种定义。
初始化:将表中的元素进行初始化。
1. Status InitList_Sq( SqList& L )
2. { // 构造一个空的线性表
3. L.length = 0; //表长度为0,即元素个数设置为0
4. return OK;
5. } // InitList_Sq
6. 指针实现
7. Status InitList_Sq( SqList& L )
8. { // 构造一个空的线性表
9. //用动态内存分配表的空间
10.L.elem=(ElemType *)malloc(MAXSIZE *sizeof(ElemType));
11. If(!L.elem)exit(OVERFLOW); //分配失败
12.L.length = 0; //表长度为0,即元素个数设置为0
13.L.listsize=MAXSIZE ;//初始存储容量,也就上定义中的10
14. return OK;
15.} // InitList_Sq
查找定位
while( i<=L.length && !(*compare)(*p++,e))中 compare是一个函数,*p++和e 都是这个函数的参数。这句代码理解为:先是p指针指向的值加1,得到的值和e变量作为实际参数传递给函数compare进行处理,然后根据compare函数处理完的结果进行真假判断(真为1,假为0),然后!取反
插入:在顺序表中指定位置插入一个已知元素
newbase=(ElemType*)realloc(L.elem,(L.listsize+MAXSIZE)*sizeof(ElemType));这句代码实际上是从新分配了一个空间,然后把原来空间复制到新空间,然后又增加了一个新的空间,这个新的空间大小为LISTINCREMENT.
删除:将表中指定位置的元素删除
1. Status ListDelete (SqList &L, int i, ElemType &e)
2. { // 在顺序表L中删除第i个元素,用e返回删除的值
3. // i 的合法范围为 1≤i≤L.length+1
4. if (i < 1 || i > L.length) return ERROR; // 插入位置不合法
5. e=L.elem[i-1]; 将被删除的元素赋给e
6. for(j=i+1;j<=length;j++)
7. L.elem[j-2]=L.elem[j-1];
8. L.length--;
9. return OK;
10. } // ListDelete_Sq
11. 指针
12. Status ListDelete (SqList &L, int i, ElemType &e)
13. { // 在顺序表L中删除第i个元素,用e返回删除的值
14. // i 的合法范围为 1≤i≤L.length+1
15. if (i < 1 || i > L.length) return ERROR; // 插入位置不合法
16. p=&(L.elem[i-1]);
17. e=*p;
18. q=L.elem+L.length-1;
19. for(++p;p<=q;++p)*(p-1)=*p;
20. --L.length;
21. return OK;
22. } // ListDelete_Sq
取元素
1. Status GetElem (SqList L, int i, ElemType &e) {
2. e=L.elem[i-1];
3. return OK;
4. }//GetElem
2.3线性表的链接存储结构及实现
优点
逻辑相邻,物理相邻
可随机存取任一元素
存储空间使用紧凑
缺点
插入、删除操作需要移动大量的元素
预先分配空间需按最大空间分配,利用不充分
表容量难以扩充
将线性表L中第i个数据元素删除
int ListDelete(SEQLIST *L,int i,Elemtype *e)
{
if (IsEmpty(L)) return ERROR; //检测线性表是否为空
if (i<1||i>L->length) return ERROR; //检查i值是否合理
*e=L->elem[i-1];
//将欲删除的数据元素内容保留在e所指示的存储单元中
for (j=i;j<=L->length-1;j++)
//将线性表第i+1个元素之后的所有元素向前移动
L->elem[j-1]=L->elem[j];
L->length--;
return OK;
}
C语言中的数组下标从“0”开始,因此,若L是Sqlist类型的顺序表,则表中第i个元素是L.data[I-1]。
线性表的顺序表示的特点是用物理位置上的邻接关系来表示结点间的逻辑关系,这一特点使我们可以随机存取表中的任一结点,但它也使得插入和删除操作会移动大量的结点.为避免大量结点的移动,我们介绍线性表的另一种存储方式,
链式存储结构,简称为链表(Linked List)。
线性链表
链表是指用一组任意的存储单元来依次存放线性表的结点,这组存储单元既
可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。因此,链表中结点的逻辑次序和物理次序不一定相同。为了能正确表示结点间的逻辑关系,在存储每个结点值的同时,还必须存储指示其后继结点的地址(或位置)信息,这个信息称为指针(pointer)或链(link)。这两部分组成了链表中的结点结构:
其中:data域是数据域,用来存放结点的值。next是指针域(亦称链域),用来存放结点的直接后继的地址(或位置)。
链表正是通过每个结点的链域将线性表的n个结点按其逻辑次序链接在一起的。由于上述链表的每一个结只有一个链域,故将这种链表称为单链表(Single Linked)。
单链表又有带头结点结构和不带头结点结构两种。头指针(设为head)所指的不存放数据元素的第一个结点称为头结点。存放第一个数据元素的结点称为首元结点或第一个结点。首元结点在带头结点的单链表中是链表的第二个结点,在不带头结点的单链表中是链表的第一个结点。
带头结点的链式结构的优点:
在首元结点前插入头结点与在其他结点前插入结点一样,不会改变head的值,改变的是head->next的值,删除首元结点也一样。
一个单链表是否带头结点是由初始化操作决定的,由初始化操作定义空的单链表头指针指向头结点时,则单链表带头结点;由初始化操作定义空的单链表头指针指向NULL时,则单链表不带头结点。
在链表中,即使知道被访问结点的序号i,也不能象顺序表中那样直接按序号i访问结点,而只能从链表的头指针出发,顺链域next逐个结点往下搜索,直到搜索到第i个结点为止。因此,链表不是随机存取结构。
设单链表的长度为n,要查找表中第i个结点,仅当1≦i≦n时,i的值是合法的。但有时需要找头结点的位置,故我们将头结点看做是第0 个结点
按值查找是在链表中,查找是否有结点值等于给定值key的结点,若有的话,则返回首次找到的其值为key的结点的存储位置;否则返回NULL。查找过程从开始结点出发,顺着链表逐个将结点的值和给定值key作比较。
插入运算是将值为x的新结点插入到表的第i个结点的位置上,即插入到
ai-1与ai之间。因此,我们必须首先找到ai-1的存储位置p,然后生成一个数据域为x的新结点*p,并令结点*p的指针域指向新结点,新结点的指针域指向结点ai。从而实现三个结点ai-1,x和ai之间的逻辑关系的变化
删除运算是将表的第i个结点删去。因为在单链表中结点ai的存储地址是在其直接前趋结点a a i-1的指针域next中,所以我们必须首先找到
a i-1的存储位置p。然后令p–>next指向ai的直接后继结点,即把ai从链上摘下。最后释放结点
ai的空间,将其归还给“存储池”。
循环链表是一种头尾相接的链表。其特点是无须增加存储量,仅对表的链接方式稍作改变,即可使得表处理更加方便灵活。
在单链表中,将终端结点的指针域NULL改为指向表头结点或开始结点,就得到了单链形式的循环链表,并简单称为单循环链表。
为了使空表和非空表的处理一致,循环链表中也可设置一个头结点。这样,空循环链表仅有一个自成循环的头结点表示。
在很多实际问题中,表的操作常常是在表的首尾位置上进行,此时头指针表示的单循环链表就显得不够方便.如果改用尾指针rear来表示单循环链表,则查找开始结点a1和终端结点an都很方便,它们的存储位置分别是(rear–>next) —>next和rear,显然,查找时间都是O(1)。因此,实际中多采用尾指针表示单循环链表。
由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或p—>next是否为空,而是判断它们是否等于某一指定指针,如头指针或尾指针等。
双向链表(Double linked list):在单链表的每个结点里再增加一个指向其直接前趋的指针域prior。这样就形成的链表中有两个方向不同的链,故称为双向链表。
2.4 线性表结构特点
线性表具有如下的结构特点:
1.均匀性:虽然不同数据表的数据元素可以是各种各样的,但对于同一线性表的各数据元素必定具有相同的数据类型和长度。
2.有序性:各数据元素在线性表中的位置只取决于它们的序号,数据元素之前的相对位置是线性的,即存在唯一的“第一个“和“最后一个”的数据元素,除了第一个和最后一个外,其它元素前面均只有一个数据元素直接前驱和后面均只有一个数据元素(直接后继)。
在实现线性表数据元素的存储方面,一般可用顺序存储结构和链式存储结构两种方法。链式存储结构将在本网站线性链表中介绍,本章主要介绍用数组实现线性表数据元素的顺序存储及其应用。另外栈、队列和串也是线性表的特殊情况,又称为受限的线性结构。
3.1 栈
3.1.1栈的逻辑结构
1. 栈:限定仅在表的一端进行插入和删除操作的线性表。
允许插入和删除的一端称为栈顶,另一端称为栈底。
空栈:不含任何数据元素的栈。
栈的操作特性:后进先出
注意:栈只是对表插入和删除操作的位置进行了限制,并没有限定插入和删除操作进行的时间。
2.栈的抽象数据类型定义
ADT Stack
Data
栈中元素具有相同类型及后进先出特性,
相邻元素具有前驱和后继关系
Operation
InitStack
前置条件:栈不存在
输入:无
功能:栈的初始化
输出:无
后置条件:构造一个空栈
DestroyStack
前置条件:栈已存在
输入:无
功能:销毁栈
输出:无
后置条件:释放栈所占用的存储空间
Push
前置条件:栈已存在
输入:元素值x
功能:在栈顶插入一个元素x
输出:如果插入不成功,抛出异常
后置条件:如果插入成功,栈顶增加了一个元素
Pop
前置条件:栈已存在
输入:无
功能:删除栈顶元素
输出:如果删除成功,返回被删元素值,否则,抛出异常
后置条件:如果删除成功,栈减少了一个元素
GetTop
前置条件:栈已存在
输入:无
功能:读取当前的栈顶元素
输出:若栈不空,返回当前的栈顶元素值
后置条件:栈不变
Empty
前置条件:栈已存在
输入:无
功能:判断栈是否为空
输出:如果栈为空,返回1,否则,返回0
后置条件:栈不变
endADT
3.1.2栈的顺序存储结构及实现
1.栈的顺序存储结构——顺序栈
(1)栈的初始化
(2)入栈操作
(3)出栈操作
(4)取栈顶元素
(5)判空操作
2.两栈共享空间
初始化
:
初始化运算是将栈顶初始化为
0
两栈共享空间:使用一个数组来存储两个栈,让一个栈的栈底为该数组的始端,另一个栈的栈底为该数组的末端,两个栈从各自的端点向中间延伸。
初始化
:
初始化运算是将栈顶初始化为
0
两栈共享空间控制类型声明
const int Stack_Size=100;
template <class DataType>
class BothStack
{
public:
BothStack( );
~BothStack( );
void Push(int i, DataType x);
DataType Pop(int i);
DataType GetTop(int i);
bool Empty(int i);
private:
DataType data[Stack_Size];
int top1, top2;
};
3.1.3栈的链接存储结构及实现
初始化
:
初始化运算是将栈顶初始化为
0
1.链栈
链栈:栈的链接存储结构、
初始化
:
初始化运算是将栈顶初始化为
0
2.链栈的类声明
template <class DataType>
class LinkStack
{
public:
LinkStack( );
~LinkStack( );
void Push(DataType x);
DataType Pop( );
DataType GetTop( );
bool Empty( );
private:
Node<DataType> *top;
}
3.1.4顺序栈和链栈的比较
时间性能:相同,都是常数时间O(1)。
初始化
:
初始化运算是将栈顶初始化为
空间性能:
顺序栈:有元素个数的限制和空间浪费的问题。
链栈:没有栈满的问题,只有当内存没有可用空间时才会出现栈满,但是每个元素都需要一个指针域,从而产生了结构性开销
。
初始化
:
初始化运算是将栈顶初始化为
0
总之,当栈的使用过程中元素个数变化较大时,用链栈是适宜的,反之,应该采用顺序栈。
3.2 队列
3.2.1队列的逻辑结构
1.队列的定义
初始化
:
初始化运算是将栈顶初始化为
0
队列:只允许在一端进行插入操作,而另一端进行删除操作的线性表。
允许插入(也称入队、进队)的一端称为队尾,允许删除(也称出队)的一端称为队头。
空队列:不含任何数据元素的队列。
初始化
:
初始化运算是将栈顶初始化为
0
队列的操作特性:先进先出。
初始化
:
初始化运算是将栈顶初始化为
0
队列的抽象数据类型定义
初始化
:
初始化运算是将栈顶初始化为
0
ADT Queue
Data
队列中元素具有相同类型及先进先出特性,
相邻元素具有前驱和后继关系
Operation
InitQueue
前置条件:队列不存在
输入:无
功能:初始化队列
输出:无
后置条件:创建一个空队列
初始化
:
初始化运算是将栈顶初始化为
0
DestroyQueue
前置条件:队列已存在
输入:无
功能:销毁队列
输出:无
后置条件:释放队列所占用的存储空间
EnQueue
前置条件:队列已存在
输入:元素值x
功能:在队尾插入一个元素
输出:如果插入不成功,抛出异常
后置条件:如果插入成功,队尾增加了一个元素
初始化
:
初始化运算是将栈顶初始化为
0
DeQueue
前置条件:队列已存在
输入:无
功能:删除队头元素
输出:如果删除成功,返回被删元素值
后置条件:如果删除成功,队头减少了一个元素
GetQueue
前置条件:队列已存在
输入:无
功能:读取队头元素
输出:若队列不空,返回队头元素
后置条件:队列不变
初始化
:
初始化运算是将栈顶初始化为
0
Empty
前置条件:队列已存在
输入:无
功能:判断队列是否为空
输出:如果队列为空,返回1,否则,返回0
后置条件:队列不变
endADT
3.2.2
初始化
:
初始化运算是将栈顶初始化为
0
队列的顺序存储结构及实现
初始化
:
初始化运算是将栈顶初始化为
0
循环队列:将存储队列的数组头尾相接。
方法二:修改队满条件,浪费一个元素空间,队满时数组中只有一个空闲单元;
方法三:设置标志flag,当front=rear且flag=0时为队空,当front=rear且flag=1时为队满。
(1)构造函数
(2)入队操作
(3)出队操作
(4)读取队头元素
(5)判空操作
3.2.3循环队列和链队列的比较
初始化
:
初始化运算是将栈顶初始化为
0
时间性能:
循环队列和链队列的基本操作都需要常数时间O (1)
初始化
:
初始化运算是将栈顶初始化为
0
空间性能:
循环队列:必须预先确定一个固定的长度,所以有存储元素个数的限制和空间浪费的问题。
链队列:没有队列满的问题,只有当内存没有可用空间时才会出现队列满,但是每个元素都需要一个指针域,从而产生了结构性开销。
初始化
:
初始化运算是将栈顶初始化为
0
4.1字符串的定义
1.串——零个或多个字符组成的有限序列
串:零个或多个字符组成的有限序列。
串长度:串中所包含的字符个数。
空串:长度为0的串,记为:""。
非空串通常记为:
S=" s1 s2 …… sn "
其中:S是串名,双引号是定界符,双引号引起来的部分是串值,si(1≤i≤n)是一个任意字符。
1.1串的逻辑结构
子串:串中任意个连续的字符组成的子序列。
主串:包含子串的串。
子串的位置:子串的第一个字符在主串中的序号。
串的数据对象约束为某个字符集。
微机上常用的字符集是标准ASCII码,由 7 位二进制数表示一个字符,总共可以表示 128 个字符。
扩展ASCII码由 8 位二进制数表示一个字符,总共可以表示 256 个字符,足够表示英语和一些特殊符号,但无法满足国际需要。
Unicode由 16 位二进制数表示一个字符,总共可以表示 216个字符,能够表示世界上所有语言的所有字符,包括亚洲国家的表意字符。为了保持兼容性,Unicode字符集中的前256个字符与扩展ASCII码完全相同。
1.2 字符串的比较
串的比较:通过组成串的字符之间的比较来进行的。
给定两个串:X="x1x2…xn"和Y="y1y2…ym",则:
1. 当n=m且x1=y1,…,xn=ym时,称X=Y;
2. 当下列条件之一成立时,称X<Y:
⑴ n<m且xi=yi(1≤ i≤n);
⑵存在k≤min(m,n),使得xi=yi(1≤i≤k-1)且xk<yk。
4.1.2字符串的存储结构
方案1:用一个变量来表示串的实际长度。
方案2:在串尾存储一个不会在串中出现的特殊字符作为串的终结符,表示串的结尾。
方案3:用数组的0号单元存放串的长度,从1号单元开始存放串值。
4.1.3模式匹配
1.模式匹配:给定主串S="s1s2…sn"和模式T="t1t2…tm",在S中寻找T 的过程称为模式匹配。如果匹配成功,返回T 在S中的位置;如果匹配失败,返回0。
模式匹配问题的特点
⑴算法的一次执行时间不容忽视:问题规模通常很大,常常需要在大量信息中进行匹配;
⑵算法改进所取得的积累效益不容忽视:模式匹配操作经常被调用,执行频率高。
2.BF算法
基本思想:从主串S的第一个字符开始和模式T 的第一个字符进行比较,若相等,则继续比较两者的后续字符;否则,从主串S的第二个字符开始和模式T 的第一个字符进行比较,重复上述过程,直到T 中的字符全部比较完毕,则说明本趟匹配成功;或S中字符全部比较完,则说明匹配失败。
在串S和串T中设比较的起始下标i和j;
2. 循环直到S或T的所有字符均比较完
2.1 如果S[i]=T[j],继续比较S和T的下一个字符;
2.2 否则,将i和j回溯,准备下一趟比较;
3. 如果T中所有字符均比较完,则匹配成功,返回匹配的起始比较下标;否则,匹配失败,返回0;
int BF(char S[ ], char T[ ])
{
i=0; j=0;
while (S[i]!='\0'&&T[j]!='\0')
{
if (S[i]==T[j]) {
i++; j++;
}
else {
i=i-j+1; j=0;
}
}
if (T[j]=='\0') return (i-j+1);
else return 0;
}
int BF(char S[ ], char T[ ])
{
i=0; j=0;start=0;
while (S[i]!='\0'&&T[j]!='\0')
{
if (S[i]==T[j]) {
i++; j++;
}
else {
start++; i=start; j=0;
}
}
if (T[j]=='\0') return start;
else return 0;
}
4.2 多维数组
4.2.1多维数组的定义
1.数组是由一组类型相同的数据元素构成的有序集合,每个数据元素称为一个数组元素(简称为元素),每个元素受n(n≥1)个线性关系的约束,每个元素在n个线性关系中的序号i1、i2、…、in称为该元素的下标,并称该数组为 n 维数组。
2.多维数组的特点
元素本身可以具有某种结构,属于同一数据类型;
数组是一个具有固定格式和数量的数据集合。
4.2.2数组的存储结构及寻址
一维数组
设一维数组的下标的范围为闭区间[l,h],每个数组元素占用 c 个存储单元,则其任一元素 ai的存储地址可由下式确定:
Loc(ai)=Loc(al)+(i-l)×c
二维数组
常用的映射方法有两种:
按行优先:先行后列,先存储行号较小的元素,行号相同者先存储列号较小的元素。
按列优先:先列后行,先存储列号较小的元素,列号相同者先存储行号较小的元素。
N维数组
n(n>2)维数组一般也采用按行优先和按列优先两种存储方法。请自行推导任一元素存储地址的计算方法。
4.3 矩阵的压缩存储
1.特殊矩阵:矩阵中很多值相同的元素并且它们的分布有一定的规律。
稀疏矩阵:矩阵中有很多零元素。
压缩存储的基本思想是:
⑴为多个值相同的元素只分配一个存储空间;
⑵对零元素不分配存储空间。
2.特殊矩阵的压缩存储——对角矩阵
对角矩阵:所有非零元素都集中在以主对角线为中心的带状区域中,除了主对角线和它的上下方若干条对角线的元素外,所有其他元素都为零。
3.对角矩阵的压缩存储
元素aij在一维数组中的序号
=2 + 3(i-2)+( j-i + 2)
=2i+ j -2
∵一维数组下标从0开始
∴元素aij在一维数组中的下标
= 2i+ j -3
4.稀疏矩阵的压缩存储
将稀疏矩阵中的每个非零元素表示为:
(行号,列号,非零元素值)——三元组
emplate <class DataType>
struct element
{
int row, col; //行号,列号
DataType item //非零元素值
};
三元组表:将稀疏矩阵的非零元素对应的三元组所构成的集合,按行优先的顺序排列成一个线性表。
5.稀疏矩阵的压缩存储——三元组顺序表
存储结构定义:
const int MaxTerm=100;
template <class DataType>
struct SparseMatrix
{
DataType data[MaxTerm]; //存储非零元素
int mu, nu, tu; //行数、列数、非零元个数
};
6.稀疏矩阵的压缩存储——十字链表
采用链接存储结构存储三元组表,每个非零元素对应的三元组存储为一个链表结点,结构为:
row:存储非零元素的行号
col:存储非零元素的列号
item:存储非零元素的值
right:指针域,指向同一行中的下一个三元组
down:指针域,指向同一列中的下一个三元组
1.树的逻辑结构
3.树的遍历操作
2.树的存储结构
1.双亲表示法
2.孩子表示法
3.二叉树的逻辑结构
1.二叉树的定义
2.二叉树的基本性质
3.二叉树的遍历操作
1.顺序存储结构
2.中序的非递推算法
1.树的定义
结点:在树中将数据元素称为结点。
树:N(N>=0)个结点的有限集合。
2.树的基本术语
结点的度:结点所拥有的子树的个数。
树的度:树中各结点度的最大值。
叶子结点:度为0的结点,也称为终端结点。
分支结点:度不为0的结点,也称为非终端结点。
孩子、双亲:树中某结点子树的根结点称为这个结点的孩子结点,这个结点称为它孩子结点的双亲结点;
兄弟:具有同一个双亲的孩子结点互称为兄弟。
路径:如果树的结点序列n1,n2, …, nk有如下关系:结点ni是ni+1的双亲(1<=i<k),则把n1, n2, …, nk称为一条由n1至nk的路径;
路径上经过的边的个数称为路径长度。
祖先、子孙:在树中,如果有一条路径从结点x到结点y,则x称为y的祖先,而y称为x的子孙。
结点所在层数:根结点的层数为1;对其余任何结点,若某结点在第k层,则其孩子结点在第k+1层。
树的深度:树中所有结点的最大层数,也称高度。
层序编号:将树中结点按照从上层到下层、同层从左到右的次序依次给他们编以从1开始的连续自然数。
有序树、无序树:如果一棵树中结点的各子树从左到右是有次序的,称这棵树为有序树;反之,称为无序树。
森林:m(m≥0)棵互不相交的树的集合。
树的应用很广泛,在不同的实际应用中,树的基本操作不尽相同。下面给出一个树的抽象数据类型定义的例子,简单起见,基本操作只包含树的遍历,针对具体应用,需要重新定义其基本操作。
ADT Tree
Data
树是由一个根结点和若干棵子树构成,
树中结点具有相同数据类型及层次关系
Operation
InitTree
前置条件:树不存在
输入:无
功能:初始化一棵树
输出:无
后置条件:构造一个空树
DestroyTree
前置条件:树已存在
输入:无
功能:销毁一棵树
输出:无
后置条件:释放该树占用的存储空间
PreOrder
前置条件:树已存在
输入:无
功能:前序遍历树
输出:树的前序遍历序列
后置条件:树保持不变
PostOrder
前置条件:树已存在
输入:无
功能:后序遍历树
输出:树的后序遍历序列
后置条件:树保持不变
endADT
树的遍历:从根结点出发,按照某种次序访问树中所有结点,使得每个结点被访问一次且仅被访问一次。
(1)前序遍历
树的前序遍历操作定义为:
若树为空,则空操作返回;否则
⑴访问根结点;
⑵按照从左到右的顺序前序遍历根结点的每一棵子树
(2)后序遍历
树的后序遍历操作定义为:
若树为空,则空操作返回;否则
⑴按照从左到右的顺序后序遍历根结点的每一棵子树;
⑵访问根结点。
(3)层序遍历
树的层序遍历操作定义为:
从树的第一层(即根结点)开始,自上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。
存储结构:数据元素以及数据元素之间的逻辑关系在存储器中的表示。
基本思想:用一维数组来存储树的各个结点(一般按层序存储),数组中的一个元素对应树中的一个结点,包括结点的数据信息以及该结点的双亲在数组中的下标。
template <class DataType>
struct PNode
{
DataType data; //数据域
int parent; //指针域,双亲在数组中的下标
} ;
链表中的每个结点包括一个数据域和多个指针域,每个指针域指向该结点的一个孩子结点。
孩子链表的基本思想:把每个结点的孩子排列起来,看成是一个线性表,且以单链表存储,则n个结点共有 n 个孩子链表。这 n 个单链表共有 n 个头指针,这 n 个头指针又组成了一个线性表,为了便于进行查找采用顺序存储。最后,将存放 n 个头指针的数组和存放n个结点的数组结合起来,构成孩子链表的表头数组。
二叉树是n(n≥0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
二叉树的特点:⑴每个结点最多有两棵子树;
⑵二叉树是有序的,其次序不能任意颠倒。
1.斜树
1 .所有结点都只有左子树的二叉树称为左斜树;
2 .所有结点都只有右子树的二叉树称为右斜树;
3.左斜树和右斜树统称为斜树。
斜树的特点:1. 在斜树中,每一层只有一个结点;
2.斜树的结点个数与其深度相同。
2.满二叉树
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上。
特点:叶子只能出现在最下一层;
只有度为0和度为2的结点。
3.完全二叉树
对一棵具有n个结点的二叉树按层序编号,如果编号为i(1≤i≤n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中的位置完全相同。
在满二叉树中,从最后一个结点开始,连续去掉任意个结点,即是一棵完全二叉树。
特点:1. 叶子结点只能出现在最下两层且最下层的叶子结点都集中在二叉树的左面;
2. 完全二叉树中如果有度为1的结点,只可能有一个,且该结点只有左孩子。
3. 深度为k的完全二叉树在k-1层上一定是满二叉树。
4. 在同样结点个数的二叉树中,完全二叉树的深度最小。
性质5-1 二叉树的第i层上最多有2i-1个结点(i≥1)
性质5-2 一棵深度为k的二叉树中,最多有2k-1个结点,最少有k个结点。
深度为k且具有2k-1个结点的二叉树一定是满二叉树,
深度为k且具有k个结点的二叉树不一定是斜树
性质5-3 在一棵二叉树中,如果叶子结点数为n0,
性质5-4 具有n个结点的完全二叉树的深度为log2n +1
性质5-5 对一棵具有n个结点的完全二叉树中从1开始按层序编号,则对于任意的序号为i(1≤i≤n)的结点(简称为结点i),有:
(1)如果i>1,则结点i的双亲结点的序号为 i/2;如果i=1,则结点i是根结点,无双亲结点。
(2)如果2i≤n,则结点i的左孩子的序号为2i;
如果2i>n,则结点i无左孩子。
(3)如果2i+1≤n,则结点i的右孩子的序号为2i+1;如果2i+1>n,则结点 i无右孩子。
(1)前序遍历
若二叉树为空,则空操作返回;否则:
①访问根结点;
②前序遍历根结点的左子树;
③前序遍历根结点的右子树。
(2)中序遍历
若二叉树为空,则空操作返回;否则:
①中序遍历根结点的左子树;
②访问根结点;
③中序遍历根结点的右子树。
(3)后序遍历
若二叉树为空,则空操作返回;否则:
①后序遍历根结点的左子树;
②后序遍历根结点的右子树。
③访问根结点;
(4)层序遍历
二叉树的层次遍历是指从二叉树的第一层(即根结点)开始,从上至下逐层遍历,在同一层中,则按从左到右的顺序对结点逐个访问。
二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置(下标)应能体现结点之间的逻辑关系——父子关系。
完全二叉树和满二叉树中结点的序号可以唯一地反映出结点之间的逻辑关系。
5.4.2二叉链表
基本思想:令二叉树的每个结点对应一个链表结点,链表结点除了存放与二叉树结点有关的数据信息外,还要设置指示左右孩子的指针。
template <class DataType>
struct BiNode
{
DataType data;
BiNode<T> *lchild, *rchild;
};
1.前序遍历
template <class DataType>
struct BiNode
{
DataType data;
BiNode<T> *lchild, *rchild;
};
2.中序遍历
emplate <class DataType>
void BiTree<DataType> :: InOrder (BiNode<DataType> *bt)
{
if (bt == NULL) return; //递归调用的结束条件
else {
InOrder(bt->lchild); //中序递归遍历bt的左子树
cout << bt->data; //访问根结点bt的数据域
InOrder(bt->rchild); //中序递归遍历bt的右子树
}
3.后序遍历
template <class DataType>
void BiTree<DataType> :: PostOrder(BiNode<DataType> *bt)
{
if (bt == NULL) return; //递归调用的结束条件
else {
PostOrder(bt->lchild); //后序递归遍历bt的左子树
PostOrder(bt->rchild); //后序递归遍历bt的右子树
cout << bt->data; //访问根结点bt的数据域
}
4.层序遍历
template <class DataType>
void BiTree<DataType> :: LeverOrder( )
{
front = rear = -1; //采用顺序队列,并假定不会发生上溢
if (root == NULL) return; //二叉树为空,算法结束
Q[++rear] = root; //根指针入队
while (front != rear) //当队列非空时
{
q = Q[++front]; //出队
cout << q->data;
if (q->lchild != NULL) Q[++rear] =q->lchild;
if (q->rchild != NULL) Q[++rear] =q->rchild;
}
5.构造函数
设二叉树中的结点均为一个字符。假设扩展二叉树的前序遍历序列由键盘输入,root为指向根结点的指针,二叉链表的建立过程是:
首先输入根结点,若输入的是一个“#”字符,则表明该二叉树为空树,即root=NULL;否则输入的字符应该赋给root->data,,之后依次递归建立它的左子树和右子树。
template <class DataType>
void BiTree::PreOrder(BiNode<DataType> *root)
{
top = -1; //采用顺序栈,并假定不会发生上溢
while (root != NULL || top != -1)
{
while (root != NULL)
{
cout<<root->data;
s[++top] = root;
root = root->lchild;
}
if (top != -1) {
root = s[top--];
root = root->rchild;
}
}
}
template <class DataType>
void BiTree::PreOrder(BiNode<DataType> *root)
{
top = -1; //采用顺序栈,并假定不会发生上溢
while (root != NULL || top != -1)
{
while (root != NULL)
{
s[++top] = root;
root = root->lchild;
}
if (top != -1) {
root = s[top--];
cout<<root->data;
root = root->rchild;
}
}
}
template <class DataType>
void BiTree<DataType> :: PostOrder(BiNode<DataType> *bt)
{
top = -1; //采用顺序栈,并假定栈不会发生上溢
while (bt != NULL || top != -1) //两个条件都不成立才退出循环
{
while (bt != NULL)
{
top++; s[top].ptr = bt;s[top].flag = 1; //root连同标志flag入栈
bt = bt->lchild;
}
while (top != -1 && s[top].flag ==2)
{
bt = s[top--].ptr; cout<< bt->data;
}
if (top != -1) {
s[top].flag = 2; bt =s[top].ptr->rchild;
}
}
}
1.树转化成二叉树
⑴加线——树中所有相邻兄弟之间加一条连线。
⑵去线——对树中的每个结点,只保留它与第一个孩子结点之间的连线,删去它与其它孩子结点之间的连线。
⑶层次调整——以根结点为轴心,将树顺时针转动一定的角度,使之层次分明。
2.森林转化成二叉树
⑴将森林中的每棵树转换成二叉树;
⑵从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树根结点的右孩子,当所有二叉树连起来后,此时所得到的二叉树就是由森林转换得到的二叉树。
3.二叉树转化成森林或树
⑴加线——若某结点x是其双亲y的左孩子,则把结点x的右孩子、右孩子的右孩子、……,都与结点y用线连起来;
⑵去线——删去原二叉树中所有的双亲结点与右孩子结点的连线;
⑶层次调整——整理由⑴、⑵两步所得到的树或森林,使之层次分明。
6.1 图的逻辑结构
6.1.1图的定义和逻辑结构
1.图的定义
图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:
G=(V,E)
其中:G表示一个图,V是图G中顶点的集合,E是图G中顶点之间边的集合。
在线性表中,元素个数可以为零,称为空表;
在树中,结点个数可以为零,称为空树;
在图中,顶点个数不能为零,但可以没有边。
如果图的任意两个顶点之间的边都是无向边,则称该图为无向图。
如果图的任意两个顶点之间的边都是有向边,则称该图为有向图。
2.图的基本术语
简单图:在图中,若不存在顶点到其自身的边,且同一条边不重复出现。
(1)线性结构
在线性结构中,数据元素之间仅具有线性关系;
在树结构中,结点之间具有层次关系;
在图结构中,任意两个顶点之间都可能有关系
(2)无向完全图和有向完全图
无向完全图:在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。
有向完全图:在有向图中,如果任意两个顶点之间都存在方向相反的两条弧,则称该图为有向完全图。
(3)稠密图和稀疏图
稀疏图:称边数很少的图为稀疏图;
稠密图:称边数很多的图为稠密图
(4)顶点地度。入读。出度
。顶点的度:在无向图中,顶点v的度是指依附于该顶点的边数,通常记为TD(v)。
顶点的入度:在有向图中,顶点v的入度是指以该顶点为弧头的弧的数目,记为ID(v);
顶点的出度:在有向图中,顶点v的出度是指以该顶点为弧尾的弧的数目,记为OD(v)。
(5)权。网
权:是指对边赋予的有意义的数值量。
网:边上带权的图,也称网图
(6)路。路径长度。回路
路径:在无向图G=(V, E)中,从顶点vp到顶点vq之间的路径是一个顶点序列(vp=vi0,vi1,vi2,…, vim=vq),其中,(vij-1,vij)∈E(1≤j≤m)。若G是有向图,则路径也是有方向的,顶点序列满足<vij-1,vij>∈E。
回路(环):第一个顶点和最后一个顶点相同的路径。
简单路径:序列中顶点不重复出现的路径。
简单回路(简单环):除了第一个顶点和最后一个顶点外,其余顶点不重复出现的回路。
(7)连通图。连通分量
连通图:在无向图中,如果从一个顶点vi到另一个顶点vj(i≠j)有路径,则称顶点vi和vj是连通的。如果图中任意两个顶点都是连通的,则称该图是连通图。
连通分量:非连通图的极大连通子图称为连通分量
(8)强连通图。强连通分量
强连通图:在有向图中,对图中任意一对顶点vi和vj (i≠j),若从顶点vi到顶点vj和从顶点vj到顶点vi均有路径,则称该有向图是强连通图。
强连通分量:非强连通图的极大强连通子图。
(9)生成树。生成森林
生成树:n个顶点的连通图G的生成树是包含G中全部顶点的一个极小连通子图。
生成森林:在非连通图中,由每个连通分量都可以得到一棵生成树,这些连通分量的生成树就组成了一个非连通图的生成森林。
6.2图的存储结构和实现
6.2.2邻接表
1.构造函数
ALGraph算法
template<class DataType>
ALGraph<DataType>:: ALGraph(DataType a[ ], int n, int e)
{
vertexNum = n; arcNum = e;
for (i = 0; i < vertexNum; i++)
{ //输入顶点信息,初始化顶点表
adjlist[i].vertex = a[i];
adjlist[i].firstedge = NULL;
}
for (k = 0; k < arcNum; k++) //输入边的信息存储在边表中
{
cin>>i>>j;
s = new ArcNode; s->adjvex = j;
s->next = adjlist[i].firstedge;
adjlist[i].firstedge = s;
}
}
2.深度优先遍历算法
template<class DataType>
voidALGraph<DataType> :: DFSTraverse(int v)
{
cout << adjlist[v].vertex; visited[v] = 1;
p = adjlist[v].firstedge; //工作指针p指向顶点v的边表
while (p !=NULL) //依次搜索顶点v的邻接点j
{
j = p->adjvex;
if (visited[j] == 0) DFSTraverse(j);
p = p->next;
}
}
3.广度优先遍历算法
template<class DataType>
voidALGraph<DataType> :: BFSTraverse(int v)
{
front = rear = -1; //初始化顺序队列
cout << adjlist[v].vertex; visited[v] = 1; Q[++rear] = v;
while (front !=rear) //当队列非空时
{
v = Q[++front];
p = adjlist[v].firstarc; //工作指针p指向顶点v的边表
while (p != NULL)
{
j = p->adjvex;
if (visited[j] == 0) {
cout << adjlist[j].vertex; visited[j] = 1;Q[++rear] = j;
}
p=p->next;
}
}
}
6.3最小生成树
6.3.最小生成树的定义
生成树的代价:设G = (V, E)是一个无向连通网,生成树上各边的权值之和称为该生成树的代价。
最小生成树:在图G所有生成树中,代价最小的生成树称为最小生成树。
6.4最短路径
在非网图中,最短路径是指两顶点之间经历的边数最少的路径。
在网图中,最短路径是指两顶点之间经历的边上权值之和最短的路径。
问题描述:给定带权有向图G=(V, E)和源点v∈V,求从v到G中其余各顶点的最短路径。
应用实例——计算机网络传输的问题:怎样找到一种最经济的方式,从一台计算机向网上所有其它计算机发送一条消息。
图的存储结构:带权的邻接矩阵存储结构
数组dist[n]:每个分量dist[i]表示当前所找到的从始点v到终点vi的最短路径的长度。初态为:若从v到vi有弧,则dist[i]为弧上权值;否则置dist[i]为∞。
数组path[n]:path[i]是一个字符串,表示当前所找到的从始点v到终点vi的最短路径。初态为:若从v到vi有弧,则path[i]为vvi;否则置path[i]空串。
数组s[n]:存放源点和已经生成的终点,其初态为只有一个源点v。
1.概述
1.查找的基本概念
2.线性表的查找顺序
3.数表的查找技术
4.散列表的查找技术
7.1概述
7.1.1查找的基本概念
1.关键码
关键码:可以标识一个记录的某个数据项。
键值:关键码的值。
主关键码:可以唯一地标识一个记录的关键码。
次关键码:不能唯一地标识一个记录的关键码
2.查找
查找:在具有相同类型的记录构成的集合中找出满足给定条件的记录。
给定的查找条件可能是多种多样的,为便于讨论,把查找条件限制为“匹配”,即查找关键码等于给定值的记录。
查找的结果:若在查找集合中找到了与给定值相匹配的记录,则称查找成功;否则,称查找失败
静态查找:不涉及插入和删除操作的查找。
动态查找:涉及插入和删除操作的查找。
静态查找适用于:查找集合一经生成,便只对其进行查找,而不进行插入和删除操作,或经过一段时间的查找之后,集中地进行插入和删除等修改操作;
动态查找适用于:查找与插入和删除操作在同一个阶段进行,例如当查找成功时,要删除查找到的记录,当查找不成功时,要插入被查找的记录。
查找结构:面向查找操作的数据结构,即查找基于的数据结构。
线性表:适用于静态查找,主要采用顺序查找技术和折半查找技术。
树表:适用于动态查找,主要采用二叉排序树的查找技术。
散列表:静态查找和动态查找均适用,主要采用散列技术。
7.2线性表的查找顺序
1.顺序查找
基本思想:从线性表的一端向另一端逐个将关键码与给定值进行比较,若相等,则查找成功,给出该记录在表中的位置;若整个表检测完仍未找到与给定值相等的关键码,则查找失败,给出失败信息。
int SeqSearch1(int r[ ], int n, int k)
//数组r[1] ~r[n]存放查找集合
{
i = n;
while (i > 0 && r[i] != k)
i--;
return i;
}
顺序查找的优缺点:
优点:算法简单而且使用面广
对表中记录的存储没有任何要求,顺序存储和链接存储均可;
对表中记录的有序性也没有要求,无论记录是否按关键码有序均可。
缺点:平均查找长度较大,特别是当待查找集合中元素较多时,查找效率较低。
2.折半查找
基本思想:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键码相等,则查找成功;若给定值小于中间记录的关键码,则在中间记录的左半区继续查找;若给定值大于中间记录的关键码,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所查找的区域无记录,查找失败。
折半查找——非递推算法
int BinSearch1(int r[ ], int n, int k)
{ //数组r[1] ~ r[n]存放查找集合
low = 1; high = n;
while (low <= high)
{
mid = (low + high) / 2;
if (k < r[mid]) high = mid - 1;
else if (k > r[mid]) low = mid + 1;
else return mid;
}
return 0;
}
折半查找——递推算法
int BinSearch2(int r[ ], int low, int high,int k)
{ //数组r[1] ~ r[n]存放查找集合
if (low > high) return 0;
else {
mid = (low + high) / 2;
if (k < r[mid])
return BinSearch2(r, low, mid-1, k);
else if (k > r[mid])
returnBinSearch2(r, mid+1, high, k);
else return mid;
}
}
折半查找判定树
判定树:折半查找的过程可以用二叉树来描述,树中的每个结点对应有序表中的一个记录,结点的值为该记录在表中的位置。通常称这个描述折半查找过程的二叉树为折半查找判定树,简称判定树。
判定树的构造方法
⑴当n=0时,折半查找判定树为空;
⑵当n>0时,折半查找判定树的根结点是有序表中序号为mid=(n+1)/2的记录,根结点的左子树是与有序表r[1] ~ r[mid-1]相对应的折半查找判定树,根结点的右子树是与r[mid+1] ~ r[n]相对应的折半查找判定树。
7.3数表的查找技术
1.二叉查找树
二叉排序树(也称二叉查找树):或者是一棵空的二叉树,或者是具有下列性质的二叉树:
⑴若它的左子树不空,则左子树上所有结点的值均小于根结点的值;
⑵若它的右子树不空,则右子树上所有结点的值均大于根结点的值;
⑶它的左右子树也都是二叉排序树。
二叉树的构造算法
BiSortTree::BiSortTree(int r[ ], int n)
{
for (i = 0; i < n; i++)
{
s = new BiNode<int>;
s->data = r[i];
s->lchild = s->rchild = NULL;
InsertBST(root, s);
}
}
小结
一个无序序列可以通过构造一棵二叉排序树而变成一个有序序列;
每次插入的新结点都是二叉排序树上新的叶子结点;
找到插入位置后,不必移动其它结点,仅需修改某个结点的指针;
在左子树/右子树的查找过程与在整棵树上查找过程相同;
新插入的结点没有破坏原有结点之间的关系。
二叉排序树的查找
BiNode*BiSortTree::SearchBST(BiNode<int> *root, int k)
{
if (root == NULL)
return NULL;
else if (root->data == k)
return root;
else if (k < root->data)
return SearchBST(root->lchild, k);
else returnSearchBST(root->rchild, k);
}
2.平衡二叉树
平衡二叉树:或者是一棵空的二叉排序树,或者是具有下列性质的二叉排序树:
⑴根结点的左子树和右子树的深度最多相差1;
⑵根结点的左子树和右子树也都是平衡二叉树。
7.4散列表的查找技术
散列的基本思想:在记录的存储地址和它的关键码之间建立一个确定的对应关系。这样,不经过比较,一次读取就能得到所查元素的查找方法。
散列表:采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间称为散列表。
散列函数:将关键码映射为散列表中适当存储位置的函数。
散列技术一般不适用于允许多个记录有同样关键码的情况。散列方法也不适用于范围查找,换言之,在散列表中,我们不可能找到最大或最小关键码的记录,也不可能找到在某一范围内的记录。
散列技术的关键问题:
⑴散列函数的设计。如何设计一个简单、均匀、存储利用率高的散列函数。
⑵冲突的处理。如何采取合适的处理冲突方法来解决冲突。
冲突:对于两个不同关键码ki≠kj,有H(ki)=H(kj),即两个不同的记录需要存放在同一个存储位置,ki和kj相对于H称做同义词。
设计散列函数一般应遵循以下原则:
⑴计算简单。散列函数不应该有很大的计算量,否则会降低查找效率。
⑵函数值即散列地址分布均匀。函数值要尽量均匀散布在地址空间,这样才能保证存储空间的有效利用并减少冲突。