当前位置:   article > 正文

数据结构(线性表&树&图)_树状图,线性表

树状图,线性表

一、线性结构

1.1线性表

 定义:线性表是n个元素的有限序列,通常表示为{a1,a2,...,an},对于非空线性表有如下几个特点:

1)存在唯一的一个被称为"第一个"(“最后一个”)的元素; 2)除第一个元素序列中的每一个元素都有唯一前驱;

3)除最后一个元素序列中的每一个元素都有唯一后继;

线性表的存储结构(顺序存储和链式存储)

顺序存储特点:逻辑上相邻的元素物理上也相连;

已知Loc(ai)和数据元素占有的字节数L,则 Loc(aj)=Loc(ai)+(j-i)*L

顺序存储的优点/缺点:实现随机存取(时间复杂度O(1)),插入/删除时间复杂度较高(时间复杂度O(n));

例:长度为n的线性表(顺序),插入/删除第i个位置上的元素需要移动的元素个数

在i位置插入元素,需要移动n-i+1个元素,删除第i个元素需要移动n-i个元素;

链式存储特点:逻辑上相邻的元素物理上未必相邻;

链式存储的优点/缺点:在已知插入和删除位置的条件下效率高(时间复杂度O(1)),不用移动大量元素;

但不支持随机存储,查找效率较低(时间复杂度O(n))

链式存储结点的基本构成:数据域&指针域;

带有头结点的线性表(链式)优点:简化操作实现插入和删除操作的统一;

例子:已知在结点p后插入一个新结点q的操作&删除结点p的后继结点操作;

插入s->next=p->next   p->next=s;删除p->next=p->next->next

线性表的初始化/查找/删除/插入实现:(顺序线性表)

  1. typedef struct List{
  2. dataType * data;
  3. int fullsize;
  4. int length;
  5. }List;
  6. #顺序线性表的定义
  7. void Init(List * list,int size){
  8. list->data=(dataType *)malloc(sizeof(dataType)*size);
  9. list->fullsize=size; #线性表容量
  10. list->length=0; #线性表长度
  11. }
  12. #顺序线性表的初始化
  13. int Search(List list,dataType data){
  14. for (int i=0;i<list.length;i++){
  15. if(list.data[i]==data){
  16. return i+1;
  17. }
  18. }
  19. return 0;
  20. }
  21. #顺序线性表的插入
  22. void Insert(List * list,int loc,dataType data){
  23. if(loc>=list->fullsize){
  24. return ; #满值操作
  25. }
  26. if(loc>list->length+1|loc<=0){
  27. return ;//位置检测
  28. }
  29. for(int i=list->length;i>=loc;i--){
  30. list->data[i] =list->data[i-1]; #元素后移
  31. }
  32. list->data[loc-1]=data;//插入数据
  33. list->length++; #元素个数(表长)+1
  34. }
  35. #顺序线性表删除
  36. void Delete(List * list,int loc){
  37. if(loc<=0|loc>list->length){
  38. return ;//位置检测
  39. }
  40. for(int i=loc-1;i<list->length;i++){
  41. list->data[i]=list->data[i+1];
  42. }
  43. list->length--; #元素个数(表长)-1
  44. }

除了单链表之外,还有双链表(解决了在链表中找前驱的问题)、循环链表(通过表尾找表头)等;

在循环链表中插入(删除)结点的指针修改(在p结点前插入s结点,删除p结点):

1)插入结点s:s->next=p    s->front=p->front    p->-front->next=s    p->front=s ;(优先更新新结点

2)删除结点p:p->front->next= p->next->next    p->next->front=p->front;

1.2栈和队列(特殊的线性表)

栈:先进后出;队列:先进先出;(同线性表类似也有顺序存储链式存储两种)

栈的相关操作:初始化、入栈、出栈、取栈顶元素、栈判空(满);

队列的相关操作:初始化、入队、出队、取队头元素、队列判空;

栈的应用:递归、表达式求值、括号匹配;队列的应用:打印设备输出、离散事件的计算机模拟;

初始化、入(出)队(栈)时间复杂度都为O(1)。//因此操作效率较高

