排序算法

来源:互联网 发布:淘宝专卖店是正品吗 编辑:程序博客网 时间:2024/05/22 12:25

转自:http://www.2cto.com/kf/201109/104886.html



排序算法有很多,所以在特定情景中使用哪一种算法很重要。为了选择合适的算法,可以按照建议的顺序考虑以下标准: 

(1)执行时间 
(2)存储空间 
(3)编程工作 
   对于数据量较小的情形,(1)(2)差别不大,主要考虑(3);而对于数据量大的,(1)为首要。 
  
主要排序法有: 
一、冒泡(Bubble)排序——相邻交换 
二、选择排序——每次最小/大排在相应的位置 
三、插入排序——将下一个插入已排好的序列中 
四、壳(Shell)排序——缩小增量 
五、归并排序 
六、快速排序 
七、堆排序 
八、拓扑排序 
九、锦标赛排序 
十、基数排序 
  
  
 
一、冒泡(Bubble)排序 
 
----------------------------------Code 从小到大排序n个数------------------------------------ 
void BubbleSortArray() 

      for(int i=1;i<n;i++) 
      { 
        for(int j=0;i<n-i;j++) 
         { 
              if(a[j]>a[j+1])//比较交换相邻元素 
               { 
                   int temp; 
                   temp=a[j]; a[j]=a[j+1]; a[j+1]=temp; 
               } 
         } 
      } 

