算法与数据结构

Posted moli说

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法与数据结构相关的知识,希望对你有一定的参考价值。


11、排序(上):为什么插入排序比冒泡排序更受欢迎?

算法与数据结构(2)

一、几种经典排序算法及其时间复杂度级别冒泡、插入、选择 O(n^2) 基于比较;快排、归并 O(nlogn) 基于比较;计数、基数、桶 O(n) 不基于比较。

二、如何分析一个排序算法?
1.学习排序算法的思路?明确原理、掌握实现以及分析性能。

2.如何分析排序算法性能?从执行效率、内存消耗以及稳定性3个方面分析排序算法的性能。

3.执行效率:从以下3个方面来衡量

1)最好情况、最坏情况、平均情况时间复杂度

2)时间复杂度的系数、常数、低阶:排序的数据量比较小时考虑

3)比较次数和交换(或移动)次数

4.内存消耗:通过空间复杂度来衡量。针对排序算法的空间复杂度,引入原地排序的概念,原地排序算法就是指空间复杂度为O(1)的排序算法。

5.稳定性:如果待排序的序列中存在值等的元素,经过排序之后,相等元素之间原有的先后顺序不变,就说明这个排序算法时稳定的。

三、冒泡排序
1.排序原理

1)冒泡排序只会操作相邻的两个数据。

2)对相邻两个数据进行比较,看是否满足大小关系要求,若不满足让它俩互换。

3)一次冒泡会让至少一个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序工作。

4)优化:若某次冒泡不存在数据交换,则说明已经达到完全有序,所以终止冒泡。

2.代码实现

3.性能分析

1)执行效率:最小时间复杂度、最大时间复杂度、平均时间复杂度
最小时间复杂度:数据完全有序时,只需进行一次冒泡操作即可,时间复杂度是O(n)。最大时间复杂度:数据倒序排序时,需要n次冒泡操作,时间复杂度是O(n^2)。平均时间复杂度:通过有序度和逆序度来分析。什么是有序度?有序度是数组中具有有序关系的元素对的个数,比如[2,4,3,1,5,6]这组数据的有序度就是11,分别是[2,4][2,3][2,5][2,6][4,5][4,6][3,5][3,6][1,5][1,6][5,6]。同理,对于一个倒序数组,比如[6,5,4,3,2,1],有序度是0;对于一个完全有序的数组,比如[1,2,3,4,5,6],有序度为n*(n-1)/2,也就是15,完全有序的情况称为满有序度。什么是逆序度?逆序度的定义正好和有序度相反。核心公式:逆序度=满有序度-有序度。排序过程,就是有序度增加,逆序度减少的过程,最后达到满有序度,就说明排序完成了。冒泡排序包含两个操作原子,即比较和交换,每交换一次,有序度加1。不管算法如何改进,交换的次数总是确定的,即逆序度。对于包含n个数据的数组进行冒泡排序,平均交换次数是多少呢?最坏的情况初始有序度为0,所以要进行n*(n-1)/2交换。最好情况下,初始状态有序度是n*(n-1)/2,就不需要进行交互。我们可以取个中间值n*(n-1)/4,来表示初始有序度既不是很高也不是很低的平均情况。换句话说,平均情况下,需要n*(n-1)/4次交换操作,比较操作肯定比交换操作多,而复杂度的上限是O(n^2),所以平均情况时间复杂度就是O(n^2)。以上的分析并不严格,但很实用,这就够了。
2)空间复杂度:每次交换仅需1个临时变量,故空间复杂度为O(1),是原地排序算法。

3)算法稳定性:如果两个值相等,就不会交换位置,故是稳定排序算法。
四、插入排序

1.算法原理:首先,我们将数组中的数据分为2个区间,即已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想就是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间中的元素一直有序。重复这个过程,直到未排序中元素为空,算法结束。
2.代码实现

3.性能分析