循环队列的判空(满)、入队、出队的相关操作(重点

设Q.front为队头指针,Q.rear为队尾指针,MaxSize为队列的最大长度;

入(出)队操作:Q.rear=(Q.rear+ 1)%MaxSize & Q.front=(Q.front + 1)%MaxSize;

队列长度:(Q.rear - Q.front + MaxSize)%MaxSize;判空(满):Q.rear = Q.front;

( 针对满和空这个问题:牺牲一个存储单元,约定尾指针的下一个位置是头指针是满的;)

1.3串(字符串)

串(字符串)是一种特殊的线性表,其内部元素都是字符(字符的个数称之为串长,无字符称之为空串)。

子串:串中任意长度的连续字符;两个串相等要求长度和对应位置元素均相同(比较ASCII值);

串的基本操作:赋值、连接、求串长、比较、求子串等;

串的模式匹配(BF与KMP算法)

BF暴力匹配算法:基本思想从主串的第一个元素开始于模式串的第一个字符比较,若相等则继续向后比较,否则

从主串的下一个字符与模式串的第一个字符重新比较。(即匹配失败主串回到起始位置的下一个位置,模式串置1)

在最坏的情况下此匹配算法的时间复杂度为O(mn);//m和n分别为主串和模式串的长度;

  1. int BF(string s1,string s2){
  2. //s1为主串,s2为模式串
  3. int m=s1.length();
  4. int n=s2.length();
  5. int i=0,j=0;//起始匹配位置
  6. while(i<m&&j<n){
  7. if(s1[i]==s2[j]){
  8. i++;
  9. j++;//同时后移
  10. }
  11. else{
  12. i=i-j+1;
  13. j=0;
  14. }
  15. }
  16. if(j>=n){
  17. return i-j+1;//匹配成功
  18. }
  19. return 0;
  20. }

KMP算法:若在匹配过程中出现与比较字符串不相等的情况不需要回溯主串指针,只需利用部分匹配的结果将模式

串尽可能的向右“滑动”(实现最大距离的跨度不在是一个一个的移动)

算法思想:当模式串的 j 位置匹配失败后说明前 j-1个字符匹配成功,若p[0]...p[k-1]与p[j-k]....p[j-1]相同则只需从k匹配即可;

例如:求模式串abababb的next数组:(最长匹配公共前后缀串的长度+1)

j0123456
模式串abababb
next[j]0112345

二、数组、矩阵&广义表

数组和广义表为线性表的推广;

数组的定义:线性表内的元素数据类型相同且结构一致(为定长线性表的推广);

数组的特点:1、数据元素的数目固定(不能改变);2、数据元素具有相同的类型和结构;

3、元素下标具有上下界且下标有序;

设每个数据元素占L个单元,m,n为数组的行数和列数,Loc(a11)为第一个元素a11的地址:

行主序公式:Loc(aij)=Loc(a11)+[ ( i - 1 )*n+( j - 1 ) ]*L

列主序公式:Loc(aij)=Loc(a11)+[ ( j - 1 )*m+( i - 1 ) ]*L;(起始地址+到目的地址的元素个数-1)*单元大小

数组的基本运算:对于给定的下标存取修改对应元素;

矩阵:上(下)三角矩阵(矩阵的对角线下(上)部分全部为0元素),对此矩阵可以进行存储压缩、

对角矩阵(对角线上含有非0数据,其他位置元素为0)、稀疏矩阵(非0元素的个数远远少于0元素的个数);

广义表:为线性表的推广,是由0个或多个单元素(子表)组成的有限序列,即为LS=(a1,a2,...,an);

广义表的长度:表内元素的个数;广义表的深度:广义表展开后括号的最大层次数;

广义表的基本操作:取表头(第一个元素,可能为表)&取表尾(去除第一个元素的剩下的部分,一定为表);

广义表的特点:1)广义表可以是多层次的结构; 2)广义表可以是一个递归表; 3)广义表可以被共享;

例题:(f,(),(e),(a,(b,c,d)))的长度是多少,深度是多少?

此广义表的长度为4(即表内元素的个数),深度为3 (每个子元素的括号匹配数加1的最大值

三、树(非线性结构)

定义:为含有n个结点的有限集合,当n=0时为空树,对于非空树有且只有一个被称为根的结点,结点可以分为m

个不同的分支,每一个分支又构成了一棵树(若分支为2则为二叉树的定义)。//  显然树的定义时递归的有层次结构;

有关树的基本概念:

1)双亲/孩子/兄弟:结点的子树为该结点的孩子,此结点为子树结点的双亲,具有同一双亲的结点互为兄弟;

