飞鸟各投林

导航

第8天排序

7.1    概 述
7.2  插入排序
7.3  快速排序
7.4  堆排序
7.5  归并排序
7.6  基数排序
7.7  各种排序方法的综合比较
 
7.1    概 述
一、排序的定义
二、内部排序和外部排序
三、内部排序方法的分类
 
内部排序和外部排序
若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序;   
反之,若参加排序的记录数量很大,  整个序列的排序过程不可能在内存中 完成,则称此类排序问题为外部排序。
 
三、内部排序的方法
内部排序的过程是一个逐步扩大 记录的有序序列长度的过程。
 
 
 基于不同的“扩大” 有序序列长度的方法,内部排序方法大致可分下列几种类型:
 
 插入类  交换类  选择类   归并类
 
//插入类:  (1)直接插入排序 (2)折半插入排序  (3)希尔排序
//交换类;  (1)冒泡排序     (2)一趟快速排序  (3)快速排序
//选择类:  (1)简单选择排序 (2) 堆排序
//归并类:  (1)归并排序
 
待排记录的数据类型定义如下:
#define MAXSIZE  1000 // 待排顺序表最大长度
typedef  int  KeyType;  // 关键字类型为整数类型
typedef  struct {
    KeyType   key;             // 关键字项
    InfoType  otherinfo;       // 其它数据项
} RcdType;                     // 记录类型
 
typedef  struct {
    RcdType    r[MAXSIZE+1];   // r[0]闲置
    int         length;        // 顺序表长度
} SqList;                      // 顺序表类型
 
 
1. 插入类
将无序子序列中的一个或几个记录“插入”到有序序列中,从而增加记录的有序子序列的长度。
2. 交换类
通过“交换”无序序列中的记录从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。
3. 选择类
从记录的无序子序列中“选择”关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。
4. 归并类
通过归并”两个或两个以上的记录有序子序列,逐步增加记录有序序列的长度。
 
 
 
 
7. 2     插 入 排 序
 
 
实现“一趟插入排序”可分三步进行:
 
1.在R[1..i-1]中查找R[i]的插入位置, R[1..j].key <= R[i].key < R[j+1..i-1].key;
 
2.将R[j+1..i-1]中的所有记录均后移一个位置;
 
3.将R[i] 插入(复制)到R[j+1]的位置上。
 
 
不同的具体实现方法 导致不同的算法描述
直接插入排序(基于顺序查找)
折半插入排序(基于折半查找)
希尔排序(基于逐趟缩小增量
 
一、直接插入排序
利用 “顺序查找”实现 “在R[1..i-1]中查找R[i]的插入位置”
算法的实现要点:
从R[i-1]起向前进行顺序查找,监视哨设置在R[0];
R[0] = R[i];                                     // 设置“哨兵
for (j=i-1; R[0].key<R[j].key; --j);    // 从后往前找
循环结束表明R[i]的插入位置为 j +1
 
对于在查找过程中找到的那些关键字不小于R[i].key的记录,并在查找的同时实现记录向后移动;
for (j=i-1; R[0].key<R[j].key; --j);          
R[j+1] = R[j]
上述循环结束后可以直接进行“插入
 
void InsertionSort ( SqList &L ) {
  // 对顺序表 L 作直接插入排序。
   for ( i=2; i<=L.length; ++i )
       if (L.r[i].key < L.r[i-1].key)
    {
           L.r[0] = L.r[i];            // 复制为监视哨
           for ( j=i-1; L.r[0].key < L.r[j].key;  -- j )
           L.r[j+1] = L.r[j];        // 记录后移
           L.r[j+1] = L.r[0];        // 插入到正确位
       }
} // InsertSort
 
 

内部排序的时间分析:

实现内部排序的基本操作有两个:
1)“比较”序列中两个关键字的 大小;
2)“移动”记录。
 
二、折半插入排序
 
 因为 R[1..i-1] 是一个按关键字有序的有序序列,则可以利用折半查找实现
“在R[1..i-1]中查找R[i]的插入位置”,如此实现的插入排序为折半插入排序。
 
void BiInsertionSort ( SqList &L ) {
    for ( i=2; i<=L.length; ++i ) {
       L.r[0] = L.r[i];      // 将 L.r[i] 暂存到 L.r[0]
       // L.r[1..i-1]中折半查找插入位置
       for ( j=i-1;  j>=high+1;  --j )
        L.r[j+1] = L.r[j];      // 记录后移
        L.r[high+1] = L.r[0];  // 插入
      } // for
} // BInsertSort
low = 1;   high = i-1;
while (low<=high) {
m = (low+high)/2;           // 折半
if (L.r[0].key < L.r[m].key)
        high = m-1;   // 插入点在低半区
else  low = m+1; // 插入点在高半区
}
 三、希尔排序 (又称缩小增量排序)
