常见排序算法剖析与比较

常见排序算法剖析与比较

在这个大数据时代,我们经常需要面对海量数据处理,其中少不了对于排序算法的优化,本篇文章将归纳部分常见排序算法的算法原理,利用图文解析算法,并且通过时间复杂度,空间复杂度以及稳定性等各方面对于常见排序算法进行简单刨析与比较。

常见排序算法分类

首先常见的排序我们根据排序的方式可以分为三大类交换排序插入排序选择排序
交换排序故名思意就是找到数据将其进行交换,从而达到数列有序。交换排序中常用的为冒泡排序与快速排序。
插入排序是局部最优的思路,在认定范围内做到有序,在插入一个数据时将其放在对应有序的位置即可。插入排序中常见的排序算法为直接插入排序与希尔排序。
选择排序则是每一次选择出最大或者最小的值进行排序。选择排序中常见的排序算法为直接选择排序与堆排序。
这里写图片描述

插入排序

首先我们来分析插入排序,插入排序是每一步将一个待排序的元素,按其排序码的大小,插入到前面已经排好序的一组元素的合适位置上去,直到元素全部插完为止。

直接插入排序

直接插入排序我们先假设一个我们已经有一个有序的序列,当我们插入一个数时我们找到让这个新序列依旧有序的数值位置并且进行插入,将这个位置后所有数据以此向后移动形成新的有序序列,当给一串无序序列时我们可以将第一个数值默认为有序序列,并且每一次插入数据都重新构造有序序列即可。
这里写图片描述
如上图所示,当我们向红色有序序列里插入新数值‘4’时,我们先找到数据的插入位置,再将插入位置后面的数逐次后移,并且将这个序列的大小加一形成新的有序序列。
这种排序方式的优点在于我们不用像冒泡排序等算法从头到尾的比较每一个数据,我们只用找到数据所在位置就停止寻找,当序列趋近于有序的时候查找的次数同样变少。不过这样也同时说明该算法的稳定性较低,该算法的时间复杂度为o(n*n),空间复杂度为o(1).可是当序列较为有序的时候便利次数则是远远低于o(n*n)。这样我们就考虑有没有一种方式可以优化直接插入排序,最好可以让他在即使是逆序情况下也可以遍历次数较少,我们提出了希尔排序。

希尔排序

希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”,是直接插入排序算法的一种更高效的改进版本。希需要注意的是希尔排序也是非稳定排序算法。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
希尔排序的意义在于可以改进直接插入排序,对序列先进行预排序,将序列排成较为有序的序列后在进行直接插入排序,这样直接插入排序便利次数将会大大减少从而提高效率。
预排序需要定义一个gap,根据gap将数据进行分组,一组gap个数据,每一组相同位置的值进行对比,从而改变每一组相同位置的数据使其相对有序。
这里写图片描述
上图中9,6,3,0根据对比进行交换,8,5,2根据顺序进行交换,7,4,1根据顺序进行交换。经过预排序后序列已经较为有序,接着在进行直接插入排序即可。
不过我们分析算法可以发现,gap大小的选择直接影响了该算法的复杂度,而且当gap等于1的时候预排序已经是直接插入排序了。根据多次实践可知,当gap为序列大小的三分之一时该算法复杂度最为合理,该算法的时间复杂度约为 o(N^1.25)
希尔排序只是理想中的排序,在现实应用中序列不会乱到甚至是倒序的程度,所以希尔排序在现实应用中很少被使用。

选择排序

选择排序是我们大多数人接触c语言时最早接触到的排序方法,也是逻辑最为简单的排序算法,大致的思路就是每一次遍历选出最小的数即可。

直接选择排序

直接选择排序我们需要对这个序列进行n次遍历,第一次找出最小的,第二次找出次小的以此类推。不过我们可以将直接选择排序进行改进,我们遍历一次不光可以找出最小的,还可以找出最大的,将最小的放在最前面,最大的放在最后,然后第二次遍历的范围除去已经确定的这两个最大的和最小的值,直到我们将这个序列整体遍历一遍即可。
选择排序的优点在于逻辑简单,我们只需要注意边界条件的把控即可,而且直接选择排序我们一次可以选出两个数据。不过我们不可能像插入排序那样不用遍历完就可以排序,我们需要逐一遍历。直接选择排序的时间复杂度为o(n*n),空间复杂度为o(1).