2)结点的度:结点的子树的个数;

3)叶结点/终端结点:度数为0的结点;

4)内部结点:度不为0的结点,除了叶子结点之外的其他结点;

5)结点的层次/高度:根为第一层、根的孩子为第二层.....以此类推(树的最大层次数称之为高度);

6)有(无)序树:子树不可以交换(有顺序)的称之为有序树,反之为无序树;(二叉树为有序树

二叉树的性质(重点

1)在二叉树的第 i 层最多有2^( i - 1)个结点;

2)高度为k的二叉树最多有 2^k - 1个结点;

3)终端结点的个数等于度数为2的结点个数+1;(n0=n2 + 1)

4)具有n个结点的完全二叉树深度为log2n下取整+1;

5)对于有n个结点的完全二叉树所有结点按照层次自上而下,自左到右进行编号则编号有一下规则:

若 i=1,则为二叉树的根;i>1,则其双亲为 i/2下取整;
若2i>n,则i结点没有左孩子,反之左孩子为2i
若2i+1>n,则i结点没有右孩子,反之右孩子为2i+1

二叉树的存储结构(顺序存储结构,链式存储结构)

 顺序:用一组连续的存储结点存储二叉树的结点根据其下标之间的关系确定双亲和孩子结点;

在最坏的情况下,深度为k的二叉树所要的存储空间为2^k - 1个。显然空间复杂度较高;

链式:结点包含三个部分数据域、左指针域、右指针域;

  1. typedef struct BiTree{ //链式二叉树的定义
  2. DataType data; //数据域
  3. struct BiTree * left; //左指针域
  4. struct BiTree * right; //右指针域
  5. }BiTreeNode,*BiTree;

二叉树的遍历(前序、中序、后序、层次遍历)

前序遍历:根左右;中序遍历:左根右;后序遍历:左右根;层次遍历:上到下&左到右(需用到队列);

这些遍历算法的时间复杂度都为O(n),知道中序和其他任意一种遍历结果可唯一确定一颗二叉树;

  1. void Pre(BiTree root){//前序遍历
  2. if(root){
  3. cout<<root<<endl;//访问根结点
  4. Pre(root->left);
  5. Pre(root->right);
  6. }
  7. }
  8. void Order(BiTree root){//中序遍历
  9. if(root){
  10. Pre(root->left);
  11. cout<<root<<endl;//访问根结点
  12. Pre(root->right);
  13. }
  14. }
  15. void Last(BiTree root){//后序遍历
  16. if(root){
  17. Pre(root->left);
  18. Pre(root->right);
  19. cout<<root<<endl;//访问根结点
  20. }
  21. }
  22. void Level(BiTree root){//树的层次遍历
  23. BiTree p;
  24. InitQuee(Q);//初始化队列
  25. EnQueue(Q,root);
  26. while(!Empty(Q)){
  27. p=DeQueue(Q);//出队
  28. cout<<p<<endl;//遍历结点
  29. if(p->left){
  30. EnQueue(Q,p->left);//左孩子入队
  31. }
  32. if(p->right){
  33. EnQueue(Q,p->right);//右孩子入队
  34. }
  35. }
  36. }

要求已知中序和任意一种遍历方式还原二叉树,并能够求解另外的两种遍历方式; 

线索二叉树

定义:二叉树遍历的实质是对一个非线性结构进行线性化的过程,使得每一个结点(除第一个和最后一个结点)都可以

找到前驱和后继,在链式存储中这种关系不能经过一次遍历找到前驱(后继),引入线索二叉树就可以解决此问题。

利用链式存储n个结点的空指针域个数为n+1,利用这些空指针域来存储这些结点的前驱(后继)。

Itaglchilddatarchildrtag

标志位为1的情况下,左指向直接前驱,右指向直接后继;(反之指向其左右孩子)

这棵树的中序遍历为:BCADE,根据左前驱(右后继),B的前驱为NULL,C的前驱为B(后继为A)

D的前驱为A,E的前驱为D(后继为NULL),充分利用n+1个空指针域方便找到结点的前驱和后继

