算法导论专题一--排序算法(2)

上节分析了O(n^2)的算法,这节就分析O(nlgn)的算法-归并,快速和堆排序。

一:综述

O(nlgn) 的算法可以分为两大类,两者所用的技术差别较大。归并和快速排序采用的是分治策略,这两者相当于一个对称的过程,一个是自顶向上合并子问题,另一个则自上向下分解子问题。而堆排序利用堆这一数据结构元素间的特殊关系来排序一个序列,另外采用二叉树的方式组织数据使其效率大大提高。

二:分治策略排序算法

1.为什么使用分治?

在上节算法的分析中,不管是冒泡。选择还是插入都不适用于大规模的数据,因为数据一大,数据间比较,移动的次数也增大,这导致运行时间大大增加。自然而然就想到如何把大的问题分解成小问题,如果小问题计算代价小且和原问题相似,同时合并的代价也小。那用分治策略是优于直接对大问题进行处理的。而在排序中,一个大数据不断的分解这个处理过程代价小,直到只剩一个元素的时候,问题规模最小只要简单处理,合并过程虽然需要较多处理,但代价也小。所以将分治用于排序主要是解决三个处理过程,怎么将大的数据分成小数据?怎么对小数据进行处理?怎么将处理结果合并成最后的结果?

2.如何实现归并排序

归并排序:

怎么分解?归并排序是将n规模的序列分成n/2规模(n为偶数情况,奇数也类似),n/2分为n/4,......。直到序列长度为1,这个小问题直接求解,1长度的序列就已经排好序了。怎么合并?归并排序的合并过程需要较多处理,我们就专门分析一下这个过程。依照算法导论,我们也利用牌堆进行分析,假设只有两张牌,比较,小的放上面,大的放下面。如果是两堆牌呢?这两堆牌是通过前面合并得到,它们各自都已经排好序了。同样的比较牌堆的第一张,小的取出来放到另一堆去,然后再比较两牌堆顶的牌继续不断的把小的取出。当某堆牌取空时,将另一堆剩下的直接放到第三堆的底下,因为它们都排好序了,所以剩下的肯定比第三堆的都大。这个过程最多执行n次,比如当两堆牌刚好牌数一样,并且大小都是交替的。

#include <iostream>
const int len=10;
using namespace std;
void merge(int *a,int p,int q,int r)
{
    int n1=q-p+1,n2=r-q;
    int *L=new int[n1]();
    int *R=new int[n2]();
    for (int i=0;i<n1;i++)
    {
        L[i]=a[p+i];
    }
    for (int j=0;j<n2;j++)
    {
        R[j]=a[q+j+1];
    }
    int i=0,j=0,k=p;
    while (i<n1&&j<n2)
    {
        if (L[i]<=R[j])
        {
            a[k++]=L[i++];
        }
        else
        {
            a[k++]=R[j++];
        }
    }
    while (i<n1)
    {
        a[k++]=L[i++];
    }
    while (j<n2)
    {
        a[k++]=R[j++];
    }

    delete [] R;
    delete [] L;
}
int main()
{
    int a[len]={2,4,5,7,10,1,2,3,6,9};
     merge(a,0,4,9);
    for(int i=0;i<len;i++) cout<<a[i]<<" ";
    cout<<endl;
}

merge代码分析,传入的是指向数组的指针,和(p,q),(q+1,r)间的两个已经排好序的序列如,主程序所举的例子2,4,5,7,10。1,2,3,6,9。在子函数里,先新建两个动态数组来存放这两个序列。然后如果任一序列未到底时,就比较,并赋值给a。一个序列到底了,就将另一个未到底的直接复制到a下。

综上我们完成了合并的过程,分解呢?分解就是要确定q的值,一般我们将q设置为 ,它能够将A[p,r]分成n/2向下和向上取整个元素。(把p,r分别为偶数,奇数情况考虑下就可以证明)。如何一直分解直到只剩一个元素时排序并返回呢?这就要利用递归的方法了,函数不断的调用自身,分解成越来越小的子问题。

#include <iostream>
const int len=5;
using namespace std;
void merge(int *a,int p,int q,int r)

{
    int n1=q-p+1,n2=r-q;
    int *L=new int[n1]();
    int *R=new int[n2]();
    for (int i=0;i<n1;i++)
    {
        L[i]=a[p+i];
    }
    for (int j=0;j<n2;j++)
    {
        R[j]=a[q+j+1];
    }
    int i=0,j=0,k=p;
    while (i<n1&&j<n2)
    {
        if (L[i]<=R[j])
        {
            a[k++]=L[i++];
        }
        else
        {
            a[k++]=R[j++];

        }
    }
    while (i<n1)
    {
        a[k++]=L[i++];
    }
    while (j<n2)
    {
        a[k++]=R[j++];
    }

    delete [] R;
    delete [] L;
}
void mergesort(int *a,int p,int r)
{   

    if (p<r)
    {
        int q=(p+r)/2;
        mergesort(a,p,q);
        mergesort(a,q+1,r);
        merge(a,p,q,r);
    }

}
int main()
{
    int a[len]={25,15,4,30,7};
    mergesort(a,0,4);

    for(int i=0;i<len;i++) cout<<a[i]<<" ";
    cout<<endl;
} //main end

