各种类型排序的实现及比较

Posted zefengyao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了各种类型排序的实现及比较相关的知识,希望对你有一定的参考价值。

插入排序法:

简单介绍:插入排序在排序过程中会把整个数组分成已排好序和还未排序两部分。每次从未排序部分的开头取出一个数字,插入到已排序的部分。

性质:是稳定的排序法。且最坏的情况下一共要移动(1+2+...+N-1)=(N^2-N)/2次,所以基本是O(n^2)复杂度的排序法,当然输入数据的顺序可以极大的影响该排序算法的复杂度。例如数据本来就是升序排列,所有数据都不用移动,此时只需判断n次就可以结束算法运行。

参考代码:

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

冒泡排序法:

简单介绍:排序过程中也会把数组分成排好序和未排序两部分。每次从数组末尾开始依次比较相邻两个元素的大小,若逆序,则交换两者顺序。

性质:也是稳定的排序。最坏的情况下,也需要进行(N-1+N-2+...+1)=(N^2-N)/2次排序,所以属于O(n^2)复杂度算法。在冒泡排序中,每次两个数交换顺序,就会减少一个逆序对,所以冒泡法的排序次数就是该数列逆序对的个数。具体实现该算法时加入了一个flag,用于判断上一次循环时是否还有逆序对,若没有,说明已经排好序了。不加flag的话不管输入数据怎么变化,冒泡排序都需要(N^2-N)/2次排序。

参考代码:

void bubble_sort() {
    bool flag = 1;
    for (int i = 0; flag;i++) {
        flag = 0;//用于判断是否还有逆序对,有交换说明还有逆序对,若没有逆序对,直接出循环
        for (int j = n - 1; j >= i + 1;j--) {
            if(a[j]<a[j-1])
            swap(a[j], a[j - 1]),flag = 1;
        }
    }
}

选择排序法:

简单介绍:排序过程中是分已排序和未排序两部分。每次从未派好的元素当中挑一个最小的与未排好数据部分的首位进行交换。

性质:不稳定的排序,因为选择排序可能会交换两个位置不相邻的元素。不管输入数据怎么变化,利用选择排序都需要(N^2-N)/2次操作。

参考代码:

void select_sort() {
    for (int i = 0; i < n; i++) {
        int min_num = INF, k;
        for (int j = i; j < n; j++) {
            if (min_num > a[j]) min_num = a[j], k = j;
        }
        swap(a[i], a[k]);
    }
}

希尔排序法:

简单介绍:希尔排序法充分发挥了插入排序可以较快的处理有顺序数据的特点。设置一个间隔数组G={1,4,13,...,k},k<n,每次按照间隔进行插入排序。

性质:间隔数组取得好,一般复杂度维持在O(n^1.25)。

参考代码:

vector<int>G;
void insert_sort(int *a,int n,int g) {//其中g代表间隔
    for (int i = g; i < n;i++) {
        int j = i - g,v=a[i];
        while (j>=0&&a[j]>v) {
            a[j + g] = a[j];
            j -= g;
        }
        a[j + g] = v;
    }
}
void shell_sort() {
    int g = 1;
    while(g<n) {
        G.push_back(g);
        g = 3 * g + 1;
    }
    for (int i = G.size() - 1; i >= 0; i--) {
        insert_sort(a, n, G[i]);
    }
}

归并排序法:

简单介绍:分治思想,要处理长度为n的数列,先将数列划分左右两部分,每部分n/2个元素,每部分分别排好序后,线性时间内就可以完成n个元素的排列。

性质:归并排序是稳定排序。n个数据会大致分成log(n)层,每层执行的时间复杂度为n,故整体nlog(n)复杂度。复杂度递推式为T(n)=2T(n/2)+n,计算复杂度也可以利用主定理。不过归并排序需要额外的使用O(n)的空间。

参考代码:

void merge_sort(int l,int r) {//[l,r)
    if (r - l <= 1)return;
    int mid = (l + r) >> 1;
    merge_sort(l, mid);
    merge_sort(mid, r);
    int p = l, q = mid,i=l;
    while (p<mid||q<r) {
        if (q>=r||p<mid&&a[p]<a[q])t[i++] = a[p++];
        else  t[i++] = a[q++];
    }
    for (int i = l; i < r;i++) {
        a[i] = t[i];
    }
}

快速排序法:

简单介绍:分治的思想。对于n个元素的数列,先找到一个基准e,比e大的元素排在e右边,比e小的元素排在e左边,接下来以e为分割点,递归分治[L,e),[e,R)部分的数列。

性质:由于分割会交换不相邻的元素,故快排属于不稳定排序。并且不需要额外的空间,是一种原地算法。基准的选择是算法快慢的关键,若基准每次都能均匀的分出两部分子列,复杂度为nlog(n),如果极端的情况,仍然可能会有O(n^2)复杂度。

参考代码:

int partition(int L,int R) {//用基准对区间[L,R]进行划分
    int i = L - 1,e=a[R];
    for (int j = L; j < R;j++) {
        if (a[j] < e) {
            i++; swap(a[i], a[j]);
        }
    }
    swap(a[i + 1], a[R]);
    return i + 1;
}
void quick_sort(int l, int r) {//[l,r]
    if (l >= r)return;
        int e = partition(l, r);
        quick_sort(l, e - 1);
        quick_sort(e + 1, r);
}

计数排序法:

简单介绍:开辟一个数组C,将所有出现的元素以下标的形式记录于C中,譬如有一个元素x,则C[x]++。之后求C中各元素的累计和,这样某一个C[k]=p,即代表小于等于k的元素还有p个,这样k就是第p小的元素,直接就能知道其所在位置了。

性质:稳定的排序。复杂度为O(n+k),其中k代表所有元素中最大的那个元素值。

参考代码:

int c[N_MAX], b[N_MAX];
void count_sort() {
    for (int i = 0; i < n; i++)c[a[i]]++;
    for (int i = 1; i < N_MAX; i++)c[i] = c[i] + c[i - 1];
    for (int i = 0; i<n; i++) {
        b[--c[a[i]]] = a[i];
    }
}

 

堆排序法:

首先需要介绍一下堆:

堆首先是完全二叉树,即最下面一层节点全部集中在该层的左边若干位置,其余层的节点数全满。其次,最大堆的性质在于堆中任何节点的键值小于其父节点的键值,故根节点是最大堆中的键值最大的节点。依据堆的这些性质,可以用一个下标从1开始的数组来存储堆。

接下来分三个块内容:堆的构造,堆的元素插入以及堆的元素抛出。

1:堆的构造

对于一个n元素的数组,如何将其调整为堆:从堆中最后一个有儿子节点的节点(即编号为n/2的节点)开始进行调整,一直调整到根节点为止,每次调整一个节点后,以该节点为根的子树就是一个堆了。故可以知道,每次调整一个节点,它的左右儿子处已经满足堆的定义了。现在要将当前节点的键值设置为左右儿子以及它本身键值中的最大值,譬如原本左儿子键值最大,就将左儿子的键值与当前节点的键值进行交换,再递归的调整左儿子所在的树。若将无序数组调整为堆,一共得调整n/2次,总复杂为O(n)。复杂度简单说明:假如是完美二叉树,一共n个节点,那么我们首先要对高度为1的n/2个子树进行调整操作,再对高度为2的n/4个子树进行调整。。所以整体要进行的操作是:n*Σ1<=k<=logn(k/2^k)=O(n)

参考代码:

void heap_adjust(int *a,int i) {//调整以节点i为根节点的子树
    int left = (i << 1), right = (i << 1) + 1,largest;
    if (left<=n&&a[left]>a[i])largest = left;
    else largest = i;
    if (right<=n&&a[right]>a[largest])largest = right;
    if (largest != i) {
        swap(a[largest], a[i]);
        heap_adjust(a,largest);
    }
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++)cin >> a[i];
    for (int i = n / 2; i >= 1; i--)heap_adjust(a, i);//从最后一个有儿子节点的节点开始调整
    for (int i = 1; i <= n; i++)cout << a[i] << " ";
    return 0;
}

2:堆的元素插入:

有元素插入,先将节点数n++,元素插入到数组的最后一个位置,然后查看插入节点的键值是否大于其父节点的值,若大于,则和父节点交换键值,再递归调整其父节点的值。log(n)复杂度

参考代码:

void insert(int key) {
    n++;//堆中元素个数加1
    a[n] = key;
    int tmp_n = n;
    while (tmp_n>1&&a[tmp_n]>a[tmp_n/2]) {
        swap(a[tmp_n], a[tmp_n / 2]);
        tmp_n /= 2;
    }
}

3:堆顶元素抛出:

堆顶元素抛出以后,把末尾元素放置到堆顶,然后进行一次head_adjust堆调整即可。log(n)复杂度

参考代码:

int pop() {
    if (n < 1)return -1;//堆中没有元素
    int maxv = a[1];
    swap(a[1], a[n--]);//为了与堆排序进行衔接,这里把堆顶元素与堆最后一个元素交换位置
    heap_adjust(a, 1);
    return maxv;
}

对于堆的基本操作都以给出,堆排序也就能简单实现了。首先对于n个无序的数字组成的数组,调整为堆,这个过程O(n)复杂度。之后不断的抛出堆顶元素,并将堆顶元素与堆尾元素进行交换,迭代n次即可。直接利用上述的pop()操作。

参考代码:

int N;//N用于记录数组元素个数
void heap_sort() {
    for (int i = n / 2; i >= 1; i--)heap_adjust(a,i);
    int t = n;
    N = n;
    while(t--){
        pop();
    }
}

 


以上是关于各种类型排序的实现及比较的主要内容,如果未能解决你的问题,请参考以下文章

各种排序总结与自写(归并排序)

交换排序(冒泡排序快速排序的算法思想及代码实现)

排序算法原理及代码实现(c#)

快速排序-递归实现

程序员面试必问系列——各种排序算法比较

十大经典排序算法的算法描述和代码实现