排序的各种方法

各种排序方法总结

C++ 常用的八种排序方法


稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
插入排序:
  思想:
    (类似打扑克时的排序方法)

    将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

    优点:稳定,快
    缺点:比较次数不一定


  程序:
//直接插入排序
//将数组中的所有元素依次和前面的已经排好序的元素相比较(依次),
//如果选择的元素比已排序的元素小,则交换位置,直到全部元素都比较过。
#include<iostream>
using namespace std;
void work1(int *a,int size)
{
    for (int i = 0; i < size-1; i++)
    {
        int end;
        end = i + 1;
        
        while (end>0)
        {
            
            if (a[end - 1] > a[end])
            {
                int temp = a[end];
                a[end] = a[end - 1];
                a[end - 1] = temp;
                end--;
            }
            else
            {
                break;
            }
        }
        
    }
}
void test()
{
    int a[] = { 7,3 ,2,9,10,6,4};
    int a_size;
    a_size = sizeof(a) / sizeof(int);
    work1(a,a_size);
    for (int i = 0; i < a_size; i++)
    {
        cout << a[i] << "  ";
    }
    cout << endl;
}
int main()
{
    test();
    return 0;
}

shell排序:
  思想:
    先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。


  程序:
//希尔排序(直接插入法排序的改进)
//将数组按一定的间隔分成数个小数组,在小数组中使用直接插入法排序,然后修改间隔使其间隔
//变小,将数组再次重新分成几个小数组使用直接插入法排序,依此下去,直到间隔缩小到1为止
//直接插入法排序是希尔排序间隔为1时的特例,
#include<iostream>
using namespace std;
void work1(int *a, int size)
{    
    int h = 1;
    while (h < size / 3) 
    {
        h = 3 * h + 1;
    }
    while (h >= 1) 
    {
        for (int i = h; i < size; i++) 
        {
            for (int j = i; j >= h && a[j] < a[j - h]; j -= h) 
            {
                std::swap(a[j], a[j - h]);
            }
        }
        h = h / 3;
    }
}
void test()
{
    int a[] = { 7,3,15,9,4,2,12,8};
    int a_size;
    a_size = sizeof(a) / sizeof(int);
    work1(a, a_size);
    for (int i = 0; i < a_size; i++)
    {
        cout << a[i] << "  ";
    }
    cout << endl;
}
int main()
{
    test();
    return 0;
}
直接选择排序:
  思想:
    1.首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。

    2.再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

    3.重复第二步,直到所有元素均排序完毕


  程序:
//选择排序
//第一轮:将第一个元素视为最小值,用第一元素个依此与后面的元素做比较,有比第一个元素小的时交换位置
//第二轮:将第二个元素视为最小值,用第二元素个依此与后面的元素做比较,有比第一个元素小的时交换位置
#include<iostream>
#include<vector>
using namespace std;
void work1(vector<int> & a, int size)
{
    for (int i = 0; i < size; i++)
    {
        int min = i;
        for (int j = i+1; j < size; j++)
        {
            if (a[min]>a[j])
            {
                min = j;
            }
        }
        int temp = a[i];
        a[i] = a[min];
        a[min] = temp;
    }
}
void test()
{
    vector<int> a;
    a.push_back(7);
    a.push_back(17);
    a.push_back(72);
    a.push_back(8);
    a.push_back(4);
    a.push_back(9);
    a.push_back(16);
    a.push_back(28);    
    work1(a, a.size());
    for (int i = 0; i < a.size(); i++)
    {
        cout << a[i] << "  ";
    }
    cout << endl;
}
int main()
{
    test();
    return 0;
}
堆排序:
  思想:
    堆排序是基于选择排序的一种排序算法,堆是一个近似完全二叉树的结构,且满足子结点的键值或索引总是小于(或者大于)它的父节点。这里采用最大堆方式:位于堆顶的元素总是整棵树的最大值,每个子节点的值都比父节点小,堆要时刻保持这样的结构,所以一旦堆里面的数据发生变化,要对堆重新进行一次构建。


  程序:
//堆排序
//堆排序是基于选择排序的一种排序算法,堆是一个近似完全二叉树的结构,
//且满足子结点的键值或索引总是小于(或者大于)它的父节点。
#include <iostream>
#include <algorithm>
using namespace std;

void max_heapify(int arr[], int start, int end) 
{
    //建立父节点指标和子节点指标
    int dad = start;
    int son = dad * 2 + 1;
    while (son <= end) //若子节点指标在范围内才做比较
    { 
        if (son + 1 <= end && arr[son] < arr[son + 1]) //先比较两个子节点大小,选择最大的
            son++;
        if (arr[dad] > arr[son]) //如果父节点大於子节点代表调整完毕,直接跳出函数
            return;
        else // //否则交换父子内容再继续子节点和孙节点比较
        { 
            swap(arr[dad], arr[son]);
            dad = son;
            son = dad * 2 + 1;
        }
    }
}

void heap_sort(int arr[], int len) {
    //初始化,i从最後一个父节点开始调整
    for (int i = len / 2 - 1; i >= 0; i--)
        max_heapify(arr, i, len - 1);
    //先将第一个元素和已经排好的元素前一位做交换,再从新调整(刚调整的元素之前的元素),直到排序完毕
    for (int i = len - 1; i > 0; i--) 
    {
        swap(arr[0], arr[i]);
        max_heapify(arr, 0, i - 1);
    }
}

int main() {
    int arr[] = { 9,7,8,5,3, 4, 0, 2, 6 };
    int len = (int) sizeof(arr) / sizeof(*arr);
    heap_sort(arr, len);
    for (int i = 0; i < len; i++)
        cout << arr[i] << ' ';
    cout << endl;
    return 0;
}
冒泡排序:
  思想:
    比较相邻的元素。如果第一个比第二个大,就交换他们两个。

    对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

    针对所有的元素重复以上的步骤,除了最后一个。

    持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

    优点:稳定
    缺点:慢,每次只能移动两个相邻的数据;


  程序:
//冒泡排序
//比较相邻的元素。如果第一个比第二个大,就交换他们两个。
//对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
//针对所有的元素重复以上的步骤,除了最后一个。持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
#include<iostream>
using namespace std;
void work1(int *a, int size)
{
    for (int j = 0; j < size-1; j++)
    {
        for (int i = 1; i < size-j; i++)
        {
            if (a[i - 1] > a[i])
            {
                int temp = a[i - 1];
                a[i - 1] = a[i];
                a[i] = temp;
            }
        }
    }
    
}
void test()
{
    int a[] = { 17,3 ,42,39,10,6,64,72,48 };
    int a_size;
    a_size = sizeof(a) / sizeof(int);
    work1(a, a_size);
    for (int i = 0; i < a_size; i++)
    {
        cout << a[i] << "  ";
    }
    cout << endl;
}
int main()
{
    test();
    return 0;
}


