插入排序/选择排序/交换排序/归并排序/基数排序

数据处理时一个主要需求就是排序,目前主要的内存排序(处理的数据量百万级以下)主要基于关键字大小,具体可分一下几种:

1.    插入排序:直接插入排序(稳定)和希尔排序(升级版,不稳定);     
直接插入排序,关键是在以排序好的序列基础上再将加入一个新元素并完成排序,即

数组a[0]-a[i-1]是已经按照从小到大的顺序排序好的序列,而a[i]-a[n-1]是待排序序列,取a[i]进入a[0]-a[i-1]重新排序并完成后a[0]-a[i-1] a[i]又是一个排序好的序列,每次从未排序中选择元素的增量d=1

void my_compare::insert_direct(int a[],int n){
int i,j;
int tmp;
for(i=1;i=0&&a[j]>tmp){a[j+1]=a[j];j--;}
    a[j+1]=tmp;
   }
}

可分析知道,当原始数组就是从小到大排列时执行效率最高O(N)反之当数据基本反序时执行效率最差O(N^2),平均耗时O(N×N),稳定。

希尔排序,根据人名翻译,其实质是缩小增量版的升级插入排序,核心是选择一个增量d(一般是以原始数组数目一般半为起始),剩下的类似直接插入排序,只不过每次从未排序中选择元素的增量d>1,完成一次排序后缩小增量d,直至d=1。

void my_compare::insert_shell(int a[],int n){
int i,j;
int tmp;
int d=n/2;
while(d>0){
    //对相距gap距离的所有元素进行插入排序
    for(i=d;i=0&&a[j]>tmp){a[j+d]=a[j];j=j-d;}
	a[j+d]=tmp;
    }
    d=d/2;
            }
}
这种平均耗时O(N log N), 不稳定

2.    选择排序:直接选择排序(不稳定)和堆排序(以完全二叉树为基础,不稳定);     

直接选择排序主要是数组a[0]-a[i-1]是已排序序列,a[i]-a[n-1]序列是未排序序列,从这未排序里选择最小值作为a[i](不同于直接插入是直接选择a[i]),放在数组a[0]-a[i-1]最后位置。其时间平均效率O(N^2),不稳定