基本思想:对待排记录序列先作“宏观”调整,再作“微观”调整。
所谓“宏观”调整,指的是,“跳跃式”的插入排序。    具体做法为:
 
记录序列分成若干子序列, 分别对每个子序列进行插入排序。
例如:将 n 个记录分成 d 个子序列:  
{ R[1],R[1+d],R[1+2d],…,R[1+kd] }  
{ R[2],R[2+d],R[2+2d],…,R[2+kd] }   
 …  
{ R[d],R[2d],R[3d],…,R[kd],R[(k+1)d] }
 
其中,d 称为增量,它的值在排序过程中从大到小逐渐缩小,
直至最后一趟排序减为 1。
 
 
 
 
void ShellInsert ( SqList &L, int dk ) {
   for ( i=dk+1; i<=n; ++i )
      if ( L.r[i].key< L.r[i-dk].key) {
        L.r[0] = L.r[i];            // 暂存在R[0]
        for (j=i-dk;  j>0&&(L.r[0].key<L.r[j].key);j-=dk)      
           L.r[j+dk] = L.r[j];  // 记录后移,查找插入位置
        L.r[j+dk] = L.r[0];                // 插入
      } // if
} // ShellInsert
 
7.3     快 速 排 序
一、起泡排序      
二、一趟快速排序
三、快速排序
 
一、起泡排序 
void BubbleSort(Elem R[ ], int n) {
     i = n;
     while (i >1)
  {
       lastExchangeIndex = 1;
       for (j = 1;  j < i;  j++)
       if (R[j+1].key < R[j].key)
     {
         Swap(R[j], R[j+1]);
         lastExchangeIndex = j;  //记下进行交换的记录位置
           } //if
       i = lastExchangeIndex; // 本趟进行过交换的        
     } // while    // 最后一个记录的位置
} // BubbleSort
 
二、一趟快速排序(一次划分)
目标:找一个记录,以它的关键字作为“枢轴”,凡其关键字小于枢轴的记录均移动至该记录之前,
反之,凡关键字大于枢轴的记录均移动至该记录之后。
致使一趟排序之后,记录的无序序列R[s..t]将分割成两部分:
R[s..i-1]和R[i+1..t],且 R[j].key≤ R[i].key ≤ R[j].key  (s≤j≤i-1)    枢轴     (i+1≤j≤t)。
int Partition (RedType R[], int low, int high) {
     R[0] = R[low];  
  pivotkey = R[low].key;  // 枢轴
     while (low<high)
  {
        while(low<high&& R[high].key>=pivotkey)
        -- high;      // 从右向左搜索
        R[low] = R[high];  
        while (low<high && R[low].key<=pivotkey)
        ++ low;      // 从左向右搜索
        R[high] = R[low];
     }
     R[low] = R[0];    
  return low;
}// Partition  
 
三、快速排序
先对无序的记录序列进行“一次划分”,之后分别对分割所得两个子序列“递归”进行快速排序。
结论: 快速排序的时间复杂度为O(nlogn),若待排记录的初始状态为按关键字有序时,快速排序将蜕化为起泡排序,其时间复杂度为O(n2)。
void QSort (RedType & R[],  int s,  int  t ) {
  // 对记录序列R[s..t]进行快速排序
    if (s < t-1)          // 长度大于1
     {            
         pivotloc = Partition(R, s, t);// 对 R[s..t] 进行一次划分                        
         QSort(R, s, pivotloc-1);// 对低子序列递归排序,pivotloc是枢轴位置      
         QSort(R, pivotloc+1, t); // 对高子序列递归排序
        }
}
假设一次划分所得枢轴位置 i=k,则对n 个记录进行快排所需时间:
 
快速排序的时间复杂度为O(nlogn),若待排记录的初始状态为按关键字有序时,快速排序将蜕化为起泡排序,其时间复杂度为O(n2)
 
10.4     堆 排 序
 
简 单 选 择 排 序
堆 排 序 
 
一、简单选择排序
void SelectSort (Elem R[], int n ) { // 对记录序列R[1..n]作简单选择排序。
        for (i=1; i<n; ++i)
     {
               // 选择第 i 小的记录,并交换到位
               j = SelectMinKey(R, i);      
              // 在 R[i..n] 中选择关键字最小的记录
             if (i!=j)  R[i]←→R[j];
              // 与第 i 个记录交换
            }
} // SelectSort
二:堆排序
 