快速排序:
  思想:
    从数列中挑出一个元素,称为 “基准”(pivot);

    重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;

    递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;


  程序:
//快速排序
//思想是每趟排序时选取一个数据(通常用数组的第一个数)作为关键数据,
//然后将所有比它小的数都放到它的左边,所有比它大的数都放到它的右边.


#include<iostream>
#include<algorithm>
using namespace std;
int Paritition1(int A[], int low, int high) 
{
    int pivot = A[low];
    while (low < high) 
    {
        while (low < high && A[high] >= pivot) 
        {
            --high;
        }
        A[low] = A[high];
        while (low < high && A[low] <= pivot) 
        {
            ++low;
        }
        A[high] = A[low];
    }
    A[low] = pivot;
    return low;
}

void QuickSort(int A[], int low, int high) //快排母函数
{
    if (low < high) 
    {
        int pivot = Paritition1(A, low, high);
        QuickSort(A, low, pivot - 1);
        QuickSort(A, pivot + 1, high);
    }
}
void test()
{
    int a[] = { 7,3 ,4,9,1,6,5,2,0 };
    int a_size;
    a_size = sizeof(a) / sizeof(int);
    QuickSort(a,0, 8);
    for (int i = 0; i < a_size; i++)
    {
        cout << a[i] << "  ";
    }
    cout << endl;
}
int main()
{
    test();
    return 0;
}
归并排序:
  思想:
    归并排序的思想是将两个有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。即先划分为两个部分,最后进行合并。


  程序:
//归并排序
//首先将初始序列的n个元素看成是n个有序的子序列,每个子序列的长度为1,
//然后两两归并,得到n/2个长度为2的有序子序列,在此基础上,再对长度为2
//的有序子序列进行两两归并,得到若干个长度为4的有序子序列,以此类推,直
//到得到一个长度为n的有序序列为止
#include<iostream>
#include<algorithm>
using namespace std;
void work1(int *ar, int size)
{
    int *a = ar;
    int *b = new int[size];
    for (int seg = 1; seg < size; seg += seg) 
    {
        for (int start = 0; start < size; start += seg + seg) 
        {
            int low = start, mid = min(start + seg, size), high = min(start + seg + seg, size);
            int k = low;
            int start1 = low, end1 = mid;
            int start2 = mid, end2 = high;
            while (start1 < end1 && start2 < end2)
                b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
            while (start1 < end1)
                b[k++] = a[start1++];
            while (start2 < end2)
                b[k++] = a[start2++];
        }
        int *temp = a;
        a = b;
        b = temp;
    }
    if (a != ar) 
    {
        for (int i = 0; i < size; i++)
            b[i] = a[i];
        b = a;
    }
    delete[] b;
}
void test()
{
    int a[] = { 17,3 ,42,39,10,6,64,72,48 };
    int a_size;
    a_size = sizeof(a) / sizeof(int);
    work1(a, a_size);
    for (int i = 0; i < a_size; i++)
    {
        cout << a[i] << "  ";
    }
    cout << endl;
}
int main()
{
    test();
    return 0;
}

//第一步:先将相邻的两个元素两两排序,
//第二步:将相邻的四个元素两两排序,
//第三步:将相邻的八个元素两两排序,
//            .........
//            .........
//依此类推


基数排序:
  思想:
    基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。


  程序:
//基数排序
//是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,
//然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)
//和特定格式的浮点数,所以基数排序也不是只能使用于整数。
#include <iostream>
#include <algorithm>
using namespace std;

