8.2再谈排序与检索(包含所有的排序总结)

排序的种类:

排序分为内部排序和外部排序,涉及多种排序的方法。

内部排序是将待排序的记录全部放在内存的排序。插入排序(直接插入排序、折半插入排序和希尔排序)、交换排序(冒泡排序和快速排序)、选择排序(简单选择排序和堆排序)、归并排序和基数排序。

1.归并排序:

按照分治法:①把序列分成元素个数尽量相等的两半  ②把两半元素分别排序  ③把两个有序表合并成一个


#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cstdio>
using namespace std;
int A[99],T[99];
void pai(int x,int y)  //x、y是左右边界
{
    if(y - x == 1) return;
    else
    {
        int m = x +(y - x) / 2;  //划分
        int p = x,q = m,i = x;
        pai(x,m);  //递归
        pai(m,y);  //递归
        while(p < m || q < y) //合并
            if(q >= y || (p < m && A[p] <= A[q]))  //右半序列空的时候,直接从另一个序列尾部复制元素,右半序列非空(可能左空右不空,可能左右都空)时,当且仅当第一个序列也非空的时候,且A[P]<=A[q]的时候,才复制A[p]
                T[i++] = A[p++];
            else  //否则全部复制A[q]  巧用||,好算法!
                T[i++] = A[q++];
        for(i = x;i < y;i++) A[i] = T[i];  //从辅助数组T复制回A数组
        memset(T,0,sizeof(T));
    }
}
int main()
{
    int n;
    cin>>n;
    for(int i = 0;i < n;i++)
        cin>>A[i];
    pai(0,n);
    for(int i = 0;i < n;i++)
        cout<<A[i]<<"  ";
}

2.快速排序:

按照分治法:①把数组重排后分成左右两部分(由于划分方法不同,不只有一种实现方法,会有不同的版本,设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。),使左边任意元素小于等于右边的   ②把左右两边的分别排序  ③不用合并。

一趟快速排序的算法是:
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1(最多n个数据);
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]赋给A[i];
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]赋给A[j];
5)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。

值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。



