数据结构视频知识点整理 1

王道数据结构传送门

第一节.数据和算法

常用时间复杂度大小关系

O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)

int sum=0;
for(int i=0;i<=n;i=2*i){
   sum=sum+i;
}

满足条件:2k<n => k<log2n
时间复杂度 O(logn) 这里时间复杂度不考虑底数2

第二节.线性表

线性表是指具有相同数据类型的n个元素的有限序列

线性表的顺序存储

1.顺序表的存储方式

1.利用一组地址连续的存储单元,一次存储线性表中的数据元素,顺序存储的线性表也叫作顺序表
2.loc(an)=loc(a1)+(n-1)*d 知道首元素的存储地址和每个数据的存储单元就可以知道顺序表中的每一个数据的存储位置,时间复杂度为O(1),即为随机存取

#define maxsize 100//静态
typedef int elemtype
typedef struct{
	elemtype data[maxsize];
	int length;
}Sqlsit;
typedef int elemtype;//动态
#define initsize 100
typedef struct{
	elemtype *data;//创建一个指针域
	int maxsize,length;
}Seqlist;
Seqlist L;
L.data=(elemtype)malloc(sizeof(elemtype)*initsize);
//按照给定的需要的initsize大小动态的分配空间给顺序表
//这里要注意:链表和这个动态分配的顺序表的区别

*** 动态分配并不是链式存储,同样还是属于顺序存储空间,只是分配的空间大小可以在运行时决定

2.C动态分配空间知识复习

double *p;
p=(double*)malloc(30*sizeof(double));

1.首先开辟了30个double类型的空间,然后把p指向这个空间的位置。在这里的指针是指向第一个double值并非我们所有开辟的30个double的空间,这就和数组一样,指向数组的指针式指向数组首元素的地址,并非整个数组的元素。所以,在这里我们的操作也和数组是一样的, p[0]就是第一个元素,p[2]就是第二个元素。
2.当我们使用malloc()开辟完内存空间以后,我们所要考虑的就是释放内存空间,在这里,C给我们提供了free()函数。free()的參数就是malloc()函数所返回的地址,释放先前malloc()函数所开辟的空间,free函数也是必不可缺少的函数。

3.顺序表的基本操作

1.线性表的插入:在 1<= i <=length+1 的位置都可以进行插入操作,如果 i 的输入不合法则返回 false,否则将顺序表的第 i 个元素及其以后的元素都向右移动一个位置,腾出位置给新插入的元素,表长加1,插入成功,返回 true

bool listinsert(sqlsit &L,int i,elemtype e){
	if(i<1||i>L.length+1||L.length>=maxsize)
		return fasle;
	for(int j=L.length;j>=i;j--)
		L.data[j+1]=L.data[j];//定义表的第一个元素存储在数组data第一个空间
	L.data[i]=e;
	L.length++;
	return true;
}

顺序表插入的算法分析:
最好情况:直接在表尾进行插入,数据不需要移动,时间复杂度为O(1)
最坏情况:在表头进行插入,数据需要移动 n次,时间复杂度为O(n)
平均情况:概率为从插入第一个位置到插入最后的n+1个位置 p = 1/(n+1)
对应着平均移动次数 n- i+1,将概率和平均移动次数相乘,从i=1到i=n+1进行求和,最后为 n/2,时间复杂度为O(n)

bool sqlistdelete(sqlist &L,int i,elemtype &e)
{
	if(i<1||i>L.length)
		return false;
	e=L.data[i];
	for(int j=i;j<=L.length;j++)
		L.data[j]=L.data[j+1];
	return true;
}

顺序表删除的算法分析:
最好情况:直接在表尾进行删除,数据不需要移动,时间复杂度为O(1)
最坏情况:在表头进行删除,数据需要移动 n-1 次,时间复杂度为O(n)
平均情况:概率为从插入第一个位置到插入最后 n 的位置 p = 1/n
对应着平均移动次数 n- i,将概率和平均移动次数相乘,从i=1到i=n+1进行求和,最后为 (n-1)/2,时间复杂度为O(n)

顺序表的优缺点分析

优点缺点
存储密度大,无需给存储数据关系增添额外的逻辑结构删除和插入需要移动大量的数据元素
随机存取,想在哪里存数据或者去数据均可对存储空间要求高,容易产生空间碎片

线性表的链式存储

1.单链表的存储方式

1.用一组任意的存储单元来存储这些数据信息,为了建立起来线性存储关系,每一个链表的结点不仅存放自身的数据信息,还存放了下一个数据信息的地址,这种方式称之为单链表

typedef struct Lnode{
	elemtype data;
	struct Lnode *next;
}Lnode,*linklist;
//该结构体命名为Lnode,指向这个结构体的指针类型称之为*linklist

2.通常用头指针来表示一个单链表,知道了头指针就知道了第一个结点的信息,这整条单链表也都知道了,linklist L,L则为这个单链表的头指针
3.在单链表的第一个结点前面附加一个一个不存储任何信息的空节点,称之为头结点,设立头结点可以处理操作方便,删除第一个元素结点或者在第一元素结点的后面增加结点都和其他的结点的操作一致,无论链表是否为空,头指针是指向头结点的非空指针。

头指针L(指向第一个结点的地址) —> 空的头结点 —> 带有存储信息的结点
不管带不带头结点,头指针始终指向链表的第一个结点,尾指针始终指向链表的最后一个节点,而头结点是带头结点链表中的第一个结点,结点内不存储信息,linklist L,则头指针L就代表一个单链表,通过头指针就能找到头结点

4.如何获取单项链表的头结点
A.声明链表后,马上定义一个头指针,指向链表的头结点,这样,不管程序运行到哪儿,都可以通过访问头指针来得到头结点。
B.定义单独的头结点,不赋值,永远只作为类似标志的性质,以后通过它访问整个链表,即链表第一个结点为头结点->Next
C.将单向链表制作为循环链表,在头结点设置特殊值,永远往一个方向找,一旦找到特殊值,即为找到头结点。

2.单链表的基本操作

1.创建单链表之头插法
linklist creatlinklist(linklist &L){
	int x;
	linklist s;
	scanf("%d",&x);
	L=(linklist)malloc(sizeof(Lnode));
	while(x!=9999){
		s=(linklist)malloc(sizeof(Lnode));
		s.data=x;
		s->next=L->next;//让s后面与头结点后面的结点相连
		L->next=s;//让s连接在头结点后面
		scanf("%d",&x);
	}
	return L;
}

新的结点都是插在头结点后面,生成的链表数据顺序与输入的数据顺序相反
创建结点指针后,linklist p,如果涉及data数值的改变
必须先向空间申请内存
p=(linklist)malloc(sizeof(Lnode));
如果仅仅只作为一个指针前后移动的话
则无需向空间申请内存