1)时间复杂度:最好、最坏、平均情况
如果要排序的数组已经是有序的,我们并不需要搬移任何数据。只需要遍历一遍数组即可,所以时间复杂度是O(n)。如果数组是倒序的,每次插入都相当于在数组的第一个位置插入新的数据,所以需要移动大量的数据,因此时间复杂度是O(n^2)。而在一个数组中插入一个元素的平均时间复杂都是O(n),插入排序需要n次插入,所以平均时间复杂度是O(n^2)。
2)空间复杂度:从上面的代码可以看出,插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是O(1),是原地排序算法。

3)算法稳定性:在插入排序中,对于值相同的元素,我们可以选择将后面出现的元素,插入到前面出现的元素的后面,这样就保持原有的顺序不变,所以是稳定的。

五、选择排序
       选择排序将数组分成已排序区间和未排序区间。初始已排序区间为空。每次从未排序区间中选出最小的元素插入已排序区间的末尾,直到未排序区间为空。

空间复杂度:选择排序是原地排序算法。

时间复杂度:(都是O(n^2))
1. 最好情况:O(n^2)。

2. 最坏情况:O(n^2)。

3. 平均情况:O(n^2)。

稳定性:选择排序不是稳定的排序算法。

 

12、排序(下):如何用快排思想在O(n)内查找第K大元素?

归并排序的原理

如果要排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。

分治是一种解决问题的处理思想,递归是一种编程技巧

 

归并排序的性能分析

稳定性:归并排序稳不稳定关键要看 merge() 函数,也就是两个有序子数组合并成一个有序数组的那部分代码。

时间复杂度:归并排序的执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度是非常稳定的,不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn)。

空间复杂度:尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了。在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小,所以空间复杂度是 O(n)。

 

快速排序的原理

如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择p 到 r 之间的任意一个数据作为 pivot(分区点)。

我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。

 

快速排序的性能分析

稳定性、空间复杂度:快排是一种原地、不稳定的排序算法。

时间复杂度:快排的时间复杂度也是 O(nlogn)。

 

归并排序和快速排序是两种稍微复杂的排序算法,它们用的都是分治的思想,代码都通过递归来实现,过程非常相似。理解归并排序的重点是理解递推公式和 merge() 合并函数。同理,理解快排的重点也是理解递推公式,还有partition() 分区函数。

归并排序算法是一种在任何情况下时间复杂度都比较稳定的排序算法,这也使它存在致命的缺点,即归并排序不是原地排序算法,空间复杂度比较高,是 O(n)。正因为此,它也没有快排应用广泛。

快速排序算法虽然最坏情况下的时间复杂度是 O(n2),但是平均情况下时间复杂度都是 O(nlogn)。不仅如此,快速排序算法时间复杂度退化到 O(n2) 的概率非常小,我们可以通过合理地选择 pivot 来避免这种情况。

 

 

13、线性排序:如何根据年龄给100万用户数据排序?

