sort简单分析

排序算法分析(都以从小到大为例):
1、Insertsort:(原地排序)
最坏情况:从大到小排列(逆序),6,5,4,3,2,1, 比较(移动次数要乘以3)次数=n*(n - 1) / 2;
最好情况:从小到大排列(正序),1,2,3,4,5,6,比较次数 = n;
平均情况:与最坏情况一样,为Θ(n^2).
算法稳定。

void insertsort(unsigned long *a,int num,unsigned long *res)
{
    unsigned long temp[num];
    temp[0] = a[0];
    //每次取一个数 插入temp数组中 
    for(int i = 1; i < num; i++)
        for(int j = i - 1; j >= 0; j--)
        {
            if(a[i] < temp[j])
            {
                temp[j + 1] = temp[j];
                if(j == 0)
                    temp[j] = a[i];
            }
            else
            {
                temp[j + 1] = a[i];
                break;
            }
        }
    for(int i = 0; i < num; i++)
        res[i] = temp[i];
}

//原地排序版本
void insertsort1(unsigned long *a,int num)
{
    unsigned long key;
    //每次取一个数 插入temp数组中 
    for(int i = 1; i < num; i++)
    {
        key = a[i];
        for(int j = i - 1; j >= 0; j--)
        {
            if(key < a[j])
            {
                a[j + 1] = a[j];
                if(j == 0)
                    a[j] = key;
            }
            else
            {
                a[j + 1] = key;
                break;
            }
        }
    }
}

2、Shellsort:(分组的插入排序)
开始无序的时候每组元素较少,最后差不多有序的时候元素才多起来,所以效果比插入排序要好。
算法不稳定 14336,步长开始取2,则33会换。

void shellsort(unsigned long *a,int num)
{
    int step = num / 3;
    unsigned long temp;
    //每次插入排序的间隔步长 
    for(int i = step; i >= 1; i /= 2)
    //插入排序
        for(int j = 0; j  < i; j++) 
        //将每隔i的数分为一组,并进行每组的插入排序 
            for(int k = j + i; k <= num - 1; k += i)
            {
                temp = a[k];
                for(int m = k - i; m >= j; m -= i)
                {
                    if(temp < a[m])
                    {
                        a[m + i] = a[m];
                        if(m == j)
                            a[m] = temp;
                    }
                    else
                    {
                        a[m + i] = temp;
                        break;
                    }
                }
            }
}

3、Bubblesort:
每次将最小的元素移到(冒泡到)最前面(i)的位置。
最坏情况:逆序,6,5,4,3,2,1,比较次数:n*(n - 1) / 2; 移动次数:3 * n*(n - 1) / 2
最好情况:正序,123456,比较次数n
平均情况:
算法稳定

void bubblesort(unsigned long *a,int num)
{
    for(int i = 0; i < num; i++)
        for(int j = num - 1; j >= i + 1; j--)
        {
            unsigned long temp;
            if(a[j] < a[j - 1])
            {
                temp = a[j - 1];
                a[j - 1] = a[j];
                a[j] = temp;
            }
        }
}

4、selectsort:
和冒泡排序不同的是每次都只找到后面s[i+1,end]中最小的,仅进行下标的记录,而不进行相邻位置的交换。
最好最坏情况和冒泡一样,不过移动次数减少,比较次数一样。

void selectsort(unsigned long *a,int num)
{
    int min_flag;
    for(int i = 0; i < num; i++)
    {
        unsigned long min = a[i];
        min_flag = i;
        for(int j = i; j < num; j++)
        {
            if(a[j] < min)
            {
                min = a[j];
                min_flag = j;
            }
        }
        unsigned long temp = a[min_flag];
        a[min_flag] = a[i];
        a[i] = temp;
    }
}

5、Qsort(挖坑、填数)
最坏情况:每次partition都取的是当前组最大或最小,62345,每次分治的时候都是一个数组中有一个,另外一个有m-1个,故需进行n次划分,而每次进行的比较次数为m-1,故复杂度为n^2
最好情况:每次flag取中间大小的数,分治2边数组大小差不多,则会划分logn次,而进行比较次数就是每组的元素数,故为nlogn。
平均情况:假设规模为N的问题分为一个规模为9/10N的问题和规模为1/10N的问题,即T(n) = T(9n/10) + T(n/10) + Θ(n),用递归树分析可得T(n) = O(nlogn),而且比分区9:1要更平均(也就是情况更好)的概率为80%,所以在绝大部分情况下快速排序算法的运行时间为O(nlogn)。

