数据结构学习笔记(基础)

绪论

数据结构三要素(数据的基本单位是数据元素,数据元素可由若干个数据项组成,一个数据项是构成数据元素的不可分割的最小单位)

  1. 数据:指的是能被计算机识别、存储和加工处理的信息载体(如 Word 文档)
  2. 数据元素(元素/结点/顶点/记录):要准确地描述一个对象,其由若干数据项组成(数据项是构成数据元素的不可分割的最小单位)
  3. 数据结构:指的是数据之间的相互关系,即数据的组织形式

表数据结构(行:数据元素,列:数据项)

学号 姓名 性别 专业课 英语 数学
20220505 李逍遥 95 88 87
20220506 张怀秋 92 90 84
20220507 王之焕 87 89 88
20220508 胡月 90 93 96

(注意!学号下的每一行都是一条记录、数据元素。而每一列如学号、姓名等等都算一个数据项,它是用来描述数据元素的。最后这个表格本身就算一种数据结构)

  • 数据对象:具有相同性质的数据元素的集合,是数据的一个子集(如上表的所有男生都可以根据性别算作一类数据对象)
  • 数据类型:看作一个值的集合和定义在此集合上的一组操作的总称,数据类型包括:原子类型、结构类型、抽象数据类型
  • 直接前趋:对表中的任意结点,与它相邻且位于它的前面的结点称为该结点的直接前趋。比如张怀秋的直接前趋是李逍遥。
  • 直接后继:对表中的任意结点,与它相邻且位于它的后面的结点称为该结点的直接后继。比如张怀秋的直接后继是王之焕。

但是首结点是没有直接前趋的,而尾结点是没有直接后继的。比如李逍遥没有直接前趋,而胡月没有直接后继

数据结构学习笔记(基础)_第1张图片

(注意!广义表类似集合,它是一种可以存放集合的集合)

数据结构是相互之间存在一种或多种特定关系的数据元素的集合,它包括逻辑结构、存储结构和数据运算三方面内容

  1. 逻辑结构:即描述数据元素之间的逻辑关系,这与数据的存储结构无关,是独立于计算机的。数据的逻辑结构可以看作是从具体问题抽象出来的数学模型。
  2. 存储结构:即描述在计算机存储器内的数据元素及其联系。
  3. 数据之间的运算:即对数据施加的操作,这定义是基于数据的逻辑结构,而每种逻辑结构都有一系列的运算。如常用的检索、插入、删除、更新、排序等等运算实际上只是在抽象的数据上施加的一系列抽象的操作。

数据的逻辑结构二大分类(线性 / 非线性)

主要分为线性结构和非线性结构

A.线性结构:

若结构是非空集,则有且仅有一个开始结点和一个终端结点,而所有结点都最多只有一个直接前趋和一个直接后继。线性表、栈、队列和串等都是线性结构。

1.线性表

线性表是具有相同数据类型的 n 个数据元素的有限序列。某线性表用带头结点的循环单链表存储,头指针为 head,当 head->next->next=head 时,线性表长度可能是 0 或 1

特点:

  • 表中元素个数有限
  • 表中元素具有逻辑上的顺序
  • 表中元素即是数据元素
  • 表中元素的数据类型都相同,即每个元素都有相同大小的存储空间
  • 表中元素具有抽象性,即仅讨论元素间的逻辑关系而不考虑元素究竟想表达什么内容

线性表是一种逻辑结构,表示元素之间一对一的相邻关系。其顺序存储:顺序表(表中元素的逻辑顺序和其物理顺序相同);其链式存储:单向链表、双向链表、循环链表、静态链表。

 

地址计算:

在线性表中所有结点类型都相同,每个结点占用存储空间大小亦相同的情况下。假设每个结点占用了 c 个存储单元(单位),其中首单元地址就代表当前结点的存储地址,并设开始结点 a1 的存储地址是 LOC(a1),那么求 LOC(ai) 的存储地址的公式如下:(i 是代入参数)

LOC(ai) = LOC(a1) + (i-1)*c

2.栈

栈是限制只能在表的一端进行插入/删除操作的线性表结构。如果有 n 个不同元素进栈,那么出栈元素不同排列的个数是:C_{2n}^{2}/(n+1) 即卡特兰数通项

