书山有路之学习算法导论(二)--排序和顺序统计量

2023-10-30 05:30

本文主要是介绍书山有路之学习算法导论(二)--排序和顺序统计量,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

今天是大年初一,祝大家新年快乐啊!

这些天看了《算法导论》的第二部分,这一部分我也写了挺久的,接下来以排序为核心来讨论一下。

书山有路勤为径,加油!

二、排序和顺序统计量

排序问题的结构如下:

输入:一个n个数的序列<a1,a2,...,an>

输出:输入序列的一个排列<a1',a2',...,an'>,满足a1'<=a2'<=...<=an'

讨论具体算法之前再回顾一个概念--原址排序,如果输入数组中仅有常数个元素需要在排序过程中存储在数组之外,则称排序算法是原址的。

6.堆排序

6.1 堆

要了解堆排序首先要引入堆的概念。

堆是一个数组,可以被看成一个近似的完全二叉树,树上的每一个结点对应数组中的一个元素。表示堆的数组A包括两个属性:A.length给出数组元素的个数,A.heap-size表示有多少个堆元素存储在该数组中,即A[1...A.length]可能都存放数据,但只有A[1...A.heap-size]中存放的是堆的有效元素,可以知道0<=A.heap-size<=A.length。下图以二叉树的形式展现了一个最大堆。


树的根节点就是A[1],当给定一个结点的下标i,很容易知道它的父节点、左孩子和右孩子的下标:

PARENT(i):return ⌊i/2⌋

LEFT(i):return 2*i

RIGHT(i):return 2*i+1

堆可以分为两种:最大堆和最小堆。

最大堆中除了根结点以外的所有结点都满足:A[PARENT[i]]>=A[i],即某个结点的值至多与其父结点一样大。

最小堆中除了根结点以外的所有结点都满足:A[PARENT[i]]<=A[i],即某个结点的值至少与其父结点一样大。

当把堆看成是一棵树时,堆中一个结点的高度就为该结点到叶结点最长简单路径上边的数目,于是可以定义堆的高度即为根结点的高度。可以证明含n个元素的堆的高度为Θ(lg n)。在堆排序过程中我们使用最大堆,接下来详细说明对堆的基本操作与堆排序过程。

6.2 维护堆的性质

MAX-HEAPIFY通过让A[i]的值在最大堆中逐级下降,使得当下标为i的结点违反堆的性质时以下标i为根结点的子树重新遵循最大堆的性质。在调用MAX-HEAPIFY时假定根结点为LEFT(i)和RIGHT(i)的二叉树都是最大堆。其时间复杂度是O(lg n)。

MAX-HEAPIFY(A,i)   //使以标号i为根结点的树满足最大堆的性质l=LEFT(i)    //左孩子标号r=RIGHT(i)   //右孩子标号if(l<=A.heap-size && A[l]>A[i])   //如果左孩子存在且左孩子值大于当前结点值就标记左孩子为largestlargest=lelselargest=iif(r<=A.heap-size && A[r]>A[largest]) //如果右孩子存在且右孩子值大于当前结点与左孩子中的较大值就标记右孩子为largestlargest=rif(largest!=i){exchange A[i] with A[largest]   //当largest不是i的时候将A[i]的值与A[largest]的值交换MAX-HEAPIFY(A,largest)   //递归调用,维护以largest结点为根结点的最大堆的性质}

6.3 建堆

为了使用堆排序算法,我们首先需要建立一个最大堆。我们利用过程MAX-HEAPIFY把一个大小为n=A.length的数组A[1...n]转换为最大堆。子数组A[n/2+1...n]中的元素都是树的叶结点,每个叶结点都可以看成是只包含一个元素的堆。过程BUILD-MAX-HEAP对树中的其他结点都调用一次MAX-HEAPIFY,最终可以使数组满足最大堆的性质,它具有线性的时间复杂度。

BUILD-MAX-HEAP(A)  //用数组A的元素建立一个最大堆A.heap-size=A.lengthfor(i=A.length/2;i>=1;i--){  //每一次循环开始,结点i+1,i+2,...,n都是一个最大堆的根结点MAX-HEAPIFY(A,i)}

6.4 堆排序算法

堆排序算法利用BUILD-MAX-HEAP将输入数组A[1...n]建成最大堆,其中n=A.length。因为数组中的最大元素总是存放在A[1]中,通过它与A[n]进行互换,互换之后再利用MAX-HEAPIFY来维护最大堆的性质,不断重复这一过程,可以实现对数组元素的排序。堆排序算法的时间复杂度是O(nlgn)。