2.创建单链表之尾插法
linklist creatlinklist(linklist &L){
	int x;
	scanf("%d",&x);
	L=(linklist)malloc(sizeof(Lnode));
	linklist s,r=L;//r为表尾元素,s为新结点
	while(x!=9999){
		s=(linklist)malloc(sizeof(Lnode));
		s.data=x;
		r->next=s;//表尾元素的下一个值为s
		r=s;//再移动表尾元素
		scanf("%d",&x);
		}
	r->next=NULL;//保证最后的表尾元素下一值为空
	return L;
}

尾插法读入数据的顺序和输入数据的顺序一致

3.查找单链表之按序号查找
linklist searchlinlist(linklist L,int i){
	int j=1;
	if(i==0)return L;
	if(i<1)return NULL;
	linklist p=L->next;//p从头结点的下一位结点开始
	while(j<i&&p!=NULL){
		p=p->next;
		j++;
	}
	return p;
}

在这里默认头结点为i=0的结点

4.查找单链表之按值查找
linklist searchlinlist(linklist L,elemtype e){
	linklist p=L->next;//p从头结点的下一位结点开始
	while(p!=NULL&&p.data!=e){
		p=p->next;
	}
	return p;
	//要不p=NULL跳出要不p.data=e跳出,直接返回p就行
}
5.插入单链表的结点
bool insertlinlist(linklist &L,elemtype e,int i){
	linklist r,s;
	r=searchlinlist(L,i-1);//找到i-1的结点的位置
	if(r==NULL)return false;
	s=(linklist)malloc(sizeof(Lnode));
	s.data=e;
	s->next=r->next;
	r->next=s;
	return true;
}
6.删除单链表的结点
bool deletelinlist(linklist &L,int i){
	linklist r,s;
	r=searchlinlist(L,i-1);//找到i-1的结点的位置
	if(r==NULL)return false;
	s=r->next;//找到i的结点的位置
	r->next=s->next;
	free(s);
	return true;
}

3.双链表

1.双链表的存储方式
typedef struct Dnode{
	elemtype data;
	struct Dnode *next,*prior;
}Dnode,*Dinklist;

相对于单链表而言,双链表就是在单链表的基础上面增加一个前驱指针,可以通过前驱指针找到前面的结点,典型的消耗空间换取时间的例子

2.双链表的按序号取值
linklist searchDinlist(Dinklist L,int i){
	int j=1;
	if(i==0)return L;
	if(i<1)return NULL;
	Dinklist p=L->next;//p从头结点的下一位结点开始
	while(j<i&&p!=NULL){
		p=p->next;
		j++;
	}
	return p;
}
3.双链表的插入结点
bool insertDinlist(Dinklist &L,elemtype e,int i){
	Dinklist r,s;
	r=searchDinlist(L,i-1);//找到i-1的结点的位置
	if(r==NULL)return false;
	s=(linklist)malloc(sizeof(Lnode));
	s.data=e;
	r->next->prior=s;
	s->next=r->next;
	r->next=s;
	s->prior=r;
	return true;
}
4.双链表的删除结点
bool deleteDinlist(Dinklist &L,int i){
	Dinklist r,s;
	r=searchDinlist(L,i-1);//找到i-1的结点的位置
	if(r==NULL)return false;
	s=r->next;//找到i的结点的位置
	r->next=s->next;
	s->next->prior=r;
	free(s);
	return true;
}

理解还有困难的话可以尝试画图看看哦

3.循环单链表

1.循环单链表在表中的最后一个元素的指针不再是指向NULL,而是改为指向头结点,从而整个链表形成一个环
2.从任何的一个节点出发都能访问到链表中的每一个元素
3.判空条件不是头结点的后继指针是否为空,而是判断头结点的后继指针是否等于头指针,等于的话头结点的后继指针指向自己,链表为空
4.可对循环单链表设立尾指针不设立头指针,设立尾指针既可以访问到尾结点的数值又可以访问到第一个结点的数值,头指针还需要遍历才能访问到最后一个节点的数值,尾指针是指向最后一个节点地址的指针

4.循环双链表

1.循环双链表就是双链表首尾构成环
2.在循环双链表L中,尾指针的后继指针指向表头结点,头结点的前驱指针指向表尾结点
3.当循环链表为空时,其头结点的next域和prior域都等于L

4.静态链表

1.静态链表是借助数组来表示的存储结构,其中结构体里面也有data和next,data域借助数组来存放,而next域借助结点数组存放的数组下标来寻找,就相当于next域存放的是这个data域下面一个data域的数组下标
2.静态链表中以next为-1作为其结束的标志
3.静态链表的操作只需要修改数组的指针,不需要移动元素

#define maxsize 50
typedef int elemtype
typedef struct{
	elemtype data;
	int next;
}slinklist[maxsize];

第三节.栈

栈(stack):只允许在一端进行插入或删除操作的线性表
栈顶(top):栈中允许进行插入和删除的那一段
栈底(bottom):固定的,不允许进行插入和删除的那一端
栈运行的规则:FILO(后进先出)

1.顺序栈

栈的顺序存储结构称之为顺序栈

#define maxsize 100
typedef struct{
	elemtype data[maxsize];
	int top;//以i=0的数组下标作为栈底
}sqstack;

top值不可以超过maxsize
判断栈空的条件通常为 top==-1
判断栈满的条件通常为 top==maxszie-1
栈中元素为 top+1
也就是从 i=0 作为栈底开始存数据一直到 i=maxsize-1 结束,最大的数据能存maxszie个数据,当前数据为 top+1个数据

顺序栈的判空
bool stackempty(sqstack S){
	if(S.top!=-1)
		return true;
	return false;
}
顺序栈的进栈
bool push(sqstack &S,elemtype e){
	if(S.top==maxszie-1)
		return false;
	s.data[++top]=e;
	return true;
}
顺序栈的出栈
bool pop(sqstack &S,elemtype &x){
	if(S.top==-1)
		return false;
	x=s.data[top--];
	return true;
}
顺序栈的读取栈顶元素
bool push(sqstack &S,elemtype &x){
	if(S.top==-1)
		return false;
	x=s.data[top];
	return true;
}

2.共享栈

顺序栈存储空间大小需要实现开辟好,会造成每个栈的利用率并不是很高,可以将两个栈进行合并存储在一个数组里面称之为共享栈

栈1底top1top2栈2底
0123maxsize-2maxsize-1

由图不难看出
栈1底 = 0--------top1数值越大栈1元素越多
栈2底 = maxsize-1--------top2数值越小栈2元素越多
栈满的判断条件 top2=top1+1

共享栈的定义
#define maxsize 100
typedef struct{
	elemtype data[maxsize];
	int top1;
	int top2;
}sqdoublestack;
共享栈的入栈操作
bool push(sqdoublestack &s,elemtype e,int stacknum){
	if(s.top1+1=s.top2)
		return false;
	if(stacknum==1)
		s.data[++top1]=e;
	if(stacknum==2)
		s.data[--top2]=e;
	return true;
}
//在这里设置stacknum标识
//当stacknum=1时对栈1进行操作,当stacknum=2时对栈2进行操作
共享栈的出栈操作
bool pop(sqdoublestack &s,elemtype &x,int stacknum){
	if(s.top1==-1||s.top2==maxsize)
		return false;
	if(stacknum==1)
		x=s.data[top--];
	if(stacknum==2)
		x=s.data[top++];
	return true;
}

