数据结构与算法--查找和排序

查找与排序

​ 建议大家好好看看查找、排序这两个程序员的必修课,希望大家能学有所获

排序

​ 常见的排序有:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序。

​ 选择一个排序,需要考虑它的时间复杂度(包括最好、最坏、平均情况时间复杂度)、内存消耗以及稳定性

​ 大家不要认为排序算法简单,抱着轻视的态度学习。排序算法简单,想要用好、用精却不容易

关于稳定性

​ 稳定性的意思是: 若待排序序列中存在值相等的元素,排序后相等元素间原有的先后顺序保持不变

稳定性是排序算法特别重要的一个性质,例如给出一个需求:总成绩按照由高到低排序;若总成绩按数学成绩由高到低排序;若总成绩&&数学成绩相同,再按英语成绩由高到低排序。

​ 如果采用具有稳定性的排序算法,只需三次排序(先对整体按英语成绩排序,再对整体按数学成绩排序,最后对整体按总成绩排序),而无需每次对相同的小区间进行排序

关于内部和外部排序

​ 由于待排序的记录数量不同,使得排序过程中涉及的存储器不同。可将排序方法分为两大类:内部排序与外部排序。

​ 内部排序:将记录存放在内存上进行的排序过程,衡量内部排序的效率通常通过时间复杂度,以上常用排序都为内部排序。

​ 外部排序:记录数量很大导致在排序过程中需要对外存(磁盘)进行访问的排序过程,衡量外部排序的效率通常通过IO次数

​ 外部排序第一阶段会根据内存大小将磁盘上的文件分成若干长度为l的子文件或段,依次读入内存并通过内部排序方法对其排序,并将得到的有序段重写回磁盘。

​ 第二阶段对这些小的有序段进行归并,直到得到整个有序文件。其中第二阶段的实现方法有:(1)多路平衡归并 (2)置换-选择排序

选择排序

​ 选择排序将数组分为 已排序区间未排序区间,每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。

​ 选择排序的流程为:

复杂度

时间复杂度:

​ 最好情况:O(n^2)

​ 最坏情况:O(n^2)

​ 平均情况:O(n^2)

空间复杂度:O(1)

稳定性:不稳定(选择排序每次都要找未排序元素中的最小值,并和前面元素交换位置,这样就破坏了稳定性。例如:数组5,8,5,2,9 ,第一次找到最小元素 2,与第一个 5 交换位置,那第一个 5 和中间的 5 顺序就变了)

冒泡排序

​ 冒泡排序特点就是只会操作相邻的两个数据。每次冒泡操作都会对相邻两元素进行比较,若不满足条件就让它俩互换。一次冒泡(让当前未排序数组中最大/最小的值沉底/冒泡,时间复杂度为O(n))会让至少一个元素移动到它应该在的位置。重复 n 次就完成了 n 个数据的排序工作,总时间复杂度为O(n^2)

​ 以下是一次冒泡的流程:

复杂度

​ 时间复杂度:

​ 最好情况:O(n),数组有序,一次遍历发现有序后break

​ 最坏情况:O(n^2),数组逆序

​ 平均情况:O(n^2)

​ 空间复杂度:O(1)

​ 稳定性:稳定(前提是代码中保证两个相邻元素相等时不做交换)

插入排序

​ 将数组中的数据分为已排序区间未排序区间。初始已排序区间只有一个元素(数组第一个元素),取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程直到未排序区间中元素为空。

​ 一下是插入排序的流程:

复杂度

时间复杂度:

​ 最好情况:O(n),数组有序

​ 最坏情况:O(n^2),数组逆序

​ 平均情况:O(n^2)

空间复杂度:O(1)

稳定性:稳定(需要保证代码中元素插入的位置在已排序区间相同元素的后面)

插入排序优于冒泡?

​ 答案是yes

​ 虽然时间、空间复杂度以及稳定性插入排序和冒泡排序类似,但是由于插入排序每次操作的指令数少于冒泡排序(冒泡排序是交换,需要三个指令,优化为两次亦或位运算也需要两个指令;插入排序是移动,只需要一个赋值指令),因此插入排序性能相比冒泡更加优异。

​ 至于选择排序,最好最坏情况都是O(n^2)且排序不稳定,几乎用不到~

归并排序

​ 归并排序就是把数组下角标p~r从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,递归这个过程直到p >= r

​ 归并排序流程为:

复杂度

时间复杂度:nlog(n)

​ 递归拆分复杂度是log(n),每次拆分后的merger合并复杂度为n,总时间复杂度为nlog(n),且不随数组的有序程度而变动(即最好、最坏、平均情况操作的次数相同)

