八大算法一一道来

1. 插入排序

对于一个待排序的序列,如果它的前半部分是有序的(假设有 M 个元素),后半部分是无序的(假设有 N-M 个元素),那么最直接的排序方法就是从无序的部分中取出一个元素,并将之插入到有序部分的合适位置上,这样,有序部分就包含了M+1个元素,无序部分剩下了N-M-1个元素,接着再从无序部分中取出一个元素,并将之插入到有序部分的合适位置上,那么有序部分就包含了M+2个元素,无序部分剩下了N-M-2个元素,这样,经过 N-M 次这样的操作之后,整个序列就变成有序的了,这就是插入排序的基本思想。

对于任意一个待排序序列,总是可以把它分成有序部分和无序部分的,只是开始的时候,有序部分只包含一个元素(M=1),而无序部分包含了 N-1 个元素,举个简单的例子,假设待排序序列为{5,3,2,6},我们需要将它进行从小到大的排序,开始的时候有序部分为{5},无序部分为{3,2,6}:

第一趟排序操作:从{3,2,6}中取出3,将之插入有序部分中,构成新的有序部分为{3,5},此时无序部分变成了{2,6};

第二趟排序操作:从{2,6}中取出2,将之插入到有序部分,构成新的有序部分为{2,3,5},此时无序部分变成了{6};

第三趟排序操作:从{6}中取出6,插入到有序部分中,有序部分变成了{2,3,5,6},排序完成。

对于插入排序而言,影响性能的操作主要是“插入”,因为我们要把一个元素插入到一个有序序列中,并保持该序列继续有序。根据“插入”方法的不同,又有了“直接插入排序”、“折半插入排序”等。所谓的直接插入就是将待插入的元素与有序序列中的元素一个个进行比较,找到合适的位置进行插入,所谓的折半插入就是将待排序的元素与有序序列的中间元素进行比较,若大于该中间元素,就在有序序列的后半段继续寻找合适的位置,否则就在有序序列的前半段继续寻找合适的位置。显然,折半插入的效率更高。不过需要注意的是,折半插入只是能更有效率地找到合适的插入位置,如果序列存放在数组中,那么插入会引起数组元素的向后移动,而折半插入并不会改变移动的次数,所以其时间复杂度仍为O(n2)。当然,如果用链表来存储序列,就不用移动元素,只是改一下节点指针即可,但在链表下就不太容易实现“折半查找”了,因为没有所谓的下标。

直接插入排序在什么情况下效率最高呢?当原序列已经是有序序列了,此时时间复杂度为O(n),当原序列已经有序时,合适的插入位置就是当前的位置,不需要移动任何元素,倒是折半插入的效率略有下降,因为它还得“机械地”找合适的位置。那啥时候效率最低呢?当原序列逆序时!此时时间复杂度为O(n2)。

直接插入排序和折半插入排序都是稳定的排序算法,所谓稳定,就是当两个元素相等时,排序后的先后顺序与排序前的先后顺序一致。不过,不是所有的插入排序都是稳定的,如希尔排序,希尔排序的基本思想是将原序列看成几个子序列,比如原序列为{x1,x2,x3,x4,x5,x6},希尔排序有个“增量”的概念,如果增量为2,那么第一个子序列就是第1、3、5个元素,第二个子序列就是2、4、6个元素,然后分别对这两个子序列进行排序(注意,排序后,原序列的第1、3、5个元素是有序的,第2、4、6个元素是有序的,其他的就不保证了),这样做的目的是使得原序列比之前稍微有序一点,然后我们就可以对这个“稍微有序一点”的序列进行普通的插入排序,因为插入排序对有序序列更有效率一点(这里的有序可千万不能是逆序,否则就直接掉到最差情况了)。那希尔排序为什么是不稳定的呢?举例如下,原序列{1,5,31,32,6,7},其中下标表示相同元素在原序列中的先后顺序。假设希尔排序的增量为2,那么子序列排序后的结果为{1,32,31,5,6,7},接着对这个序列进行直接插入排序,由于直接插入排序是稳定的,不会改变相同元素的先后顺序,所以最终的结果是相同的元素的先后顺序被改变了!所以希尔排序是不稳定的