堆排序

堆排序是选择排序的一种,首先我们需要学过堆的操作,我们这里用序列模拟建一个堆,用向下调整算法将其调整为大堆,保证堆顶元素为最大值,接下来我们将堆顶的最大值与该序列最后一个值进行交换,相当于选出了该序列目前的最大值放在序列的最后,然后让这个堆的容量减一,正好将最大值的下标减下去,接着除了堆顶,这个堆的子树皆为大堆,我们只需要对堆顶进行一次向下调整就可以将这个堆变成大堆,接着重复前面的工作选出最大值放在倒数第二个位置,直到这个堆只剩一个元素那他一定是这个堆的最大值,同时也是该序列的最小值。
这里写图片描述
如图所示,将堆顶的9与3交换,在对3进行向下调整算法完成选择,该过程的复杂度为lgn。由于选择一次为单趟排序,而我们要将这个序列完成排序需要遍历整个序列,所以堆排序的时间复杂度为o(n*lgn),空间复杂度为o(1).

交换排序

交换排序就是将两个值做对比进行交换从而达成序列的有序。

冒泡排序

冒泡排序是排序算法中最简单的一种方法,我们在一个无序序列中找到通过与相邻位置的值对比从而完成将最大值冒泡到最后,从而完成排序。

快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
对于快排我们可以用递归实现,首先将某元素作为基准值,在这个基准值的左边数都小于这个基准值,在这个基准值的右边都大于基准值,接着在左边与右边实现这个算法,直到都只剩一个数据则排序完成,所以需要我们利用递归实现,可是如何找关键码效率最高以及如何将关键码左半区与右半区变成我们想要的是我们需要研究的问题。
这里写图片描述
关于这个关键码的选择,我们尽量选取中间值,因为比较好的情况下左半区与右半区相等那么这个排序就类似于满二叉树的形式,时间复杂度将会是o(lgn*n)可是如果关键码选择较大或者较小那么复杂度将会相对偏高。所以在关键码的选择上我们采用三数取中法。我们把这个序列的第一个数,最后一个数以及中间的数进行对比,选取中间数作为关键码,这样可以尽量保证关键码靠近中位数。
对于如何实现左子区与右子区的平衡实现,本文给出三种方式:
1. 左右指针法
定义一个指向最左边的指针与一个指向最右边的指针,让左指针与右指针一起向序列中间走,当左指针碰见比关键码大的数停下,当右指针碰见比关键码小的数停下,交换左指针与右指针的内容并且继续走,直到相遇停止,然后让关键码与相遇位置的数据调换,便完成了左右半区的平衡。
这里写图片描述
2. 挖坑法
先定义一个关键码,然后依旧是和第一种方法一样定义左右指针,当左指针指向比关键码大的位置时,这是第一个坑,将右指针第一个比关键码小的值填进来,然后右指指针向的位置为下一个坑,直到左右指针相遇的时候剩下最后一个坑就是关键码的位置。
这里写图片描述
3. 前后指针法
定义一个先走的指针与一个后走的指针,当先走的指针没碰到比关键码大的数之前一直向前走,后走的指针也同时向后走,当碰见比关键码大的数的时候先走的继续走,后走的停下,前一个指针找到比关键码小的数的时候与后指针交换数值,当先走的指针遍历序列的时候已经完成平衡。
这里写图片描述

排序算法的对比

