基本算法复习之排序:性能比较、代码分析


先定义好swap函数:

void swap(int *a, int *b)
{
    int temp;
    temp= *a;
    *a = *b;
    *b = temp;
}


(1) 冒泡排序


时间: O(n2)
空间:O(1)
稳定性:稳定

void bubbleStore(int a[], int length)
{
    if(a == NULL)
        return;
    for(int i = 0; i < length-1; i++)
    {

        for(int j = i+1; j < length; j++)
        {
            if(a[j] < a[i])
                swap(&a[i], &a[j]);
        }
    }
}


(2) 插入排序


时间: O(n2)
空间:O(1)
稳定性:稳定

void insertStore(int a[],int length)
{
    if(a == NULL)
        return;
    for(int i = 1; i < length; i++)     
    {
        int toInsert = a[i];    //即将插入到有序序列中的数
        for(int j = i-1; j >= 0 && toInsert < a[j]; j--)    //有序序列
        {
            //一步一步地把toInsert往前移动,直到移到合适的位置,这与交换两个数值是不一样的!!
            a[j+1] = a[j];
            a[j] = toInsert;
        }
    }

}

插入排序与冒泡排序的区别:冒泡排序是从后n(n的初始值即为数组长度)个数中找到最小的放到前面,然后将n所界定的范围缩小。插入排序是保证前n个数有序(方法是将第n个数插入到前n个已经排好序的数中,故曰插入排序),n从0开始,n逐渐增大即可将整个数组排序。


(3) 快速排序

原理:找一个分割点,比他小的数移到一边,比他大的数移到另一边 性能:

时间:O(n·log n),总体最快
空间:O(log n),但由于是递归的,对于内存非常有限的机器不是一个好的选择
稳定性:不稳定

void quickSort(int a[], int length)
{
    if(a == NULL || length < 2)
        return;

    int partPoint = length/2 - 1;       //空间:O(log n),也可使用随机数
    swap(&a[partPoint], &a[length-1]);  //分割点换到最后
    int smallerCount = 0;           //每把比分割点小的数换到前面,其值+1
    for(int i = 0; i < length -1; i++)  //注意这里i不能从1开始
    {
        if(a[i] < a[length-1])  //如果a[i]比分割点(a[length-1])小,则把a[i]移到前面(比分割点小的数集中在a[]的前面一部分,把a[i]加到这部分的末位)
        {
            swap(&a[smallerCount], &a[i]);
            smallerCount++;
        }
    }
    swap(&a[smallerCount], &a[length-1]);   //把分割点换回到两部分的交界处
    quickStore(a, smallerCount);        //排分割点的前半部分
    int offset = smallerCount + 1;      //前面这么多个数不用排了,故偏移量是这么多
    quickStore(a+offset, length-offset);    //排分割点的后半部分
}


(4) 希尔排序


基本思想:插入排序的升级版(根据其特点:序列大部分已排好序时效率很高),将数据按逐渐减小的步长gap分组(每组元素下标满足b+a·gap,每组内b>=0为常数,a从0开始自然增大,如第二组的b=1,则第一组下标为(1,1+gap,1+2gap,1+3gap,……)),分别对每一组进行排序,然后对所有元素进行一次排序(即最后步长必须为1),步长的选择是递减的,比如5、3、1,现在一般使用D.E.Knuth分组方法(n很大是,用h(n+1)=3h(n)+1来分组,即1、4、13……)。

性能:
时间:O(n·log n)
空间:O(1)
稳定性:不稳定

Shell排序比冒泡排序快5倍,比插入排序大致快2倍。Shell排序比起QuickSort,MergeSort,HeapSort慢很多。但是它相对比较简单,它适合于数据量在5000以下并且速度并不是特别重要的场合。它对于数据量较小的数列重复排序是非常好的。

void shellSort(int arr[], int len)
{
    int delta,i1,i2,tmp;
    for(delta = len/2; delta > 0; delta /= 2)   //这里采用依次减半的步长
    {
        for(i1 = delta; i1 < len; i++)
        {
            tmp = arr[i1];
            for(i2 = i1; i2 >= delta; i2 -= delta)
            {
                if(arr[i2-delta] > tmp)
                    arr[i2] = arr[i2-delta];
                else
                    break;
            }
            arr[i2] = tmp;
        }
    }
}

//更好理解的版本
void shellSort2(int a[], int n)
{
    int i, j, gap;

    for (gap = n / 2; gap > 0; gap /= 2)
        for (i = gap; i < n; i++)
            for (j = i - gap; j >= 0 && a[j] > a[j + gap]; j -= gap)
                swap(&a[j], &a[j + gap]);
}