HEAPSORT(A)   //使用堆排序算法将数组A中的元素从小到大排序BUILD-MAX-HEAP(A)   //首先用数组A的元素建立一个最大堆for(i=A.length;i>=2;i--){    //每次循环的过程就是将在堆中的最大值取出并排好至相应的位置exchange A[1] with A[i]         //i代表堆中元素的个数,将A[1]与A[i]交换也就是找到第i小的元素并排在第i位上A.heap-size=A.heap-size-1      //排好一个元素,堆中元素的个数减1MAX-HEAPIFY(A,1)     //元素交换后,A[1]的值发生了变化,于是要从1号结点出发来维护堆的性质}

6.5 优先队列

优先队列是一种用来维护由一组元素构成的集合S的数据结构,其中每一个元素都有一个相关的值,称为关键字(key)。一个最大优先队列可以支持如下操作:插入一个新元素、返回具有最大关键字的元素、去掉并返回具有最大关键字的元素、改变某个元素关键字的值。最小优先队列的操作与之相似,在返回队首元素时有差别,最小优先队列可以返回具有最小关键字的元素。接下来,以最大优先队列为例讨论如何实现优先队列的相关操作。

①返回具有最大关键字的元素。因为以堆为背景,堆对应的二叉树的根就是具有最大关键字的元素,所以该操作只需要在Θ(1)的时间内就可以实现。

HEAP-MAXIMUM(A)  //返回优先队列A中具有最大关键字的元素return A[1]    //A[1]就是具有最大关键字的元素

②去掉并返回具有最大关键字的元素。这个操作也就是先取出具有最大关键字的元素,再从根结点出发维护堆的性质。它的时间复杂度是O(lgn),主要的时间开支在维护堆的性质中。

HEAP-EXTRACT-MAX(A)if(A.heap-size<1)    //检查优先队列是否为空error "heap underflow"max=A[1]    //取出具有最大关键字的元素A[1]=A[heap-size]   //与堆排序的操作相似,将A[heap-size]置于根结点A.heap-size=A.heap-size-1   //堆元素的个数减一MAX-HEAPIFY(A,1)   //从根结点出发维护堆的性质return max   //返回具有最大关键字的元素

③将元素x的关键字增加至k,假设k的值不小于x的关键字值。因为增大关键字可能会违反最大堆的性质,需要在当前结点到根结点的路径上为新增的关键字寻找恰当的插入位置。在该操作实现的过程中,当前元素会不断地与其父结点进行比较,如果当前元素地关键字较大,则当前元素与其父结点进行交换。不断重复这一过程,直到当前元素的关键字小于其父结点时终止,因为此时就可以符合最大堆的性质了。在包含n个元素的堆上,此操作的时间复杂度是O(lgn)

HEAP-INCREASE-KEY(A,i,key)  //将下标为i元素的关键字的值增加到key,假设key的值不小于原关键字的值if(key<A[i])   //判断新关键字是否比原关键字的值小error "new key is smaller than current key"A[i]=key    //修改关键字的值while(i>1 && A[PARENT(i)]<A[i]){    //寻找合适的插入位置,判断父结点是否存在且父结点的关键字的值是否小于该关键字exchange A[i] with A[PARENT(i)]  //当父结点关键字的值小于该关键字那么就将其与父结点进行交换i=PARENT(i)   //修改i的值为父结点的下标}

④插入一个新元素到优先队列中。这个插入操作的思想是首先增加一个关键字为-∞的叶结点来扩展最大堆,然后通过调用HEAP-INCREASE-KEY为新结点设置对应的关键字,同时保持最大堆的性质。在包含n个元素的堆上,该操作的时间复杂度是O(lgn)。

MAX-HEAP-INSERT(A,key)     //插入一个关键字为key的新元素A.heap-size=A.heap-size+1    //堆元素的个数加1A[A.heap-size]=-∞    //先用一个极小的关键字来扩展最大堆HEAP-INCREASE-KEY(A,A.heap-size,key)   //为新结点设置对应的关键字
可以看出,在一个包含n个元素的堆中,所有优先队列的操作都可以在O(lgn)时间内完成。


7.快速排序

7.1 快速排序的描述

对于包含n个数的输入数组来说,快速排序是一种最坏情况时间复杂度为Θ(n^2)的排序算法。虽然最坏情况时间复杂度很差,但是快速排序通常是实际排序应用中最好的选择,因为它的平均性能非常好:它的期望时间复杂度为Θ(nlgn),而且其中隐含的常数因子很小,同时快速排序能够进行原址排序(输入数组中仅有常数个元素需要在排序过程中存储在数组之外)。

快速排序的主要思想也是分治思想。对子数组A[p...r]进行快速排序主要有以下三步:

分解:数组A[p...r]被划分成两个(可能为空)子数组A[p...q-1]和A[q+1...r],使得A[p...q-1]中的每一个元素都小于等于A[q],而A[q]也小于等于A[q+1...r]中的每个元素。其中计算下标q也是划分过程的一部分。

解决:通过递归调用快速排序,对子数组A[p...q-1]和A[q+1...r]进行排序。

合并:因为子数组都是原址排序的,所以不需要合并操作,进行分解和解决之后,数组A[p...r]已经有序。

QUICKSORT(A,p,r)  //对数组元素A[p...r]进行快速排序if(p<r){    //当需要排序的元素至少有两个时才进行下列排序操作,否则已经有序q=PARTITION(A,p,r)   //划分数组,找到划分点qQUICKSORT(A,p,q-1)   //对数组元素A[p...q-1]进行快速排序QUICKSORT(A,q+1,r)  //对数组元素A[q+1...r]进行快速排序}   //递归调用结束以后A[p...r]有序

算法的关键步骤就是对数组的划分过程PARTITION,它实现了对子数组A[p...r]的原址排序。

PARTITION(A,p,r)    //对A[p...r]进行划分x=A[r]    //选择一个主元,以A[r]为中心对数组进行划分i=p-1     //划分之后标号<=i的元素的值都小于等于xfor(j=p;j<=r-1;j++)   //循环检查以划分数组if(A[j]<=x){   //将当前元素与主元进行比较,如果小于等于主元进行如下操作i=i+1   //将i加1exchange A[i] with A[j]  //把A[i]的值与A[j]交换使得标号<=i的元素的值保证<=x}exchange A[i+1] with A[r]  //把主元置于合适的位置上return i+1   //返回主元现在的标号
可以证明PARTITION在子数组A[p...r]上的时间复杂度是Θ(n),其中n=r-p+1。

7.2 快速排序的性能

快速排序的运行时间依赖于划分是否平衡。如果划分是平衡的,那么快速排序算法性能与归并排序一样。如果划分是不平衡的,那么快速排序的性能就接近于插入排序。对于快速排序来说,当其划分都是最大程度不平衡的,算法的时间复杂度是Θ(n^2),当其好的划分和差的划分交替出现时,快速排序的时间复杂度与全是好的划分时一样,都是O(nlgn)。

7.3 快速排序的随机化版本

很多人都选择随机化版本的快速排序作为大数据输入情况下的排序算法。

随机化版本与始终采用A[r]作为主元的方法不同,是从子数组A[p...r]中随机选择一个元素作为主元,为了达到这一目的,需要将A[r]与从A[p...r]中随机选出的一个元素交换。因为主元元素是随机选择的,我们期望在平均情况下,对输入数组的划分是比较均衡的。新的划分程序中,我们只是在真正进行划分前进行一次交换,随机化划分的伪码如下:

RANDOMIZED-PARTITION(A,p,r)   //对A[p..r]进行随机划分i=RANDOM(p,r)     //随机选择在[p,r]中的以一个整数exchange A[r] with A[i]   //将随机选中的A[i]与A[r]进行交换return PARTITION(A,p,r)   //随机选定主元后,再对A[p..r]进行划分

新的随机化版本的快速排序程序如下:

RANDOMIZED-QUICKSORT(A,p,r)  //对A[p..r]进行快速排序的随机化版本if(p<r){   //检查子数组A[p..r]中元素的个数是否多于1个q=RANDOMIZED-PARTITION(A,p,r)  //随机化划分RANDOMIZED-QUICKSORT(A,p,q-1)  //递归调用快速排序RANDOMIZED-QUICKSORT(A,q+1,r)}

我们通过证明可以得出结论:使用RANDOMIZED-QUICKSORT,在输入元素互异的情况下,快速排序算法的期望运行时间为O(nlgn)。


8.线性时间排序

8.1 排序算法的下界

先介绍一个概念--比较排序,比较排序是指在排序的结果中,各元素的次序依赖于它们之间比较的排序方法。可以证明,任何比较排序在最坏情况下都要经过Ω(nlgn)次比较。同时,堆排序和归并排序都是渐进最优的比较排序算法,因为堆排序和归并排序的运行时间上界为O(nlgn)。

8.2 计数排序

计数排序假设n个输入元素中的每一个都是在0到k区间内的一个整数,k为某个整数。当k=O(n)时,排序的运行时间为Θ(n)。

计数排序的基本思想是:对每一个输入元素x,确定小于x的元素个数。利用这一信息,就可以直接把x放到它在输出数组中的位置上了。在伪代码中,假设输入是一个数组A[1...n],A.length=n。同时还需要两个数组B[1...n]存放排序的输出,C[0...k]提供临时存储空间。