2. 交换排序

插入排序不同,交换排序的主要操作是“交换”,所以不需要大规模的移动元素。举个简单的例子,比如要把序列 {3,1,2} 按从小到大的顺序排列,我们先比较3与1,因为 1<3,所以将两者交换得到 {1,3,2},再比较3与2,因为2<3,所以将两者进行交换,得到{1,2,3},这样就完成了排序。

最简单的交换排序是冒泡排序,举个简单例子,比如要把数组 x[6] (下标从0~5)按照从小到大的顺序排列,第一趟排序的结果就是把最大的元素放在x[5]处,具体是这么操作的:

1、比较x[0]和x[1],如果x[0]>x[1],则将两者交换(tmp = x[1]; x[1]=x[0]; x[0]=tmp),否则不交换;

2、比较x[1]和x[2],如果x[1]>x[2],则将两者交换(tmp = x[2]; x[2]=x[1]; x[1]=tmp),否则不交换;

……

5、比较x[4]和x[5],如果x[4]>x[5],则将两者交换(tmp = x[5]; x[5]=x[4]; x[4]=tmp),否则不交换;

这样经过5次比较后,最大的元素就被交换到了x[5]处,接着进行第二趟排序,结果就是将第二大的元素放在了x[4]处(这个过程需要4次比较:x[0]与x[1],x[1]与x[2],x[2]与x[3],x[3]与x[4],x[5]就不需要参与比较了)。接着进行第三趟排序…… 五趟排序后,序列就变成有序的了。

伪代码如下(设序列为x[N],N为序列的长度):

for(int i=N-1; i>0; i--){
    for(int j=0;j<i;j++){
        if(x[j]>x[j+1]) swap(x[j],x[j+1]);
    }
}

总结一下:冒泡排序要进行N-1趟排序,平均每趟排序需要进行N/2次比较操作,所以时间复杂度为O(N2)。冒泡排序是一种稳定的排序算法(大家试试就知道了:))。

改善性能的基本思路是尽量减少重复的操作,比如减少“比较”操作,对于冒泡排序而言,经过一趟排序后,无序部分仍然像之前那样“无序”,所以上一趟排序对下一趟排序带来的好处太少,导致下一趟排序仍需要进行很多操作。下面将要讨论的快速排序估计就是受到了类似的启发。

快速排序的第一趟排序不是把最大的元素放在序列的末端,而是把第一个元素放在序列的合适位置,同时保证该位置前边的元素均小于第一个元素,该位置后边的元素均不小于第一个元素。第二趟排序呢,思路一样,只是分别就处理刚刚生成的两个子序列,递归算法很容易就实现了。以第一趟排序为例,假设输入的序列为 x[N]

tmp = x[0];
i = 0;
j = N-1;

while(i<j){
    for(; j>i && x[j]>=tmp; j--);
    if(j>i) {x[i]=x[j]; i++; }

    for(; i<j && x[i]<tmp; i++);
    if(i<j) {x[j]=x[i]; j--; }
}

x[i]=x[0];

改成函数的形式如下:

int partition(int x[], int i,int j) {
    tmp = x[i];
    while(i<j){
        for(; j>i && x[j]>=tmp; j--);
        if(j>i) {x[i]=x[j]; i++; }
        for(; i<j && x[i]<tmp; i++);
        if(i<j) {x[j]=x[i]; j--; }
    }
    x[i]=x[0];
    return i;
}

快速排序算法如下:

void quickSort(int x[],int left,int right){
    int tmp;
    while(left<right){
        tmp = partition(x,left,right);
        quickSort(x,left,tmp-1);
        quickSort(x,tmp+1,right);
    }
}

