八大排序算法

前言

不管是提高自身的能力,还是面试,八大排序都是很重要的一个知识点,所以理解并实践实现是很有必要的,以下给出算法思想与代码实现,并且进行运行时间测试八大排序的效率。

时间复杂度对比图

先看下对于算法时间复杂度的对比: 
这里写图片描述


冒泡排序

冒泡排序是一种简单的排序算法。主要思想是顺序的比较相邻的两个数,如果符合比较条件就替换两个数,做法可以从后往前推,也可以从前往后推,每次推出当前长度的最大值,直至最后便可以完整的排序。

时间复杂度为 : O(n*n);

#include
#include 
#include 
#define N 10000
#define MAX 100000000
//冒泡
void bubbleSort(int arr[],int n){
   int temp;
   for(int i=0;i < n-1;++i){

    for(int j=n-1;j>i;--j){
        if(arr[j](end_time-start_time)/CLOCKS_PER_SEC*1000);
    printf("%s",isSorted(arr,N) == true ? "true":"false");
    printf("\n");
    return 0;
}

用10000个数进行测试: 
这里写图片描述
运行时间为279ms 
不要觉得很快这才一万的数据,再加两个0,就听几首歌慢慢等吧 
冒泡有改进的方法是如果在比较相邻数的过程中都没交换过一次,说明后面的数已经排好序,可以直接跳出循环。


选择排序

思想:设当前位置为i,每次从i+1到n的数中选择最大(从大到小)或最小(从小到大)后,与位置i的值进行条件比较,符合条件则调换。

**时间复杂度为:**O(n*n)

#include
#include 
#include 
#define N 10000
#define MAX 100000000

//选择排序
void chioseSort(int arr[],int n){
   int temp,index;
   for(int i=0;i < n-1;++i){
        index = i;
        for(int j=i+1;j(end_time-start_time)/CLOCKS_PER_SEC*1000);
    printf("%s",isSorted(arr,N) == true ? "true":"false");
    printf("\n");
    return 0;
}

用10000个数进行测试: 
这里写图片描述
运行时间为141ms 
可以发现比冒泡速度更快点,主要原因是选择不需要每次都进行替换,只在最后一次再对比替换。


直接插入排序

思想:设当前位置为i,记录当前i的值为value,每次从i到0推,在推的过程中比较value的值,符合比较条件的值向后移,直至不符合条件时推结束,然后再将之前记录的value赋给不符和条件索引下标的后一个。 
动画演示: 
这里写图片描述

**时间复杂度:**O(n*n)

#include
#include 
#include 
#define N 10000
#define MAX 100000000
//插入排序
void insertSort(int arr[], int len)
{
    int i, j, key;
    for(i = 1; i < len; ++i){
        key = arr[i];
        for(j = i-1; j >=0; --j){
            if(arr[j] > key)
                arr[j+1] = arr[j];
            else
                break;
        }
        arr[j+1] = key;
    }
}

//判断是否排序
bool isSorted(int arr[],int n){
    bool flag = true;
    for(int i=0;i(end_time-start_time)/CLOCKS_PER_SEC*1000);
    printf("%s",isSorted(arr,N) == true ? "true":"false");
    printf("\n");
    return 0;
}

用10000个数进行测试: 
这里写图片描述
用时75ms 
可以发现比以上两种都快,原因主要是插入排序没有用到交换,而是直接赋值,交换两个数要3步,直接赋值只要1步,在数据的大情况下就差了2倍。


快速排序

思想:将一个复杂的问题划分为两部分,递归。每次在当前部分的数组中任取一个值value,重排数组(从小到大来说)将比value小的放左边,比value大的放右边,递归划分后得到的就是一个排完序的。

图片演示: 
这里写图片描述

时间复杂度:O(n*logn); 
特殊情况下(已经排好序的):O(n*n);

#include
#include 
#include 
#define N 10000
#define MAX 100000000
void swap(int *a,int *b){
    int temp = *a;
    *a = *b;
    *b = temp;
}

int partition(int arr[], int start, int end){

    swap(&arr[start],&arr[end]);
    int value = arr[end];
    int index = start;
    for(int i=start;istart){
        int index = partition(arr,start,end);
        quickSort(arr, start,index-1);
        quickSort(arr, index+1,end);
    }

}

//判断是否排序
bool isSorted(int arr[],int n){
    bool flag = true;
    for(int i=0;i(end_time-start_time)/CLOCKS_PER_SEC*1000);
    printf("%s",isSorted(arr,N) == true ? "true":"false");
    printf("\n");
//    for(int i=0;i

这里写图片描述
同样10000数据 
只用了1ms 
可知与以上的方法相比快了很多很多。


堆排序

二叉堆的定义

  • 二叉堆是完全二叉树或者是近似完全二叉树。
  • 二叉堆满足二个特性: 
    1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。 
    2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。

当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆。下图展示一个最小堆:

这里写图片描述 
堆的存储 
一般都用数组来表示堆,i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。如第0个结点左右子结点下标分别为1和2。 
这里写图片描述 
堆的操作——插入删除

这里写图片描述

堆的插入 
每次插入都是将新数据放在数组最后。可以发现从这个新数据的父结点到根结点必然为一个有序的数列

//  新加入i结点  其父结点为(i - 1) / 2  
void MinHeapFixup(int a[], int i)  
{  
    int j, temp;  

    temp = a[i];  
    j = (i - 1) / 2;      //父结点  
    while (j >= 0 && i != 0)  
    {  
        if (a[j] <= temp)  
            break;  

        a[i] = a[j];     //把较大的子结点往下移动,替换它的子结点  
        i = j;  
        j = (i - 1) / 2;  
    }  
    a[i] = temp;  
}  

更简短的表达为:

void MinHeapFixup(int a[], int i)  
{  
    for (int j = (i - 1) / 2; (j >= 0 && i != 0)&& a[i] > a[j]; i = j, j = (i - 1) / 2)  
        Swap(a[i], a[j]);  
}  

堆的删除 
按定义,堆中每次都只能删除第0个数据。为了便于重建堆,实际的操作是将最后一个数据的值赋给根结点,然后再从根结点开始进行一次从上向下的调整。调整时先在左右儿子结点中找最小的,如果父结点比这个最小的子结点还小说明不需要调整了,反之将父结点和它交换后再考虑后面的结点。相当于从根结点将一个数据的“下沉”过程。下面给出代码:

//  从i节点开始调整,n为节点总数 从0开始计算 i节点的子节点为 2*i+1, 2*i+2  
void MinHeapFixdown(int a[], int i, int n)  
{  
    int j, temp;  

    temp = a[i];  
    j = 2 * i + 1;  
    while (j < n)  
    {  
        if (j + 1 < n && a[j + 1] < a[j]) //在左右孩子中找最小的  
            j++;  

        if (a[j] >= temp)  
            break;  

        a[i] = a[j];     //把较小的子结点往上移动,替换它的父结点  
        i = j;  
        j = 2 * i + 1;  
    }  
    a[i] = temp;  
}  
//在最小堆中删除数  
void MinHeapDeleteNumber(int a[], int n)  
{  
    Swap(a[0], a[n - 1]);  
    MinHeapFixdown(a, 0, n - 1);  
}  

堆排序的两大步骤: 
1. 建立初始化堆 
2. 每次选取堆定元素进行排序 
建立初始化堆 
有了堆的插入和删除后,再考虑下如何对一个数据进行堆化操作。要一个一个的从数组中取出数据来建立堆吧,不用!先看一个数组,如下图: 
这里写图片描述

很明显,对叶子结点来说,可以认为它已经是一个合法的堆了即20,60, 65, 4, 49都分别是一个合法的堆。只要从A[4]=50开始向下调整就可以了。然后再取A[3]=30,A[2] = 17,A[1] = 12,A[0] = 9分别作一次向下调整操作就可以了。下图展示了这些步骤:

这里写图片描述

写出堆化数组的代码:

//  从i节点开始调整,n为节点总数 从0开始计算 i节点的子节点为 2*i+1, 2*i+2  
void MinHeapFixdown(int a[], int i, int n)  
{  
    int j, temp;  

    temp = a[i];  
    j = 2 * i + 1;  
    while (j < n)  
    {  
        if (j + 1 < n && a[j + 1] < a[j]) //在左右孩子中找最小的  
            j++;  

        if (a[j] >= temp)  
            break;  

        a[i] = a[j];     //把较小的子结点往上移动,替换它的父结点  
        i = j;  
        j = 2 * i + 1;  
    }  
    a[i] = temp;  
} 
//建立最小堆  
void MakeMinHeap(int a[], int n)  
{  
    for (int i = n/ 2 -1 ; i >= 0; i--)  
        MinHeapFixdown(a, i, n);  
}  

每次选取堆定元素进行排序 
首先可以看到堆建好之后堆中第0个数据是堆中最小的数据。取出这个数据再执行下堆的删除操作。这样堆中第0个数据又是堆中最小的数据,重复上述步骤直至堆中只有一个数据时就直接取出这个数据。

由于堆也是用数组模拟的,故堆化数组后,第一次将A[0]与A[n - 1]交换,再对A[0…n-2]重新恢复堆。第二次将A[0]与A[n – 2]交换,再对A[0…n - 3]重新恢复堆,重复这样的操作直到A[0]与A[1]交换。由于每次都是将最小的数据并入到后面的有序区间,故操作完成后整个数组就有序了。

void MinheapsortTodescendarray(int a[], int n)  
{  
    for (int i = n - 1; i >= 1; i--)  
    {  
        Swap(a[i], a[0]);  
        MinHeapFixdown(a, 0, i);  
    }  
}  

注意使用最小堆排序后是递减数组,要得到递增数组,可以使用最大堆。

时间复杂度为:O(n*logn)

#include
#include 
#include 
#define N 10000000
#define MAX 100000000

void swap(int *a,int *b){
    int temp = *a;
    *a = *b;
    *b = temp;
}


//判断是否排序
bool isSorted(int arr[],int n){
    bool flag = true;
    for(int i=0;iarr[l])
            l++;
        if(arr[l] <= temp)
            break;
        arr[index] = arr[l];
        index = l;
        l = index*2+1;
    }

    arr[index] = temp;
}

//新建堆
void buildHeap(int arr[],int n){

    for(int i = n/2 -1;i>=0;--i){
        adjustHeap(arr,i,n);
    }
}

void heapSort(int arr[],int  n){

    buildHeap(arr,n);

    for(int i = n-1;i>0;--i){
        swap(&arr[0],&arr[i]);
        adjustHeap(arr,0,i);
    }
}


//----------------------

int arr[N];

int main()
{
    srand((unsigned)time(NULL));
    for(int i =0;i < N;++i ){
        arr[i] =  rand()% MAX;
    }


    clock_t start_time=clock();
    {
      //  bubbleSort(arr,N);
       // chioseSort(arr,N);
        //insertSort(arr,N);
       // quickSort(arr,0,N-1);
        heapSort(arr,N);
    }

   clock_t end_time=clock();
    printf("Running time is: %lfms\n",static_cast(end_time-start_time)/CLOCKS_PER_SEC*1000);
    printf("%s",isSorted(arr,N) == true ? "true":"false");
    printf("\n");
//    for(int i=0;i

排序1000万数据: 
这里写图片描述
运行速度为4096ms

用快排排序1000万数据: 
这里写图片描述
运行速度为6381ms

可知在数据特别大的情况下堆排序的效率会比快排更好


希尔排序

略,笔者暂时没有研究希尔排序


归并排序

思想:所谓归并就是用了分治的算法思想(将一个大的问题划分为n个小问题,再用小问题得出的结果,解决大问题),主要思想就是将两个已经排好序的数组合并,最后这个数组就是有序的。 
主要有两大步骤: 
1. 先递归分解 
2. 再递归合并 
分解 
一般采取二分的方法:将一个数组分为两部分,自顶向下分解 
这里写图片描述

合并: 
当全都分解完后,开始递归合并,自底向上合并 
这里写图片描述

所以先了解下合并两数组: 
将两个已经排好序的合并成一个排序数组

//辅助数组
 int temp[N];
//-----------------------
void combineArr(int arrL[],int ln,int arrR[],int rn){

    int i = 0;
    int j = 0;
    int k=0;
    while(i

 

这里写图片描述
1000万数据,运行时速度为2642ms

时间复杂度为:O(n*logn)

可知归并排序比快排,堆排序都快,并且稳定。


基数排序(桶排序)

基数排序是一种特殊的排序,它并不是通过比较数组中的数值进行排序,而是用了很特殊的方法:通过一个二维数组,每次根据进行位数入桶,最后按顺序遍历这二维数组,便可得出一个排好序的数组。

口诉太抽象了,下面用图解释:

这里写图片描述

时间近似复杂度为:O(n)

#include
#include 
#include 
#define N 10000000
#define MAX 100000000


//------------基数排序
//辅助数组
int temps[10][N];
//获取最大位数的位置 如8->1 , 17->2,7832->4 
int getMexExp(int arr[],int n){

    int maxN = arr[0];
    for(int i=1;imaxN?arr[i]:maxN;
    }
    int exps = 1;
    maxN = maxN/10;
    while(maxN!=0){
        maxN = maxN/10;
        exps++;
    }
    return exps;
}
//基数排序
void radixSort(int arr[],int n){
    //存每个桶中数的个数
    int countIndex[10] = {0};
    int exps = getMexExp(arr,n);
    int exp = 1;
    for(int e=0;e0){
                int n  = countIndex[i];
                //将数取出,改变数组
                for(int j =0;j(end_time-start_time)/CLOCKS_PER_SEC*1000);
    printf("%s",isSorted(arr,N) == true ? "true":"false");
    printf("\n");
//    for(int i=0;i

这里写图片描述
1000万数据,运行用时只用了941ms, 
基数排序会占用比较大的空间,网上还有空间压缩的方法可参照:http://www.cnblogs.com/skywang12345/p/3603669.html

可知:基数排序的效率比以上所有排序速度都快(在数特别多的情况下,如果位数k,桶数10,k*10>n(数组个数),就不用基数排序了)

总结

这里写图片描述


参考文献: http://blog.csdn.net/morewindows/article/details/6709644/

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_24028753/article/details/75108075

文章标签: 排序算法算法测试面试

个人分类: 算法

你可能感兴趣的:(C++面试,排序算法,算法,测试,面试,算法)