3.链式栈

1.可以用链表的方式来实现栈
2.对于一个单链表来讲,可以将头指针当做栈顶指针,所以栈顶放在单链表的头部,栈底则为最后一个元素
3.链式栈一般不存在栈满的情况,可以在内存允许的范围下面无限进栈
4.判断栈空的条件通常为top==NULL

链式栈的定义
typedef struct snode{
	elemtype data;
	struct snode *next;
}snode,*slink;
typedef struct{
	slink top;
	int count;
}linkstack;

我自己的理解就是snode的作用就是盖房子,linkstack的作用是按照snode的标准该一个房子作为头房子,头房子后面还可以连接房子,用count来记录头房子后面总共还有多少房子

链式栈的进栈
bool push(linkstack &s,elemtype e){
	slink p;//创造一个新的房子
	p=(slink)malloc(sizoef(snode));
	p.data=e;
	p->next=s.top;//新房子的后继指向头房子,连在头房子前面
	】
	s.top=p;//将新房子命名为头房子
	s.count++;//房子数量加加
	return true;
}

有点类似于头插法哦,在链表的头部插入元素

链式栈的出栈
bool pop(linkstack &s,elemtype &x){
	if(s->top==NULL)return false;
	x=s->top->data;
	slink p=s->top;//保存栈顶的地址
	s->top=s->top->next;//将栈顶向下移
	free(p);释放栈顶元素
	return true;
}

第四节.队列

队列(queue):只允许在一端进行插入或删除操作的线性表
队头(front):允许删除的一端,或者称为队首
队尾(rear):允许插入的一端
队列的操作特点是:先进先出(FIFO)first in first out
一般来说队首指向队列的第一个元素,队尾指向队列最后的一个元素下一个位置

1.顺序队列

A.顺序队列设想1

用数组来实现队列,把队首放在数组下标为0的位置,队尾放在最后一个元素的后面一个数组下标的位置,那么入队的话就只需要在数组最大限制个数内把队尾下标赋值,队尾加加即可,出队的话把队首元素删除,每一个元素都向前移动,队首不变还是数组下标为0 的位置,队尾减减即可,队空条件是就是队首等于队尾等于数组下标为0,但是可能出队操作过于麻烦

B.顺序队列设想2

还是用数组来实现队列,把队首和队尾不再固定,入队还是填入元素,队尾加加,出队就是队首移动加加即可,但是发现随着队首元素的不断移动,不断的入队,这个队列能存放的数据越来越小,我们可以想象把数组给掰弯,如果达到最大数组限制范围,继续入队的话我们可以把队尾指针指向队头移动空余的前面那些数组上面,就好比一个环,这样我们引出了循环队列、

#define maxsize
typedef struct{
	elemtype data[maxsize];
	int front,rear;
}sqqueue

2.循环队列

1.把数组掰弯,形成一个环,rear指针到了下标为最大数组限制的地方还能回到下标为0的地方,这样首尾相连的顺序存储的队列称之为循环队列
2.那么数组并不能被掰弯,我们想到了取余操作,超出了maxsize后对maxsize取余就能使数回到前面来,由此可知
入队:rear=(rear+1)%maxsize
出队:front=(front+1)%maxsize
3.那么问题又来了,当队满的情况下,rear指针和front指针重合是,我们怎么判断是这个队列满的情况还是这个队列空的情况?
队列空:rear=front 队列满:rear=front ???

A.解决方案一

我们可以设置一个flag的表示,当入队的时候顺便令flag=1,当出队的时候顺便令flag=0,那么当队头指针等于队尾指针的时候,如果flag=1,此时有入队操作应该是队列满,如果当flag=0,此时有出队操作应该是队列空

B.解决方案二(重点)

我们可以规定队列当队列中仍还有一个空留单元的时候,队列为满的状态,队列为空的状态是rear=front

2345
rearfront

可以看到此时我们就规定队列为满
那么此时队列为满的条件就为(rear+1)maxsize=front
队列为空的条件就是 rear=front
队列中还有的元素为 (rear-front+maxsize)%maxsize

顺序队列入队操作
bool enqueue(sqqueue &q,elemtype e){
	if((q.rear+1)%maxsize==q.front)
		return false;
	q.data[q.rear]=e;
	rear=(rear+1)%maxsize;
	return true;
}
顺序队列出队操作
bool dequeue(sqqueue &q,elemtype &x){
	if(q.rear==q.front)
		return false;
	x=q.data[q.front];
	front=(front+1)%maxsize;
	return true;
}

3.链式队列

1.队列的链式存储结构,其实就是线性表的单链表,只不过需要加一点限制条件,只能在表尾插入元素,表头删除元素
2.为了操作方便,我们分别设置队头指针和队尾指针,队头指针指向头结点,队尾指针指向尾结点

1.链式队列的定义
typedef struct linknode{
	elemtype data;
	struct linknode *next;
}linknode,*linklist;
typedef struct{
	linknode *front,*rear;
	//linklist front,rear;两种均可
}linkqueue;
2.链式队列的入队操作

在队尾指针进行结点插入操作

bool enlinkqueue(linkqueue &q,elemtype m){
	linknode *s;
	s=(linklist)mallco(sizeof(linknode));
	//s=(linknode *)mallco(sizeof(linknode));两种均行
	s->data=m;
	s->next=NULL;
	q.rear->next=s;
	q.rear=s;
	return true;
}

这里要注意q的元素是以.的形式表示,s的元素是以->的形式表示

3.链式队列的出队操作

在队头指针指向的头结点的后继元素进行结点删除操作,并将头结点的后面元素改为头结点后继元素的后继元素

bool dequeue(linkqueue q,elemtype &x){
	if(q.front=q.rear)
		return false;
	x=q.front->next->data;
	if(q.front->next==q.rear)
	{	
		q.rear=q.front;
		free(q.front->next);
	}
	q.front->next=q.front->next->next;
	free(q.front->next);
	return true;
}

这里特别要注意当只有一个元素的时候,如果直接删除头结点的后继结点的话,会使队尾指针也被删除了,要先修改一下这个队尾指针再进行删除

3.双端队列

1.双端队列是指两端都可以进行入队和出队操作的队列
两端就分别命名为前端和后端
2.输入受限制的双端队列:一端只能输出一端都能输出和输入的双端队列
3.输出受限的双端队列:一端只能输入一端都能输出和输入的双端队列

第五节.栈的应用

1.括号匹配问题

预备知识

switch的用法

直接点进去看看了解了解即可