总结:桶排序、计数排序、基数排序
一、线性排序算法介绍
1.线性排序算法包括桶排序、计数排序、基数排序。
2.线性排序算法的时间复杂度为O(n)。
3.此3种排序算法都不涉及元素之间的比较操作,是非基于比较的排序算法。
4.对排序数据的要求很苛刻,重点掌握此3种排序算法的适用场景。
二、桶排序(Bucket sort)
1.算法原理:
1)将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行快速排序。
2)桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。
2.使用条件
1)要排序的数据需要很容易就能划分成m个桶,并且桶与桶之间有着天然的大小顺序。
2)数据在各个桶之间分布是均匀的。
3.适用场景
1)桶排序比较适合用在外部排序中。
2)外部排序就是数据存储在外部磁盘且数据量大,但内存有限无法将整个数据全部加载到内存中。
4.应用案例
1)需求描述:
有10GB的订单数据,需按订单金额(假设金额都是正整数)进行排序
但内存有限,仅几百MB
2)解决思路:
扫描一遍文件,看订单金额所处数据范围,比如1元-10万元,那么就分100个桶。
第一个桶存储金额1-1000元之内的订单,第二个桶存1001-2000元之内的订单,依次类推。
每个桶对应一个文件,并按照金额范围的大小顺序编号命名(00,01,02,…,99)。
将100个小文件依次放入内存并用快排排序。
所有文件排好序后,只需按照文件编号从小到大依次读取每个小文件并写到大文件中即可。
3)注意点:若单个文件无法全部载入内存,则针对该文件继续按照前面的思路进行处理即可。
三、计数排序(Counting sort)
1.算法原理
1)计数其实就是桶排序的一种特殊情况。
2)当要排序的n个数据所处范围并不大时,比如最大值为k,则分成k个桶
3)每个桶内的数据值都是相同的,就省掉了桶内排序的时间。
2.代码实现(参见下一条留言)
案例分析:
假设只有8个考生分数在0-5分之间,成绩存于数组A[8] = [2,5,3,0,2,3,0,3]。
使用大小为6的数组C[6]表示桶,下标对应分数,即0,1,2,3,4,5。
C[6]存储的是考生人数,只需遍历一边考生分数,就可以得到C[6] = [2,0,2,3,0,1]。
对C[6]数组顺序求和则C[6]=[2,2,4,7,7,8],c[k]存储的是小于等于分数k的考生个数。
数组R[8] = [0,0,2,2,3,3,3,5]存储考生名次。那么如何得到R[8]的呢?
从后到前依次扫描数组A,比如扫描到3时,可以从数组C中取出下标为3的值7,也就是说,到目前为止,包括自己在内,分数小于等于3的考生有7个,也就是说3是数组R的第7个元素(也就是数组R中下标为6的位置)。当3放入数组R后,小于等于3的元素就剩下6个了,相应的C[3]要减1变成6。
以此类推,当扫描到第二个分数为3的考生时,就会把它放入数组R中第6个元素的位置(也就是下标为5的位置)。当扫描完数组A后,数组R内的数据就是按照分数从小到大排列的了。
3.使用条件
1)只能用在数据范围不大的场景中,若数据范围k比要排序的数据n大很多,就不适合用计数排序;
2)计数排序只能给非负整数排序,其他类型需要在不改变相对大小情况下,转换为非负整数;
3)比如如果考试成绩精确到小数后一位,就需要将所有分数乘以10,转换为整数。
四、基数排序(Radix sort)
1.算法原理(以排序10万个手机号为例来说明)
1)比较两个手机号码a,b的大小,如果在前面几位中a已经比b大了,那后面几位就不用看了。
2)借助稳定排序算法的思想,可以先按照最后一位来排序手机号码,然后再按照倒数第二位来重新排序,以此类推,最后按照第一个位重新排序。
3)经过11次排序后,手机号码就变为有序的了。
4)每次排序有序数据范围较小,可以使用桶排序或计数排序来完成。
2.使用条件
1)要求数据可以分割独立的“位”来比较;
2)位之间由递进关系,如果a数据的高位比b数据大,那么剩下的地位就不用比较了;
3)每一位的数据范围不能太大,要可以用线性排序,否则基数排序的时间复杂度无法做到O(n)。
五、思考
1.如何根据年龄给100万用户数据排序?
2.对D,a,F,B,c,A,z这几个字符串进行排序,要求将其中所有小写字母都排在大写字母前面,但是小写字母内部和大写字母内部不要求有序。比如经过排序后为a,c,z,D,F,B,A,这个如何实现呢?如果字符串中处理大小写,还有数字,将数字放在最前面,又该如何解决呢?


以上是关于算法与数据结构的主要内容,如果未能解决你的问题,请参考以下文章

Golang数据结构与算法全能战士

数据结构与算法—算法与算法分析

python 数据结构与算法之排序(冒泡,选择,插入)

计算机常用英语词汇 —— 数据结构与算法

数据结构与算法(中文版) PDF 完整版下载

算法入门《数据结构与算法图解》+《我的第一本算法书》+《学习JavaScript数据结构与算法第3版》