空间复杂度:O(n),相比其他算法,merger需要有额外空间的开辟和释放

稳定性:稳定(需要保证merger()函数遇到相同时先添加左边的)

快速排序

​ 每次从数组下标 p 到 r 间任选一个数据作为分区点(pivot,一般选择区间最后一个元素),然后遍历 p 到 r 之间的数据,将小于分界区的放左,大于分界区放右,于是数组下角标 p 到 r 之间的数据就被分成了两个部分,递归这两个部分直到p >= r。

​ 其核心原理就是设置标记点i代表[p, i - 1]的数据都小于分区点,[i,r - 1]的数据要么大于等于分区点,要么还未进行比较。

​ 伪代码如下:

partition(A, p, r) {

  pivot := A[r]
  i := p
  
  for j := p to r-1 do {
    if A[j] < pivot {
      swap A[i] with A[j]
      i := i+1
    }
  }
  
  swap A[i] with A[r]
  
  return i
}

​ 流程图如下:

复杂度

时间复杂度:

​ 前提:一下情况都是每次选取区间最后一个元素作为分区点

​ 最好情况:O(nlog(n)),完全无序

​ 最坏情况:O(n^2),有序(正序或逆序)

​ 平均情况:O(nlog(n))

空间复杂度:O(1)

稳定性:不稳定( 分区过程涉及交换操作,如果数组中有两个相同的元素,比如序列 6,8,7,6,3,5,9,4。在经过第一次分区操作之后,两个 6 的相对先后顺序就会改变)

降低最坏情况发生的概率

​ 我们知道快速排序算法在最坏情况下时间复杂度退化到 O(n^2),我们可以通过合理选择分界区来避免这种情况的发生。

​ 我们要想减少最坏情况发生的概率,就要尽量让分区点两边的区间数据量差不多。

​ 这时我们可以采用以下两个办法来尽量使得两边数据量平均:

(1)三数取中法:从区间的首、尾、中间分别取一个数,然后取这3个数的中间值作为分区点。当然,取得越多中值越精确,但是耗费的额外时间也越多。所以我们要依据当前数组的数据规模来决定几数取中法~

(2)随机法:每次随机一个元素作分区点。

快排比优于归并排序?

​ 归并排序需要额外的空间(O(n)),并涉及到内存的开辟和释放。

​ 即使快排最坏情况下会达到O(n^2),但由于其最坏情况发生概率极小,我们一般还是采用快速排序这种算法。

扩展小作业

​ 利用快排思想,O(n)时间复杂度求无序数组中第K大元素

桶排序

​ 将要排序的数据分到几个有序的桶里(如果桶代表的是一个区间,则每个桶内的数据再单独进行排序)。 桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。

复杂度

​ 最好情况:O(n),当每个桶内数据无需单独排序时

​ 最坏情况:O(nlog(n)),所有数据在一个桶里且桶代表一个区间而非一个值

​ 平均情况:O(n)

​ 空间复杂度:O(n + m),其中m为桶的个数

​ 稳定性:稳定(需要保证代码中入桶和从桶中取出的顺序一致)

优化空间利用

​ 上面方法由于是链表有了指针节点的耗费。有没有办法可以优化掉?

​ 答案是yes。

​ 我们利用sum数组来累加每个有序桶的前缀和,来达到桶排序的目的:

适应场景

​ 桶排序虽然时间复杂度只有O(n),但是有严格的限制条件~

​ 适合的场景有:

​ (1)适用于一些数据范围小、对内存要求不大的情况:按年龄大小对100W用户进行排序。我们可以分成150个桶分别代表1150岁(应该没有150岁以上的人了吧)。每个桶是一个链表,每入桶一个用户就追加一个节点~这样复杂度只有O(n),同样空间复杂度也是O(n)

​ (2)适用于外部排序:对于10GB的用户信息按资产排序。由于无法一次性加载10G数据,我们可以分成10个桶,假设第一个桶为01K,第二个桶1KW…依次递推,然后分别对每个桶内部单独排序。

基数排序

​ 我们知道,桶排序只能解决数据范围小的情况,如果数据范围大了怎么办?把年龄扩展为资产,如果还要维持一个桶一个值,难道要开几百亿个桶?

​ 这时我们可以先对所有用户的资产的个位进行桶排,把排序后的结果再进行十位的桶排…依次直到最高位(如果有的用户达不到该位则前补0)。

​ 上述这种基于桶排的排序,就是基数排序