代码展示
bool check(char *str){
	stack s;
	initstack(s);
	int len=strlen(str);
	for(int i=0;i<len;i++){
		char a=str[i];
		switch(a){
			case'(':
			case'[':
				push(s,a);
				break;
			case')':
				x=pop(s);
				if(x!='(')
					return false;
					break;
			case']':
				x=pop(s);
				if(x!='[')
					return false;
					break;
			}
		if(stackempty(s)) return true;
		else return false;
	}
}

算法思想:如果是左括号的话,就入栈,如果是右括号的话就出栈,判断栈顶元素的左括号是否和右括号匹配,一直操作至字符串的最后一个元素,最后判断栈是否为空,不为空的话证明匹配不成功

2.简单后缀表达式求值问题

1.代码实现

C++栈的用法及栈的实现

在默认以#号结束
不超过9的正整数的简单后缀表达式四则求值

#include<cstdio>
#include<stack>
#include<iostream>
using namespace std; 
#define N 10
char CalPostfix(char a[]){
	int num1,num2,num,k=0,i=0;
	stack<int>s;//建立一个栈
	while(a[k]!='#')//看数组中元素的数量
		k++;//默认最后以#结束
	while(i<k){
		if(a[i]>='0'&&a[i]<='9'){//这里的数组为字符数组
			num=(int)(a[i]-'0');//要转化为整形的数字
			s.push(num);
			i++;
		}
		else 
		{
			num1=s.top();
			s.pop();
			num2=s.top();
			s.pop();
			char b=a[i];
			switch(b)
			{
				case'+':
					num=num2+num1;
					break;
				case'-':
					num=num2-num1;
					break;
				case'*':
					num=num2*num1;
					break;
				case'/':
					num=num2/num1;
					break;
			}
			s.push(num);
			i++;
		}
	}
	return s.top();
}
int main()
{
	char a[N]="231*+9-#";
	int x=CalPostfix(a);
	cout<<x<<endl;
	return 0; 
 } 

算法思想:建立一个栈,如果遇到整数就存储在栈中,遇到四则运算就取栈顶的两个元素进行四则运算,运算后的数再存储在栈顶,依次进行,则运算结果就是最后栈的栈顶
一定要特别注意,取数的时候,第二个数作为开头进行计算
num2/num1 必须是这样的

2.将中缀表达式转换为后缀表达式

1.按照运算符的优先级将所有的运算符和运算数加括号

(5+20+13)/14
==> ( ( (5+20) + (13)) / 14)

2.将所有的运算符提取到括号的后面

==>(((5 20)+(1 3)*)+14)/

3.把括号的删除

==>5 20 + 1 3 * + 1 4 /

后缀表达式就转换完毕了

3.多位数的后缀表达式求值

只需要在字符数组里面的每一个字符中间添加一个空格
空格前后是数字或者运算符即可
多位数的后缀表达式求值
去看一看这个博客就行啦

第六节.特殊矩阵的存储和压缩

1.普通矩阵的存储

A.按行进行存储(行优先)

通俗理解就是先把二维数组一行一行的存储

7777777
7777777
7777777
7777

判断aij的存储位置
先判断在这个元素到已知元素中间有多少整行,再看已知元素到后面行截止有多少个单位,再看aij前面有多少个元素,加起来,看题目要求乘以每个存储单位的空间加上已知元素的存储地址即可

B.按行进行存储(列优先)

通俗理解就是先把二维数组一列一列的存储

7777777
777777
777777
777777

判断aij的存储位置
先判断在这个元素到已知元素中间有多少整列,再看已知元素到后面列截止有多少个单位,再看aij上面有多少个元素,加起来,看题目要求乘以每个存储单位的空间加上已知元素的存储地址即可
要特别注意给的元素地址是这个元素的开头地址还是包含这个元素的元素截止后的地址,这个关系到要不要加这个元素的地址

2.对称矩阵的存储

满足aij=aji的一个矩阵

1234567
2234567
3334567
4444567
5555567
6666667
7777777

这种矩阵的存储思路就是只存储上三角矩阵或者只存储下三角矩阵,按照行优先的方式存储到一个一维数组里面

A.存储下三角(默认)

就是先存1 2 2 3 3 3 4 4 4 4…7 7 7 7 7 7 7
总共需要(1+n)n/2个存储单位,数组的最大下标为(1+n)n/2-1
那么aij的位置
从1到i-1需要(1+i-1)
(i-1)/2个位置
在第i行需要j个位置
总共需要 (1+i-1)
(i-1)/2+j 个空间,对应数组下标是 k=(1+i-1)*(i-1)/2+j-1

B.存储上三角

就是先存1 2 3 4 5 6 7…5 6 7 6 7 7
总共需要(1+n)*n/2个存储单位,数组的最大下标为(1+n)n/2-1
相当是把下三角的 i 和 j 交换一下位置
那么aij
对应数组下标是 k=(1+j-1)
(j-1)/2+i-1

3.三角矩阵的存储

矩阵的上三角或者下三角都是一个固定的常数
下三角矩阵

1000000
2200000
3330000
4444000
5555500
6666660
7777777

上三角矩阵

1234567
9234567
9934567
9994567
9999567
9999967
9999997

先按照行优先的存储方式存储那个三角矩阵,最后另外那一边的常数存储在这个一维数组的最后面
总共在一维数组里面存储的元素有(1+n)*n/2+1个元素,数组的最大下标为(1+n)*n/2

下三角矩阵和对称矩阵的下三角存储方式是一样的
那么aij的位置
从1到i-1需要(1+i-1)(i-1)/2个位置
在第i行需要j个位置
总共需要 (1+i-1)
(i-1)/2+j 个空间,对应数组下标是 k=(1+i-1)*(i-1)/2+j-1
对于上三角元素存在k=(1+n)*n/2的地方即可

对于上三角矩阵来说
那么aij的位置
第一行有n个元素
第二行有n-1个元素
第三行有n-3个元素

第i-1行有n-(i-1)个元素
第i行有j-i+1个元素
总共是第 (n+n-(i-1))(i-1)/2+j-i+1的元素
对应的数组下标为 k=(n+n-(i-1))
(i-1)/2+j-i

4.三对角矩阵的存储

对对三对角矩阵的每一个元素aij来讲,如果 |i-j|>1时,则aij=0

1200000
2230000
0334000
0044500
0005560
0000667
0000077

按照行优先的顺序存储每一个不为0的元素的值在一个一维数组里面
对于三对角矩阵来说

那么aij的位置
第一行有2个元素
第二行有j-i+1个元素
除去第一行和最后一行 还有(i-2)*3个元素
总共有 2+(i-2)*3+j-i+1个元素
对应数组的下标为k=2+(i-2)*3+j-i

那么排在第k个位置的元素
排在第k个数组的位置,实际上有k+1个元素,前面还有k个元素
除去第一行的两个元素 还有k-2个元素
除去最后一行 还有[(k-2)/3](向下取余)行
总共有i=[(k-2)/3+1+1] (向下取余)行
怎么说呢
满足这个能取余的一定不是最后一行,最后一行向下取余一定没有
那么第i-1行有 2+(i-1)*3 个元素,看k+1个元素前面还有(k+1)- 2+(i-1)*3 个元素,这个数值(k+1)-(2+(i-1)*3)意思就是第i行非零元素有这么多元素
那么第i行都有i-2个非零元素,观察那个矩阵可以得到
所以 j=(k+1)-(2+(i-1)3)+i-2
化简整理 i=[(k+1)/3+1] (向下取余) j=k-2
i+3
多理解理解 不要背公式 遇到题直接上去推就行
OK啦!fight! fight!