int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
    int maxData = data[0];              ///< 最大数
    /// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。
    for (int i = 1; i < n; ++i)
    {
        if (maxData < data[i])
            maxData = data[i];
    }
    int d = 1;
    int p = 10;
    while (maxData >= p)
    {
        maxData /= 10;
        ++d;
    }
    return d;
}
void radixsort(int data[], int n) //基数排序
{
    int d = maxbit(data, n);
    int *tmp = new int[n];
    int *count = new int[10]; //计数器
    int i, j, k;
    int radix = 1;
    for (i = 1; i <= d; i++) //进行d次排序
    {
        for (j = 0; j < 10; j++)
            count[j] = 0; //每次分配前清空计数器
        for (j = 0; j < n; j++)
        {
            k = (data[j] / radix) % 10; //统计每个桶中的记录数
            count[k]++;
        }
        for (j = 1; j < 10; j++)
            count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
        for (j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
        {
            k = (data[j] / radix) % 10;
            tmp[count[k] - 1] = data[j];
            count[k]--;
        }
        for (j = 0; j < n; j++) //将临时数组的内容复制到data中
            data[j] = tmp[j];
        radix = radix * 10;
    }
    delete[]tmp;
    delete[]count;
}

int main() {
    int arr[] = { 67,9,7,8,5,3, 4, 0, 2, 6,72,39 };
    int len = (int) sizeof(arr) / sizeof(*arr);
    radixsort(arr, len);
    for (int i = 0; i < len; i++)
        cout << arr[i] << ' ';
    cout << endl;
    return 0;
}

排序方法有十种,分别是:一、冒泡排序;二、选择排序;三、插入排序;四、希尔排序;五、归并排序;六、快速排序;七、堆排序;八、计数排序;九、桶排序;十、基数排序。

一、冒泡排序

冒泡排序是排序算法中较为简单的一种,英文称为Bubble Sort。它遍历所有的数据,每次对相邻元素进行两两比较,如果顺序和预先规定的顺序不一致,则进行位置交换;这样一次遍历会将最大或最小的数据上浮到顶端,之后再重复同样的操作,直到所有的数据有序。

冒泡排序

思路分析:在要排序的一组数中,对当前还未排好的序列,从前往后对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。

冒泡排序的算法原理(从后往前):
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。

二、选择排序

选择排序简单直观,英文称为Selection Sort,先在数据中找出最大或最小的元素,放到序列的起始;然后再从余下的数据中继续寻找最大或最小的元素,依次放到排序序列中,直到所有数据样本排序完成。

选择排序
思路分析:选择排序和冒泡排序的思路类似,也是依次相邻的数进行两两比较,不同之处在于,他不是每一次比较就进行交换位置,而是一轮比较完毕,找到最大值或者最小值之后,将其放在合适的位置,其他数的位置不变,先假设下标为0的数是最大数或者最小数,将数组遍历一遍,寻找是否存在比假设的这个数还要大或者还要小的数,如果有,交换位置,如果没有,不交换位置,然后下标自增,继续循环之前的程序。

冒泡排序法和选择排序法的异同点:
冒泡算法,每次比较如果发现较小的元素在后面,就交换两个相邻的元素。而选择排序算法的改进在于:先并不急于调换位置,先从arr[0]开始逐个检查,看哪个数最小就记下该数所在的位置max,等一躺扫描完毕,再把arr[max]和arr[0]对调,这时arr[0]到arr[i]中最小的数据就换到了最前面的位置。


所以,选择排序每扫描一遍数组,只需要一次真正的交换,而冒泡可能需要很多次。比较的次数是一样的。

三、插入排序

插入排序英文称为Insertion Sort,它通过构建有序序列,对于未排序的数据序列,在已排序序列中从后向前扫描,找到相应的位置并插入,类似打扑克牌时的码牌。插入排序有一种优化的算法,可以进行拆半插入。

基本思路是先将待排序序列的第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列;然后从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置,直到所有数据都完成排序;如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。

思路分析:先把arr[1]对应的数字拿出来作为将要插入的数,和原数组的比他下标小的数做比较,如果满足条件,交换位置,否则原封不动,然后后几轮循环前面的步骤,最后输出这个数组。

四、希尔排序

希尔排序也称递减增量排序,是插入排序的一种改进版本,英文称为Shell Sort,效率虽高,但它是一种不稳定的排序算法。

插入排序在对几乎已经排好序的数据操作时,效果是非常好的;但是插入排序每次只能移动一位数据,因此插入排序效率比较低。

希尔排序在插入排序的基础上进行了改进,它的基本思路是先将整个数据序列分割成若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时,再对全部数据进行依次直接插入排序。

五、归并排序

归并排序英文称为Merge Sort,归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。它首先将数据样本拆分为两个子数据样本, 并分别对它们排序, 最后再将两个子数据样本合并在一起; 拆分后的两个子数据样本序列, 再继续递归的拆分为更小的子数据样本序列, 再分别进行排序, 直到最后数据序列为1,而不再拆分,此时即完成对数据样本的最终排序。

归并排序严格遵循从左到右或从右到左的顺序合并子数据序列, 它不会改变相同数据之间的相对顺序, 因此归并排序是一种稳定的排序算法.

作为一种典型的分而治之思想的算法应用,归并排序的实现分为两种方法:

1、自上而下的递归;

2、自下而上的迭代;

六、快速排序

快速排序,英文称为Quicksort,又称划分交换排序 partition-exchange sort 简称快排。

快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。首先从数列中挑出一个元素,并将这个元素称为「基准」,英文pivot。重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。之后,在子序列中继续重复这个方法,直到最后整个数据序列排序完成。

思路分析:

       1、先从数列中取出一个数作为基准数

       2、分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边

       3、再对左右区间重复第二步,直到各区间只有一个数

七、堆排序

堆排序,英文称Heapsort,是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序实现分为两种方法:

1、大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;

2、小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

算法步骤:

1、创建一个堆 H[0……n-1];

2、把堆首(最大值)和堆尾互换;

3、把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;

4、重复步骤 2,直到堆的尺寸为 1

八、计数排序

计数排序英文称Counting sort,是一种稳定的线性时间排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于 i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。基本的步骤如下:

1、找出待排序的数组中最大和最小的元素

2、统计数组中每个值为i的元素出现的次数,存入数组C的第i项

3、对所有的计数累加,从C中的第一个元素开始,每一项和前一项相加

4、反向填充目标数组,将每个元素i放在新数组的第C[i]项,每放一个元素就将C[i]减去1

九、桶排序

桶排序也称为箱排序,英文称为 Bucket Sort。它是将数组划分到一定数量的有序的桶里,然后再对每个桶中的数据进行排序,最后再将各个桶里的数据有序的合并到一起。

十、基数排序

基数排序英文称Radix sort,是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串和特定格式的浮点数,所以基数排序也仅限于整数。它首先将所有待比较数值,统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

选择排序快速排序希尔排序堆排序不是稳定的排序算法,
冒泡排序插入排序归并排序基数排序是稳定的排序算法。(推荐)

冒泡法: 
这是最原始,也是众所周知的最慢的算法了。他的名字的由来因为它的工作看来象是冒泡:  复杂度为O(n*n)。当数据为正序,将不会有交换。复杂度为O(0)。

直接插入排序:O(n*n)

选择排序:O(n*n)

快速排序:平均时间复杂度log2(n)*n,所有内部排序方法中最高好的,大多数情况下总是最好的。

归并排序:log2(n)*n

堆排序:log2(n)*n

希尔排序:算法的复杂度为n的1.2次幂
这里我没有给出行为的分析,因为这个很简单,我们直接来分析算法:

首先我们考虑最理想的情况


1.数组的大小是2的幂,这样分下去始终可以被2整除。假设为2的k次方,即k=log2(n)。
2.每次我们选择的值刚好是中间值,这样,数组才可以被等分。
第一层递归,循环n次,第二层循环2*(n/2)......
所以共有n+2(n/2)+4(n/4)+...+n*(n/n) = n+n+n+...+n=k*n=log2(n)*n
所以算法复杂度为O(log2(n)*n)
其他的情况只会比这种情况差,最差的情况是每次选择到的middle都是最小值或最大值,那么他将变成交换法(由于使用了递归,情况更糟)。但是你认为这种情况发生的几率有多大??呵呵,你完全不必担心这个问题。实践证明,大多数的情况,快速排序总是最好的。
如果你担心这个问题,你可以使用堆排序,这是一种稳定的O(log2(n)*n)算法,但是通常情况下速度要慢 于快速排序(因为要重组堆)。
这几天笔试了好几次了,连续碰到一个关于常见排序算法稳定性判别的问题,往往还是多选,对于我以及和我一样拿不准的同学可不是一个能轻易下结论的题目,当然如果你笔试之前已经记住了数据结构书上哪些是稳定的,哪些不是稳定的,做起来应该可以轻松搞定。

本文是针对老是记不住这个或者想真正明白到底为什么是稳定或者不稳定的人准备的。

      首先,排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj, Ai原来在位置前,排序后Ai还是要在Aj位置前。

     其次,说一下稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,对基于比较的排序算法而言,元素交换的次数可能会少一些(个人感觉,没有证实)。

回到主题,现在分析一下常见的排序算法的稳定性,每个都给出简单的理由。

(1)冒泡排序

        冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。

(2)选择排序

      选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9, 我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。

(3)插入排序


     插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

(4)快速排序


    快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j]交换的时刻。

