插入排序、希尔排序、堆排序、归并排序、快速排序

以下排序都从小到大排序,假设数列均为整数数列。

先从插入排序说起吧,插入排序是最基础的排序了,估计大学都讲烂了。

插入排序基本思想是将一个元素插入到已经排序的数列中,从而得到一个新的个数加1的序列。所以对一队无序序列进行插入排序,可以从第二个元素开始,依次插入前面的序列中,第一个元素可以看作是一个个数为1的有序序列。源码如下:

void insertionsort(int array[], int num)
{
    int j, p;
    int tmp;

    for(p = 1; p < num; p++)
    {
        tmp = array[p];
        for(j = p; j > 0 && array[j - 1] > tmp; j--)
        {
            array[j] = array[j - 1];
        }
        array[j] = tmp;
    }
}

时间复杂度为O(N^2);

接下来再说说希尔排序,希尔排序算是插入排序的一个变种吧,他采用了一组增量因子,增量因子依次递减,直至为1。对于每个增量因子,都进行一系列排序,基本步骤是将相差增量因子N倍的元素列为1组,对该组进行插入排序,直到增量因子变为1,整个序列排序完成。源码如下:

/*希尔排序,增量因子取值为num/2*/
void shellsort(int array[], int num)
{
    int i, j, tmp, increment, m;

    increment = num / 2;
    for(; increment > 0; increment /= 2)
    {
        for(i = increment; i < num; i++)
        {
           tmp = array[i];
           for(j = i; j >= increment; j -= increment)
           {
                /*
                                每次只比较间隔为increment的两个元素。
                                如果不成立,就退出。如果成立,就继续比较
                                前边间隔为increment的元素(貌似没必要,因为
                                前面经过类似的步骤,前边的元素已经肯定是
                                排过序的了,肯定会经过else退出,所以j最多减
                                一次increment。)。
                        */
                if(tmp < array[j - increment])
                    array[j] = array[j - increment];
                else
                    break;
           }
           array[j] = tmp; /*此时,j为减去increment的值()。*/
        }
    }
}

增量因子一般取值为数组个数的一般,还有一些其他的增量因子会使该算法的运行速度更快,但是也更为复杂,希尔排序的效率完全与增量因子的选取有关。由于希尔排序本质上是分组插入排序,保证了在相对较小的序列内进行插入排序,在相对较大的序列内对有序序列进行插入排序,所以他的效率比直接使用插入排序更优,在中等数据时会更明显。它的最坏时间复杂度为O(N^2),使用 Hibbard增量的希尔排序的时间复杂度为O(

 )。

然后再说说堆排序,堆排序是利用堆这种数据结构设计出来的一种算法。

堆排序使用完全二叉树作为基础模型,用数据将二叉树的数据存储。每个父节点都不大于其子节点的值,将二叉树的节点按照从上到下、从左到右的顺序依次放入数组中,根节点放在数组的开头即0的位置上,位置为i的节点其左子节点在数组中的位置为2*i+1,右子节点在数组中的位置为左子节点位置+1。

堆排序的基本思想是先建堆、然后通过不断地调整堆,完成对整个堆的排序。

建堆的过程通过不断地将父节点与两个子节点比较,将较大的值放入父节点,然后依次向下更新整个子树,从而完成整个树的更新。堆建立完成后,根节点应该是值最大的节点,其子树中也保持了父节点不小于两个子节点的特性。

对序列进行排序的过程就是在不断地调整堆。前面我们生成的堆中,对应到数组a里,a[0]存储的是根节点,即最大的值。我们将a[0]与数组最后一个元素互换,从而让最大值放置到数组最末尾,从二叉树来看,相当于将最底层的最右侧的节点放置到根节点上,根节点放置到最底层的最右侧的位置上,此时我们认为此节点已经被删除,后续二叉树调整中,不再处理此节点,所以二叉树的节点数减一。然后我们从根节点开始,再次更新二叉树,让较大值上移值根节点,从而再次建立一个满足原始特性的二叉树。然后继续将数组倒数第二个元素与a[0]互换,让第二大的元素,放置在数组倒数第二的位置上,重复以上的更新树活动。。不断地执行该过程,直到最后只剩根节点位置,此时,序列排序完成。源码如下:

/*
    由于本次实例中堆对应的数组从0开始,所以
    左子结点位置为2*i+1
*/
#define LEFTCHILD(i) (2 * (i) + 1)      

/*堆排序*/
void percdown(int array[], int i, int num)
{
    int child;
    int tmp;
    printf("i %d array: ", i);
    /*每次循环都把更新child代表的位置,从而更新整个子树*/
    for(tmp = array[i]; (child = LEFTCHILD(i)) < num; i = child)
    {   
        /*查找最大子节点并且保证未越界*/
        if(child != num - 1 && array[child + 1] > array[child])
        {
            child++;    /*右子节点比较大*/
        }

        if(tmp < array[child]) /*当前节点小于子节点*/
        {
            array[i] = array[child];
        }
        else
            break;  /*子树满足要求,退出*/
    }
    array[i] = tmp;

    #if 1
    for(i = 0; i < 17; i++)
    {
        printf(" %d", array[i]);
    }
    printf("\n");
    #endif
}

void heapsort(int array[], int num)
{
    int i;

    for(i = num / 2; i >= 0; i--)
        percdown(array, i, num);  /*创建堆*/

    for(i = num - 1; i > 0; i--)
    {
        swap(&array[0], &array[i]); /*将首尾互换,保证最大值在队列最后*/
        percdown(array, 0, i);
    }
    
}
堆排序的平均时间复杂度为O(N*logN)。


接着,我们来看下归并排序。

归并排序的基本思想是将两个已经排好序的序列,合并排序到一个序列中。对于一个无序序列,最常见的是分治归并,通过采取二分法加递归来完成不同级别的子序列的排序及合并操作。

对于已经排序过的两个数组a、b,我们分别对a[i]和b[j]进行比较,若a[i]<=b[j],则将a[i]复制到数组c[k]中,然后将i加1,k加1,否则将b[j]复制到c[k]中,然后j、k分别加1。然后继续比较两者,如此循环,知道有一个列表元素复制完,则将另一个列表的剩余元素全部复制到c数组后面,从而两个序列合并排序到第三个数组序列中。源码如下:

void merge(int array[], int temp_array[], 
    int leftpos, int rightpos, int rightend)
{
    int i, leftend, num, tmppos;

    leftend = rightpos - 1;
    tmppos = leftpos;
    num = rightend - leftpos + 1;

    while(leftpos <= leftend && rightpos <= rightend)
    {
        if(array[leftpos] <= array[rightpos])
            temp_array[tmppos++] = array[leftpos++];
        else 
            temp_array[tmppos++] = array[rightpos++];
    }

    while(leftpos <= leftend) /*左侧剩余*/
        temp_array[tmppos++] = array[leftpos++];

    while(rightpos <= rightend)
        temp_array[tmppos++] = array[rightpos++];

    /* Copy TmpArray back */
    for( i = 0; i < num; i++, rightend-- )
        array[ rightend ] = temp_array[ rightend ];
}

void msort(int array[], int tmparray[], int left, int right)
{
    int center;

    if(left < right)
    {
        center = (right + left) / 2;
        msort(array, tmparray, left, center);
        msort(array, tmparray, center + 1, right);
        merge(array, tmparray, left, center + 1, right);
    }
}

void mergesort(int array[], int num)
{
    int *tmp = NULL;

    tmp = (int *)malloc(sizeof(int) * num);
    if(tmp == NULL)
    {
        printf("malloc failed\n");
    }

    msort(array, tmp, 0, num - 1);
    free(tmp);
}

由于涉及到递归调用,我们在合并之前,动态申请了一块内存,从而避免在每次递归调用的时候都从栈中开辟临时存储空间,提升效率。
时间复杂度为O(N*logN)。


最后来看下快速排序,快速排序可以算是冒泡排序的一种改进。