COUNTING-SORT(A,B,k)    //对数组A进行计数排序,结果存在数组B中,A中元素的值均小于klet C[0..k] be a new arrayfor(i=0;i<=k;i++)   //初始化数组CC[i]=0for(j=1;j<=A.length;j++)  //这个循环结束后,C[i]中存放A数组中值为i元素的个数C[A[j]]=C[A[j]]+1for(i=1;i<=k;i++)   //这个循环结束后,C[i]中存放A数组中值小于等于i元素的个数C[i]=C[i]+C[i-1]for(j=A.length;j>=1;j--){    //让j从A.length降到1可以保证计数排序稳定B[C[A[j]]]=A[j]    //在输出数组的相应位置写入A[j]C[A[j]]=C[A[j]]-1   //修改相应C[A[j]]的值}

计数排序的一个重要性质就是它是稳定的:具有相同值得元素在输出数组中得相对次序与它们在输入数组中得相对次序相同。

8.3 基数排序

基数排序是先按最低有效位来进行排序的算法。伪码比较直观,如下:

RADIX-SORT(A,d)    //对数组A中的元素进行基数排序,其中A中的元素均为n位数,第1位是最低位,第n位是最高位for(i=1;i<=d;i++)    //对每一位进行排序use a stable sort to sort array A on digit i   //使用一种稳定的排序算法(比如计数排序)对第i位进行排序

8.4 桶排序

桶排序假设输入数据服从均匀分布,即输入是由一个随机过程产生的,该过程将元素均匀、独立地分布在[0,1)区间上,平均情况下它的时间代价为O(n)。因为对输入数据作出了假设,桶排序的速度也很快。

桶排序将[0,1)区间划分为n个相同大小的子区间,或称为桶。然后将n个输入数分别放到各个桶中。为了得到输出结果,我们先对每个桶中的数进行排序,然后遍历每个桶,按照次序把各个桶中的元素列出来即可。

BUCKET-SORT(A)    //对数组A进行桶排序,数组A中的元素在区间[0,1)上均匀分布n=A.length    //n保存数组A 中元素的个数let B[0..n-1] be a new array  //临时数组B存放链表,即桶,有n个桶for(i=0;i<=n-1;i++)make B[i] an empty list   //初始化B[i]为空表for(i=1;i<=n;i++)insert A[i] into list B[⌊nA[i]⌋]   //根据A[i]值的大小将A[i]插入到对应的桶中for(i=0;i<=n-1;i++)sort list B[i] with insertion sort   //把每个桶中的元素进行插入排序concatenate the lists B[0],B[1],...,B[n-1] together in order   //把每个桶中的元素合并起来将可以得到有序的数组了

我们可以证明知道:计数排序、基数排序和桶排序都可以在线性时间内完成。


9.中位数和顺序统计量

9.1 最小值和最大值

在一个由n个元素组成的集合中,第i个顺序统计量是该集合中第i小的元素。所以,最小值是第1个顺序统计量,最大值是第n个顺序统计量。中位数是所属集合的“中点元素”。

在一个有n个元素的集合中,进行n-1次比较就可以得到其最小值,代码很简单:

MINIMUM(A)   //求数组A的最小值min=A[1]   //初始化最小值为A[1]for(i=2;i<=A.length;i++)  //遍历数组A进行比较if(min>A[i])   //如果当前元素值小于min就修改最小值min=A[i]return min   //返回最小值

当然最大值也可以通过这种比较得出来。如果用这种方法分别独立地找出最大值和最小值,那么共需要2n-2次比较。

如果需要同时找出最大值和最小值,还有一种更优的方法:对输入元素成对进行处理,首先,我们将一对输入元素相互进行比较,然后把较小的与当前最小值比较,把较大的与当前最大值比较,这样对每两个元素共需比较3次。用这种方法,找出最大值和最小值只需要最多3⌊n/2⌋次比较,比分别独立寻找最大最小值要优一些。

9.2 期望为线性时间的选择算法

本节介绍一种解决选择问题的分治算法,其渐进运行时间为Θ(n),是线性时间的。与快速排序一样,我们仍然将输入数组进行递归划分,但与快速排序不同的是,选择算法中只处理划分的一边。这里,假设输入的数据都是互异的。