程序分析:

上图分析了程序的递归调用流程,我们就根据上图来分析,首先调用mergesort(a,0,4);,0<4,所以计算q=2.调用mergesort(a,0,2);0<2,q=1,调用mergesort(a,0,1);0<1,q=0,调用mergesort(a,0,0);0==0,递归返回,执行mergesort(a,p,q)的下一条语句mergesort(a,q+1,r)这时q=0,r=1。调用mergesort(a,1,1),1==1,调用返回,执行mergesort(a,q+1,r)的下一条语句merge(a,p,q,r)。此时p=0,q=0,r=1.调用merge(a,0,0,1)。这时返回至mergesort(a,0,2),q=1,调用mergesort(a,q+1,r),即mergesort(a,2,2),返回,调用merge(a,0,1,2)。于是左边这一半排序完成,开始右边的排序,其中q=2,r=4,调用mergesort(a,q+1,r),即mergesort(a,3,4)。同样的在这一半中左边调用mergesort(a,p,q),右边调用mergesort(a,q+1,r),merge,merge后就对数组排好序了。

递归的过程确实容易搞混淆的,在程序里是顺序的,而不是并行的两个排序过程。另外变量间的变化也是容易弄错的。

复杂度分析:

MERGE函数的复杂度为c •n (n为元素个数, c为移动一个元素, 比较一次所花费的工作量)。T(n)= 0, n=1 (只一个元素的数组无需排序),T(n)= 2 T(n/2) + c • n = , n>1。将T(n)展开,就的下图。

3.如何实现快速排序

如果说归并是从底不断排序好,不断合并,到顶时使序列最终成为有序,那么快速,就是从顶一直向下使序列不断有序,当到底时,序列也就成为有序了的。其中有序是确定一个值Aq把序列分为两个部分,一部分的所有值都比Aq大,另一部分都比Aq小。

数组A[p…r]被划分为两个(可能空)子数组A[p…q-1]和A[p+1..r],使得A[p…q-1]中每个元素都小于或等于A[q],A[q+1..r]中的元素大于等于A[q]。下标q在这个划分过程中进行计算;这个数组的划分过程很重要,通过交换来维护两个子区域的性质。

#include <iostream>
const int len=8;
using namespace std;
int partition(int *a,int p,int r)
{
    int x=a[r],i=p-1,temp=0;
    for (int j=p;j<r-1;j++)
    {
        if (a[j]<x)
        {
            i++;
            temp=a[i];
            a[i]=a[j];
            a[j]=temp;
        }
    }
    temp=a[i+1];
    a[i+1]=a[r];
    a[r]=temp;
    return i+1;

}
int main()
{
    int p,a[len]={2,8,7,1,3,5,6,4};
    p=partition(a,0,7);

    for(int i=0;i<len;i++) cout<<a[i]<<" ";
    cout<<endl;
} //main end

如上图和程序,首先初始化x称为主元为最后一个元素a[r],i为序列开始之前的一个值,然后j从p开始的r-1,判断其是否小于等于主元,若满足则i加1,并交换a[i]和a[j]。这是为了满足当元素属于下面各个范围时,符合一定性质 1.p<=k<=i,a[k]<=x; 2. i+1<=k<=j-1,a[k]>x;  3.k=r,a[k]=x;初始时,p和i间,i+1和j-1间没有元素,成立,进入循环,假设第一个元素大于主元,j加1,什么都不做,符合上面的性质。如第一个元素小于主元,i加1,交换a[i]和a[j],先扩大i的范围,再将小于主元的元素交换进来。

整个快速排序的过程,也类似于归并排序,分解,解决,合并。分解就是确定主元在序列中位置,解决即递归用于两个数组,合并这无需,因为递归到最后,数组都排好序了。

#include <iostream>
const int len=8;
using namespace std;
int partition(int *a,int p,int r)
{
    int x=a[r],i=p-1,temp=0;
    for (int j=p;j<=r-1;j++)
    {
        if (a[j]<x)
        {
            i++;
            temp=a[i];
            a[i]=a[j];
            a[j]=temp;
        }
    }
    temp=a[i+1];
    a[i+1]=a[r];
    a[r]=temp;
    return i+1;

}
void quicksort(int *a,int p,int r)
{
    int q;
    if (p<r)
    {
        q=partition(a,p,r);
        quicksort(a,p,q-1);
        quicksort(a,q+1,r);

    }

}
int main()
{
    int a[len]={2,8,7,1,3,5,6,4};
    quicksort(a,0,7);
    for(int i=0;i<len;i++) cout<<a[i]<<" ";
    cout<<endl;
} //main end

算法分析(略)

算法导论专题一--排序算法(2)

时间: 2024-10-11 11:36:39

算法导论专题一--排序算法(2)的相关文章

算法导论专题一--排序算法