在选择排序,插入排序,交换排序三种大类排序算法下,插入排序的好处在于单趟排序的过程中不用遍历每一个数字,只用便利到需要插入到的位置处即可,所以插入排序的复杂度极其依赖当前序列的顺序。相比之下选择排序以及交换排序稳定性就要高很多。
选择排序好处在于我们可以优化单趟排序的复杂度,比如我们可以建堆选出数据,交换排序中的快速排序可以算是这几种算法中时间复杂度最低的排序算法,可是快速排序的空间复杂度高于上面几种排序算法。
以下给出几种排序算法的复杂度以及稳定性的对比
这里写图片描述
以下为各种排序的代码实现:

#include<stdio.h>
#include<stdlib.h>
#include<windows.h>

void InsertSort(int* a,int n)
{
    int end = 1;
    int pos = n;
    while (pos!=1)
    {
        int need = a[end];
        int ptr = end - 1;
        while (ptr >=0 && a[ptr] > need)
        {
            ptr--;
        }
            for (int i = end-1;i >ptr;i--)
            {
                a[i+1] = a[i];
            }
            a[ptr+1] = need;
        end++;
        pos--;
    }
}
void ShellSort(int* a, int n)
{
    int gap = n / 3;
    int pos = n;
    int end = gap;
    while (pos != 1)
    {
        int need = a[end];
        int ptr = end - gap;
        while (ptr >= 0 && a[ptr] > need)
        {
            ptr-=gap;
        }
        for (int i = end - gap;i >ptr;i-=gap)
        {
            a[i + gap] = a[i];
        }
        a[ptr + gap] = need;
        end++;
        pos--;
    }
}

void swap(int* a, int* b)
{
    int c;
    c = *b;
    *b = *a;
    *a = c;
}
void SelectSort(int* a,int n)
{
    int begin = 0;
    int end = n - 1;
    while (end > begin)
    {
        int big = begin;
        int sma = begin;
        for (int i = begin;i <= end;i++)
        {
            if (a[i] > a[big])
            {
                big = i;
            }
            if (a[i] < a[sma])
            {
                sma = i;
            }
        }
        swap(&a[big],&a[end]);
        if (end == sma)
        {
            swap(&a[big], &a[begin]);
        }
        else
        {
            swap(&a[sma], &a[begin]);
        }
        end--;
        begin++;
    }
}

void AdjustDown(int parents, int* a,int n)
{
    int child = parents * 2 + 1;
    while (child < n)

    {
        if (child + 1 < n&&a[child] < a[child + 1])
        {
            child++;
        }
        if (a[parents] < a[child])
        {
            swap(&a[parents], &a[child]);
        }
        parents = child;
        child = parents * 2 + 1;
    }
}
void HeapSort(int* a, int n)
{
    for (int i = (n - 2) / 2;i >= 0;i--)
    {
        AdjustDown(i, a, n);
    }//建大堆
    while (n != 1)
    {
        AdjustDown(0, a, n);
        swap(&a[0],&a[n-1]);
        n--;
    }
}

int GetMid(int a, int b, int c)
{
    if (a > b)
    {
        if (b > c)
        {
            return b;
        }
        else
        {
            if (a > c)
            {
                return c;
            }
            else
            {
                return a;
            }
        }
    }
    else//(a<b)
    {
        if (b < c)
        {
            return b;
        }
        else
        {
            if (a > c)
            {
                return a;
            }
            else
            {
                return c;
            }
        }
    }
}

int PartSort1(int* a, int left, int right)
{
    int key = GetMid(left,right,left+(right-left)/2);
    swap(&a[key],&a[right]);
    int begin = left;
    int end = right;
    while (begin!=end)
    {
        if (a[begin] <= a[key])
        {
            begin++;continue;
        }
        if (a[end] >= a[key])
        {
            end--;continue;
        }
            swap(&a[begin],&a[end]);
    }
        swap(&a[begin],&a[key]);
        return begin;
}

void QuickSort(int* a, int left,int right)
{
    if (left < right)
    {
        int div = PartSort1(a, left, right);
        QuickSort(a, 0, div);
        QuickSort(a, div, right);
    }
    return;
}

以上就是对于几种常见排序算法的剖析与比较,如有问题还请指教。

猜你喜欢

转载自blog.csdn.net/qq9116136/article/details/80384204
今日推荐