void my_compare::select_direct(int a[],int n){
int i,j;
int tmp;
for(i=0;i

直接选择排序与原始序列是否有序无关,,时间复杂度都是O(N^2)。

堆排序,借鉴了优先队列里堆序的特点即每个树的根是最值,类似堆Deletemin操作每次选择根值(最值)进行n-1操作即完成排序。

void my_compare::sift(int a[],int low,int high){
int i=low,j=2*i;
int tmp=a[i];
while(j<=high){
    if(j=1;i--)
    sift(a,i,n);
for(i=n;i>=2;i--)
 {
   tmp=a[1];a[1]=a[i];a[i]=tmp;sift(a,1,i-1);
 }
}
时间效率(N log N), 不稳定

3.    交换排序:冒泡排序(稳定)和快速排序(升级版,选择基准,不稳定);    

不同于直接插入和直接选择区分排序区和未排序区,冒泡排序算法思想是直接对整个无序的原始数组进行处理,每趟对相邻关键字进行比较和位置置换,一趟完成使得最值如气泡一般漂浮到最后位置,接着对剩下的数组进行类似处理。

void my_compare::swap_bubble(int a[],int n){
int i,j;
int tmp;
for(i=0;ia[j+1]){tmp=a[j];a[j]=a[j+1];a[j+1]=tmp;}
   }
}

类似直接插入排序当原始数组就是从小到大排列时执行效率最高O(N)反之当数据基本反序时执行效率最差O(N^2),平均耗时O(Nlog N),稳定。

快速排序相比较之前排序有点复杂, 快速排序由于排序效率在同为O(N*logN)的几种排序方法中效率较高,因此经常被采用,再加上快速排序思想----分治法也确实实用,因此很多软件公司的笔试面试,包括像腾讯,微软等知名IT公司都喜欢考这个,还有大大小的程序方面的考试如软考,考研中也常常出现快速排序的身影。故重点介绍:

快速排序是基于交换的冒泡排序的改进,基本思想是在n个关键字中取一个记录作为基准枢纽元,然后对剩下的n-1个元素进行分类,所有比基元小的放置在前子区间A,比基元大的都放在后子区间B,完成了一趟快排即:

前子区间A     基元    后子区间B-----------------------------------A与B数目尽量相同,否则为劣质分割

此时基元上的关键字已完成最终位置的排序,剩下的就是分别对两个子区间也采用这种思路,直到每个子区间只有一个关键字为止,总而言之是分而治之。

虽然上述算法无论选择哪个元素作为基准都能完成排序,但是不同基准选择效果不同:

1 最容易想到的选择第一个元素作为基元

void my_compare::quicksort(int a[],int low,int high){
int i=low,j=high;//指向无序区的第一个和最后一个以便从两端向中间
int tmp;
if(lowi&&a[j]>tmp)j--;
	if(j>i){a[i]=a[j];i++;}
	while(j>i&&a[i]i){a[j]=a[i];j--;}
               }
    a[i]=tmp;
    //完成一趟快排,之后对两个子区间递归
    quicksort(a,low,i-1);
    quicksort(a,i+1,high);
             }
}
void my_compare::swap_quick(int a[],int n){
quicksort(a,0,n-1);
}
这种情况一般针对与输入的关键字都是随机的,此时时间效率能达到O(Nlog N), 不稳定。但是一旦输入是预排序或者反序,这样的基准将产生非常恶劣影响:剩下的n-1个元素不是都划入A就是被划入B。例如数组是预排序的,若采用第一个元素作为基元,则根本没有“一分为二”,时间效率O(N^2),时间增加了,但是实际上是浪费了(因为花了如此多时间还只是对已经排好的序列再进行一次排列而已)。

2 最安全的选择随机元素作为基元

一般选择采用随机选择基元是安全的,另外也可以采用叁数中值分隔法,即选择序列左端/右端/及中心位置的这三个元素中值作为基元例如8,1,4,9,6,3,5,2,7,0最左端8,最右端0中间位置(left_right)/2上是6,这三个数中值为6故基元为6。且在选择基元后,可把这三个数值进行排序,把最小者放在最左位置(这也正是它在A中的应有位置)把最大放在最右端(这也正是它在B中的应有位置)以减少分割时的操作。

具体的分割策略如下:

所有元素互异:当i在j左边时,i右移,移过那些小于基元的元素,同时j左移,移过那些大于基元的元素,当i和j停止时,当i小于j前提下,i指向一个大于基元而j指向一个小于基元,交换i和j所指元素,直到i和j已经交错,分割的最后一步是将基元与i最后到达位置进行交换以便让基元回到最终位置

有元素等于基元:无非三种情况,要么i和j遇见与基元相等的直接跳过;要么停下进行交换;要么一个停下一个跳过,这种不对称做法容易导致分割两部分不均故直接舍弃。为了分析可以假设所有元素都相同。

对于直接跳过情景,由于基元与i最后到达位置进行交换,故将导致产生两个非常不均衡的子区间,类似与预排序数组与第一个元素作为基准的情景此时时间O(N^2),故舍弃;

对于都停下交换情景:虽然没有实际意义,但是效果在于i和j将在中间交替,而不像直接跳过情景i直接跳到了序列最后位置。因此将基元与i最后到达位置进行交换以便让基元回到最终位置后,分割两部分基本均衡,这种完美均衡效果就是运行时间O(N log N)。故采用这种。