//i从左往右,j从右往左,flag为第一个元素,将左边(i所指)比flag大的放到右边,把右边(j所指)的放到左边,i和j交替进行,挖坑、填数 
int partition(unsigned long *a,int start,int end)
{
    unsigned long flag = a[start];
    int i = start;
    int j = end;
    //每次先跳过不需要动的,然后对需要动的进行移动 
    while(i < j)
    {
        //跳过右边比flag大的 ,这里有(i< j)是为了防止数组a后面值没有初始化会是乱值!
        while((a[j] >= flag) && (i < j)) j--;
        if(i < j)
            a[i++] = a[j];
        //跳过左边比flag小的 
        while((a[i] <= flag) && (i < j))i++;
            if(i < j)
                a[j--] = a[i];          
    }
    a[i] = flag;
    return i;
}

//不断进行左右分治的partition,直到每个左右数组"仅有一个元素 "
void qsort(unsigned long *a,int start,int end)
{
    //start < end,保证仅有一个元素的时候停止 
    if(start < end)
    {
        int mid = partition(a,start,end);
        qsort(a,start,mid - 1);
        qsort(a,mid + 1,end);
    }
}

6、mergesort
算法里面有一种分治策略:Divide(分解子问题的步骤) 、 Conquer(递归解决子问题的步骤)、 Combine(子问题解求出来后合并成原问题解的步骤)
使得复杂度为nlogn,就是将问题分成2个同样的子问题,然后再找一个O(n)复杂度的方法将2个子问题的结果合并成原问题的结果,复杂度计算式为:

T(n)=2T(n/2)+O(n);

这样的T(n) = nlogn,mergesort就是这种思路。
Merge的最坏情况,需要进行n次比较与n次移动。
最好情况,进行n次移动+比较。
最好情况:已排好序,不用移动,仅比较 nlogn
最坏情况:一直都是merge的最坏情况,分组每次都是平均分的,故分logn次,所以也是nlogn

//将2个已排好序的数组合并成一个有序数组
//[start:mid]是array1,[mid+1:end]是array2 
void merge(unsigned long *a,int start,int mid,int end,unsigned long *res)
{
    int frsize = mid - start;
    unsigned long temp[1000];
    int j = mid + 1;
    int i = start;
    int k = 0;
    while((i <= mid) && (j <= end))
    {
        if(a[i]  > a[j])
            temp[k++] = a[j++];
        else
            temp[k++] = a[i++];
    }
    if(i == mid + 1)//first array finishes first
        for(int m = j; m <= end; m++)
            temp[k++] = a[m];
    else//second finishes first
        for(int m = i; m <= mid; m++)
            temp[k++] = a[m];
    for(int i = 0; i <=  end; i++)
        res[i] = temp[i];
}

void mergesort(unsigned long *a ,int start,int end)
{
    if(start < end)
    {
        int i = (start + end) / 2;
        mergesort(a,start,i);
        mergesort(a,i + 1,end);
        merge(a,start,i,end,a);
    }
}

归并排序与快速排序比较:归并的merge要开额外的空间,而快排的partition可以原地进行!

7.堆排序
堆排序主要需要建立堆,通过heapify(int *a,int start,int num)操作将堆建立,然后每次取出根节点并将其移除后再将余下num-1个数用heapify操作建立堆。heapify操作一次复杂度为logn,一共n次heapify操作,复杂度O(nlogn)。其为不稳定排序算法。是就地排序,只需要O(1)的空间。

#include "heap.h"

using namespace std;

//a数组有效元素为a[1]-a[num].
void heap::min_heapify(int *a, int p,int num)
{
    int lchild = 2 * p, rchild = 2 * p + 1;
    int temp;
    if (lchild > num)//已经到叶子节点
        return;
    else if (rchild > num)
    {
        if (a[lchild] < a[p])
        {
            temp = a[p];
            a[p] = a[lchild];
            a[lchild] = temp;
        }
        return;
    }

    min_heapify(a, 2 * p, num);
    min_heapify(a, 2 * p + 1, num);

    int min;
    if (a[lchild] < a[rchild])
        min = lchild;
    else
        min = rchild;
    if (a[min] < a[p])
    {
        temp = a[p];
        a[p] = a[min];
        a[min] = temp;
    }
}


void heap::min_heapsort(int *a, int num)
{
    for (int i = num; i >= 1; i--)
    {
        min_heapify(a, 1, i);
        int temp = a[i];
        a[i] = a[1];
        a[1] = temp;    
    }
}

8、桶排序
此排序是为了优化计数排序的空间太大问题,即要排序的数很多(如500万个数),但是数的范围不大(如0-900),通过将数组的数据全部归一化到[0,1)之间,然后建立m个区间分别为[0,1/m)、[1/m,2/m)、……[m-1/m,1)的bucket,每个桶都是一个链表,将归一化的数据放入到对应的桶中,然后对每个桶进行排序(用qsort、insertsort.etc都行),然后按顺序输出即可。
这里写图片描述

