大话数据结构
前言
- 人们无法理解他们没有经历过的事情。--尼采
- 吸引学生的注意力,比较好的办法是从他们比较熟知的知识开始。
- A picture is worth a thousand words.(一图值千言) --谚语
- 挫折感是很强烈的
- 涉及到比较复杂的算法知识,需要读者有一定的数学修养和逻辑思维能力,否则可能书籍的后半部分阅读起来会比较吃力。
- 《如何阅读一本书》,阅读越主动,效果越好。
- “最淡的墨水也胜于最强的记忆!”,笔记减缓阅读的速度,有助于大脑学习。
第一章 数据结构绪论
1.1 开场白
1.2 你数据结构怎么学的?
1.3 数据结构起源
1.4 基本概念和术语
1.4.1 数据
数值数据、字符数据(包括声音、视频等)
1.4.2 数据元素
组成数据的、有一定意义的基本单位,也称为记录
1.4.3 数据项
数据不可分割的最小单位
1.4.4 数据对象
性质相同的数据元素的集合,是数据的子集
1.4.5 数据结构
是相互之间存在一种或多种特定关系的数据元素的集合
1.5 逻辑结构与物理结构
1.5.1 逻辑结构
数据对象中数据元素之间的相互关系
1.集合结构
数据元素同属于一个集合,没有其他关系,平等关系
2.线性结构
数据元素是一对一关系
3.树形结构
数据元素之间存在一对多的层次关系
4.图形结构
数据元素是多对多的关系
1.5.2 物理结构
也叫存储结构
是指数据的逻辑结构在计算机中的存储形式
两种形式:顺序存储和链式存储
1.顺序存储结构
把数据元素存放在地址连续的存储单元里
逻辑关系=物理关系
2.链式存储结构
数据元素存放在任意的存储单元
引入指针存放数据元素的位置
逻辑结构是面向问题的,而物理结构就是面向计算机的,其基本的目标就是将数据及其逻辑关系存储到计算机的内存中。
1.6 抽象数据类型
1.6.1 数据类型
指一组数值相同的值的集合及定义在此集合上的一些操作的总称
C语言中数据类型:
- 原子类型:不可分解的基本类型
- 结构类型:可分解
抽象是指抽取出事物具有的普遍性的本质。抽象是一种思考问题的方式,它隐藏细节,只保留实现目标所必需的信息。
1.6.2 抽象数据类型
是指一个数学模型及定义在该模型上的一组操作
1.7 总结回顾
1.8 结尾语
第2章 算法
2.1 开场白
2.2 数据结构与算法关系
梁山伯与祝英台、罗密欧与朱丽叶
2.3 两种算法的比较
高斯:求1+2+3+...+100的值的传统算法和高效算法
2.4 算法定义
2.5 算法的特性
输入、输出、有穷性、确定性和可行性
2.5.1 输入输出
2.5.2 有穷性
2.5.3 确定性
无歧义
2.5.4 可行性
2.6 算法设计的要求
2.6.1 正确性
2.6.2 可读性
2.6.3 健壮性
2.6.4 时间效率高和存储量低
2.7 算法效率的度量方法
2.7.1 事后统计方法
缺点很多,不科学,不准确,基本不用
2.7.2 事前分析估算方法
每天打游戏与每天学习的差别
2.8 函数的渐进增长
判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数
2.9 算法时间复杂度
2.9.1 算法时间复杂度定义
2.9.2 推导大O阶方法
2.9.3 常数阶
O(1)
2.9.4 线性阶
O(n)
2.9.5 对数阶
O(㏒n)
2.9.6 平方阶
O(n²)
2.10 常见的时间复杂度
2.11 最坏情况与平均情况
2.12 算法空间复杂度
不是讨论重点,时间复杂度才是重点。
2.13 总结回顾
2.14 结尾语
愚公移山固然可敬,但发明炸药和推土机,可能更加实在和聪明。
第3章 线性表
0个或多个数据元素的有限序列
3.1 开场白
幼儿园的小朋友按次序排队一样
3.2 线性表的定义
前驱元素、后继元素
空表
3.3 线性表的抽象数据类型
基本操作:创建、初始化、置空、读表、查表、求长、插入、删除等
及各种基本操作的组合
3.4 线性表的顺序存储结构
3.4.1 顺序存储定义
用一段地址连续的存储单元一次存储线性表的数据元素
3.4.2 顺序存储方式
起始位置、最大长度、当前长度
3.4.3 数据长度与线性表长度区别
3.4.4 地址计算方法
存储单元的编号成为地址
线性表时间复杂度为O(1)
随机存取结构
3.5 顺序存储结构的插入和删除
3.5.1 获得元素操作
3.5.2 插入操作
插入位置之后的元素统一后移一个单位,注意越界问题
3.5.3 删除操作
插入和删除操作的时间复杂度都是O(n)
3.5.4 线性表顺序存储结构的优缺点
3.6 线性表的链式存储结构
3.6.1 顺序存储结构不足的解决办法
3.6.2 线性表链式存储结构定义
任意的存储单元
数据域、指针域组成一个结点
单链表:结点中只有一个指针域
头结点
3.6.3 头指针和头结点的异同
头结点不一定会有
头指针一定会有
3.6.4 线性表链式存储结构代码描述
3.7 单链表的读取
必须从头开始找
3.8 单链表的插入与删除
3.8.1 单链表的插入
事先判断插入位置是否存在
3.8.2 单链表的删除
事先判断删除位置是否存在
插入和删除的时间复杂度均为O(n)
3.9 单链表的整表创建
头插法、尾插法
3.10 单链表的整表删除
3.11 单链表结构与顺序存储结构优缺点
- 存储分配方式
- 时间性能
- 空间性能
3.12 静态链表
用数组描述的链表叫做静态链表
应用场景:为没有指针的高级语言设计的一种单链表能力
3.12.1 静态链表的插入操作
3.12.2 静态链表的删除操作
3.12.3 静态链表的优缺点
3.13 循环链表
3.14 双向链表
新增指向前驱结点的指针域
用空间来换时间
3.15 总结回顾
线性表的顺序结构和链式存储结构是其他数据结构的基础,很重要
3.16 结尾语
- 鱼塘的人工鱼和河中的野鱼
- 舒适环境很难培养出坚强品格,被安排好的人生,也很难做出伟大事业。
- 不怕苦,吃苦半辈子,怕吃苦,吃苦一辈子。
第4章 栈与队列
栈:仅在表尾进行插入和删除操作
队列:只允许在一端进行插入,另一端进行删除
4.1 开场白
左轮手枪与弹夹式手枪
4.2 栈的定义
4.2.1栈的定义
栈顶:允许插入和删除的一端
栈底
后进先出 LIFO结构
4.2.2 进栈出栈变化形式
4.3 栈的抽象数据类型
4.4 栈的顺序存储结构及实现
4.4.1 栈的顺序存储结构
栈底:下标为0的一端
类比游标卡尺
4.4.2 栈的顺序存储结构--进栈操作
4.4.3 栈的顺序存储结构--出栈操作
4.5 两栈共享空间
4.6 栈的链式存储结构及实现
4.6.1 栈的链式存储结构
链栈
4.6.2 栈的链式存储结构--进栈操作
4.6.2 栈的链式存储结构--出栈操作
进栈、出栈的时间复杂度均为O(1)
4.7 栈的作用
简化程序设计
类比用脚走路和乘坐交通工具
很多高级语言都有对栈结构的封装
4.8 栈的应用--递归
4.8.1 斐波那契数列实现
兔子繁殖问题
数学模型
4.8.2 递归定义
直接调用自己或通过一系列的调用语句间接地调用自己的函数
递归会建立函数副本,消耗时间和内存
4.9 栈的应用--四则运算表达式求值
4.9.1 后缀(逆波兰)表示法定义
四则运算表达式的一种新的显示方式,巧妙地解决了程序四则运算的难题,所有的符号都是在要运算数字的后面出现
4.9.2 后缀表达式计算结果
遇到数字就进栈,遇到符号,就将处于栈顶的两个数字出栈进行运算,运算结果进栈
4.9.3 中缀表达式转后缀表达式
标准四则运算表达式即中缀表达式
4.10 队列的定义
先进先出的线性表,FIFO
队尾:允许插入
队头:允许删除
4.11 队列的抽象数据类型
是一种特殊的线性表
4.12 循环队列
4.12.1 队列顺序存储的不足
入队列操作,时间复杂度为O(1)
出队列操作,时间复杂度为O(n)
假溢出:内存利用率不高
front指针:指向队头元素
rear指针:指向队尾元素的下一个位置
4.12.2 循环队列定义
头尾相接的顺序存储结构的队列
4.13 队列的链式存储结构及实现
简称链队列
4.13.1 队列的链式存储结构--入队操作
入队:链表尾部插入结点
4.13.2 队列的链式存储结构--出队操作
出队:头结点的后续结点出队,将头结点的后继改为它后面的结点
4.14 总结回顾
栈和队列都是特殊的线性表,只不过对插入和删除操作做了限制。
栈:
- 顺序栈(两栈共享空间的实现)
- 链栈
队列: - 顺序队列(循环队列的实现)
- 链队列
4.15 结尾语
第5章 串
由零个或多个字符组成的有限序列,又叫字符串
5.1 开场白
5.2 串的定义
早期计算机主要做一些计算工作,后来在计算机上作非数值处理的工作越来越多,就不得不引入对字符的处理。
空串用“”或希腊字母Φ表示
空格串、子串与主串
5.3 串的比较
通过组成串的字符之间的编码来进行的,而字符的编码指的是字符在对应字符集中的序号。
标准ASCII编码:7位二进制数表示一个字符,总共128个,后来拓展到8位二进制数,可以表示256个字符。
Unicode编码:常用的是由16位二进制数表示一个字符,约65万多个字符,足够表示世界上所有语言的所有字符,为了和ASCII兼容,前256个字符和ASCII码完全相同。
类比查字典
5.4 串的抽象数据类型
串的基本操作和线性表的基本操作有很大区别,线性表关注的是单个元素的操作,串更多的是查找子串位子、得到指定位置子串、替换子串等操作。
5.5 串的存储结构
5.5.1 串的顺序存储结构
容易越界
5.5.2 串的链式存储结构
为了避免浪费内存空间,可以在一个结点存放多个字符
5.6 朴素的模式匹配算法
子串的定位操作通常称做串的模式匹配。
对主串的每一个字符作为子串开头,与要匹配的字符串进行匹配,对子串做大循环,每个字符开头做T的长度的小循环,直到匹配成功或全部遍历完成为止。
太过低效。
5.7 KMP模式匹配算法
以三个研究过该算法的前辈的名字缩写命名。
5.7.1 KMP模式匹配算法原理
5.7.2 next数组值推导
5.7.3 KMP模式匹配算法实现
KMP算法仅当模式与子串之间存在许多“部分匹配”的情况下才体现出它的优势,否则两者差异并不明显。
KMP模式匹配算法改进
5.7.4 KMP模式匹配算法改进
5.7.5 nextval数组值推导
5.8 总结回顾
5.9 结尾语
第6章 树
空树
根
根的子树
6.1 开场白
6.2 树的定义
线性结构:一对一
树形结构:一对多
根节点是唯一的
子树个数没有限制,但它们一定是互不相交的
6.2.1 结点分类
结点的度:结点拥有的子树数
叶结点或终端结点:度为0
非终端结点或分支结点:度不为0
内部结点:除根节点之外的分支结点
树的度:树内各结点的度的最大值
6.2.2 结点间的关系
孩子
双亲
兄弟
祖先
子孙
6.2.3 树的其他相关概念
结点的层次
堂兄弟
树的深度或高度
森林:m棵互不相交的树的集合
6.3 树的抽象数据类型
6.4 树的存储结构
6.4.1 双亲表示法
每个结点中,附设一个指示器指示其双亲结点到链表中的位置
6.4.2 孩子表示法
每个结点有多个指针域,其中每个指针指向一棵子树的根结点,也叫做多重链表表示法。
6.4.3 孩子兄弟表示法
6.5 二叉树的定义
左子树和右子树
6.5.1 二叉树的特点
- 每个结点最多有两棵子树
- 左子树和右子树是有顺序的,次序不能任意颠倒
- 即使结点只有一棵子树,也要区分它是左子树还是右子树
6.5.2 特殊二叉树
1.斜树:左斜树、右斜树
2.满二叉树:最完美、最平衡的二叉树
3.完全二叉树:满二叉树一定是一棵完全二叉树,但完全二叉树不一定是满二叉树
6.6 二叉树的性质
6.6.1 二叉树性质1
6.6.2 二叉树性质2
6.6.3 二叉树性质3
6.6.4 二叉树性质4
6.6.5 二叉树性质5
6.7 二叉树的存储结构
6.7.1 二叉树顺序存储结构
顺序存储结构一般只用于完全二叉树
6.7.2 二叉链表
data:数据域
lchild:左孩子指针域
rchild:右孩子指针域
6.8 遍历二叉树
6.8.1 二叉树遍历原理
访问
次序
6.8.2 二叉树遍历方法
1.前序遍历
2.中序遍历
3.后序遍历
4.层序遍历
6.8.3 前序遍历算法
6.8.4 中序遍历算法
6.8.5 后序遍历算法
6.8.6 推导遍历结果
- 已知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树
- 已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树
6.9 二叉树的建立
扩展二叉树
6.10 线索二叉树
6.10.1 线索二叉树原理
指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树。
线索二叉树等于是把一棵二叉树转变成了一个双向链表。
对二叉树以某种次序遍历使其变为线索二叉树的过程称作是线索化。
ltag、rtag区分是指向孩子还是指向前驱或后继的。
6.10.2 线索二叉树结构实现
线索化的实质就是将二叉树链表中的空指针改为指向前驱或后继的线索。所以线索化的过程就是在遍历的过程中修改空指针的过程。
如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构是很不错的。
6.11 树、森林与二叉树的转换
6.11.1 树转换为二叉树
6.11.2 森林转换为二叉树
6.11.3 二叉树转换为树
6.11.4 二叉树转换为森林
判断一棵二叉树能够转换成一棵树还是森林的标准:只要看这棵二叉树的根节点有没有右孩子,有就是森林,没有就是一棵树。
6.11.5 树与森林的遍历
6.12 赫夫曼树及其应用
6.12.1 赫夫曼树
压缩文件:重新编码
赫夫曼编码
成绩评级算法的优化
6.12.2 赫夫曼树定义与原理
路径:从树中一个结点到另一个结点之间的分支
路径长度:路径上的分支数目
树的路径长度:从树根到每一结点的路径长度之和
赫夫曼树:带权路径长度WPL最小的二叉树称为赫夫曼树,也称为最优二叉树。
寻找最优二叉树的步骤。
6.12.3 赫夫曼编码
解决远距离通信(主要是电报)的数据传输的最优化问题
6.13 总结回顾
需要在理解的基础上去记忆。
二叉树的遍历,前序、中序、后序以及层序遍历都是需要熟练掌握的。
6.14 结尾语
第7章 图
7.1 开场白
7.2 图的定义
线性表:线性关系
树形结构:类比人类族谱,一对多
图:更复杂,结点之间的关系可以是任意的
元素:线性表中的单位
结点:树中的数据元素
顶点:图中的数据元素
线性关系:线性表中相邻元素之间的关系
层次关系:树结构中,相邻两层结点的关系
边:图中任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边表示
7.2.1 各种图的定义
无向边:顶点之间的边没有方向
无序偶对
无向图
无向完全图
有向边:也称为弧
弧头、弧尾
有向图
有向完全图
简单图
稀疏图、稠密图
权:与图或弧相关的数
网:带权的图
子图
7.2.2 图的顶点与边间关系
互为邻接点(有向图)
邻接到、邻接自(无向图)
顶点的度:和顶点相关联的边的数目(有向图)
入度、出度、度=入度+出度(有向图)
顶点序列:一个顶点到另一个顶点的所有路径
路径长度:边或弧的数目
回路或环:首尾顶点相同的路径
简单路径、简单回路、简单环
7.2.3 连通图相关术语
连通图(无向图)
连通分量(无向图)
强连通图(有向图)
强连通分量(有向图)
有向树
7.2.4 图的定义与术语总结
7.3 图的抽象数据类型
7.4 图的存储结构
图的存储结构更复杂。不能用顺序存储结构来表示,也不推荐多重链表的方式,会造成很多存储单元的浪费,或者操作的不便。
7.4.1 邻接矩阵
顶点:一维数组存储
边或弧:二维数组存储
邻接矩阵:一维+二维
无向图的边数组是一个对称矩阵。
网的邻接矩阵,需要包含权值信息。
7.4.2 邻接表
邻接矩阵的缺点:边数较少(稀疏图)时,会极大浪费存储空间。
邻接表:数组和链表相结合的存储方式称为邻接表。
边表(无向图)
弧尾的出边表(有向图)
统计顶点的出度:邻接表很清晰
统计顶点的入度:逆邻接表
网的邻接表:边表结点中新增weight数据域
7.4.3 十字链表
正向思维、逆向思维、整合思维
同时统计出度和入度:邻接表+逆邻接表=十字链表
7.4.4 邻接多重表
为了简化无向图的边操作
7.4.5 边集数组
关注的是边的集合,查找度效率太低。
7.5 图的遍历
7.5.1 深度优先遍历
也称深度优先搜索,简称DFS.
走迷宫策略。
7.5.2 广度优先遍历
也称广度优先搜索,简称BFS.
深度与广度算法在时间复杂度上是一样的,不同之处在于对顶点访问的顺序不同。
7.6 最小生成树
构造连通网的最小代价生成树称为最小生成树。
7.6.1 普里姆算法
7.6.2 克鲁斯卡尔算法
7.7 最短路径
网图最短路径:权值之和最少的路径,源点、终点。
人脑是用来创造而不是做枯燥复杂的计算的。
7.7.1 迪杰斯特拉算法
解决了从某个源头到其余各顶点的最短路径问题。
7.7.2 弗洛伊德算法
所有顶点至所有顶点的最短路径问题可以选择弗洛伊德算法。
7.8 拓扑排序
7.8.1 拓扑排序介绍
AOV网:表示工程,用顶点表示活动,用弧表示活动之间的优先关系的有向图。
对一个有向图构造拓扑序列的过程。
7.8.2 拓扑排序算法
入度域
7.9 关键路径
AOE网:表示工程,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间的带权有向图。
路径长度
关键路径、关键活动
7.9.1 关键路径算法原理
7.9.2 关键路径算法
存在多条关键路径的有向无环图,只提高一条关键路径上的关键活动的速度并不能导致整个工程缩短工期,而必须同时在几条关键路径上提高活动的速度。
就像仅仅有事业的成功,而没有健康的身体以及快乐的生活,压根谈不上幸福的人生。
7.10 总结回顾
7.11 结尾语
第8章 查找
8.1 开场白
搜索引擎:网络蜘蛛
关键字的云端之旅
8.2 查找概论
- 查找表
- 关键字(键值)、关键码
- 主关键字、主关键码
- 次关键字、次关键码
查找方式 - 静态查找表:只查询检索
- 动态查找表:查找过程中同时插入或删除
8.3 顺序表查找
顺序查找(线性查找)
8.3.1 顺序表查找算法
循环查找
8.3.2 顺序表查找优化
哨兵
参考:顺序表查找优化(哨兵元素的重要作用)
8.4 有序表查找
有序排序
时间复杂度:O(n)
8.4.1 折半查找
- 猜数字游戏
- 折半查找又称二分查找
- 前提:关键码有序
- 时间复杂度:O(㏒n)
8.4.2 插值查找
- 翻字典时有意识的往前或往后翻。
- 数学推导过程
- 核心是插值的计算公式:与最大最小记录的关键字比较后的查找方法
8.4.3 斐波那契查找
- 黄金分割原理
- 折半查找是进行加法和除法运算
- 插值查找是进行复杂的四则运算
- 斐波那契查找只是最简单的加减法运算
- 海量数据的查找过程中,细微差别也可能会影响查找效率
- 三种查找本质上是分隔点的选择不同,各有优劣
8.5 线性索引查找
- 实际上,考虑时间代价,很多数据都不是关键字有序的。
- 索引:加快查找速度而设计的一种数据结构。关键字与对应的记录相关联的过程。
- 线性索引、树形索引、多级索引
- 线性索引(索引表):稠密索引、分块索引、倒排索引
8.5.1 稠密索引
- 本子记录家中所有物品的位置,本子就是索引
- 稠密索引的索引表一定是按照关键码有序的排列
- 缺点:数据集非常大时,索引表规模很大,性能下降
8.5.2 分块索引
- 类比图书馆的图书分类体系
- 先分块再建立索引,减少索引项的个数
- 块内无序、块内有序
- 分块索引在兼顾了对细分块不需要有序的情况下,大大增加了整体查找的速度,所以普遍被用于数据库表查找等技术中。
8.5.3 倒排索引
- 搜索引擎的高效查找
8.6 二叉排序树
- 老虎追赶问题。
- 所谓优势只不过是比别人多深入思考一点而已。
- 提高查找和插入删除关键字的速度
8.6.1 二叉排序树查找操作
8.6.2 二叉排序树插入操作
8.6.3 二叉排序树删除操作
8.6.4 二叉排序树总结
- 查找性能取决于二叉排序树的形状
- 最好是构建一棵平衡的二叉排序树
8.7 平衡二叉树(AVL树)
- 每一个节点的左子树和右子树的高度差至多为1
- 平衡因子:结点左子树减去右子树深度,-1、0或1
- 前提必须是一棵二叉排序树
8.7.1 平衡二叉树实现原理
- 不停旋转,左旋或右旋
- 一旦有不平衡的情况,马上处理
8.7.2 平衡二叉树实现算法
- 把不平衡消灭在萌芽
8.8 多路查找树(B树)
- 语言是沟通的工具,文字是记录存证的工具,而文字化的过程,又可以让思考彻底沉淀,善于使用文字的人,通常是深沉而严谨的。
- 内存由硅制的存储芯片组成,每个存储单位代价都要比磁存储技术昂贵两个数量级
- 之前讨论的数据结构,考虑的都是内存中的运算时间复杂度
- 涉及到外部存储设备,时间复杂度的计算就会发生变化,考虑对外部设备的访问时间
- 多路查找树,其每一个节点的孩子数可以多于两个,且每一个结点处可以存储多个元素
8.8.1 2-3树
- 多路查找树:每一个结点都具有两个或三个孩子
- 2结点:1个元素+(2个孩子或0个孩子)
- 3结点:一大一小2个元素+(3个孩子或0个孩子)
- 所有叶子都在同一层次
- 2-3树的插入实现
- 2-3树的删除实现
8.8.2 2-3-4树
- 4结点:小中大3个元素+(4个孩子或没有孩子)
8.8.3 B树
- 是一种平衡的多路查找树
- B树的阶:结点最大的孩子数目
8.8.4 B+树
8.9 散列表查找(哈希表)概述
- 之前的查找方法,“比较”都不可避免
8.9.1 散列表查找定义
- 存储位置 = f(关键字)
- 散列技术:在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)
- 散列函数(哈希函数):对应关系f
- 散列表(哈希表):采用散列技术将记录存储在一块连续的存储空间,这块存储空间的名称
8.9.2 散列表查找步骤
- 第一步(存储):每一个记录,都需要用同一个散列函数计算出地址再存储
- 第二步(查找):通过同样的散列函数计算记录的散列地址
- 散列技术的记录之间不存在什么逻辑关系,只与关键字有关联,所以是面向查找的存储结构
- 优点:简化了查找的比较过程,效率极高
- 缺点:不具备很多常规数据结构的能力
- 散列技术关键:散列函数的简单、均匀、存储利用率高
- 冲突:key1≠key2 但是f(key1) = f(key2)
- 同义词:key1和key2
- 冲突不能完全避免,只能尽量减小
8.10 散列函数的构造方法
- 计算简单
- 散列地址分布均匀
8.10.1 直接定址法
- 取关键字的某个线性函数值为散列地址
8.10.2 数字分析法
- 抽取:使用关键字的一部分来计算散列存储位置
8.10.3 平方取中法
- 不知道关键字的分布,位数较小
8.10.4 折叠法
8.10.5 除留余数法
- f(key) = key mod p (p ≦ m)
- 散列表表厂为m,通常p为小于或等于表厂的最小质数,或不包含小于20质因子的合数
8.10.6 随机数法
- f(key) = random(key)
8.11 处理散列冲突的方法
8.11.1 开放定址法
- 线性探测法
- 堆积:不是同义词,却争夺一个地址
- 二次探测法:增加平方运算
- 随机探测法:伪随机数、随机种子
8.11.2 再散列函数法
- 多准备几个散列函数
8.11.3 链地址法
- 同义词子表
- 存在遍历单链表的性能损耗
8.11.4 公共溢出区法
- 为所有冲突的关键字建立一个公共的溢出区来存放
8.12 散列表查找实现
8.12.1 散列表查找算法实现
8.12.2 散列表查找性能分析
- 散列函数是否均匀
- 处理冲突的方法
- 散列表的装填因子
8.13 总结回顾
8.14 结尾语
第9章 排序
9.1 开场白
9.2 排序的基本概念与分类
- 记录:数据元素
- 依据:关键字的大小
- 关键字可以是主关键字,也可以是次关键字,甚至是若干记录的组合
- 多个关键字的排序最终都可以转化为单个关键字的排序
9.2.1 排序的稳定性
- 关键字相同时,排序后前后顺序是否变化
9.2.2 内排序与外排序
- 内排序:记录个数少,排序时全放在内存中
- 外排序:记录个数太多,排序时需要在内外存之间多次交换数据
内排序算法的性能:
- 时间性能:排序往往属于系统的核心功能,比较和移动操作要尽可能的少
- 辅助空间
- 算法的复杂性:算法本身的复杂度
内排序常用方法:
- 插入排序
- 交换排序
- 选择排序
- 归并排序
简单算法:
- 冒泡排序
- 简单选择排序
- 直接插入排序
改进算法:
- 希尔排序
- 堆排序
- 归并排序
- 快速排序
9.2.3 排序用到的结构与函数
9.3 冒泡排序
9.3.1 最简单排序实现
- 冒泡基本思想:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止
- 最简单排序算法效率很低
9.3.2 冒泡排序算法
- 冒泡算法由来
9.3.3 冒泡算法优化
- 避免因已经有序的情况下的无意义循环判断
9.3.4 冒泡排序复杂度分析
9.4 简单选择排序
9.4.1 简单选择排序算法
9.4.2 简单选择排序复杂度分析
- 时间复杂度为O(n²)
- 性能上略优于冒泡排序
9.5 直接插入排序
- 类比理牌
9.5.1 直接插入排序算法
9.5.2 直接插入排序复杂度分析
- 时间复杂度为O(n²)
- 性能上优于冒泡和简单选择排序
9.6 希尔排序
Ⅸ变6的几种方法
- 翻转截取
- 加S变为SIX
- 加6变为IX6
超越历史,复杂度超越O(n²)
9.6.1 希尔排序原理
- 跳跃分割策略:将相距某个“增量”的记录组成一个子序列,保证基本有序
9.6.2 希尔排序算法
- 将相隔某个“增量”的记录组成一个子序列,实现跳跃式的移动
9.6.3 希尔排序复杂度分析
- 难点是增量的选取
- 并不是一种稳定的排序算法
9.7 堆排序
- 堆是特殊的完全二叉树
- 大顶堆:每个结点的值都大于或等于其左右孩子结点的值
- 小顶堆:每个结点的值都小于或等于其左右孩子结点的值
9.7.1 堆排序算法
9.7.2 堆排序复杂度分析
9.8 归并排序
9.8.1 归并排序算法
- 归并:将两个或两个以上的有虚表组合成一个新的有序表
- 归并排序、2路归并排序
9.8.2 归并排序复杂度分析
- 归并排序是一种比较占用内存,但却效率高且稳定的算法
9.8.3 非递归实现归并排序
- 大量使用递归会造成空间上的性能损耗
9.9 快速排序
- 20世纪十大算法之一
- 冒泡排序的升级
9.9.1 快速排序算法
- 基本思想:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
9.9.2 快速排序复杂度分析
- 由于关键字的比较和交换是跳跃进行的,因此,快速排序是一种不稳定的排序方法
9.9.3 快速排序优化
1.优化选取枢轴
- 关键字太大或太小都会影响性能
- 随机选取,全凭运气
- 三数取中法(小数组)
- 九数取中法(很大的数组)
2.优化不必要的交换
3.优化小数组时的排序方案 - 大材小用有时会变得反而不好用
- 小数组排序不需要使用快速排序法,用直接插入排序效果更好
4.优化递归操作 - 递归会消耗栈空间
5.了不起的排序算法 - 至少今天,快速排序在整体性能上,依然是排序算法王者
9.10 总结回顾
- 目前还没有十全十美的排序算法,有优点就会有缺点
- 综合指标:经过优化的快速排序是性能最好的排序算法,但是不同的场合我们也应该考虑使用不同的算法来应对它
9.11 结尾语
- 算法研究者:都是在“似乎不可能”的情况下,逐步提高排序算法的性能
- 不是不可能用四段、三段、一段直线一笔连九点,只是暂时还没有找到方法而已,所有的事情都是可能的,只是我们暂时还没有找到方法而已