八大排序算法总结

算法一:冒泡排序

每次比较相邻两元素,若错序则交换,多次遍历,直至无交换,实质上需要排序k-1趟,每次找到未排序元素中的最大值放在已排序序列最后面,稳定

void bubble_sort(int array[], int n)
{
    int i, j;
    for (i = 0; i < n - 1; i++)  //排序n-1趟,每次寻找未排序序列中的最大值
    {
        for (j = 0; j< n-i-1; j++) //每次循环从头相邻两两比较,将大值升上去
            if (array[j] >array[j + 1]) //每趟可能需要交换多次
            {
                int temp = array[j];
                array[j] = array[j+1];
                array[j+1] = temp;
            }
    }
}

改进:增加状态变量flag,若某趟循环无交换发生,则停止继续循环

时间复杂度分析:最好情况,全部有序,在已改进的基础上,比较n-1次,故时间复杂度为O(n),最坏情况,逆序排列,则需要比较n(n-1)/2次,交换n(n-1)/2次,时间复杂度为O(n^2)。

缺点:一次循环内多次交换

 

算法二:简单选择排序

进行n-1趟排序,每次寻找未排序所有元素的最小值放在已排序序列的最末端,稳定

void selection_sort(int array[],int n)
{
    int i, j, min, temp;
    for (i = 0; i < n - 1; i++)  //排序n-1趟,每次寻找i位置上的值
    {
        min= i;
        for (j = i + 1; j< n; j++)  //i后数据与min依次比,找应放在i位置上的索引给min
            if (array[j] <array[min])
                min= j;
        if (min != i)    //每个位置只交换一次
        {
            temp = array[i];
            array[i] = array[min];
            array[min] = temp;
        }
    }
}

时间复杂度分析:无论最好或最坏情况,均需要比较n(n-1)/2次,最好情况无交换,故时间复杂度为O(n),最坏情况交换n-1次,时间复杂度为O(n^2),但性能上还是略优于冒泡排序。
 

算法三:插入排序

将数据依次插入,对于每次新插入的数据,从末尾开始在已排好序的数列中找到其正确的位置,将其后数据从最后一个依次后移,最后插入。若待插入元素与有序序列中某元素相等,则插入在相等元素的后面,故稳定。

void insert_sort(int array[], int n)
{
    int i, j, temp;
    for (i = 1; i < n; i++)   //i为当前要插入的数据索引,开始时均插在最后
    {
        temp= array[i]; //备份要插入的值
        for (j = i; j > 0&& array[j - 1] > temp; j--) //将i前数据与i比较,后移空位置
            array[j] = array[j - 1];
        array[j] = temp;  //将i放置该放位置
    }
}

时间复杂度分析:最好情况,全部有序,需要比较n-1次,最好情况无交换,故时间复杂度为O(n),最坏情况比较(n+2)(n-1)/2次(注意j循环中有两次比较),移动次数为(n+4)(n-1)/2次,时间复杂度为O(n^2),但性能上还是略优于冒泡排序和简单选择排序。

优势:在基本有序或者记录数目较少的时候

缺点:共需n趟排序,效率低,并且每次循环只能将数据移动一位

 

算法四:希尔排序

如上述算法三,在记录数目较少时,直接插入排序有优势,若记录数目较多,则可将记录分割成多个子序列,在每个子序列(记录数目较少)内直接插入排序。当整个序列基本有序时,再对全体进行一次直接插入排序。选择一个增量序列,按照增量序列进行k趟排序,每趟排序将整个待排序的记录序列分割成为m个子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时(增量降为1),再对全体记录进行依次直接插入排序,由于跳跃式移动不稳定

void shell_sort(int array[], int n)
{
    int i, j, Increment,temp;
    for (Increment = n / 2; Increment >0; Increment /= 2)  //计算希尔增量,对应k趟排序
        for (i = Increment; i< n; i++) //i为要插入的数据索引,开始时均插在子序列最后
        {
            temp= array[i];
            for (j = i; j >=Increment &&array[j - Increment] > temp; j -=Increment)
                //将i在子序列中前面数据依次与i比较, 后移空出位置
                array[j] = array[j - Increment];
            array[j] = temp;
        }
}

时间复杂度分析:取决于增量序列的选择,对于上述序列复杂度为O(n^3/2),第一个突破O(n^2)的算法

 

算法五:堆排序

直接选择排序并没有把每趟的比较结果记录下来,导致后一趟的比较中有许多前一趟已经做过了,因而比较次数较多,若能找到本趟循环要找的值的同时,对其他记录进行相应的调整,则会提高整体效率。

大顶堆:每个节点的值都大于或等于左右孩子结点的值的完全二叉树

小顶堆:每个节点的值都小于或等于左右孩子结点的值的完全二叉树

将待排序的序列构造成一个大顶堆,根结点则为最大值,将其与堆数组末尾元素交换,再将剩余n-1个序列重新构成一个大顶堆,如此反复循环,便能得到一个有序序列。故需解决两个问题:如何构建堆?输出堆顶后,如何调整成新的堆?

void heapadjust(int *a, int start, int end)
{
    int temp = a[start]; //当前根结点
   while( 2 * start + 1 < end)//当前根结点的左孩子
    {
        int i = 2 * start + 1;
        if (i + 1 < end && a[i + 1] > a[i]) // 比较两个孩子中的较大值
            i++;  
        if (a[i] < temp)
            break;
        a[start] = a[i];  //将孩子与根结点交换
        start = i;
    }
    a[start] = temp;
}

