经典排序算法 -----冒泡排序,插入排序,快速排序,归并排序,堆排序

排序算法  

 
 
 
 
比较排序算法比较:
 
 
 
 
 

 
 
简单的比较排序  ----冒泡排序与插入排序
 
1.冒泡排序(Bubble Sort,台湾译为:泡沫排序或气泡排序)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。
走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
 
#include <iostream>
#include <algorithm>
using namespace std;


void bubblesort(int a[],int len )
{
  for(int i = 0; i<len;++i)
   for(int j = i+1; j<len;++j)
     if(a[i]>a[j])
        swap(a[i],a[j]);

}

int main()
{
   int a[]={2,8,3,5,4,6,7,1,0};
   int len = sizeof(a)/sizeof(int);
  
   bubblesort(a, len);
   for(int i =0; i<len;i++)
   cout<<a[i]<<" ";

   cout<<endl;


}

 

 
 
 冒泡排序算法效果如下:
 
 
 
 
 
 
 
 
2.  插入排序
1)基本思想:在要排序的一组数中,假设前面(n-1)[n>=2] 个数已经是排好顺序的,现在要把第n个数插到前面的有序数中,使得这n个数也是排好顺序的。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。如此反复循环,直到全部排好顺序。
 
 
2>实例
 
 
 
 
#include <iostream>


using namespace std;


void InsertSort( int a[], int length)
{
  for(int i =1; i<length;i++)
  {
       int temp = a[i];//  copy it first
        int j = i-1;
    for(;j>=0 && temp<a[j];j--)  // unsorted region;(0~(i-1) is sorted)
         a[j+1] = a[j];         // move back elements to empty a right position
   
        a[j+1] = temp;        // place it to the right position

  }
}


int main()
{
  int a[] = {3,4,7,8,9,0,2,1,10,5,6};
  int length = sizeof(a)/sizeof(int);

InsertSort(a,length);


  for(int i =0; i<length;i++)
    cout<<a[i]<<" ";

  cout<<endl;
return 0;

}

 

 
 
3.快速排序  -- 实用
 
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来,且在大部分真实世界的数据,可以决定设计的选择,减少所需时间的二次方项之可能性。
 
基本思想:选择一个基准元素,通常选择第一个元素或者最后一个元素,通过一趟扫描,将待排序列分成两部分,一部分比基准元素小,一部分大于等于基准元素,此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地排序划分的两部分。
 
 
挖坑填数方法,我个人觉得比较好理解! 推荐给大家!
 

它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。

该方法的基本思想是:

1.先从数列中取出一个数作为基准数。

2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。

3.再对左右区间重复第二步,直到各区间只有一个数。

 

虽然快速排序称为分治法,但分治法这三个字显然无法很好的概括快速排序的全部步骤。因此我的对快速排序作了进一步的说明:挖坑填数+分治法

先来看实例吧,定义下面再给出(最好能用自己的话来总结定义,这样对实现代码会有帮助)。

 以一个数组作为示例,取区间第一个数为基准数

0

1

2

3

4

5

6

7

8

9

72

6

57

88

60

42

83

73

48

85

 

 

 

 

初始时,i = 0;  j = 9;   X = a[i] = 72
由于已经将a[0]中的数保存到X中,可以理解成在数组a[0]上挖了个坑,可以将其它数据填充到这来。

从j开始向前找一个比X小或等于X的数。当j=8,符合条件,将a[8]挖出再填到上一个坑a[0]中。a[0]=a[8]; i++;  这样一个坑a[0]就被搞定了,但又形成了一个新坑a[8],这怎么办了?简单,再找数字来填a[8]这个坑。这次从i开始向后找一个大于X的数,当i=3,符合条件,将a[3]挖出再填到上一个坑中a[8]=a[3];

 

数组变为:

0

1

2

3

4

5

6

7

8

9

48

6

57

88

60

42

83

73

88

85

 

 

 

i = 3;   j = 7;   X=72

再重复上面的步骤,先从后向前找,再从前向后找

从j开始向前找,当j=5,符合条件,将a[5]挖出填到上一个坑中,a[3] = a[5];

从i开始向后找,当i=5时,由于i==j退出。

此时,i = j = 5,而a[5]刚好又是上次挖的坑,因此将X填入a[5]。

 

数组变为:

0

1

2

3

4

5

6

7

8

9

48

6

57

42

60

72

83

73

88

85

 
 
 
 
 
可以看出a[5]前面的数字都小于它,a[5]后面的数字都大于它。因此再对a[0…4]和a[6…9]这二个子区间重复上述步骤就可以了。
 

 对挖坑填数进行总结

1.i =L; j = R; 将基准数挖出形成第一个坑a[i]。

2.j--由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。

3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。

4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中。
 
 
----------------------------------------------------------------------------------------------
 
#include <iostream>

using namespace std;
#include <iostream>