复杂度

​ 时间复杂度:O(n*k),其中k是数据范围最大值的位数。

​ 空间复杂度:O(n+m),其中m为桶的个数

​ 稳定性:稳定(需要保证代码中入桶和从桶中取出的顺序一致)

​ 一般情况下,一般情况下,logn并不比k大多少,且基数排序的空间耗费,导致其应用场景远远少于快排。

高效通用排序

​ 由于桶排、基数排序对条件要求苛刻,选择排序时间复杂度太高,因此无法采用~

​ 如果数据量不大(几十M以内),我们可以采用归并排序(虽然有O(n)的内存消耗,但能一直保持nlog(n)复杂度且具有稳定性)

​ 如果数据量太大(超过百M),出于内存方面的考虑,我们可以采用快速排序(通过三点取中法降低最坏情况发生的可能)。我们都知道快速排序是一个递归过程,出于以下几个考虑:(1)避免递归太深导致堆栈溢出 (2)当[l, r]范围过小时有序的概率大大增加 (3) 当[l, r]范围过小时O(nlogn)几乎等价于O(n^2),所以当递归到元素个数小于等于32时,退化为插入排序

​ 当然,为了避免递归太深导致堆栈溢出,我们可以实现一个堆上的栈,手动模拟递归来解决。

​ 如果数据量无比巨大(超过内存大小),那么我们就考虑外部排序,将数据拆分成多个部分,对每个部分分别进行内部排序,最后通过多路平衡归并,使得整体有序。

思考题

​ 我们在面试时经常会碰到类似问题: 有 10 日志文件,每个日志文件约 300MB,每个文件里的日志都是按照时间戳从小到大排序的。如果将10 个日志文件合并为 1 个,合并之后的日志仍然按照时间戳从小到大排列,而机器内存只有 1GB,有什么快速合并的方法吗?大家会有什么样的方案?

二分查找

​ 二分查找的基石就是有序数组(第一有序,第二数组。无序、链表等均不可), 查找思想有点类似分治思想。每次都通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到找到要查找的元素或区间被缩小为 0

复杂度

时间复杂度:

​ 最优情况时间复杂度:O(1)

​ 最坏情况时间复杂度:O(log(n))

​ 平均时间复杂度:O(log(n))

空间复杂度:O(1)

三要素

  1. 循环条件:l <= r
  2. mid取值: mid = l+((r-l)>>1),这种写法可以避免整数溢出
  3. l和r的更新:l = mid + 1; r = mid - 1

变型

​ 比如查找第一个值等于给定值的元素、查找最后一个值等于给定值的元素、查找第一个大于等于给定值的元素、查找第一个大于给定值的元素等等,大家可以试着想一下怎么二分

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
智慧校园整体解决方案是响应国家教育信息化政策,结合教育改革和技术创新的产物。该方案以物联网、大数据、人工智能和移动互联技术为基础,旨在打造一个安全、高效、互动且环保的教育环境。方案强调从数字化校园向智慧校园的转变,通过自动数据采集、智能分析和按需服务,实现校园业务的智能化管理。 方案的总体设计原则包括应用至上、分层设计和互联互通,确保系统能够满足不同用户角色的需求,并实现数据和资源的整合与共享。框架设计涵盖了校园安全、管理、教学、环境等多个方面,构建了一个全面的校园应用生态系统。这包括智慧安全系统、校园身份识别、智能排课及选课系统、智慧学习系统、精品录播教室方案等,以支持个性化学习和教学评估。 建设内容突出了智慧安全和智慧管理的重要性。智慧安全管理通过分布式录播系统和紧急预案一键启动功能,增强校园安全预警和事件响应能力。智慧管理系统则利用物联网技术,实现人员和设备的智能管理,提高校园运营效率。 智慧教学部分,方案提供了智慧学习系统和精品录播教室方案,支持专业级学习硬件和智能化网络管理,促进个性化学习和教学资源的高效利用。同时,教学质量评估中心和资源应用平台的建设,旨在提升教学评估的科学性和教育资源的共享性。 智慧环境建设则侧重于基于物联网的设备管理,通过智慧教室管理系统实现教室环境的智能控制和能效管理,打造绿色、节能的校园环境。电子班牌和校园信息发布系统的建设,将作为智慧校园的核心和入口,提供教务、一卡通、图书馆等系统的集成信息。 总体而言,智慧校园整体解决方案通过集成先进技术,不仅提升了校园的信息化水平,而且优化了教学和管理流程,为学生、教师和家长提供了更加便捷、个性化的教育体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值