一般情况下,每趟排序会将原序列分成两个子序列,所以需要进行log2(n)趟排序(当子序列的长度为1时就不用继续递归了),每趟排序还是需要处理那n个元素,所以需要进行 O(n) 次操作,所以平均意义下的时间复杂度为O(nlog2(n))。最差的情况是什么呢?当原序列已经有序时,程序会“机械”地执行n-1趟排序,所以时间复杂度变成了O(n2)。因为采用了递归,每次函数调用会发生压栈操作,就会占用一定数量的栈空间,又因为递归深度为log2(n),所以会发生O(log2(n))次压栈,所以算法的空间复杂度为O(log2(n))。快速排序是一种不稳定的排序算法,举个简单例子,原序列为{5,31,4,6,7,32},第一趟排序的结果是{32,31,4,5,6,7,},显然,相同元素的先后顺序被打乱了。

3. 选择排序

选择排序的基本思想是: 第一趟排序选择一个最大的元素放在序列的末端,第二趟排序选择一个次大的元素放在倒数第二的位置……所以在选择排序中,每趟排序的效果跟冒泡排序是一样的,简单选择排序是这样操作的,给定一个序列 x[N],第一趟排序是从 1~N-1 个元素中选一个最小的,假如下标为k,则将 x[0] 与 x[k] 交换,第二趟排序是从 2~N-1 中选择一个最小的,假如下标为p,则将 x[1] 与 x[p] 交换……不难发现,对于一个长度为N的序列,需要进行 N-1 趟排序,平均每趟排序要进行 N/2 次比较,所以时间复杂度为 O(N2),又每趟排序仅需要一个临时变量来存放最小元素的下标,以及一个临时变量用于交换元素,所以空间复杂度为O(1)。代码如下:

void selectSort(int x[],int N){
    for(int i=0;i<N-1;i++){
        int min = i;
        for(int j=i+1;j<N;j++){
            if(x[j])<x[min]) min=j;
        }
        swap(x[i],x[min]);
    }
}

简单选择排序是一种不稳定的排序算法,举个简单例子,原序列为{31,32,2,1,6,7,8,9},第一趟排序的结果是{1,32,2,31,6,7,8,9},第二趟排序的结果是{1,2,32,31,6,7,8,9},显然是不稳定的。

简单选择排序的上一趟排序仍然不能给下一趟排序带来更多的好处,堆排序的第 i 趟排序也会选出序列中的第 i 小的元素,但是堆排序的上一趟排序会给下一趟排序省下不少操作。

堆排序把序列看成了一颗完全二叉树,比如有序列{16,14,16,8,7,9,3,2,4,1},对应到完全二叉树如下,x[i]的子节点分别是x[2i+1]和x[2i+2]:

对于有n个节点的完全二叉树,树的高度为?log2(n+1)?。最后一个有子节点的节点为 x[?n/2??1]。对于上图而言,就是 x[4](5号节点)。

堆排序的基本思想就是保证每个节点都要比它的子节点大(这就是所谓的最大堆),那么根节点就是序列中最大的元素,所以建立一个最大堆的过程,就是第一趟排序,选出了最大的元素,将该元素与序列末端的元素交换后,再调整 x[0~n-2] 对应的堆,使之仍然为一个最大堆,这样就选出了第二大的元素,以此类推…..尽管这样仍然需要进行 n-1 趟排序,但是每趟排序(第一趟排序除外)只需要执行的比较次数与树深成正比,即O(log2(n)),所以时间复杂度为O(nlog2(n))。

堆排序的基本操作是“调整”,给定了一个完全二叉树,假设它的子树已经满足了最大堆的性质,但是由于根节点可能小于它的子节点,所以必须通过调整才能使得这个二叉树也满足最大堆的性质,这样我们就需要比较根节点与它的两个子节点,如果较大的子节点为左子节点,且左子节点大于根节点,则将这两个节点交换。交换后可能破坏了左子树的最大堆性质,所以需要继续调整左子树…… “堆调整”的代码如下:

void adjustHeap(int x[],int i,int n){
    int l = 2*i+1, r = 2*i+2;
    int max = i;
    if(l<n && x[l]>x[max]) max = l;
    if(r<n ** x[r]>x[max]) max = r;

    if(max==i) return;
    if(max==l) {swap(x[i],x[l]); adustHeap(x,l,n); }
    if(max==r) {swap(x[i],x[r]); adustHeap(x,r,n); }
}

刚才只是介绍了如何调整堆,但是给出了待排序序列,我们首先需要建立一个堆。基本的思想就是先建立最小的子堆,然后调整得到一个稍大一点的堆,然后调整得到更大一点的堆…… (最小的堆当然就是叶子节点了:))代码如下:

void createHeap(int x[],int n){
    for(int i=n/2-1;i>=0;i--) adustHeap(x,i,n);
}

把堆排序的细节介绍完了,现在介绍当给了一个待排序序列后,如何用堆排序的方式对它排序。第一步是建立一个最大堆,则堆顶元素(根节点)就是序列的最大元素,将该元素与序列末端的元素进行交换,现在堆的性质可能被破坏了,所以调整堆,使之重新成为最大堆(现在只考虑x[0~n-2]),然后将新的堆顶元素与序列的倒数第二个元素交换,以此类推…… 代码如下:

void heapSort(int x[],int n){
    heapCreate(x,0,n);
    for(int i=n-1; i>0; i--){
        swap(x[0],x[i]);
        adustHeap(x,0,n);
    }
}

在堆排序中,建堆是比较耗时的操作,但也是O(n)的量级,之后的调整的时间复杂度仅为O(log2(n))。堆排序是一种不稳定的排序算法,举例如下:原序列为{2,1,6,3,0,71,5,72},建堆的示意图如下:

第一趟排序完成,选出的最大的元素72,第二趟排序如下:

第二趟排序完成,选出次大的元素71,以此类推,这个例子倒是没有体现出不稳定性,大家可以自己修改修改例子:)。

4. 归并排序

归并排序的思想是把序列分成两个子序列,分别对两个子序列排序后,再把它们合并在一起,大家或许注意到之前的快速排序也会将原序列分为两个子序列(一般而言),所以归并排序的性能与快速排序差不多,但是归并排序一定会把原序列分成两个子序列,而快速排序不一定,所以归并排序的最差性能比快速排序好,付出的代价就是增加了空间复杂度。

归并排序的核心在于“归并”,假设子序列x1[n1]、x2[n2]已经分别有序,那么归并的方法如下(需要一些额外的空间,设为t[n1+n2]):

int p = 0;
for(int i=0,j=0;i<n1 && j<n2;){
    if(x1[i]<=x2[j]){t[p]=x[i];i++;p++;}
    else {t[p]=x[j];j++;p++;}
}
if(i==n1){
    for(;j<n2;j++) t[p++] = x2[j];
}
if(j==n2){
    for(;i<n2;i++) t[p++] = x1[i];
}

归并排序是稳定的排序方式,因为排序过程中不涉及“大跨度的元素交换”。

最后一类排序:基数排序就不多说了:)

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-20 03:22:02

八大算法一一道来的相关文章

八大算法思想

八大算法思想分别是:枚举.递推.递归.分治.贪心.试探法.动态迭代和模拟算法思想. 1.比较“笨”的枚举算法思想 枚举最大的缺点是运算量比较大,解题效率不高. 如果题目的规模不是很大,在规定的时间与空间限制内能够求出解,那么最好是采用枚举法,而无须太在意是够还有更快的算法,这样可以使你有更多的时间去解答其他难题. //枚举法解决“填写运算符”的问题 import java.util.Scanner; public class meijujisuan5ge5 { public static voi

算法-PHP实现八大算法

八大算法原理详解 交换函数:注意要按引用传递,否则无法真正交换两个数的值 function exchange(&$a, &$b){ $temp = $a; $a = $b; $b = $temp; } 1.直接插入算法 //第一种实现 function insert_sort($arr){ for ($i = 0; $i < count($arr)-1; $i++){ for($j = $i+1; $j > 0; $j--){ if($arr[$j] > $arr[$j-

