各种排序算法及其C++代码实现

概念一:排序算法是否是稳定的

给定序列{3,15,8,8,6,9}
若排序后得到{3,6,8,8,9,15}, 则该排序算法是不稳定的,原本后面的8(加粗)现在位于前面了。

概念二: 内部排序、外部排序
内部排序是指将待排序的数据存放在内存中进行排序的过程。
外部排序是指待排序的数据很多,以致内存一次不能容纳所有数据,在排序过程中需要对外存进行访问的排序过程。

概念三:评价标准
排序过程中最基本的操作是关键字的比较和数据的移动,因此以关键字比较次数和数据的移动次数来度量排序算法的时间复杂度。

插入排序
一、直接插入排序:
遍历到第i个元素的时候,将其与前面的元素(已有序)进行比较,放入合适的位置。
时间复杂度 O ( n 2 ) O(n^2)
排序算法是稳定的

#include <iostream>
using namespace std;
void InsertionSort(int data[],int n){
     int tmp,i,j;
     for( i=1;i<n;i++){
        tmp = data[i];
        for( j=i-1;j>=0;j--){
            if(data[j]>tmp){
                data[j+1]=data[j];
            }else{
              break;
            }
        }
        data[j+1] = tmp;
     }
}


int main(){
 int data[6] = {32,18,65,48,27,9};
 InsertionSort(data,6);
 for(int i=0;i<6;i++)
    cout<<data[i]<<endl;
 return 0;
}

二、折半插入排序
上面提到的直接插入排序,当遍历到第i个元素的时候,它前面的所有元素都已经排好序了,因此可以使用二分法来确定第i个元素的位置。这就是折半插入排序。
时间复杂度 O ( n 2 ) O(n^2)
排序算法是稳定的

#include <iostream>
using namespace std;
void BinaryInsertionSort(int data [], int n){
     int tmp,left,right,mid;
     for(int i = 1; i< n; i++){
        tmp = data[i];
        left = 0;
        right = i-1;
        // 二分法确定当前元素要位于什么位置
        while(left <= right){     // 需仔细考虑这里,因为当前元素可能要位于的位置就是当前位置
            mid = (left + right)/2;
            if(data[mid] > tmp)
                right = mid-1;
            else
                left = mid+1;
        }
         // 确定好位置后,进行移动
        for(int j=i-1;j>=left;j--){
            data[j+1]=data[j];
        }
        data[left]=tmp;
     }
}

int main(){
 int data[6] = {32,18,65,48,27,9};
 BinaryInsertionSort(data,6);
 for(int i=0;i<6;i++)
    cout<<data[i]<<endl;
 return 0;
}

三、希尔排序
希尔排序是直接插入排序改良的算法,思想是:先将待排序数据序列划分成若干个子序列分别进行直接插入排序;待整个序列基本有序后,再对全部数据进行一次直接插入排序。
希尔排序步长从大到小调整,后面元素逐个和前面元素按间隔步长进行比较并交换,直至步长为1,步长选择是关键。
希尔排序算法复杂度依赖于增量序列的选择,但是大致是 O ( n 1.3 ) O(n^{1.3})
排序算法是不稳定的
https://blog.csdn.net/yushiyi6453/article/details/76407640
该链接里面的动态图做得很棒

#include <iostream>
using namespace std;
void ShellSort(int data[], int n ){
    // 初始化增量
    int d = n/2,i,j,tmp,k;
    while(d>=1){
        for( i=0;i<d;i++){
            j = i+d;
            while(j<n){
                 tmp = data[j];
                 k = j-d;
                 while(k>=i && data[k]>tmp){
                    data[k+d] = data[k];
                    k = k - d;
                 }
                 data[k+d] = tmp;
                 j = j + d;
            }
        }
     d = d/2;

    }
}
int main(){
 int data[12] = {65,34,25,87,12,38,56,46,14,77,92,23};
 ShellSort(data,12);
 for(int i=0;i<12;i++)
    cout<<data[i]<<endl;
 return 0;
}

交换排序
一、冒泡排序
平均时间复杂度 O ( n 2 ) O(n^2) ,稳定的排序算法。