第七节.树

ps:树和图上课的时候没太学好
王道的树我前几章有点看着吃力,我换成了讲解比较详细的青岛大学的王卓的树和图,准备做那里的笔记
青岛大学王卓数据结构传送门

1.树的定义

树是n个结点的有限集合,当n=0时,此数为一棵空树,当n>0时,有且仅有一个根结点,其余结点为m个互不相交的有限集T1,T2,T3…Tm ,其中每一个集合本身又是一棵树,并称为根的子树

2.树的基本术语

在这里插入图片描述
画图工具传送门

1.树

树的根结点:非空树中无前驱的结点
结点的度:结点拥有的子树的数量
树的度:树中所有结点的度的最大值
度=0:叶子结点,终端结点
度!=0:分支结点,非终端结点
内部结点:除了根结点之外的非终端结点
树的层次:根结点为第一层,依次往下增加
树的深度(高度):这棵树的最大的层次
森林:m(m>=0)棵互不相交的树的集合
树一定是森林,森林不一定是树
如图可知:
度(树)=3,度(A)=3,度(B)=2,度(G)=0,度(H)=1
根结点为A,叶子结点为K,分支结点就是除了叶子结点和根结点的所有结点
树有4层,树的深度为4

2.二叉树

所有数都可以转换为唯一二叉树
二叉树的左子树和右子树不能颠倒
二叉树并不是树特殊情况
具有两个节点的二叉树有两种状态
具有两个节点的树只有一种状态
在这里插入图片描述
有三个节点的二叉树有五种不同的状态
有三个节点的树有两种不同的状态
二叉树有五种不同状态
空树,根和空的左右子树,根和左子树,根和右子树,根和左右子树

3.二叉树的性质和存储特性

1.二叉树的性质
A.在二叉树的第i层上面至多有2i-1个结点,至少有1个结点

这个显而易见,画一个图就可以知道了

B.深度为k的二叉树总结点数至多为2k-1个,至少有k个结点

这个也不难知道,只需要将每一层的2i-1都累加就行

C.叶子结点的数量是度为2的结点的数量加1

证明:从下往上看,每一个结点都和其前驱结点有一条边相连,但是根结点无前驱结点故无这条边,所有总的结点数等于总边数减一,从上往下看,度为2的结点2+度为1的结点1+叶子结点*0也等于总边数,联立这两个方程组就可以推出这个结论了

2.满二叉树和完全二叉树
A.满二叉树

顾名思义就是这棵二叉树每一层都达到了最大的结点数
深度为k,结点数必定达到了2k-1个,每一层都是满的
叶子结点必定在最下面一层
在这里插入图片描述

B.完全二叉树

深度为k有n个结点的二叉树,当且仅当每一个结点都与深度为k的满二叉树中编号为1~n的结点一一对应的时候,称这棵二叉树为完全二叉树在这里插入图片描述
在这里第一棵树为满二叉树,第二树为完全二叉树,完全二叉树中的每一个结点的编号都与这个满二叉树像对应
在这里插入图片描述
在这里的话,第二棵树就不是完全二叉树,因为在这棵树L 12上面对应着是满二叉树的M 13,没有做到一一对应

其实我觉得吧,判断一棵树是否为完全二叉树,只要看最下面一层上面的其它层是不是每一层都满了,并且最下面这一层每一个结点必须从左往右一次填入,但凡有一个填入不正确,则都不是完全二叉树

完全二叉树的叶子结点只可能分布在最后两层,且对于任何的一个结点,右子树的最大层次为i,那么左子树的层次只可能为i或者i+1、

2.完全二叉树的性质
A.具有n个结点的完全二叉树的深度为[log2n]+1

对log2n进行向下整数的取余,在编程中,如果你定义的就是整形,它会自动取余取整数的

B.一个不为根结点的完全二叉树的结点编号如果是i的话,那么这个结点的双亲结点编号为i/2,这个结点的后继结点的编号为2i和2i+1

使用这个性质,我们能很快的看出在完全二叉树中的结点的后继和前驱
在这里插入图片描述

3.二叉树的顺序存储方式

按照满二叉树的结点编号,分别对应存储在一个一维数组的相应数组下标的位置

#define maxsize 100
typedef Telemtype sqbitree[maxsize];
sqbitree BT;

一维数组表示:

1122556677778899100
123456789

在这里插入图片描述
如果一些结点为空,那些数组对应的结点编号的数组下标的元素设置为空或者为0

一维数组表示:

11225566770880100
123456789

在这里插入图片描述
例题展示:
二叉树的结点采用如图顺序存储的方式,已知该数组的表示,请你画出这个二叉树

aebfdcgh
123456789101112

按照满二叉树的编号表示,我们画出满二叉树来,把对应空的位置删除即可
在这里插入图片描述
最后二叉树如图所示:
在这里插入图片描述
二叉树顺序存储的缺点:
对于一些右单只二叉树来讲,深度为k需要2k-1个存储空间,但实际存储的数据只有k个元素,极大的浪费了数组的存储空间,但是对于满二叉树和完全二叉树来讲,顺序存储不失一种很好的存储方式

3.二叉树的链式存储方式

对于一些单二叉树,我们可以采用链式存储结构,二叉链表或者三叉链表来存储这个二叉树

A.二叉链表
typedef struct binode{
Telemtype data;
struct binde *lchild,*rchild;
}binode,*bitree;

链表的存储结构展示:
在这里插入图片描述
在n个结点的二叉链表中,其中存在n+1个空的指针域
证明:因为在n个结点的二叉链表中,我们可以知道有2n个指针域,除去了根结点之外,每一个结点都有一个双亲结点,所用总共占用了n-1个指针域,2n-(n-1)

B.三叉链表

采用二叉链表不方便看到双亲的信息,我们还可以怎填一个指针域来看双亲的信息

typedef struct tribinode{
Telemtype data;
struct tribinde *lchild,*rchild,*parent;
}tribinode,*tribitree;

三叉链表的展示:
在这里插入图片描述

4.二叉树的遍历

顺着一个结点开始按照某种路径寻访二叉树中的每一个结点,使得这个二叉树的每一个结点都被访问,且只访问一次,访问后能做很多的操作
遍历能够使二叉树得到一个线性的数据排列
得到这个线性排列后,我们后序的插入删除排序等操作就都可以实现
在这里插入图片描述

1.三种遍历方式

找到这个二叉树的定义,我们可以把遍历分为六种,左子树根右子树,根左子树右子树,左子树右子树根,右子树根左子树,根右子树左子树,右子树左子树根,我们探讨前三种