(5)归并排序


    归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。

(6)基数排序


   基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。

(7)希尔排序(shell)


    希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。

(8)堆排序


   我们知道堆的结构是节点i的孩子为2*i和2*i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n/2-1, n/2-2, ...1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法

1 快速排序(QuickSort)

快速排序是一个就地排序,分而治之,大规模递归的算法。从本质上来说,它是归并排序的就地版本。快速排序可以由下面四步组成。

(1) 如果不多于1个数据,直接返回。
(2) 一般选择序列最左边的值作为支点数据。
(3) 将序列分成2部分,一部分都大于支点数据,另外一部分都小于支点数据。
(4) 对两边利用递归排序数列。

快速排序比大部分排序算法都要快。尽管我们可以在某些特殊的情况下写出比快速排序快的算法,但是就通常情况而言,没有比它更快的了。快速排序是递归的,对于内存非常有限的机器来说,它不是一个好的选择。

2 归并排序(MergeSort)

归并排序先分解要排序的序列,从1分成2,2分成4,依次分解,当分解到只有1个一组的时候,就可以排序这些分组,然后依次合并回原来的序列中,这样就可以排序所有数据。合并排序比堆排序稍微快一点,但是需要比堆排序多一倍的内存空间,因为它需要一个额外的数组。

3 堆排序(HeapSort)

堆排序适合于数据量非常大的场合(百万数据)。

堆排序不需要大量的递归或者多维的暂存数组。这对于数据量非常巨大的序列是合适的。比如超过数百万条记录,因为快速排序,归并排序都使用递归来设计算法,在数据量非常大的时候,可能会发生堆栈溢出错误。

堆排序会将所有的数据建成一个堆,最大的数据在堆顶,然后将堆顶数据和序列的最后一个数据交换。接下来再次重建堆,交换数据,依次下去,就可以排序所有的数据。

4 Shell排序(ShellSort)

Shell排序通过将数据分成不同的组,先对每一组进行排序,然后再对所有的元素进行一次插入排序,以减少数据交换和移动的次数。平均效率是O(nlogn)。其中分组的合理性会对算法产生重要的影响。现在多用D.E.Knuth的分组方法。

Shell排序比冒泡排序快5倍,比插入排序大致快2倍。Shell排序比起QuickSort,MergeSort,HeapSort慢很多。但是它相对比较简单,它适合于数据量在5000以下并且速度并不是特别重要的场合。它对于数据量较小的数列重复排序是非常好的。

5 插入排序(InsertSort)

插入排序通过把序列中的值插入一个已经排序好的序列中,直到该序列的结束。插入排序是对冒泡排序的改进。它比冒泡排序快2倍。一般不用在数据大于1000的场合下使用插入排序,或者重复排序超过200数据项的序列。

6 冒泡排序(BubbleSort)

冒泡排序是最慢的排序算法。在实际运用中它是效率最低的算法。它通过一趟又一趟地比较数组中的每一个元素,使较大的数据下沉,较小的数据上升。它是O(n^2)的算法。

7 交换排序(ExchangeSort)和选择排序(SelectSort)

这两种排序方法都是交换方法的排序算法,效率都是 O(n2)。在实际应用中处于和冒泡排序基本相同的地位。它们只是排序算法发展的初级阶段,在实际中使用较少。

8 基数排序(RadixSort)

基数排序和通常的排序算法并不走同样的路线。它是一种比较新颖的算法,但是它只能用于整数的排序,如果我们要把同样的办法运用到浮点数上,我们必须了解浮点数的存储格式,并通过特殊的方式将浮点数映射到整数上,然后再映射回去,这是非常麻烦的事情,因此,它的使用同样也不多。而且,最重要的是,这样算法也需要较多的存储空间。

9 总结

下面是一个总的表格,大致总结了我们常见的所有的排序算法的特点。

排序法 平均时间 最差情形 稳定度 额外空间 备注
冒泡 O(n2)     O(n2) 稳定 O(1) n小时较好
交换     O(n2)     O(n2) 不稳定 O(1) n小时较好
选择 O(n2) O(n2) 不稳定 O(1) n小时较好
插入 O(n2) O(n2) 稳定 O(1) 大部分已排序时较好
基数 O(logRB) O(logRB) 稳定 O(n) B是真数(0-9),

R是基数(个十百)
Shell O(nlogn) O(ns) 1<s<2 不稳定 O(1) s是所选分组
快速 O(nlogn) O(n2) 不稳定 O(nlogn) n大时较好
归并 O(nlogn) O(nlogn) 稳定 O(1) n大时较好
堆 O(nlogn) O(nlogn) 不稳定 O(1) n大时较好

概述

排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。

我们这里说说八大排序就是内部排序。

当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。

   快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;


 

1.插入排序—直接插入排序(Straight Insertion Sort)
基本思想:

将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表。即:先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。

要点:设立哨兵,作为临时存储和判断数组边界之用。

直接插入排序示例:

如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

算法的实现:

void print(int a[], int n ,int i){
    cout<<i <<":";
    for(int j= 0; j<8; j++){
        cout<<a[j] <<" ";
    }
    cout<<endl;
}
 
 
void InsertSort(int a[], int n)
{
    for(int i= 1; i<n; i++){
        if(a[i] < a[i-1]){               //若第i个元素大于i-1元素,直接插入。小于的话,移动有序表后插入
            int j= i-1;    
            int x = a[i];         //复制为哨兵,即存储待排序元素
            a[i] = a[i-1];           //先后移一个元素
            while(x < a[j]){     //查找在有序表的插入位置
                a[j+1] = a[j];
                j--;         //元素后移
            }
            a[j+1] = x;         //插入到正确位置
        }
        print(a,n,i);            //打印每趟排序的结果
    }
    
}
 
int main(){
    int a[8] = {3,1,5,7,2,4,9,6};
    InsertSort(a,8);
    print(a,8,8);
}
 

效率:

时间复杂度:O(n^2).