using namespace std;

void quicksort(int a[], int left, int right){
     int first = left;
     int last = right;
     int key = a[left];
     if(left>=right) return ;
     while(first<last){

     while(first<last && a[last]>=key)
           last--;
      
             a[first] = a[last];
        
     while(first<last && a[first]<key) 
            first++;
       
            a[last]= a[first];

         }

         a[first] = key;
         quicksort(a,left , first-1);
         quicksort(a,first+1,right);
    

}

int main()
{

  int a[] = {8,2,6,1,4,3,5,7,0};
  int len = sizeof(a)/sizeof(int); 
quicksort(a,0,len-1);
  for(int i =0; i<len;++i)
     cout<<a[i]<<" " ;

  cout<<endl;

 
  return 0;
 

}

快速排序的效果图

 

 
 
 
4 归并排序 
 
 归并排序是将两个(或两个以上)有序数列合并成一个新的有序表示,即把待排序序列分为若干个子序列。每个序列都是有序的,然后把有序子序列合并为整体有序序列。 该算法是采用分治法(Divide and Conquer)的一个非常典型的应用
 
   这里先介绍下 将两个有序数列 合并成一个有序数列。
 
void mergeTwo(int a[],int m, int b[],int n,int c[]){
     int i ,j,k;   
      i =j =k=0;
      while(i<m&&j<n)
       {
         if(a[i]<b[j])
             c[k++] = a[i++];
          else
             c[k++] =b[j++];
        }
        while(i<m)
            c[k++]=a[i++];
        while(j<n)
            c[k++]=b[j++];

}

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

 

(2)实例:

 

----------------------------------------------------------------------------------------------------------------------------------------------
 
#include <iostream>

using namespace std;

void mergearray(int a[], int first, int mid, int last, int temp[])
{
     int i = first, j = mid + 1;
     int m = mid,   n = last;
     int k = 0;
    
     while (i <= m && j <= n)
     {
          if (a[i] <= a[j])
               temp[k++] = a[i++];
          else
               temp[k++] = a[j++];
     }
    
     while (i <= m)
          temp[k++] = a[i++];
    
     while (j <= n)
          temp[k++] = a[j++];
    
     for (i = 0; i < k; i++)
          a[first + i] = temp[i];
}
void mergesort(int a[], int first, int last, int temp[])
{
     if (first < last)
     {
          int mid = (first + last) / 2;
          mergesort(a, first, mid, temp);    //左边有序
          mergesort(a, mid + 1, last, temp); //右边有序
          mergearray(a, first, mid, last, temp); //再将二个有序数列合并
     }
}


int main()
{
  int a[] = {0,2,1,6,3,7,4,5,8};
  int len = sizeof(a)/sizeof(int);
 
    int * tmp = new int[len];
     if(tmp==NULL) return 1;
    mergesort(a,0,len-1,tmp);

   for(int i =0;i<len;++i)
     cout<<a[i]<<" ";

    cout<<endl;
    delete[] tmp;
   return 0;
}

 

 

归并排序效果图

 
 
 
参考归并排序链接: http://blog.csdn.net/morewindows/article/details/6678165/
 
5 堆排序
 
堆排序与 快速排序归并排序一样都是时间复杂度为O(N*logN)的几种常见排序方法
 
学习堆排序前,先讲解下什么是数据结构中的二叉堆。

二叉堆的定义

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

二叉堆满足二个特性:

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

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

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

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

堆的存储

 
一般都用数组来表示堆,i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。如第0个结点左右子结点下标分别为1和2。
 
 
 
 
下面先给出《数据结构C++语言描述》中最小堆的建立插入删除的图解,再给出本人的实现代码。
 
 
 
堆插入
 
每次插入都是将新数据放在数组最后。可以发现从这个新数据的父结点到根结点必然为一个有序的数列,现在的任务是将这个新数据插入到这个有序数据中——这就类似于直接插入排序中将一个数据并入到有序区间中。
 
 
void MinHeapFixup(int a[], int i)
{
  for(int j = (i-1)/2 ; (j>=0&& i!=0)&&(a[i]<a[j]); i= j,j=(j-1)/2)
      swap(a[i],a[j]);
}

// 在最小堆中加入新的数据num
void MinHeapAddNumber(int a[],int n , int num)
{
     a[n] = num;
     MinHeapFixup(a,n);
}

 

堆的删除

 
按定义,堆中每次都只能删除第0个数据。为了便于重建堆,实际的操作是将最后一个数据的值赋给根结点,然后再从根结点开始进行一次从上向下的调整。调整时先在左右儿子结点中找最小的,如果父结点比这个最小的子结点还小说明不需要调整了,反之将父结点和它交换后再考虑后面的结点。相当于从根结点将一个数据的“下沉”过程。下面给出代码:
 