void bucketsort(unsigned long *a,int num,int m)
{
    double data[1000];
    unsigned long max = 0;
    vector<double> bucket[20];
    float seg = 1.0 / m;
    for(int i = 0; i < num; i++)
    {
        if(max < a[i])
            max = a[i];
    }
    max = max + 100;
    for(int i = 0; i < num; i++)
    {
        data[i] = (double)a[i] / max;
        for(int j = 0; j < m; j++)//放桶里 
        {
            if(data[i] >= seg * j && data[i] <= seg * (j + 1))
            {
                bucket[j].push_back(data[i]);
                break;
            }
        }
    }
    for(int i = 0; i < m; i++)
        sort(bucket[i].begin(),bucket[i].end());
    int base = 0;

    for(int i = 0; i < m; i++)
    {
        for(int j = 0; j < bucket[i].size(); j++)
            a[base++] = (bucket[i])[j] * max;
    }
}

复杂度分析:
在做出数据落入每个bucket都是等可能的情况下,我们可以看到T() = O(n) + 每个桶排序时间总和,两边同时取期望,然后可以证明,排序时间的期望(用insertsort)为:n * (2 - 1/n).最终桶排序复杂度为O(n + c),其中C=N*(logN-logm),空间复杂度为O(N+M)。

eg:
在一个文件中有10G个整数,乱序排列,要求找出中位数。内存限制为2G。只写出思路即可(内存限制为2G意思是可以使用2G空间来运行程序,而不考虑本机上其他软件内存占用情况。) 

分析:既然要找中位数,很简单就是排序的想法。那么基于字节的桶排序是一个可行的方法。

思想:将整型的每1byte作为一个关键字,也就是说一个整形可以拆成4个keys,而且最高位的keys越大,整数越大。如果高位keys相同,则比较次高位的keys。整个比较过程类似于字符串的字典序。

第一步: 把10G整数每2G读入一次内存,然后一次遍历这536,870,912即(1024*1024*1024)*2 /4个数据。每个数据用位运算">>"取出最高8位(31-24)。这8bits(0-255)最多表示256个桶,那么可以根据8bit的值来确定丢入第几个桶。最后把每个桶写入一个磁盘文件中,同时在内存中统计每个桶内数据的数量NUM[256]。

代价:(1) 10G数据依次读入内存的IO代价(这个是无法避免的,CPU不能直接在磁盘上运算)。(2)在内存中遍历536,870,912个数据,这是一个O(n)的线性时间复杂度。(3)把256个桶写回到256个磁盘文件空间中,这个代价是额外的,也就是多付出一倍的10G数据转移的时间。

第二步:根据内存中256个桶内的数量NUM[256],计算中位数在第几个桶中。很显然,2,684,354,560个数中位数是第1,342,177,280个。假设前127个桶的数据量相加,发现少于1,342,177,280,把第128个桶数据量加上,大于1,342,177,280。说明,中位数必在磁盘的第128个桶中。而且在这个桶的第1,342,177,280-N(0-127)个数位上。N(0-127)表示前127个桶的数据量之和。然后把第128个文件中的整数读入内存。(若数据大致是均匀分布的,每个文件的大小估计在10G/256=40M左右,当然也不一定,但是超过2G的可能性很小)。注意,变态的情况下,这个需要读入的第128号文件仍然大于2G,那么整个读入仍然可以按照第一步分批来进行读取。

代价:(1)循环计算255个桶中的数据量累加,需要O(M)的代价,其中m<255。(2)读入一个大概80M左右文件大小的IO代价。

第三步:继续以内存中的某个桶内整数的次高8bit(他们的最高8bit是一样的)进行桶排序(23-16)。过程和第一步相同,也是256个桶。

第四步:一直下去,直到最低字节(7-0bit)的桶排序结束。我相信这个时候完全可以在内存中使用一次快排就可以了。

整个过程的时间复杂度在O(n)的线性级别上(没有任何循环嵌套)。但主要时间消耗在第一步的第二次内存-磁盘数据交换上,即10G数据分255个文件写回磁盘上。一般而言,如果第二步过后,内存可以容纳下存在中位数的某一个文件的话,直接快排就可以了(修改者注:我想,继续桶排序但不写回磁盘,效率会更高?).

9、基数排序

void radixsort(unsigned long *a,int num)
{
    vector<int> radix[10];//装每次排序后的序列 
    int allzero = 0;
    unsigned long temp[1000];

    int step = 1;

    while(!allzero)
    {
        allzero = 1;
        for(int i = 0; i < num; i++)
        {
            int digit = a[i] % (int)pow(10,step);
            digit /= (int)pow(10,step - 1);
            radix[digit].push_back(i);
            if(digit > 0)
                allzero = 0;
        }
        int k = 0;
        for(int i = 0; i < 10; i++)//按照序列顺序输出即可 
        {
            for(int j = 0; j < radix[i].size(); j++)
                temp[k++] = a[(radix[i])[j]];
            radix[i].clear();
        }
        for(int i = 0; i < num; i++)
            a[i] = temp[i];
        step++;
    }
}

这里写图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值