在这里插入图片描述

A.先序遍历(根左子树右子树)

若二叉树为空,则空操作
访问根结点
先序遍历左子树
先序遍历右子树
则先序遍历的线性序列为:ABELDHMIJ

B.中序遍历(左子树根右子树)

若二叉树为空,则空操作
中序遍历左子树
访问根结点
中序遍历右子树
则中序遍历的线性序列为:ELBAMHIDJ

C.后序遍历(左子树右子树根)

若二叉树为空,则空操作
后序遍历左子树
后序遍历右子树
访问根结点
则中序遍历的线性序列为:LEBMIHJDA

例题1练习:
在这里插入图片描述
先序遍历:ABDGCEHF
中序遍历:DGBAEHCF
后序遍历:GDBHEFCA
OK 就是这样啦

例题2练习:
在这里插入图片描述
先序遍历:—+AB—CD/EF(前缀表达 波兰式)
中序遍历:A+B
C—D—E/F(中缀表达)
后序遍历:ABCD—*+EF/— (后缀表达 逆波兰式)

2.由两种遍历来求二叉树

遍历序列总共有三种
我们已知前序和中序序列能够求出二叉树
我们已知后序和中序序列能够求出二叉树
但是已知前序和后序序列不能求出二叉树

例题一:
已知二叉树的前序和中序序列,请求出这棵二叉树
前序序列:ABCDEFGHIJ
中序序列:CDBFEAIHGJ

~~ 一般做这种题的大概思路就是通过两种序列不断的判断根,左子树和右子树,判断完毕接着判断左子树的这三个,一直到左右子树为一个单节点为止 ~~
这道题通过前序序列能看出根为A,通过中序序列能看出左子树为CDBFE,右子树为IHGJ,接着判断左子树,通过前序序列能看出根为B,通过中序序列能看出左子树为CD,右子树为FE,接着判断左子树,通过前序序列能看出根为C,通过中序序列能看出左子树为空,右子树为D,依次这样进行即可
在这里插入图片描述
道理都是这样,但是容易做错呀,刚刚就又错了
要记得通过前序序列判断根,通过中序序列判断左右子树

例题二:
已知二叉树的后序和中序序列,请求出这棵二叉树
后序序列:DECBHGFA
中序序列:BDCEAFHG

思路也都一样,在这里只不过是把判断根的条件换成了后序序列,通过后序序列判断根,通过中序序列判断左右子树
在这里插入图片描述
如图就行啦

3.二叉树的递归遍历算法

我们可以采用递归的定义来表示二叉树的先序遍历

typedef int status;//status是为了表示一般性,实际使用时要把写status的地方换成对应的数据类型(如int,float,char等)
#define ERROR -1//便于理解,将-1定义为失败,1定义为成功
#define OK 1
status preordertravels(bitree T){
if(T->data==NULL)return OK;//如果为空节点的话就返回到上一层
else{
visit(T->data);//先访问根结点
preordertravels(T->lchild);//访问左孩子
preordertravels(T->rchild);//访问右孩子
}
}

其实这种先序遍历的递归实现,理解起来有一定难度,如果这棵树不为空,就先访问根结点,按照相同的方法访问左孩子,进入到左孩子的判断,左孩子不为空,访问左孩子的根结点,再按照同样的方法访问左孩子的左孩子,一直进行下去,直到左右孩子均为空的时候逐级返回,一直到二叉树中每一个结点都访问到了根结点,左孩子,右孩子为止,还不理解的小朋友可以画图理解一下递归
二叉树的中序遍历

typedef int status;
#define ERROR -1
#define OK 1
status inordertravels(bitree T){
if(T->data==NULL)return OK;
else{
preordertravels(T->lchild);//先访问左孩子
visit(T->data);//访问根结点
preordertravels(T->rchild);//访问右孩子
}
}

二叉树的后序遍历

typedef int status;
#define ERROR -1
#define OK 1
status postordertravels(bitree T){
if(T->data==NULL)return OK;
else{
preordertravels(T->lchild);//先访问左孩子
preordertravels(T->rchild);//访问右孩子
visit(T->data);//访问根结点
}
}

对比这三种算法,如果去掉访问语句,那么这三种算法是完全一样的,访问的路径也都一样,每一个算法只是在不同的时间对这个结点进行访问
在这里插入图片描述
每一个结点都访问到了三次,如左子树结点,就有顺序为1 3 5的访问,而先序遍历是在第1次经过的时候就访问了,顺序为1,而中序遍历是在第2次经过的时候就访问了,顺序为3,而先序遍历是在第3次经过的时候就访问了,顺序为5
第一次访问 先序遍历
第二次访问 中序遍历
第三次访问 后序遍历

这时候你要问了,好像左左,左右,右左,右右只经过一次啊,其实不然,他们下面还有空结点,他们也都像根,左孩子,右孩子一样,都经过了三次

那么我们再来看一下这种递归算法的时间复杂度和空间复杂度吧
时间复杂度:访问路径就是按照顺序把二叉树都访问一遍,为O(n)
空间复杂度:按照最坏的情况来看,这棵二叉树为一棵单只树,每一次递归调用,系统都要存储调用的这个结点,系统用一个栈来实现,那么为单只树的话,系统就要开辟出栈的n个空间来每一个结点,故这种空间复杂度也是为O(n)

4.二叉树的非递归遍历算法

我们先讨论一下中序遍历
中序遍历是先先访问左子树,然后访问根结点,最后访问右子树
遇到一个根结点,先把这个根结点存储下来,访问它的左子树,不断的进行知道左子树为空,输出这个左子树为空的结点,访问它的右子树,不断的回溯,先访问的后输出,对比这个,我们很容易想到了用栈来存储这个根结点,先让根结点入栈,访问左孩子,返回的时候,根结点出栈,访问右孩子

基本思想:
1.建立一个栈
2.根结点入栈,访问左子树
3.根结点出栈,输出根结点,访问遍历右子树
C++栈的用法及栈的实现用C++栈再复习一下

#include<stack>
typedef int status;
#define ERROR -1
#define OK 1
status inordertravels(bitree T){
	stack<status>S;
	bitree p,temp;
	p=T;
	while(p||!empty(S)){//满足一个都会执行,满足p执行if,满足!empty(S),执行else
	if(p){//p!=NULL
		s.push(p);
		p=p->lchild;}
	else{
		temp=S.top();
		S.pop();
		cout<<temp<<endl;
		p=temp->lchild;}
	}
	return OK;
}

5.二叉树的层次遍历算法

层次遍历就是讲二叉树一层一层从左向右的进行遍历
在这里插入图片描述
那么这个层次遍历就是:ABFCGDEH
因为要一层一层的进行遍历,先进入一个结点,输出这个结点,进入这个结点的左孩子右孩子,先进入的先输出,我们不难想到用队列来实现这种层次遍历
C++队列传送门可以先看看复习一下,用C++的话就不需要自己写队列了