-------------------------------------------------Code------------------------------------------------ 
效率 O(n2),适用于排序小列表。 
  
  
二、选择排序 
----------------------------------Code 从小到大排序n个数-------------------------------- 
void SelectSortArray() 

    int min_index; 
    for(int i=0;i<n-1;i++) 
    { 
         min_index=i; 
         for(int j=i+1;j<n;j++)//每次扫描选择最小项 
            if(arr[j]<arr[min_index])  min_index=j; 
         if(min_index!=i)//找到最小项交换,即将这一项移到列表中的正确位置 
         { 
             int temp; 
             temp=arr[i]; arr[i]=arr[min_index]; arr[min_index]=temp; 



-------------------------------------------------Code----------------------------------------- 
效率O(n2),适用于排序小的列表。 
  
  
三、插入排序 
--------------------------------------------Code 从小到大排序n个数------------------------------------- 
void InsertSortArray() 

for(int i=1;i<n;i++)//循环从第二个数组元素开始,因为arr[0]作为最初已排序部分 

    int temp=arr[i];//temp标记为未排序第一个元素 
    int j=i-1; 
while (j>=0 && arr[j]>temp)/*将temp与已排序元素从小到大比较,寻找temp应插入的位置*/ 

    arr[j+1]=arr[j]; 
    j--; 

arr[j+1]=temp; 


------------------------------Code-------------------------------------------------------------- 
最佳效率O(n);最糟效率O(n2)与冒泡、选择相同,适用于排序小列表 
若列表基本有序,则插入排序比冒泡、选择更有效率。 
  
  
四、壳(Shell)排序——缩小增量排序 
-------------------------------------Code 从小到大排序n个数------------------------------------- 
void ShellSortArray() 

  for(int incr=3;incr<0;incr--)//增量递减,以增量3,2,1为例 

       for(int L=0;L<(n-1)/incr;L++)//重复分成的每个子列表 

   for(int i=L+incr;i<n;i+=incr)//对每个子列表应用插入排序 
   { 
      int temp=arr[i]; 
      int j=i-incr; 
      while(j>=0&&arr[j]>temp) 
      { 
          arr[j+incr]=arr[j]; 
          j-=incr; 

arr[j+incr]=temp; 




--------------------------------------Code------------------------------------------- 
适用于排序小列表。 
效率估计O(nlog2^n)~O(n^1.5),取决于增量值的最初大小。建议使用质数作为增量值,因为如果增量值是2的幂,则在下一个通道中会再次比较相同的元素。 
壳(Shell)排序改进了插入排序,减少了比较的次数。是不稳定的排序,因为排序过程中元素可能会前后跳跃。 
  
  
五、归并排序 
----------------------------------------------Code 从小到大排序--------------------------------------- 
void MergeSort(int low,int high) 

   if(low>=high)   return;//每个子列表中剩下一个元素时停止 
   else int mid=(low+high)/2;/*将列表划分成相等的两个子列表,若有奇数个元素,则在左边子列表大于右侧子列表*/ 
   MergeSort(low,mid);//子列表进一步划分 
   MergeSort(mid+1,high); 
   int [] B=new int [high-low+1];//新建一个数组,用于存放归并的元素 
   for(int i=low,j=mid+1,k=low;i<=mid && j<=high;k++)/*两个子列表进行排序归并,直到两个子列表中的一个结束*/ 
   { 
       if (arr[i]<=arr[j];) 

    B[k]=arr[i]; 
    I++; 

else
    { B[k]=arr[j]; j++; } 

for(   ;j<=high;j++,k++)//如果第二个子列表中仍然有元素,则追加到新列表 
      B[k]=arr[j]; 
   for(   ;i<=mid;i++,k++)//如果在第一个子列表中仍然有元素,则追加到新列表中 
      B[k]=arr[i]; 
   for(int z=0;z<high-low+1;z++)//将排序的数组B的 所有元素复制到原始数组arr中 
      arr[z]=B[z]; 

-----------------------------------------------------Code--------------------------------------------------- 
效率O(nlogn),归并的最佳、平均和最糟用例效率之间没有差异。 
适用于排序大列表,基于分治法。 
  
六、快速排序 
------------------------------------Code-------------------------------------------- 
/*快速排序的算法思想:选定一个枢纽元素,对待排序序列进行分割,分割之后的序列一个部分小于枢纽元素,一个部分大于枢纽元素,再对这两个分割好的子序列进行上述的过程。*/                  void swap(int a,int b){int t;t =a ;a =b ;b =t ;} 
        int Partition(int [] arr,int low,int high) 
        { 
            int pivot=arr[low];//采用子序列的第一个元素作为枢纽元素 
            while (low < high) 
            { 
                //从后往前栽后半部分中寻找第一个小于枢纽元素的元素 
                while (low < high && arr[high] >= pivot) 
                { 
                    --high; 
                } 
                //将这个比枢纽元素小的元素交换到前半部分 
                swap(arr[low], arr[high]); 
                //从前往后在前半部分中寻找第一个大于枢纽元素的元素 
                while (low <high &&arr [low ]<=pivot ) 
                { 
                    ++low ; 
                } 
                swap (arr [low ],arr [high ]);//将这个枢纽元素大的元素交换到后半部分 
            } 
            return low ;//返回枢纽元素所在的位置 
        } 
        void QuickSort(int [] a,int low,int high) 
        { 
            if (low <high ) 
            { 
                int n=Partition (a ,low ,high ); 
                QuickSort (a ,low ,n ); 
                QuickSort (a ,n +1,high ); 
            } 
        } 
----------------------------------------Code------------------------------------- 
平均效率O(nlogn),适用于排序大列表。 
此算法的总时间取决于枢纽值的位置;选择第一个元素作为枢纽,可能导致O(n2)的最糟用例效率。若数基本有序,效率反而最差。选项中间值作为枢纽,效率是O(nlogn)。 
基于分治法。 
  
  
 
七、堆排序 
最大堆:后者任一非终端节点的关键字均大于或等于它的左、右孩子的关键字,此时位于堆顶的节点的关键字是整个序列中最大的。 
思想: 
(1)令i=l,并令temp= kl ; 
(2)计算i的左孩子j=2i+1; 
(3)若j<=n-1,则转(4),否则转(6); 
(4)比较kj和kj+1,若kj+1>kj,则令j=j+1,否则j不变; 
(5)比较temp和kj,若kj>temp,则令ki等于kj,并令i=j,j=2i+1,并转(3),否则转(6) 
(6)令ki等于temp,结束。 
-----------------------------------------Code--------------------------- 
void HeapSort(SeqIAst R) 
 
    { //对R[1..n]进行堆排序,不妨用R[0]做暂存单元    int I;    BuildHeap(R); //将R[1-n]建成初始堆for(i=n;i>1;i--) //对当前无序区R[1..i]进行堆排序,共做n-1趟。{      R[0]=R[1]; R[1]=R[i]; R[i]=R[0]; //将堆顶和堆中最后一个记录交换      Heapify(R,1,i-1);  //将R[1..i-1]重新调整为堆,仅有R[1]可能违反堆性质     }    } ---------------------------------------Code-------------------------------------- 
 
  
堆排序的时间,主要由建立初始堆和反复重建堆这两部分的时间开销构成,它们均是通过调用Heapify实现的。 
 
      堆排序的最坏时间复杂度为O(nlgn)。堆排序的平均性能较接近于最坏性能。     由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。     堆排序是就地排序,辅助空间为O(1),     它是不稳定的排序方法。 
 
  
堆排序与直接插入排序的区别: 
     直接选择排序中,为了从R[1..n]中选出关键字最小的记录,必须进行n-1次比较,然后在R[2..n]中选出关键字最小的记录,又需要做n-2次比较。事实上,后面的n-2次比较中,有许多比较可能在前面的n-1次比较中已经做过,但由于前一趟排序时未保留这些比较结果,所以后一趟排序时又重复执行了这些比较操作。 
     堆排序可通过树形结构保存部分比较结果,可减少比较次数。 
  
 
八、拓扑排序 
例 :学生选修课排课先后顺序 
拓扑排序:把有向图中各顶点按照它们相互之间的优先关系排列成一个线性序列的过程。 
方法: 
在有向图中选一个没有前驱的顶点且输出 
从图中删除该顶点和所有以它为尾的弧 
重复上述两步,直至全部顶点均已输出(拓扑排序成功),或者当图中不存在无前驱的顶点(图中有回路)为止。 
---------------------------------------Code-------------------------------------- 
void TopologicalSort()/*输出拓扑排序函数。若G无回路,则输出G的顶点的一个拓扑序列并返回OK,否则返回ERROR*/ 

      int indegree[M]; 
      int i,k,j; 
      char n; 
      int count=0; 
      Stack thestack; 
      FindInDegree(G,indegree);//对各顶点求入度indegree[0....num] 
      InitStack(thestack);//初始化栈 
      for(i=0;i<G.num;i++) 
          Console.WriteLine("结点"+G.vertices[i].data+"的入度为"+indegree[i]); 
      for(i=0;i<G.num;i++) 
      { 
           if(indegree[i]==0) 
              Push(thestack.vertices[i]); 
      } 
      Console.Write("拓扑排序输出顺序为:"); 
      while(thestack.Peek()!=null) 
      { 
               Pop(thestack.Peek()); 
               j=locatevex(G,n); 
               if (j==-2) 
                  { 
                         Console.WriteLine("发生错误,程序结束。"); 
                         exit(); 
                  } 
                Console.Write(G.vertices[j].data); 
                count++; 
                for(p=G.vertices[j].firstarc;p!=NULL;p=p.nextarc) 
                { 
                     k=p.adjvex; 
                     if (!(--indegree[k])) 
                         Push(G.vertices[k]); 
                } 
      } 
      if (count<G.num) 
          Cosole.WriteLine("该图有环,出现错误,无法排序。"); 
      else
          Console.WriteLine("排序成功。"); 

----------------------------------------Code-------------------------------------- 
算法的时间复杂度O(n+e)。 
  
  
 
九、锦标赛排序 
锦标赛排序的算法思想与体育比赛类似。 
    首先将n个数据元素两两分组,分别按关键字进行比较,得到n/2个比较的优胜者(关键字小者),作为第一步比较的结果保留下来, 
    然后对这n/2个数据元素再两两分组,分别按关键字进行比较,…,如此重复,直到选出一个关键字最小的数据元素为止。 
 
 
  
--------------------------------Code in C--------------------------------------- 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <math.h> 
#define SIZE 100000 
#define MAX 1000000 
struct node 

 long num;//关键字 
 char str[10]; 
 int lastwin;//最后胜的对手 
 int killer;//被击败的对手 
 long times;//比赛次数 
}data[SIZE]; 
long CompareNum=0; 
long ExchangeNum=0; 
long Read(char name[])//读取文件a.txt中的数据,并存放在数组data[]中;最后返回数据的个数 

 FILE *fp; 
 long i=1; 
 fp=fopen(name,"rw"); 
 fscanf(fp,"%d%s",&data[i].num,data[i].str); 
 while(!feof(fp)) 
 { 
  i++; 
  fscanf(fp,"%d%s",&data[i].num,data[i].str);  
 } 
 return (i-1); 

long Create(long num)//创建胜者树,返回冠军(最小数)在数组data[]中的下标 

 int i,j1,j2,max,time=1; 
 long min;//记录当前冠军的下标 
 for(i=1;pow(2,i-1)<num;i++) 
  ; 
 max=pow(2,i-1);//求叶子结点数目 
 for(i=1;i<=max;i++)//初始化叶子结点 
 { 
  data[i].killer=0; 
  data[i].lastwin=0; 
  data[i].times=0; 
  if(i>num) 
   data[i].num=MAX; 
 } 
 for(i=1;i<=max;i+=2)//第一轮比赛 
 { 
  ++CompareNum; 
  if(data[i].num <= data[i+1].num) 
  { 
   data[i].lastwin = i+1; 
   data[i+1].killer=i; 
   ++data[i].times; 
   ++data[i+1].times; 
   min=i; 
  } 
  else
  { 
   data[i+1].lastwin=i; 
   data[i].killer=i+1; 
   ++data[i].times; 
   ++data[i+1].times; 
   min=i+1; 
  } 
 } 
 j1=j2=0;//记录连续的两个未被淘汰的选手的下标 
 while(time <= (log(max)/log(2)))//进行淘汰赛 
 { 
  for(i=1;i<=max;i++) 
  { 
   if(data[i].times==time && data[i].killer==0)//找到一名选手 
   { 
    j2=i;//默认其为两选手中的后来的 
    if(j1==0)//如果第一位置是空的,则刚来的选手先来的 
     j1=j2; 
    else//否则刚来的选手是后来的,那么选手都已到场比赛开始 
    { 
     ++CompareNum; 
     if(data[j1].num <= data[j2].num)//先来的选手获胜 
     { 
      data[j1].lastwin = j2;//最后赢的是j2 
      data[j2].killer=j1;//j2是被j1淘汰的 
      ++data[j1].times; 
      ++data[j2].times;//两选手场次均加1  
      min=j1;//最小数下标为j1 
      j1=j2=0;//将j1,j2置0 
     } 
     else//同理 
     { 
      data[j2].lastwin=j1; 
      data[j1].killer=j2; 
      ++data[j1].times; 
      ++data[j2].times;      
      min=j2; 
      j1=j2=0; 
     } 
    } 
   } 
   
  } 
  time++;//轮数加1 
 } 
 return min;//返回冠军的下标 

void TournamentSort(long num)//锦标赛排序 

 long tag=Create(num);//返回最小数下标 
 FILE *fp1; 
 fp1=fopen("sort.txt","w+");//为写入创建并打开文件sort.txt 
 while(data[tag].num != MAX)//当最小值不是无穷大时 
 { 
  printf("%d %s\n",data[tag].num,data[tag].str);//输出数据 
  fprintf(fp1,"%d %s\n",data[tag].num,data[tag].str);//写入数据 
  data[tag].num=MAX;//将当前冠军用无穷大替换 
  tag=Create(num);//返回下一个冠军的下标  
 } 

int main() 

 int num; 
 char name[10]; 
 printf("Input name of the file:"); 
 gets(name); 
 num=Read(name);//读文件 
 TournamentSort(num);//锦标赛排序 
 printf("CompareNum=%d\nExchangeNum=%d\n",CompareNum,ExchangeNum); 
 return 0; 

------------------------------------------Code------------------------------------- 
  
  
十、基数排序 
基数排序又被称为桶排序。与前面介绍的几种排序方法相比较,基数排序和它们有明显的不同。 
    前面所介绍的排序方法都是建立在对数据元素关键字进行比较的基础上,所以可以称为基于比较的排序; 
    而基数排序首先将待排序数据元素依次“分配”到不同的桶里,然后再把各桶中的数据元素“收集”到一起。 
通过使用对多关键字进行排序的这种“分配”和“收集”的方法,基数排序实现了对多关键字进行排序。 
——————————————————————————————————————— 
例: 
    每张扑克牌有两个“关键字”:花色和面值。其大小顺序为: 
    花色:§<¨<?<a 
    面值:2<3<……<K<A 
    扑克牌的大小先根据花色比较,花色大的牌比花色小的牌大;花色一样的牌再根据面值比较大小。所以,将扑克牌按从小到大的次序排列,可得到以下序列: 
 §2,…,§A,¨2,…,¨A,?2,…,?A,a2,…,aA 
    这种排序相当于有两个关键字的排序,一般有两种方法实现。 
    其一:可以先按花色分成四堆(每一堆牌具有相同的花色),然后在每一堆牌里再按面值从小到大的次序排序,最后把已排好序的四堆牌按花色从小到大次序叠放在一起就得到排序的结果。 
其二:可以先按面值排序分成十三堆(每一堆牌具有相同的面值),然后将这十三堆牌按面值从小到大的顺序叠放在一起,再把整副牌按顺序根据花色再分成四堆(每一堆牌已按面值从小到大的顺序有序),最后将这四堆牌按花色从小到大合在一起就得到排序的结果。 
——————————————————————————————————————— 
实现方法: 
  最高位优先(Most Significant Digit first)法,简称MSD法:先按k1排序分组,同一组中记录,关键码k1相等,再对各组按k2排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码kd对各子组排序后。再将各组连接起来,便得到一个有序序列。 
  最低位优先(Least Significant Digit first)法,简称LSD法:先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列。 
---------------------------------Code in C#------------------------------------------ 
  using System; 
  using System.Collections.Generic; 
  using System.Linq; 
  using System.Text; 
  namespace LearnSort 
  { 
  class Program 
  { 
  static void Main(string[] args) 
  { 
  int[] arr = CreateRandomArray(10);//产生随机数组 
  Print(arr);//输出数组 
  RadixSort(ref arr);//排序 
  Print(arr);//输出排序后的结果 
  Console.ReadKey(); 
  } 
  public static void RadixSort(ref int[] arr) 
  { 
  int iMaxLength = GetMaxLength(arr); 
  RadixSort(ref arr, iMaxLength); 
  } 
  private static void RadixSort(ref int[] arr, int iMaxLength) 
  { 
  List<int> list = new List<int>();//存放每次排序后的元素 
  List<int>[] listArr = new List<int>[10];//十个桶 
  char currnetChar;//存放当前的字符比如说某个元素123 中的2 
  string currentItem;//存放当前的元素比如说某个元素123 
  for (int i = 0; i < listArr.Length; i++)//给十个桶分配内存初始化。 
  listArr[i] = new List<int>(); 
  for (int i = 0; i < iMaxLength; i++)//一共执行iMaxLength次,iMaxLength是元素的最大位数。 
  { 
  foreach (int number in arr)//分桶 
  { 
  currentItem = number.ToString();//将当前元素转化成字符串 
  try { currnetChar = currentItem[currentItem.Length-i-1]; }//从个位向高位开始分桶 
  catch { listArr[0].Add(number); continue; }//如果发生异常,则将该数压入listArr[0]。比如说5 是没有十位数的,执行上面的操作肯定会发生越界异常的,这正是期望的行为,我们认为5的十位数是0,所以将它压入listArr[0]的桶里。 
  switch (currnetChar)//通过currnetChar的值,确定它压人哪个桶中。 
  { 
  case '0': listArr[0].Add(number); break; 
  case '1': listArr[1].Add(number); break; 
  case '2': listArr[2].Add(number); break; 
  case '3': listArr[3].Add(number); break; 
  case '4': listArr[4].Add(number); break; 
  case '5': listArr[5].Add(number); break; 
  case '6': listArr[6].Add(number); break; 
  case '7': listArr[7].Add(number); break; 
  case '8': listArr[8].Add(number); break; 
  case '9': listArr[9].Add(number); break; 
  default: throw new Exception("unknow error"); 
  } 
  } 
  for (int j = 0; j < listArr.Length; j++)//将十个桶里的数据重新排列,压入list 
  foreach (int number in listArr[j].ToArray<int>()) 
  { 
  list.Add(number); 
  listArr[j].Clear();//清空每个桶 
  } 
  arr = list.ToArray<int>();//arr指向重新排列的元素 
  //Console.Write("{0} times:",i); 
  Print(arr);//输出一次排列的结果 
  list.Clear();//清空list 
  } 
  } 
  //得到最大元素的位数 
  private static int GetMaxLength(int[] arr) 
  { 
  int iMaxNumber = Int32.MinValue; 
  foreach (int i in arr)//遍历得到最大值 
  { 
  if (i > iMaxNumber) 
  iMaxNumber = i; 
  } 
  return iMaxNumber.ToString().Length;//这样获得最大元素的位数是不是有点投机取巧了... 
  } 
  //输出数组元素 
  public static void Print(int[] arr) 
  { 
  foreach (int i in arr) 
  System.Console.Write(i.ToString()+'\t'); 
  System.Console.WriteLine(); 
  } 
  //产生随机数组。随机数的范围是0到1000。参数iLength指产生多少个随机数 
  public static int[] CreateRandomArray(int iLength) 
  { 
  int[] arr = new int[iLength]; 
  Random random = new Random(); 
  for (int i = 0; i < iLength; i++) 
  arr[i] = random.Next(0,1001); 
  return arr; 
  } 
  } 
  } 
---------------------------------Code --------------------------------------------- 

基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的比较性排序法。



转: http://blog.csdn.net/xiazdong/article/details/8462393

排序的定义:
输入:n个数:a1,a2,a3,...,an
输出:n个数的排列:a1',a2',a3',...,an',使得a1'<=a2'<=a3'<=...<=an'。

In-place sort(不占用额外内存或占用常数的内存):插入排序、选择排序、冒泡排序、堆排序、快速排序。
Out-place sort:归并排序、计数排序、基数排序、桶排序。

当需要对大量数据进行排序时,In-place sort就显示出优点,因为只需要占用常数的内存。
设想一下,如果要对10000个数据排序,如果使用了Out-place sort,则假设需要用200G的额外空间,则一台老式电脑会吃不消,但是如果使用In-place sort,则不需要花费额外内存。

stable sort:插入排序、冒泡排序、归并排序、计数排序、基数排序、桶排序。
unstable sort:选择排序(5 8 5 2 9)、快速排序、堆排序。

为何排序的稳定性很重要?

在初学排序时会觉得稳定性有这么重要吗?两个一样的元素的顺序有这么重要吗?其实很重要。在基数排序中显得尤为突出,如下:




算法导论习题8.3-2说:如果对于不稳定的算法进行改进,使得那些不稳定的算法也稳定?
其实很简单,只需要在每个输入元素加一个index,表示初始时的数组索引,当不稳定的算法排好序后,对于相同的元素对index排序即可。

基于比较的排序都是遵循“决策树模型”,而在决策树模型中,我们能证明给予比较的排序算法最坏情况下的运行时间为Ω(nlgn),证明的思路是因为将n个序列构成的决策树的叶子节点个数至少有n!,因此高度至少为nlgn。

线性时间排序虽然能够理想情况下能在线性时间排序,但是每个排序都需要对输入数组做一些假设,比如计数排序需要输入数组数字范围为[0,k]等。

在排序算法的正确性证明中介绍了”循环不变式“,他类似于数学归纳法,"初始"对应"n=1","保持"对应"假设n=k成立,当n=k+1时"。

一、插入排序


特点:stable sort、In-place sort
最优复杂度:当输入数组就是排好序的时候,复杂度为O(n),而快速排序在这种情况下会产生O(n^2)的复杂度。
最差复杂度:当输入数组为倒序时,复杂度为O(n^2)
插入排序比较适合用于“少量元素的数组”。

其实插入排序的复杂度和逆序对的个数一样,当数组倒序时,逆序对的个数为n(n-1)/2,因此插入排序复杂度为O(n^2)。
在算法导论2-4中有关于逆序对的介绍。

伪代码:


证明算法正确性:

循环不变式:在每次循环开始前,A[1...i-1]包含了原来的A[1...i-1]的元素,并且已排序。

初始:i=2,A[1...1]已排序,成立。
保持:在迭代开始前,A[1...i-1]已排序,而循环体的目的是将A[i]插入A[1...i-1]中,使得A[1...i]排序,因此在下一轮迭代开       始前,i++,因此现在A[1...i-1]排好序了,因此保持循环不变式。
终止:最后i=n+1,并且A[1...n]已排序,而A[1...n]就是整个数组,因此证毕。

而在算法导论2.3-6中还问是否能将伪代码第6-8行用二分法实现?

实际上是不能的。因为第6-8行并不是单纯的线性查找,而是还要移出一个空位让A[i]插入,因此就算二分查找用O(lgn)查到了插入的位置,但是还是要用O(n)的时间移出一个空位。

问:快速排序(不使用随机化)是否一定比插入排序快?

答:不一定,当输入数组已经排好序时,插入排序需要O(n)时间,而快速排序需要O(n^2)时间。

递归版插入排序




二、冒泡排序


特点:stable sort、In-place sort
思想:通过两两交换,像水中的泡泡一样,小的先冒出来,大的后冒出来。
最坏运行时间:O(n^2)
最佳运行时间:O(n^2)(当然,也可以进行改进使得最佳运行时间为O(n))

算法导论思考题2-2中介绍了冒泡排序。

伪代码:



证明算法正确性:

运用两次循环不变式,先证明第4-6行的内循环,再证明外循环。

内循环不变式:在每次循环开始前,A[j]是A[j...n]中最小的元素。

初始:j=n,因此A[n]是A[n...n]的最小元素。
保持:当循环开始时,已知A[j]是A[j...n]的最小元素,将A[j]与A[j-1]比较,并将较小者放在j-1位置,因此能够说明A[j-1]是A[j-1...n]的最小元素,因此循环不变式保持。
终止:j=i,已知A[i]是A[i...n]中最小的元素,证毕。

接下来证明外循环不变式:在每次循环之前,A[1...i-1]包含了A中最小的i-1个元素,且已排序:A[1]<=A[2]<=...<=A[i-1]。

初始:i=1,因此A[1..0]=空,因此成立。
保持:当循环开始时,已知A[1...i-1]是A中最小的i-1个元素,且A[1]<=A[2]<=...<=A[i-1],根据内循环不变式,终止时A[i]是A[i...n]中最小的元素,因此A[1...i]包含了A中最小的i个元素,且A[1]<=A[2]<=...<=A[i-1]<=A[i]
终止:i=n+1,已知A[1...n]是A中最小的n个元素,且A[1]<=A[2]<=...<=A[n],得证。

在算法导论思考题2-2中又问了”冒泡排序和插入排序哪个更快“呢?

一般的人回答:“差不多吧,因为渐近时间都是O(n^2)”。
但是事实上不是这样的,插入排序的速度直接是逆序对的个数,而冒泡排序中执行“交换“的次数是逆序对的个数,因此冒泡排序执行的时间至少是逆序对的个数,因此插入排序的执行时间至少比冒泡排序快。


递归版冒泡排序




改进版冒泡排序


最佳运行时间:O(n)
最坏运行时间:O(n^2)



三、选择排序


特性:In-place sort,unstable sort。
思想:每次找一个最小值。
最好情况时间:O(n^2)。
最坏情况时间:O(n^2)。

伪代码:


证明算法正确性:

循环不变式:A[1...i-1]包含了A中最小的i-1个元素,且已排序。

初始:i=1,A[1...0]=空,因此成立。
保持:在某次迭代开始之前,保持循环不变式,即A[1...i-1]包含了A中最小的i-1个元素,且已排序,则进入循环体后,程序从         A[i...n]中找出最小值放在A[i]处,因此A[1...i]包含了A中最小的i个元素,且已排序,而i++,因此下一次循环之前,保持       循环不变式:A[1..i-1]包含了A中最小的i-1个元素,且已排序。
终止:i=n,已知A[1...n-1]包含了A中最小的i-1个元素,且已排序,因此A[n]中的元素是最大的,因此A[1...n]已排序,证毕。


算法导论2.2-2中问了"为什么伪代码中第3行只有循环n-1次而不是n次"?

在循环不变式证明中也提到了,如果A[1...n-1]已排序,且包含了A中最小的n-1个元素,则A[n]肯定是最大的,因此肯定是已排序的。


递归版选择排序



递归式:

T(n)=T(n-1)+O(n) 
=> T(n)=O(n^2)

四、归并排序


特点:stable sort、Out-place sort
思想:运用分治法思想解决排序问题。
最坏情况运行时间:O(nlgn)
最佳运行时间:O(nlgn)

分治法介绍:分治法就是将原问题分解为多个独立的子问题,且这些子问题的形式和原问题相似,只是规模上减少了,求解完子问题后合并结果构成原问题的解。
分治法通常有3步:Divide(分解子问题的步骤) 、 Conquer(递归解决子问题的步骤)、 Combine(子问题解求出来后合并成原问题解的步骤)。
假设Divide需要f(n)时间,Conquer分解为b个子问题,且子问题大小为a,Combine需要g(n)时间,则递归式为:
T(n)=bT(n/a)+f(n)+g(n)

算法导论思考题4-3(参数传递)能够很好的考察对于分治法的理解。

就如归并排序,Divide的步骤为m=(p+q)/2,因此为O(1),Combine步骤为merge()函数,Conquer步骤为分解为2个子问题,子问题大小为n/2,因此:
归并排序的递归式:T(n)=2T(n/2)+O(n)

而求解递归式的三种方法有:
(1)替换法:主要用于验证递归式的复杂度。
(2)递归树:能够大致估算递归式的复杂度,估算完后可以用替换法验证。
(3)主定理:用于解一些常见的递归式。

伪代码:



证明算法正确性:

其实我们只要证明merge()函数的正确性即可。
merge函数的主要步骤在第25~31行,可以看出是由一个循环构成。

循环不变式:每次循环之前,A[p...k-1]已排序,且L[i]和R[j]是L和R中剩下的元素中最小的两个元素。
初始:k=p,A[p...p-1]为空,因此已排序,成立。
保持:在第k次迭代之前,A[p...k-1]已经排序,而因为L[i]和R[j]是L和R中剩下的元素中最小的两个元素,因此只需要将L[i]和R[j]中最小的元素放到A[k]即可,在第k+1次迭代之前A[p...k]已排序,且L[i]和R[j]为剩下的最小的两个元素。
终止:k=q+1,且A[p...q]已排序,这就是我们想要的,因此证毕。

归并排序的例子:


问:归并排序的缺点是什么?

答:他是Out-place sort,因此相比快排,需要很多额外的空间。

问:为什么归并排序比快速排序慢?

答:虽然渐近复杂度一样,但是归并排序的系数比快排大。

问:对于归并排序有什么改进?

答:就是在数组长度为k时,用插入排序,因为插入排序适合对小数组排序。在算法导论思考题2-1中介绍了。复杂度为O(nk+nlg(n/k)) ,当k=O(lgn)时,复杂度为O(nlgn)

五、快速排序


Tony Hoare爵士在1962年发明,被誉为“20世纪十大经典算法之一”。
算法导论中讲解的快速排序的PARTITION是Lomuto提出的,是对Hoare的算法进行一些改变的,而算法导论7-1介绍了Hoare的快排。
特性:unstable sort、In-place sort。
最坏运行时间:当输入数组已排序时,时间为O(n^2),当然可以通过随机化来改进(shuffle array 或者 randomized select pivot),使得期望运行时间为O(nlgn)。
最佳运行时间:O(nlgn)
快速排序的思想也是分治法。
当输入数组的所有元素都一样时,不管是快速排序还是随机化快速排序的复杂度都为O(n^2),而在算法导论第三版的思考题7-2中通过改变Partition函数,从而改进复杂度为O(n)。

注意:只要partition的划分比例是常数的,则快排的效率就是O(nlgn),比如当partition的划分比例为10000:1时(足够不平衡了),快排的效率还是O(nlgn)

“A killer adversary for quicksort”这篇文章很有趣的介绍了怎么样设计一个输入数组,使得quicksort运行时间为O(n^2)。

伪代码:



随机化partition的实现:



改进当所有元素相同时的效率的Partition实现:



证明算法正确性:

对partition函数证明循环不变式:A[p...i]的所有元素小于等于pivot,A[i+1...j-1]的所有元素大于pivot。
初始:i=p-1,j=p,因此A[p...p-1]=空,A[p...p-1]=空,因此成立。
保持:当循环开始前,已知A[p...i]的所有元素小于等于pivot,A[i+1...j-1]的所有元素大于pivot,在循环体中,
            - 如果A[j]>pivot,那么不动,j++,此时A[p...i]的所有元素小于等于pivot,A[i+1...j-1]的所有元素大于pivot。
            - 如果A[j]<=pivot,则i++,A[i+1]>pivot,将A[i+1]和A[j]交换后,A[P...i]保持所有元素小于等于pivot,而A[i+1...j-1]的所有元素大于pivot。
终止:j=r,因此A[p...i]的所有元素小于等于pivot,A[i+1...r-1]的所有元素大于pivot。

六、堆排序


1964年Williams提出。

特性:unstable sort、In-place sort。
最优时间:O(nlgn)
最差时间:O(nlgn)
此篇文章介绍了堆排序的最优时间和最差时间的证明:http://blog.csdn.net/xiazdong/article/details/8193625 
思想:运用了最小堆、最大堆这个数据结构,而堆还能用于构建优先队列。

优先队列应用于进程间调度、任务调度等。
堆数据结构应用于Dijkstra、Prim算法。



证明算法正确性:

(1)证明build_max_heap的正确性:
循环不变式:每次循环开始前,A[i+1]、A[i+2]、...、A[n]分别为最大堆的根。

初始:i=floor(n/2),则A[i+1]、...、A[n]都是叶子,因此成立。
保持:每次迭代开始前,已知A[i+1]、A[i+2]、...、A[n]分别为最大堆的根,在循环体中,因为A[i]的孩子的子树都是最大堆,因此执行完MAX_HEAPIFY(A,i)后,A[i]也是最大堆的根,因此保持循环不变式。
终止:i=0,已知A[1]、...、A[n]都是最大堆的根,得到了A[1]是最大堆的根,因此证毕。

(2)证明heapsort的正确性:
循环不变式:每次迭代前,A[i+1]、...、A[n]包含了A中最大的n-i个元素,且A[i+1]<=A[i+2]<=...<=A[n],且A[1]是堆中最大的。

初始:i=n,A[n+1]...A[n]为空,成立。
保持:每次迭代开始前,A[i+1]、...、A[n]包含了A中最大的n-i个元素,且A[i+1]<=A[i+2]<=...<=A[n],循环体内将A[1]与A[i]交换,因为A[1]是堆中最大的,因此A[i]、...、A[n]包含了A中最大的n-i+1个元素且A[i]<=A[i+1]<=A[i+2]<=...<=A[n],因此保持循环不变式。
终止:i=1,已知A[2]、...、A[n]包含了A中最大的n-1个元素,且A[2]<=A[3]<=...<=A[n],因此A[1]<=A[2]<=A[3]<=...<=A[n],证毕。

七、计数排序


特性:stable sort、out-place sort。
最坏情况运行时间:O(n+k)
最好情况运行时间:O(n+k)

当k=O(n)时,计数排序时间为O(n)

伪代码:



八、基数排序


本文假定每位的排序是计数排序。
特性:stable sort、Out-place sort。
最坏情况运行时间:O((n+k)d)
最好情况运行时间:O((n+k)d)

当d为常数、k=O(n)时,效率为O(n)
我们也不一定要一位一位排序,我们可以多位多位排序,比如一共10位,我们可以先对低5位排序,再对高5位排序。
引理:假设n个b位数,将b位数分为多个单元,且每个单元为r位,那么基数排序的效率为O[(b/r)(n+2^r)]。
当b=O(nlgn),r=lgn时,基数排序效率O(n)

比如算法导论习题8.3-4:说明如何在O(n)时间内,对0~n^2-1之间的n个整数排序?
答案:将这些数化为2进制,位数为lg(n^2)=2lgn=O(lgn),因此利用引理,b=O(lgn),而我们设r=lgn,则基数排序可以在O(n)内排序。

基数排序的例子:




证明算法正确性:

通过循环不变式可证,证明略。

九、桶排序


假设输入数组的元素都在[0,1)之间。
特性:out-place sort、stable sort
最坏情况运行时间:当分布不均匀时,全部元素都分到一个桶中,则O(n^2),当然[算法导论8.4-2]也可以将插入排序换成堆排序、快速排序等,这样最坏情况就是O(nlgn)。
最好情况运行时间:O(n)

桶排序的例子:


伪代码:



证明算法正确性:

对于任意A[i]<=A[j],且A[i]落在B[a],A[j]落在B[b],我们可以看出a<=b,因此得证。