排序算法作为许多程序的中间步骤,是计算机科学中的一个基本操作. 一.问题描述 排序算法输入的是n个数的一个序列<a1,a2…..an>,输出为输入的一个排列<a1’,…..an’>,满足a1’<a2’<….<an’ 简言之就是输入一个序列,输出的是这个数组元素从小到大排列的另一序列. 二.方法思想综述 从算法导论这本书上根据算法的复杂度可以将排序算法分为三种,,.,这两种方法都需要数据间的比较,而不需要. 其中有三种为选择,冒泡,插入. 选择排序:最直观,简单但是

算法导论之所有排序算法的Python实现

最近一段时间学习了算法导论第二版书的第一部分和第二部分的内容,自己编写了其中排序相关的几乎全部算法,包括冒泡排序(bubble sort).选择排序( selection sort).插入排序(insertion sort).希尔排序(shell sort).归并排序(merge sort).快速排序(quick sort).计数排序(count sort).基数排序(radix sort).桶排序(bucket sort).期望线性时间的第k个顺序统计量选择.最坏情况线性时间的中位数选择,并给

算法导论三剑客之 分治算法 合并排序

1 #include "iostream" 2 #include "windows.h" 3 #define MAX 0x7fffffff 4 using namespace std; 5 6 void merge(int s,int q,int e,int A[]){ 7 int i,j,k; 8 int B[100],C[100]; 9 for(i=s;i<=q;i++) 10 B[i-s+1]=A[i]; 11 for(j=q+1;j<=e;j++

算法导论——lec 13 贪心算法与图上算法

之前我们介绍了用动态规划的方法来解决一些最优化的问题.但对于有些最优化问题来说,用动态规划就是"高射炮打蚊子",采用一些更加简单有效的方法就可以解决.贪心算法就是其中之一.贪心算法是使所做的选择看起来是当前最佳的,期望通过所做的局部最优选择来产生一个全局最优解. 一. 活动选择问题 [问题]对几个互相竞争的活动进行调度:活动集合S = {a1, a2, ..., an},它们都要求以独占的方式使用某一公共资源(如教室),每个活动ai有一个开始时间si和结束时间fi ,且0 ≤ si &

基本算法研究1-冒泡排序算法测试

基本算法研究1-冒泡排序算法测试 1.经典冒泡排序法基本原理 先看一个动态图,感觉比较形象: 冒泡排序(Bubble Sort)是一种简单的排序算法.默认是从小到大排序,即把最大的数据排在最后,相当于每次把最大数据像气泡一样浮到水面一样.它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来.走访数列的工作是重复地进行直到没有再需要交换. 基本步骤: 1.比较相邻的元素.如果第一个比第二个大,就交换他们两个.        2.对每一对相邻元素作同样的工作,从开始第一对

【JavaScript】【算法】JavaScript版排序算法

JavaScript版排序算法:冒泡排序.快速排序.插入排序.希尔排序(小数据时,希尔排序会比快排快哦) 1 //排序算法 2 window.onload = function(){ 3 var array = [0,1,2,44,4, 4 324,5,65,6,6, 5 34,4,5,6,2, 6 43,5,6,62,43, 7 5,1,4,51,56, 8 76,7,7,2,1, 9 45,4,6,7,8]; 10 //var array = [4,2,5,1,0,3]; 11 array

链表插入和删除,判断链表是否为空,求链表长度算法的,链表排序算法演示——C语言描述

关于数据结构等的学习,以及学习算法的感想感悟,听了郝斌老师的数据结构课程,其中他也提到了学习数据结构的或者算法的一些个人见解,我觉的很好,对我的帮助也是很大,算法本就是令人头疼的问题,因为自己并没有学习过算法的系统性的课程,现在还是处于不断摸索的阶段,好多算法题目根本就没有什么思路,导致自己对好多题目都很是头疼,就算是自己做过的一些算法的题目,再次遇到也还是不一定会做出来,他给出的建议就是,看懂别人的程序,然后自己去敲,一定会出错,然后调试,有错误接着调试,一直到没有错误为止,并且要时常的去复习

冒泡排序算法和简单选择排序算法的js实现

之前已经介绍过冒泡排序算法和简单选择排序算法和原理,现在有Js实现. 冒泡排序算法 let dat=[5, 8, 10, 3, 2, 18, 17, 9]; function bubbleSort(data) { for(let i=0;i<data.length-1;i++){ for(let j=0;j<data.length-1-i;j++){ if(data[j]>data[j+1]){ [data[j],data[j+1]]=[data[j+1],data[j]]; } } }

python数据结构与算法第八天【排序算法】

1.排序算法的稳定性 稳定排序算法会让原本有相同键值的记录维持相对次序 例如:对以下元组按照元组的第一个元素升序排列,元组如下: (4,1) (3,1) (3,7) (5,6) 若要满足条件,则可能的排序有: 情况一: (3,1) (3,7) (4,1) (5,6) 情况二: (3,7) (3,1) (4,1) (5,6) 虽然情况一和情况二都是满足条件的,但是情况二在满足条件下打破了原本无需改变的顺序 原文地址:https://www.cnblogs.com/liuzhiqaingxyz/p/