接下来会陆续更新有关数据结构的文章。仅记录自己学习的心得和总结。供有兴趣的同学一起学习!
(一)基本概念
- 什么是数据? 数据是信息的载体;数据元素是数据的基本单位,而一个数据元素可以由若干个数据项组成。
-
什么是数据结构? 数据结构指的是数据之间的相互关系,即数据的组织形式。
-
分类
- 数据的逻辑结构
- 数据的存储结构
- 数据的运算
-
-
数据的逻辑结构 数据元素之间的逻辑关系,也称数据的逻辑结构,数据的逻辑结构是从逻辑关系上描述数据, 与数据的存储无关,是独立于计算机的。
-
分类:
-
线性结构
若结构是非空集,则有且仅有一个开始节点和一个终端节点,并且所有结点都最多只有一个直接前趋和一个直接后趋。 线性表是一个典型的线性结构。栈、队列、串等都是线性结构。 -
非线性结构
一个几点可能有多个直接前趋和直接后趋。 数组、广义表、树和图等数据结构都是非线性结构。
-
-
数据的存储结构 数据元素及其关系在计算机存储器内的表示,称为数据的存储结构。
- 分类:
-
顺序存储方法:
该方法把逻辑上相邻的结点存储在物理位置上相邻的存储单元里,结点见的逻辑关系有存储单元的邻接关系来提现。通常借助程序语言的数组描述。 -
链接存储方法:
该方法不要求逻辑上相邻的结点在物理位置上依相邻,结点见得逻辑关系由附加的指针字段表示。通常借助于程序语言的指针类型描述。 -
索引存储方法:
该方法通常在储存结点信息的同时,还建立附加的索引表。 索引表有若干索引项组成。 若每个结点在索引表中都有一个索引项,则该索引表称之为稠密索引,稠密索引中索引项的地址指示结点所在的存储位置。 若每个结点在索引表中只对应一个索引项,则该索引表称之为稀疏索引,索引中索引项的地址指示一组结点的起始存储位置。 索引项的一般格式是:关键字+地址。 关键字是能唯一标识一个结点的那些数据项。 -
散列存储方法:
该方法的基本思想是:根据结点的关键字直接计算出该结点的存储地址。
-
- 分类:
-
数据的运算 数据的运算,即对数据施加的操作。最常见的有:检索、删除、更新和排序等。
算法+数据结构=程序
数据结构:是指数据的逻辑结构和存储结构;
算法:是对数据运算的描述; -
-
如何选用正确的算法?
- 执行算法所耗费的时间;
- 执行算法所耗费的存储空间,其中主要考虑辅助存储空间;
- 算法应易于理解,易于编码,易于调试等等;
一个算法所耗费的时间=算法中每条语句的执行时间之和
每条语句的执行时间=语句的执行次数(频度)*语句执行一次所需时间
-
算法的时间复杂度 T(n) 在计算机科学中,算法的时间复杂度是一个函数,它定性描述了该算法的运行时间。
-
分类:
-
时间复杂度
指执行算法所需要的计算工作量 -
空间复杂度
指执行这个算法所需要的内存空间
-
-
计算时间复杂度
一般看看有几重for循环,只有一重则时间复杂度为O(n),二重则为O(n^2),依此类推, 如果有二分则为O(logn),二分例如快速幂、二分查找,如果一个for循环套一个二分, 那么时间复杂度则为O(nlogn)。
例子1
public class example01 { public static void main(String[] args) { System.out.println("a"); System.out.println("b"); } } // T(n)=O(1)例子2
public class example02 { public static void main(String[] args) { int sum = 0; for(int i=1;i<=100;i++) { sum = sum + i; } } } // T(n)=O(n)例子3
public class example03 { public static void main(String[] args) { int sum = 0; for(int i=1;i<=100;i++) { for(int j=1;j<=100;j++) sum = sum + i; } } } // T(n)=O(n^2) -
(二)-线性表
-
线性表
-
定义
是由n(n≥0)个数据元素(结点)a1,a2....an组成的有限序列。其中,数据元素的个数n为表的长度。当n为零时称为空表。 -
逻辑结构特征
- 有且仅有一个称为开始元素的a1 ,没有直接前趋,有且仅有一个直接后继 a2;
- 有且仅有一个终结元素的an,没有直接后继,有且仅有一个直接前趋an-1;
- 其余的元素结点ai(2≤i≤n-1)都有且仅有一个直接前趋ai-1和一个直接后继ai+1;
-
基本运算
- InitList(L):构造一个空的线性表L,即表的初始化,置空表
- ListLength(L):求线性表L中的结点个数,即求表长
- GetNode(L,i):取线性表L中的第i个结点,这里要求1≤i≤ListLength(L)
- LocateNode(L,x):在 L 中查找值为 x 的结点,并返回该结点在 L 中的位置。若 L 中有多个结点的值和 x 相同,则返回首次找到的结点位置;若 L 中没有结点的值为 x ,则返回0值。
- InsertList(L,x,i):在线性表 L 的第 i 个位置上插入一个值为 x 的新结点,插入后,表 L 的长度加 1。
- DeleteList(L,i):删除线性表 L 的第 i 个结点,删除后表 L 的长度减 1。
-
例子:
【例子】:假设有两个线性表LA和LB分别表示两个几个A和B,现要求一个新集合A=A∪B。 思想:扩大线性表LA,将LB中不在LA中出现的元素插入到LA中。只要从线性表LB中依次取出每个元素,按值在线性表LA中查找,若没查到则插入之。void union(Linear_List LA,Linear_List LB){ n=ListLength(LA); //求LA的表长 for(i=1;i<=ListLength(LB);i++){ x=GetNode(LB,i); //取LB中第i个元素赋给x if(LocateNode(LA,x)==0) //判断LA中是否有x InsertList(LA,++n,x); } }
-
-
顺序表(Sequential List)
-
定义
线性表的顺序存储指的是将线性表的数据元素按其逻辑次序依次存入一组地址连续的存储单元里,用这种方式存储的线性表称为顺序表。# define ListSize 100 //表空间的大小应根据实际需要来定义,这里假设为100 typedef int DataType; //DataType的类型可根据实际情况而定,这里假设为int typedef struct{ DataType data[ListSize]; //数组data用来存放表结点 int length; //线性表的当前表长(实际存储元素的个数) }SeqList -
存储位置
LOC(ai)=LOC(a1)+(i-1)*d; d:每个元素需占用d个存储单元 LOC(a1):表示为线性表的第一个元素a1的存储位置,通常称为基地址。 -
基本运算
-
插入
线性表的插入元素是指在线性表的第i-1个元素和第i个元素之间插入一个新元素x,使长度为n的线性表。
void InsertList(SeqList *L,iny i,DataType x){ int j; if(i<1 || i>L->Length+1){ printf("position error"); return; } if(L->length>=ListSize){ printf("overflow"); return; } for(j=l->Length-1;j>=i-1;j--){ L->data[j+1]=L->data[j]; //从最后一个元素开始逐一后移 L->data[i-1]=x; //插入新元素x L->length++; //实际表长加1 } }-
删除
线性表的删除运算指的是将表中第i(1≤i≤n)个元素删除,使长度为n的线性表变为长度为n-1的线性表。
DataType DeleteList(SeqList *L,int i){ int jl DataType x; if(i<1 || i>L->Length){ printf("position error"); return; } x=L->data[i]; for (j=i;j<=L->Length;j++){ L->data[j-1]=L->data[j]; //元素前移 L->length--; //实际表长减1 return x; //返回被删除的元素 } } -
-
-
链表(Linked List)
-
定义
链式存储结构存储线性不爱数据元素的存储空间可能是连续的,也可能是不连续的,因而链表的结点是不可能随机存储的。 -
单链表(线性链表)
在使用链式存储结构表示每个数据元素ai时,除了存储ai本身的信息之外,还需要一个存储指示其后继元素ai+1存储位置的指针,有这两部分组成元素ai的存储映像通常称为结点。由于这种链式表的每个结点中只包含一个指针域,因此称为单链表。
-
基本运算
-
建立单链表
- 头插法
头插法表示从一个空表开始,重复读数据,生成新结点,将读入的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头上,直到读入结束标志为止。
LinkList CreateList(){ LinkList head; ListNode *p; char ch; head=null; //置空单链表 ch=getchar(); //读入第一个字符 while(ch!='\n'){ //读入字符不是结束标志时作循环 p=(ListNode *)malloc(sizeof(ListNode)); //申请新节点 p->data=ch; //数据域赋值 p->next=head; //指针域赋值 head=p; //头指针执行新结点 ch=getchar(); //读入下一个字符 } return head; //返回链表的头指针 }-
尾插法(不带头结点)
将新结点插入在单链表的表尾上,需增添一个尾指针rear,使其始终指向链表的尾结点。
LinkList CreateList(){ LinkList head,rear; ListNode *p; char ch; head=NULL;rear=NULL; //置空表 ch=getchar(); //读入第一个字符 while(ch!='\n'){ //读入字符2不是结束标志符时作循环 p=(ListNode *)malloc(sizeof(ListNode)); //申请新结点 p->data=ch; //数据域赋值 if(head=NULL) head=p; //新结点 *p插入空表 else rear->nexr=p; //新结点*p插入到非空表的表为结点*rear之后 rear=p; //表尾指针指向新的表尾结点 ch=getchar();//读入下一个字符 } if(rear!=NULL) rear-next=NULL; //终端结点指针域置空 return head; }-
尾插法(带头结点)
为了简化算法,方便操作,可在链表的开始结点之前附加一个结点,并称为头结点。
LinkList CreateList(){ LinkList head=(ListNode *)malloc(sizeof(ListNode)); //申请头结点 ListNode *p,*r; DataType ch; r=head; while((ch=getchar())!='\n'){ p=(ListNode *)malloc(sizeof(ListNode)); p->data=ch; //新结点连接到尾结点之后 r->next=p; //尾指针指向新结点 r=p; //终端结点指针域置空 } r->next=NULL; return head; } - 头插法
头插法表示从一个空表开始,重复读数据,生成新结点,将读入的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头上,直到读入结束标志为止。
-
查找运算(带头结点)
- 按结点序号查找
ListNode * GetNode(LinkList head,int i){ //head为带头结点的单链表多的头指针,i为要查找的结点序号 //若查找成功,则返回查找结点的存储地址,否则返回NULL ListnNode *p;int j; p=head->next; //使p指向第一个结点 j=1; while(p!=NULL&&j<i){ //顺时针向后查找,直到p指向第i个结点或p为空为止 p=p->next; ++j; } if(j==i){ return p; }else { return NULL; } }- 按结点值查找
ListNode * LocateNode(LinkList head,DataType k){ ListNode *p=head->next; while(p&&p->data!=k){ //循环直到p等于NULL或p->data等于k为止 p=p->next; } return p; } -
插入运算(带头结点)
先使p指向ai-1的位置,然后生成一个数据域值为x的新结点*s,再进行插入操作。
void InsertList(LinkList head,int i,DataType x){ ListNode *p,*s; int j; p=head; j=0; while(p!=NULL&&j<i-1){ p=p->next; ++j; } if(p==NULL){ printf("ERROR\n"); return; }else{ s=(ListNode *)malloc(sizeof(ListNode)); //申请新结点 s->data=x; s->next=p->next; p->next=s; } }-
删除运算(带头结点)
由于第i个结点的存储地址是存储在第i-1个结点的指针域next中,因此要先使p指向1第i-1个结点,然后使得p->next指向第i+1个结点,再将第i个结点释放掉。
DataType DeleteList (LinkList head,int i){ ListNode *p,*s; DataType x; int j; p=head; j=0; while(p!=NULL&&j<i-1){ //使p指向第i-1个结点 p=p->next; ++j; } if(p==NULL){ printf("位置错误\n"); exit(0); }else{ s=p->next; //s指向第i个结点 p->next=s->next; //使p->next指向第i+1个结点 x=s->data;//保存被删除结点的值 free(s); //删除第i个结点 return x; } } -
-
-
循环链表(线性链表)
单链表中最后一个结点(终端结点)的指针域不为空,而是指向链表的头结点,使整个链表构成一个环。 -
双向链表(线性链表) 若希望从表中快速确定一个结点的直接前趋,只要在单链表的结点类型中增加一个指向其直接前趋的指针域prior即可。这样形成的链表中有两条不同方向的链,因此称为双向链表。
-
比较
(三)-栈
- 栈
-
定义
是限制仅在表的一端进行插入和删除运算的线性表。 -
属性
-
通常称插入、删除的这一端为 栈顶(Top),另一端称为 栈底(Bottom)。
-
当表中没有元素时称为 空栈
-
栈为 后进先出(Last In First Out)的线性表,简称为 LIFO 表。
-
-
顺序栈-基本运算
- 置空栈
void InitStack (SeqStack *s){ s-top=-1; // 置空顺序栈,由于c语言数组下标是从0开始,所以栈中元素亦从0开始 // 存储,因此空栈时栈顶指针不能是0,而只能是-1 }- 判栈空
int StackEmpty (SeqStack *S){ return S->top===-1; }- 判栈满
int StackFull(SeqStack *S){ return S->top==StackSize-1; }- 进栈
void Push(SeqStack *S,DataType x){ if(StackFull(s)){ printf("stack overflow"); }else{ S->top=S->top+1; S->data[S->top]=x; } }- 退栈
DataType Pop(SeqStack *S){ if(StackEmpty(S)){ printf("stack underflow"); exit(0); }else{ return S->data[S->top-1] } }- 取栈顶元素
DataType GetTop(SeqStack *S){ if(StackEmpty(S)){ printf("stack empty"); exit(0); }else{ return S->data[S->data]; } } -
链栈-基本运算
- 判栈空
int StackEmpty(LinkStack top){ return top==null; }- 进栈
LinkStack Push(LinkStack top,DataType x){ StacKNode *p; p=(StackNode *)malloc(sizeof (StacKNode)); p->data=x; p-next=top; top=p; return top; }- 退栈
LinkStack Pop(LinkStack top, DataType *x){ SrackNode *p=top; if(StackEmpty(top)){ printf("stack empty"); exit(0); }else{ *x=p->data; top=p->next; free(p); return top; } }- 取栈顶元素
DataType GetTop(LinkStack top){ if(StackEmpty(top)){ printf("stack empty"); exit(0); }else{ return top->data; } }
-
(四)-队列
- 队列
-
定义
是只允许在一端进行插入,而在另一端进行删除的运算受限的线性表。 -
属性
- 允许删除的一端称为 队头(Front),允许插入的一端称为 队尾(Rear)
- 当队列中没有元素时称为空队列
- 队列称作先进先出(First In First Out)的线性表,简称为FIFO
-
顺序队列-基本运算
- 置空队列
void InitQueue(CirQueue *Q){ Q->front=Q->rear=0; }- 判队空
int QueueEmpty(CirQueue *Q){ return Q->rear==Q->front; }- 判队满
int QueueFull (CirQueue *Q){ return (Q->rear+1) % QueueSize == Q->front; }- 入队列
void EnQueue(CirQueue *Q , DataType x){ if(QueueFull(Q)){ //出错退出处理 printf("Queue overflow"); }else{ Q->data[Q->rear]=x; Q->rear=(Q->rear+1) % QueueSize; //循环意义下的加1 } }- 取队头元素
DataType GetFront(CirQueue *Q){ if(QueueEmpty(Q)){ //出错退出处理 printf("Queue empty"); exit(0); }else{ return Q->data[Q->front]; //返回队头元素值 } }- 出队列
DataType DeQueue(CirQueue *Q){ DataType x; if(QueueEmpty(Q)){ //出错退出处理 printf("Queue empty"); exit(0); }else{ x=Q->data[Q->front]; //保存待删除元素值 Q->front=(Q->front+1) % QueueSize; //头指针加一 return x; //返回删除元素值 } } -
链队列-基本运算
- 构造空队列
void InitQueue (LinkQueue *Q){ Q->front=(QueueNode *)malloc(sizeof(QueueNode)); Q->rear=Q->front; // 尾指针也指向头结点 Q->rear->next=NULL; }- 判队空
int QueueEmpty(LinkQueue *Q){ return Q->rear==Q->front; //头尾指针相等队列为空 }- 入队列
void EnQueue(LinkQueue *Q , DataType x){ QueueNode *p->(QueueNode *)malloc(sizeof(QueueNode)); p->data=x; p->next=NULL; Q->rear->next=p; //*p 链到原队尾结点之后 Q->rear=p; //队尾指针指向新的队尾结点 }- 取队头元素
DataType GetFront(LinkQueue *Q){ if(QueueEmpty(Q)){ //出错退出处理 printf("Queue underflow"); exit(0); }else{ return Q->front->next->data; //返回原队头元素值 } }- 出队列
DataType DeQueue(LinkQueue *Q){ QueueNode *p; if(QueueEmpty(Q)){ //出错退出处理 printf("Queue underflow"); exit(0); }else{ p->Q->front; //p指向头结点 Q->front=Q->front->next; //头指针指向原队头结点 free(p);//删除释放原头结点 return Q->front->data; //返回原队头结点的数据值 } }
-
(五)-多维数组
- 多维数组
-
数组的顺序存储
-
按行优先顺序:将数组元素按行向量排列,第 i+1 个行向量紧接在第 i 个行向量后面。
【例】二维数组 A mn 的按行优先存储的线性序列为: a11 ,a12 ,…,a1n ,a21 ,a22,…,a2n ,……,am1 ,am2 ,…,amn -
按列优先顺序:将数组元素按列向量排列,第 i+1 个列向量紧接在第 i 个列向量后面。
【例】二维数组 A mn 的按列优先存储的线性序列为: a11 ,a21 ,…,am1 ,a12 ,a22 ,…,am2 ,……,a1n ,a2n ,…,amn
-
-
数组元素的地址计算公式
-
LOC(aij)=LOC(a00)+(i×n+j)×d
【例】LOC(00)=100,d=2,计算a23的存储地址: LOC(a23)=100+(2*5+3)*2=126 -
三维数组的地址计算:
LOC(aijk)=LOC(000)+(i * n * p+j * p+k)*d
-
-
特殊矩阵
指的是相同值的元素或者零元素在矩阵中的分布有一定规律的矩阵。-
对称矩阵
若n阶方阵A中的元素满足aij=aji(0≤i;j≤n-1),则称A为n的对称矩阵。 -
sa[k]与aij之间存在的关系
若i≥j时,k=i*(i+1)/2+j; 若i<j时,k=j*(j+1)/2+i; 因此,aij的存储地址的计算公式:LOC(aij)=LOC(sa[k])=LOC(sa[0])+k*d
-
-
三角矩阵 三角矩阵有上三角和下三角两种。下三角矩阵的主对角线上方均为常数c或者零(图1)。上三角矩阵的主对角线下方均为常数c或者零(图2)。
求三角矩阵的aij一共的元素个数:i*(2*n-i+1)/2-
sa[k]与aij之间存在的关系
若i≥j时,k=i*(i+1)/2+j; 若i<j时,k=n*(n+1)/2;
-
-
-
稀疏矩阵
通常把有s个非零元素,而s远远小于矩阵元素的总数,则把这种矩阵称为稀疏矩阵。-
三元组表
如果将表示叙述矩阵非零元素的三元组按行优先的顺序排列,则可得到一个其结点均为三元组的线性表,则把这种线性表的顺序存储结构称为三元组表。
-
-
转置方法
-
一般的转置算法
对矩阵A中的行号和列号互换后再依次存入到矩阵B中。
-
快速转置算法
设两个向量:num和cpot num[col]表示矩阵M中第col列中的非零元素个数。 cpot[col]指M中第col列的第一个非零元在b.data中的恰当位置。 有下面两个公式: cpot[1]=1; cpot[col]=copt[col-1]+num[col-1] (col≥2)
-
-
(六)-广义表
-
广义表
广义表是线性表的推广,又称列表。是 n(n≥0)个元素 a1 ,a2 ,…,ai ,…,an 的有限序列。
广义表通常记作:Ls=( a1 ,a2 ,…,ai ,…,an ): 1.Ls 是广义表的名字,n为它的长度; 2.为了区分原子和广义表,书写时用大写字母表示广义表,用小写字母表示原子。 3.若广义表 Ls非空(n≥1),则 al是 LS的表头,其余元素组成的表(a1 ,a2 ,…,an )称为Ls的表尾。 4.广义表是递归定义的;
性质: 1.广义表的元素可以是子表,而子表又可以含有子表,因此广义表是一个多层次结构的表; 2.广义表具有递归和共享的性质;
-
例子
A=()-A是一个空表,其长度为零; B=(a)-B是一个只有一个原子的广义表,其长度为1; C=(a,(b,c))-C是一个长度为2的广义表,第一元素是原子,第二个元素是子表; D=(A,B,C)=((),(a),(a,(b,c)))-D是一个长度为3的广义表,其中三个元素是子表; E=(C,d)=((a,(b,c)),d)-E是一个长度为2的广义表,第一个元素是子表,第二个元素是原子; F=(e,F)=(e,(e,(e,...)))-F是一个递归的表,它的长度为2,第一个元素是原子,第二个元素是表自身,展开后它是一个无限的广义表;
-
-
广义表的存储结构
tag是一个标志位,用来区分当前结点是原子还是子表; 当tag为零值时,该结点是子表;第二个域为slink,用以存放子表的地址;
当tag为1时,该结点是子表;第二个域为data,用以存放元素值。link域是用来存放与本元素同一层的下一个元素对应结点的地址,当该元素是所在层的最后一个元素时,link的值为NULL。
-
广义表的基本运算
-
建立广义表的存储结构
Glist CreatGList(Glist GL){ char ch; scanf("%c",&ch); if(ch!=''){ GL=(GLNode *)malloc(sizeof(GLNode)); if(ch=='('){ GL->tag=list; GL->slink=CreatGList(GL->slink);//递归调用的构造子表 }else{ //构造原子结点 GL->tag=atom; GL->data=ch; } }else GL=NULL; scanf("%c",&ch); if(GL!=NULL) if(ch==","){ GL->link=CreatGList(GL->link); //递归构造后续广义表 }else{ GL->link=NULL; //表示遇到‘)’或结束符‘;’时,无后续表 } return GL } -
输出广义表
void PrintGList(Glist GL){ if(GL!=NULL){ if(GL->tag==list){ printf("("); if(GL->slink==NULL) printf(" "); else PrintfGList(GL->slink); //递归调用输出子表 }else printf("%c",GL->data); //输出结点数据域值 if(GL->tag==list) printf(")"); if (GL->link!=NULL){ printf(","); PrintfGList(GL->link); //递归调用输出下一个结点 } } } -
广义表的查找
void FindGListX(GList GL,DataType x,int *mark){ if(GL!=NULL){ if(GL->tag==0 && GL->data==x){ p=GL; *mark=1; }else{ if(GL->tag==1) FindGListX(GL->slink,x,mark); FindGListX(GL->link,x,mark); } } } -
求广义表表头
Glist head(Glist GL){ Glist p; if(GL!=NULL && GL->tag!=0){ p=GL->slink; p->link=NULL; return p; }else return NULL; } -
求广义表表尾
Glist tail(Glist GL){ Glist p; if(GL!=NULL && GL->tag!=0){ p=GL->slink; p=p->link; GL->slink=p; } return p; } -
求广义表的深度
void depth(Glist GL,int *maxdh){ int h; if(GL->tag==0) *maxdh=0; //说明广义表尾单个元素 else if(GL->tag==1 && GL->slink==NULL) *maxdh=1; //广义表为空表 else //进行递归求解 GL=GL->slink; //进入第一层 *maxdh=0; do{ //循环扫描表的第一层的每个结点,对每个结点求其子表深度 depth(GL,&h); if(h>*maxdh) *maxdh=h; //取最大的子表深度 GL=GL->link; }while(GL!=NULL); *maxdh=*maxdh+1; //子表最大深度加 }
-
(七)-二叉树
-
树
定义:是n(n≥0)个结点的有限集 T。 性质: a.当n=0时,则为空树; b.有且仅有一个特定的称为根(Root)的结点; c.其余的结点可分为 m(m≥0)个互不相交的子集 Tl ,T2,…,Tm,其中每个子集本身又是一棵树,并称其为根的子树(Subree); 表示形式: a.树形表示 b.嵌套集合表示 c.凹形表示法 d.广义表示法 基本术语: (1) 结点的度(Degree) a.树中的一个结点拥有的子树数称为该结点的度(Degree); b.一棵树的度是指该树中结点的最大度数; c.度为零的结点称为叶子(Leaf)或终端结点; d.度不为零的结点称分支结点或非终端结点; e.除根结点之外的分支结点统称为内部结点; f.根结点又称为开始结点; (2) 孩子(Child)和双亲(Parents) a.树中某个结点的子树之根称为该结点的 孩子(Child)或儿子; b.该结点称为孩子的双亲(Parents)或父亲; c.同一个双亲的孩子称为兄弟(Sibling); (3)路径 a.若树中存在一个结点序列k1 ,k2 ,…,ki ,使得ki是ki+1 的双亲(1≤i<j),则称该结点序列是从kl到kj的一条路径(Path)或道路; b.路径的长度指路径所经过的边(即连接两个结点的线段)的数目,等于j-1; (4)祖先(Ancestor)和子孙(Descendant) a.若树中结点k到ks存在一条路径,则称k是ks的祖先(Ancestor) b.ks是k的子孙(Descendant); (5)结点的层数(Level)和树的高度(Height) a.结点的层数(Level)从根起算:根的层数为 1,其余结点的层数等于其双亲结点的层数加 1; b.树中结点的最大层数称为 树的高度(Height)或 深度(Depth) (6)有序树(OrderedTree)和无序树(UnoderedTree) a.若将树中每个结点的各子树看成是从左到右有次序的(即不能互换),则称该树为 有序树(OrderedTree); 否则称为 无序树(UnoderedTree) (7)森林(Forest) a. m(m≥0)棵互不相交的树的集合。树和森林的概念相近。删去一棵树的根,就得到一个森林;反之,加上一个结点作树根,森林就变为一棵树; -
二叉树
定义:是n(n≥0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及两棵互不相交的、分别称作这个根的左子树和右子树的二叉树组成。 性质: a.二叉树第 i 层上的结点数目最多为2^i-1 (i≥1)。例如5层的二叉树,第5层上的结点数目最多为 2^4=16; b.深度为k的二叉树至多有(2^k)-1个结点(k≥1)。例如深度为 的二叉树,至多有 2^5-1=31 个结点; c.在任意-棵二叉树中,若终端结点的个数为n0 ,度为 2 的结点数为n2 ,则no=n2+1。 例如一棵深度为 4 的二叉树(a),其终端结点数 n0 为 8,度为2的结点树为7,则8=7+1,no=n2+1 成立; d.具有 n 个结点的完全二叉树的深度为。⌊lgn⌋+1⌉; 例,具有100个结点的完全二叉树的深度为:⌊lg100⌋+1=7 ; 2^6 =64;2^7=128 所以⌊lg100⌋=6 ,⌈lg(100+1)⌉=7-
满二叉树和完全二叉树
-
满二叉树
定义:一棵深度为 k 且有 (2^k) -1 个结点的二又树称为满二叉树。 特点: a.每一层上的结点数都达到最大值。即对给定的高度,它是具有最多结点数的二叉树; b.满二叉树中不存在度数为1的结点,每个分支结点均有两棵高度相同的子树,且树叶都在最下一层上; -
完全二叉树
定义:若一棵二叉树至多只有最下面的两层上结点的度数可以小于2,并且最下一层上的结点都 集中在该层最左边的若干位置上,则此二叉树称为完全二叉树。 特点: a.满二叉树是完全二叉树,完全二叉树不一定是满二叉树; b.在满二叉树的最下一层上,从最右边开始连续删去若干结点后得到的二叉树仍然是一棵完全二叉树; c.在完全二叉树中,若某个结点没有左孩子,则它一定没有右孩子,即该结点必是叶结点;
-
-
-
存储结构
-
顺序存储结构
定义:在顺序存储一颗具有n结点的完全二叉树时,只要从树根开始自上到下,每层从左至右地给 该树中每个结点进行编号(假设编号从0开始),就能够得到一个反映整个二叉树结构的线性序列 。然后以各接待你的编号为下标,把每个结点的值对应存储到一个一维数组bt中。 完全二叉树的编号特点编号特点: a.若 i>1,则 k i 的双亲编号为⌊i/2⌋;若 i=1,则 K i 是根结点,无双亲; b.若 2i≤n,则 K i 的左孩子的编号是 2i;否则,K i 无左孩子,即 Ki 必定是叶子。因此完全二叉树中编号 i>⌊n/2⌋的结点必定是叶结点; c.若 2i+1≤n,则 K i 的右孩子的编号是 2i+1;否则,K i 无右孩子; d.若 i 为奇数且不为 1,则 K i 的左兄弟的编号是 i-1;否则,K i 无左兄弟; e.若 i 为偶数且小于 n,则 K i 的右兄弟的编号是 i+1;否则,K i 无右兄弟; -
链式存储结构
定义:用链接方式存储二叉树时,每个结点除了存储结点本身的数据外,还应设置两个指针域 lchild 和 rchild,分别指向该结点的左孩子和右孩子。
-
-
二叉树的运算
-
二叉树的生成
-
二叉树的遍历
所谓遍历,是指沿着某条搜索路径(线)周游二叉树,依次对树中每个结点访问且仅访问一次。-
前序遍历二叉树的递归定义:
若二叉树非空,则依次进行操作:①访问根结点;②前序遍历左子树;③前序遍历右子树; -
中序遍历二叉树的递归定义:
若二叉树非空,则依次进行操作:①中序遍历左子树;②访问根结点;③中序遍历右子树; -
后序遍历二叉树的递归定义:
若二叉树非空,则依次进行操作:①后序遍历左子树;②后序遍历右子树;③访问根结点; -
例题
前序遍历序列:ABDECF 中序遍历序列:DBEACF 后序遍历序列:DEBFCA
-
-
-
二叉线索树
定义:n 个结点的二叉链表中含有 n+1 个空指针域。利用二叉链表中的空指针域,存放指向结点在某种遍历次序下的前趋和后继结点的 指针(这种附加的指针称为"线索")。这种加上了线索的二叉链表称为线索链表,相应的二叉树称为 线索二叉树。 分类:根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。 线索链表解决:二叉链表找左、右孩子困难的问题,出现了无法直接找到该结点在某种遍历序列中的前趋和后继结点的问题。 线索链表的结点结构:-
二叉树的线索化
将二叉树变为线索二叉树的过程称为线索化;按某种次序将二叉树线索化的实质是:按该次序遍历二叉树,在遍历过程中用线索取代空指针。
-
-
哈夫曼树
定义:在权为wl,w2,…,wn的n个叶子所构成的所有二叉树中,带权路径长度最小(即代价最小)的二叉树称为最优二叉树或哈夫曼树。 树的路径长度是从树根到树中每一结点的路径长度之和。在结点数目相同的二叉树中,完全二叉树的路径长度最短。 结点的权:在一些应用中,赋予树中结点的一个有某种意义的实数。 结点的带权路径长度:结点到树根之间的路径长度与该结点上权的乘积。 树的带权路径长度:定义为树中所有叶结点的带权路径长度之和,通常记为: 其中:n 表示叶子结点的数目,wi和li分别表示叶结点ki的权值和根到结点ki之间的路径长度。 树的带权路径长度亦称为树的代价。 性质: a.叶子上的权值均相同时,完全二叉树一定是最优二叉树,否则完全二叉树不一定是最优二叉树; b.最优二叉树中,权越大的叶子离根越近; c.最优二叉树的形态不唯一,WPL 最小;(a)WPL=7*2+5*2+2*2+4*2=36 (b)WPL=7*3+5*3+2*1+4*2=46 (c)WPL=7*1+5*2+2*3+4*3=35 其中(c)树的 WPL 最小,可以验证,它就是哈夫曼树。