数据结构在学什么?
- 如何用程序代码把现实世界的问题信息化。
- 如何用计算机高效的处理这些信息从而创造价值。
《第三次浪潮》
农业阶段–工业阶段–信息化阶段
第一章:绪论
1、数据结构三要素:
逻辑结构:
物理结构:
数据运算:
2、算法
程序= 数据结构+ 算法
算法的五个特性:有穷性、确定性、可行性、输入、输出
好算法的特质:正确性、可读性、健壮性、高效率与低存储
3、算法效率
时间复杂度:事先预估算法时间开销T与问题规模n的关系
空间复杂度:。。。
第二章:线性表
2.1、线性表
定义:具有相同数据类型的n个数据元素的有限序列。
tip:线性表是一种逻辑结构,表示元素之间一对一的相邻关系。顺序表和链表是指存储结构。
2.2、线性表的顺序表示:
- 又称顺序表,是用一组地址连续的存储单元依次存储线性表中的数据,使逻辑上相邻的两个元素在物理位置上也相邻。
- 既是逻辑结构又是物理结构。
特点:
- 随机访问、存储密度高(与链表不同,结点只存储数据元素)、逻辑上相邻物理上也相邻。
一维数组空间分配:静态分配、动态分配(malloc)
各操作的时间复杂度:
- 插入:需要整体移动,平均移动 n/2 次,平均时间复杂度O(n)
- 删除:元素删除平均次数 (n-1)/2 次,平均时间复杂度O(n)
- 按值查找:元素比较平均次数 (n+1)/2 次,平均时间复杂度O(n)
2.3、线性表的链式表示
- 是指通过一组任意的存储单元来存储线性表中的数据元素。
- 对于每个链表结点,除存放元素自身信息外,还需存放一个指向其后继的指针。
2.3.1、单链表
**优点:**对比顺序表
- 不需要大量连续存储单元
- 插入和删除不需要移动元素
缺点:
- 附加指针域,浪费存储空间
- 查找特定结点时,需要从头开始遍历,依次查找
各操作时间复杂度:
-
头插法建立表:单次头插O(1), 表长为n,则总时间复杂度O(n)
-
尾插法建立表:由于存在尾指针,与头插相同
-
按序号查找结点:O(n)
-
按值查找结点:O(n)
-
插入节点:主要时间开销在于查找第i-1个元素,时间复杂度为O(n)。若在给定节点下插入,为O(1)
- 前插:法一 GetElem(L, i-1),O(n)。法二 后插实现前插,先后插再交换数据域。
- 后插:O(1)
-
按位序删除:先找到第 i-1 个元素,再删除,O(n)
-
删除指定结点:
- 法一:GetElem(L, i-1),找到前驱节点再删除,O(n)
- 法二:通过删除后继来实现,实质是将后继的值赋予自身,然后删除后继,O(1)
2.3.2、双链表
- 为了克服单链表只能从头节点依次顺序向后遍历,不能快速对其前驱进行操作的缺点,引入了双链表。
双链表的插入:
双链表的删除:
2.3.3、循环链表、静态链表
循环单链表:
- 和单链表的区别在于:表中最后一个结点的指针不是null,而是指向头节点。
- 判空条件:L->next == L;
- 判断p是否为表尾结点:p->next == L;
- tip:循环单链表不设头指针,只设尾指针 ,使操作效率更高。
循环双链表:
- 相比于循环单链表,头结点的prior结点还要指向表尾结点。
- 判空:L-next == L && L->prior == L;
- 判断p是否为表尾结点:p=>next == L;
静态链表:
- 借助 数组 来描述线性表的链式存储结构
- 结点也有数据域和指针域,指针是节点的相对地址(数组下标)。
2.4、顺序表和链表的比较
-
存取(读写)方式:
- 顺序表可顺序可随机
- 链表只能从表头顺序存取。
-
逻辑结构与物理结构:
- 顺序:逻辑上相邻的元素,物理存储位置也相邻
- 链表:逻辑上相邻的元素,物理存储位置不一定相邻
-
查找、插入和删除操作:
…
-
空间分配:
- 顺序:静态分配、动态分配
- 链表:空间分配灵活
选取存储结构:
- 基于存储的考虑:
- 难以事先估计长度河存储规模时,选链表,但链表存储密度略低。
- 基于运算的考虑:
- 按序号访问,顺序表由于链表
- 进行插入、删除:链表优
- 基于环境的考虑:
- 顺序表基于数组更容易实现
- 链表基于指针,在某些编程语言中不适合实现。
第三章:栈和队列
3.1、栈
- 只允许在一端进入插入或删除操作的线性表。
- 栈顶和栈底,后进先出
栈的顺序存储结构:
- 顺序栈,利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针top表示当前栈顶元素位置
- 栈顶指针 S.top ,初始值 -1或0,明白代表的不同含义,以及两种情况下栈空、栈满的判定,和进栈、出栈时的操作。
- **共享栈:**两个顺序栈共享一个一维数组空间,两个栈的栈底设置在共享空间的两端,两个栈顶向中间延伸。
- 栈满:两个栈顶指针相邻,top1 - top0 = 1 时
- 栈空:top0 = -1时,0号栈为空;top1 = Maxsize 时,1号栈为空。
- tip: 更好的利用了存储空间。
栈的链式存储结构:
- 利用单链表实现,规定所有操作都是在单链表表头操作。
- 优点:
- 不存在栈满上溢的情况
- 便于多个栈共享存储空间和提高其效率
3.2、队列
- 一种操作受限的线性表,只允许在表的一端插入,在另一端进行删除。(就像排队)
队列的顺序存储结构:
-
分配一块连续的存储单元,并附设两个指针,front指向队头,rear指向队尾或队尾的下一个位置。
-
**假溢出:**队列中依然存在空位置,但已经满足队满条件。
==> 循环队列:存储队列元素的表从逻辑上视为一个环。队尾指向元素的下一位置
-
初始状态:Q.front = Q.rear = 0;
-
队头指针进1:出队
-
队尾指针进1:入队
-
队空/队满:Q.front == Q.rear;
==>队满判断:
- 牺牲一个单元 (空一个位置) 来区分队空和队满,即”队头指针在队尾指针的下一位置作为队满标志“
- 队空:Q.front == Q.rear;
- 队满:(Q.rear + 1)%MaxSize == Q.front;
- 类型中增设表示元素个数的数据成员:size
- 插入成功:size++ 删除成功:size–
- 队空: 队满:
- 类型中增设tag数据成员
- 插入成功:令tag = 1
- 删除成功:令tag = 0
- 牺牲一个单元 (空一个位置) 来区分队空和队满,即”队头指针在队尾指针的下一位置作为队满标志“
-
队列的链式存储结构:
- 一个同时带有队头指针和队尾指针的单链表。
- 方便插入
双端队列:
- 允许两端都可以进行入队和出队操作的队列,逻辑结构仍是线性结构;
- 输出受限的双端队列:
- 输入受限的… :
3.3、栈和队列的应用
栈的应用:
- 括号匹配
- 表达式求值
- 中缀表达式转后缀表达式
- 中缀表达式转前缀表达式
- 中缀、后缀表达式的计算
- …
- 递归
- 求斐波那契数列
- 计算正整数的阶乘 n!
- 缺点:太多层递归可能会导致栈溢出
- tip:理论上所有递归都可以手动建栈,用非递归方式实现
队列的应用:
- 解决逐行或逐层的问题(层序遍历二叉树)
- 解决主机与外设之间速度不匹配的问题(如缓冲区)
- 解决因多用户引起的资源竞争(进程就绪队列)
3.4、矩阵的压缩存储
特殊矩阵的压缩存储:
- 多维数组:
- 按行优先:
- 按列优先:
矩阵的压缩存储:
- 对称对阵:
- 三角矩阵:1、上三角 :按行/按列 2、下三角:按行/按列
- 三对角矩阵:
- 稀疏矩阵:存储方式:三元组(行标、列标、值) 十字链表法
第四章:串
4.1、串的定义和实现
概念:
- 零或多个字符组成的有限序列
存储结构:
- 定长顺序存储表示:类似线性表的顺序存储结构,用一组地址连续的存储空间存储字符序列。
- 堆分配存储表示:仍以一组地址连续的存储空间存放字符串,但存储空间是在程序执行过程中动态分配得到的。
- 块存储表示:每个节点存放一个或多个字符,每个节点称为块。
4.2、串的模式匹配
简单的模式匹配:
- 主串中与模式串长度相同的子串逐个与模式串对比。
- 缺点:主串指针会回溯导致时间开销增加。最坏时间复杂度O(m*n)
改进–KMP:
- next[j] 的含义:子串第 j 个字符与主串发生失配时,则跳到子串next[j] 的位置继续匹配。
- j = 1时,为什么next[1] = 0 ? 结合next[j]的含义理解, next[1] = 0 表示模式串应右移一位,主串当前指针后移一位
- 优点:主串不会进行回溯, 时间复杂度O(m+n)。
进一步优化–nextval
- xxx
第五章:树
5.1、树的基本概念
树是一种逻辑结构,也是一种分层结构。
基本术语:
- 节点的度:节点的孩子个数。区别于 树的度。
- 分支节点、叶子节点:
- 节点的层次、深度(从根节点自顶向下)、高度(从叶子节点自低向上)
- 树的高度/深度:树中结点的最大层数
- 有序树、无序树:树中结点的各子树是否是有次序。
- 路径:两个节点之间经过的节点序列
- 路径长度:路径上经过边的个数
- 森林:互不相交的树的集合
树的性质:
- 树中节点数等于所有结点的度数加1.
- m叉树和度为m的书的区别
- m叉树:每个节点最多只有m个孩子的树
- 度为m的树:任意节点度<=m
5.2、二叉树
定义:
- n个结点的有效集合,或者为空的二叉树,或者由一个根节点和两个互不相交的左右子树组成
- 二叉树是有序树,子树有左右之分,次序不能颠倒。
- 二叉树与度为2的树的区别:二叉树可以为空,度为2的树至少有三个节点。
特殊的二叉树:
- 满二叉树:高度为h的话,含有2的h次方 - 1个结点的二叉树
- 完全二叉树:只有最后一层节点没有满的二叉树
- 二叉排序树
- 又称二叉查找树
- 性质:左子树节点值 < 根节点值 < 右子树节点值,左右子树各是一颗二叉排序树。
- 查找过程:先将给定值与根节点比较,若相等,则查找成功;若小于,则递归查询左子树;否则递归查右子树;
- **插入过程:**与查找类似,同样用递归。
- 删除:
- **查找效率:**主要取决于树的高度
- 若二叉排序树是平衡二叉树,平均执行时间O(logn)
- 若只有左(右)孩子单只树,O(n)
- 平衡二叉树:
- 左右子树的深度之差不超过1,这是为了更高的搜索效率。左右子树都是平衡二叉树
- 平衡因子:左子树与右子树的高度差为该节点的平衡因子。
- 平衡调整:先找到最小不平衡子树
- LL右单旋转、RR左单旋转、LR先左后右双旋转、RL先右后左双旋转
- 查找:平均查找长度O(logn)
二叉树性质:
- 非空二叉树叶子节点数等于度为2的节点数加1。
- 第k层节点数? 高度为h的二叉树节点数?
二叉树的存储结构:
- 顺序存储结构:用一组连续存储单元,自上而下自左至右存储完全二叉树上的节点元素。(适合完全二叉树和满二叉树)
- 链式存储结构:数据域和左右指针。n个节点的二叉链表含有n+1个空链。
二叉树的遍历和线索二叉树:
- 先序遍历:根左右
- 中序遍历:左根右
- 后序遍历:左右根
- 层次遍历:
- 操作过程:根节点入队,然后出队,访问出队结点,若它有左子树,则将左子树根节点入队,若有右子树,右子树根节点入队;然后出队,访问出队结点。如此反复。
线索二叉树:
- 为了解决无法直接找到一个节点在某种遍历序列中的直接前驱和后继结点的问题
- 二叉树的线索化是将二叉链表中的空指针改为指向前驱或后继的线索 ,而前驱或后继的信息只有在遍历时才能得到,因此线索化的实质就是遍历一次二叉树
- 结构: lchild ltag data rtag rchild
- 二叉树线索化后仍不能有效求解后续线索二叉树求后继的问题
- 根结点由右孩子,但右孩子与直接后继无直接关系,右孩子占了指针
红黑树
- 平衡的二叉查找树,具有二叉查找树和平衡二叉树的性质
- 左根右、根叶红、不红红、黑路同
- 引入带颜色的节点也是为了方便在进行插入或删除操作时,如果破坏了二叉查找树的平衡性能通过一系列变换保持平衡。
- 适用于频繁插入、删除的场景,实用性更强。O(logn)
5.3、树、森林
树的存储结构:
- 双亲表示法:
- 一组连续的空间存储节点,每个节点增设一个伪指针指示其双亲结点在数组中的位置
- 优点:很快得到每个节点的双亲结点
- 缺点:求结点的孩子时须遍历整个结构
- 孩子表示法:
- 将每个节点的孩子节点都用单链表连接起来形成一个线性结构,n个节点就有n个孩子链表
- 优点:寻找孩子节点方便
- 缺点:寻找双亲需要遍历n个节点中孩子链表指针域所指向的n个孩子链表
- 孩子兄弟表示法:
- 二叉链表作为存储结构,节点值、左指针(指向孩子)、右指针(指向兄弟)
- 优点:方便实现树转换为二叉树的操作,易于查找节点的孩子
树、森林、二叉树的转化:
- 树转化为二叉树:孩子兄弟表示法,左孩子右兄弟
- 森林转化为二叉树:将森林中的每棵树转化成二叉树,每棵树的根视为兄弟关系,连接起来
- 二叉树转化为森林:将根的右链断开,右子树运用同样的方法
树和森林的遍历:
- 先转化成二叉树
- 树的先根遍历,对应森林的先序遍历,对应二叉树的先序遍历。
- 树的后根遍历,对应森林的中序遍历,对应二叉树的中序遍历。
5.4、树的应用
树的应用–并查集:
- 双亲表示法! 用一个数组即可表示集合关系。
- Find(S,x):查找集合S中单元素x所在的子集合,即寻找x的根(根的S[] < 0) O(n)
- 优化:压缩路径,先找到根节点,再将查找路径上的所有节点都挂到根节点下。 O(logn)
- Union(int S[], int Root1, int Root2):合并两个子集合,即令S[Root2] = Root1; O(1)
- 优化:让小树合并到大树,减小合并后的高度,用根节点的绝对值表示树的节点总数 O(1)
- 应用:
- 判断图的连通分量
- 判断图是否有环
- Krustral算法
哈夫曼树:
-
节点的带权路径长度:从根到任意节点的路径长度(经过的边数),与该节点权值的乘积
-
树的带权路径长度WPL:结点带权路径长度之和
-
哈夫曼树:也称最优二叉树,在含有n个带权叶节点的二叉树中,WPL最小的二叉树
-
**构造:**每次从合成后存在的节点中选出两个权值最小的进行构造二叉树,直到所有节点均在树内。
哈夫曼编码:
- 固定长度编码:每个字符用等长的二进制编码表示
- 可变长度编码:频率高的字符用短编码,频率低的字符用长编码,起到压缩数据的效果
- 前缀编码:没有一个编码是另一个编码的前缀
- 构造:
- 每个字符当作独立的节点,字符的频率作为其权值,构造对应的哈夫曼树
- 字符的编码即为从根至该字符的路径上标记的序列
- 0表示“转向左孩子” ; 1表示“转向右孩子”
第六章:图
6.1、图的基本概念
概念:
- 图G由有限非空的顶点集V和边集E组成
基本术语:
- 有向图:边是有向边 无向图:边是无向边
- 简单图:不存在重复便,不存在顶点到自身的边 反之是 多重图;
- 完全图:任意两个顶点之间都存在边 (有向完全图、无向完全图)
- 连通、连通图、连通分量:无向图中,顶点到顶点有路径,则称连通。任意两个顶点联通的图为连通图,极大连通子图称为连通分量
- 强连通、强连通图、强连通分量:在有向图中,…
- 路径:两个顶点之间相连的边
- 路径长度:路径上边的数目
- 距离:两个顶点之间的最短路径
- 回路:第一个顶点和最后一个顶点相同的路径
6.2、图的存储方式:
-
**邻接矩阵:(有向/无向)**用一个一维数组存储图中顶点信息,用一个二维数组存储图中边的信息(顶点之间的邻接关系)
-
表示唯一
-
局限:
- 计算入度、出度:必须遍历对应的行或列
- 找相邻的边:同上
-
-
**邻接表:(有向/无向)**一维数组表示图中的顶点,同时每个元素还要存储指向第一个邻接点的指针。顶点表、边表
- 表示不唯一
- 无向图存储空间 O(V+ 2E); 有向图O(V+E) 稀疏图 适合用邻接表
- 局限:
- 计算入度不方便,其余方便
- 找入边不方便,其余方便
-
十字链表:(有向)
- 表示不唯一
-
邻接多重表:(无向)
6.3、图的遍历
- 从图的某一顶点出发访问图中其余顶点,且每个顶点仅被访问一次的过程。
广度优先遍历BFS
-
类似二叉树的层序遍历
-
算法思想:借助辅助队列
- 首先访问起始节点v,再访问v的各个未访问过的邻接结点w1,w2,w3…;
- 然后依次访问w1,w2…的所有未被访问的邻接结点;
- 从这些访问过的顶点出发,访问他们所有未被访问过的邻接顶点,直到所有顶点都被访问过。
-
如何处理非连通图?
-
性能分析:
时间复杂度:
-
邻接矩阵存储方式:O(V的平方)
-
邻接表存储方式:O(V+E)
空间复杂度:借助辅助队列
- n个顶点,最坏 O(V)
-
-
**广度优先生成树:**广度遍历中得到的遍历树,是否唯一由树的存储结构决定。
-
可以解决最短路径问题
深度优先搜索
-
类似树的先序遍历
-
**算法思想:**借助辅助栈
- 首先访问起始顶点v,从v出发,访问与v邻接且未被访问的任一顶点w1
- 再访问与w1临界且未被访问的顶点w2…重复上述过程
- 当不能再继续向下访问时,退回到最近未被访问的节点,继续上述搜索过程。
-
性能分析:
时间复杂度
- 邻接矩阵存储方式:O(V的平方)
- 邻接表存储方式:O(V+E)
空间复杂度
- n个顶点,最坏 O(V)
-
深度优先生成树:是否唯一由树的存储结构决定
6.4、图的应用
最小生成树:
- 连通图的生成树中权值之和最小的那棵生成树
- 树形不是唯一的,但权值之和是唯一的。
- Prim算法:
- 算法过程:
- 任取一顶点加入树T;
- 再选择距树T中集合距离最近的一个顶点加入树T
- 以此类推,知道所有顶点都在一个连通分量
- 时间复杂度:O(V的平方) 适合边稠密的图
- 算法过程:
- Kruskal算法:
- 算法过程:
- 按照边的权值从小到大的顺序,不断选取当前未被选取且权值最小的边
- 若该边的两个顶点落在不同的连通分量,则将该边加入,否则选择下一条权值最小的边
- 以此类推,直至所有的边都在一个连通分量
- 时间复杂度:O(E*logE) 适合边稀疏的图
- 算法过程:
最短路径:
- Dijkstra算法:(贪心) 单源最短路径
- 算法过程:
- 有两个顶点集S和T,S中存放已找到最短路径的顶点,T中存放图中剩余顶点,初始状态时,S中只包含源点V0;
- 然后不断从T中选取到顶点V0路径最短的顶点V1,并加入集合S,集合S每加入一个新的顶点,都要修改V0到集合T中各个顶点的最短路径长度。
- 不断重复这个过程,直至T中顶点全部并入S
- 时间复杂度:O(V的平方)
- tip:不适用带负权值的图和回路。
- 算法过程:
- Floyd算法:(动态规划)各顶点之间的最短路径
- 算法过程:
- 初始时,对于任意两个顶点Vi和Vj,若他们之间存在边,以该边权值作为它们之间的最短路径
- 之后逐步尝试在原路径中加入顶点k(0.1.2…) 作为中间顶点
- 若加入中间顶点后,得到的路径比原来路径短,则以此路径代替原路径。
- 时间复杂度:O(V的三次方)
- tip:不允许带负权回路。
- 算法过程:
拓扑排序:
-
每个有向无环图都有一个或多个拓扑排序
-
概念:对图中所有顶点,如果存在一条从顶点A到顶点B的路径,那么排序中A在B的前面。
-
操作过程:
- 选择一个没有前驱的顶点并输出。
- 删除该顶点和以该顶点为起点的有向边。
- 重复以上过程直到网中不存在无前驱的顶点。
逆拓扑排序:
- DFS算法
- 操作过程:
- 与拓扑排序相反
关键路径:
- 概念:从源点到汇点的所有路径中,具有最大路径长度的路径
- 关键活动:关键路径上的活动
第七章:查找
7.1、查找的基本概念
- 平均查找长度:所有查找过程中进行的关键字比较次数的平均值。
- 平均查找长度是衡量查找算法效率的最主要指标
7.3、顺序查找、折半查找
顺序查找:
-
一般线性表
- 思想:从线性表的一端开始,逐个检查关键字是否满足给定条件。
- 平均查找长度:成功:(n+1)/2 ;失败:n+1
-
优化
-
表中元素有序,减小了查找失败的平均查找长度。
-
按被查概率降序排列,减少查找成功的平均查找长度
-
折半查找:
- 又称二分查找,仅适用于有序的顺序表
- 基本思想:
- 先将给定的值与表中间位置的元素比较,若相等,查找成功
- 若不等,则所要查找的元素只能在表的前半部分或后半部分
- 然后在缩小的范围内继续进行同样的查找,如此重复。
- 判定树
- 时间复杂度:O(logn)
分块查找
- 又称索引顺序查找,吸取了顺序查找和折半查找的优点
- 基本思想:将查找表分为若干子块,块内可以无序,块间必须有序,第一个块中最大关键字小于第二个块中所有记录的关键字,以此类推。
- 查找过程:1、在索引表中确定待查记录所在的块。 2、在块内顺序查找。
- 若查找表是动态查找表? :用链式存储。
7.4、B树和B+树
**B树:**全部节点是关键字
-
又称多路平衡查找树,m阶B树表示树中每个结点最多有m棵子树,最多含有m-1个关键字。
-
特点
-
节点的子树个数比关键字个数多1
-
除根结点的所有非叶节点至少有⌈m/2⌉棵子树,至少有⌈m/2⌉-1个关键字。
-
所有叶节点都出现在同一层次上,且不带信息。
-
节点中,从左到右递增有序,关键字两侧,左指针指向的子树关键字小于。。。右指针指向的大。。
-
-
查找
- 从根开始,先在有序表中进行查找,若找到则查找成功,否则按照对应的指针信息到所指的子树中进行查找。
-
插入
- 定位:按照B树的查找算法,找到最低层的某个非叶节点
- 插入:执行插入,节点内关键字大于 m-1 个时进行分裂操作。
-
删除
- 根据要删除的结点的不同位置执行不一样的操作
- 非终端(用前驱或后继来代替)、终端(直接删除、兄弟够借、兄弟不够借)
**B+树:**叶节点是关键字
- m阶B+树最多有m棵子树,最多有m个关键字
- 特点:
- 节点的子树个数与关键字个数相等。
- 所有非叶节点包含全部关键字及指向相应记录的指针,叶节点中将关键字按大小顺序排列,并且相邻叶节点相互连接(支持按关键字顺序查询)
- 所有分支节点仅包含它的各个子结点中关键字的最大值及指向其子节点的指针。
B树和B+树的区别:
- B树中n个关键字对应n+1棵子树,而B+树每个关键字对应一棵子树。
- B+树中叶节点包含全部关键字,B树中,全部关键字由叶节点和非叶节点共同构成。
- B+树中,叶节点包含信息,所有非叶节点仅起索引作用(非叶节点的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址),B树的叶节点不带信息。
7.5、散列表
“用空间换时间”
**散列函数:**把查找表中的关键字映射为该关键字对应的地址的函数
冲突:两个或两个以上不同的关键字映射为同一地址
**散列表:**一种数据结构,建立关键字和存储地址之间的一种直接映射关系。
散列函数的构造方法:
原则:地址应 等概率、均匀的分布,减少冲突的发生
-
直接定址法:
-
线性函数:H(key) = a*key+b
-
计算简单,不会冲突。适合关键字分布连续的情况,否则空位较多,造成存储空间的浪费
-
-
除留取余法:
- H(key) = key%b 用质数取模,分布更均匀,冲突更少
- 关键是选好p的数值
-
数字分析法:
- 例如以手机号作为关键字
-
平方取中法:
- 取关键字平方值的中间几位作为散列地址。
处理冲突的方法:
-
1、开放定址法:
H = ( H(key) + d) % m m:散列表表长
增量d的选取 ?
- 线性探测法:
- d = 0、1、2…
- 会出现聚集现象,降低查找效率。解决办法:平方探测法
- 平方探测法:
- d = 0方、1方、-1方、2方、-2方…
- 可以避免堆积;m必须是一个可以写成4K+3的素数。
- 再散列法:
- 使用两个散列函数
- 伪随机序列法:
- d=伪随机序列
- 线性探测法:
-
2、拉链法:
- 把所有的同义词存储在一个线性链表中
- 适用:经常进行插入和删除的情况
散列查找及性能分析:
- 装填因子:表中记录数/散列表长度 ; 表示一个表的装满程度,越大越容易发生冲突。
- 散列表的查找效率取决于:散列函数、处理冲突的方法、装填因子。
第八章:排序
8.1、排序的基本概念
- 重新排列表中数据,使表中关键字有序的过程
- 根据数据是否在内存中进行处理分为:内部排序 和 外部排序
8.3、插入排序:
- 基本思想:每次将一个待排序的记录插入到前面已排序好的子序列中,直到全部记录插入完成。
直接插入排序:
- 思想:首先以一个元素有序的序列,然后将后面的元素依次插入到有序的序列中合适的位置。
- 实现过程:整个序列分为两部分,有序序列和无序序列。从无序序列中取出一个元素插入有序序列。
- 先查找除元素的待插入位置,再插入
- 性能:
- 时间复杂度:O(n的平方)
- 空间复杂度:O(1)
- 稳定
折半插入排序:
-
仅适用有序表
-
先利用折半查找找到插入位置,再插入元素。
-
性能:
- 时间复杂度:最好O(nlogn);平均O(n的平方)
- 空间复杂度:O(1)
- 稳定
希尔排序:
- 本质还是插入排序,只不过把待排序列分为几个子序列,再分别对这几个子序列进行直接插入排序。
- 实现过程:
- 先选取一个小于n的步长d,把表中数据分为d组,在各组内进行直接插入排序
- 缩小步长d,继续上述步骤
- 重复上述过程,直到d = 1。
- 性能:
- 时间复杂度:无法计算
- 空间复杂度:O(1)
- 不稳定(可能划分到不同子表)
8.4、交换排序:
根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置。
冒泡排序:
- 实现过程:
- 从后往前(或从前往后)两两比较相邻元素的值,若为逆序则交换他们;
- 将最小或最大的元素交换到待排序列的第一个位置。
- 再进行下一趟排序,直到某一趟排序未发生交换,则提前结束。
- 性能:
- 时间复杂度:O(n的平方)
- 空间复杂度:O(1)
- 稳定
快速排序:
- 实现过程:
- 首先选取一个“枢纽”,然后通过一段排序,将排序表分为两个部分,左边小于该枢纽值,右边大于改枢纽值。
- 然后对这两个部分分别递归的重复上述步骤。
- 直到所有元素都放在最终位置。
- 性能:
- 时间复杂度:O(nlogn)
- 空间复杂度:平均O(logn)
- 不稳定
8.5、选择排序
在后面的待排元素中选取关键字最小的元素,放入有序子序列的最后,直到待排元素只剩一个元素。
简单选择排序:
-
实现过程:
- 进行n趟排序,第 i 趟排序从 第 i~n 个元素中选择关键字最小的元素与第 i 个元素交换
- i 每增加一次,没趟排序可以确定一个元素的最终位置
- 经过 n-1 趟排序可使得整个排序表有序
-
性能:
- 时间复杂度:O(n的平方)
- 空间复杂度:O(1)
- 不稳定
堆排序:
- 堆:一棵完全二叉树
- 大根堆:每个结点的值都不小于它的左右孩子节点的值
- 小根堆:每个结点的值都不大与他的左右孩子结点的值
- 实现过程:
- 建堆:按照大根堆或小根堆的规则将待排元素建立相应的二叉树。
- 输出根结点,调整堆:
- 重复过程,直至堆里仅剩一个元素。
- 性能:
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)
- 不稳定
归并排序—二路归并
- n个记录,两两归并,直到合并成一个有序表。
- 性能:
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)
- 稳定
基数排序:
- 最高位优先法、最低位优先法
- 实现过程:
- 借助“分配” 和 “收集” 两种操作对关键字排序,直到左右关键字被 分配和收集 完毕。
- 性能:
- 时间复杂度:O(d*( n+r )) ;d趟分配和收集,n个关键字,r个队列;一趟分配O(n),一趟收集O®
- 空间复杂度:O®
- 稳定
排序算法小结:
- 快排、堆排、归并排序 时间复杂度均为O(nlogn)
- 快排可能出现最坏的情况,待排序列有序,时间复杂度O(n的平方)
- 堆排最好最坏平均时间复杂度都为O(nlogn),且需要辅助空间比快排小
- 但是快排和堆排不稳定
- 归并排序稳定
8.6、外部排序
- 对大文件进行排序,需要将待排序文件存储在外存上,排序时把数据一部分一部分地调入内存进行排序,在排序过程中多次进行内存和外村之间的交换。
归并排序
-
实现过程:
-
将外存上的文件分成若干长度依次读入内存并利用内部排序方法对它们进行排序,将排序后的有序文件写回外存。
-
再将归并段继续归并,直到得到整个有序文件。
-
-
耗费时间:
- 内部排序所需时间 + 外存读写时间 + 内部归并所需时间
-
优化:
- 增大归并路数 ====》引入败者树
- 减少归并段个数
败者树:
-
在败者树中,用父结点记录其左右子结点进行比赛的败者,而让胜者参加下一轮的比赛。败者树的根结点记录的是败者,需要加一个结点来记录整个比赛的胜利者。
-
采用败者树可以简化重构的过程。
-
败者树重构过程如下:
-
将新进入选择树的结点与其父结点进行比赛:将败者存放在父结点中;而胜者再与上一级的父结点比较。
-
比赛沿着到根结点的路径不断进行,直到ls[1]处。把败者存放在结点ls[1]中,胜者存放在ls[0]中。
-
置换选择排序
- 用来生成初始归并段 (MINIMAX记录)
- 生成的初始归并段长度不固定 =======》如何组织长度不等的初始归并段使得I/O次数最少? :引入哈夫曼树的思想
=》最佳归并树:
- 让记录数少的初始归并段最先归并,记录数多的初始归并段最晚归并,就可以建立I/O次数最少的最佳归并树
- 若初始归并段不足以构成一棵严格的二叉树,需添加长度为0的“虚段”。
- 计算虚段的个数?