经典排序算法归纳笔记(1)


   常见的排序算法有选择排序、 冒泡排序、插入排序、希尔排序、归并排序、堆排序、快速排序这些都是以前教科书上教给我们的。科技在发展,人类在进步,在前人们不懈努力下新的排序算法总是层出不穷,特别是大数据时代关于海量数据的处理方面显得尤为重要,所以出现了诸如计数排序(couting sort)、桶排序(bucket sort)、基数排序(radix sort)。这些暂不属于我们的讨论范围。

选择排序
   所谓的“选择”就是在待排序列里,找出一个最大(小)的元素,然后将它放在序列某个位置,这就完成了一次选择过程。如果将这样的选择循环继续下去,就是我们所说的选择排序。这也是选择排序的精髓。
   注:本文所讨论的都是升序排序,降序也一样,没啥本质区别。

   假如,有一个无须序列A={6,3,1,9,2,5,8,7,4},选择排序的过程应该如下:
   第一趟:选择最小的元素,然后将其放置在数组的第一个位置A[0],将A[0]=6和A[2]=1进行交换,此时A={1,3,6,9,2,5,8,7,4};
   第二趟:由于A[0]位置上已经是最小的元素了,所以这次从A[1]开始,在剩下的序列里再选择一个最小的元素将其与A[1]进行交换。即这趟选择过程找到了最小元素A[4]=2,然后与A[1]=3进行交换,此时A={1,2,
6 ,9,3,5,8,7,4};
   第三趟:由于A[0]、A[1]已经有序,所以在A[2]~A[8]里再选择一个最小元素与A[2]进行交换,然后将这个过程一直循环下去直到A里所有的元素都排好序为止。这就是选择排序的精髓。因此,我们很容易写出选择排序的核心代码部分,即选择的过程,就是不断的比较、交换的过程。
   整个选择的过程如下图所示:
经典排序算法归纳笔记(1)_第1张图片
   其中,黄色表示即将要选择的位置,即该位置上要安放从该位置往后开始最小的那个元素;桔色表示在无须序列里最小元素所在的位置,经过交换后的结果如右边箭头所示。而所有的灰色块,表示那些位置上的元素已经排好序了。所以,整个选择排序算法的核心代码如下:

点击(此处)折叠或打开

  1. int min,tmp,i,j;
  2. for(i=0;i< span>len-1;i++){
  3.   min = i;             //在本趟选择过程中,我们要将最小的元素放在a[i]的位置上

  4.   for(j=i+1;j< span>len;j++) /××××××××××××××××××××××××××××××××××××××××××××××××××××××××
  5.     if(a[min]<a[j])     ×在剩下的len-i个元素里选择一个最小,然后用min记住其下标
  6.      min = j;           ××××××××××××××××××××××××××××××××××××××××××××××××××××××××/
  7.    /× 如果a[i]本身就已经是最小的元素,则不要交换,这样可以提高一点算法的效率×/
  8.   if(min != i){
  9.     swap(a[min],a[i])  //交换两个数
  10.   }
  11. }
    我们可以看到,不论待排序列是否有序,选择排序算法都要进行大量的比较、交换。第一趟选择过程中,需要比较n-1次,第二趟需要比较n-2次,第i次就需要n-i次比较,所以最后第n-1个元素只需要和第n个元素比较一次就可以了。如果我们将交换两个元素所耗费的时间认为是个常数忽略不计,每次比较所耗费的时间又是固定单位时间,那么整个选择排序所耗费的时间就是:
   1+2+3+4+5+... ...+(n-2)+(n-1)=n*(n-1)/2,其时间复杂度是O(n^2)。任何情况下,无论待排序列是否有序,选择排序所耗费的比较时间都是O(n^2)。唯一的区别在于,如果待排序列已经是升序的,那么最好的情况就是不需要任何交换;如果待排序列是逆序的,则最坏的情况就是总共需要交换n/2次。
   所以,选择排序算法,最好、最坏和平均时间复杂度都是O(n^2)。

冒泡排序
   冒泡排序算法的核心是每次冒泡过程中,比较相邻的两个元素,如果A[i]大于A[i+1],则将其交换,然后 A[i+1 ]和A[i+2]再进行比较,将大的元素往后放。这样一趟下来,最大元素就被逐次“冒”到序列的末尾了。继续以前面的序列A为例:我们将序列“竖起来”,这样看冒泡的效果会更好。
经典排序算法归纳笔记(1)_第2张图片
   第五趟没有画出来,实际上第五趟冒泡完了之后,整个序列就已经升序排列好了。上图中,在每一趟冒泡过程中,绿色都表示本趟、本次相邻两个元素比较后值较大者,而紫红色则是较小者。同样滴,我们也很容易就写出冒泡排序的核心算法:

点击(此处)折叠或打开

  1. int i;
  2. while(len--){
  3. for(i=0;i< span>len;i++){
  4. if(a[i]<a[i+1]){
  5.             swap(a[i],a[i+1]);  //交换两个数
  6. }
  7. }
  8. }
    因为a[i]要和a[i+1]进行比较,所以得确保i+1的值不会造成数组访问越界的情况。第一趟比较时序号i+1最大可以指向序列最后一个元素,即i+1=len-1,i此时的值即为i=len-2。注意理解上述代码第2行的while(len--)在冒泡排序算法上的用意。
    关于冒泡排序的时间复杂度,如果序列的长度为n,第一趟的冒泡需要的比较次数是n-1,第二趟冒泡比较时最后一个元素已经脱颖而出,所以第二趟冒泡的总比较次数是n-2,以此类推,第i次冒泡的比较次数是n-i。同样地,和选择排序一样,冒泡排序在最坏和最好的情况下,其时间复杂度都是O(n^2)。
   让我们把冒泡排序第五趟及剩下的过程再分析一下:
经典排序算法归纳笔记(1)_第3张图片
   虽然我们看到第五趟冒泡完成后,整个序列都已经有序了,但接下来的第六、第七、第八和第九趟冒泡过程依然要做相邻元素比较的无用功。基于此,有人提出了改进的冒泡排序算法。算法主体和传统的冒泡排序一致,改进之处在于将上述这种无用的比较操作给滤掉了。其核心思想是,在冒泡排序过程中,如果有一趟冒泡过程中没有发生交换操作,则待排序的序列一定已经有序了。这个结论可以用数学归纳法来证明,有兴趣的朋友可以去研究一下。反应到程序层面就是我们需要用一个标记变量来记录某趟排序是否有交换操作发生,如果有则继续冒泡的过程;如果没有则立即停止冒泡,此时待排序的序列一定已经有序了。代码的改动也很简单:

点击(此处)折叠或打开

  1. int i,goon=1;
  2. while(goon && len--){
  3.     goon=0;
  4. for(i=0;i< span>len;i++){
  5. if(a[i]<a[i+1]){
  6.             swap(a[i],a[i+1]);  //交换两个数
  7.             goon = 1;
  8. }
  9. }
  10. }
    改进之后的冒泡排序,时间复杂度最好的情况可以达到O(n),最坏的情况依然是O(n^2)。
   未完,待续...

你可能感兴趣的:(算法)