{12, 36, 27, 65, 40, 34, 98, 81, 73, 55, 49} 是小顶堆
{12, 36, 27, 65, 40, 14, 98, 81, 73, 55, 49} 不是小顶堆
 
 
堆排序的时间复杂度为O(nlogn)。
void HeapSort ( HeapType &H ) {
  // 对顺序表 H 进行堆排序
  for ( i=H.length/2;   i>0;   --i )
     HeapAdjust ( H.r, i, H.length );    // 建大顶堆
  for ( i=H.length; i>1; --i )
    {
     H.r[1]←→H.r[i];          
     // 将堆顶记录和当前未经排序子序列
     //  H.r[1..i]中最后一个记录相互交换
     HeapAdjust(H.r, 1, i-1);  // 对 H.r[1] 进行筛选
    }
} // HeapSort
 
void HeapAdjust (RcdType &R[], int s, int m)
{   // 已知 R[s..m]中记录的关键字除 R[s] 之外均
    // 满足堆的特征,本函数自上而下调整 R[s] 的
    // 关键字,使 R[s..m] 也成为一个大顶堆
  rc = R[s];    // 暂存 R[s]
  for ( j=2*s; j<=m; j*=2 )
   { // j 初值指向左孩子自上而下的筛选过程;
    if ( j<m && R[j].key<R[j+1].key )  ++j;    
             // 左/右“子树根”之间先进行相互比较
             // 令 j 指示关键字较大记录的位置
    if ( rc.key >= R[j].key )  break;
          // 再作“根”和“子树根”之间的比较,
           // 若“>=”成立,则说明已找到 rc 的插
           // 入位置 s ,不需要继续往下调整
     R[s] = R[j];   s = j;    
        // 否则记录上移,尚需继续往下调整
   }
  R[s] = rc;  // 将调整前的堆顶记录插入到 s 位置
} // HeapAdjust
 
 
7.5   归 并 排 序
 
归并排序的过程基于下列基本思想进行:     将两个或两个以上的有序子序列 “归并” 为一个有序序列。 
 
在内部排序中,通常采用的是2-路归并排序。即:将两个位置相邻的记录有序子序列 归并为一个记录的有序序列。
 
对 n 个记录进行归并排序的时间复杂度为Ο(nlogn)
 
void Merge (RcdType SR[], RcdType &TR[], int i, int m, int n) {                                                    
  // 将有序的记录序列 SR[i..m] 和 SR[m+1..n]
  // 归并为有序的记录序列 TR[i..n]
  for (j=m+1, k=i;  i<=m && j<=n;  ++k)    // 将SR中记录由小到大地并入TR
    {                  
      if (SR[i].key<=SR[j].key)  
      TR[k] = SR[i++];
      else TR[k] = SR[j++];
    }
    if (i<=m)
  TR[k..n] = SR[i..m];
                 // 将剩余的 SR[i..m] 复制到 TR
 if (j<=n)
  TR[k..n] = SR[j..n];
          // 将剩余的 SR[j..n] 复制到 TR
} // Merge
7.7  各种排序方法的综合比较
 
 
 
1.  平均的时间性能
 
时间复杂度为 O(nlogn):快速排序、堆排序和归并排序
 
时间复杂度为 O(n2):直接插入排序、起泡排序和 简单选择排序
 
时间复杂度为 O(n): 基数排序
 
2. 当待排记录序列按关键字顺序有序时

直接插入排序和起泡排序能达到O(n)的时间复杂度,        
 
快速排序的时间性能从O(nlogn),蜕化为O(n2) 。
 
3. 简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变。
 
4.  快速排序、堆排序和希尔排序是不稳定的排序方法。
 
//插入类:  (1)直接插入排序 (2)折半插入排序  (3)希尔排序
//交换类;  (1)冒泡排序     (2)一趟快速排序  (3)快速排序
//选择类:  (1)简单选择排序 (2) 堆排序
//归并类:  (1)归并排序
 
特别关注:快速排序
快速排序 :先对无序的记录序列进行“一次划分”,之后分别对分割所得两个子序列“递归”进行快速排序。
结论: 快速排序的时间复杂度为O(nlogn),若待排记录的初始状态为按关键字有序时,快速排序将蜕化为起泡排序,其时间复杂度为O(n2)。
掌握各种排序方法的时间复杂度的分析方法。能从“关键字间的比较次数”分析排序算法的平均情况和最坏情况的时间性能。
按平均时间复杂度划分,内部排序可分为三类:
O(n2)的简单排序方法,
O(nlogn)的高效排序方法  
O(dn)的基数排序方法。

posted on 2016-04-11 16:59  飞鸟各投林  阅读(259)  评论(0编辑  收藏  举报