特点:

  • 在表中允许插入/删除的这端称为“栈顶”,另一端称为“栈底”
  • 通常把往栈顶插入元素的操作称为入栈/进栈,删除栈顶元素的操作称为出栈/退栈
  • 当栈中没有元素称为空栈

栈就像一个子弹夹:压入/弹出子弹都要在子弹夹的上端操作,压入子弹=入栈,弹出子弹=出栈。先压入子弹是最后弹出的(先入后出)

格式 描述
InitStack( S ) 构造一个空栈,即初始化
StackEmpty( S ) 判断栈是否为空,并有标志值辅助
StackFull( S ) 判断栈是否为满,并有标志值辅助
Push( S, x ) 进栈操作,在栈顶插入新元素 x
Pop( S, x ) 出栈出栈,在栈顶删除元素 x
StackTop( S ) 专门获取栈顶元素,但不删除

3.队列

简称队,队是一种只允许在一端插入、而另一端删除的运算受限的线性表。把允许删除的一端叫做队头,允许插入的一端叫做队尾。(因此若用单向链表来表示队列,就应该选用带尾指针的循环链表以方便操作统一)

队列在层序遍历的应用:根结点入队;若队空则结束遍历,否则重复下一步操作;队列中的第一个结点出队并访问,若有左孩子则左孩子入队,若有右孩子则右孩子入队,返回上一步
队列在计算机系统中的作用:第一个方面是解决主机与外部设备之间速度不匹配的问题(缓冲区),第二个方面是解决由多用户引起的资源竞争的问题(请求资源队列)

页面置换算法(此为 OS 内容)用到了队列

特点:

队列就像排队打饭一样,先来的人可以优先打完饭离开队列。

格式 描述
InitQueue( Q ) 构造一个空队列
QueueEmpty( Q ) 判断队列是否为空。为空返1,否则返0
QueueFull( Q ) 判断队列是否为满。为满返1,否则返0
EnQueue( Q, x ) 入队操作(在队尾插入一个新元素)
DeQueue( Q, x ) 出队操作(删除队头元素并返回它)
QueueFront( Q ) 取队头元素并返回它,但不删除

栈和队列具有类似的逻辑结构,但它们都属于线性表只是运算不同。

4.字符串(KMP)

串 string 是指零个或多个字符组成的有限序列。一般是 S="a1, a2, a3 ... an",其中 S 是串名,双括号里的字符序列是串值;而将串值包裹起来的双引号本身不属于串,它的作用是避免串和常数及标识符混淆。

特点:

  • 串的字符序列可以由字母、数字及其它字符组成,这字符个数等于该串的长度。
  • 若一个串是空串,即代表它不包含任何字符/长度为零。而空白串(空格串)是由一个或多个纯空格组成的串。
  • 串的逻辑结构和线性表相似,区别仅仅是串的数据对象限定为字符集
子串定位运算(串的模式匹配) VS 串的基本运算中的字符定位运算
前者侧重找子串 后者侧重找字符在主串中首次出现的位置

串匹配中一般叫主串为目标串,叫子串为模式串。若从目标串 i 点开始能够匹配模式串,就叫 i 为有效位移;否则就叫 i 为无效位移。所以串匹配问题可以简化成某给定模式串 P 在给定目标串 T 中首次出现的有效位移

B.非线性结构:

一个结点可能有多个直接前趋和直接后继。树和图等数据结构都是非线性结构。

1.1.树(度即是后代)

树是 n 个结点的有限集,当 n=0 时,即为空树

树的某个结点所拥有的子树(孩子)个数称为该结点的,而一棵树的度指的是该树所有结点的最大度。度是零的结点称为叶子(Leaf)或终端结点,度不为零的结点称为分支结点(Fork)或非终端结点。树或子树最上层那个结点称为该树或该子树的。根即父亲,子为孩子,同级互为兄弟。

