前言
本文持续更新中。。。
可能会有未覆盖的知识点,本篇笔记是小编根据自己老师教的内容所作,希望能帮到大家O^O
速成只是个玩笑话,数据结构(C语言版)是计算机专业的重要基础学科,大家平时一定要多加练习才能牢记于心哦
目录
一、数据结构与算法
1.数据结构
- 数据是对客观事物的符号表示,如图像、声音等。
- 数据元素是数据的基本单位。
- 数据项是构成数据元素的不可分割的最小单位。 一个数据元素可由若干个数据项组成,例如,一位学生的信息记录为一个数据元素,它是由学号、姓名、性别等数据项组成。
- 数据对象是具有相同性质的数据元素的集合。
- 数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
数据结构包括三方面的内容:逻辑结构、存储结构和数据的运算。
数据结构的形式定义为:数据结构是一个二元组
Data Structure =(D,S)
其中:D是数据元素的有限集,S是D上关系的有限集。
逻辑结构是指数据元素之间的逻辑关系,与数据的存储无关,独立于计算机。
存储结构(物理结构)是指数据结构在计算机中的表示,它包括数据元素的表示和关系的表示。
2.算法
算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作。此外,一个算法还具有下列5个重要特性:
- 有穷性。一个算法必须总是在执行有穷步后结束,且每一步都是在有穷时间内完成。
- 确定性。算法中每条指令必须有确切的含义,且相同的输入只能得到相同的输出。
- 可行性。算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
- 输入。一个算法有零个或多个输入。
- 输出。一个算法有一个或多个输出。
通常设计一个“好”的算法应考虑达到以下目标:
- 正确性。算法应能够正确地求解问题。
- 可读性。算法能具有良好的可读性,以帮助人们理解。
- 健壮性。输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名其妙的输出结果。
- 效率与低存储量需求。效率是指算法执行的时间,存储量需求是指算法执行过程中所需的最大存储空间。
- 算法效率的度量是通过时间复杂度和空间复杂度来描述的。
- 一个语句的频度是指该语句在算法中被重复执行的次数。
- 算法中所有语句的频度之和记为T(n),它是该算法问题规模n的函数,时间复杂度主要分析T(n)的数量级。
二、顺序表
1.线性表的类型定义
- 线性表是具有相同数据类型的n(n≥0)个数据元素的有限序列。
- 用L命名线性表,则其一般表示为
L=(a1, a2, …, ai, ai+1, …, an)
- 除第一个元素外,每个元素有且仅有一个直接前驱。
- 除最后一个元素外,每个元素有且仅有一个直接后继。
线性表的基本操作:
- InitList(&L):初始化表。构造一个空的线性表。
- Length(L):求表长。返回线性表L的长度,即L中数据元素的个数。
- LocateElem(L,e):按值查找操作。获取表L中查找具有给定关键字值的元素。
- GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。
- ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。
- ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
- PrintList(L):输出操作。按前后顺序输出线性表L的元素值。
- Empty(L):判空操作。
- DestroyList(&L):销毁操作
2.顺序表的结构
线性表的顺序存储称为顺序表,它是由一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。
假定线性表的元素类型为ElemType,则线性表的顺序存储类型描述为
#define MaxSize 50 //定义线性表的最大长度 typedef struct{ ElemType data[MaxSize]; //顺序表的元素 int length; //顺序表的当前长度 }SqList; //顺序表的类型定义
上述一维数组是属于静态分配,由于数组的大小和空间事先已经固定,一旦空间占满,再加入新的数据将会产生溢出,进而导致程序崩溃。
而在动态分配时,存储数组的空间是在程序执行过程中通过动态存储分配语句分配的,一旦数据空间占满,就另外开辟一块更大的存储空间,用以替换原来的存储空间,从而达到扩充存储数据空间的目的,而不需要为线性表一次性地划分所有空间。
#define InitSize 50 //表长度的初始定义 typedef struct{ ElemType *data; //指示动态分配数组的指针 int MaxSize,length; //数组的最大容量和当前个数 }SeqList; //动态分配数组顺序表的类型定义
C语言的初始动态分配语句为
L.data=(ElemType*)malloc(sizeof(ElemType)*InitSize;
顺序表的特点:
- 顺序表最主要的特点是随机存取,即通过首地址和元素序号可在时间O(1)内找到指定的元素。
- 顺序表的存储密度高,每个结点只存储数据元素。
- 顺序表逻辑上相邻的元素物理上也相邻,所以插入和删除操作需要移动大量元素。
3.顺序表的实现
(1)插入操作
在顺序表L的第i(1≤i≤L.length+1)个位置插入新元素e。若i的输入不合法,则返回false,表示插入失败;否则,将顺序表的第i个元素及其后的所有元素右移一个位置,腾出一个空位置插入新元素e,顺序表长度增加1,插入成功,返回true。
bool ListInsert(SqList &L,int i,ElemType e){
if(i<1||i>L.length+1) //判断i的范围是否有效
return false;
for(int j=L.length;j>=i;j--) //将第i个元素及之后的元素后移
L.data[j]=L.data[j-1];
L.data[i-1]=e; //在位置i处放入e
L.length++; //线性表长度增加1
return true;
}
(2)删除操作
删除线性表L中第i(1≤i≤L.length)个位置的元素,若成功则返回true,并将被删除的元素用引用变量e返回,否则返回false。
bool ListDelete(SqList &L,int i,ElemType &e){
if(i<1||i>L.length) //判断i的范围是否有效
return false;
e=L.data[i-1]; //将被删除的元素赋给e
for(int j=i;i<L.length;j++) //将第i个位置后的元素后移
L.data[j-1]=L.data[j];
L.length--; //线性表长度减1
return true;
}
(3)按值查找操作
在顺序表L中查找第一个元素值等于e的元素,并返回其位序。
int LocateElem(SqList L,ElemType e){
int i;
for(i=0;i<L.length;i++)
if(L.data[i]==e)
return i+1; //下标为i的元素值等于e,返回其位序
return 0; //退出循环,说明查找失败
}
(4)按位查找操作
int GetElem(SqList L,int i){
if(i<1||i>L.length) //判断i的范围是否有效
return 0;
return L.data[i-1];
}
三、链表
1.单链表的结构
线性表的链式存储,又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。
为了建立数据元素之间的线性关系,对每个链表结点,除存放元素自身的信息外,还需要存放一个指向其后继的指针。
其中data为数据域,存放数据元素;next为指针域,存放其后继结点的地址。
//单链表结点类型的描述如下: typedef struct LNode{ //定义单链表结点类型 ElemType data; //数据域 struct LNode *next; //指针域 }LNode,*LinkList;
通常用头指针来标识一个单链表,如单链表L,头指针为NU/LL时表示一个空表。此外,为了操作上的方便,在单链表第一个结点之前附加一个结点,称为头结点。头结点的数据域可以不设任何信息。头结点的指针域指向线性表的第一个元素结点。
头结点和头指针的区分:不管带不带头结点,头指针始终指向链表的第一个结点,而头结点是带头结点的链表中第一个结点,结点内通常不存储信息。
引入头结点后,可以带来两个优点:
- 由于第一个数据结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作和在表的其他位置上的操作一致,无需进行特殊处理。
- 无论链表是否为空,其头指针都指向头结点的非空指针(空表中头结点的指针域为空)。
2.单链表的实现
(1)按序号查找结点值
在单链表中从第一个结点出发,顺指针next域逐个往下搜索,直到找到第i个结点为止,否则返回最后一个指针域NULL。
//按序号查找结点值的算法如下:
LNode *GetElem(LinkList L,int i){
int j=1; //计数,初始为1
LNode *p=L->next; //头结点指针赋给p
if(i==0)
return L; //若i等于0,则返回头结点
if(i<0)
return NULL; //若i无效,则返回NULL
while(p&&j<i){ //从第1个结点开始找,查找第i个结点
p=p->next;
j++;
}
return p; //返回第i个结点的指针,若i大于表长则返回NULL
}
按序号查找操作的时间复杂度为O(n)。
(2)按值查找表结点
从单链表的第一个结点开始,由前往后一次比较表中各结点数据域的值,若某结点数据域的值等于给定值e,则返回该结点的指针,若整个单链表中没有这样的结点,则返回NULL。
//按值查找表结点的算法如下
LNode *LocateElem(LinkList L,ElemType e){
LNode *p=L->next;
while(p!=NULL&&p->data!=e) //从第一个结点开始查找data域为e的结点
p=p->next;
return p; //找到后返回该结点指针,否则返回NULL
}
按值查找操作的时间复杂度为O(n)。
(3)插入结点操作
插入结点操作将值为x的新结点插入到单链表的第i个位置上。先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第i一1个结点,再在其后插入新结点。
算法首先调用按序号查找算法GetElem(L,i一1),查找i一1个结点。假设返回的第i-1个结点为*p,然后令新结点*s的指针域指向*p的后继结点,再令结点*p的指针域指向新插入的结点*s。
//实现插入结点的代码如下 p = GetElem (L,i –1); //查找插入位置的前驱结点 s–> next = p–>next; p->next=s;
注意:第二行与第三行代码的顺序不能颠倒!
前插法:
前插操作是指在某结点的前面插入一个新结点。
我们仍然*s插入到*p的后面,然后将p一>data和s一>data交换,这样实现了将新结点*s插入到了结点*p的前面。
//算法的代码片段如下 s–> next =p-> next; p->next =s; temp = p – > data; p–> data = s –> data; s – > data = temp;
(4)删除结点操作
删除结点操作是将单链表的第i个结点删除。先检查删除位置的合法性,后查找表中第i-1个结点,即被删除结点的前驱结点,再将其删除。
//实现删除结点的代码片段如下
p = GetElem (L,i – 1); //查找删除位置的前驱结点
q=p->next; //令q指向被删除的结点
p–>next =q->next; //将*q结点从链中断开
free(q); //释放结点的内存空间
3.双向链表、循环链表
(1)双向链表
双链表结点中有两个指针prior和next,分别指向其前驱结点和后继结点。
//双向链表中结点类型描述如下 typedef struct DNode{ //定义双链表结点类型 ElemType data; //数据域 struct DNode *prior,*next; //前驱和后继指针 }DNode,*DLinkList;
①双向链表的插入操作
在双向链表中p所指的结点之后插入结点*s。
//插入操作的代码片段如下 s->next=p->next; p – > next – > prior = s; s–> prior=p; p->next = s;
其中,第一、二行代码必须在第四行代码之前。
②双向链表的删除操作
删除双向链表中结点*p的后继节点*q。
//删除操作的代码片段如下 p–>next =q–>next; q–> next –> prior =p; free (q);
(2)循环链表
①循环单链表
在循环单链表中,表为结点*,的next域指向L,故表中没有指针域为NULL的结点,因此,循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针。
②循环双链表
在循环双链表L中,某结点*p为尾结点时,p->next=L;
当循环双链表为空表时,其头结点的prior域和next域都等于L。
四、栈和队列
1.栈的基本概念
栈(Stack)是只允许在一端进行插入或删除操作的线性表。
限定这种线性表只能在某一端进行插入和删除操作。
栈顶:线性表允许进行插入删除的那一端。
栈底:不允许进行插入和删除的另一端。
空栈:不含任何元素的空表。
栈操作的特性是先进后出(First In LAST Out,FILO)。
栈的基本操作:
- InitStack(&S):初始化一个空栈S。
- StackEmpty(S):判断一个栈是否为空,若栈S为空则返回true,否则返回false。
- Push(&S,x):进栈,若栈S未满,则将x加入使之成为新栈顶。
- Pop(&S,&x):出栈,若栈S非空,则弹出栈顶元素,并用x返回。
- GetTop(S,&x):读栈顶元素,若栈S非空,则用x返回栈顶元素。
- DestroyStack(&S):销毁栈,并释放栈S占用的存储空间("&”表示引用调用)。
2.栈的存储结构
栈是一种操作受限的线性表,类似于线性表,它也有对应的两种存储方式。
(1)顺序栈的实现
采用顺序存储的栈称为顺序栈,它利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针(top)指向当前栈顶元素的位置。
//栈的顺序存储类型可描述为
#define MaxSize 50 //定义栈中元素的最大个数
typedef struct{
ElemType data [MaxSize]; //存放栈中元素
int top; //栈顶指针
}SqStack;
栈顶指针:S.top,初始时设置S.top=-1;栈顶元素:S.data[S.top]。
进栈操作:栈不满时,栈顶指针先加1,再送值到栈顶元素。
出栈操作:栈非空时,先取栈顶元素值,再将栈顶指针减1。
栈空条件:S.top==-1;栈满条件:S.top==MaxSize-1;栈长:S.top+1。
(2)顺序栈的基本运算
①初始化操作
//①初始化 void InitStack(SqStack &S){ S.top=-1; //初始化栈顶指针 }
②判断栈是否为空
//判栈空 bool StackEmpty(SqStack S){ if(S.top=-1) //栈空 return true; else return false; //栈非空 }
③入栈
//入栈 bool Push(SqStack &S,ElemType x){ if(S.top==MaxSize-1) //栈满,报错 return false; S. data[++S.top]=x; //指针先加1,再入栈 return true; }
④出栈
//出栈 bool Pop(SqStack &S,ElemType &x){ if(S.top=-1) //栈空,报错 return false; x=S.data[S.top-]; //先出栈,指针再减1 return true; }
⑤读取栈顶元素
//读取栈顶元素 bool GetTop(SqStack S,ElemType &x){ if(S.top==-1) //栈空,报错 return false; x=S.data[S. top]; //x记录栈顶元素 return true; }
(3)共享栈
利用栈底位置相对不变的特性,可让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。
两个栈的栈顶指针都指向栈顶元素,top0=-1时0号栈为空,topl=MaxSize时1号栈为空;仅当两个栈顶指针相邻(top1-top0=1)时,判断为栈满.当0号栈进栈时top0先加1再赋值,1号栈进栈时1op1先减1再赋值;出栈时则刚好相反。
共享栈是为了更有效的利用存储空间,两个栈的空间相互调节,只有在整个存储空间被占满时才发生上溢。
(4)栈的链式结构
采用链式存储的栈称为链栈,链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。通常采用单链表实现,并规定所有操作都是在单链表的表头进行的.这里规定链栈没有头结点,Lhead指向栈顶元素。
//栈的链式存储类型可描述为 typedef struct LinkNode{ ElemType data; //数据域 struct LinkNode *next; //指针域 }*LiStack; //栈类型定义
3.队列的基本概念
队列(Oueue)简称队,也是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一端进行删除。
向队列中插入元素称为入队或进队;删除元素称为出队或离队。
队列操作的特性是先进先出(First In First Out,FIFO)。
队头:允许删除的一端,又称队首。
队尾:允许插入的一端。
队列的基本操作:
- InitQueue(&Q):初始化队列,构造一个空队列Q。
- QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false。
- EnQueue(&O,x):入队,若队列Q未满,将x加入,使之成为新的队尾。
- DeQueue(&Q,&x):出队,若队列Q非空,删除队头元素,并用x返回。
- GetHead(Q,&x):读队头元素,若队列Q非空,则将队头元素赋值给x。
需要注意的是,栈和队列是操作受限的线性表,因此不是任何对线性表的操作都可以作为栈和队列的操作。比如,不可以随便读取栈或队列中间的某个数据。
4.队列的存储结构
(1)队列的顺序存储
队列的顺序实现是指分配一块连续的存储单元存放队列中的元素,并附设两个指针;队头指针front指向队头元素,队尾指针rear指向队尾元素的下一个位置。
//队列的顺序存储类型可描述为 #define MaxSize 50 //定义队列中元素的最大个数 typedef struct{ ElemType data [MaxSize]; //存放队列元素 int front,rear; //队头指针和队尾指针 } SqQueue;
初始状态(队空条件):Q.front==Q.rear==0
进队操作:队不满时,先送值到队尾元素,再将队尾指针加1
出队操作:队不空时,先取队头元素值,再将队头指针加1
不能用O.rear==MaxSize作为队列满的条件。图(d)中,队列中仅有一个元素,但仍满足该条件。这时入队出现“上溢出”,但这种溢出并不是真正的溢出,在data数组中依然存在可以存放元素的空位置,所以是一种假溢出。
(2)循环队列
- 把存储队列元素的表从逻辑上视为一个环,称为循环队列。
- 当队首指针Q.front=MaxSize-1后再前进一个位置就自动到0,这可以利用除法取余运算(%)来实现。
- 初始时:Q.front=Q.rear=0.
- 队首指针进1:Q.front=(Q.front+1)%Max:Size
- 队尾指针进1:Q.rear=(Q.rear+1)%MaxSize
- 队列长度:(Q.rear+Max:Size-Q.front)%MaxSize
- 出队入队时:指针都按顺时针方向进1。
牺牲一个单元来区分队空和队满,入队时少用一个队列单元。
- 队满条件:(Q.rear+1)%Max:Size==Q.front
- 队空条件:Q.front==Q.rear队列中元素的个数:(Q.rear-Q.front+MaxSize)%MaxSize
(3)队列的链式存储结构
队列的链式表示称为链队列,它实际上是一个同时带有队头指针和队尾指针的单链表。头指针指向队头结点,尾指针指向队尾结点,即单链表的最后一个结点。
五、串
1.串的基本概念
串是由零个或多个字符组成的有限序列。一般记为
S ='a1a2… an'
其中,S是串名,单引号括起来的字符序列是串的值;a可以是字母、数字或其他字符;串中字符的个数n称为串的长度。n=0时的串称为空串(∅)。
串中任意个连续的字符组成的子序列称为该串的子串,包含子串的串相应地称为主串。某个字符在串中的序号称为该字符在串中的位置。子串在主串中的位置以子串的第一个字符在主串中的位置来表示。当两个串的长度相等且每个对应位置的字符都相等时,称这两个串 是相等的。
由一个或多个空格组成的串称为空格串,其长度为串中空格字符的个数。
例如,A='China Beijing',B='Beijing',C='China',它们的长度分别是13,7,5。B,C都是A的子串,B在A中的位置是7,C在A的位置是1。
串的基本操作:
- StrCopy(&T,S):复制操作。由串S复制得到T。
- StrEmpty(S):判空操作。StrCompare(T,S):比较操作。
- StrLength(S):求串长。
- SubString(&Sub,S,pos,len):求子串。用Sub返回S的第pos个字符起长度为len的子串。
- Concat(&T,S1,S2):串联接。用T返回由S1和S2联接而成的新串。
- Index(S,T):定位操作。若主串S中存在与串T值相同的子串,则返回它在主串S中第一次出现的位置;否则返回函数值为0。
2.串的简单模式匹配模式
子串的定位操作通常称为串的模式匹配,它求的是子串在主串中的位置。
3.矩阵的压缩存储
压缩存储:指为多个值相同的元素只分配一个存储空间,对零元素不分配存储空间。其目的是为了节省存储空间。
特殊矩阵:指具有许多相同矩阵元素或零元素,并且这些相同矩阵元素或零元素的分布有一定规律性的矩阵。常见的特殊矩阵有对称矩阵、上(下)三角矩阵、稀疏矩阵等。
(1)对阵矩阵
若对一个n阶方阵A[1…n][1…n]中的任意一个元素au=a(1≤i.j≤n),则其称为对称矩阵。
将对称矩阵A[1…n][1…n]存放在一维数组B[n(n+1)/2]中,B数组下标从0开始。
因此,元素au在数组B中的下标k=1+2+…+(i-1)+j-1=i(i-1)/2+j-1。元素下标之间的对应关系如下:
(2)三角矩阵
上三角矩阵,下三角区的所有元素均为同一常量。其存储思想与对称矩阵类似,不同之处在于存储完上三角区和主对角线上的元素之后,还要存储对角线下方的元素一次,因此将这个上三角区矩阵A[1…n][1…n]压缩存储在B[n(n+1)/2+1]中。
因此,元素a在数组中的下标k=n+(n-1)+…+(n-i+2)+(j-i+l)-l=(i-1)(2n-i+2)/2+(j-i)。
元素下标的对应关系如下:
因此,元素ay在数组中的下标k=1+2+3+…+(i-1)+j-1=i(i-1)/2+j-1
元素下标的对应关系如下:
(3)稀疏矩阵
矩阵中非零元素的个数t,相对于矩阵元素的个数s非常少,这样的矩阵称为稀疏矩阵。
将非零元素及其行和列构成一个三元组(行标、列标、值)。稀疏矩阵压缩存储后就失去了随机存取特性。
稀疏矩阵的三元组既可以采用数组存储,也可以采用十字链表法存储。
六、树和二叉树
1.树的基本概念
树是n(n≥0)个结点的有限集。当n=0时,称为空树。在任意一颗非空树上应满足:
- 有且仅有一个特定的根的结点
- 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1T2Tm,其中每个集合本身又是一棵树,并且称为根的子树
树作为一种逻辑结构,同时也是一种分层结构,具有以下两个特点:
- 树的根结点没有前驱,除根结点外的所有结点有且只有一个前驱
- 树中所有结点可以有零个或多个后继
基本术语:
- 根A到结点K的唯一路径上的任意结点,称为结点K的祖先。路径上最接近结点K的结点E称为K的双亲,而K为结点E的孩子
- 树中一个结点的孩子个数称为该结点的度,树中结点的最大度数称为树的度
- 度大于0的结点称为分支结点(又称非终端结点);度为0(没有孩子结点)的结点称为叶子结点(又称终端结点)。有相同双亲的结点称为兄弟
- 结点的层次从树根开始定义,根结点为第1层,它的子结点为第2层。结点的深度是从根结点开始自顶向下逐层累加。结点的高度是从叶结点开始自底向上逐层累加。树的高度(或深度)是树中结点的最大层数
- 有序树和无序树。树中结点的各子树从左到右是有次序的,不能互换,称该树为有序树,否则称为无序树。假设图为有序树,若将子结点位置互换,则变成一颗不同的树
- 路径和路径长度。树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的,而路径长度是路径上所经过的边的个数
树的性质:
- 树中的结点数等于所有结点的度数加1
- 度为m的树中第i层上至多有m-1个结点(i≥1)
- 高度为h的m叉树至多有(m”一1)/(m-1)个结点
- 具有n个结点的m叉树的最小高度为[logm(n(m-1)+1)]
2.二叉树
二叉树的定义:
二叉树的特点是每个结点至多只能有两棵子树(即二叉树中不存在度大于2的结点),并且二叉树的子树有左右之分,其次序 不能任意颊倒。二叉树也已递归的形式定义。
二叉树是n(n≥0)个结点的有限集合:
- 或者为空二叉树;
- 或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成,左右子树又分别是一棵二叉树。
几个特殊的二叉树:
- 满二叉树
一颗高度为h,且含有2”-1个结点的二叉树称为满二叉树,即树中的每层都含有最多的结点,如下图(a)所示。满二叉树的叶子结点都集中在二叉树的最下一层,并且除叶子结点之外的每个结点度数均为2。
对满二叉树按层序编号:约定编号从根结点(根结点编号为1)起,自上而下,自左向右。这样,每个结点对应一个编号,对于编号为i的结点,若有双亲,则其双亲为[i/2],若有左孩子,则左孩子为2i;若有右孩子,则右孩子为2i+1。
- 完全二叉树
高度为h、有n个结点的二叉树,当且仅当其每个结点都与高度为h的满叉树中编号为1~n的结点一一对应时,称为完全二叉树,如图(b)所示。
其特点如下:
①若i≤ln/2],则结点i为分支结点,否则为叶子结点。
②叶子结点只可能在层次最大的两层上出现。对于最大层次中的叶子结点,都依次排列在该层最左边的位置上。
③若有度为1的结点,则只可能有一个,且该结点只有左孩子而无右孩子。
④按层序编号后,一旦出现某结点(编号为i)为叶子结点或只有左孩子,则编号大于i的结点均为叶子结点。
⑤若n为奇数,则每个分支结点都有左孩子和右孩子;若n为偶数,则编号最大的分支结点(编号为n/2)只有左孩子,没有右孩子,其余分支结点左、右孩子都有。- 二叉排序树
左子树上所有结点的关键字均小于根结点的关键字;右子树上的所有结点的关键字均大于根结点的关键字;左子树和右子树又各是一颗二叉排序树。- 平衡二叉树
树上任一结点的左子树和右子树的深度之差绝对值不超过1。
二叉树的性质:
- 非空二叉树上的叶子结点数等于度为2的结点数加1,即no=n2+1。
- 非空二叉树上第k层上至多有2*-1个结点(k≥1)。
- 高度为h的二叉树至多有2”-1个结点(h≥1)。
- 对完全二叉树按从上到下、从左到右的顺序依次编号1,2,…,n,则有以下关系:
①当i>1时,结点i的双亲的编号为li/2],即当i为偶数时,其双亲的编号为i/2,它是双亲的左孩子;当i为奇数时,其双亲的编号为(i-1)/2,它是双亲的右孩子。
②当2i≤n时,结点i的左孩子编号为2i,否则无左孩子。
③当2i+1≤n时,结点i的右孩子编号为2i+1,否则无右孩子。- 具有n(n>0)个结点的完全二叉树的高度为[log2(n+1)1或[log2n]+1。
二叉树的存储结构:
- 顺序存储结构
指用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树的结点元素,即将完全二叉树上编号为i的结点元素存储在一维数组下标i-1的分量中。
- 链式存储结构
用链表结点来存储二叉树中的每个结点。在二叉树中,结点结构通常包括若干数据域和若干指针域,二叉链表至少包含3个域:数据域data,左指针域lchild和右指针域rchild。//二叉树的链式存储结构描述如下: typedef struct BiTNode{ ElemType data; //数据域 struct BiTNode *lchild,*rchild; //左、右孩子指针 }BiTNode,*BiTree;
3.二叉树的遍历
二叉树的遍历是指按某条搜索路径访问树中每个结点,使得每个结点均被访问一次,而且仅被访问一次。
由二叉树的递归定义可知,遍历一颗二叉树便要决定对根结点N,左子树L,右子树R的访问顺序。按照先遍历左子树再遍历右子树的原则,常见的遍历次序有先序(NLR)、中序(LNR)和后序(LRN)三种遍历算法,其中“序”指的是根结点在何时被访问。
(1)先序遍历
先序遍历(PreOrder)的操作过程如下:
若二叉树为空,则什么也不做;否则,
①访问根结点;
②先序遍历左子树;
③先序遍历右子树。
//对应的递归算法如下:
void PreOrder(BiTree T){
if(T!=NULL){
visit(T); //访问根结点
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchild); //递归遍历右子树
}
}
(2)中序遍历
中序遍历(InOrder)的操作过程如下:
若二叉树为空,则什么也不做;否则,
①中序遍历左子树;
②访问根结点;
③中序遍历右子树。
//对应的递归算法如下:
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild); //递归遍历左子树
visit(T); //访问根结点
InOrder(T->rchild); //递归遍历右子树
}
}
(3)后序遍历
后序遍历(PostOrder)的操作过程如下:
若二叉树为空,则什么也不做;否则,
①后序遍历左子树;
②后序遍历右子树;
③访问根结点。
//对应的递归算法如下:
void PostOrder(BiTree T){
if(T!=NULL){
PostOrder(T->lchild); //递归遍历左子树
PostOrder(T->rchild); //递归遍历右子树
visit(T); //访问根结点
}
}
(4)层次遍历
要进行层次遍历,需要借助一个队列。先将二叉树根结点入队,然后出队,访问出队结点,若它有左子树,则将左子树根结点入队;若它有右子树,则将右子树根结点入队。然后出队,访问出队结点……如此反复,直至队列为空。
//二叉树的层次遍历算法如下:
void Level0rder(BiTree T){
InitQueue (Q); //初始化辅助队列
BiTNode *p=T;
EnQueue (Q,p); //将根结点入队
while(!IsEmpty(Q)){ //队列不空则循环
DeQueue (Q,p); //队头元素出队
visit(p); //访问出队结点
if(p->1child!=NULL)
EnQueue(Q,p->lchild); //左子树不空,左子树根结点入队
if(p->rchild!=NULL)
EnQueue(Q,p->rchild); //右子树不空,右子树根结点入队
}
}
4.书和森林
树的存储结构:双亲表示法、孩子表示法、孩子兄弟表示法。
树转换成二叉树的画法:
- 在兄弟结点之间加一条线;
- 对每个结点,只保留它与第一个孩子的连线,抹去与其他孩子的连线;
- 以树根为轴心,顺时针旋转45°。
由树转换成二叉树,其根结点的右子树总是空的
森林转换成二叉树的画法:
- 将森林中的每棵树转换成相应的二叉树;
- 每棵树的根也可视为兄弟节点,在每棵树之间加一根连线;
- 以第一颗树的根为轴心顺时针旋转45°。
树的遍历是指用某种方式访问树中的每个结点,且仅访问一次。主要有两种方式:
- 先根遍历。若树非空,先访问根结点,再依次遍历根结点的每棵子树,遍历子树时仍遵循先根后子树的规则。其遍历次序与这棵树对应二叉树的先序序列相同。
- 后根遍历。若树非空,先依次遍历根结点的每棵子树,再访问根结点,遍历子树时仍遵循先子树后根的规则。其遍历次序与这棵树对应二叉树的中序序列相同。
森林的两种遍历方法:
- 先序遍历。若森林非空,先访问森林中第一棵子树的根结点,再先序遍历第一棵树中根结点的子树森林,再先序遍历除去第一棵树之后剩余的树构成的森林。
- 中序遍历。若森林非空,先中序遍历森林中第一棵树的根结点的子树森林,再访问第一棵树的根结点,再中序遍历除去第一棵树之后剩余的树构成的森林。
5.二叉排序树
二叉排序树(二叉查找树)或者是一颗空树,或者是具有下列特性的二叉树
- 若左子树非空,则左子树上所有结点的值均小于根结点的值;
- 若右子树非空,则右子树上所有结点的值均大于根结点的值;
- 左、右子树也分别是一棵二叉排序树。
- 根据二叉排序树的定义,左子树结点值<根结点值<右子树结点值,所以对二叉排序树进行中序遍历,可以得到一个递增的有序序列。
- 若对一颗二叉排序树构成的序列采用中序遍历,可得到升序结果。
二叉排序树的构造:从一棵空树出发,依次输入元素,将它们插入二叉排序树的合适位置。
二叉树的插入:
若原二叉排序树为空,则直接插入结点;否则,若关键字k小于根结点值,则插入到左子树,若关键字k大于根结点值,则插入到右子树。
向二叉排序树插入一个新结点时,新结点一定会成为二叉排序树的一个叶子结点。
二叉树的删除:
- 若被删除的结点是叶子结点,则直接删除;
- 若结点只有一棵左子树或者右子树,则让该结点的子树成为其父结点的子树;
- 若结点有左、右两棵子树,则令该结点的直接后继(直接前驱)代替该结点,然后从二叉排序树中删去这个直接后继(直接前驱),这样就转换成了前两种情况。
6.哈夫曼树
- 树中结点常被赋予一个代表某种意义的数值,那个数值称为该结点的权。
- 从树的根到任意结点的路径长度(经过的边数)与该结点上权值的乘积,称为该结点的带权路径长度。
- 树中所有叶结点的带权路径长度之和称为该树的带权路径长度,
记为WPL=- 带权路径长度最小的二叉树称为哈夫曼树,也称最优二叉树。
给定n个权值分别为w1,w2,…,w„的结点,构造哈夫曼树的算法描述如下:
- 将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
- 构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
- 从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
- 重复步骤2和步骤3,直至F中只剩下一棵树为止。
未完待续。。。