#include <iostream>
using namespace std;
void BubbleSort(int data[], int n){
     int flag;
     for(int i=0;i<n;i++){
        flag = 0;
        for(int j=0;j<n-i-1;j++){
            if(data[j+1]<data[j]){
                swap(data[j+1],data[j]);
                flag=1;
            }
        }
        if(flag==0) return ;
     }

}
int main(){
    int data[6]={10,5,7,8,6,9};
    BubbleSort(data,6);
    for(int i=0;i<6;i++)
        cout<<data[i]<<endl;
    return 0;
}

二、快速排序
快排是名副其实的,在实际应用中,几乎是最快的排序算法,被评选为20世纪十大算法之一。 平均时间复杂度 O ( n l o g n ) O(nlogn) 。不稳定的算法。
快排的最坏情形出现在当输入序列有序时,时间复杂度 O ( n 2 ) O(n^2)
步骤:
1)分割:选择序列的一个元素作为轴元素,小于等于轴元素的元素放在轴元素左边,大于轴元素的元素放在轴元素的右边,此时,轴元素已经放在了正确的位置
2)分治:对左端和右端中的元素递归调用1)

这里我们将序列的左边第一个元素作为轴元素。在实际应用中如果不是选择第一个元素,则可以将先将轴元素与第一个元素进行交换,从而转换为用第一个元素作为轴的情形。

#include <iostream>
#include <vector>
using namespace std;
int partition(vector<int> & A, int left, int right){
    int pivot = A[left];
    while(left<right){
        while(left<right && A[right]>pivot)
            right--;
        A[left]=A[right];
        while(left<right && A[left]<=pivot)
            left++;
        A[right]=A[left];
    }
    A[left] = pivot;
    return left;   
}
void quicksort(vector<int> & A, int left, int right){
    if(left < right){
       int index = partition(A,left,right);
       quicksort(A,left,index-1);
       quicksort(A,index+1,right);
    }
    return ;
}
int main(){
    int n;
    cin>>n;
    vector<int> A;
    int tmp;
    for(int i=0;i<n;i++){
       cin>>tmp;
       A.push_back(tmp);
    }
           
    quicksort(A,0,n-1);
     
    cout<<A[0];
    for(int i=1;i<n;i++)
        cout<<" "<<A[i];
    cout<<endl;
    return 0;
     
}

选择排序
一、简单选择排序
时间复杂度 O ( n 2 ) O(n^2) ,不稳定的排序算法。

#include <iostream>
using namespace std;
void SelectionSort(int data[], int n){
     int index;
     for(int i=0;i<n;i++){
        index = 0;
        for(int j=1;j<n-i;j++){
            if(data[j]>data[index])
               index = j;
        }
        swap(data[index],data[n-i-1]);
     }
     return ;
}
int main(){
    int data[6]={9,5,4,0,-1,-2};
    SelectionSort(data,6);
    for(int i=0;i<6;i++)
        cout<<data[i]<<endl;
    return 0;
}

二、堆排序
堆是一棵完全二叉树,可以用数组实现堆的存储。
最大堆中每一个节点对应的值大于等于其子节点的值;
最小堆中每一个节点对应的值小于等于其子节点的值。
时间复杂度 O ( n l o g n ) O(nlogn) ,不稳定的排序算法。

#include <iostream>
using namespace std;
void SiftDown(int data[], int n, int i){
     int val = data[i];
     int j = 2*i+1;
     while(j<n){
        if(j+1<n && data[j+1]>data[j])
            j++;
        if(data[j]> val){
            data[i]=data[j];
            i = j;
            j = 2*i+1;
        }else {
           break;
        }
     }
     data[i]=val;
}
/* void SiftDown(int data[], int n, int i){
     int left = 2*i+1, right=2*i+2, cur=i;
     if(left<n && data[left]>data[cur])
        cur = left;
     if(right<n && data[right]>data[cur] )
        cur = right;
     if(cur!=i){
        swap(data[cur],data[i]);
        SiftDown(data,n,cur);
     }
} */
void BuildHeap(int data[], int n){  // 建堆的时间复杂度是 O(n)
     int p = n/2-1;  // 最大的非叶子节点坐标
     for(int i=p;i>=0;i--)
        SiftDown(data,n,i);
}
int main(){
 int data[12] = {65,34,25,87,12,38,56,46,14,77,92,23};
 BuildHeap(data,12);   // 建一个最大堆
 int n=12;
 while(n>1){
    swap(data[0],data[n-1]);
    n--;
    SiftDown(data,n,0);
 }
 for(int i=11;i>=0;i--)
    cout<<data[i]<<endl;
 return 0;
}