最优二叉树(哈弗曼树):带权路径长度最小的树(所有叶结点data乘以层次数之和);

  1. 哈夫曼树构造的过程:
  2. 1)根据给定的n个权值,构造n颗只有根结点的二叉树,这n颗二叉树构成一个森林。
  3. 2)在森林中选取两颗根结点最小的树作为左右子树构造一颗新的二叉树。且这颗新二叉树的根结点权值为
  4. 左右子树权值之和。
  5. 3)在森林中删除这两颗二叉树,同时将一颗新的二叉树加入其中。
  6. 4)重复2和3步骤直到森林中只有一颗树为止。

根据左0右1的方式对每个分支赋予标识,这样每一个叶子结点都有唯一的01码标识(哈夫曼编码)

哈夫曼编码是前缀编码(唯一);哈夫曼编码是最优前缀编码。

树和森林(树的存储&树与森林与二叉树的相互转化)

树的存储结构:双亲表示法、孩子表示法、孩子兄弟表示法;

双亲表示法是采用一组连续的地址存储树【数据域,双亲域(双亲下标)】

孩子表示法是采用邻接链表的形式将其孩子顺序连接在某双亲结点的链表处;

孩子兄弟表示法(二叉链表法),其在链表中设置两个指针域一个指向其第一个孩子,另一个指向其兄弟结点;

树和森林的遍历: 

树的先根遍历:先访问树的根结点,在按照先根遍历遍历子树;(先序遍历)

树的后根遍历:先遍历各个子树,最后在访问根结点;(中序遍历)

先序遍历森林:若森林非空,访问根结点,先序遍历子树森林。

中序遍历森林:若森林非空,中序遍历子树森林,在访问根结点。

树、森林、二叉树之间的转化(重点)

1、树转化为二叉树:连接所有的兄弟结点,去除除了第一个结点之外的其他与父节点的连线,

以根为轴心顺时针旋转45度;

2、森林转化为二叉树:将森林中的每一颗树转化成二叉树。

连接新生成二叉树的每一个根结点(第一颗树的根结点为生成二叉树的树根,有指针域指向第二颗子树,依次类推);

以根为旋转轴顺时针旋转45度;

3、二叉树转化为森林:断开从根结点开始一直向右下方的所有连线,生成多颗二叉子树,

将每颗二叉子树转化成树即可构成森林;

四、图

图的基本概念

定义:图G由两部分组成,顶点集合V和边集合E,任何一个图都不能为空(即V不能为空E可以为空);

无向图:若E是无向边的集合,此图G为无向图;(边无正向和反向)

有向图:若E是有向边的集合,此图G为有向图;(边存在正向和反向)

简单图:1)不存在重复的边(无平行边)      2)不存在顶点到自身的边(无环) 注多重图的定义与之相反;

子图:若G1图中的点和边在G2图中都有则称G1为G2的子图;(反过来未必成立)

无向完全图和有向完全图:(和图中所有的顶点都产生关联的图)

                                            无向完全图边的个数为n(n-1)/2;有向完全图边的个数为n(n-1);

稠密图和稀疏图:根据边的数目来定义,边多->稠密图、边少->稀疏图;

权和网:边上给予一定的值称为该边的权值,带有权值的图称之为网;

度、入度、出度:

度是指与某一结点相关联的边的数目,称之为该结点的度;入度和出度对应的是有向图中,入度是指以某一结点为头的

边的数目,出度是指以某一结点为尾的边的数目;

性质:1)图中所有顶点的度数总和等于边数目的2倍;//一条边提供2度

           2)对于有向图中入度等于出度等于边的数目;

连通、连通图、连通分量:

1)图中任意两个结点之间都存在路径,则此图是连通的,该图称为连通图;

2)连通分量:图中的极大连通子图;

总结:存有路径即连通、全部结点都有路径为连通图、极大连通子图为连通分量;

强连通图和强连通分量:(了解)

针对有向图的连通性,图中v到w和w到v之间都存在有向路径,则称此两结点是强连通的,图中任意顶点都是强连通的

则此图称为强连通图,极大强连通子图称之为强连通分量;

生成树与生成森林:生成树是指使图中全部顶点相互连通的极小连通子图,对于有n个顶点的图,使其连通的最小连通