其他的插入排序有二分插入排序,2-路插入排序。

 2. 插入排序—希尔排序(Shell`s Sort)
希尔排序是1959 年由D.L.Shell 提出来的,相对直接排序有较大的改进。希尔排序又叫缩小增量排序

基本思想:

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。

操作方法:

选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
按增量序列个数k,对序列进行k 趟排序;
每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
希尔排序的示例:shell排序的排序过程

假设待排序文件有10个记录,其关键字分别是:49,38,65,97,76,13,27,49,55,04。

增量系列的取值依次为:5,3,1

算法实现:

我们简单处理增量序列:增量序列d = {n/2 ,n/4, n/8 .....1} n为要排序数的个数

即:先将要排序的一组记录按某个增量d(n/2,n为要排序数的个数)分成若干组子序列,每组中记录的下标相差d.对每组中全部元素进行直接插入排序,然后再用一个较小的增量(d/2)对它进行分组,在每组中再进行直接插入排序。继续不断缩小增量直至为1,最后使用直接插入排序完成排序。

void print(int a[], int n ,int i){
    cout<<i <<":";
    for(int j= 0; j<8; j++){
        cout<<a[j] <<" ";
    }
    cout<<endl;
}
/**
 * 直接插入排序的一般形式
 *
 * @param int dk 缩小增量,如果是直接插入排序,dk=1
 *
 */
 
void ShellInsertSort(int a[], int n, int dk)
{
    for(int i= dk; i<n; ++i){
        if(a[i] < a[i-dk]){            //若第i个元素大于i-1元素,直接插入。小于的话,移动有序表后插入
            int j = i-dk;    
            int x = a[i];            //复制为哨兵,即存储待排序元素
            a[i] = a[i-dk];            //首先后移一个元素
            while(x < a[j]){        //查找在有序表的插入位置
                a[j+dk] = a[j];
                j -= dk;             //元素后移
            }
            a[j+dk] = x;            //插入到正确位置
        }
        print(a, n,i );
    }
    
}
 
/**
 * 先按增量d(n/2,n为要排序数的个数进行希尔排序
 *
 */
void shellSort(int a[], int n){
 
    int dk = n/2;
    while( dk >= 1  ){
        ShellInsertSort(a, n, dk);
        dk = dk/2;
    }
}
int main(){
    int a[8] = {3,1,5,7,2,4,9,6};
    //ShellInsertSort(a,8,1); //直接插入排序
    shellSort(a,8);              //希尔插入排序
    print(a,8,8);
}

希尔排序时效分析很难,关键码的比较次数与记录移动次数依赖于增量因子序列d的选取,特定情况下可以准确估算出关键码的比较次数和记录的移动次数。目前还没有人给出选取最好的增量因子序列的方法。增量因子序列可以有各种取法,有取奇数的,也有取质数的,但需要注意:增量因子中除1 外没有公因子,且最后一个增量因子必须为1。希尔排序方法是一个不稳定的排序方法。

3. 选择排序—简单选择排序(Simple Selection Sort)
基本思想:

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

简单选择排序的示例:

操作方法:

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

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

以此类推.....

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

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

算法实现:

void print(int a[], int n ,int i){
    cout<<"第"<<i+1 <<"趟 : ";
    for(int j= 0; j<8; j++){
        cout<<a[j] <<"  ";
    }
    cout<<endl;
}
/**
 * 数组的最小值
 *
 * @return int 数组的键值
 */
int SelectMinKey(int a[], int n, int i)
{
    int k = i;
    for(int j=i+1 ;j< n; ++j) {
        if(a[k] > a[j]) k = j;
    }
    return k;
}
 
/**
 * 选择排序
 *
 */
void selectSort(int a[], int n){
    int key, tmp;
    for(int i = 0; i< n; ++i) {
        key = SelectMinKey(a, n,i);           //选择最小的元素
        if(key != i){
            tmp = a[i];  a[i] = a[key]; a[key] = tmp; //最小元素与第i位置元素互换
        }
        print(a,  n , i);
    }
}
int main(){
    int a[8] = {3,1,5,7,2,4,9,6};
    cout<<"初始值:";
    for(int j= 0; j<8; j++){
        cout<<a[j] <<"  ";
    }
    cout<<endl<<endl;
    selectSort(a, 8);
    print(a,8,8);
}

 简单选择排序的改进——二元选择排序

简单选择排序,每趟循环只能确定一个元素排序后的定位。我们可以考虑改进为每趟循环确定两个元素(当前趟最大和最小记录)的位置,从而减少排序所需的循环次数。改进后对n个数据进行排序,最多只需进行[n/2]趟循环即可。具体实现如下:

/** 这是伪函数, 逻辑判断不严谨
void selectSort(int r[],int n) {
    int i ,j , min ,max, tmp;
    for (i=1 ;i <= n/2;i++) {  
        // 做不超过n/2趟选择排序 
        min = i; max = i ; //分别记录最大和最小关键字记录位置
        for (j= i+1; j<= n-i; j++) {
            if (r[j] > r[max]) { 
                max = j ; continue ; 
            }  
            if (r[j]< r[min]) { 
                min = j ; 
            }   
      }  
      //该交换操作还可分情况讨论以提高效率
      tmp = r[i-1]; r[i-1] = r[min]; r[min] = tmp;
      tmp = r[n-i]; r[n-i] = r[max]; r[max] = tmp; 
 
    } 
}
 */
void selectSort(int a[],int len) {
        int i,j,min,max,tmp;  
        for(i=0; i<len/2; i++){  // 做不超过n/2趟选择排序 
            min = max = i;  
            for(j=i+1; j<=len-1-i; j++){  
                //分别记录最大和最小关键字记录位置
                if(a[j] > a[max]){  
                    max = j;  
                    continue;  
                }  
                if(a[j] < a[min]){  
                    min = j;  
                }  
            }  
            //该交换操作还可分情况讨论以提高效率
            if(min != i){//当第一个为min值,不用交换  
                tmp=a[min];  a[min]=a[i];  a[i]=tmp;  
            }  
            if(min == len-1-i && max == i)//当第一个为max值,同时最后一个为min值,不再需要下面操作  
                continue;  
            if(max == i)//当第一个为max值,则交换后min的位置为max值  
                max = min;  
            if(max != len-1-i){//当最后一个为max值,不用交换  
                tmp=a[max];  a[max]=a[len-1-i];  a[len-1-i]=tmp;  
            }
            print(a,len, i);            
        }  
 }

4. 选择排序—堆排序(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 print(int a[], int n){
    for(int j= 0; j<n; j++){
        cout<<a[j] <<"  ";
    }
    cout<<endl;
}
 
 
 
/**
 * 已知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; //左孩子结点的位置。(i+1 为当前调整结点的右孩子结点的位置)
    while (child < length) {
        if(child+1 <length && H[child]<H[child+1]) { // 如果右孩子大于左孩子(找到比当前待调整结点大的孩子结点)
            ++child ;
        }
        if(H[s]<H[child]) {  // 如果较大的子结点大于父结点
            H[s] = H[child]; // 那么把较大的子结点往上移动,替换它的父结点
            s = child;         // 重新设置s ,即待调整的下一个结点的位置
            child = 2*s+1;
        }  else {             // 如果当前待调整结点大于它的左右孩子,则不需要调整,直接退出
             break;
        }
        H[s] = tmp;            // 当前待调整的结点放到比其大的孩子结点位置上
    }
    print(H,length);
}
 
 
/**
 * 初始堆进行调整
 * 将H[0..length-1]建成堆
 * 调整完之后第一个元素是序列的最小的元素
 */
void BuildingHeap(int H[], int length)

    //最后一个有孩子的节点的位置 i=  (length -1) / 2
    for (int i = (length -1) / 2 ; i >= 0; --i)
        HeapAdjust(H,i,length);
}
/**
 * 堆排序算法
 */
void HeapSort(int H[],int length)
{
    //初始堆
    BuildingHeap(H, length);
    //从最后一个元素开始对序列进行调整
    for (int i = length - 1; i > 0; --i)
    {
        //交换堆顶元素H[0]和堆中最后一个元素
        int temp = H[i]; H[i] = H[0]; H[0] = temp;
        //每次交换堆顶元素和堆中最后一个元素之后,都要对堆进行调整
        HeapAdjust(H,0,i);
  }

 
int main(){
    int H[10] = {3,1,5,7,2,4,9,6,10,8};
    cout<<"初始值:";
    print(H,10);
    HeapSort(H,10);
    //selectSort(a, 8);
    cout<<"结果:";
    print(H,10);
 
}
 

分析:

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

                                

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

5. 交换排序—冒泡排序(Bubble Sort)
基本思想:

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

冒泡排序的示例:

算法的实现:

void bubbleSort(int a[], int n){
    for(int i =0 ; i< n-1; ++i) {
        for(int j = 0; j < n-i-1; ++j) {
            if(a[j] > a[j+1])
            {
                int tmp = a[j] ; a[j] = a[j+1] ;  a[j+1] = tmp;
            }
        }
    }
}
 

冒泡排序算法的改进

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

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

改进后算法如下:

void Bubble_1 ( int r[], int n) {
    int i= n -1;  //初始时,最后位置保持不变
    while ( i> 0) { 
        int pos= 0; //每趟开始时,无记录交换
        for (int j= 0; j< i; j++)
            if (r[j]> r[j+1]) {
                pos= j; //记录交换的位置 
                int tmp = r[j]; r[j]=r[j+1];r[j+1]=tmp;
            } 
        i= pos; //为下一趟排序作准备
     } 
}  
 

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

改进后的算法实现为:

void Bubble_2 ( int r[], int n){
    int low = 0; 
    int high= n -1; //设置变量的初始值
    int tmp,j;
    while (low < high) {
        for (j= low; j< high; ++j) //正向冒泡,找到最大者
            if (r[j]> r[j+1]) {
                tmp = r[j]; r[j]=r[j+1];r[j+1]=tmp;
            } 
        --high;                    //修改high值, 前移一位
        for ( j=high; j>low; --j) //反向冒泡,找到最小者
            if (r[j]<r[j-1]) {
                tmp = r[j]; r[j]=r[j-1];r[j-1]=tmp;
            }
        ++low;                    //修改low值,后移一位
    } 

6. 交换排序—快速排序(Quick Sort)
基本思想:

1)选择一个基准元素,通常选择第一个元素或者最后一个元素,

2)通过一趟排序讲待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的 元素值比基准值大。

3)此时基准元素在其排好序后的正确位置

4)然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。

快速排序的示例:

(a)一趟排序的过程:

(b)排序的全过程

算法的实现:

 递归实现:

void print(int a[], int n){
    for(int j= 0; j<n; j++){
        cout<<a[j] <<"  ";
    }
    cout<<endl;
}
 
void swap(int *a, int *b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}
 
int partition(int a[], int low, int high)
{
    int privotKey = a[low];                                //基准元素
    while(low < high){                                    //从表的两端交替地向中间扫描
        while(low < high  && a[high] >= privotKey) --high;  //从high 所指位置向前搜索,至多到low+1 位置。将比基准元素小的交换到低端
        swap(&a[low], &a[high]);
        while(low < high  && a[low] <= privotKey ) ++low;
        swap(&a[low], &a[high]);
    }
    print(a,10);
    return low;
}
 
 
void quickSort(int a[], int low, int high){
    if(low < high){
        int privotLoc = partition(a,  low,  high);  //将表一分为二
        quickSort(a,  low,  privotLoc -1);            //递归对低子表递归排序
        quickSort(a,   privotLoc + 1, high);        //递归对高子表递归排序
    }
}
 
int main(){
    int a[10] = {3,1,5,7,2,4,9,6,10,8};
    cout<<"初始值:";
    print(a,10);
    quickSort(a,0,9);
    cout<<"结果:";
    print(a,10);
 
}

分析:

快速排序是通常被认为在同数量级(O(nlog2n))的排序方法中平均性能最好的。但若初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。为改进之,通常以“三者取中法”来选取基准记录,即将排序区间的两个端点与中点三个记录关键码居中的调整为支点记录。快速排序是一个不稳定的排序方法。


快速排序的改进

在本改进算法中,只对长度大于k的子序列递归调用快速排序,让原序列基本有序,然后再对整个基本有序序列用插入排序算法排序。实践证明,改进后的算法时间复杂度有所降低,且当k取值为 8 左右时,改进算法的性能最佳。算法思想如下:

void print(int a[], int n){
    for(int j= 0; j<n; j++){
        cout<<a[j] <<"  ";
    }
    cout<<endl;
}
 
void swap(int *a, int *b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}
 
int partition(int a[], int low, int high)
{
    int privotKey = a[low];                    //基准元素
    while(low < high){                    //从表的两端交替地向中间扫描
        while(low < high  && a[high] >= privotKey) --high; //从high 所指位置向前搜索,至多到low+1 位置。将比基准元素小的交换到低端
        swap(&a[low], &a[high]);
        while(low < high  && a[low] <= privotKey ) ++low;
        swap(&a[low], &a[high]);
    }
    print(a,10);
    return low;
}
 
 
void qsort_improve(int r[ ],int low,int high, int k){
    if( high -low > k ) { //长度大于k时递归, k为指定的数
        int pivot = partition(r, low, high); // 调用的Partition算法保持不变
        qsort_improve(r, low, pivot - 1,k);
        qsort_improve(r, pivot + 1, high,k);
    } 

void quickSort(int r[], int n, int k){
    qsort_improve(r,0,n,k);//先调用改进算法Qsort使之基本有序
 
    //再用插入排序对基本有序序列排序
    for(int i=1; i<=n;i ++){
        int tmp = r[i]; 
        int j=i-1;
        while(tmp < r[j]){
            r[j+1]=r[j]; j=j-1; 
        }
        r[j+1] = tmp;
    } 
 

 
 
 
int main(){
    int a[10] = {3,1,5,7,2,4,9,6,10,8};
    cout<<"初始值:";
    print(a,10);
    quickSort(a,9,4);
    cout<<"结果:";
    print(a,10);
 
}

7. 归并排序(Merge Sort)
基本思想:

归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

归并排序示例:

合并方法:

设r[i…n]由两个有序子表r[i…m]和r[m+1…n]组成,两个子表长度分别为n-i +1、n-m。

j=m+1;k=i;i=i; //置两个子表的起始下标及辅助数组的起始下标
若i>m 或j>n,转⑷ //其中一个子表已合并完,比较选取结束
//选取r[i]和r[j]较小的存入辅助数组rf
如果r[i]<r[j],rf[k]=r[i]; i++; k++; 转⑵
否则,rf[k]=r[j]; j++; k++; 转⑵
//将尚未处理完的子表中元素存入rf
如果i<=m,将r[i…m]存入rf[k…n] //前一子表非空
如果j<=n ,  将r[j…n] 存入rf[k…n] //后一子表非空
合并结束。
//将r[i…m]和r[m +1 …n]归并到辅助数组rf[i…n]
void Merge(ElemType *r,ElemType *rf, int i, int m, int n)
{
    int j,k;
    for(j=m+1,k=i; i<=m && j <=n ; ++k){
        if(r[j] < r[i]) rf[k] = r[j++];
        else rf[k] = r[i++];
    }
    while(i <= m)  rf[k++] = r[i++];
    while(j <= n)  rf[k++] = r[j++];
}
 

归并的迭代算法

1 个元素的表总是有序的。所以对n 个元素的待排序列,每个元素可看成1 个有序子表。对子表两两合并生成n/2个子表,所得子表除最后一个子表长度可能为1 外,其余子表长度均为2。再进行两两合并,直到生成n 个元素按关键码有序的表。

void print(int a[], int n){
    for(int j= 0; j<n; j++){
        cout<<a[j] <<"  ";
    }
    cout<<endl;
}
 
//将r[i…m]和r[m +1 …n]归并到辅助数组rf[i…n]
void Merge(ElemType *r,ElemType *rf, int i, int m, int n)
{
    int j,k;
    for(j=m+1,k=i; i<=m && j <=n ; ++k){
        if(r[j] < r[i]) rf[k] = r[j++];
        else rf[k] = r[i++];
    }
    while(i <= m)  rf[k++] = r[i++];
    while(j <= n)  rf[k++] = r[j++];
    print(rf,n+1);
}
 
void MergeSort(ElemType *r, ElemType *rf, int lenght)

    int len = 1;
    ElemType *q = r ;
    ElemType *tmp ;
    while(len < lenght) {
        int s = len;
        len = 2 * s ;
        int i = 0;
        while(i+ len <lenght){
            Merge(q, rf,  i, i+ s-1, i+ len-1 ); //对等长的两个子表合并
            i = i+ len;
        }
        if(i + s < lenght){
            Merge(q, rf,  i, i+ s -1, lenght -1); //对不等长的两个子表合并
        }
        tmp = q; q = rf; rf = tmp; //交换q,rf,以保证下一趟归并时,仍从q 归并到rf
    }
}
 
 
int main(){
    int a[10] = {3,1,5,7,2,4,9,6,10,8};
    int b[10];
    MergeSort(a, b, 10);
    print(b,10);
    cout<<"结果:";
    print(a,10);
 
}

两路归并的递归算法

void MSort(ElemType *r, ElemType *rf,int s, int t)

    ElemType *rf2;
    if(s==t) r[s] = rf[s];
    else
    { 
        int m=(s+t)/2;            /*平分*p 表*/
        MSort(r, rf2, s, m);        /*递归地将p[s…m]归并为有序的p2[s…m]*/
        MSort(r, rf2, m+1, t);        /*递归地将p[m+1…t]归并为有序的p2[m+1…t]*/
        Merge(rf2, rf, s, m+1,t);    /*将p2[s…m]和p2[m+1…t]归并到p1[s…t]*/
    }
}
void MergeSort_recursive(ElemType *r, ElemType *rf, int n)
{   /*对顺序表*p 作归并排序*/
    MSort(r, rf,0, n-1);
}

8. 桶排序/基数排序(Radix Sort)
说基数排序之前,我们先说桶排序:

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

 例如要对大小为[1..1000]范围内的n个整数A[1..n]排序  

 首先,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1..10]的整数,集合B[2]存储   (10..20]的整数,……集合B[i]存储(   (i-1)*10,   i*10]的整数,i   =   1,2,..100。总共有  100个桶。  

  然后,对A[1..n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。  再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任  何排序法都可以。

  最后,依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这  样就得到所有数字排好序的一个序列了。  

  假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。如果  

  对每个桶中的数字采用快速排序,那么整个算法的复杂度是  

  O(n   +   m   *   n/m*log(n/m))   =   O(n   +   nlogn   -   nlogm)  

  从上式看出,当m接近n的时候,桶排序复杂度接近O(n)  

  当然,以上复杂度的计算是基于输入的n个数字是平均分布这个假设的。这个假设是很强的  ,实际应用中效果并没有这么好。如果所有的数字都落在同一个桶中,那就退化成一般的排序了。  

        前面说的几大排序算法 ,大部分时间复杂度都是O(n2),也有部分排序算法时间复杂度是O(nlogn)。而桶式排序却能实现O(n)的时间复杂度。但桶排序的缺点是:

        1)首先是空间复杂度比较高,需要的额外开销大。排序有两个数组的空间开销,一个存放待排序数组,一个就是所谓的桶,比如待排序值是从0到m-1,那就需要m个桶,这个桶数组就要至少m个空间。

        2)其次待排序的元素都要在一定的范围内等等。

       桶式排序是一种分配排序。分配排序的特定是不需要进行关键码的比较,但前提是要知道待排序列的一些具体情况。

分配排序的基本思想:说白了就是进行多次的桶式排序。

基数排序过程无须比较关键字,而是通过“分配”和“收集”过程来实现排序。它们的时间复杂度可达到线性阶:O(n)。

实例:

扑克牌中52 张牌,可按花色和面值分成两个字段,其大小关系为:
花色: 梅花< 方块< 红心< 黑心  
面值: 2 < 3 < 4 < 5 < 6 < 7 < 8 < 9 < 10 < J < Q < K < A

若对扑克牌按花色、面值进行升序排序,得到如下序列:


即两张牌,若花色不同,不论面值怎样,花色低的那张牌小于花色高的,只有在同花色情况下,大小关系才由面值的大小确定。这就是多关键码排序。

为得到排序结果,我们讨论两种排序方法。
方法1:先对花色排序,将其分为4 个组,即梅花组、方块组、红心组、黑心组。再对每个组分别按面值进行排序,最后,将4 个组连接起来即可。
方法2:先按13 个面值给出13 个编号组(2 号,3 号,...,A 号),将牌按面值依次放入对应的编号组,分成13 堆。再按花色给出4 个编号组(梅花、方块、红心、黑心),将2号组中牌取出分别放入对应花色组,再将3 号组中牌取出分别放入对应花色组,……,这样,4 个花色组中均按面值有序,然后,将4 个花色组依次连接起来即可。

设n 个元素的待排序列包含d 个关键码{k1,k2,…,kd},则称序列对关键码{k1,k2,…,kd}有序是指:对于序列中任两个记录r[i]和r[j](1≤i≤j≤n)都满足下列有序关系:

                                                               

其中k1 称为最主位关键码,kd 称为最次位关键码     。

两种多关键码排序方法:

多关键码排序按照从最主位关键码到最次位关键码或从最次位到最主位关键码的顺序逐次排序,分两种方法:

最高位优先(Most Significant Digit first)法,简称MSD 法:

1)先按k1 排序分组,将序列分成若干子序列,同一组序列的记录中,关键码k1 相等。

2)再对各组按k2 排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码kd 对各子组排序后。

3)再将各组连接起来,便得到一个有序序列。扑克牌按花色、面值排序中介绍的方法一即是MSD 法。

最低位优先(Least Significant Digit first)法,简称LSD 法:

1) 先从kd 开始排序,再对kd-1进行排序,依次重复,直到按k1排序分组分成最小的子序列后。

2) 最后将各个子序列连接起来,便可得到一个有序的序列, 扑克牌按花色、面值排序中介绍的方法二即是LSD 法。

基于LSD方法的链式基数排序的基本思想

  “多关键字排序”的思想实现“单关键字排序”。对数字型或字符型的单关键字,可以看作由多个数位或多个字符构成的多关键字,此时可以采用“分配-收集”的方法进行排序,这一过程称作基数排序法,其中每个数字或字符可能的取值个数称为基数。比如,扑克牌的花色基数为4,面值基数为13。在整理扑克牌时,既可以先按花色整理,也可以先按面值整理。按花色整理时,先按红、黑、方、花的顺序分成4摞(分配),再按此顺序再叠放在一起(收集),然后按面值的顺序分成13摞(分配),再按此顺序叠放在一起(收集),如此进行二次分配和收集即可将扑克牌排列有序。   

基数排序:

是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。

算法实现:

Void RadixSort(Node L[],length,maxradix)
{
   int m,n,k,lsp;
   k=1;m=1;
   int temp[10][length-1];
   Empty(temp); //清空临时空间
   while(k<maxradix) //遍历所有关键字
   {
     for(int i=0;i<length;i++) //分配过程
    {
       if(L[i]<m)
          Temp[0][n]=L[i];
       else
          Lsp=(L[i]/m)%10; //确定关键字
       Temp[lsp][n]=L[i];
       n++;
   }
   CollectElement(L,Temp); //收集
   n=0;
   m=m*10;
  k++;
 }
}

总结
各种排序的稳定性,时间复杂度和空间复杂度总结:

 我们比较时间复杂度函数的情况:

                             时间复杂度函数O(n)的增长情况

所以对n较大的排序记录。一般的选择都是时间复杂度为O(nlog2n)的排序方法。

时间复杂度来说:

(1)平方阶(O(n2))排序
  各类简单排序:直接插入、直接选择和冒泡排序;
 (2)线性对数阶(O(nlog2n))排序
  快速排序、堆排序和归并排序;
 (3)O(n1+§))排序,§是介于0和1之间的常数。

       希尔排序
(4)线性阶(O(n))排序
  基数排序,此外还有桶、箱排序。

说明:

当原表有序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n);

而快速排序则相反,当原表基本有序时,将蜕化为冒泡排序,时间复杂度提高为O(n2);

原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。

稳定性:

排序算法的稳定性:若待排序的序列中,存在多个具有相同关键字的记录,经过排序, 这些记录的相对次序保持不变,则称该算法是稳定的;若经排序后,记录的相对 次序发生了改变,则称该算法是不稳定的。 
     稳定性的好处:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,可以避免多余的比较;

稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序

不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序

选择排序算法准则:

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

选择排序算法的依据

影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:

1.待排序的记录数目n的大小;

2.记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;

3.关键字的结构及其分布情况;

4.对排序稳定性的要求。

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

1.当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。

   快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
       堆排序 :  如果内存空间允许且要求稳定性的,

       归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。

2. 当n较大,内存空间允许,且要求稳定性 =》归并排序

3.当n较小,可采用直接插入或直接选择排序。

    直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。

    直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序

5.一般不使用或不直接使用传统的冒泡排序。

6.基数排序
它是一种稳定的排序算法,但有一定的局限性:
  1、关键字可分解。
  2、记录的关键字位数较少,如果密集更好
  3、如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。

不同的排序方法适用于不同的数据结构排序需求。以下是几种常见的排序方法: 1. 堆排序(Heapsort) - 堆排序是一种选择排序算法,使用堆这种数据结构进行排序。堆是一种完全二叉树,且满足父节点的值大于等于(或小于等于)其子节点的值。在堆排序中,首先将待排序的数据构建成一个大顶堆(或小顶堆),然后将堆顶元素与最后一个元素交换,然后对剩下的元素进行调整,重复这个过程直到所有元素都有序。堆排序的时间复杂度为O(nlogn)。 2. 希尔排序(Shell Sort) - 希尔排序是直接插入排序的一种改进版本,它通过将待排序的数据分成若干个子序列,分别进行插入排序,最后再进行一次整体的插入排序。希尔排序的特点是通过减小增量的方式,使得每一轮的排序数据规模减小,从而提高了排序的效率。希尔排序的时间复杂度取决于增量序列的选取,一般情况下为O(nlogn)。 3. 归并排序(Merge Sort) - 归并排序是一种基于分治法的排序算法,它将待排序的数据分成两个子序列,分别进行排序,然后将排序好的子序列合并成一个有序序列。归并排序的核心思想是将大问题不断划分为小问题,然后通过合并解决小问题,最终得到整个问题的解。归并排序的时间复杂度为O(nlogn)。 这些排序方法各有优缺点,适用于不同的场景和数据结构。选择合适的排序方法可以提高排序的效率和性能。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值