//*********对基元的选择********************//
//将三者最小者放在左端,最大者放在右端
int my_compare::getpivot(int a[],int low,int high){
int middle=(low+high)/2;
int tmp;
if(a[low]>a[middle]){tmp=a[low];a[low]=a[middle];a[middle]=tmp;}
if(a[low]>a[high]){tmp=a[low];a[low]=a[high];a[high]=tmp;}
if(a[high]i&&a[i]i&&a[j]>pivot)j--;
       if(i

快排一般也会问时间复杂度:

在理想情况即A和B两子区间分得均匀:

对于一长度n的序列,先扫描找到基准,然后两个子区间分别递归:

第一次时:T(n)=2*T(n/2)+n

第二次时:T(n)=2*T(n/2)+n=T(n)=2*(2*T(n/4)+n/2)+n=4T(n/4)+2n

第三次时:T(n)=2*T(n/2)+n=8T(n/8)+3n

第k次时:T(n)=2*T(n/2)+n=2^kT(n/2^k)+kn

已知T(1)=0,则k=log2(n),T(n)=n*log2(n)

在恶劣条件下:

当待排序的序列为正序或逆序排列时,且每次划分只得到一个比上一次划分少一个记录的子序列,注意另一个为空。需要进行n-1次比较,每次比较只少一个元素。


4.     归并排序(从小到大,稳定);

其核心思想是把a[0]-a[n-1]看成n个长度为1的有序表,将相邻的有序表成对归并得到n/2个长度为2的有序表,然后继续按照此思路归并知道最后得到1个长度为n的有序表。设计思路:

a 先完成相邻两个有序表a[low]-a[mid]与a[mid+1]-a[high]的合并

b 完成给定长度lenghth下原始数组的合并(注意对于最后一个子表长度小于Length的处理,这是经常遇见的情形)

c 完成lenght=1,=2,=4...n的循环

void my_compare::merge_two(int a[],int low,int mid,int high){
int len=high-low+1;
int *pt=(int *)malloc(sizeof(int)*len);
int i=low,j=mid+1;//两个有序区a[low]-a[mid] a[mid+1]-a[high]开始位置
int k=0;
while(i<=mid&&j<=high){
    if(a[i]
归并排序不同于插入之希尔排序/选择之堆排序/交换之快速排序从长到短的分而治之,他是从短到长一一破解。时间效率也是O(N log N), 稳定

5 番外:基数排序(稳定)

核心是不同于插入/交换/选择排序比较关键字比较,基数排序直接比较数值的每一位数字,对数组n个元素进行若干趟分配与和收集。要求每个元素是d位的十进制正整数元素。对于n个元素每一位数字无非从0-9,建立这样的数组alist[10],且每个数组元素alist[j]指向一个 单链表,每条单链表上元素均是某趟下数字为j的元素。下一趟处理是建立在上一趟分配收集完基础上。

int my_compare::getres(int a,int d,int i){
if(i<1||i>d)return -1;
int j=i;
int res;
do{res=a%10;
    a=a/10;
    j++;
   }while(j<=d);
 return res;   
}// 得到指定位数字
void  my_compare::radix(int a[],int n,int d){
typedef struct ele{
int key;
ele *next;
}newtype;
newtype *tp[10],*tail[10],*p=NULL;
int i,j,k;
//从低位到高位做d趟排序
for(i=d-1;i>=0;i--){
for(j=0;j<10;j++){tp[j]=tail[j]=NULL;}
for(k=0;kkey=a[k];s->next=NULL;
    if(tp[res]==NULL){tp[res]=s;tail[res]=s;}
    else {tail[res]->next=s;tail[res]=s;}

}
//完成一趟排序后,收集
k=0;
for(j=0;j<10;j++)
  if(tp[j]){p=tp[j];while(p){a[k++]=p->key;p=p->next;}}
                    }
}
void my_compare::radix_(int a[],int n){
radix(a,n,3);
}
这种情况时间复杂度O(d*(n+10)),空间复杂度O(n+10), 稳定


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