• C/C++ 数据结构之算法


    数据结构中的排序算法。

    排序算法的相关知识:

    (1)排序的概念:所谓排序就是要整理文件中的记录,使之按关键字递增(或递减)次序排列起来。

    (2)稳定的排序方法:在待排序的文件中,若存在多个关键字相同的记录,经过排序后这些具有相同关键字的记录之间的相对次序保持不变,该排序方法是稳定的。相反,如果发生改变,这种排序方法不稳定。

    (3)排序算法的分类(分为5类):插入排序、选择排序、交换排序、归并排序和分配排序。

    (4)排序算法两个基本操作:<1>比较关键字的大小。

                                          <2>改变指向记录的指针或移动记录本身。

    具体的排序方法:

    插入排序

    <1>插入排序(Insertion Sort)的思想:每次将一个待排序的记录按其关键字大小插入到前面已经排好序的子记录中的适当位置,直到全部记录插入完成为止。

    <2>常用的插入排序方法有直接插入排序和希尔排序。

    (1)直接插入排序

    <1>算法思路:把一个记录集(如一个数组)分成两部分,前半部分是有序区,后半部分是无序区;有序区一开始有一个元素r[0],无序区一开始是从r[1]到之后的所有元素;然后每次从无序区按顺序取一个元素r[i],拿到有序区中由后往前进行比较,每次比较时,有序区中比r[i]大的元素就往后移动一位,直到找到小于r[i]的元素,这时r[i]插到小元素的后面,则完成一趟直接插入排序。如此反复,从无序区不断取元素插入到有序区,直到无序区为空,则插入算法结束。

    <2>算法演示:

    复制代码
    复制代码
    复制代码
    //直接插入排序:#include<iostream> using namespace std;
    void InsertSort(int r[],int n);
    int main() { int r[]={24,1,56,2,14,58,15,89}; InsertSort(r,8); for(int i=0;i<8;i++) { cout<<r[i]<<' '; } cout<<endl; return 0; }
    void InsertSort(int r[],int n) { for(int i=1;i<n;i++) { for(int j=i-1,s=r[i];s<r[j] && j>=0;j--) { r[j+1]=r[j]; } r[j+1]=s; } }
    复制代码
    复制代码
    复制代码

    (2)折半插入排序

    <1>算法思路:我们看到在直接插入排序算法中,需要在有序区查找比r[i]的小的元素,然后插入到这个元素后面,但这里要注意这个元素是从无序区算第一个比r[i]小的元素。折半插入排序就是在有序区折半查找这个元素。

    <2>算法演示:

    复制代码
    复制代码
    复制代码
    //折半插入排序#include<iostream> using namespace std;
    void BinInsertSort(int r[],int n);
    int main() { int r[]={53,34,76,23,55,28,63,88,34,66}; BinInsertSort(r,10); for(int i=0;i<10;i++) { cout<<r[i]<<" "; } cout<<endl; return 0; }
    void BinInsertSort(int r[],int n) { for(int i=1;i<n;i++) { int s=r[i]; int low=0; int high=i-1; while(low <= high) { int mid=(low+high)/2; if(s < r[mid]) { high=mid-1; } else { low=mid+1; } } for(int j=i-1;j>=high+1;j--) { r[j+1]=r[j]; } r[high+1]=s; //r[high+1]是要找的元素 } }
    复制代码
    复制代码
    复制代码

    (3)希尔排序(Shell Sort)

    <1>算法思路:把整个记录近一个步长step(一般取记录长度的1/2),分成step个组,再分别对每个级进行直接插入排序;然后再把整个记录近一个新的步长(一般取step/2)分成step/2个组,再分别对每个组进行直接插入排序;如此不断的缩小步长,反复分组排序,直到步长等于1为此(步长为1则不可能再分组,1是元素之间距离的最小值)。可以看出,希尔排序实质是一种分组插入方法。

    <2>算法演示:

    复制代码
    复制代码
    复制代码
    //希尔排序:#include<iostream> using namespace std;
    void ShellSort(int r[],int n); int main() { int r[]={24,1,56,2,14,58,15,89}; ShellSort(r,8); for(int i=0;i<8;i++) { cout<<r[i]<<' '; } cout<<endl; return 0; }
    void ShellSort(int r[],int n) { int step=n/2; while(step >= 1) { for(int i=step;i<n;i+=step) { for(int j=i-step,s=r[i];s<r[j] && j>=0;j-=step) { r[j+step]=r[j]; } r[j+step]=s; } step/=2; } }
    复制代码
    复制代码
    复制代码

    选择排序

    <1>选择排序的思想:每一趟从待排序的记录中选出关键字最小的记录,顺序放在已经排好的记录最后,直到全部记录排序完毕。

    <2>常用的选择排序方法有直接选择排序和堆排序。

    (1)直接选择排序

    <1>算法思路:把待排序的n个元素看成一个有序区和一个无序区,开始的时候有序区为空,无序区包含了全部n个元素。排序的时候,每次从无序区中选择比较出其中最小一个元素放到有序区中。如此反复操作,无序区中每小一个元素,有序区中就多一个元素,直到无序区的所有元素都转到有序区中。

    <2>算法演示:

    复制代码
    复制代码
    复制代码
    //简单选择排序:#include<iostream> using namespace std;
    void SelectSort(int r[],int n);
    int main() { int r[]={53,34,76,23,55,28,63,88,34,66}; SelectSort(r,10); for(int i=0;i<10;i++) { cout<<r[i]<<" "; } cout<<endl; return 0; }
    void SelectSort(int r[],int n) { for(int i=0;i<n-1;i++) { int small_loc=i; for(int j=i+1;j<n;j++) { if(r[small_loc] > r[j]) { small_loc=j; } } if(small_loc != i) { int temp=r[i]; r[i]=r[small_loc]; r[small_loc]=temp; } } }
    复制代码
    复制代码
    复制代码

    (2)堆排序

    <1>算法思路:大根堆二叉树中的非终端结点的元素值均大于它的左右孩子的值,因此知道堆的最大值是它的根结点。当根结点移出,则重新调整堆后,堆的次大值称为根结点,依次操作,可以得到堆的从大到小的有序序列。这个算法过程就是堆排序。

          堆排序有一个建堆、筛选堆、调整堆的过程。

    <2>算法演示:

    复制代码
    复制代码
    复制代码
    //堆排序:#include<iostream> using namespace std;
    void HeapAdjust(int r[],int i,int j); void HeapSort(int r[],int n);
    int main() { int r[]={53,34,76,23,55,28,63,88,34,66}; HeapSort(r,10); for(int i=0;i<10;i++) { cout<<r[i]<<" "; } cout<<endl; return 0; }
    void HeapAdjust(int r[],int i,int j) //调整堆{ int child=2*i; int temp=r[i]; //temp临时存放根结点 while(child <= j) //沿大儿子向下调整 { if(child<j && r[child+1]>r[child]) child++; if(temp >= r[child]) break; r[child/2]=r[child]; child=2*child; } r[child/2]=temp; }
    void HeapSort(int r[],int n) //建堆{ for(int i=(n-1)/2;i>=0;--i) { HeapAdjust(r,i,n-1); //初始建堆 } for(i=n-1;i>0;--i) { //将当前堆顶元素与当前堆尾元素互换 int temp=r[0]; r[0]=r[i]; r[i]=temp; HeapAdjust(r,0,i-1); //将剩下的元素重新调整成堆 } }
    复制代码
    复制代码
    复制代码

    交换排序

    <1>交换排序的思想:两两比较待排序记录的关键字,发现两个记录的次序相反时即进行交换,直到没有反序的记录为止。

    <2>常用的交换排序方法有冒泡排序和快速排序。

    (1)冒泡排序

    <1>算法思路:通过相邻元素的值的大小比较,并交换值较大的(较小的)元素,使得元素从一端移到到另一端,就像水底冒出的气泡一样。

    <2>算法演示:

    复制代码
    复制代码
    复制代码
    //起泡法排序:#include<iostream> using namespace std; #define N 5   //N为数的总个数
    void BubbleSort(int r[],int n);
    int main() { int i; int a[N]; cout<<"请输入"<<N<<"个数字:"; for(i=0;i<N;i++) { cin>>a[i]; } BubbleSort(a,N); for(i=0;i<N;i++) { cout<<a[i]<<" "; } cout<<endl; return 0; }
    void BubbleSort(int r[],int n) { for(int i=0;i<n-1;i++) //进行n-1次循环,实现n-1趟比较 { for(int j=0;j<n-1-i;j++) //在每一趟中进行n-1-i次比较 { if(r[j]>r[j+1]) { int temp=r[j]; r[j]=r[j+1]; r[j+1]=temp; } } } }
    复制代码
    复制代码
    复制代码

    (2)快速排序

    <1>算法思路:通过一趟排序将准备排序的元素集合分成两个部分,其中一部分的元素的值都小于另一部分,然后对这两部分的元素集合内部再分别重复进行上面的排序过程,直到所有的元素都排列有序。

    <2>算法演示:

    复制代码
    复制代码
    复制代码
    //快速排序:#include<iostream> using namespace std;
    int Partition(int r[],int low,int high); void QuickSort(int r[],int low,int high);
    int main() { int r[]={53,34,76,23,55,28,63,88,34,66}; QuickSort(r,0,10-1); for(int i=0;i<10;i++) { cout<<r[i]<<" "; } cout<<endl; return 0; }
    int Partition(int r[],int low,int high) { int pivotkey=r[low]; int i=low; int j=high; while(i<j) { while(i<j && r[j]>pivotkey) j--; if(i<j){r[i]=r[j];i++;} while(i<j && r[i]<pivotkey) i++; if(i<j){r[j]=r[i];j--;} } r[j]=pivotkey; return j; }
    void QuickSort(int r[],int low,int high) { if(low<high) { int pivot=Partition(r,low,high); QuickSort(r,low,pivot-1); QuickSort(r,pivot+1,high); } }
    复制代码
    复制代码
    复制代码

    归并排序

    <1>归并排序的思想:假设数组r有n个元素,那么可以看成数组r是由n个有序的子序列组成,每个子序列的长度为1,然后再两合并,得到了一个长度是2(或1)的有序子序列,再两两合并,如此重复,直到得到一个长度为n的有序数据序列为止,这种排序方法称为二路归并排序。

    <2>常用的交换排序方法有二路归并排序和三路归并排序。

    (1)二路归并排序

    <1>算法思路:如上。

    <2>算法演示:

    复制代码
    复制代码
    复制代码
    //二路归并排序#include <iostream> using namespace std; int *a=new int[20]; int n=0;
    //归并排序,排序结果放到了b[]中void Merge(int a[],int b[],int left ,int mid,int right)//此处的right指向数组中左后一个元素的位置{ int i=left; int j=mid+1; int k=left; while(i<=mid && j<=right) { if(a[i]<=a[j])b[k++]=a[i++]; else b[k++]=a[j++]; } while(i<=mid) b[k++]=a[i++]; while(j<=right) b[k++]=a[j++]; }
    //从b[]中又搬到了a[]中void Copy(int a[],int b[],int left,int right)//right同上{ for(int i=left;i<=right;i++) a[i]=b[i]; }
    //划分并排序void MergeSort(int a[],int left,int right)//同上{ int *b = new int[right-left+1]; if(left<right) { //将当前传经来的数组划分成更小的大小几乎相同的数组 int i=(left+right)/2; MergeSort(a,left,i); MergeSort(a,i+1,right); //将小数组合成大数组,同时排序,结果放到b[]中 Merge(a,b,left,i,right); //从b[]中挪到a[]中 Copy(a,b,left,right); } }
    void Input() { cout<<"Please Input array's size:"; cin>>n; cout<<"Array's elemants:"<<endl; for(int i=0;i<n;i++) cin>>a[i]; //调用算法 MergeSort(a,0,n-1); }
    void Output() { for(int i=0;i<n;i++) cout<<a[i]<<" "; cout<<endl; }
    int main() { Input(); Output(); return 0; }

    1、线性数据结构:典型的有:数组、栈、队列和线性表
    (1)数组和链表
    a、数组:存放着一组相同类型的数据,需要预先指定数组的长度,有一维数组、二维数组、多维数组等
    b、链表:链表是C语言中一种应用广泛的结构,它采用动态分配内存的形式实现,用一组任意的存储单元存放数据元素链表的,一般为每个元素增设指针域,用来指向后继元素
    c、数组和链表的区别:
    从逻辑结构来看:数组必须事先定义固定的长度,不能适应数据动态地增减的情况;链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项(数组中插入、删除数据项时,需要移动其它数据项)
    从内存存储来看:(静态)数组从栈中分配空间(用NEW创建的在堆中), 对于程序员方便快速,但是自由度小;链表从堆中分配空间, 自由度大但是申请管理比较麻烦
    从访问方式来看:数组在内存中是连续存储的,因此,可以利用下标索引进行随机访问;链表是链式存储结构,在访问元素的时候只能通过线性的方式由前到后顺序访问,所以访问效率比数组要低
     
    (2)栈、队列和线性表:可采用顺序存储和链式存储的方法进行存储
    顺序存储:借助数据元素在存储空间中的相对位置来表示元素之间的逻辑关系
    链式存储:借助表示数据元素存储地址的指针表示元素之间的逻辑关系
    a、栈:只允许在序列末端进行操作,栈的操作只能在栈顶进行,一般栈又被称为后进先出或先进后出的线性结构
       顺序栈:采用顺序存储结构的栈称为顺序栈,即需要用一片地址连续的空间来存储栈的元素,顺序栈的类型定义如下:
      
       链栈:采用链式存储结构的栈称为链栈:
        
     
    b、队列:只允许在序列两端进行操作,一般队列也被称为先进先出的线性结构
       循环队列:采用顺序存储结构的队列,需要按队列可能的最大长度分配存储空空,其类型定义如下:
      
      链队列:采用链式存储结构的队列称为链队列,一般需要设置头尾指针只是链表的头尾结点:
      
     
    c、线性表:允许在序列任意位置进行操作,线性表的操作位置不受限制,线性表的操作十分灵活,常用操作包括在任意位置插入和删除,以及查询和修改任意位置的元素
      顺序表:采用顺序存储结构表示的线性表称为顺序表,用一组地址连续的存储单元一次存放线性表的数据元素,即以存储位置相邻表示位序相继的两个元素之间的前驱和后继关系,为了避免移动元素,一般在顺序表的接口定义中只考虑在表尾插入和删除元素,如此实现的顺序表也可称为栈表:
     
     线性表:一般包括单链表、双向链表、循环链表和双向循环链表
     单链表:
      
     双向链表:
       
     线性表两种存储结构的比较:
     顺序表:
     优点:在顺序表中,逻辑中相邻的两个元素在物理位置上也相邻,查找比较方便,存取任一元素的时间复杂度都为O(1)
     缺点:不适合在任意位置插入、删除元素,因为需要移动元素,平均时间复杂度为O(n)
     链表:
     优点:在链接的任意位置插入或删除元素只需修改相应指针,不需要移动元素;按需动态分配,不需要按最大需求预先分配一块连续空空
     缺点:查找不方便,查找某一元素需要从头指针出发沿指针域查找,因此平均时间复杂度为O(n)
     
    2、树形结构:结点间具有层次关系,每一层的一个结点能且只能和上一层的一个结点相关,但同时可以和下一层的多个结点相关,称为“一对多”关系,常见类型有:树、堆
    (1)二叉树:二叉树是一种递归数据结构,是含有n(n>=0)个结点的有限集合,二叉树具有以下特点:
    二叉树可以是空树;二叉树的每个结点都恰好有两棵子树,其中一个或两个可能为空;二叉树中每个结点的左、右子树的位置不能颠倒,若改变两者的位置,就成为另一棵二叉树
     
    (2)完全二叉树:从根起,自上而下,自左而右,给满二叉树的每个结点从1到n连续编号,如果每个结点都与深度为k的满二叉树中编号从1至n的结点一一对应,则称为完全二叉树
    a、采用顺序存储结构:用一维数组存储完全二叉树,结点的编号对于与结点的下标(如根为1,则根的左孩子为2*i=2*1=2,右孩子为2*i+1=2*1+1=2)
       
    b、采用链式存储结构:
    二叉链表:
     
    三叉链表:它的结点比二叉链表多一个指针域parent,用于执行结点的双亲,便于查找双亲结点
     
    两种存储结构比较:对于完全二叉树,采用顺序存储结构既能节省空间,又可利用数组元素的下标值确定结点在二叉树中的位置及结点之间的关系,但采用顺序存储结构存储一般二叉树容易造成空间浪费,链式结构可以克服这个缺点
     
    (3)二叉查找树:二叉查找树又称二叉排序树,或者是一课空二叉树,或者是具有如下特征的二叉树:
    a、若它的左子树不空,则左子树上所有结点的值均小于根结点的值
    b、若它的右子树不空,则右子树上所有结点的值均大于根结点的值
    c、它的左、右子树也分别是二叉查找树
     
    (4)平衡二叉树:平衡二叉查找树简称平衡二叉树,平衡二叉树或者是棵空树,或者是具有下列性质的二叉查找树:它的左子树和右子树都是平衡二叉树,且左子树和右子树的高度之差的绝对值不超过1
     
    平衡二叉树的失衡及调整主要可归纳为下列四种情况:LL型、RR型、LR型、RL型
     
    (5)树:树是含有n(n>=0)个结点的有限集合,在任意一棵非空树种:
    a、有且仅有一个特定的称为根的结点
    b、当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,...,Tm,其中每一个集合本身又是一棵树,并且T1,T2,...,Tm称为根的子树
     
    (6)堆:堆是具有以下特性的完全二叉树,其所有非叶子结点均不大于(或不小于)其左右孩子结点。若堆中所有非叶子结点均不大于其左右孩子结点,则称为小顶堆(小根堆),若堆中所有非叶子结点均不小于其左右孩子结点,则称为大顶堆(大根堆)
     
     
    (7)并查集:并查集是指由一组不相交子集所构成的集合,记作:S={S1,S2,S3,...,Sn}
    (8)B树
     
    3、图形结构:在图形结构中,允许多个结点之间相关,称为“多对多”关系,可分为有向图和无向图
     
    排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。

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

        

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

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


     

    1.插入排序—直接插入排序(Straight Insertion Sort)

    基本思想:

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

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

    直接插入排序示例:


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

    算法的实现:

    [cpp] view plain copy
     
    1. //直接插入排序:将第一个数据看做一个顺序表,将后面的数据一次插入表中  
    2. void InsertSort(int a[], int n)    
    3. {    
    4.     for(int i= 1; i<n; i++){    
    5.         if(a[i] < a[i-1]){               //若第i个元素大于i-1元素,直接插入。小于的话,移动有序表后插入    
    6.             int j= i-1;   //表中最后一个数据  
    7.             int x = a[i];        //复制为哨兵,即存储待排序元素    
    8.             a[i] = a[i-1];           //先后移一个元素 (因为a[i]就是X,所以不怕丢失)   
    9.             while(j>=0 && x < a[j]){  //查找在有序表的插入位置  (遍历表)  
    10.                 a[j+1] = a[j];    
    11.                 j--;         //元素后移    
    12.             }    
    13.             a[j+1] = x;      //插入到正确位置    
    14.         }    
    15.     }    
    16.         
    17. }   
    18. int main()  
    19. {  
    20.     int n;  
    21.     cin>>n;  
    22.     int *a=new int[n];  
    23.     for(int j=0;j<n;j++)  
    24.         cin>>a[j];  
    25.     InsertSort(a,n);  
    26.     for(int i=0;i<n;i++)  
    27.         cout<<a[i];  
    28.     delete []a;  
    29. }  

    效率:

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

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

     2. 插入排序—折半插入排序(二分插入)

    将有序数列折半,看看插入到哪个序列中去
    [cpp] view plain copy
     
    1. //折半插入  
    2. void BInsertSort(int a[], int n)    
    3. {    
    4.     for(int i= 1; i<n; i++){    
    5.         int low=0,high=i;  
    6.         if(a[i] < a[i-1]){               //若第i个元素大于i-1元素,直接插入。小于的话,移动有序表后插入    
    7.             int x = a[i];        //复制为哨兵,即存储待排序元素    
    8.             a[i] = a[i-1];           //先后移一个元素 (因为a[i]就是X,所以不怕丢失)   
    9.             while(low<=high){  //查找在有序表的插入位置  (遍历表)  
    10.                 int m=(low+high)/2;  
    11.                 if(x<a[m])  high=m-1;  
    12.                 else low=m+1;  
    13.             }    
    14.             for(int j=i-1;j>=high+1;j--)  
    15.                 a[j+1]=a[j];  
    16.             a[j+1] = x;      //插入到正确位置    
    17.         }    
    18.     }    
    19.         
    20. }   
    21. int main()  
    22. {  
    23.     int n;  
    24.     cin>>n;  
    25.     int *a=new int[n];  
    26.     for(int j=0;j<n;j++)  
    27.         cin>>a[j];  
    28.     BInsertSort(a,n);  
    29.     for(int i=0;i<n;i++)  
    30.         cout<<a[i];  
    31.     delete []a;  
    32. }  

     3. 插入排序—希尔排序(Shell`s Sort)

    希尔排序是1959 年由D.L.Shell 提出来的,相对直接排序有较大的改进。希尔排序又叫缩小增量排序

    基本思想:

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

    操作方法:

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

    希尔排序的示例:

     

    算法实现:

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

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

    [cpp] view plain copy
     
    1. //希尔排序:去增量为d1的分为一组,共分成d1组分别进行插入排序,然后每组对应元素放在一起,然后取d2...知道d=1  
    2. void ShellSort(int a[],int n)  
    3. {  
    4.     int dk;  
    5.     int tmp;  
    6.     for(dk=n/2;dk>0;dk/=2)  
    7.         for(int i=dk;i<n;i++)  
    8.         {  
    9.             tmp=a[i];  
    10.             for(int j=i;j>=dk;j-=dk)  
    11.                 if(tmp<a[j-dk])  
    12.                     a[j]=a[j-dk];  
    13.                 else break;  
    14.             a[j]=tmp;  
    15.         }  
    16. }  
    17. int main()  
    18. {  
    19.     int n;  
    20.     cin>>n;  
    21.     int *a=new int[n];  
    22.     for(int j=0;j<n;j++)  
    23.         cin>>a[j];  
    24.     ShellSort(a,n);  
    25.     for(int i=0;i<n;i++)  
    26.         cout<<a[i];  
    27.     delete []a;  
    28. }  
    希尔排序时效分析很难,关键码的比较次数与记录移动次数依赖于增量因子序列d的选取,特定情况下可以准确估算出关键码的比较次数和记录的移动次数。目前还没有人给出选取最好的增量因子序列的方法。增量因子序列可以有各种取法,有取奇数的,也有取质数的,但需要注意:增量因子中除1 外没有公因子,且最后一个增量因子必须为1。希尔排序方法是一个不稳定的排序方法。

    4. 选择排序—简单选择排序(Simple Selection Sort)

    基本思想:

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

    简单选择排序的示例:

     

    操作方法:

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

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

    以此类推.....

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

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


    算法实现:

    [cpp] view plain copy
     
    1. //简单选择排序:遍历一次找到最小与第一个元素呼唤位置,再从第二个元素开始遍历找到最小与第二个元素呼唤位置...  
    2. void SelectSort(int a[],int n)  
    3. {  
    4.     for(int i=0;i<n-1;i++)  
    5.     {  
    6.         int k=i;//记录最小的那个下标的  
    7.         for(int j=i+1;j<n;j++)  
    8.             if(a[j]<a[k])  
    9.                 k=j;  
    10.         if(k!=i)  
    11.         {  
    12.             int t=a[i];  
    13.             a[i]=a[k];  
    14.             a[k]=t;  
    15.         }  
    16.   
    17.     }  
    18. }  
    19. int main()  
    20. {  
    21.     int n;  
    22.     cin>>n;  
    23.     int *a=new int[n];  
    24.     for(int j=0;j<n;j++)  
    25.         cin>>a[j];  
    26.     SelectSort(a,n);  
    27.     for(int i=0;i<n;i++)  
    28.         cout<<a[i];  
    29.     delete []a;  
    30. }  

     简单选择排序的改进——二元选择排序(有bug)

    参考另一篇:http://blog.csdn.net/ye_scofield/article/details/39312717

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

    1. void SelectSort(int r[],int n) {  
    2.     int i ,j , min ,max, tmp;  
    3.     for (i=1 ;i <= n/2;i++) {    
    4.         // 做不超过n/2趟选择排序   
    5.         min = i; max = i ; //分别记录最大和最小关键字记录位置  
    6.         for (j= i+1; j<= n-i; j++) {  
    7.             if (r[j] > r[max]) {   
    8.                 max = j ; continue ;   
    9.             }    
    10.             if (r[j]< r[min]) {   
    11.                 min = j ;   
    12.             }     
    13.       }    
    14.       //该交换操作还可分情况讨论以提高效率  
    15.       tmp = r[i-1]; r[i-1] = r[min]; r[min] = tmp;  
    16.       tmp = r[n-i]; r[n-i] = r[max]; r[max] = tmp;   
    17.   
    18.     }   
    19. }  

    5. 选择排序—堆排序(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)
                                  


                                  

     算法的实现:

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

    [cpp] view plain copy
     
    1. //堆排序:树形选择排序,将带排序记录看成完整的二叉树,第一步:建立初堆,第二步:调整堆  
    2. //第二步:调整堆  
    3. void HeapAdjust(int a[],int s,int n)  
    4. {  
    5.     //调整为小根堆,从小到大  
    6.     int rc=a[s];  
    7.     for(int j=2*s;j<=n;j*=2)  
    8.     {  
    9.         if(j<n && a[j]>a[j+1])//判断左右子数大小  
    10.             j++;  
    11.         if(rc<=a[j])  
    12.             break;  
    13.         a[s]=a[j];  
    14.         s=j;  
    15.     }  
    16.     a[s]=rc;  
    17. }  
    18. //第一步:建初堆  
    19. void CreatHeap(int a[],int n)  
    20. {  
    21.     //小根堆  
    22.     for(int i=n/2;i>0;i--)  
    23.         HeapAdjust(a,i,n);  
    24. }  
    25. //整合  
    26. void HeapSort(int a[],int n)  
    27. {  
    28.     CreatHeap(a,n);//第一步,建立初堆  
    29.     for(int i=n;i>1;i--)  
    30.     {  
    31.         int x=a[1];//堆顶与最后一个元素互换  
    32.         a[1]=a[i];  
    33.         a[i]=x;  
    34.         HeapAdjust(a,1,i-1);  
    35.     }  
    36.   
    37. }  
    38. int main()  
    39. {  
    40.     int n;  
    41.     cin>>n;  
    42.     int *a=new int[n+1];  
    43.     for(int j=1;j<n;j++)//注意:这里是从1开始的  
    44.         cin>>a[j];  
    45.     HeapSort(a,n);  
    46.     for(int i=1;i<n;i++)  
    47.         cout<<a[i];  
    48.     delete []a;  
    49. }  

    分析:

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

                                    

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

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

    基本思想:

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

    冒泡排序的示例:

     

    算法的实现:

    [cpp] view plain copy
     
    1. //传统冒泡排序  
    2. void maopao(int a[],int n)  
    3. {  
    4.     for(int i=0;i<n-1;i++)  
    5.         for(int j=0;j<n-i-1;j++)  
    6.             if(a[j]>a[j+1])  
    7.             {  
    8.                 int t=a[j];  
    9.                 a[j]=a[j+1];  
    10.                 a[j+1]=t;  
    11.             }  
    12. }  
    13. int main()  
    14. {  
    15.     int n;  
    16.     cin>>n;  
    17.     int *a=new int[n];  
    18.     for(int j=0;j<n;j++)  
    19.         cin>>a[j];  
    20.     maopao(a,n);  
    21.     for(int i=0;i<n;i++)  
    22.         cout<<a[i];  
    23.     delete []a;  
    24. }  

    冒泡排序算法的改进

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

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

    改进后算法如下:

    [cpp] view plain copy
     
    1. //冒泡排序改进1,添加标志位,如果某一次排序中出现没有交换位置,说明排序完成  
    2. void maopao(int a[],int n)  
    3. {  
    4.     int flag=0;  
    5.     for(int i=0;i<n-1;i++)  
    6.     {  
    7.         flag=0;  
    8.         for(int j=0;j<n-i-1;j++)  
    9.             if(a[j]>a[j+1])  
    10.             {  
    11.                 int t=a[j];  
    12.                 a[j]=a[j+1];  
    13.                 a[j+1]=t;  
    14.                 flag=1;  
    15.             }  
    16.         if(flag==0)  
    17.             break;  
    18.     }  
    19. }  
    20. int main()  
    21. {  
    22.     int n;  
    23.     cin>>n;  
    24.     int *a=new int[n];  
    25.     for(int j=0;j<n;j++)  
    26.         cin>>a[j];  
    27.     maopao(a,n);  
    28.     for(int i=0;i<n;i++)  
    29.         cout<<a[i];  
    30.     delete []a;  
    31. }  

    2.改进后的算法实现为:

    [cpp] view plain copy
     
    1. //冒泡排序改进2,添加标志位,记录最后一次交换位置的地方,证明最后一次交换位置之后的地方时排好序的,下一次只需要排最后一次之前的地方就好了  
    2. void maopao(int a[],int n)  
    3. {  
    4.     int flag=n-1;//刚开始,最后交换位置的地方设置为数组的最后一位  
    5.     while(flag>0)//flag在逐渐减小,到最后肯定会变为0  
    6.     {  
    7.         int pos=0;//每一轮的最开始,标志位置在数组0  
    8.         for(int i=0;i<flag;i++)  
    9.             if(a[i]>a[i+1])  
    10.             {  
    11.                 int t=a[i];  
    12.                 a[i]=a[i+1];  
    13.                 a[i+1]=t;  
    14.                 pos=i;  
    15.             }  
    16.         flag=pos;     
    17.     }  
    18. }  
    19. int main()  
    20. {  
    21.     int n;  
    22.     cin>>n;  
    23.     int *a=new int[n];  
    24.     for(int j=0;j<n;j++)  
    25.         cin>>a[j];  
    26.     maopao(a,n);  
    27.     for(int i=0;i<n;i++)  
    28.         cout<<a[i];  
    29.     delete []a;  
    30. }  

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

    改进后的算法实现为:

    [cpp] view plain copy
     
    1. //冒泡改进3,传统冒泡每趟排序遍历一次找到一个最大值或者最小值,如果每趟遍历两次就会找打一个最大值和一个最小值,减少了一半的排序趟数  
    2. void maopao ( int r[], int n){    
    3.     int low = 0;     
    4.     int high= n -1; //设置变量的初始值    
    5.     int tmp,j;    
    6.     while (low < high) {    
    7.         for (j= low; j< high; ++j) //正向冒泡,找到最大者    
    8.             if (r[j]> r[j+1]) {    
    9.                 tmp = r[j]; r[j]=r[j+1];r[j+1]=tmp;    
    10.             }     
    11.         --high;                 //修改high值, 前移一位    
    12.         for ( j=high; j>low; --j) //反向冒泡,找到最小者    
    13.             if (r[j]<r[j-1]) {    
    14.                 tmp = r[j]; r[j]=r[j-1];r[j-1]=tmp;    
    15.             }    
    16.         ++low;                  //修改low值,后移一位    
    17.     }     
    18. }   
    19. int main()  
    20. {  
    21.     int n;  
    22.     cin>>n;  
    23.     int *a=new int[n];  
    24.     for(int j=0;j<n;j++)  
    25.         cin>>a[j];  
    26.     maopao(a,n);  
    27.     for(int i=0;i<n;i++)  
    28.         cout<<a[i];  
    29.     delete []a;  
    30. }  

    7. 交换排序—快速排序(Quick Sort)

    基本思想:

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

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

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

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

    快速排序的示例:

    (a)一趟排序的过程:

    (b)排序的全过程

    算法的实现:

     递归实现:

    [cpp] view plain copy
     
    1. //快速排序   
    2. //第一个参数要排的数组,第二个参数第一个数,第三个参数数组成员个数  
    3. void kuaipai(int array[],int low,int hight)  
    4. {  
    5.     int i,j,t,m;  
    6.     if(low<hight)  
    7.     {  
    8.         i=low;  
    9.         j=hight;  
    10.         t=array[low];//第一个数为轴  
    11.         while(i<j)  
    12.         {  
    13.             while(i<j && array[j]>t)//从右边找出小于轴的数  
    14.                 j--;  
    15.             if(i<j)//将小于轴的数array[j]放到左边array[i]的位置  
    16.             {  
    17.                 m=array[i];  
    18.                 array[i]=array[j];  
    19.                 array[j]=m;  
    20.                 i++;  
    21.             }  
    22.             while(i<j && array[i]<=t)//从左边找出大于轴的数  
    23.                 i++;  
    24.             if(i<j)//将大于轴的数array[i]放在右边array[j]的位置  
    25.             {  
    26.                 m=array[j];  
    27.                 array[j]=array[i];  
    28.                 array[i]=m;  
    29.                 j--;  
    30.             }     
    31.         }  
    32.           
    33.         array[i]=t;//轴放在中间,现在就有两个区域了分别是[0 i-1]和[i+1 hight],分别快排  
    34.         kuaipai(array,0,i-1);  
    35.         kuaipai(array,i+1,hight);  
    36.     }  
    37. }  
    38. void PX_kuaipai(int buf[],int size)  
    39. {  
    40.     kuaipai(buf,0,size-1);  
    41. }  
    42. void main()  
    43. {  
    44.     while(1)  
    45.     {  
    46.         int m,i;  
    47.         cin>>m;  
    48.         int *buf=new int[m];  
    49.         for(i=0;i<m;i++)  
    50.             cin>>buf[i];  
    51.         PX_kuaipai(buf,m);  
    52.         for(i=0;i<m;i++)  
    53.             cout<<buf[i];  
    54.         cout<<' ';  
    55.         delete []buf;  
    56.     }  
    57.       
    58. }  

    分析:

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

     
    快速排序的改进

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

    参考:http://blog.csdn.net/hguisu/article/details/7776068

    1. void print(int a[], int n){  
    2.     for(int j= 0; j<n; j++){  
    3.         cout<<a[j] <<"  ";  
    4.     }  
    5.     cout<<endl;  
    6. }  
    7.   
    8. void swap(int *a, int *b)  
    9. {  
    10.     int tmp = *a;  
    11.     *a = *b;  
    12.     *b = tmp;  
    13. }  
    14.   
    15. int partition(int a[], int low, int high)  
    16. {  
    17.     int privotKey = a[low];                 //基准元素  
    18.     while(low < high){                   //从表的两端交替地向中间扫描  
    19.         while(low < high  && a[high] >= privotKey) --high; //从high 所指位置向前搜索,至多到low+1 位置。将比基准元素小的交换到低端  
    20.         swap(&a[low], &a[high]);  
    21.         while(low < high  && a[low] <= privotKey ) ++low;  
    22.         swap(&a[low], &a[high]);  
    23.     }  
    24.     print(a,10);  
    25.     return low;  
    26. }  
    27.   
    28.   
    29. void qsort_improve(int r[ ],int low,int high, int k){  
    30.     if( high -low > k ) { //长度大于k时递归, k为指定的数  
    31.         int pivot = partition(r, low, high); // 调用的Partition算法保持不变  
    32.         qsort_improve(r, low, pivot - 1,k);  
    33.         qsort_improve(r, pivot + 1, high,k);  
    34.     }   
    35. }   
    36. void quickSort(int r[], int n, int k){  
    37.     qsort_improve(r,0,n,k);//先调用改进算法Qsort使之基本有序  
    38.   
    39.     //再用插入排序对基本有序序列排序  
    40.     for(int i=1; i<=n;i ++){  
    41.         int tmp = r[i];   
    42.         int j=i-1;  
    43.         while(tmp < r[j]){  
    44.             r[j+1]=r[j]; j=j-1;   
    45.         }  
    46.         r[j+1] = tmp;  
    47.     }   
    48.   
    49. }   
    50.   
    51.   
    52.   
    53. int main(){  
    54.     int a[10] = {3,1,5,7,2,4,9,6,10,8};  
    55.     cout<<"初始值:";  
    56.     print(a,10);  
    57.     quickSort(a,9,4);  
    58.     cout<<"结果:";  
    59.     print(a,10);  
    60.   
    61. }  

    8. 归并排序(Merge Sort)

    基本思想:

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

    归并排序示例:

     

    合并方法:

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

    1. j=m+1;k=i;i=i; //置两个子表的起始下标及辅助数组的起始下标
    2. 若i>m 或j>n,转⑷ //其中一个子表已合并完,比较选取结束
    3. //选取r[i]和r[j]较小的存入辅助数组rf
      如果r[i]<r[j],rf[k]=r[i]; i++; k++; 转⑵
      否则,rf[k]=r[j]; j++; k++; 转⑵
    4. //将尚未处理完的子表中元素存入rf
      如果i<=m,将r[i…m]存入rf[k…n] //前一子表非空
      如果j<=n ,  将r[j…n] 存入rf[k…n] //后一子表非空
    5. 合并结束。
    [cpp] view plain copy
     
    1. //归并排序  
    2. void copyArray(int source[], int dest[],int len,int first)    
    3. {    
    4.     int i;    
    5.     int j=first;    
    6.     for(i=0;i<len;i++)    
    7.     {    
    8.         dest[j] = source[i];    
    9.         j++;    
    10.     }    
    11.             
    12. }   
    13. //相邻两个有序子序列的归并函数,将a[low...mid]和a[mid+1...high]归并到T[LOW..high]中   
    14. void merge(int a[],int left,int right)    
    15. {    
    16.     int begin1 = left;    
    17.     int mid = (left+right)/2 ;    
    18.     int begin2 = mid+1;    
    19.     int k=0;    
    20.     int newArrayLen = right-left+1;    
    21.     int *b = (int*)malloc(newArrayLen*sizeof(int));    
    22.     while(begin1<=mid && begin2<=right)    
    23.     {    
    24.         if(a[begin1]<=a[begin2])    
    25.             b[k++] = a[begin1++];    
    26.         else    
    27.             b[k++] = a[begin2++];    
    28.     }    
    29.     while(begin1<=mid)    
    30.         b[k++] = a[begin1++];    
    31.     while(begin2<=right)    
    32.         b[k++] = a[begin2++];    
    33.     copyArray(b,a,newArrayLen,left);    
    34.     free(b);    
    35. }    

    两路归并的递归算法

    [cpp] view plain copy
     
    1. //归并函数,将a[low...high]归并到T[low...high]中  
    2. void mergeSort(int a[],int left,int right)    
    3. {    
    4.     int i;    
    5.     // 保证至少有两个元素    
    6.     if(left < right)    
    7.     {    
    8.         i = (left+right)/2;    
    9.         mergeSort(a,left,i);    
    10.         mergeSort(a,i+1,right);    
    11.         merge(a,left,right);    
    12.     }    
    13. }   
    14. void MergeSort(int a[],int n)  
    15. {  
    16.     mergeSort(a,0,n-1);  
    17. }  

    完整程序
    [cpp] view plain copy
     
    1. //归并排序  
    2. void copyArray(int source[], int dest[],int len,int first)    
    3. {    
    4.     int i;    
    5.     int j=first;    
    6.     for(i=0;i<len;i++)    
    7.     {    
    8.         dest[j] = source[i];    
    9.         j++;    
    10.     }    
    11.             
    12. }   
    13. //相邻两个有序子序列的归并函数,将a[low...mid]和a[mid+1...high]归并到T[LOW..high]中   
    14. void merge(int a[],int left,int right)    
    15. {    
    16.     int begin1 = left;    
    17.     int mid = (left+right)/2 ;    
    18.     int begin2 = mid+1;    
    19.     int k=0;    
    20.     int newArrayLen = right-left+1;    
    21.     int *b = (int*)malloc(newArrayLen*sizeof(int));    
    22.     while(begin1<=mid && begin2<=right)    
    23.     {    
    24.         if(a[begin1]<=a[begin2])    
    25.             b[k++] = a[begin1++];    
    26.         else    
    27.             b[k++] = a[begin2++];    
    28.     }    
    29.     while(begin1<=mid)    
    30.         b[k++] = a[begin1++];    
    31.     while(begin2<=right)    
    32.         b[k++] = a[begin2++];    
    33.     copyArray(b,a,newArrayLen,left);    
    34.     free(b);    
    35. }    
    36. //归并函数,将a[low...high]归并到T[low...high]中  
    37. void mergeSort(int a[],int left,int right)    
    38. {    
    39.     int i;    
    40.     // 保证至少有两个元素    
    41.     if(left < right)    
    42.     {    
    43.         i = (left+right)/2;    
    44.         mergeSort(a,left,i);    
    45.         mergeSort(a,i+1,right);    
    46.         merge(a,left,right);    
    47.     }    
    48. }   
    49. void MergeSort(int a[],int n)  
    50. {  
    51.     mergeSort(a,0,n-1);  
    52. }  
    53. int main()  
    54. {  
    55.     int n;  
    56.     cin>>n;  
    57.     int *a=new int[n];  
    58.       
    59.     for(int j=0;j<n;j++)  
    60.         cin>>a[j];  
    61.     MergeSort(a,n);  
    62.     for(int i=0;i<n;i++)  
    63.         cout<<a[i];  
    64.     delete []a;  
    65.       
    66. }  


    9. 桶排序/基数排序(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摞(分配),再按此顺序叠放在一起(收集),如此进行二次分配和收集即可将扑克牌排列有序。   

    基数排序:

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

    算法实现:

    1. Void RadixSort(Node L[],length,maxradix)  
    2. {  
    3.    int m,n,k,lsp;  
    4.    k=1;m=1;  
    5.    int temp[10][length-1];  
    6.    Empty(temp); //清空临时空间  
    7.    while(k<maxradix) //遍历所有关键字  
    8.    {  
    9.      for(int i=0;i<length;i++) //分配过程  
    10.     {  
    11.        if(L[i]<m)  
    12.           Temp[0][n]=L[i];  
    13.        else  
    14.           Lsp=(L[i]/m)%10; //确定关键字  
    15.        Temp[lsp][n]=L[i];  
    16.        n++;  
    17.    }  
    18.    CollectElement(L,Temp); //收集  
    19.    n=0;  
    20.    m=m*10;  
    21.   k++;  
    22.  }  
    23. }  


    总结

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

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

                                 时间复杂度函数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、如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。

  • 相关阅读:
    C#中NULL,"",DBNULL,String.Empty,Convert.IsDBNull()的区别
    vs 扩展和更新下载的插件在什么位置呢,看看吧,哈哈
    全面了解 Nginx 主要应用场景
    Nginx配置详解
    ulimit -n 修改
    mysql系列十一、mysql优化笔记:表设计、sql优化、配置优化
    java.net.UnknownHostException 异常处理
    Oracle 11G在用EXP 导出时,空表不能导出解决
    expdp和impdp导入和导出数据
    CentOS6.5运行yum报错:No module named yum
  • 原文地址:https://www.cnblogs.com/klb561/p/9027133.html
Copyright © 2020-2023  润新知