快速排序的基本思想是在序列中选取一个元素作为参照KEY值,然后分别从序列的两头向该元素方向检索。在左侧遇到大于KEY值的元素,停止左侧检索,在右侧遇到小于KEY值的元素,停止右侧检索。当两侧的检索都停止时,如果两个检索指针还未到达KEY值处,则互换当前两个检索指针指向的两个元素,如果已经到达或是已经越过,则该轮快速排序完成。此时KEY左侧的元素均小于KEY值,右侧的元素均大于KEY值。

通过递归调用二分法,将整个序列分割成最小的序列,然后使用插入排序或是其他方法进行排序,保证最小序列有序后,不断地与上级回调合并,从而达到有整个序列排序的效果。源码如下:

#define CUTOFF 3

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

int mid3(int array[], int left, int right)
{
    int center = (left + right) / 2;

    if(array[left] > array[center])
        swap(&array[left], &array[center]);

    if(array[left] > array[right])
        swap(&array[left], &array[right]);

    if(array[center] > array[right])
        swap(&array[center], &array[right]);

    swap(&array[center], &array[right - 1]);

    return array[right - 1];
}

void Q_sort(int array[], int left, int right)
{
    int i, j, pivot;

    if(left + CUTOFF <= right)
    {
        pivot = mid3(array, left, right);
        i = left;
        j = right - 1; /*取完中值后,right处是肯定大于KEY的值,所以可以从right-1处开始检索*/

        for( ; ; )
        {
            while(array[++i] < pivot) {}
            while(array[--j] > pivot) {}
            if(i < j)
                swap(&array[i], &array[j]);
            else 
                break;
        }
        if(i != right -1) /*因为我们通过异或来交互,所以要确保两者不是同一个数*/
            swap(&array[i], &array[right - 1]);
        
        Q_sort(array, left, i - 1);
        Q_sort(array, i + 1, right);
    }
    else
        insertionsort(array + left, right - left + 1);
}

void quicksort(int array[], int num)
{
    Q_sort(array, 0, num - 1);
}

对于快速排序,合理的选择KEY值是个关键。一般来说有以下三种方法:

1、选取第一个元素。该方法对于随机序列来说是没有问题的,当时如果是对于一个反序的序列,会产生非常劣质的分割,从而导致在所有的递归调用中,均使用了最坏的时间量。该做法不应该随意使用。

2、随机选取元素。一般来说该种策略是安全的,但是对于随机数的产生也是非常昂贵的,在递归调用中,每次都必须先产生随机数也会是个不小的开销。

3、中值分割法。一般来说,我们选取第N/2大的元素作为KEY值,但是该值是比较难算出的。通常,我们选取队列的三个元素,使用这三个元素的中值来作为KEY值。我们可以随机选取三个元素,但是因为使用到随机,所以该方法也是不会有多大帮助。所以我们选取序列的开头、结尾和中间这三个元素进行比较。将三者中最小值放置到序列开头,次小值放置到中间,最大值放置到结尾。

快速排序的平均运行时间为O(N*logN),最坏为O(N^2)。

可以看出在快速排序法中,每完成一次快速排序,key值所正在位置i,正好该序列中第i大的元素。因为每完成一次,在i左侧的都比i小或等于,在i右侧的都比i大或等于。

由此,我们可以想出一个快速选择序列第i小元素的方法。源码如下:

void quickselect(int array[], int k, int left, int right)
{
    int i, j, pivot;

    if(left + CUTOFF <= right)
    {
        pivot = mid3(array, left, right);
        i = left;
        j = right - 2;

        for( ; ; )
        {
            while(array[++i] < pivot) {}
            while(array[--j] > pivot) {}
            if(i < j)
                swap(&array[i], &array[j]);
            else 
                break;
        }
        if(i != right -1)
            swap(&array[i], &array[right - 1]);

        if(k < i + 1)   /*比当前i位置小,还需对左侧进行排序*/
            quickselect(array, k, left, i - 1);
        else if(k > i + 1)  /*比当前i位置大,还需对右侧进行排序*/
            quickselect(array, k, i + 1, right);
    }
    else
        insertionsort(array + left, right - left + 1);
}
运行停止后,第K位置上就是该序列第K小的元素。





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值