排序算法总结

来源:互联网 发布:java遍历map的键值对 编辑:程序博客网 时间:2024/06/11 19:49

在程序中当我们需要对数据进行排序时,会面临很多排序算法,这个时候对排序算法的选择多种多样。下面我们列出以下一些常用的排序算法。
常用的排序算法有:
冒泡排序 选择排序 插入排序 快速排序 归并排序 堆排序 希尔排序
1. 冒泡排序(Bubble Sort)
基本思想:
在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
冒泡排序流程:
这里写图片描述
冒泡排序程序:

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

运行结果
这里写图片描述
2.选择排序(SelectSort)
基本思想:
在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。
选择排序流程:
这里写图片描述
进行比较操作的时间复杂度为O(n^2),进行移动操作的时间复杂度为O(n)。选择排序是不稳定排序。
选择排序程序:

void select_sort(T* arr , int size){    int index ,i ,j;    T item;    for (i = 0; i < size - 1; i++)    {           index = i;  item = arr[i];        for (j = i + 1; j < size; j++)        {            if (arr[j] < item)            {                item = arr[j];                index = j;            }        }        arr[index] = arr[i];        arr[i] = item;    }}

3.插入排序(InsertSort)
基本思想:
将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表。即:先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。
要点:设立哨兵,作为临时存储和判断数组边界之用。
插入排序流程:
这里写图片描述
插入排序程序:

void InsertSort(int* a, int length){    int i, j;    int data;    for (i = 1; i < length; i++)    {        j = i - 1; data = a[i];        while (j >= 0 && a[j] > data)        {            a[j + 1] = a[j];            j--;        }        a[j + 1] = data;    }}

4.快速排序(QuickSort)
基本思想:
一趟快速排序的算法是:
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j–),找到第一个小于key的值A[j],将A[j]和A[i]互换;
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
5)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。
分析:
快速排序是通常被认为在同数量级(O(nlog2n))的排序方法中平均性能最好的。但若初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。为改进之,通常以“三者取中法”来选取基准记录,即将排序区间的两个端点与中点三个记录关键码居中的调整为支点记录。快速排序是一个不稳定的排序方法。
快速排序程序:

void quickSort(int* a, int left , int right){    int i = left;    int j = right;    int temp = a[left];    int t;    if (i >= j)        return;    while (i < j)    {        while (j>i && a[j] >= temp)            j--;        while (j > i && a[i] <= temp)            i++;        if (i < j)        {            t = a[i];            a[i] = a[j];            a[j] = t;        }    }    a[left] = a[i];    a[i] = temp;    quickSort(a, left, i - 1);    quickSort(a, i + 1, right);}

快速排序的改进
在本改进算法中,只对长度大于k的子序列递归调用快速排序,让原序列基本有序,然后再对整个基本有序序列用插入排序算法排序。实践证明,改进后的算法时间复杂度有所降低,且当k取值为 8 左右时,改进算法的性能最佳。
5.堆排序(Heap Sort)
堆排序是一种树形选择排序,是对直接选择排序的有效改进。

基本思想:
堆的定义如下:具有n个元素的序列(k1,k2,…,kn),当且仅当满足

时称之为堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最小项(小顶堆)。
若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的。如:
(a)大顶堆序列:(96,83,27,38,11,09)
(b)小顶堆序列:(12,36,24,85,47,30,53,91)

初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序。

因此,实现堆排序需解决两个问题:
1. 如何将n 个待排序的数建成堆;
2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。

首先讨论第二个问题:输出堆顶元素后,对剩余n-1元素重新建成堆的调整过程。
调整小顶堆的方法:
1)设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶((最后一个元素与堆顶进行交换),堆被破坏,其原因仅是根结点不满足堆的性质。
2)将根结点与左、右子树中较小元素的进行交换。
3)若与左子树交换:如果左子树堆被破坏,即左子树的根结点不满足堆的性质,则重复方法 (2).
4)若与右子树交换,如果右子树堆被破坏,即右子树的根结点不满足堆的性质。则重复方法 (2).
5)继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。
称这个自根结点到叶子结点的调整过程为筛选。如图:
这里写图片描述

再讨论对n 个元素初始建堆的过程。
建堆方法:对初始序列建堆的过程,就是一个反复进行筛选的过程。
1)n 个结点的完全二叉树,则最后一个结点是第个结点的子树。
2)筛选从第个结点为根的子树开始,该子树成为堆。
3)之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。
如图建堆初始过程:无序序列:(49,38,65,97,76,13,27,49)
这里写图片描述
算法的实现:
从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。
堆排序程序:

//调整为一个堆void Heap_Adjust(int *list,int s,int m){    int temp = list[s];    for(int j=2*s+1;j<=m;j = 2*j+1)    {        if(list[j]<list[j+1]&&j<m)        {            j++;        }        if(temp>list[j])            break;        list[s] = list[j];        s = j;    }    list[s] = temp;}//堆排序void Heap_Sort(int *list,int len){    //创建一个大顶堆    for(int s = len/2-1;s>=0;s--)    {        Heap_Adjust(list,s,len-1);    }    //排序    for(int i = len-1;i >= 1;i--)    {        swap(list[0],list[i]);        Heap_Adjust(list,0,i-1);    }}

由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。堆排序是就地排序,辅助空间为O(1),它是不稳定的排序方法。
总结一下:
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序
不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序

选择排序算法准则:
每种排序算法都各有优缺点。因此,在实用时需根据不同情况适当选用,甚至可以将多种方法结合起来使用。

选择排序算法的依据
影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:
1.待排序的记录数目n的大小;
2.记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;
3.关键字的结构及其分布情况;
4.对排序稳定性的要求。

设待排序元素的个数为n.

1)当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。
快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序:如果内存空间允许且要求稳定性的,
归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。

2)当n较大,内存空间允许,且要求稳定性 =》归并排序
3)当n较小,可采用直接插入或直接选择排序。
直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。
直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序
4)一般不使用或不直接使用传统的冒泡排序。
5)基数排序
它是一种稳定的排序算法,但有一定的局限性:
1、关键字可分解。
2、记录的关键字位数较少,如果密集更好
3、如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。