假设用户输入了如下数组:
下标
0
1
2
3
4
5
数据
6
2
7
3
8
9
创建变量i=0(指向第一个数据), j=5(指向最后一个数据), k=6(赋值为第一个数据的值)。
我们取走了下标0的数据,于是,我们需要找到一个数字来替换他。由于我们要把所有比6小的数移动到左面,所以我们可以开始寻找比6小的数并从右往左找。别急,我们要按顺序找哦。不断递减j的值,我们发现下标3的数据比6小,于是把3移到下标0(实际是i指向的位置。代码中要用i,因为后面还会循环这个步骤,不用i的话第二次循环:
下标
0
1
2
 
4
5
数据
3
2
7
6
8
9
i=0 j=3 k=6
由于变量k已经储存了下标0的数据,所以我们可以放心的把下标0覆盖了。如此一来,下标3虽然有数据,但是相当于没有了,因为数据已经复制到别的地方了。于是我们再找一个数据来替换他。这次要变成找比k大的了,而且要从前往后找了。递加变量i,发现下标2是第一个比k大的,于是用下标2的数据7替换j指向的下标3的数据,数据状态变成下表:
下标
0
1
2
3
4
5
数据
3
2
6
7
8
9
i=2 j=3 k=6
重复上面的步骤,递减变量j。这时,我们发现i和j“碰头”了:他们都指向了下标2。于是,循环结束,把k填回下标2里,即得到结果。
如果i和j没有碰头的话,就递加i找大的,还没有,就再递减j找小的,如此反复,不断循环。注意判断和寻找是同时进行的。
注意:快速排序不会直接得到最终结果,只会把比k大和比k小的数分到k的两边。(你可以想象一下i和j是两个机器人,数据就是大小不一的石头,先取走i前面的石头留出回旋的空间,然后他们轮流分别挑选比k大和比k小的石头扔给对面,最后在他们中间把取走的那块石头放回去,于是比这块石头大的全扔给了j那一边,小的全扔给了i那一边。只是这次运气好,扔完一次刚好排整齐。)为了得到最后结果,需要再次对下标2两边的数组分别执行此步骤,然后再分解数组,直到数组不能再分解为止(只有一个数据),才能得到正确结果。

3.冒泡法(比较法)排序:

它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
冒泡排序算法的运作如下:(从后往前)
  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
 冒泡排序最好的 时间复杂度
 ,最坏时间复杂度为
 。 综上,因此冒泡排序总的平均时间复杂度为
 。
算法稳定性:冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
以下是模板:
#include<stdio.h>
#define SIZE 8
voidbubble_sort(inta[],int n)//n为数组a的元素个数
{
    int i,j,temp;
    for(j=0;j<n-1;j++)
        for(i=j;i<n-1;i++)
            if(a[i]>a[i+1])//数组元素大小按升序排列
            {
                temp=a[i];
                a[i]=a[i+1];
                a[i+1]=temp;
            }
}
int main()
{
    int number[SIZE]={95,45,15,78,84,51,24,12};
    int i;
    bubble_sort(number,SIZE);
    for(i=0;i<SIZE;i++)
    {
        printf("%d",number[i]);
    }
    printf("\n");
}

4.选择法排序:

选择排序的基本思想是:每一趟在n-i+1(i=1,2,…n-1)个记录中选取关键字最小的记录作为有序序列中第i个记录。基于此思想的算法主要有简单选择排序、树型选择排序和堆排序。

简单选择排序的基本思想:
第1趟,在待排序记录r[1]~r[n]中选出最小的记录,将它与r[1]交换;
第2趟,在待排序记录r[2]~r[n]中选出最小的记录,将它与r[2]交换;
以此类推,第i趟在待排序记录r[i]~r[n]中选出最小的记录,将它与r[i]交换,使有序序列不断增长直到全部排序完毕。


以下为简单选择排序的存储状态,其中大括号内为无序区,大括号外为有序序列:
初始序列:{ 49 27 65 97 76 12 38 }
第1趟:12与49交换:12 { 27 65 97 76 49 38 }
第2趟:27不动 :12 27 { 65 97 76 49 38 }
第3趟:65与38交换:12 27 38 { 97 76 49 65 }
第4趟:97与49交换:12 27 38 49 { 76 97 65 }
第5趟:65与76交换:12 27 38 49 65 { 97 76 }
第6趟:97与76交换:12 27 38 49 65 76 97 完成
时间复杂度O(n2)。选择排序是不稳定的
#include<stdio.h>
void sa(int array[],int n)
{
    int i,j,k,temp;
    for(i=0;i<10;i++)
    {
        k=i;                 //保存i的值,用k来进行循环排序
        for(j=i+1;j<n;j++)         //将第i个元素后面的元素与第i个元素进行比较
            if(array[j]<array[k])   //如果第k=i个元素后面的元素小于i号元素,交换两个元素的标号,                            这样就将最小元素的标号放到最前面
                k=j;                //交换标号
            temp=array[k];          //循环结束后,交换两个标号下的元素的值
            array[k]=array[i];
            array[i]=temp;
    }
} 
void main()
{
    void sa(int array[],int n);
    int array[10],i;
    printf("enter the array:\n");
    for(i=0;i<10;i++)
        scanf("%d",&array[i]);
    sa(array,10);
    printf("the sorted array:\n");
    for(i=0;i<10;i++)
        printf("%d\t",array[i]);
    getch();

}


5.插入法排序:

所谓插入排序法,就是检查第i个数字,如果在它的左边的数字比它大,进行交换,这个动作一直继续下去,直到这个数字的左边数字比它还要小,就可以停止了。插入排序法主要的回圈有两个变数:i和j,每一次执行这个回圈,就会将第i个数字放到左边恰当的位置去。
假设我们输入的是 “5,1,4,2,3” 我们从第二个数字开始,这个数字是1,我们的任务只要看看1有没有正确的位置,我们的做法是和这个数字左边的数字来比,因此我们比较1和5,1比5小,所以我们就交换1和5,原来的排列就变成了“1,5,4,2,3 ”
接下来,我们看第3个数字有没有在正确的位置。这个数字是4,它的左边数字是5,4比5小,所以我们将4和5交换,排列变成了 “1,4,5,2,3 "我们必须继续看4有没有在正确的位置,4的左边是1,1比4小,4就维持不动了。
再来看第四个数字,这个数字是2,我们将2和它左边的数字相比,都比2大,所以就将2一路往左移动,一直移到2的左边是1,这时候排序变成了 “1,2,4,5,3 ”
最后,我们检查第五个数字,这个数字是3,3必须往左移,一直移到3的左边是2为止,所以我们的排列就变成了 “1,2,3,4,5 ”排序因此完成了。

优点:稳定,快;

时间复杂度:O(n^2)
# include <stdio.h>
main()
{
  int a[10],i,j,t;
  printf("Please input 10 numbers: ");
  for(i=0;i<10;i++)
    scanf("%d",&a[i]);
  for(i=1;i<10;i++)		/*外循环控制趟数,n个数从第2个数开始到最后共进行n-1次插入*/
  {
    t=a[i];    			/*将待插入数暂存于变量t中*/
    for( j=i-1 ; j>=0 && t>a[j] ; j-- ) 	/*在有序序列(下标0 ~ i-1)中寻找插入位置*/
      a[j+1]=a[j]; 		/*若未找到插入位置,则当前元素后移一个位置*/
    a[j+1]=t;       	/*找到插入位置,完成插入*/
  }
  printf("The sorted numbers: ");
  for(i=0;i<10;i++)
    printf("%d   ",a[i]);
  printf("\n");
}



6.树形选择排序(锦标赛排序)

7.堆排序:O(n log n) 只需要一个额外的辅助空间。

8.基数排序:O(n)

9.希尔排序(缩小增量排序):O(n^1.25)




10.典型比较排序法时间复杂度对比:

 

平均情况

最好情况

最坏情况

归并排序

O(nlogn)

O(nlogn)

O(nlogn)

快速排序

O(nlogn)

O(nlogn)

O(n2)

希尔排序

O(n1.5)

O(n)

O(n1.5)

插入排序

O(n2)

O(n)

O(n2)

选择排序

O(n2)

O(n2)

O(n2)

排序

时间复杂度O(nlogn)

排序

时间复杂度O(n2)

冒泡排序

时间复杂度O(n2)

基数排序

基数排序:时间复杂度为O(nlogn),当k=n时,为O(n)

归并排序占用附加存储较多,需要另外一个与原待排序对象数组同样大小的辅助数组。这是这个算法的缺点。

外部排序是对存放在外存的大型文件的排序,外部排序基于对有序归并段的归并,而其初始归并段的产生基于内部排序。

依据这些因素,可得出如下几点结论:

(1) 若n较小(如n值小于50),对排序稳定性不作要求时,宜采用选择排序方法,若关键字的值不接近逆序,亦可采用直接插入排序法。但如果规模相同,且记录本身所包含的信息域比较多的情况下应首选简单选择排序方法。因为直接插入排序方法中记录位置的移动操作次数比直接选择排序多,所以选用直接选择排序为宜。

(2) 如果序列的初始状态已经是一个按关键字基本有序的序列,则选择直接插入排序方法和冒泡排序方法比较合适,因为“基本”有序的序列在排序时进行记录位置的移动次数比较少。

(3) 如果n较大,则应采用时间复杂度为O(nlog2n)的排序方法,即快速排序、堆排序或归并排序方法。快速排序是目前公认的内部排序的最好方法,当待排序的关键字是随机分布时,快速排序所需的平均时间最少;堆排序所需的时间与快速排序相同,但辅助空间少于快速排序,并且不会出现最坏情况下时间复杂性达到O(n2)的状况。这两种排序方法都是不稳定的,若要求排序稳定则可选用归并排序。通常可以将它和直接插入排序结合在一起用。先利用直接插入排序求得两个子文件,然后,再进行两两归并。

 



11.二分查找(折半查找):


按照分治法:①把原序列划分成元素个数尽量接近的两个子序列   ②递归查找  

PS:只适用于有序序列,一般写成非递归形式

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cstdio>
//检索某个值在数组第一次出现的地方,二分法查找,迭代实现
using namespace std;
int A[99];
int bsearch(int *A,int x,int y,int v)
{
    int m;
    while(x < y)
    {
        m = x+ (y - x) / 2;
        if(A[m] == v) return m;
        else if(A[m] > v) y = m;
        else x = m + 1;
    }
    return -1;  //出现错误
}
int main()
{
    int n,v;
    cin>>n;
    for(int i = 0;i < n;i++)
        cin>>A[i];
    cin>>v;
    cout<<bsearch(A,0,n - 1,v)<<endl;
}

这种方法有漏洞:当数组有多个要查找的值的时候,返回的是中间的那个。下面的是改进版:
A[m] = v:至少已经找到一个,而左边可能还有,区间变为[x,m]
A[m] > v:所求的位置不可能在后面,但有可能是m,因此区间变为[x,m]
A[m] < v:m和前面的不行,区间变为[m + 1,y]

//二分法查找求下界
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cstdio>
//检索某个值在数组第一次出现的地方,二分法查找,迭代实现
using namespace std;
int A[99];
int bsearch(int *A,int x,int y,int v)
{
    int m;
    while(x < y)
    {
        m = x+ (y - x) / 2;
        if(A[m] >= v) y = m;
        else x = m + 1;
    }
    return x;
}
int main()
{
    int n,v;
    cin>>n;
    for(int i = 0;i < n;i++)
        cin>>A[i];
    cin>>v;
    cout<<bsearch(A,0,n - 1,v)<<endl;
}




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值