//  从i节点开始调整,n为节点总数 从0开始计算 i节点的子节点为 2*i+1, 2*i+2
void MinHeapFixdown(int a[], int i, int n)
{
    int j, temp;

     temp = a[i];
     j = 2 * i + 1;
     while (j < n)
     {
          if (j + 1 < n && a[j + 1] < a[j]) //在左右孩子中找最小的
               j++;

          if (a[j] >= temp)
               break;

          a[i] = a[j];     //把较小的子结点往上移动,替换它的父结点
          i = j;
          j = 2 * i + 1;
     }
     a[i] = temp;
}
//在最小堆中删除数
void MinHeapDeleteNumber(int a[], int n)
{
     Swap(a[0], a[n - 1]);
     MinHeapFixdown(a, 0, n - 1);
}

 

 

堆化数组

有了堆的插入和删除后,再考虑下如何对一个数据进行堆化操作。要一个一个的从数组中取出数据来建立堆吧,不用!先看一个数组,如下图:

很明显,对叶子结点来说,可以认为它已经是一个合法的堆了即20,60, 65, 4, 49都分别是一个合法的堆。只要从A[4]=50开始向下调整就可以了。

然后再取A[3]=30,A[2] = 17,A[1] = 12,A[0] = 9分别作一次向下调整操作就可以了。下图展示了这些步骤:

//建立最小堆
void MakeMinHeap(int a[], int n)
{
     for (int i = n / 2 - 1; i >= 0; i--)
          MinHeapFixdown(a, i, n);
}
 

 

堆排序

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

由于堆也是用数组模拟的,故堆化数组后,第一次将A[0]与A[n - 1]交换,再对A[0…n-2]重新恢复堆。第二次将A[0]与A[n – 2]交换,再对A[0…n - 3]重新恢复堆,重复这样的操作直到A[0]与A[1]交换。

由于每次都是将最小的数据并入到后面的有序区间,故操作完成后整个数组就有序了。有点类似于直接选择排序

 

 void MinheapsortTodescendarray(int a[], int n)  
 {  
     for (int i = n - 1; i >= 1; i--)  
         {  
             Swap(a[i], a[0]);  
             MinHeapFixdown(a, 0, i);  
         }  
 }  

 

注意使用最小堆排序后是递减数组,要得到递增数组,可以使用最大堆。
 
 
---------------------------------------------------------
 
#include <iostream>
#include <algorithm>
using namespace std;


void MinHeapFixup(int a[], int i)
{
  for(int j = (i-1)/2 ; (j>=0&& i!=0)&&(a[i]<a[j]); i= j,j=(j-1)/2)
      swap(a[i],a[j]);
}

void MinHeapAddNumber(int a[],int n , int num)
{
     a[n] = num;
     MinHeapFixup(a,n);
}


void MinHeapFixdown(int a[], int i ,int n){
  int j,temp;
  j = 2*i+1;
  temp = a[i];
  while(j<n)
  {
    if(j+1<n && a[j+1]<a[j])
         ++j;   // find the min of the left and right child

    if(a[j]>=temp)
         break;

     a[i] = a[j];
     i = j;
     j = 2*j+1;
}
   a[i] = temp;

}

void MinHeapDeleteNumber(int a[], int n)
{
       swap(a[0],a[n]);
       MinHeapFixdown(a,0,n-1);

}


void MakeMinHeap(int a[],int n)
{
 
  for(int i = n/2-1;i>=0;i--)
     MinHeapFixdown(a,i,n);
}


void MinHeapSort(int a[],int n)
{
  for(int i = n-1;i>=1;i--)
   {

    swap(a[i],a[0]);
    MinHeapFixdown(a,0,i);

   }

}


int main()
{
   int a[] ={0,3,5,6,4,7,8,2,1};
   int len = sizeof(a)/sizeof(int);

   MakeMinHeap(a,len);
   MinHeapSort(a,len);

   for(int i = 0; i<len;i++)
   cout<<a[i]<<" ";
  
    cout<<endl;
    return 0;


}

 

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

 堆排序参考 :http://blog.csdn.net/morewindows/article/details/6709644/
 
 
 
 堆排序的效果图:
    
 
 
--------------------------------------------------------------------------------------------------------------------------------
 
 
上面的算法 :在排序的最终结果中,各个元素的次序依赖于他们之间的比较(比较排序)  对于包含n 个 元素的输入序列来说,任何比较排序在最坏的情况下都要经过O(logN)比较。
因此归并排序和堆排序是渐进最优的,并且任何已知的比较排序最多就是在常数因子上优于它们。
 
 
博客中的程序,均在 ubuntu 下,gcc 4.9.2 下编译,运行通过。可以直接运行。
 
在下一篇博客中将详细讲解 三种线性排序算法, 计数排序,基数排序与桶排序!
 
 
posted @ 2015-08-10 15:33  deanlan  阅读(2152)  评论(0编辑  收藏  举报