1 讲算法更好的理解数据结构,相辅相成的关系
2 算法提高计算效率,那些公式是很必要的,是重要算法
算法定义:解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。
五个基本特征:输入,输出,有穷性,确定性和可行性
1 正确性:
无语法错误;
合法输入有满足要求输出;
非法输入有满足规格说明输出;
对于精心选择甚至刁难的数据有满足要求的输出结果
算法一般无法用程序证明,而是用数学方法证明,代价昂贵;一般到第三层次就可以了。
2 可读性
便于阅读、理解和交流
3 健壮性
当输入数据不合法时,算法也能做相关处理,而不是产生一场或者莫名其妙的结果
4 时间效率高和存储量低
1 事后统计
先写好算法,测试,统计时间
2 事前分析评估
消耗时间取决于四个因素:
1 算法采用的策略、方法;
2 编译产生的代码质量; 软件支持
3 问题的输入规模;
4 机器执行指令的速度 硬件性能
刨去软硬件因素,一个程序的运行时间取决于,算法的好坏和问题的输入规模
对于给定的两个函数f(n) 和g(n),如果存在一个整数N,使得对所有的n>N,f(n) 总是比g(n)大,那么我们说f(n) 的增长渐进快于g(n)
T(n) =O(f(n)) 随着问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称时间复杂度。 O(1)常数阶,线性阶,平方阶,对数阶(比如二分法)
最坏情况运行时间是一种保证,那就是运行时间将不会再坏了,通常我们提到的运行时间都是最坏情况的运行时间。
平均运行时间是所有情况中最有意义的,因为它是期望的运行时间。
存储程序本身的 指令、常数、变量、和输入数据、 还需要存储对数据操作的存储单元。 后面这个可能是指 临时存储
零个或多个数据元素的有限序列:序列(前驱和后继)、有限
在较复杂的线性表中, 一个数据元素可以由若干个数据项组成。
基本操作: 初始化,判断是否为空,清空,获取/定位,删除/插入,长度
其他复杂操作可以由这些基本操作组合而成
定义:用一段地址连续的存储单元依次存储线性表的数据元素
顺序存储结构需要三个属性:
- 存储空间起始位置:数组data的存储位置
- 最大存储容量: 数组的长度 Maxsize
- 线性表当前长度:length
由于存储位置是编号的,连续的,我们对每个线性表位置的存入或者取出数据,对于计算来说都是相等的时间,
存取时间性能为O(1). 我们把这种存储结构称为随即存储结构。
比较适合元素个数不太变化,存取数据的应用
除了存储数据元素信息,还要存储它后继元素的存储地址,即 逻辑关系。
这两部分信息组成数据元素的存储映像,称为节点(Node).
为了方便,常在第一个结点前附设一个结点,称为头结点。
获取第i个元素 就是从头开始找; O(n)
一个动态生成链表的过程,从空表的初始状态起,依次建立各元素节点
有两种方式:头插法和尾插法
经验性结论:
- 若线性表需要频繁查找,很少需要插入和删除是,宜采用顺序存储结构,反之用单链表结构;
有些语言没有指针,该怎么实现链表呢?
用数组实的现: 数组的元素有两个数据域组成,data和cul,cul相当于单链表中的next指针,存放该元素大的后继在数组中的下标
这种用数组描述的链表叫做静态链表,游标实现法。!!!
对于单链表,每个结点存储了向后的指针,前驱结点找不到
* 将单链表中终端结点的指针端由空指针改为头指针,整个链表就形成一个环* 循环链表
判断条件,p - next 不等于头结点,则循环未结束
用尾指针表示循环列表,这样查找头指针和终端指针都很方便,这样也很方便两个链表的合并
在单链表的每个结点中,再设置一个指向其前驱结点的指针域
* 栈 限定 仅在 表尾 进行插入和删除操作的线性表*
* 队列 只允许 在一段进行插入操作 另一端进行删除操作的线性表*
允许插入和删除的一端称为栈顶 top ,另一端称为 栈底 bottom : 先进后出的线性表 Last in First out LIFO
插入: 进栈 压栈 入栈
删除: 出栈 弹栈
栈:stack
插入:push ;弹出:pop 特殊 不同于单链表
由于栈本身是一个线性表,那么前面讨论的线性表的顺序存储和链式存储 也是适用的
顺序栈,定义一个top 变量来指示栈顶元素在数组中的位置 空栈 top = -1 数组首元素为0
进栈和出栈 操作都挺简单,时间复杂度O(1)
因为当空间不够用时,数组的扩展很麻烦;
定义一个数组, 里面存储两个类型相同的栈
top1和top2 是栈1和栈2的栈顶指针,可以想象只要它两不见面,两个栈可以一直使用。
top1 +1 = top2 时为栈满。
通常是当两个栈的空间需求有相反关系时使用,比如股票买卖,一方出,一方进。
链栈
链表有头指针,栈顶指针也是必须的,可以把栈顶放在单链表的头部,对于链栈来说,是不需要头结点的。
* 不存在栈满的情况,链栈为空就是top = NULL *
链栈的操作绝大部分和单链表类似,只是在插入和删除上特殊一些
push 和 pop 操作都很简单,没有任何循环操作,时间复杂度均为 O(1)
* 对比: *
顺序栈存取是定位方便,但是可能存在空间浪费;链栈要求每个元素都要有指针域,也会增加内存开销,长度无限制。
引入简化了程序设计的问题,划分了不同关注层次,使得思考范围缩小,更加聚焦于我们要解决问题的可信,如果总是使用素族,要分散精力去考虑数组的下标增减等细节问题,反而容易掩盖问题的本质。 高级语言都有栈结构的封装,可以直接使用。
注:这里提到的观点,是我学习中一直存在的问题,过于关注原理,而不是聚焦于问题的解决,容易拖延进度和受挫,毕竟还没有到真正天才的地步。 不管怎样站在前人肩膀上都是不错的。
斐波那契数列计算两种实现方式:迭代和递归
迭代: 循环,不需要反复调用函数副本,大量消耗时间和内存
递归: 选择结构,结构更清晰,简洁好理解
递归过程退回的顺序是它前行顺序的逆序。在退回过程中,可能要执行某些动作,包括恢复在前行过程中存储起来的某些数据。
* 编译器使用栈实现递归 *
* 只有碰到左括号,就将此左括号进栈,不管表达式有多少重括号,反正遇到左括号就进栈,而后面出现右括号时,就让栈顶的左括号出栈,期间让数字运算*
逆波兰: 后缀表达法,不需括号,巧妙解决了程序实现四则运算的难题。
9 + (3-1)3 + 10 /2 换成 : 9 3 1 - 3 + 10 2 / +
* 计算规则 * : 从左到右遍历表达式的每个数字和符号,遇到数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。
看看书中的实例 P136 挺好理解的
队列(queue):只允许在一端进行插入操作,而在另一端进行删除操作的线性表。 先进先出 FIFO
god 与 dog
顺序存储结构:
进队容易,出队都要移动,耗时;
不去限制队列的元素必须存储在数组的前n个单元这一条件,出对的性能就会大大增加,对头不一定在下标为0的位置。
引入两个指针,front 指针指向对头元素,rear指针指向队尾元素的下一个位置,这样当front等于rear时,此队列不是还剩一个元素,而是空队列。
把队列头尾相接的顺序存储结构称为循环队列。
串(string)是由零个或多个字符组成的有限序列,又名叫字符串。
序列说明串的相邻字符之间具有前驱和后继的关系。
常用ASCII编码,总共可以表示256个字符,足够满足以英语为主的语言和特殊符号进行输入、存储、输出等字符需要。
但是全世界有各种语言和文字,所以有了Unicode编码,16位的二进制数表示一个字符,65万多个字符。为了和ASCII码兼容,Unicode的前256个字符与ASCII码完全相同。
字符串的各种操作,用的还是比较多的。
顺序存储结构: 申请一个数组存放,然后有在0这个位置记录串长度,也有的在末尾加‘\0’
实际上这样的存储很有问题,因为经常要连接多个字符串,容易超长,对于这样的变化,串值得存储空间可在程序执行过程中动态分配而得,比如在计算机中存在一个自由存储区,叫做“堆”。
链式存储结构
一个结点存一个字符,会有很大的空间浪费,可以存多个字符,一个结点未被占满时,可以用“#”或者其他字符补全。
一个结点存多少个字符才合适很重要,会直接影响串的处理效率,需要根据实际情况作出选择。
* 算法很低效! *
差不多是穷举,遍历,对于二进制,很多0-1的序列,效率低。
Knuth Morris Pratt
大大避免重复遍历的情况
O(m+n) 比O((n-m+1)*m)更好。
当模式与主串之间存在许多 部分匹配 时优势明显
改进算法
算法导论第32章字符串匹配。
这一节需要重新看。
* 树是n个结点的有限集。 n= 0时称为空树。在任意一棵非空树中:(1) 有且仅有一个特定的称为根的结点; (2) 当n >1 时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、……、Tm,其中每个集合本身又是一棵树,并且称为根的子树*
- 一对多的数据结构
- 树的定义其实是我们在讲解栈时提到的递归的方法。
- 子树是互不相交的
结点拥有的子树数称为结点的度(Degree)。度为0的结点称为叶结点(Leaf)或终端结点;度不为0的结点称为非终端结点或分支结点。
树的度是树内各结点的度的最大值。
- 树中结点的最大层次称为树的深度(Depth) 或高度
森林(Forest)是m棵互不相交的树的集合。 对树中每个结点而言,其子树的集合即为森林。
线性存储结构:
- 双亲表示法:
每个结点中,附设一个恶指示器指示其双亲结点在链表中的位置。容易找到双亲,但是需要遍历才可以找到子结点。
可以再加一个域,存储最左子结点。
增加右兄弟域来体现兄弟关系。
存储结构的设计是一个非常灵活的过程,设计是否合理,取决于基于该存储结构的运算是否适合、是否方便,时间复杂度好不好等。
方案二 : 每个结点指针域的个数等于该结点的度,我们专门取一个位置来存储结点指针域的个数。
由于各个结点的链表是不同的结构,加上要维护结点的度的数值,在运算上就会带来时间上的损耗。
二叉树有雾中基本形态:
1 空二叉树; 2 只有一个根结点; 3 根结点只有左子树; 4 根结点只有右子树; 5 根结点既有左子树又有右子树。
特殊二叉树:
1 斜树
所有结点都只有左子树叫左斜树;所有结点都是只有右子树的叫右斜树。
其实就类似于线性表,线性表可以理解为树的一种特殊形式。
2 满二叉树
所有分支结点都存在左子树和右子树,并且所有的叶子都在同一层上,这样的二叉树称为满二叉树。
- 但是每个结点都存在左右子树,不能算是满二叉树,还必须要所有的叶子都在同一层上,这就做到了整棵树的平衡。
可以一一对应,没有结点的为空,一般顺序存储结构只用于完全二叉树,以免空间浪费太多
是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
* 看代码更容易理解这个过程。*
已知前序和后序遍历,不能确定一棵二叉树。
类似于二叉树的遍历
利用空余的地址,存放指向结点在某种遍历次序下的前驱和后继结点的地址。
- 把这种之前前驱和后继的指针称为线索。
为了区分指针到底指向了哪里,增加了两个标签
、
树和森林的遍历问题:
树:先根遍历;后根遍历
森林:前序遍历;后序遍历
树的先根遍历和后根遍历完全可以借用二叉树的前序遍历和中序遍历的算法来实现。我们找到了对树和森林这种复杂问题的简单解决办法。
数据传输,根据字母出现概率还构造二叉树,然后转化成相应的二进制
图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E)
无向图:边用无序偶对(vi,vj)来表示。
有向图:有向边,弧,弧尾,弧头:
了解这些操作
由于图结构的复杂,所以不能用简单的顺序存储结构来表示。
多重链表,即一个数据多个指针域组成的结点表示图中顶点,尽管可以实现图结构,但是,如果各个顶点的度数相差很大,按度数最大的顶点设计会造成资源浪费,若按照每个顶点自己的度数设计不同的顶点结构,又会带来操作不便。
图的邻接矩阵(adjacency matrix)存储方式是用两个数组来表示图:一个一维数组存储顶点信息,一个二维数组存储边或者弧的信息。
很熟悉了。
网:
无穷表示一个计算机允许的、大于所有边上权值的值,也就是一个不可能的极限值, 代表不存在。
初始化耗费时间 O(n^2)
邻接矩阵对于边数相对顶点较少的图,存在存储空间的极大浪费。
第一列是 数组存储顶点信息;
每一行代表对应顶点的邻接表,记录其邻接点,用单链表存储
对于有向图,则是出度的邻接点;可以建立逆邻接表,获取入度信息
对于有向图,邻接表有缺陷,出度和入度每次只能方便一个;十字链表法就是把邻接表和逆邻接表法结合起来。
结构较复杂,还没认真完全看清晰,
在有向图应用中,十字链表是非常好的数据结构类型
操作无向图的边,用邻接表比较麻烦。因为与两个链表有关。
仿造十字链表,对边表结点的结构进行改造,。
适合对边依次进行处理的操作。
Kruskal 克鲁斯卡尔算法中有应用
从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历
避免重复,需要记录 访问数组;避免陷入死循环
深度优先遍历 和 广度优先遍历
我们把构造连通网的最小代价生成树称为最小生成树 (Minimum Cost Spanning Tree)
要注意,所有的u,然后怎样找出最小的边,算法中是用一个数组迭代存储边的值。
Kruskal :直接从边出发,把所有的边按照权值,从小到大排序,然后一个个往里加,同时判断是否会形成回路,这个回路判断很巧妙!
定义一个数组parent,每次往树里加入一条边,设置 数组起点位置的值为边终点的值,
每次将待加入边的起始和终点,用find函数,如果返回值一样说明形成环路。 注意find中的while,f值的设定。
这里的 里面是用连通分量来说明的。
每得到一个最佳顶点,就更新最短路径,知道达到最后一个顶点。
第一个点很好获取的,然后就是第二个点。。。
O(N^2)
无环图的应用
在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,称为AOV网(Activity On Vertex Network)
活动之间有制约关系,不能存在回路
解决工程完成需要的最短时间问题
对一个流程图获取最短时间,就必须分析它们的拓扑关系,并且找到当中最关键的流程,这个流程的时间就死最短时间。
把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上的活动叫关键活动
找到所有活动的最早开始时间和最晚开始时间,并且比较它们,如果相等就意味着此活动是关键活动,活动的路径为关键路径。
提到搜索引擎,很好奇其数据存储结构是什么样的,如何才能保持如此快速的搜索。:高级算法,分布式,多服务器,
这里实际上 讲述数据库的一些原理
查找表:同一类型的数据元素构成的集合
关键字Key:数据元素中某个数据项的值,又称为键值,用它可以标识一个数据元素。
查找就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素。
查找表:静态查找表和动态查找表
静态查找表:只作查找操作的查找表;
动态查找表:在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素。
为了提高查找的效率,需要专门为查找操作设置数据结构,这种面向查找操作的数据结构称为查找结构。
逐个进行记录的关键字和给定值比较
优化:用for循环,需要对i是否越界做判断,实际上可以再开头设置哨兵,a[0]=key,用while循环处理比较,数据很多时,效率提高很大。
要使用有序表,但是对于频繁插入和删除的数据集来说,维护有序的排序会带来不小的工作量。
根据实际的值来更改查找的位置,不一定是在中间。
对于表长较大,而关键字分布比较均匀的查找表来说,性能较好。 斜率,
按照时间顺序存储的快速增长的数据,网站的数据,日志。
索引是为了加快查找速度而设计的一种数据结构。
索引就是把一个关键字与它对应的记录相关联的过程。
索引技术是组织大型数据库以及磁盘文件的一种重要技术。
索引:线性索引、树形索引、和多级索引
线性索引就是将索引项集合组织为线性结构,也称为索引表。 有 稠密索引,分块索引,倒排索引
稠密索引 指在线性索引中,将数据中的每个记录对应一个索引项,索引项是按照关键码有序的排列
数据量大时,仍效率低
分块索引 把数据集的记录分成了若干块, 块内无序,块间有序
因为有序维护成本太高
每一块对应一个索引项
倒排索引 - 最基础的搜索技术
搜索引擎在极短时间内回复答案
索引项的通用结构是: 次关键码(文章关键词),记录号(文章编码),其中记录号表存储相同次关键字的所有记录的记录号。
由于不是由记录来确定属性,而是由属性值来确定记录的位置,因而称为倒排索引。
一列数,按照一定规则构造成一棵二叉树,当我们对它进行中序遍历时,就可以得到有序的序列,所以我们通常称之为二叉排序树。
规则:
它的左右子树也分别为二叉排序树。
构造一棵二叉排序树的目的,是为了提高查找和插入删除关键字的速度。
插入和删除操作 都挺有意思的,不愿意看详细的介绍。。
是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1.
将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF (Balance Factor)
首先是二叉排序树,平衡因子BF = -1 0 +1
为什么需要平衡二叉树?
举例说明:{3,2,1,4,5,6,7,10,9,8} 构建二叉排序树
但是高度达到8的二叉树,对于查找是很不利的;右图高度为4的二叉排序树可以提高查找效率。
在构建的过程中,保证平衡,不断的调整才行,看过程挺麻烦的。
多路查找树 multi-way search tree: 其每一个结点的孩子数可以多于两个,且每一个结点处可以存储多个元素
所有元素之间存在某种特定的排序关系。
2-3树 其中每一个结点都具有两个孩子或者三个孩子:
2结点 包含一个元素和两个孩子或没有孩子;
3结点包含一大一小两个元素和三个孩子或没有孩子
2-3树困难之处在于插入和删除,毕竟某个结点可能是3结点,可能是2结点。
2-3-4树 同样是插入删除较复杂
B树:是一种平衡的多路查找树,2-3树和2-3-4树都是B树的特例。结点最大的孩子数目称为B树的阶。
由于B树每结点可以具有比二叉树多得多的元素,所以与二叉树的操作不同,它们减少了必须访问结点和数据块的数量,从而提高了性能。
B树的数据结构就是为了内外存的数据交互准备的。
B+树
解决元素遍历中元素重复的过程,加入新的元素组织方式
不是严格的树结构。
储存位置 = f(关键字) 这样不需要比较就可获得记录的存储位置,通过一个函数:散列技术
把对应关系f称为散列函数,又叫哈希函数hash。
采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(hash table)
哈希表查找步骤:
1)在存储时,通过散列函数计算记录的散列地址,并按此散列地址存储该记录。
2)当查找时,通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录。
散列技术 既是一种存储方法,也是一种查找方法。
散列主要是面向查找的存储结构。
散列技术最适合的求解问题是查找与给定值相等的记录
有些关键字 对应很多记录的情况 不适合散列技术;也不适合范围查找
设计一个简单、均匀、存储利用率高的散列函数是散列技术中最关键的问题。
key 不同 值一样 造成冲突
算法: 1 计算简单; 2 散列地址分布均匀
1 直接定址法
取关键字的某个线性函数值为散列地址
2 数字分析法
比如手机号,可以用后四位作为散列地址;
抽取方法是使用关键字的一部分来计算散列存储位置的方法
3 平方取中法
一个数平方,再抽取中间的3位,用作散列地址。
适合不知道关键字的分布,而位数又不是很大的情况。
4 折叠法
关键字分割成位数相等的几部分,然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
有时不能保证分布均匀,也可以将某部分反转;
折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。
5 除留余数法
6 随即数法
当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。
综合考虑:
1 计算散列地址所需的时间
2 关键字的长度
3 散列表的大小
4 关键字的分布情况
5 记录查找的频率
1 开放定址法
一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
增加平方运算的目的是为了不让关键字都聚集在某一块区域。我们称之为二次探测法。正负变化可以双向寻找。
位移量线性变化的,称为线性探测法,
位移量di 采用随即函数计算得到,称之为随即探测法。
2 再散列函数法
事先准备多个散列函数,一个冲突了,换另外一个。
3 链地址法
将所有关键字为同义词的记录存储在一个单链表里,我们称之为同义词字表,在散列表中只存储所有同义词子表的头指针。
4 公共溢出区法
为所有冲突的关键字建立了一个公共的溢出区来存放。
如果相对不基本表而言,有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。
挺简单的
散列表性能分析:
如果没有冲突,时间复杂度为O(1);但实际上冲突不可避免,那么散列查找的平均查找长度取决于哪些因素?
1 散列函数是否均匀
2 处理冲突的方法
线性探测处理可能产生堆积,二次探测法更好,链地址法处理不会产生任何堆积,具有更好的平均查找性能。
3 散列表的装填因子 = 填入表中记录个数/散列表长度
总可以选择一个合适的装填因子以便将平均查找长度限定在一个单位之内。以空间换时间。
假设有n个记录的序列为{r1,r2,……,rn},其对应的关键字分别为{k1,k2,……,kn},需确定1,2,……,n的一种排列p1,p2,……,pn,使相应的的关键字满足非递减或非递增关系,即使得序列成为一个按关键字有序的序列,这样的操作就称为排序。
排序可以看成是线性表的一种操作。
1 排序的稳定性
待排序的记录序列中可能存在两个或两个以上的关键字相等的记录,排序结果可能会存在不唯一的情况,因此给出稳定和不稳定排序的定义。
算法是否稳定,要通过分析才能得出。
2 内排序和外排序:待排序的记录是否全部被放置在内存中
外排序是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。
对于内排,算法性能主要受3个方面的影响:
1 时间性能
在内排中,主要进行两种操作:比较和移动。 这两个操作尽量少才好。 移动可以通过改变记录的存储方式来予以避免。
2 辅助空间
除了存放待排序所占用的存储空间之外,执行算法所需要的其他存储空间。
3 算法的复杂性
指的是算法本身的复杂度,而不是指算法的时间复杂度。
内排序分为:插入排序、交换排序、选择排序、和归并排序。
简单算法:冒泡排序,简单选择排序、直接插入排序
改进算法:希尔排序、堆排序、归并排序、快速排序。
Bubble Sort 一种交换排序,基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
冒泡的细节上可以有很多种变化。
很多种变化!!!
初级版:让每一个关键字都和它后面的每一个关键字比较,如果大则交换,这样第一位置的关键字在一次循环后一定变成最小值。
总是把最小的数放到最前面,但是有些次小的因为交换放到后面了,其实效率很低。
正宗的冒泡排序:从后往前循环,相邻的比较,较小的数字如同气泡般慢慢浮到上面,因此叫冒泡算法。
冒泡算法优化:增加一个标记变量,当排序已经完成时,不需要继续循环下去。
复杂度分析: 最好的情况是n-1次比较,O(n);最坏的是逆序,需要比较n(n-1)/2次,复杂度为O(n^2)。
冒泡排序的思想是不断地在交换,通过交换完成最终的排序; 在排序是找到合适的关键字再做交换,并且只移动一次就完成相应关键字的排序定位,这是选择排序法的初步思想。
选择排序的基本思想是 每一趟在n-i+1(i=1,2, … ,n-1)个记录中选取关键字最小的记录作为有序序列的第i个记录。
简单选择排序法(Simple Selection Sort)就是通过n-i次关键字间的比较,从n-i+1(i=1,2, … ,n-1)个记录中选取关键字最小的记录,并和第i个记录交换之。
复杂度分析:最大的特点是交换移动数据次数相当少,这样就节约了响应的时间,无论最好最差情况,比较次数都是一样多n(n-1)/2,复杂度和冒泡排序一样O(n^2),但是性能上还是要略优于冒泡排序。
Straight Insertion Sort 的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
先相邻比较,找到逆序的结点,然后将其后面的结点后移,把这个逆序结点插入。(用的链表记录)
空间上只需要一个记录的辅助空间,时间复杂度,平均比较和移动次数约为n^2/4,比冒泡和简单选择排序性能要好。
前面智力题很好玩。
之前排序算法的时间复杂度基本都是O(n^2),希尔算法是突破这个时间复杂度的第一批算法之一。
插入排序在,记录本身基本有序或者记录数较少时直接插入的优势比较明显,但是现实中这属于特殊情况,希尔排序是先将大量的记录进行分组先对 子序列插入排序,然后对整个序列排序。
避免子序列合并之后,整个序列不能达到基本有序,采用
- 跳跃分割的策略: 将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。
关键并不是随便分组后各自排序,而是将相隔某个增量的记录组成一个子序列,实现跳跃式的移动,使得排序的效率提高。
增量的选取很关键,目前只有经验值, 增量序列的最后一个增量值必须等于1才行。
时间复杂度为O(n^1.5)。
简单选择排序,没有把每一趟的比较结果保存下来,在后一趟中,有许多比较在前一趟已经做过了,但是由于未保存这些比较结果,所以后面重复执行这些比较。
如果在选择到最小记录的同时,并根据比较结果对其他记录做出相应的调整,那样排序的总体效率就会非常高了。
堆排序就是对简单选择排序进行一种改进。
堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
还需要解决两个问题:
1 如何由一个无序序列构建成一个堆?
2 如何在输出堆顶元素后,调整剩余元素成为一个新的堆?
构建堆的复杂度为O(n),第i次取堆顶记录重建堆需要用O(logi)的时间,并且需要取n-1次堆记录,因此重建堆的时间复杂度为O(nlogn)。
最好、最坏都是O(nlogn)。 空间复杂度上,它只有一个用来交换的暂存单元。
由于记录的比较和交换是跳跃式进行,因此堆排序也是一种不稳定的排序方法。
初始构建堆所需的比较次数较多,因此,它并不适合待排序序列个数较少的情况。
时间复杂度:需要将待排序序列中的所有记录扫描一遍,耗费O(n),完全二叉树的深度克制,归并排序要进行log2n次,所以总的时间复杂度O(nlogn),这是归并排列算法中最好、最坏、平均的时间性能。
由于归并排序在归并过程中需要与原始记录序列同样数量的存储空间存放归并结果以及递归时深度为log2n的栈空间,空间复杂度为O(n+logn)。
需要两两比较,不存在跳跃,归并排序是一种稳定的排序算法。
比较占内存,但效率高且稳定的算法。
递归会造成时间和空间上的性能损耗,可以将递归转化成迭代。
使用归并排序时,尽量考虑用非递归方法。
插入排序- 希尔排序 插入类
简单选择排序-堆排序 选择类
冒泡排序 - 快速排序 交换排序
快排也是通过不断比较和移动交换来实现排序的,只不过它的实现,增大了记录的比较和移动的距离,将关键字较大的记录从前面直接移动到后面,关键字较小的记录从后面直接移动到前面,从而减少了总的比较次数和移动交换次数。
时间复杂度
空间复杂度
非稳定排序,因为是跳跃式的交换
1 优化选取枢轴
然后选取首次的枢轴的时候,这里可能选取的并不好,导致效率低。
需要改进:随机数,三数法,九数法
2 优化不必要的交换
采用替换而不是交换的方式
-3 优化小数组时的排序方案
直接插入是简单排序中性能最好的;
快速排序的递归反而影响性能。 分割到后面的时候可以采用插入排序,这个数是7,也有说是50.
两种方法结合起来用。