RANDOMIZED-SELECT(A,p,r,i)   //寻找A[p..r]中的第i个顺序统计量if(p==r)   //如果只有一个元素则返回它return A[p]q=RANDOMIZED-PARTITION(A,p,r)  //随机版本的数组划分,A[q]称为主元,含义与快速排序中的相同   k=q-p+1   //A[p..q]中元素的个数if(i==k)   //如果k正好等于i,说明A[q]就是第i个顺序统计量return A[q]else if(i<k)   //如果i<k,说明第i个顺序统计量在A[p..q-1]中return RANDOMIZED-SELECT(A,p,q-1,i)   //递归选择else    //如果i>k,说明第i个顺序统计量在A[q+1,r]中return RANDOMIZED-SELECT(A,q+1,r,i-k)   //递归选择

我们可以得到以下结论:假设所有的元素互异,在期望线性时间内,我们可以找到任意顺序统计量,特别是中位数。

至于最坏情况为线性时间的选择算法我暂时不深入讨论,我也还需要花些功夫才可以搞懂-_-!

这篇关于书山有路之学习算法导论(二)--排序和顺序统计量的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/305869

相关文章

Spring Boot 配置文件之类型、加载顺序与最佳实践记录

《SpringBoot配置文件之类型、加载顺序与最佳实践记录》SpringBoot的配置文件是灵活且强大的工具,通过合理的配置管理,可以让应用开发和部署更加高效,无论是简单的属性配置,还是复杂... 目录Spring Boot 配置文件详解一、Spring Boot 配置文件类型1.1 applicatio

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.

Java时间轮调度算法的代码实现

《Java时间轮调度算法的代码实现》时间轮是一种高效的定时调度算法,主要用于管理延时任务或周期性任务,它通过一个环形数组(时间轮)和指针来实现,将大量定时任务分摊到固定的时间槽中,极大地降低了时间复杂... 目录1、简述2、时间轮的原理3. 时间轮的实现步骤3.1 定义时间槽3.2 定义时间轮3.3 使用时

一文详解SQL Server如何跟踪自动统计信息更新

《一文详解SQLServer如何跟踪自动统计信息更新》SQLServer数据库中,我们都清楚统计信息对于优化器来说非常重要,所以本文就来和大家简单聊一聊SQLServer如何跟踪自动统计信息更新吧... SQL Server数据库中,我们都清楚统计信息对于优化器来说非常重要。一般情况下,我们会开启"自动更新

C++快速排序超详细讲解

《C++快速排序超详细讲解》快速排序是一种高效的排序算法,通过分治法将数组划分为两部分,递归排序,直到整个数组有序,通过代码解析和示例,详细解释了快速排序的工作原理和实现过程,需要的朋友可以参考下... 目录一、快速排序原理二、快速排序标准代码三、代码解析四、使用while循环的快速排序1.代码代码1.由快

Java进阶学习之如何开启远程调式

《Java进阶学习之如何开启远程调式》Java开发中的远程调试是一项至关重要的技能,特别是在处理生产环境的问题或者协作开发时,:本文主要介绍Java进阶学习之如何开启远程调式的相关资料,需要的朋友... 目录概述Java远程调试的开启与底层原理开启Java远程调试底层原理JVM参数总结&nbsMbKKXJx

如何通过Golang的container/list实现LRU缓存算法

《如何通过Golang的container/list实现LRU缓存算法》文章介绍了Go语言中container/list包实现的双向链表,并探讨了如何使用链表实现LRU缓存,LRU缓存通过维护一个双向... 目录力扣:146. LRU 缓存主要结构 List 和 Element常用方法1. 初始化链表2.

golang字符串匹配算法解读

《golang字符串匹配算法解读》文章介绍了字符串匹配算法的原理,特别是Knuth-Morris-Pratt(KMP)算法,该算法通过构建模式串的前缀表来减少匹配时的不必要的字符比较,从而提高效率,在... 目录简介KMP实现代码总结简介字符串匹配算法主要用于在一个较长的文本串中查找一个较短的字符串(称为

通俗易懂的Java常见限流算法具体实现

《通俗易懂的Java常见限流算法具体实现》:本文主要介绍Java常见限流算法具体实现的相关资料,包括漏桶算法、令牌桶算法、Nginx限流和Redis+Lua限流的实现原理和具体步骤,并比较了它们的... 目录一、漏桶算法1.漏桶算法的思想和原理2.具体实现二、令牌桶算法1.令牌桶算法流程:2.具体实现2.1

Python中顺序结构和循环结构示例代码

《Python中顺序结构和循环结构示例代码》:本文主要介绍Python中的条件语句和循环语句,条件语句用于根据条件执行不同的代码块,循环语句用于重复执行一段代码,文章还详细说明了range函数的使... 目录一、条件语句(1)条件语句的定义(2)条件语句的语法(a)单分支 if(b)双分支 if-else(