正在学习,未完待续······
文章目录
逻辑结构:集合结构,线性结构,树结构,图结构
物理结构:顺序存储结构,链式存储结构
一、线性表
二、栈和队列
1.栈
2.队列
三、字符串
四、树
1.二叉树性质和存储结构
1.性质:
在二叉树的第i层上至少有一个结点,至多有2的i-1次方个结点
深度为k的二叉树至少有k个结点,至多有2的k次方-1个结点
2.满二叉树和完全二叉树:
对满二叉树结点位置编号:自上而下,自左而右,每个结点位置都有元素
在满二叉树中,从最后一个结点开始连续去掉任意个结点,即为完全二叉树
完全二叉树的任意结点,若其右子树最大层次为i,则其左子树的最大层次必为i或i+1
3.存储结构:
顺序存储:(缺点:尤其对于右单支树会很浪费空间,因此适合满二叉树和完全二叉树)
链式存储:(左指针域,数据元素,右指针域)
在n个结点的二叉链表中,必有2n个指针域,除根结点外,每个结点只有一个双亲,所以共有n-1个指针域存放指针,因此有n+1个空指针域
三叉链表(左指针域,数据元素,双亲指针,右指针域)
2.遍历二叉树和线索二叉树
1.DLR先序遍历、LDR中序遍历、LRD后序遍历
typedef struct Ttree
{
char data;
struct Ttree* PL;
struct Ttree* PR;
}Ttree;
void pretraversal(BTtree tree)
{
if (tree!=NULL)
{
cout << tree->data << endl;
if (tree->PL!=NULL)
pretraversal(tree->PL);
if (tree->PR!=NULL)
pretraversal(tree->PR);
}
}
void intraversal(BTtree tree)
{
if (tree!=NULL)
{
if (tree->PL!=NULL)
intraversal(tree->PL);
cout << tree->data << endl;
if (tree->PR!=NULL)
intraversal(tree->PR);
}
}
void posttraversal(BTtree tree)
{
if (tree!=NULL)
{
if (tree->PL!=NULL)
posttraversal(tree->PL);
if (tree->PR!=NULL)
posttraversal(tree->PR);
cout << tree->data << endl;
}
}
三种遍历如果去掉输出部分在本质上是相同的,都是按照相同的路径,只不过访问结点数据的时机不同,其时间复杂度和空间复杂度均为O(n)
可以通过前序和中序或中序和后序来确定唯一的二叉树结构
中序遍历的栈算法:
根结点入栈,访问其左子树,遇到根结点入栈,直到其左子树为空,根结点出栈,然后访问其右子树
void InorderTraverse(BiTree T)
{
BiTree p;
InitStackEmpty(S);
p=T;
if(p)
{
Push(S,p);
p=p->lchild;
}
else
{
Pop(S,q);
cout << q->data;
p=q->rchild;
}
}
二叉树的层次遍历:(队列实现)
根结点入队,然后出队,如果它有孩子,则同时令左右孩子依次入队,以此类推,不断从一端出队同时孩子另一端入队
void LevelOrder(BTNode *b)
{
BTNode *p;
SqQueue *qu;
InitQueue(qu);
enQueue(qu,b);
while (!QueueEmpty(qu))
{
deQueue(qu,p);
cout << p->data;
if (p->lchild!=NULL)
enQueue(qu,p->lchild)
if (p->rchild!=NULL)
enQueue(qu,p->rchild)
}
}
2.遍历算法的应用
二叉树的建立:
//键盘输入时空结点不能省略,用'#'表示
void CreatBiTree(BiTree &T)
{
cin >> ch;
if (ch=='#')
T=NULL;
else
{
if (!(T=(BiTNode *)malloc(sizeof(BiTNode))))
exit(OVERFLOW);
T->data=ch;
CreatBitree(T->lchild);
CreatBitree(T->rchild);
}
}
复制二叉树:
void Copy(BiTree T, BiTree &NewT)
{
if (T==NULL)
NewT=NULL;
else
{
NewT=new BiTNode;
NewT->data=T->data;
Copy(T->lchild, NewT->lchild);
Copy(T->rchild, NewT->rchild);
}
}
计算二叉树深度:
int Depth(BiTree T)
{
if (T==NULL)
return 0;
else
{
m=Depth(T->lchild);
n=Depth(T->rchild);
if (m>n)
return (m+1);
else
return (n+1);
}
}
计算二叉树结点总数:
(左子树结点数+右子树结点数+1)
int NodeCount(BiTree T)
{
if (T==NULL)
return 0;
else
return NodeCount(T->lchild)+(T->rchild)+1;
}
计算叶子结点数:
int LeadCount(BiTree T)
{
if (T==NULL)
return 0;
if (T->lchild==NULL&&T->rchild==NULL)
return 1;
else
return NodeCount(T->lchild)+(T->rchild);
}
3.线索二叉树:
利用二叉树中n+1个空指针域,若结点的左指针域为空则将指针指向其前驱,若右空则指向其后继,这些指针称为线索,同时增加两个标志域,若为0表示指针指向孩子,为1则指向前驱或后继(这里的前驱或后继为先序,中序,后序序列中各结点的前驱和后继)
3.树与森林
1.树的存储:
双亲表示法:
孩子表示法:
孩子兄弟表示法(二叉链表表示法)
从根节点开始其左指针域指向从左往右第一个孩子,右指针域指向其兄弟
该方法用于实现树和二叉树的转换
森林传化二叉树:
先把每棵树转化为二叉树,在将其他树的根结点当作第一个树根结点的兄弟
2.树的遍历:
先根遍历:若树不空,先访问根结点,然后依次先根遍历各棵子树
后根遍历:若树不空,先依次后根遍历各棵子树,然后访问根结点
层次遍历:若树不空,则自上而下自左而右访问各个结点
3.森林的遍历:
先序遍历:依次从左至右对森林中每一棵树进行先根遍历
中序遍历:依次从左至右对森林中每一棵树进行后根遍历
4.哈夫曼树及应用
树的带权路径长度:树中所有叶子结点的带权路径长度之和,记作WPL
1.哈夫曼算法:每次选取权值最小的两个结点构造树
包含n棵树的森林要经过n-1次合并才能形成哈夫曼树,共产生n-1个新结点
包含n个叶子结点的哈夫曼树共有2n-1个结点
void CreatHuffmanTree(HuffmanTree HT, int n)
{//构造哈夫曼树
if (n<=1)
return;
m=2*n-1;//数组共2n-1个元素
HT=new HTNode[m+1];//0号单元未用,HT[m]表示根结点
for (i=1;i<=m;i++)//将2n-1个元素的lch,rch,parent置为0
{
HT[i].lch=0;
HT[i].rch=0;
HT[i].parent=0;
}
for (i=1;i<=n;++i)输入前n个元素的weight值
cin >> HT[i].weight;//初始化结束,下面开始建立哈夫曼树
for (i=n+1;i<=m;i++)//合并产生n-1个结点--构造树
{
Select(HT,i-1,s1,s2);//在HT[k](1<=k<=i-1)中选择两个其双亲域为0,且权值最小的结点,并返回它们在HT中的序号s1和s2
HT[s1].parent=i;
HT[s2].parent=i;//从F中删除s1,s2
HT[i].lch=s1;
HT[i].rch=s2;//s1,s2分别作为i的左右孩子
HT[i].weight=HT[s1].weight+HT[s2].weight;//i的权值为左右孩子权值之和
}
}
2.哈夫曼编码
统计字符集中每个字符在电文中出现的平均概率(概率越大要求编码越短)
利用哈夫曼树的特点:权越大的叶子离根越近;将每个字符的概率值作为权值,构造哈夫曼树。则概率越大的结点,路径越短
哈夫曼树的每个结点的左分支标0,右分支标1,把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码
为什么哈夫曼编码能够保证是前缀编码?
因为没有一片叶子是另一片叶子的祖先,所以每个叶结点的编码就不可能是其他叶结点编码的前缀
为什么哈夫曼编码能够保证字符编码总长最短?
因为哈夫曼树的带权路径长度最短,故字符编码的总长最短
void CreatHuffmanCode(HuffmanTree HT, HuffmanCode &HC, int n)
{//从叶子到根逆向求每个字符的哈夫曼编码,存储在编码表HC中
HC=new char *[n+1];//分配n个字符编码的头指针矢量
cd=new char [n];//分配临时存放编码的动态数组空间
cd[n-1]='\0';//编码结束符
for (i=1;i<=n;++i)//逐个字符求哈夫曼编码
{
start=n-1;
c=i;
f=HT[i].parent;
while (f!=0)//从叶子结点开始向上回溯,直到根结点
{
--start;//回溯一次start向前指一个位置
if (HT[f].lchild==c)
cd[start]='0';//结点c是f的左孩子,则生成代码0
else
cd[start]='1';//结点c是f的右孩子,则生成代码1
c=f;
f=HT[f].parent; //继续向上回溯
}//求出第i个字符的编码
HC[i]=new char [n-start];//为第i个字符串编码分配空间
strcpy(HC[i],&cd[start]);//将求得的编码从临时空间cd复制到HC的当前行中
}
delete cd;//释放临时空间
}
文件的编码和解码:
编码:
输入各字符及其权值
构造哈夫曼树–HT[i]
进行哈夫曼编码–HC[i]
查HC[i],得到各字符的哈夫曼编码
解码:
构造哈夫曼树
依次读入二进制码
读入0,则走向左孩子;读入1,则走向右孩子
一旦到达某叶子时,即可译出字符
然后再从根出发继续译码,直到结束
5.树的操作
五.图
1.定义和基本术语
1.G=(V,E) //Graph= (Vertex, Edge)
V: 顶点(数据元素)的有穷非空集合;
E: 边的有穷集合
2.有向图:弧
无向图:边
完全图:任意两个点都有一条边相连
稀疏图:有很少边或弧的图(e<nlogn)
稠密图:有较多边或弧的图
网:边/弧带权的图
邻接:有边/弧相连的两个顶点的关系
(Vi,Vj):互为邻接点
<Vi,Vj>:Vi邻接到Vj,Vj邻接于Vi
关联(依附):边/弧与顶点之间的关系
(Vi,Vj)/<Vi,Vj>:该边/弧关联于Vi和Vj
3.顶点的度:与该顶点相关联的边的数目,记作TD(v)
在有向图中,顶点的度等于该顶点的入度与出度之和
顶点v的入度是以v为终点的有向边的条数,记作ID(v)
顶点v的出度是以v为始点的有向边的条数,记作OD(v)
如果一个图中只有一个顶点的入度为0,其余顶点入度都为1,则该图形状一定为有向树
2.图的存储结构
图没有顺序存储结构,但可以借助二维数组来表示元素间的关系:数组表示法(邻接矩阵)
链式存储结构:多重链表:邻接表、邻接多重表、十字链表
邻接矩阵:(一个一维数组存储顶点信息,一个二维数组存储邻接矩阵)
1.无向图邻接矩阵:如果i, j之间有边,则二维数组a[i][j]=1,否则=0
特点:为对称矩阵,且对角线上均为0(完全图的邻接矩阵对角元素为0,其余均为1)
2.有向图邻接矩阵:第i行含义:出度边,第i列含义:入度边
3.网(有权图)的邻接矩阵:有弧即记录权值,无弧记为无穷大
4.无向图的算法实现:
#define MVNum 100
typedef char VerTexType;
typedef int ArcType;
typedef struct
{
VerTexType ves[MVNum];
ArcType arcs[MVNum][MVNum];
int vexnum,arcnum;
}AMGraph;
Status CreateUDN(AMGraph &G)
{
cin >> G.vexnum >> G.arcnum;
for (int i=0;i<G.vexnum;i++)
cin >> G.ves[i];
for (int i=0;i<G.vexnum;i++)
for (int j=0;j<G.vexnum;j++)
G.arcs[i][j]=MaxInt;//权值均初始化为无穷大
int v1, v2, w;
for (int k=0;k<G.arcnum;k++)
cin >> v1 >> v2 >> w;//输入一条边所依附的两个bian顶点以及边的权值
i=LocateVex(G,v1);
j=LocateVex(G,v2);//确定v1,v2在顶点表中的位置
G.arcs[i][j]=w;
G.arcs[j][i]=w; //对称边权值相等
return OK;
}
邻接表表示法:(链式)
按照编号顺序将顶点数据存储在一维数组里,关联同一顶点的边(或以该顶点为尾的弧)用线性链表存储
无向图:邻接表不唯一,如果有n个顶点,e条边,则邻接表需要n个头结点(数组元素),2e个表结点(每条边被存储两次),适宜于存储稀疏图
有向图:顶点Vi的出度为第i个链表中结点个数(不包括头结点),入度需要遍历整个结点域