void heapSort(int a[], int n)
{
    for (int i = (n - 1) / 2; i >= 0; i--)//n - 1/2为最后一个双亲结点
        heapadjust(a, i, n); //从最后一个双亲结点开始逐步向上调整为大顶堆
    for (int i = n -1 ; i > 0; i--)
    {
        int temp = a[i];
        a[i] = a[0];
        a[0] = temp;//将堆顶结点与最后一个元素交换
        heapadjust(a, 0, i);//将剩余元素调整为大顶堆
    }
}

时间复杂度分析:时间消耗在初始建堆和重建堆的反复筛选,从最下层最右边非端点开始与左右孩子比较,最多两次比较和互换,因此构建堆为O(n);正式排序时,第i次取堆顶记录重建堆需要O(logi)的时间(完全二叉树的某个结点到根结点距离为[logi]+1),并且需要取n-1次,因此重建堆复杂度为O(nlogn),整体时间复杂度为O(nlogn)

特点:对原始记录排序状态不敏感,跳跃式交换不稳定。由于初始建堆所需比较次数较多,不适合待排序个数较少的情况

 

算法六:归并排序

采用分治思想,对任一序列,分成左右子列,子列内部排序,递归实现,然后用指针移动合并两子列实现归并操作,稳定。

代码实现:

void Merge(int array[],int tempArr[], int startIndex,int midIndex,int endIndex)

{

    int i = startIndex, j = midIndex + 1, k = startIndex;

    while (i != midIndex + 1 && j !=endIndex + 1) //直至有一子列全部排进合并列

    {

        if (array[i] >array[j])

            tempArr[k++] = array[j++];

        else

            tempArr[k++] = array[i++];

    }

    while (i != midIndex + 1)  //未排完子列剩余元素直接送人合并列

        tempArr[k++] = array[i++];

    while (j != endIndex + 1)

        tempArr[k++] = array[j++];

    for (i = startIndex; i <= endIndex; i++) //将合并列放入原序列应有的位置

        array[i] = tempArr[i];

}

void mergeSort(intarray[],inttempArr[], intstartIndex,intendIndex)

{

int midIndex;

    if (startIndex <endIndex)

    {

        midIndex= (startIndex +endIndex) / 2;

        mergeSort(array, tempArr, startIndex, midIndex); //对左子列排序

        mergeSort(array, tempArr, midIndex + 1, endIndex); //对右子列排序,注意加1

        Merge(array, tempArr, startIndex, midIndex, endIndex); //归并操作合并两子列

    }

}

*** 需要提前申请临时空间tempArr[]存放合并列,分治思想降低了时间复杂度,但递归实现提高了空间复杂度

 

算法七:快速排序--目前最受推崇的排序算法

取序列中任一元素作为枢纽元,将其余元素与该枢纽元比较,分成大小两个集合,等于枢纽元的元素随机分配,递归地对两个集合快速排序。

根据细节不同,有不同的方法,标准算法取第一个数做枢纽元,这样在数组已经有序的情况下,每次划分将得到最坏的结果。一种比较常见的优化方法是随机化算法,即随机选取一个元素作为主元。这种情况下虽然最坏情况仍然是O(n^2),但最坏情况不再依赖于输入数据,而是由于随机函数取值不佳。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。随机化快速排序的唯一缺点在于,一旦输入数据中有很多的相同数据,随机化的效果将直接减弱。最好的方法是取左端,右端和中间位置上的三个元素的中值做枢纽元。取这3个值的好处是在实际问题中,出现近似顺序数据或逆序数据的概率较大,此时中间数据必然成为中值,而也是事实上的近似中值。万一遇到正好中间大两边小(或反之)的数据,取的值都接近最值,那么由于至少能将两部分分开,实际效率也会有2倍左右的增加,而且利于将数据略微打乱,破坏退化的结构。

代码实现:

int Median3(intarray[],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];

}

#define cutoff 3

void QuickSort(intarray[],int left, int right)

{

    if (left +cutoff <=right)

    {

        int pivot = Median3(array,left,right);

        int i = left;

        int j = right - 1;

        while (1)

        {

            while (array[++i] < pivot) {}//从两侧分别找到错误分组的元素

            while (array[--j] > pivot) {}

            if (i < j)  //若i,j尚未交错

                swap(&array[i], &array[j]);

            else

                break; //当前分组正确

        }

        swap(array[i], array[right - 1]); //把枢纽元放到中间

        QuickSort(array, left, i - 1);

        QuickSort(array, i + 1,right);

    }

    else

        InsertionSort(array + left, right - left + 1);//对于小数组,插入排序更合适

}

*** 与归并相比,同样是递归,可是分成的两个子问题并不一定相等(隐患),但分成两组是在适当的位置进行且非常有效,故比递归更快。

 

算法八:基数排序

       基数排序用于对多关键字域数据(例如:一副扑克牌,大小可以看做一个关键字域,花色也可以看做另一个关键字域)进行排序,每次对数据按一种关键字域进行排序,然后将该轮排序结果按该关键字的大小顺序堆放,依次进行其他关键字域的排序,最后实现序列的整体排序。

限制:

       基数排序需要一种稳定的排序算法作为子程序,在这里使用计数排序。

时间复杂度:

       (1)给定n个d位k进制数,使用计数排序(耗时:)作为子程序,那么时间复杂度为:,因此,当d为常数,时,为线性代价;

       (2)给定n个b位k进制数,若b太大,可考虑将b分成r段,这时得到n个b/r位k^r进制数,同样使用计数排序(耗时:),那么时间复杂度为:,当时,此时时间复杂度为:

缺点:

       不是原址排序;虽然可以达到线性时间复杂度,但是常数因子较大。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值