归并排序
归并排序着重于合并两个已排好序的数据序列。
时间复杂度 O ( n l o g n ) O(nlogn) ,稳定的排序算法,但是需要辅助空间。

#include <iostream>
#include <vector>
using namespace std;
void mergesort(vector<int> & A, int start, int end){
     if(start < end){
     int mid = (start + end)/2;
     mergesort(A, start,mid);
     mergesort(A, mid+1, end);
     vector<int> tmp1;
     vector<int> tmp2;
     for(int i=start;i<=mid;i++ )
         tmp1.push_back(A[i]);
     for(int i=mid+1;i<=end;i++)
         tmp2.push_back(A[i]);
     int s = start, s1 = 0, s2 =0;      
     while(s1< tmp1.size() && s2<  tmp2.size()){
         if(tmp1[s1]>tmp2[s2]){
             A[s] = tmp2[s2];
             s2++;
         }else{
             A[s] = tmp1[s1];
             s1++;
         }
         s++;  
     }
     while(s1< tmp1.size())
         A[s++]=tmp1[s1++];
     while(s2< tmp2.size())
         A[s++]=tmp2[s2++];
      
    }
}
int main(){
    int n;
    cin>>n;
    vector<int> A;
    int tmp;
    for(int i=0;i<n;i++){
       cin>>tmp;
       A.push_back(tmp);
    }
            
    mergesort(A,0,n-1);
      
    cout<<A[0];
    for(int i=1;i<n;i++)
        cout<<" "<<A[i];
    cout<<endl;
    return 0;
      
}

基数排序
基数排序是一种借助多关键字排序的思想对单逻辑关键字进行排序的方法。稳定的排序算法。时间复杂度是 O ( d n ) O(dn)
基数排序算法可以分为高位优先法和低位优先法。高位优先法在执行分配操作后要处理各个子集的排序问题,一般是一个递归的过程;低位优先法通过若干次分配和收集操作就可以完成排序,执行的次数取决于关键字的多少。


总结
以上介绍的插入排序算法、交换排序算法、选择排序算法、归并排序算法、基数排序算法都是内部排序算法。

  • 堆排序、快排、归并排序的平均时间复杂度是 O ( n l o g n ) O(nlogn)
  • 快排的最坏时间复杂度是 O ( n 2 ) O(n^2)
  • 归并排序是稳定的,堆排序和快排是不稳定的。
  • 归并排序辅助空间 O ( n ) O(n) ,快排辅助空间 O ( l o g n ) O(logn)

外部排序
一般来说,外部排序分为两个阶段:预处理和归并处理。根据内存的大小分批将数据读入内存,采用有效的内存排序方法进行排序,将其处理为若干个有序的子文件;在归并阶段,利用归并的方法将其合并成一个有序的文件。
预处理:置换选择排序
置换选择排序是堆排序的一种变形,能够在对数据文件扫描一遍的前提下,使得生成的各个初始顺串有最大的长度。
步骤:

  1. 初始化最小堆
    1) 从磁盘读入M个记录到内存数组中
    2) 堆尾索引是LAST=M-1
    3) 建立最小堆,时间复杂度是 O ( M ) O(M)
  2. 重复以下步骤直到堆为空,即LAST=-1
    1)将根节点的记录送到输出缓冲区
    2)输入缓冲区的下一条记录是R,如果R的值大于刚刚输出的根节点的值,则将其放到根节点;否则,将LAST位置的记录放在根节点,将R放在LAST位置处,并将LAST=LAST-1
    3) 重新调整堆

多路归并
归并阶段是将多个初始顺串归并成一个有序序列。

猜你喜欢

转载自blog.csdn.net/YQMind/article/details/83819339