(5) 归并排序


原理:将两个有序数组合并。这个函数在归并排序中最先执行的时候两个数组长度不超过1,所以能按从小到大的顺序排列,
然后数组长度逐渐增大,这时候参与合并的数组都已经是排好序的了

性能:
时间:O(n·log n),比堆排序稍快(可计算其算法循环执行的次数,或简单理解为归并排序只有一层循环,堆排序两层循环)
空间:O(n),也是递归的,数据量非常大时可能堆溢出
稳定性:稳定
int* merge(int a1[], int a2[], int len1, int len2)
{
    if(a1 == NULL)
        return a2;
    if(a2 == NULL)
        return a1;

    int *a=(int *)malloc(sizeof(int)*(len1 + len2));    //空间:O(n),分配在堆存储区中
    assert(a != NULL);

    int n1 = 0, n2 = 0;
    while(n1 < len1 && n2 < len2)
    {
        if(a1[n1] < a2[n2])     //这里决定了排序是从小到大还是从大到小
        {
            a[n1+n2] = a1 [n1];
            n1++;
        }
        else
        {
            a[n1+n2] = a2[n2];
            n2++;
        }
    }
    while(n1 < len1)
    {
        a[n1+n2] = a1[n1];
        n1++;
    }
    while(n2 < len2)
    {
        a[n1+n2] = a2[n2];
        n2++;
    }

    return a;
}

void mergeStore(int a[], int startIndex, int endIndex)
{
    if(startIndex >= endIndex)
        return;

    int middleIndex = (startIndex + endIndex) / 2;  //一般选取中点作为分割点,也可以选其他的位置作为分割点
    mergeStore(a, startIndex, middleIndex);     //递归——第一部分排序
    mergeStore(a, middleIndex+1, endIndex);     //递归——第二部分排序
    //两部分合起来。最先把下面第一个参数忘了加startIndex,坑死爹了
    int* temp = merge(a+startIndex, a + middleIndex + 1, middleIndex - startIndex + 1, endIndex - middleIndex);

    //排好序的部分放回到原来的数组
    for(int i = startIndex; i <= endIndex; i++)
        a[i] = temp[i-startIndex];

    free(temp);
}


(6) 堆排序


原理:先大堆化数组,此时最大元素就是A[0],将数组首尾元素互换,再把除末尾元素之外的堆化,依次循环,完成排序

性能:
时间:O(n·log n),适合于数据量非常大的场合(百万数据)
空间:O(1)
稳定性:不稳定
#define LeftChild(i) (2*(i)+1)  //把数组看成满二叉树按层遍历的结果,这是A[i]节点的左孩子在数组中的索引,并不是节点值

//堆化数组:把以A[i]为根节点的树调整成最大堆
void makeMaxHeap(int A[], int i, int N)
{
    int child,tmp;  //temp用于存储子堆中的最大节点
    for(tmp = A[i]; LeftChild(i) < N; i = child)
    {
        child = LeftChild(i);
        if(child != N-1 && A[child+1] > A[child])   //将child变成较大子节点的索引
            child++;
        if(A[child] > tmp)  //较大子节点比父节点也大,说明该子节点就是父、左、右三节点中最大的,则需要把该子节点“上浮”(赋值给父节点)
            A[i] = A[child];
        else
            break;
    }
    A[i]=tmp;   //被上浮的位置用父节点来填补
}

//版本2,更好理解一点
void makeMaxHeap2(int A[], int i, int N)
{
    int child;
    for(; LeftChild(i) < N; i = child)
    {
        child = LeftChild(i);
        if(A[child+1] > A[child] && child != N-1)
            child++;
        if(A[child] > A[i])
            swap(&A[child], &A[i]);
        else 
            break;
    }
}

void HeapSort(int A[], int N)
{
    int i;

    //把A[]调整成大堆,构造这个堆要把下层的大的子节点上浮成父节点,所以循环的顺序只能从大到小
    for(i = N/2; i >= 0; i--)
        makeMaxHeap(A, i, N);
    //上面构造的大堆根节点A[0]就是数组中的最大值,下面每次循环都把最大值放到后面,然后把前面剩余的数中的最大值放到A[0]的位置,从而实现从小到大排序
    for(i = N-1; i > 0; i--)
    {
        Swap(&A[0], &A[i]);
        makeMaxHeap(A, 0, i);
    }
}


规律:时间复杂度为为 O(n2) 的算法都稳定。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值