排序的知识点总结

        排序(sorting)是指将一组数据,按特定规则(排序算法)调换位置,使数据有序(递增或递减)。

        在排序过程中,数据的移动方式可分为“直接移动”和“逻辑移动”。“直接移动”使直接交换存储数据的物理位置,而“逻辑移动”并不会移动数据,仅改变指向这些数据的辅助指针的位置。一般大数据的排序不会使用直接移动,因为改变存储位置需要花费过多时间,相反改变其指针指向的位置则高效得多。

        一、排序的分类

        按照数据量的多寡和所使用的内存分为“内部排序(internal sort)”和“外部排序(external sort)”,数据量小则可以全部加载到内存(RAM)中进行排序就称为内部排序,大部分排序属于此类。数据量大无法全部一次加载到内存中,必须借助磁盘等辅助存储器进行排序则称为外部排序。

       二、常见内部排序算法的比较

        冒泡排序(稳定)、直接选择排序(不稳定)和插入排序(稳定)比较容易理解,适合数据量小,或有部分数据已经做过排序的情况; 当原始数据大部分已经排好序的情况下,插入排序会非常有效率,因为它不需要执行太多的数据搬移操作。

        希尔排序:是D.L.Shell在1959年7月发明的一种排序法,可以减少插入排序法中数据搬移的次数,以加速排序的进行。排序的原则是将数据区分城特定的几个小区块,以插入排序法排完区块内的数据后再渐渐减少区块的间隔距离。

    希尔排序C++代码示例:

void shell(int  data[], int size){
    int i; //扫描次数
    int j; //用j定位比较的元素
    int k = 1; //打印次数
    int tmp; //暂存数据
    int jmp; //设置间距位移量
    jmp = size >> 1;
    while(jmp != 0){
        for(i = jmp; i < size; ++i)
        {
            tmp = data[i];
            j = i - jmp;
            while(tmp < data[j[ && j >= 0){ //插入排序
                 data[j + jmp] = data[j];
                 j = j - jmp;            }
            data[jmp + j] = tmp;
        }
    cout << "The " << k++ << " th Sort: ";
    showdata(data);  //输出data数组的函数
    jmp = jmp >> 1; //控制循环次数    }}


        从以上算法可知,希尔排序是插入排序的变形,或者说优化,也是稳定排序。其时间复杂度分析比较复杂,实际所需的时间取决于各次排序时增量的个数和增量的取值。增量取值合理的情况下约为O((N*logN)^2)

        归并排序(merge sort)通常是外部存储器最常用的排序方法,工作原理是针对已排序好的两个或两个以上的文件,通过合并的方式,将其组合成一个大的且排好序的文件。

    归并排序的最大好处就是在数据呈现出最坏的情况时,是所有排序算法中最好的(与堆排序一样,都为O(N*logN)),也属于二分切割法的稳定排序,其最好、平均和最差时间复杂度都为O(N*logN)

    快速排序:又称为分割交换排序,公认的最佳排序法,使用“分治(二分)法”的思想。先会在数据中找出一个假定的中间值,并将数据中小于中间值的数据放在左边,大于中间值的放在右边。再以同样的方法分别处理左右两边的数据,直到排序完成。操作与分割的步骤如下:

    假设有n项R1、R2、R3...Rn记录,其键值为k1、k2、k3...kn:

    1、先假设K的值为第一个键值;

    2、从左向右找出键值Ki,使得Ki > K;

    3、从右向左找出键值Kj,使得Kj < K;

    4、如果I < j,那么Ki与Kj 交换,并回到步骤2;

    5、若I >= j则将K与Kj交换,并以j为基准点分割成左右部分。然后再针对左右两边进行步骤1-5,直到左半边键值=右半边键值为止

快速排序C++代码示例:

#include <iostream>
 
using namespace std;
 
int quicksort(vector<int> &v, int left, int right){
        if(left < right){
                int key = v[left];
                int low = left;
                int high = right;
                while(low < high){
                        while(low < high && v[high] > key){
                                high--;
                        }
                        v[low] = v[high];
                        while(low < high && v[low] < key){
                                low++;
                        }
                        v[high] = v[low];
                }
                v[low] = key;
                quicksort(v,left,low-1);
                quicksort(v,low+1,right);
        }
}

        快速排序的时间主要耗费在划分操作上,对长度为k的区间进行划分,共需k-1次关键字的比较。

        最坏情况是每次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中记录数目,仅仅比划分前的无序区中记录个数减少一个。时间复杂度为O(n*n),空间复杂度为O(n)

        在最好情况下,每次划分所取的基准都是当前无序区的"中值"记录,划分的结果是基准的左、右两个无序子区间的长度大致相等。总的关键字比较次数:O(nlgn)

        尽管快速排序的最坏时间为O(n2),但就平均性能而言,它是基于关键字比较的内部排序算法中速度最快者,快速排序亦因此而得名。它的平均时间复杂度为O(nlgn)。

        堆排序:可以算是直接选择排序算法的改进版,它可以减少在直接选择排序法中的比较次数,进而减少排序时间。

        二叉堆的定义:

              二叉堆是完全二叉树或者是近似完全二叉树。

         二叉堆满足二个特性:

         1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。

         2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。

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

       由于其它几种堆(二项式堆,斐波纳契堆等)用的较少,一般将二叉堆就简称为堆。

          一般都用数组来表示堆,i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。如第0个结点左右子结点下标分别为1和2。

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

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

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

       由于每次重新恢复堆的时间复杂度为O(logN),共N - 1次重新恢复堆操作,再加上前面建立堆时N / 2次向下调整,每次调整时间复杂度也为O(logN)。二次操作时间相加还是O(N * logN)。故堆排序的时间复杂度为O(N * logN)。STL也实现了堆(heap)的相关函数。

        堆和堆排序的概念可以参考:http://blog.csdn.net/morewindows/article/details/6709644

         基数排序:基数排序和之前所讨论的排序法都不太一样,它并不需要进行元素间的比较操作,而是属于一种分配模式排序方式。

         基数排序法按比较的方向可分为最高位优先(most significant digit first, MSD)和最低位优先(least significant digit first, LSD)两种。以LSD将三位数的整数数据来加以比较为例,它是按个位数、十位数、百位数来进行排序。下面举例如图:


            基数排序是一种稳定排序法(每次都是从左向右扫描,依次入桶),它需要使用很大的额外空间来存放列表(桶)数据。其空间复杂度为O(n*p),n是原始数据个数,p是数据的字符数。若n很大,p固定或很小,此排序算法将很有效率(以空间换时间效率)。其平均时间复杂度为O(d*(n+r)),d代表长度,r代表关键字基数,n代表关键字(数据)个数。

     三、外部排序算法

    当所要排序的数据量太多或文件太大,无法加载进内存内排序时,就需要依赖外部存储设备,使用外部排序法。外部存储设备又可按照存取方式分为两种:顺序存取(如磁带)和随机存储(如磁盘)。

    要顺序存储的文件就像是链表一样,必须遍历整个链表才有办法进行排序,而随机存取的文件就像数组,存取方便,所以相对而言,它的排序会比顺序存取快一些。一般来说,外部排序法最常用的就是归并排序算法,它适用于顺序长期的文件。

    直接(2-路)归并排序法:它可以分为两个步骤——

        1、将待排序的文件分为几个可以加载到内存空间大小的小文件,再使用内部排序法将各个文件内的数据排序;

        2、将第一步所建立的小文件每两个合并成一个文件。两两合并后,把所有文件合并成一个文件后就可以完成排序了。

        其中,两两合并的过程如下:

            1)首先再两个文件中分别读出一个元素进行比较,比较后较小的元素放入合并缓冲区内,同时较小的元素所在文件的文件指针往后移动一个元素;

            2)等到缓冲区的数据满了就进行写入文件操作,否则一直重复步骤1),直至所有元素都写入文件。

    多路归并排序法:如果把两两合并改为k个小文件合并,就得到k-路归并排序(多路归并排序)。需要知道的是,使用多路归并排序的原意是希望减少输入输出的时间,但合并k个轮次前要决定下一项输出的排列数据,必须进行k-1此比较才可以得到答案,也就是说减少了输入输出时间,但是增加了比较的时间,因此选择合适的k值才能在这两者之间取得平衡。


其他问题:

什么是稳定排序?

答:稳定排序是指数据经过排序后,两个相同键值的记录仍然保持原来的顺序。冒泡排序、插入排序、归并排序和基数排序都是稳定排序。

平均时间复杂度最好的有哪些?最好时间复杂度最好的有哪些?最坏时间复杂度最好的有哪些?

答:快速排序、堆排序、归并排序,都为O(N*logN)。直接插入排序、冒泡排序和希尔排序,都为O(N)。堆排序和归并排序,都为O(N*logN)。

如何改进快速排序?

答:快速排序执行时最好的情况是使两边的数据个数尽量一样,所以可以通过先找出实际中间值作为基准,此方法会使快排在最坏情况下的时间复杂度从O(N^2)提升为O(N*logN)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值