子图要有n-1条边;如果图本身就是非连通的,则由每一个连通分量生成的各自的生成树称之为生成森林;

路径:是指在连通图中从起点到终点途中所经过的有限顶点序列集合;

路径长度:是指从起点到终点所经历的边的数目;

简单回路或者环:起点和终点相同的路径称之为环或者回路;

简单路径:路径中的顶点不重复出现;简单回路或简单环:除了起点和终点,其余各个顶点都不重复;

总结:极大连通子图对应的是连通分量,极小连通子图对应的是生成树;

☆☆☆重点 图的两种存取方式(邻接矩阵和邻接链表)

邻接矩阵:(采用二维数组存取即可)

对于无向图中:数组内元素A[i][j]为1表示存在由 i 到 j 的边,如果为0表示不存在由 i 到 j 的边;

性质:         1)无向图的邻接矩阵是对称矩阵;

                    2)无向图中非零元素个数为该图边的数目;

                    3)图中顶点 i 对应的度的大小为横向(纵向)非零元素的个数;

对于有向图(网)中:数组内元素A[i][j]为k(k!=∞)表示存在由 i 到 j 的边,且该边权值为k;反之不存在由 i 到 j 的边;

性质:         1)有向图未必是对称矩阵;

                    2)有向图中非零且非无穷元素个数为该图边的数目;

                    3)图中顶点 i 对应的出度为横向非零且非无穷元素个数;图中顶点 i 对应的入度为纵向非零且非无穷元素个数;

生成邻接矩阵的时间复杂度为O(n^2);对于稠密图可以采用邻接矩阵存取;

邻接链表:(由顶点表和边表组成)

顶点表存有顶点信息,边表存有该顶点直接相关联的顶点;

性质:          1)边表的结点数目,即为该图中的边的条数;

                     2)对于有向图中获取某一顶点的出度只需计算与之相关联的边表的结点个数即可,时间复杂度为O(n);

                           但要想知道图中某一顶点的入度需要访问整个邻接链表,时间复杂度为O(n+e);

                     3)邻接链表的生成与算法有关,生成的邻接表并不唯一;

生成邻接链表的时间复杂度为O(n+e);对于稀疏图常采用邻接链表;

其他存储方式:十字链表(有向图),邻接多重表(无向图);

对于图的邻接矩阵表示其表示是唯一的,而对于邻接链表,由于算法的不同其表示方法不唯一;

☆☆☆图的两种遍历方式:(深度遍历、广度遍历)

深度优先遍历过程:(前序遍历)

1)从某一个结点出发,访问此结点并将其标志为已访问;

2)找出刚访问顶点的第一个未被访问的邻接点,访问此邻接点、重复此步骤直至访问过的顶点不存在未被访问的邻接点为止;

3)回溯至前一个访问过的邻接点位置,继续访问未被访问的邻接点;

4)重复步骤2和3直到图中所有顶点均被访问为止;

由深度优先搜索构成的树称之为深度优先生成树;

广度优先遍历过程:(层次遍历)

1)从某一个结点出发,访问此结点并将其标志为已访问;

2)访问与刚访问的顶点相关联的所有邻接点,并将这些点加入一个队列中去,并将这些点标志为已访问;

3)(取队头顶点)分别从这些点出发,在访问与这些顶点相关联的且未被访问的邻接点,

将这些点加入到队列尾部,并将这些点标志为已访问;

4)重复2和3步骤直至队列为空;

由广度优先搜索构成的树称之为广度优先生成树;

无向图的遍历过程中如果从一个顶点出发一次遍历就可以访问全部结点则此图是连通的,反之就是非连通的;调用遍历方法的

次数即为连通分量的个数;

☆☆☆最小生成树的两种算法(普里姆算法、克鲁思卡尔算法)

最小生成树的定义:连通整个图代价之和最小的那一颗生成树(极小连通子图)

普里姆算法(Prim):以点为主

思想:构建两个点集合A与B,其中A为已确定为生成树的部分,B为未确定待添加的部分;

找出有集合B内某一点,使得此点到集合A的路径长度最小,将集合B中的这一点加入到集合A中,

重复此步骤直到集合B中不存在结点为止;对于含有n个结点的图只需重复n-1次即可;