若树中存在一条结点序列:k1,k2,...,kj        让 ki 是 ki+1 的父亲(1<=i

结点的层数从根起算:根的层数为 1,其余结点的层数等于其父结点的层数 +1

森林:即一些互不相交的树的集合

特点:

  1. 树是一种递归的数据结构,树作为一种逻辑结构同时也是一种分层的结构
  2. 结点的深度是从根开始自顶向下累加;结点的高度是从叶结点自底向上累加
  3. 树的分支是有向的,即从双亲指向孩子,而同一个双亲的两个孩子间不存在路径
  4. 树的结点数等于所有结点度数和 +1
  5. 度为 m 的树中第 i 层上至多有 pow(m, i-1) 个结点
  6. 高度为 h 的m叉树至多有 pow(m, h)-1 / (m-1) 个结点
  7. 树的路径长度是从树根到每个结点的路径长度的总和

 

1.2.二叉树

二叉树即父结点只有两个孩子(分为左孩子和右孩子)

特点:

  1. 二叉树是有序树,二叉树可以为空
  2. 一颗高度为 h 且含有 pow(2,h)-1 个结点的二叉树为满二叉树,每层结点为 pow(2,h-1)
  3. 完全二叉树叶子结点只可能出现在最大的两层上;若有度为1的结点只可能有一个且在左孩子上
  4. 非空二叉树上的叶子结点数等于度为 2 的结点数加 1,即 n0=n2+1
  5. 具有 n 个结点的完全二叉树的高度为 log(n+1) 或 logn+1
  6. 二叉树的遍历分为先序、中序、后序遍历
  7. 二叉树的线索化是将二叉链表中的空指针改为指向前驱或后继的线索。而前驱或后继的信息只有在遍历时才能得到,因此线索化的实质是遍历一次二叉树
  8. 引入线索二叉树的目的是加快查找结点的前驱或后驱的速度
  9. 树转二叉树:在兄弟结点之间加一连线;对每个结点只保留它与第一个孩子的连线;以树根为轴心顺时针旋转45°
  10. 二叉排序树的非递归查找算法
  11. 叉排序树的删除:若为叶节点则直接删除;若只有左或右则让子树代替;若有左和右则在右孩子找中序第一个填补

  12. 从树的根到任意结点的路径长度与该结点上权值的乘积称为该节点的带权路径长度

  13. 树中所有叶节点的带权路径长度和称为该树的带权路径长度

  14. 在二叉排序树中进行查找的效率与二叉排序树的深度有关

满二叉树 完全二叉树
一个深度为 k 有且仅有 2^k -1 个结点的二叉树,特点是二叉树的每层结点的个数都必须达到最大值 2 比较满二叉树来说,区别仅仅是允许最下层的结点都尽量集中在左边的若干位置上,而右边可以缺失结点
满二叉树隶属于完全二叉树,满二叉树是完全二叉树的真子集

1.3.二叉链表

二叉链表又叫左孩子、右兄弟表示法,可用来表示树或森林

1.4.线索二叉树

出现背景:当使用二叉链表作为二叉树的存储结构时,因为每个结点都只有指向其左、右孩子结点的指针域,所以从任意结点出发是只能直接找到它的左、右孩子。一般情况下无法直接找到某结点在遍历序列中的前趋/后继信息。

所以利用空指针域存储指向结点在某种遍历次序下的前趋/后继结点的指针,这种附加的指针叫做“线索”,而相应的二叉树叫线索二叉树,将普通二叉树变为线索二叉树的过程叫做线索化

数据结构学习笔记(基础)_第2张图片

(为 1 代表前趋/后继,为 0 代表左/右孩子)

1.5.哈夫曼树

介绍:

  • 路径和路径长度:从一个结点向下可以达到的孩子或子孙结点间的通路,称为路径。路径中分支的数目称为路径长度。假如规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为 L-1
  • 权和带权路径长度:若将树中结点赋给一个有着某种含义的值,则称该值为该结点的权。结点的带权路径长度为从根结点到该结点之间的路径长度和该结点的权的乘积
  • 树的带权路径长度:树的带权路径长度规定为其所有叶子结点的带权路径长度之和,记为 WPL
  • 哈夫曼树 / 最优二叉树:若一个二叉树的带权路径长度设计达到最小,则称这种二叉树为最优二叉树(又叫 Huffman tree)“长短路配小大权”
  • 构造哈夫曼树的过程共新建了 n-1 个结点,因此哈夫曼树的结点总数为 2n-1

2.广义表

属于是线性表的推广,或说是线性及非线性之间的过渡结构(即广义表放松对表元素的原子限制,容许它具有其自身结构)

特点:

  • 元素允许是原子或广义子表
  • 表的“深度” = 所有非同级子表 + 1

head(L)=a, tail(L)=b

即 head(tail(L))=b, tail(tail(L))=()

3.图(其结构比树更加复杂)

图 G 是由两个集合 V、E 组成,记作 G=(V, E),其中 V 是顶点的有穷非空集合,而 E 是边的有穷可空集合。通常也把前者顶点集记为 V(G),后者边集记为 E(G)。若 E(G) 为空,则图 G 只有顶点而无边

3.1.无向图

即图的边没有方向,不是向量。其边均是顶点的无序对,无序对常用圆括号表示。如 (1, 2) 和 (2, 1) 均代表同一条边

3.2.有向图

图的边皆是向量。其边均是两个顶点的有序对,有序对常用尖括号表示。如 <1, 3> 和 <3, 1> 是两条不同的有向边。

尖括号靠左的顶点是起始点(弧尾 Tail),靠右的顶点是终止点(弧头 Head)。有向边又叫弧(Arc)图边的方向是以起始点指向终止点的箭头来表示的

It starts at the end and ends at the beginning

n 代表结点数

无向完全图 有向完全图
描述 一个无向图中任意两个结点间均有边连接 一个有向图中任意两个结点间均有方向互反的边连接
边数 n(n-1)/2 n(n-1)

 

图 G 顶点的度:即与此结点相关联的边的数目

若是有向图,以顶点 V 为终点的边的数目称为 V 的入度 ID(V);以顶点 V 为起点的边的数目称为 V 的出度 OD(V);通常认为该顶点的度是其入度和出度之和,即 TD(V) = ID(V) + OD(V)

子图

若图 G = (V, E),G‘ = (V', E') 中 V' 是 V 的子集,而 E’ 是 E 的子集,则称 图 G' 是 G 的一个子图

图的路径、简单路径、简单回路(路描述)
路径 简单路径 简单回路
描述 边及边的和是路径,路径长度为边的数目和 即只允许某条路径的起点和终点相同,而其余结点均不相同 起点和终点相同的简单路径
范围 路径 > 简单路径 > 简单回路

连通、连通图、连通分量(连接状态)
连通 连通图 / 强连通图 连通分量
无向图 若两个顶点互有路径,即为连通 无向图任意两顶点连通,即为连通图 连通图本身 = 连通分量,而非连通图的无向图会有多个连通分量
有向图 有向图任意两顶点连通,即为强连通图 强连通图本身 = 连通分量,而非强连通图的有向图会有多个连通分量

邻接矩阵表示法

用二维数组(矩阵)表示图的顶点之间相邻的关系,数组构造方法:设有顶点 A[i, j] 若顶点 Vi 和 Vj 是连通的,则 A[i, j] 为 1,否则为 0

明显以上只是存储了顶点的坐标,还需要一个一维数组来存储其结点的数据域:0:V0 ——> 1:V1 ——> 2:V2 ——> ... ——> n:Vn (数组 vexs,冒号前是下标后是顶点信息)

1.邻接矩阵结构定义
typedef struct
{
    VertexType vexs[Num];    //vexs 用于存储顶点信息,VertexType 为数据类型
    EdgeType edges[Num][Num];//邻接矩阵,存储边信息
    int n, e;//分别代表了图的顶点数和边数
}MGraph;    //结构体类型
邻接表表示法

将所有邻接于某顶点 V 的所有顶点连接成一个单链表,此单链表又叫顶点 V 的邻接表。邻接表里的每个结点都存储了其邻接顶点的信息/下标 adjvex 和指向下一个结点/邻接顶点的指针 next

很明显这些由顶点主导的单链表需要一个纵向的一维数组把它们串起来,此数组元素将会存储了顶点信息 vertex  和指向邻接表的指针 link

1.邻接表结构定义
typedef struct node    
{
    int adjvex;
    struct node *next;
}Anode;

2.数组元素结构定义
typedef struct 
{
    char vertex;
    Anode *link;
}Vnode;

3.顶点结构定义
typedef struct
{
    Vnode adjlist[1024];//顶点存储信息
    int vexnum, arcnum;//前者是顶点数目,后者是边的数目
}Adjgraph;

数据的四种基本 存储方法/存储结构

在存储数据时,通常不仅要存储各数据元素的值,而且要存储数据元素之间的关系

a.顺序存储方法

所谓顺序存储方法就是让结点按逻辑次序来存储在一组地址连续的存储单元里的方法。

1.顺序表(顺序存储 + 线性表)

用顺序存储方法存储的线性表,即顺序表(线性表的链式存储结构)

特点:

用物理位置上的邻接关系来符合结点之间的逻辑关系,所以实现了遍历/随处访问:它的存储密度高,每个结点只存储数据元素。因此插入和删除操作需要移动大量元素。

顺序表插入、删除操作的时间复杂度都是 O(n)

2.顺序栈(顺序存储 + 栈)

即栈的顺序存储结构,本质上栈是运算受限的线性表,顺序栈的类型定义例子如下:

typedef struct
{
    DataType data[StackSize];//DataType 是数据类型
    int top;//记录栈顶位置
}SeqStack;
若 S 是 SeqStack 类型的指针变量,S->data[0] 是栈底元素

注意!S.top == StackSize-1 表示栈满;当栈满时,再入栈就会产生空间上溢的现象。而 S.top == -1 表示空栈;当栈空时,再出栈就会产生空间下溢的现象。最后栈长:S.top + 1

由于顺序栈的入栈操作受数组上界的约束,即有可能发生栈上溢的风险。

利用栈底位置相对不变的特性,可让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸(提高了空间利用率)

3.顺序队列(顺序存储 + 队列)

采用了顺序存储结构,而顺序队列实际上是运算受限的顺序表。队头指针 front 指向队头元素,队尾指针 rear 指向队尾元素的下一个位置

typedef struct
{
    DataType data[QueueSize];//DataType 为数据类型,QueueSize 为队列大小
    int front,rear;
}CirQueue;

“假溢出”:队列即将满时插入元素,由于队头出列和队尾入列就会导致队头有空闲位置而队尾插入不了新元素(因为此时 rear == MAXSIZE),即队列存储空间并未全部占满。

循环队列(解决了“假溢出”)

为了解决“假溢出”现象,让队列的存储空间得到充分利用,我们巧妙的把顺序队列的数组看作一个头尾相连的循环结构。(队列头尾相接的顺序存储结构称为循环结构)

可当循环队列为空或满时,都是队尾指针 = 队头指针,即 rear = front。此时如何判断其空或满呢?

方法一:设置一个计数器 count,开始时 count 设为 0,新元素入队时,count++,元素出队,count--。而当 count == MAXSIZE 时,队满;count == 0 时,队空。

方法二:保留一个元素空间,当队尾指针指的空闲单元的后继单元恰好是队头元素所在单元时,判断队满。

  • 即队满的条件:(Q.rear + 1) % MAXSIZE == Q.front;
  • 队空的条件:Q.rear == Q.front;(即都指向头结点)

推荐方法二,因为它操作方便统一,比较稳定。

4.顺序串(顺序存储 + 串)

即把串所包含的字符序列依次存储到连续的存储单元中去,也就是用向量存储串。(C 语言中用字符 '\0' 表示字符串的结束)

typedef struct
{
    char ch[MAXLEN];//MAXLEN 为向量大小
    int len;//len 为已经存储字符的数量
}SeqString;

5.顺序树(存储结构 + 树)

把二叉树的所有结点按照一定的线性次序存储到连续的存储单元中去。结点在这个序列中的相互位置还能反映出结点之间的逻辑关系(“Z”形走法)

数据结构学习笔记(基础)_第3张图片

5.1.父亲表示法

此法考虑到树中每个结点的父亲是唯一的性质,利用顺序存储结构在存储结点信息的同时再为结点设置一个域(parent)来存储其父亲的下标。而根没有父亲,所以其 parent 域存储的是 -1

5.2.孩子链表表示法

该方法为树中每个结点设置一个孩子链表(左右孩子皆在里面),并将这些结点及其相应的孩子链表的头指针顺序存储在一个顺序结构里,如下图所示:

数据结构学习笔记(基础)_第4张图片

b.链式存储方法(单向和双向、循环和非循环、静态和非静态)

各个不同结点的存储空间可以不连续,但结点内的存储单元地址必须连续

单向和双向

1.1.单链表示例:(链式结构 + 线性表)

用一组任意的存储单元来放置线性表的结点,这组存储单元对结点连续性无要求。只是为了正确表示结点间的逻辑关系,在存储每个结点值的同时还必须存储了指向其后继结点的地址/位置(即存储了指针或链)

单链表的结点结构示意图
数据域 指针域
data next
链表结点的定义/模型
typedef struct_node
{
    DataType data;    //结点的数据域,DataType 为数据类型
    struct_node *next;//结点的指针域
}ListNode;
头插法 尾插法
采用头插法建立单向链表时,读入数据的顺序和插入链表中元素的顺序是相反的(尾插法顺序是一致的)。每个结点插入时间复杂度为 O(1),一共是 O(n) 采用尾插法的前提是:必须增加一个尾指针使其始终指向当前链表的尾结点
单链表 双链表
对给定 n 个元素的一维数组建立一个有序单链表的最低时间复杂度为 O(nlogn) 双向链表的按值查找及按位查找与单向链表相同,但双向链表在尾结点的插入及删除操作的实现时间复杂度仅仅为 O(1)

 存储密度:结点数据本身所占的存储量 / 完整结点结构所占的存储量(一般来说存储密度越高,存储空间的利用率就越高)

顺序表 链表
空间 存储密度 = 1 存储密度 < 1
时间 对于顺序表这种随处存取结构,对表中任意结点都可以在 O(1) 时间内直接存取 对于链表中的结点,至少需要从头指针顺着链扫描才能查到

1.2.双链表

与单链表相比,双链表的优点之一是访问前后相邻结点更灵活。其余大差不差,不再赘述

2.链式栈(链式结构 + 栈)

采用链式存储的栈称为链栈,链栈的优点是便于多个栈共享存储空间且提高了效率,且不存在栈满上溢的情况,通常采用单链表实现

3.链式队列(链式结构 + 队列)

队列的链式表示称为链队列,它实际上是一个同时带有队头指针和队尾指针的单链表。

当 Q.front== NULL 且 Q.rear==NULL 时,链表队列为空(通常把链式队列设计成一个带头结点的单链表)

对链式队列进行删除操作时,可能会修改头、尾指针

队头指针指向链队列的头结点,队尾指针指向链队列的终端结点。(对于顺序队列来说:增加了指向链表上的最后一个结点的尾指针)

1.结点结构
typedef struct QNode

{
    DataType data;
    struct QNode *next;
}LinkNode;

2.链队列结构
typedef struct
{
    LinkNode *front, *rear;//队头、队尾指针
}LinkQueue;

循环和非循环

顾名思义,循环链表是一种首尾相连的链表(比非循环链表更为方便灵活)

头指针和头结点区分:
头指针 头结点
位置 即链表指向第一个结点的指针人,若该链表有头结点,那么它也是指向头结点的指针 放在第一个数据元素之前,其数据域一般无意义(也可以用来存储链表的长度等等)
必要性 无论一个链表是否为空,它的头指针均不为空。头指针是链表的必要元素 头结点不一定是链表的必要元素
作用 指向链表 为了操作的统一/方便而建立的

不管带不带头结点,头指针都始终指向链表的第一个结点。打个比方:不论一串火车带不带火车头(头结点),反正头指针就代表这串火车的“首位”。头结点的数据域一般不存储数据元素,但可以存储链表的长度等附加信息当作监视哨...

头指针具有标志作用,所以一般头指针被冠以链表的名字(指针变量的名字)

循环单向链表 循环双向链表
有时循环单向链表的常做操作是在表头/表尾进行的,此时对它应不设置头指针而部署尾指针 在循环双链表 L 中,某结点 *p 为尾结点时,p->next==L;

循环链表中没有指针域是 NULL 的结点,故它的判空条件为它是否等于头指针(当循环双向链表为空表时,其头结点的 prior 域和 next 域都会等于 L)

4.链式串(链式结构 + 串)

同顺序表一样,顺序串的插入/删除操作也不方便(需要移动大量字符)因此可以采用单链表方式来存储串值,即链串。

单链表和链式串的区别在于:后者的结点数据域只是 字符

typedef struct node
{
    char data;
    struct node *next;
}LinkStrNode;//结点类型

静态和非静态

静态链表借助数组来描述线性表的链式存储结构,其结点也有数据域 data 和指针域 next,这里的指针域实际上是结点的相对地址(数组下标/游标)静态链表以 next==-1 作为其结束的标志,静态链表数据之间“一对一”的逻辑关系以及通过整型变量(游标,功能类似指针)组织起了链表的功能。所有它在插入和删除操作时是不需要移动元素的仅仅需要修改游标。

由于静态链表需要提前申请内存,不能动态增长,所以为了方便应预先分配一个较大的空间。又因为它数据之间“一对一”的逻辑关系,所以它存储位置可以是随机的、且不便于遍历查找;而数组中可能有未使用的空间,因此我们还需要一条连接空闲位置的链表方便我们的随取随用,称它为备用链表备用链表的作用是回收数组中未使用或之前使用过(目前未使用)的存储空间,留待后期使用。也就是说,静态链表使用数组申请的物理空间中,一般存有两个链表,一条连接数据,另一条连接数组中未使用的空间。

在静态链表的插入和删除操作中,都会与备用链表有着联系,当进行插入时,则是用备用链表上面取得一个结点作为待插入的新结点,反之,当在删除时,则将从链表上删除下来的结点链接到备用链表上面。整个过程中,我们需要做的工作就是更新游标的值。

总结:静态链表可以让没有指针的高级语言也能借助数组实现链表功能!

静态链表使用备用链表的优缺点
优点 缺点
可以清楚地知道数组中是否还有空闲位置,综合了顺序表和动态链表优点 维护麻烦

通常存储较稳定的线性表选择顺序存储,而频繁插入、删除的线性表适宜选择链式存储(链式存储比顺序存储更方便地描述各种逻辑结构,这体现了链式存储的灵活性)

链式树

数据结构学习笔记(基础)_第5张图片

(这是顺序树!)

背景:由上图可知,树这种形态不适合采用顺序存储结构,且结点的插入和删除需要移动大量结点。

用链式存储结构存储二叉树时,其每个结点处理存储结点本身的数据 data 外,还应设置两个指针域 lchildrchild (它们分别指向该结点的左孩子和右孩子)

c.索引存储方法

根据地址就可以找到对应的关键字。可以理解成一个黄页,你根据一个人的名字,就可以找到他的电话。所以索引存储又被称为直接寻址。 数组就是一个常见的索引存储结构,可以通过下标来直接访问,下标(也就是关键字和地址相关)

d.散列存储方法

“散列存储”名字中的“散列”就是常听到的 hash(哈希值),hash 是通过一种算法来运算出来的,比如 MD5。在这种存储格式下,地址会通过 hash 算法来运算成一个相同长度的 hash 值,然后存放这个 hash 值,而不是直接存放地址。在访问关键字的时候会通过运算解码 hash 值,然后再访问。这个时候,节点的存储地址和关键字是有某种映射关系的。

散列比索引存储效率要平均高些!

综述:数据的逻辑结构和存储结构是密不可分的

逻辑结构 存储结构(存储方法)
算法 算法的定义/设计取决于所选定的逻辑结结构 而算法的实现依赖于采用的存储结构
运算的定义/实现 定义是针对逻辑结构的,指出运算的功能 实现是针对存储结构的,指出运算的具体操作步骤

对于两种不同的数据结构,它们的逻辑结构和物理结构(存储结构)都一定不同吗?

答:不一定,比如二叉树和二叉排序树。

扩展一:矩阵的压缩存储

高阶矩阵中有许多值相同的元素或是零元素。为了节省空间,对此类矩阵采用多个值相同的元素只分配一个存储空间以及零元素不存储的存储策略,这称为矩阵的压缩存储(压缩矩阵)

  1. 压缩矩阵:指为多个值相同的元素只分配一个存储空间,对零元素不分配存储空间
  2. 特殊矩阵:指具有许多相同矩阵元素或零元素(对称矩阵,上下三角矩阵,对角矩阵)
  3. 对 n 阶对称矩阵压缩存储时,需要表长为 n (n+1) / 2 的顺序表

由于计算机的内存结构是一维的,因此用一维内存来表示二维数组,就必须以某种次序将数组元素排列成一个线性序列,然后把这个线性序列顺序存储在存储器中。

  • 行优先存储
    • 数组元素按行向量排序,二维数组 Amn 的按行优先存储的线性序列为:
    • A11, A12, ... , A1n、A21, A22, ... , A2n, ... , Am1, Am2, ... , Amn
    • 行优先存储公式:LOC(Aij) = LOC(A11) + [(i-1)*n+j-1]*d
  • 列优先存储
    • 数组元素按列向量排序,二维数组 Amn 的按列优先存储的线性序列为:
    • A11, A21, ... , Am1、A12, A22, ... , Am2, ... , A1n, A2n, ... , Amn
    • 列优先存储公式:LOC(Aij) = LOC(A11) + [(j-1)*n+i-1]*d 

注释:LOC( A11 ) 是开始结点的存储地址,d 是每个元素所占用的存储单元数,mn 是行列的上界。

1.对称矩阵

在 n 阶矩阵 A 中,若元素满足:aij = aji(0<=i,j<=n-1)则称 A 为对称矩阵

2.三角矩阵

上三角矩阵,即它的下三角(不包括主对角线)中的元素均为常数 C(通常是 0);而下三角矩阵反之

3.1.稀疏矩阵

设置矩阵 Amn 中有 s 个非零元素,若 s 小于矩阵元素的总和(m*n),即称它为稀疏矩阵

3.2.三元组表

出现背景:为了节省存储单元,采用稀疏矩阵的压缩存储方式,即只可存储非零元素。又因为非零元素的分布一般无规律,因此在存储其元素值的同时还必须存储其所在的行、列号,才能确定其位置。很明显这种压缩存储方式会失去随处存取能力。

其结点按行优先(或列优先)存储了行、列、值三种信息(主要用来存储稀疏矩阵的一种数据结构)并依次存储在向量中,故叫三元组表

3.3.十字链表

十字链表将行单链表和列单链表结合起来存储稀疏矩阵

提一提:算法

算法是对特定问题求解步骤的一种描述,它是指令的有限序列,而其中每条指令包括了若干个操作

算法五大特性:

  1. 有穷性
  2. 确定性
  3. 可行性
  4. 输入(应具有零个或多个输入)
  5. 输出(应具有一个或多个输出,无输出的算法无意义就像除数为零一样)

通常设计一个好的算法应考虑:正确性、可读性、健壮性、效率与低存储量需求

1.递归

递归必须满足两个条件:递归表达式、边界条件

 2.1.遍历

2.2.二叉树的遍历

此遍历按照根、左子树以及右子树的先后顺序大致分为三种:

  1. 先序根左右:访问根 ——> 遍历左子树 ——> 遍历右子树
  2. 中序左根右:遍历左子树 ——> 访问根 ——> 遍历右子树
  3. 后序左右根:遍历左子树 ——> 遍历右子树 ——> 访问根

数据结构学习笔记(基础)_第6张图片

(根从总根算起,子树同层往上爬)

2.3.树的遍历

首先树(森林)的遍历不包括中序遍历,这是因为树的结点允许有超过两个的子树,所以无法确定根的访问顺序。其余两种遍历方式同二叉树遍历方式,这里不再赘述。

森林的先序遍历 森林的后序遍历
  1. 访问森林中第一个树的根结点
  2. 先序遍历第一棵树中根结点的各子树
  3. 对除第一棵树外的其它树继续 1、2 步骤
  1. 后序遍历第一颗树中根结点的各子树
  2. 访问森林中第一颗树的根结点
  3. 对除第一棵树外的其它树继续 1、2 步骤

3.树、森林和二叉树的相互转换

首先说树转换为二叉树的步骤:

  1. 将树的所有兄弟结点之间平连起来
  2. 每个结点只允许和长子有连线,和其余孩子的连线都去掉
  3. 将图水平线顺时针旋转 45° 查看

森林转换为二叉树的步骤:

  1. 将里面已经是二叉树的根结点视为兄弟,从左向右连在一起
  2. 其它树按转换为二叉树的方法进行,再执行 1 步骤

二叉树转换为森林的步骤:

  1. 若结点 B 是父亲 A 的左孩子,则把 B 的右孩子及其右孩子支脉全部和 A 直接连接起来
  2. 去掉所有结点和其右孩子的连线

时间复杂度(通常认作是最坏情况下估算算法执行时间的一个上界)

算法的时间复杂度不仅依赖于问题的规模,也取决于待输入数据的性质。

一般程序执行的时间是无法准确计算的,因此通常采用程序执行次数来估算,使用 T(n) 代表。而使用大写字母 O 代表时间复杂度,称为算法的渐进时间复杂度。

算法执行次数是常数,即 T(n) = O(1)

空间复杂度

一般情况下,算法的时间效率和空间效率属于一对矛盾体。

若输入数据所占空间只取决于问题本身而和算法无关,则只需分析除输入和程序之外的额外空间

算法原地工作是指算法所需辅助空间为常量,即 O(1)

维护复杂度

一般来说,同一个算法,实现语言的级别越高,执行效率越低。

你可能感兴趣的:(数据结构,学习,笔记)