八大排序算法

说明:
本文基本参照http://blog.csdn.net/hguisu/article/details/7776068
进行稍加修改。

大名鼎鼎——快速排序

int quicksort(vector<int> &v, int left, int right){
        if(left < right){
                int key = v[left];
                int low = left;
                int high = right;
                while(low < high){
                        while(low < high && v[high] >= key){
                                high--;
                        }
                        v[low] = v[high];
                        while(low < high && v[low] <= key){
                                low++;
                        }
                        v[high] = v[low];
                }
                v[low] = key;
                quicksort(v,left,low-1);
                quicksort(v,low+1,right);
        }
}

或者代码:

void quickSort(int a[], int left, int right)
{
    if (left < right)
    {
        int key = a[left];
        int low = left, high = right;
        while (low < high)
        {
            while (low < high&&a[high] >= key) high--;
            swap(a[low], a[high]);
            while (low < high&&a[low] <= key) low++;
            swap(a[low], a[high]);
        }
        quickSort(a, left, low - 1);
        quickSort(a, low + 1, right);
    }
}

思想

快速排序采用的思想是分治思想。

快速排序是找出一个元素(理论上可以随便找一个)作为基准(pivot),然后对数组进行分区操作,使基准左边元素的值都不大于基准值,基准右边的元素值 都不小于基准值,如此作为基准的元素调整到排序后的正确位置。递归快速排序,将其他n-1个元素也调整到排序后的正确位置。最后每个元素都是在排序后的正 确位置,排序完成。所以快速排序算法的核心算法是分区操作,即如何调整基准的位置以及调整返回基准的最终位置以便分治递归。

举例说明一下吧,这个可能不是太好理解。假设要排序的序列为

2 2 4 9 3 6 7 1 5 首先用2当作基准,使用i j两个指针分别从两边进行扫描,把比2小的元素和比2大的元素分开。首先比较2和5,5比2大,j左移

2 2 4 9 3 6 7 1 5 比较2和1,1小于2,所以把1放在2的位置

2 1 4 9 3 6 7 1 5 比较2和4,4大于2,因此将4移动到后面

2 1 4 9 3 6 7 4 5 比较2和7,2和6,2和3,2和9,全部大于2,满足条件,因此不变

经过第一轮的快速排序,元素变为下面的样子

[1] 2 [9 3 6 7 4 5]

之后,在把2左边的元素进行快排,由于只有一个元素,因此快排结束。右边进行快排,递归进行,最终生成最后的结果。

分析

快速排序的时间主要耗费在划分操作上,对长度为k的区间进行划分,共需k-1次关键字的比较。

最坏情况是每次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中记录数目,仅仅比划分前的无序区中记录个数减少一个。时间复杂度为O(n*n)

在最好情况下,每次划分所取的基准都是当前无序区的”中值”记录,划分的结果是基准的左、右两个无序子区间的长度大致相等。总的关键字比较次数:O(nlgn)

尽管快速排序的最坏时间为O(n2),但就平均性能而言,它是基于关键字比较的内部排序算法中速度最快者,快速排序亦因此而得名。它的平均时间复杂度为O(nlgn)。

分析:

快速排序是通常被认为在同数量级(O(nlog2n))的排序方法中平均性能最好的。但若初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。为改进之,通常以“三者取中法”来选取基准记录,即将排序区间的两个端点与中点三个记录关键码居中的调整为支点记录。快速排序是一个不稳定的排序方法。
摘自博客:
http://www.cnblogs.com/luchen927/archive/2012/02/29/2368070.html#!comments
不过该博客有点错误,我已经修改了。

插入排序——稳

基本思想:

将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表。即:先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。
要点:设立哨兵,作为临时存储和判断数组边界之用。
时间

复杂度O(N^2),空间复杂度O(1)

void InsertSort(int a[], int n)
{
    for (int i = 1; i < n; i++)
    {
        if (a[i] < a[i - 1])//后面一个元素大于前面的元素,要把后面这个元素移到前面正确的位置。稳定算法,复杂度为O(N^2)
        {
            int j = i - 1;
            int x = a[i];
            while (j >= 0 && x < a[j])
            {
                a[j + 1] = a[j];
                j--;
            }
            a[j + 1] = x;
        }
    }
}
#include
using namespace std;
#include"InsertSort.h"
#include"ShellSort.h"
void print(int a[], int n) {
    for (int j = 0; jcout << a[j] << " ";
    }
    cout << endl;
}
int main()
{
    int a[8] = { 3,1,5,7,2,4,9,6 };
    //InsertSort(a, 8);
    shellSort(a, 8);
    print(a, 8);
}

希尔排序——插入排序的升级版