克鲁思卡尔算法(Kruskal):以边为主

思想:将图中所有的边从小到大依次排序,选出其中边最小的将关联的结点置于一个连通分量中。

之后在不同的连通分量之间选取最小边,重置连通分量,直到所有的结点都位于一个连通分量时为止;

☆☆☆最短路径的两种算法(迪杰斯特拉算法,弗洛伊德算法)

迪杰斯特拉算法思想:最短路径要么直接到达,要么经过最短中转结点到达;(单源点的最短路径)

弗洛伊德算法思想:允许通过中转结点访问不断地修改整个矩阵的值当经历n-1次循环后所得到的矩阵

就是各个点相互到达的最短路径矩阵;Picture[ i ][ j ]=Picture[ i ][ k ]+Picture[ k ][ j ]; 

(对于求整个图中的所有点的最短路径两个算法时间复杂度均为O(N^3),单点时间复杂度为O(N^2))

  1. for(int k=0;k<n;k++){ //弗洛伊德算法
  2. for(int i=0;i<n;i++){
  3. for(int j=0;j<n;j++){
  4. if(Pic[i][j]>Pic[i][k]+Pic[k][j]){
  5. Pic[i][j]=Pic[i][k]+Pic[k][j];
  6. //依次经历一个、两个、三个.....结点逐步修改矩阵的值
  7. //从i到j的路径,经过k结点到达
  8. }
  9. }
  10. }

终点12345
210(1-2)truetruetruetrue
3——60(1-2-3)50(1-4-3)truetrue
430(1-4)30(1-4)truetruetrue
5100(1-5)100(1-5)90(1-4-5)60(1-4-3-5)true
路径(1-2)(1-4)(1-4-3)(1-4-3-5)(1-4-3-5)

☆☆☆重点  关键路径(要用到拓扑排序,AOE网)

AOE网:边表示活动,顶点表示事件的图,边上的权值通常表示该活动的持续时间;

性质:       1)在AOE网中有且只有一个入度为0的点,该点称为源点,有且只有一个出度为0的点,该点称为汇点;

                  2)从源点到汇点的最长路径称为关键路径,一个AOE网中的关键路径可能有多条,位于关键路径上的活动

                  称为关键活动,关键活动会影响工程的提前或拖延;

经常考察的四个定义:

1)事件vi的最早开始时间;

进入事件vi的每一个活动都完成事件vi才能进行;

定义ve(0)=0;ve(i)=Max{ve(k),w(k,i)}           //k点与i点直接相连;

事件vi的所有前驱结点的最早开始时间+活动<ak,ai>的权值的最大值;

2)事件vi的最迟开始时间;

要求:vi的发生不会延误vi后面的事件发生的最迟时间;

为了不拖延工期,vi的最迟发生时间不得迟与后继事件vk的最迟发生时间减去活动<vi,vk>的权值;

定义vl(n-1)=ve(n-1);//活动最后总时间

vl(i)=Min{vl(k)-w(i,k)}      //i与k直接相连

活动vi进行完,刚好能进行下面的活动,使得最后完成的工作不受影响;

3)活动ai=<vj,vk>的最早开始时间;

当事件vj发生了与活动相关联的边既可以进行;

活动a的最早发生时间与事件vj的最早开始时间相同,即e(ai)=ve(j);

4)活动ai=<vj,vk>的最迟开始时间;

要求:活动ai的开始时间保证不延误事件vk的最迟发生时间;

活动ai=<vi,vk>不得延误vk的最迟发生时间,所以活动的最迟开始时间等于事件vk的最迟开始时间减去

活动<vi,vk>的权值。即:l(i)=vl(k)-w{j,k};

总结:对于最早开始时间都采用顺推的方式进行,对于最迟开始时间均采用逆推的方式进行;

常见时间复杂度问题:(邻接矩阵、邻接链表)

求边的数目:邻接矩阵O(n^2),邻接链表对于有向图O(n+e),对于无向图O(n+2e);

创建图的复杂度:邻接矩阵O(n^2),邻接链表O(n+e);

求顶点的度:邻接矩阵的出入度O(n);邻接链表无向图最坏O(n),有向图中出度O(n),入度时间复杂度O(n+e)
 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/427317
推荐阅读
相关标签
  

闽ICP备14008679号