(一)八大算法思想

八大算法 八大算法:枚举.递推.递归.分治.贪心.试探法.动态迭代和模拟算法思想. 一.枚举算法思想(暴力算法) 将问题的所有可能答案一一列举,根据判断条件判断此答案是否合适,一般用循环实现. 经典运用:百钱买百鸡.填写运算符 二.递推算法思想 1.顺推法:从已知条件出发,逐步推算出要解决问题的方法. 2.逆推法:从已知结果出发,用迭代表达式逐步推算出问题开始的条件,即顺推法的逆过程. 经典运用:斐波那契数列(顺推法).银行存款(逆推法) 三.递归算法思想 1.递归过程一般通过函数或子过程实现:

《转》八大算法详细讲解

转自http://blog.csdn.net/jobbofhe/article/details/51426934 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存. 我们这里说说八大排序就是内部排序. 当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序.堆排序或归并排序序. 快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短:

寻找第K大元素的八大算法、源码及拓展

一.问题描述 所谓“第(前)k大数问题”指的是在长度为n(n>=k)的乱序数组中S找出从大到小顺序的第(前)k个数的问题. 第K大问题可以是现实问题,譬如竞价排名中的第K个排名,或者多个出价者中的第K大价格等等. 二.解法归纳 解法1: 我们可以对这个乱序数组按照从大到小先行排序,然后取出前k大,总的时间复杂度为O(n*logn + k). 很好理解,利用快排对所有元素进行排序,然后找到第K个元素即可. 解法2: 利用选择排序或交互排序,K次选择后即可得到第k大的数.总的时间复杂度为O(n*k)

ios开发——常用经典算法OC篇&amp;冒泡/快速

冒泡排序与快速排序 1.序言 ios开发中涉及到算法的地方还真不多,除非你的应用程序真的非常大,或者你想你的应用程序性能非常好才会去想到关于算法方面的性能优化,而在ios开发中真的能用得到的也就是关于排序的,当然如果你是做游戏的话那么你可能会涉及到不少的算法或者优化问题,但是这不是本篇文章讨论的范围. 后面的文章中,我将会给大家详细介绍八大算法. 2.冒泡排序 2.1 引出 前面的两篇博客里讲的插入排序是基于“逐个记录插入”,选择排序是基于“选择”,那么冒泡排序其实是基于“交换”.每次从第一个记

PHP SPL神器实现堆排序

之前学习过内部排序的八大算法,也一一写过代码实现.其中堆排序的原理是 将一颗二叉树初始化为堆 依次将最后一个结点与堆顶结点交换.然后调整堆顶元素位置,重置堆. 将二叉树初始化为堆可以看做从最后一个非叶子结点开始,依次调整子堆的堆顶元素,重置堆是指重置堆顶元素. 这种算法的实现如下: <?php #堆排序 function heapSort(&$arr) { #初始化大顶堆 initHeap($arr); #开始交换首尾节点,并每次减少一个末尾节点再调整堆,直到剩下一个元素 for($end

机器学习_深度学习_入门经典(永久免费报名学习)

机器学习_深度学习_入门经典(博主永久免费教学视频系列) https://study.163.com/course/courseMain.htm?courseId=1006390023&share=2&shareId=400000000398149 作者座右铭---- 与其被人工智能代替,不如主动设计机器为我们服务. 长期以来机器学习很多教材描述晦涩难懂,大量专业术语和数学公式让学生望而止步.生活中机器学习就在我们身边,谷歌,百度,Facebook,今日头条都运用大量机器学习算法,实现智能

八大排序算法

转载:http://blog.csdn.net/hguisu/article/details/7776068 目录(?)[+] 概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存. 我们这里说说八大排序就是内部排序. 当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序.堆排序或归并排序序. 快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速