void ShellInsertSort(int a[], int n, int dk)
{
    for (int i = dk; i < n; i++)//和InsertSort基本一样,只是每次增量为dk
    {
        if (a[i] < a[i - dk])
        {
            int j = i - dk;
            int x = a[i];
            while (j >= 0 && x < a[j])
            {
                a[j + dk] = a[j];
                j = j - dk;
            }
            a[j + dk] = x;
        }
    }
}
void shellSort(int a[], int n)
{
    int dk = n / 2;
    while (dk >= 1)
    {
        ShellInsertSort(a, n, dk);
        dk = dk / 2;
    }
}

复杂度分析:
时间复杂度:比较难计算,不稳定。不超过O(N^2)
不需要大量的辅助空间,和归并排序一样容易实现。希尔排序是基于插入排序的一种算法, 在此算法基础之上增加了一个新的特性,提高了效率。希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n²),而Hibbard增量的希尔排序的时间复杂度为O( N^(3/2) ),希尔排序时间复杂度的下界是n*log2n。希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O( )复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。 此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。专家们提倡,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法. 本质上讲,希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。 原因是,当n值很大时数据项每一趟排序需要的个数很少,但数据项的距离很长。当n值减小时每一趟需要和动的数据增多,此时已经接近于它们排序后的最终位置。 正是这两种情况的结合才使希尔排序效率比插入排序高很多。Shell算法的性能与所选取的分组长度序列有很大关系。只对特定的待排序记录序列,可以准确地估算关键词的比较次数和对象移动次数。想要弄清关键词比较次数和记录移动次数与增量选择之间的关系,并给出完整的数学分析,至今仍然是数学难题。

空间复杂度:O(1)

原理解释图:
https://baike.baidu.com/pic/%E5%B8%8C%E5%B0%94%E6%8E%92%E5%BA%8F/3229428/0/b3fb43166d224f4a87a8c9380af790529822d1ec?fr=lemma&ct=single#aid=0&pic=b3fb43166d224f4a87a8c9380af790529822d1ec

选择排序——简单选择排序

复杂度:O(N^2)。空间复杂度:O(1)

稳:
基本思想:

在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。
操作方法:

第一趟,从n 个记录中找出关键码最小的记录与第一个记录交换;

第二趟,从第二个记录开始的n-1 个记录中再选出关键码最小的记录与第二个记录交换;

以此类推…..

第i 趟,则从第i 个记录开始的n-i+1 个记录中选出关键码最小的记录与第i 个记录交换,

直到整个序列按关键码有序。

void selectSort(int a[], int n)
{
    for (int i = 0; i < n; i++)
    {
        //选择从i到n-1之间最小的元素
        int k = i;//最小元素 下标
        for (int j = i + 1; j < n; j++)
        {
            if (a[k] > a[j]) k = j;
        }
        if (k != i)//可以直接swap()
        {
            swap(a[k], a[i]);
        }
    }
}
//选择排序改进版,每次确定最大最小两个元素,只需要n/2趟
void selectSort2(int a[], int n)
{
    for (int i = 0; i <= n / 2; i++)
    {
        int min = i, max = i;//用于记录最大最小位置
        for (int j = i + 1; j < n - i; j++)
        {
            if (a[j] > a[max]) max = j;
            if (a[j] < a[min]) min = j;
        }
        swap(a[i], a[min]);
        swap(a[n - i-1], a[max]);
    }
}

选择排序——堆排序(Heap Sort)

算法描述:http://blog.csdn.net/hguisu/article/details/7776068

从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。

/**
* 已知H[s…m]除了H[s] 外均满足堆的定义
* 调整H[s],使其成为大顶堆.即将对第s个结点为根的子树筛选,
*
* @param H是待调整的堆数组
* @param s是待调整的数组元素的位置
* @param length是数组的长度
*
*/
void HeapAdjust(int H[], int s, int length)//大顶堆调整
{
    int tmp = H[s];
    int child = 2 * s + 1;//左孩子
    while (child < length)
    {
        if (child + 1 < length&&H[child] < H[child + 1])
        {
            child++;//child变成右孩子
        }
        if (H[s] < H[child])
        {
            H[s] = H[child];
            s = child;
            child = 2 * s + 1;//重新指定child的值
        }
        else//已经调整好
            break;
        H[s] = tmp;
    }
}
void BuildHeap(int H[], int length)
{
    for (int i = (length - 1) / 2; i >= 0; i--)
        HeapAdjust(H, i, length);//从中间(也就是最下面非叶子节点的节点开始调整)
}
void HeapSort(int H[], int length)
{
    BuildHeap(H, length);
    for (int i = length - 1; i > 0; i--)
    {
        swap(H[0], H[i]);
        HeapAdjust(H, 0, i);
    }
}

分析:

设树深度为k,。从根到叶的筛选,元素比较次数至多2(k-1)次,交换记录至多k 次。所以,在建好堆后,排序过程中的筛选次数不超过下式:

2*n*log2(n)

而建堆时的比较次数不超过4n 次,因此堆排序最坏情况下,时间复杂度也为:O(nlogn )。

交换排序——冒泡排序(Bubble Sort)

基本思想:

在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。

void bubbleSort(int a[], int n)
{
    for (int i = 0; i < n; i++)//是说要进行n趟冒泡
        for (int j = 0; j < n - i - 1; j++)
            if (a[j]>a[j + 1])
                swap(a[j], a[j + 1]);
}

冒泡排序算法的改进

对冒泡排序常见的改进方法是加入一标志性变量exchange,用于标志某一趟排序过程中是否有数据交换,如果进行某一趟排序时并没有进行数据交换,则说明数据已经按要求排列好,可立即结束排序,避免不必要的比较过程。本文再提供以下两种改进算法:

1.设置一标志性变量pos,用于记录每趟排序中最后一次进行交换的位置。由于pos位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到pos位置即可。

改进后算法如下:

void bubbleSort1(int a[], int n)
{
    int i = n - 1;
    while (i > 0)
    {
        int pos = 0;
        for (int j = 0; j < i; j++)
        {
            if (a[j] > a[j + 1])
            {
                swap(a[j], a[j + 1]);
                pos = j;//记录交换的位置(其实是相当于保存最后一次交换的位置
            }
        }
        i = pos;//为下一次冒泡做准备
    }
}

2.传统冒泡排序中每一趟排序操作只能找到一个最大值或最小值,我们考虑利用在每趟排序中进行正向和反向两遍冒泡的方法一次可以得到两个最终值(最大者和最小者) , 从而使排序趟数几乎减少了一半。

改进后的算法实现为:

void bubbleSort2(int a[], int n)
{
    int low = 0, high = n - 1;
    while (low < high)
    {
        for (int i = low; i < high; i++)
        {
            if (a[i] > a[i + 1])
                swap(a[i], a[i + 1]);
        }
        high--;
        for (int i = high; i > low; i--)
        {
            if (a[i] < a[i - 1])
                swap(a[i], a[i - 1]);
        }
        low++;
    }
}

归并排序

归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
1 个元素的表总是有序的。所以对n 个元素的待排序列,每个元素可看成1 个有序子表。对子表两两合并生成n/2个子表,所得子表除最后一个子表长度可能为1 外,其余子表长度均为2。再进行两两合并,直到生成n 个元素按关键码有序的表。

//先写递归的算法

//1.将有二个有序数列a[first...mid]和a[mid...last]合并。 
void mergearray(int a[], int first, int mid, int last, int temp[])
{
    int i = first, j = mid+1;
    int k = 0;
    while (i <= mid&&j <= last)
    {
        if (a[i] < a[j])
            temp[k++] = a[i++];
        else
            temp[k++] = a[j++];
    }
    while (i <= mid)
        temp[k++] = a[i++];
    while (j <= last)
        temp[k++] = a[j++];
    for (int i = 0;i < k;i++)
        a[i+first] = temp[i];
}
//递归
void mergesort(int a[], int first, int last, int temp[])
{
    if (first < last)
    {
        int mid = (first + last) / 2;
        mergesort(a, first, mid, temp);//左边有序
        mergesort(a, mid + 1, last, temp);//右边有序
        mergearray(a, first, mid, last, temp);//合并两个有序表
    }
}
void Mergesort(int a[], int n)
{
    int *p = new int[n];
    if (p == NULL)
        return;
    mergesort(a, 0, n - 1, p);
    delete[] p;
}


//非递归方法
void MergeSort2(int a[], int n)
{
    int *p = new int[n];
    if (p == NULL)
        return;
    int i = 1;
    while (i < n)
    {
        int s = i;
        i = 2 * s;
        int k = 0;
        while (k + i < n)//每i个归并一起
        {
            mergearray(a, k, k + s - 1, k + i - 1,p);
            k = k + i;
        }
        if (k + s < n)//最后不够i个的
        {
            mergearray(a, k, k + s - 1, n-1,p);
        }
    }
}

归并排序的效率是比较高的,设数列长为N,将数列分开成小数列一共要logN步,每步都是一个合并有序数列的过程,时间复杂度可以记为O(N),故一共为O(N*logN)。因为归并排序每次都是在相邻的数据中进行操作,所以归并排序在O(N*logN)的几种排序方法(快速排序,归并排序,希尔排序,堆排序)也是效率比较高的。

基数排序/桶排序

基本思想:是将阵列分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的阵列内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。

你可能感兴趣的:(数据结构)