基本思路
1.建立一个队列
while(队列不为空){
2.取出队列中的一个结点并输出
2.将该结点的左右孩子入队}

#include<queue>
void levelorder(bitree T){
	bitree p;//建立一个暂存结点
	queue<int>q;//建立队列
	q.push(T);//头结点放入
	while(!q.empty()){//队列不为空一直执行
		p=q.front();//取队列的第一个元素
		q.pop();//删除第一个元素
		if(!p->lchild)//左孩子不为空入队列
			q.push(p->lchild);
		if(!p->rchild)//右孩子不为空入队列
			q.push(p->rchild);
	}
}

6.根据二叉树的前序遍历序列建立二叉树

现在我们知道一个前序序列ABCDEFG
单靠这个序列我们不可能知道这个二叉树长什么样子的
因为有很多种可能
在这里插入图片描述
那我们把一些空的地方补充上去
补充成空的结点用#表示
在这里插入图片描述
那么这样序列就变得唯一了
ABC##DE#G##F###

基本思路
1.读入一个字符
2.不为#的话建立一个结点存储这个字符
3.递归执行建立这个结点的左孩子和右孩子

status creatbitree(bitree &T){
	char ch;
	cin>>ch;//输入一个字符
	if(ch=='#'){//为#的话这个是个空节点
	 T=NULL;}
	else{
	T=(binode *)malloc(sizeof(binode));
	T->data=ch;//不为#的话,那么存储这个结点值
	creatbitree(T->lchild);//建立左子树
	creatbitree(T->rchild);//建立右子树
	}
	return OK;
}

递归真的很神奇,因为前序序列就是按照根左右的顺序来排列的,输入前序序列,不断的建立左子树,建立到空节点为止,将其赋值为NULL ,通过return OK返回到上一步建立右子树,再次这样一直执行…看一遍不懂的话再看一遍视频里面那个老师的演示,这个很重要的,递归递归!!!! 传送门传送坐标10:30开始

7.二叉树遍历算法运用

1.复制二叉树

基本思路:
依然是递归
1.如果结点空则新结点也为空,并且返回上一级
2.申请新空间,复制结点
3.递归调用复制左子树和右子树

int copybitree(bitree T,bitree &newT){
	if(T->data=NULL){
		newT->data=NULL;
		return 1;
	}
	else{
	newT=(binode*)malloc(sizeof(binode));
	//一定要记得,普通的传递一下数不需要动态申请内存
	//但是要用到data等里面的数据的时候就要动态申请内存
	newT->data=T->data;
	copybitree(T->lchild,newT->lchild);
	copybitree(T->rchild,newT->rchild);
	}
}

递归万岁!!!!

2.计算二叉树的深度

基本思路
依然是递归
若是空树深度为0
1.计算左子树的深度
2.计算右子树的深度
3.比较左右子树的深度,二叉树的深度就为最大值加一

int m,n;
int bitreedepth(bitree T){
	if(T->data=NULL)
		return 0;
	m=bitreedepth(T->lchild);
	n=bitreedepth(T->rchild);
	if(m>n)return m+1;
	else return n+1;
}

怎么说呢,不断的递归下去,一直到data=NULL为止,return返回上一级,不断的返回,有不断的递归下去,知道把整棵二叉树都遍历完全
递归万岁!!!!

3.计算二叉树的结点总个数

基本思路
如果是空树的话,返回0
1.计算结点的左子树的结点个数
2.计算结点的右子树的结点个数
3.将结点的左右子树的个数相加再加根的个数即可

int m,n;
int bitrenodecount(bitree T){
	if(T->data=NULL)//如果是空树就返回0
		return 0;
	m=bitrenodecount(T->lchild);//计算左子树的结点个数
	n=bitrenodecount(T->rchild);//计算右子树的结点个数
	else return m+n+1;//相加再加上根的个数
}
4.计算二叉树的叶子结点总个数

如果是空树的话,返回0
如果是叶子结点的话,返回1
1.计算结点的左子树的叶子结点个数
2.计算结点的右子树的叶子结点个数
3.将结点的左右子树的叶子个数相加即可

int m,n;
int bitreleadcount(bitree T){
	if(T->data=NULL)//如果是空树就返回0
		return 0;
	if(T->lchild=NULL&&T->rchild=NULL)
	m=bitreleadcount(T->lchild);//计算左子树的结点个数
	n=bitreleadcount(T->rchild);//计算右子树的结点个数
	else return m+n;//相加即为总叶子结点个数
}

5.线索二叉树

当使用二叉链表来存储一棵二叉树的时候,可以很方便的找到某个结点的左右孩子,但是我们很难找到按照结点对应的前序中序后序序列排序字母的前驱和后继,
为了解决这种情况
1.可以再遍历一次寻找其前驱和后继–浪费时间
2.再增加前驱和后继指针–浪费空间
3.我们前面知道一棵二叉树有2n个指针域,其中空存着n+1个指针域,我们不妨利用
这些空存的指针域来进行操作

那么我们规定:如果一个结点的左孩子为空,那么我们把他的左孩子指针域指向对应的排序序列的前驱字母,如果一个结点的右孩子为空,那我们把他的右孩子指针域指向对应排序序列的后继字母,左前右后

这种改变的指针域我们称之为线索
这种改变的二叉树我们称之为线索二叉树
将二叉树改变为线索二叉树的过程我们称之为线索化
在这里插入图片描述
如图所示:按照序列C后面是B,那么C的右指针域指向B,E后面是G,E的右指针域指向G,依次类推,按照前序中序后序序列的字母排列顺序不同来线索化

那么接下来问题又来了,我们让一个结点无论是否为空左右指针域都不为空,我们怎么判断这些指针是普通指针呢还是线索指针呢?

我们可以牺牲一点存储空间设置两个标志域,ltag和rtag
当ltag和rtag为0的时候 这时候是分别指向左孩子和右孩子
当ltag和rtag为1的时候,这时候它们是线索指针域
那么现在结点的结构就为:

lchildltagdatartagrchild
typedef struct bithrnode{
	elemtype data;
	int ltaf,rtag;
	struct bithrtree *lchild,*rchild;
}bithrnode,*bithrtree;

先序遍历二叉树线索化:
在这里插入图片描述
中序遍历二叉树线索化:
在这里插入图片描述
例题1:
在这里插入图片描述
这种题的一般思路就是先把二叉树按照题目给定的遍历序列把字母排序序列写出,再进行线索化即可

那么从这里我们可以看出其实序列的第一个元素H前面没有元素,所以它的lchild指针还是为空的,G元素后面没有元素,所以它的rchild指针域也还是空的,为了规范统一,我们可以再引入一个头指针

头指针里面的data域为空
头指针的ltag=0 指向根结点
头指针的rtag=1 指向该排序序列的最后一个结点元素
让空余的那两个指针域指向头结点
这样就都完成了其中的规范性
在这里插入图片描述
OK!今天的学习完成啦!!!!奥利给!!!!

5.树和森林

