文章目录
一、基础概念
1.1复杂度
- 时间复杂度: 是算法执行语句的次数。
- 空间复杂度:一个算法在运行过程中临时占用存储空间大小的量度。
1.2 数据结构
- 数据结构:相互之间存在一种或多种关系的数据元素的集合。。
- 逻辑结构:
- 线性结构——数据结构中的数据元素是一对一关系的
- 树性结构——数据结构中的数据元素之间存在一对多的层次关系
- 图形结构——数据结构中的数据元素之间存在多对多的关系
- 存储结构:
- 顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。其优点是可以实现随机存取,每个元素占用最少的存储空间;缺点是只能使用相邻的一整块存储单元,因此可能产生较多的外部碎片。
- 链式存储。不要求逻辑上相邻的元素在物理位置上也相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系。其优点是不会出现碎片现象,能充分利用所有存储单元;缺点是每个元素因存储指针而占用额外的存储空间,且只能实现顺序存取。
- 索引存储。在存储元素信息的同时,还建立附加的索引表。索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)。其优点是检索速度快;缺点是附加的索引表额外占用存储空间。另外,增加和删除数据时也要修改索引表,因而会花费较多的时间。
- 散列存储。根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash) 存储。其优点是检索、增加和删除结点的操作都很快;缺点是若散列函数不好,则可能出现元素存储单元的冲突,而解决冲突会增加时间和空间开销。
1.3 算法
- 什么是算法:对特定问题求解步骤的描述。
- 算法的性质:
- 有穷性——有限的步骤
- 确定性——不可二义性
- 可行性——每一步都是通过执行有限次数完成的
- 输入——零个或多个输入
- 输出——至少有一个或多个输出
二、线性结构
- 线性表的基于数组的存储表示叫做顺序表(SeqList),线性表的基于指针的存储表示叫做链表(LinkedList)(单链表、双链表、循环链表等)
2.1线性表-数组
- 数组(array)是一种线性表数据结构,它用一组连续的内存空间来存储一组具有相同类型的数据。
- 数组的定义是有限个类型相同的变量集合命名,但是在实际编程过程中,不同的程序语言对于数组存储是有差异的,比如C,C++, java 元素类型就是一样的,而一些脚本语言,比如python js脚本语言数组中定义的元素类型就可以不同。
- 从本质上讲,数组与顺序表、链表、栈和队列一样,都用来存储具有 “一对一” 逻辑关系数据的线性存储结构
- 以存储不可再分的数据元素(如数字 5、字符 ‘a’),也可以继续存储数组(即 n 维数组)
- 数组有连续的内存空间和相同的数据类型,因此可以根据下标实现随机访问,必须要指定大小,删除和插入不方便。
- 查询很快
- 一些特殊矩阵:
- 对称矩阵
- 上三角矩阵/下三角矩阵:压缩为大小为n(n+1)/2的一维数组
- 对角矩阵(带状矩阵)
特殊矩阵的共性:都是方阵,行数和列数相同。
特殊矩阵压缩存储后,由于它矩阵元素分布的规律,仍然具有随机存取的特性。
- 随机稀疏矩阵的压缩存储方法:
- 1.三元组顺序表:
- 2.行逻辑链接的顺序表。它可以看作是三元组顺序表的升级版,即在三元组顺序表的基础上改善了提取数据的效率。在存储矩阵时比后者多使用了一个数组,专门记录矩阵中每行第一个非 0 元素在一维数组中的位置。
- 3.十字链表法
- 1.三元组顺序表:
2.2 线性表-链表
-
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
是线性表的链式存储方式
。 -
由于链表里面的内存是手动分配的,当不再使用这些内存时需要手动删除
-
链表的类型:
- 带头结点的(头指针的后继指向第一个元素)。有头结点的链表实现比较方便(每次插入新元素的时候,不需要每次判断第一个节点是否为空),并且可以直接在头结点的数据块部分存储链表的长度,而不用每次都遍历整个链表
- 不带头结点的(头指针指向第一个元素)
- 单链表(每个元素只需要维护一个指针)
- 双链表(每个元素需要维护两个指针,分别指向前驱和后继结点)能够通过在O(1)时间内通过目的节点直接找到前驱节点,但是同时会增加大量的指针存储空间。
- 循环的:在链表的尾部增加一个指向头结点的指针,头结点也增加一个指向尾节点的指针,以及第一个节点指向头节点的指针,从而更方便索引链表元素。(一个空的双向循环链表中只有一个头节点,头节点的前驱和后驱都指向本身)
- 不循环的
数组和链表的区别?
-
1.从逻辑结构来看:数组的存储长度是固定的,不能动态的增减,即数组大小定义之后不能改变,当数据增加时会超出数组的长度,当数据减少时会造成内存浪费。链表与数组相反,它能够动态分配存储空间以适应数据动态增减的情况,并且易于进行插入和删除操作。当插入和删除操作频繁时,链表的优势就越明显。
-
2.从访问方式来看:数组在内存中是一片连续的存储空间,可以通过数组下标对数组进行随机访问,访问效率较高。链表是连式存储结构,他的存储空间不是必须连续的,可以是任意的,因此链表的访问必须从前往后依次进行,访问效率较数组来说比较低。
-
3.从内存存储来看,
数组从栈中分配空间
, 对于程序员方便快速,但是自由度小.链表从堆中分配空间
自由度大但是申请管理比较麻烦
2.3 线性表-队列
-
允许在表的一端插入数据,在另一端删除元素。插入元素的这一端称之为队尾。删除元素的这一端我们称之为队首
-
队列的特性:
- 在队尾插入元素,在队首删除元素。
- FIFO(先进先出),就向排队取票一样。
- 循环队列中为什么要空一个位置:为了区分队空和队满的状态,队空是首尾指针指向同一个位置,队满是尾指针的下一个位置是首指针。
- 队列在计算机系统中的应用:第一是用于缓冲区,缓冲区是用来解决外部设备与CPU之间速度不匹配的问题,缓冲区主要用队列实现;第二是在CPU进行进程调度时,当某个进程处于就绪态时,就把该进程挂载到进程队列中,等待CPU的调用。
2.4 线性表-栈
-
共享栈:利用栈底位置的相对不变性,可以让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享栈的两端,两个栈顶向共享空间的中间延伸,这样能够更有效的利用存储空间,两个栈的空间相互调节,只有在整个数组空间被占满后才会发生上溢。
-
栈的应用:括号匹配,表达式求值,递归时存储环境
括号匹配算法思想
(1)出现的凡是“左括号”,则进栈;
(2)出现的是“右括号”,
1.首先检查栈是否空?
2.若栈空,则表明该“右括号”多余
3.否则和栈顶元素比较?
4.若相匹配,则栈顶“左括号出栈”
5.否则表明不匹配
(3)表达式检验结束时,若栈空,则表明表达式中匹配正确否则表明“左括号”有余;
- 栈在通过后缀表达式求值的算法思想:顺序扫描表达式的每一项,然后根据它的类型做如下相应操作:若该项是操作数,则将其压入栈中;若该项是操作符,则连续从栈中退出两个操作数y 和x, 形成运算指令XY, 并将计算结果重新压入栈中。当表达式的所有项都扫描并处理完后,栈顶存放的就是最后的计算结果。
栈和队列异同
栈 | 队列 |
---|---|
先进后出 | 先进先出 |
只能在表尾进行插入和删除 | 在表头进行删除,在表尾进行插入 |
top==-1 为空 | front==rear 为空 |
相同:
- 都是线性结构
- 插入时都在队尾
- 都可以采用顺序存储结构和链式存储结构
- 插入和删除操作的时间复杂度和空间复杂度是一样的
2.5 串
- 字符串要单独用一种存储结构来存储,称为串存储结构。这里的串指的就是字符串。严格意义上讲,串存储结构也是一种线性存储结构,因为字符串中的字符之间也具有"一对一"的逻辑关系。只不过,与之前所学的线性存储结构不同,串结构只用于存储字符类型的数据。
- 串的定长顺序存储结构,可以简单地理解为采用 “固定长度的顺序存储结构” 来存储字符串,因此限定了其底层实现只能使用静态数组。
- 串的堆分配存储,其具体实现方式是采用动态数组存储字符串。
- 串的块链存储,指的是使用链表结构存储字符串。
- BF算法
KMP算法
2.6 广义表
- 广义表,又称列表,也是一种线性存储结构。
- 广义表中既可以存储不可再分的元素,也可以存储广义表
- 广义表中存储的单个元素称为 “原子”,而存储的广义表称为 “子表”。
通常情况下广义表结构采用链表实现
。使用顺序表实现广义表结构,不仅需要操作 n 维数组(例如 {1,{2,{3,4}}} 就需要使用三维数组存储),还会造成存储空间的浪费。
三、非线性结构
3.1 树
- 度:拥有的子树数(结点有多少分支)称为结点的度(Degree),
一棵树的度是树内各结点的度的最大值
- 树的深度和高度:深度是从根节点数到它的叶节点,高度是从叶节点数到它的根节点,即深度定义是从上往下的,高度定义是从下往上的。虽然树的深度和高度一样,但是具体到树的某个节点,其深度和高度是不一样的。
3.1.1 二叉树
- 本身是有序树;
- 树中包含的各个节点的度不能超过 2,即只能是 0、1 或者 2;
-二叉树的顺序存储,指的是使用顺序表(数组)存储二叉树。需要注意的是,顺序存储只适用于完全二叉树。换句话说,只有完全二叉树才可以使用顺序表存储
满二叉树:
- 如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树。
完全二叉树:
- 如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。
二叉排序树(二叉搜索树、有序二叉树或二叉查找树:
1.若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
2.若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
3.任意节点的左、右子树也分别为二叉查找树;
4.没有键值相等的节点。
二叉平衡树:
- 在二叉排序树的基础上,只要保证每个节点左子树和右子树的高度差小于等于1就可以了。
- 适用于插入删除比较少,但是查找比较多的情况
3.1.2 红黑树
- 红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它是在1972年由鲁道夫·贝尔发明的,称之为"对称二叉B树",它现代的名字是在 Leo J. Guibas 和 Robert Sedgewick 于1978年写的一篇论文中获得的。它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的: 它可以在O(logn)时间内做查找,插入和删除,这里的n是树中元素的数目。
- 红黑树的性质:
红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树强制的一般要求以外,有如下的额外要求:
性质1. 节点是红色或黑色。
性质2. 根是黑色。
性质3. 所有叶子都是黑色(叶子是NIL节点)。
性质4. 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
性质5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
3.1.3 B和B+树
B树
又称多路平衡查找树(绝对平衡),B树中所有结点的孩子个数的最大值成为b树的阶,通常用m表示。
一棵m阶B树或为空树,或为满足如下特性的m叉树:
1.树中的每个结点至多有m棵子树,至多含有m-1关键字
2.若根节点不是终端结点,至少有两棵子树
3.除根结点外的所有非叶结点至少有m/2课子树,即至少都含有m/2-1个关键字。{向上取整}
4.所有的叶结点都出现在同一层次上。
B+树
类似分块查找
m阶B树 | m阶B+树 |
---|---|
结点中的n个关键字对应n+1棵子树 | 结点中的n个关键字对应n棵子树 |
根节点关键字数:【1,m-1】 | 根节点关键字数:【1,m】 |
其他结点的关键字数【m/2-1,m-1】 | 其他结点的关键字数【m/2,m】 |
关键字不重复 | 叶结点包含全部关键字,会出现非叶节点中的关键字 |
包含关键字对应的存储地址 | 非叶结点仅起索引作用,不含有该关键字对了记录的存储地址 |
3.1.4 堆
- 堆是一种经过排序的树形数据结构。堆根据“堆属性”来排序,“堆属性”决定了树中节点的位置。
- 堆是在程序运行时,而不是在程序编译时,申请某个大小的内存空间。即动态分配内存,对其访问和对一般内存的访问没有区别。堆是指程序运行时申请的动态内存,而栈只是指一种使用堆的方法(即先进后出)。
- 堆的性质:
(1)堆中某个节点的值总是不大于或不小于其父节点的值;
(2)堆总是一棵完全二叉树。
- 堆的常见用处:
构建优先队列
支持堆排序
快速找出一个集合中的最小值(或者最大值)
- 堆的分类:
堆分为两种:最大堆和最小堆,两者的差别在于节点的排序方式。
在最大堆中,父节点的值比每一个子节点的值都要大。
在最小堆中,父节点的值比每一个子节点的值都要小。
这就是所谓的“堆属性”,并且这个属性对堆中的每一个节点都成立。
堆和栈的区别:
栈是系统自动分配空间的,而堆则是程序员根据需要自己申请的空间。
由于栈上的空间是自动分配自动回收的,所以栈上的数据的生存周期只是在函数的运行过程中,运行后就释放掉,不可以再访问。
而堆上的数据只要程序员不释放空间,就一直可以访问到,不过缺点是一旦忘记释放会造成内存泄露。
申请效率: 栈由系统自动分配,速度较快。但程序员是无法控制的。堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
3.1.5 哈夫曼树
路径:在一棵树中,一个结点到另一个结点之间的通路,称为路径。图 1 中,从根结点到结点 a 之间的通路就是一条路径。
路径长度:在一条路径中,每经过一个结点,路径长度都要加 1 。例如在一棵树中,规定根结点所在层数为1层,那么从根结点到第 i 层结点的路径长度为 i - 1 。图 1 中从根结点到结点 c 的路径长度为 3。 结点的权:给每一个结点赋予一个新的数值,被称为这个结点的权。例如,图 1 中结点 a 的权为 7,结点 b 的权为 5。
结点的带权路径长度:指的是从根结点到该结点之间的路径长度与该结点的权的乘积。例如,图 1 中结点 b 的带权路径长度为 2 * 5 = 10 。
- 构建哈夫曼树
对于给定的有各自权值的 n 个结点,构建哈夫曼树有一个行之有效的办法:
1.在 n 个权值中选出两个最小的权值,对应的两个结点组成一个新的二叉树,且新二叉树的根结点的权值为左右孩子权值的和;
2.在原有的 n 个权值中删除那两个最小的权值,同时将新的权值加入到 n–2 个权值的行列中,以此类推;
3.重复 1 和 2 ,直到所以的结点构建成了一棵二叉树为止,这棵树就是哈夫曼树。
3.1.6 树的使用
- 树的表示法:
- 1.双亲表示法
双亲表示法采用顺序表(也就是数组)存储普通树,其实现的核心思想是:顺序存储各个节点的同时,给各节点附加一个记录其父节点位置的变量。
- 2.孩子表示法。
孩子表示法存储普通树采用的是 “顺序表+链表” 的组合结构,其存储过程是:从树的根节点开始,使用顺序表依次存储树中各个节点,需要注意的是,与双亲表示法不同,孩子表示法会给各个节点配备一个链表,用于存储各节点的孩子节点位于顺序表中的位置。
- 孩子兄弟表示:
孩子兄弟表示法,采用的是链式存储结构,其存储树的实现思想是:从树的根节点开始,依次用链表存储各个节点的孩子节点和兄弟节点
。向右为兄弟。
- 1.双亲表示法
二叉树的遍历:
- 先序遍历:先根节点->遍历左子树->遍历右子树
- 中序遍历:遍历左子树->根节点->遍历右子树
- 后序遍历:遍历左子树->遍历右子树->根节点
- 层次遍历:从根结点的第一层开始访问从上到下进行遍历,从左到右访问结点(利用队列来实现)
树的遍历:
先跟遍历:
- 先访问根结点
- 从左到右先跟遍历树的每个子树
后跟遍历:
- 先依次后跟遍历每根子树,
- 然后再访问根结点
深度优先遍历:
- 类似于二叉树的先序遍历
步骤:
(1)访问起始点v
(2)若v的第一个邻接点没有被访问过,则深度遍历该邻接点;
(3)若v的第一个邻接点已经被访问,则访问其第二个邻接点,进行深度遍历;重复以上步骤直到所有节点都被访问过为止
广度优先遍历 :
- 类似于层次遍历
步骤:
(1)访问起始点v
(2)依次遍历v的所有未访问过得邻接点
(3)再依次访问下一层中未被访问过得邻接点;重复以上步骤,直到所有的顶点都被访问过为止
- 回溯:回溯法,又被称为“试探法”。解决问题时,每进行一步,都是抱着试试看的态度,如果发现当前选择并不是最好的,或者这么走下去肯定达不到目标,立刻做回退操作重新选择。这种走不通就回退再走的方法就是回溯法。
3.2 图
3.2.1 图的概念
- 欧拉回路(Euler Circuit):不重复的经过所有边并且返回出发点
- 哈密顿回路(Hamilton Circuit):不能重复经过点
3.2.2 图的表示法
- 邻接矩阵表示
- 用邻接矩阵可以直接从二维关系中获得任意两个顶点的关系,可直接判断是否相连。
- 但是在对矩阵进行存储时,却需要完整的一个二维数组。
- 若图中顶点数过多,会导致二维数组的大小剧增,从而占用大量的内存空间。
- 邻接表
通过邻接表可以获得从某个顶点出发能够到达的顶点,从而省去了对不相连顶点的存储空间。然而,这还不够。
于有向图而言,图中有效信息除了从顶点“指出去”的信息,还包括从别的顶点“指进来”的信息。
这里的“指出去”和“指进来”可以用出度和入度来表示。
入度:有向图的某个顶点作为终点的次数和。
出度:有向图的某个顶点作为起点的次数和。
- 逆邻接表
- 十字链表
3.2.3 图的算法
- 迪杰斯特拉(dijkstra)算法
迪杰斯特拉算法是经典的单源最短路径算法,用于求某一顶点到其他顶点的最短路径,它的特点是以起始点为中心层层向外扩展,直到扩展的终点为止,迪杰斯特拉算法要求边的权值不能为负权。
- 弗洛伊德(Floyd)算法:
- 普里姆(prim)算法:弗洛伊德算法是经典的求任意顶点之间的最短路径,其边的权值可为负权,该算法的时间复杂度为O(N^3),空间复杂度为O(N^2)。经典的三重for循环。
- 克鲁斯卡尔(kruskal)算法:类似dijkstra,用来求最小生成树,其基本思想为:从联通网络N={V,E}中某一顶点u0出发,选择与他关联的最小权值的边,将其顶点加入到顶点集S中,此后就从一个顶点在S集中,另一个顶点不在S集中的所有顶点中选择出权值最小的边,把对应顶点加入到S集中,直到所有的顶点都加入到S集中为止。
选边,用来求最小生成树,其基本思想为:设有一个有N个顶点的联通网络N={V,E},初试时建立一个只有N个顶点,没有边的非连通图T,T中每个顶点都看作是一个联通分支,从边集E中选择出权值最小的边且该边的两个端点不在一个联通分支中,则把该边加入到T中,否则就再从新选择一条权值最小的边,直到所有的顶点都在一个联通分支中为止。