数据结构与算法之查找排序算法
第八章:查找
一:基本概念
- 查找:
根据给定的某个值,在查找表中确定一个与其关键字等于给定值的数据元素(或记录) - 关键字:
用来表示一个数据元素(或记录)的某个数据项的值
主关键字:
可以唯一地表示一个记录的关键字
次关键字:
用以识别若干记录的关键字 - 查找表:
由同一类型的数据元素(或记录)构成的集合。由于集合中的数据元素之间存在着松散的关系,因此查找表是一种应用灵便的结构
静态查找表:
做查询、检索操作的查找表
动态查找表:
做插入、删除操作的查找表 - 对查找表常进行的几个操作:
1.查询某个特定的数据元素是否在查找表中
2.检索某个特定的数据元素的各种属性
3.如果查询结果为不存在,则在查找表中插入一个数据元素
4.如果查询结果为存在,则删除查找表中的某个数据元素 - 查找算法的评价标准:
- 关键字的平均比较次数,也叫平均查找长度(ASL)
二:线性表的查找
1.顺序(线性)查找
优点:
算法简单,逻辑次序无要求,不同存储结构均适用
缺点:
ASL太长,时间效率低
查找范围:
- 顺序表/线性表表示的静态查找表
表内元素之间无序
//数据元素类型定义 |
//在ST中查找值为key的值 |
改进:
把待查关键字key存入表头(哨兵/监视哨),从后往前逐个比较,可以免去查找过程中每一步都要检查是否查找完毕的步骤(从后往前找,不需要检测是否越界,但越界时也就是比较的元素等于表头元素,此时自然就返回表头元素的位置0了)
//设置监视哨的顺序查找,可使平均时间减少一半 |
顺序查找的性能分析:
2.二分查找
折半查找仅适用于有序表,且仅限于顺序存储结构m,对链式存储结构无效
折半查找算法(非递归算法):
int Search_Bin(SSTable ST, KeyType key) |
折半查找(递归算法):
int Search_Bin(SSTable ST, KeyType key,int low, int hight){ |
折半查找算法的性能分析:
折半查找法的查找次数小于等于二叉数的深度
平均查找长度=所有元素查找成功的总次数/元素个数
3.分块(索引顺序)查找
假如一个顺序表,按元素的内容分为三块,建立一个索引表,记录每一块内容的最大值以及从哪开始的,如果块内元素是无序的,则里面的元素用顺序查找
分块查找性能分析:
分块查找法的平均查找效率由两部分组成,一部分是索引表的平均查找效率,索引表的查找类似于折半查找再加上块内的顺序查找
三:树表的查找
1.二叉排序树(Binary Sort Tree)
1.1BST的定义
- 若其左子树非空,则左子树上所有结点的值均小于根节点的值
- 若其右子树非空,则右子树上所有结点的值均大于等于根节点的值
- 其左右子树本身又各是一颗二叉排序树
- 二叉排序树可以是空树
规律:中序遍历二叉排列树,得到的数据元素序列是一个递增有序的序列
1.2 BST的存储结构
typedef struct |
1.3 BST的查找
查找过程:
- 若查找到的关键字等于根节点,成功
- 否则
- 若小于根节点,查左子树
- 若大于根节点,查右子树
二叉排序树查找算法:
BSTree SearchBST(BSTree T, KeyType key) |
二叉排序树的查找分析:
问题:如何提高形态不均衡的二叉排序树的查找效率?
解决办法:做平衡化处理,也就是平衡二叉树
1.4 BST的插入和生成
二叉排序树的插入:
void InsertBST(BSTree &T,ElemType e){ |
插入的元素一定是在叶子节点山
二叉排序树的生成:
1.5 BST的删除
- 被删除的是叶子结点:
直接删去该结点 - 被删除的结点只有左子树或只有右子树:
用其左/右子树替换它
其双亲节点的指针域的值改为:指向被删除节点的左子树或右子树
- 被删除的结点既有左子树又有右子树:
用中序前趋值替换它,再删除该前趋结点。前趋是左子树中最大结点 或用后继替换它,再删除该后继结点,后继是右子树中最小结点。(虽然两种替换都可以,但是一般使用让树的深度小一点的那种替换方式)
void DeleteBST(BSTree &T, KeyType key){ |
这段删除节点的操作很类似与查找节点的操作,需要额外实现具体怎么删除节点的算法
Delete
void Delete(BSTree &p){ |
2.二叉平衡树(Adelson-Velskii and Landis)
2.1 AVL的定义
平衡二叉树是具有以下性质的二叉排序树
- 左子树与右子树的高度之差的绝对值小于等于1
- 左子树和右子树也是平衡二叉排序树
为了方便起见,给每个节点附加一个数字,此数字给出该节点左子树与右子树的高度差,这个数字称为该节点的平衡因子(BF)
平衡二叉树可以是空树;平衡因子 = 结点左子树高度 - 结点右子树高度 。AVL的平衡因子只能是-1,0,1
对于一颗由n个节点的AVL树,其高度需要保持在O(log2n)数量级,这样的话才能让AVL算法的效率保持在这个数量级
2.2 失衡二叉排序树的分析与调整
如果失衡节点不止一个,找最小失衡子树的根节点
调整失衡节点的原则:1)降低高度;2)保持二叉排序树的性质
平衡二叉树节点结构的定义(同BST的结构一致,需要加上平衡因子这个参数)
typedef struct BSTNode |
- LL型调整过程:
void LLAdjust(const AVLTree &p){ |
- RR型调整过程:
- LR型调整:
四:哈希表的查找
基本思想:记录的存储位置与关键字之间存在对应关系(hash函数)
- 优点:查找效率快O(1)
- 缺点:空间效率低
散列表的若干术语:
1.构造散列函数的方法
1)构造好的散列函数:
- 所选函数尽可能简单,提高转换速度读
- 所选函数对关键码计算出的地址,应在散列地址集中致均匀分布,减少空间浪费
2)构造散列函数考虑的因素:
- 关键字长度
- 执行速度(计算散列函数所需时间)
- 散列表的大小
- 关键字的分布情况
- 查找频率
散列函数的构造方法:
2.处理冲突的方法
制定一个好的解决冲突的方案:
- 查找时,如果从散列函数中计算出的地址中查不到关键码,应依据解决冲突的规则,有规律地查询其他相关单元
开放定址法(开地址法)
链地址法(拉链法)
优点:
- 非同义词不会冲突,无“聚集”现象
- 适合表长不确定的情况(链表动态申请)
3.散列表的查找
余数取13是因为散列表长度为16,16以下的最大质数为13
散列表的查找效率分析:
结论
- 散列表技术具有很好的平均性能,优于一些传统的技术
- 链地址法优于开地址法
- 除留余数法做散列函数优于其他类型函数
五:散列表查找代码实现
|
查找代码懒得写了,很类似于插入算法,值得注意的是在while循环里面需要加上没有查到的判别代码,即判断
H.elem[addr] == NULLKEY || addr == Hash(key)
,如果循环回到了起点(也就是判断了m次仍然不匹配)或者是下一个元素为NULLKEY
都能说明不存在这样的关键字key
第九章:排序
1.分类
按数据存储介质:
- 内部排序:数据量不大、数据在内存,无需内外存交换数据
- 外部排序:数据量较大、数据在外存(文件排序),外存排序时,要将数据分批调入内存来排序,中间结果还要及时放入外存,比较复杂
按比较器个数:
- 串行排序:单处理机(同一时刻比较一对元素)
- 并行排序:多处理机(同一时刻比较多对元素)
按主要操作:
- 比较排序:插入排序、交换排序、选择排序、归并排序
- 基数排序:不比较元素大小,仅根据元素本身的取值确定其有序位置
按辅助空间:
- 原地排序:辅助空间用量为O(1),与参加排序的数据量大小无关
- 非原地排序:辅助空间用量超过O(1),与参加排序的数据量大小有关
按稳定性:
- 稳定排序:能使任何数值相等的元素,排序后相对次序不变
- 非稳定排序:数值相等的元素,排序后相对次序变化
按自然性:
- 自然排序:输入数据越有序,排序的速度越快
- 非自然排序:输入数据越有序,排序的速度越慢
2.存储结构——记录序列以顺序表存储
|
3.插入排序
每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止
插入排序的分类:
3.1 直接插入排序
void InsertSort(SqList &L){ |
插入排序的原始数据越接近有序,排序速度越快
时间复杂度:
- 最好情况:O(n)
- 最坏情况:O(n2)
- 平均情况:O(n2)
空间复杂度:O(1) ,是一种稳定的排序方法
3.2 折半插入排序
void BLinsertSort(SqList &L){ |
3.3 希尔排序
特点:
- 一次移动,移动位置比较大,跳跃式地接近排序后的最终位置
- 最后一次只需少量移动
- 增量必须递减,最后一次必须是1
- 增量序列应该是互质的
void ShellSort(SqList &L, int dlta[], int t){ |
在这段代码里面每一组的插入排序并不是一次性排完的
时间复杂度:
- 最好情况:O(n)
- 最坏情况:O(n2)
- 平均情况:O(n1.3)
空间复杂度:O(1)
是一种不稳定的排序方法
4.交换排序
4.1 冒泡排序
n个记录需要比较n-1次
第m次需要比较n-m次
void BubbleSort(SqList &L){ |
时间复杂度:
- 最好情况:O(n)
- 最坏情况:O(n2)
- 平均情况:O(n2)
空间复杂度:O(1)
是一种稳定的排序方法
4.2 快速排序
void QSort(SqList &L, int low, int high){ |
快速排序时,越乱越好,基本有序的不适合用快速排序
时间复杂度:
- 最好情况:O(nlogn)
- 最坏情况:O(n2)
- 平均情况:O(nlogn)
空间复杂度:O(nlogn)
是一种不稳定的排序方法,例如对于49,38,49*,20,97,76
的第一次划分:20,38,49*,49,97,76
5.选择排序
5.1 简单选择排序
void SelectSort(SqList &K){ |
时间复杂度:
- 最好情况:O(n2)
- 最坏情况:O(n2)
- 平均情况:O(n2)
空间复杂度:O(1)
是一种不稳定的排序方法
5.2 堆排序
- 堆的定义
先解决一个问题:在输出堆顶元素后,如何调整其余元素为一个新的堆?
小根堆:
- 输出堆顶后,用堆中最后一个元素代替之
- 将根节点与左、右子树的根节点值比较,并与其中较小的交换
- 重复上述操作,直到叶子结点,将得到新的堆,称这个从堆顶到叶子的调整过程为“筛选”
void HeapAdjust(Elem R[], int s, int m){ |
再解决第二个问题:如何建立一个堆
for(int i = n / 2; i >= 1; i--) |
实现堆排序
void HeapSort(Elen R[]){ |
时间复杂度:O(nlog2n)
空间复杂度:O(1)
是一种不稳定的排序方法
6.归并排序
将多个有序子序列归并成一个有序序列
需要进行⌈log2n⌉次,就是上面这颗树的高度
关键问题:如何将两个有序的序列合并成一个有序的序列?
在两个有序序列的首部分别建立一个指针,比较两指针所指元素,哪个指针所指元素值小就把这个元素移到新的序列中,同时此指针向右移动一位,这两个指针都移动到末尾则合并成功。
时间复杂度:O(nlogn)
空间复杂度:O(n)
是一种稳定的排序方法
7.基数排序(桶/箱排序)
基本思想:分配 + 收集
8.总结
时间复杂度
- O(nlogn):快速排序、堆排序、归并排序
- O(n2):直接插入排序、冒泡排序、简单选择排序
- O(n):基数排序
- 当待排序序列为有序是:用直接插入排序、冒泡排序时间复杂度为O(n),而快速排序退化为O(n2)
- 简单选择排序、堆排序、归并排序的时间性能不随序列分布二改变
空间复杂度:
- O(1):所有简单排序(直接插入排序、冒泡排序、简单选择排序)和堆排序
- O(logn):快速排序
- O(n):归并排序