前面我们学习了二叉树的存储方式,有用数组存储的顺序存储方式(按照满二叉树的编号来存储对应数组的地址来存储数据),有用链表存储的链式存储方式,那今天我们来学习一下更为普遍的树的存储方式

1.普通树的存储表示
A.双亲表示法

定义结构数组存放树的结点,每个结点包含两个域,一个是本身的数值域,另外一个是该结点双亲在数组中存放的位置下标

Arraydataparent
0R-1
1A0
2B0
3C0
4D1
5E1
6F3
7G6
8H6
9K6

那么按照这样数组的存储方式,我们就可以很清晰的画出这棵树了
在这里插入图片描述
像这种存储方式的特点就是:找到双亲很容易,但是要找到孩子结点的时候,需要遍历整个数组来看每个结点的双亲结点是否为这个结点的数组下标

C语言类型描述:

typedef struct PTnode{//单个结点的数值域和双亲域
	int data;
	int parent;
}PTnode;
#define maxtreesize 100
typedef strcut{
	PTnode nodes[maxtreesize];
	int r,n;//分别记录根结点的位置和结点的总个数
}PTtree,*PTptr;
B.孩子表示法

为了解决上面那个寻找孩子困难的问题,我们不妨采用把结点的所有孩子按照从左往右的顺序连成单链表(叶子结点的单链表为空),这些链表的头指针又和每一个结点的数据元素共同组成顺序表,这样一来,寻找孩子结点就变得很容易了,只需要从顺序表中每一个结点后面相连的单链表,孩子结点就都知道了
在这里插入图片描述
按照这样的存储方式,r(根结点)=4,n=10,这棵树我们也可以一目了然的知道长什么样子了
在这里插入图片描述
C语言类型描述:

#define maxtreesize 100
typedef struct CTnode{//定义单链表孩子格式
	int child;
	struct CTnode *next;
}*childptr;
typedef struct{//定义每一个结点的数值域和第一个孩子的位置
	elemtype data;
	childptr firstchild;
}CTbox;
typedef strcut{//按照每一个结点的定义方式组成顺序表
	CTbox nodes[maxtreesize];
	int r,n;//分别记录根结点的位置和结点的总个数
}CTtree,*CTptr;

按照这样定义以后
数组第一个结点的数值域——>第一个孩子的数组下标——>下一个孩子数组下标…
数组第二个结点的数值域——>第一个孩子的数组下标——>下一个孩子数组下标…
数组第三个结点的数值域——>第一个孩子的数组下标——>下一个孩子数组下标…
数组第四个结点的数值域——>第一个孩子的数组下标——>下一个孩子数组下标…

总共就构成了一个顺序表的形式

但是这种孩子表示法又产生了一个缺点,就是寻找双亲很困难,要找结点的双亲要遍历顺序表找到某个结点下面对应的孩子正好是这个结点的话才能找到,那我们可以试试把第一种方法和第二种方法结合起来,在孩子表示法的基础上面,顺序表再增加一个对应双亲结点数组下标的数值域,记录每一个结点的双亲,这样一来,对于任意的一个结点,我们寻找它的孩子和双亲都变得很容易

C.孩子兄弟表示法(二叉链表表示法)

利用二叉链表作为树的存储结构,里面总共有三个域,第一个是一样的数值域,第二个第三个分别是指向第一个孩子结点和指向下一个兄弟

typedef struct csnode{
	elemtype data;
	struct csnode *firstchild,*nextbro;
}csnode,*cstree;

在这里插入图片描述
那么一棵树就可以通过这样存储在二叉链表上面
在这里插入图片描述
找孩子结点只需要向左移动一位,向右就全部都是孩子结点
找兄弟结点只需要向右移动全部就是兄弟结点了

2.树和二叉树的转换

我们知道树可以通过二叉链表的形式来存储,而二叉树的链式存储结构和二叉链表也基本类似,那么我们就可以通过二叉链表的形式来进行对数和二叉树的转换,如果成功的转换成功的话,我们就知道可以利用二叉树的算法来对树进行操作

前面我们知道对于任何的一棵树来讲,如果要存储在二叉链表上面,它的左孩子是第一个孩子,右孩子是第一个兄弟结点,那么我们按照这样的规律就来对树进行转换

1.树转换为二叉树

在这里插入图片描述
通过这样的形式我们就可以对树进行转化,一棵树有且仅有一棵二叉树与之对应
那么接下来我们观察树和其对应的二叉树

我们不难看出这样规律:
在所有的兄弟结点之间连接一条线
每一个结点出来第一个左孩子之外,去除与其他的孩子连线
将整颗转换的树旋转45度

兄弟相连留长子在这里插入图片描述

2.二叉树转化为树

那么这样的相逆的转化,我们通过上面操作的逆运算即可完成
首先找到一个结点的左孩子,将左孩子的右孩子,左孩子的右孩子的右孩子…与这个结点都连一条线,然后把左孩子与左孩子的右孩子…之间的连线全部都断开
在这里插入图片描述
左孩右右连双亲,去掉原来右孩线

3.森林和二叉树的转换
1.森林转换为二叉树

在这里插入图片描述
先把每一棵树都转化为二叉树
再把这些二叉树的根结点全部都相连
以第一棵二叉树的根结点为轴顺时针旋转45度即可

树变二叉根相连

2.二叉树转换为森林

相逆即可
去掉全部的根结点相连的右孩子,使转换为孤立的二叉树
再对每一棵二叉树进行还原即可

去掉全部右孩线
孤立二叉再还原

4.树的遍历

树的遍历右三种方法
1.先根遍历:
树不空,先访问根结点,依次遍历各课子树
2.后根遍历
树不空,先依次遍历各棵子树,再访问根结点
3.层次遍历
树不空,自上而下从左往右的访问每一棵树的结点
在这里插入图片描述
先根遍历:ABCDE
后根遍历:BDCEA
层次遍历:ABCED

这里又有同学要问了,二叉树不是有四种遍历方式吗,怎么树的遍历少了一种中根遍历,这是因为树可能会有多个孩子,中根遍历是把树结点的位置放到中间,但是又个孩子的情况下,你不知道该放到哪个中间,所以那就彻底把这种遍历去掉比较简单

5.森林的遍历

我们可以将森林看做三部分
1.森林中的第一棵树的结点
2.森林中的第一棵树的子树
3.森林中其他的树

按照这些排序方式,我们又可以把森林的遍历分作俩种情况
1.先序遍历:
按照从左往右的顺序,先根遍历森林中的每一棵树
2.中序遍历:
按照从左往右的顺序,后跟遍历森林中的每一棵树
在这里插入图片描述
按照先序遍历的方式:ABCDEFGHIJ
按照中序遍历的方式:BCDAFEHJIG

6.哈夫曼树(最优二叉树)

哈夫曼是一个在计算机领域方面杰出的领军者
对于不同的判断树,会有不同的效率
而哈夫曼树就是在所有判断树中效率最高的一棵树
又被称之为最优二叉树

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值