排序(三)

快速排序(QuickSort):

     快速排序是实践中最快的排序算法,它的平均运行时间是O(NlogN) ,它的最坏情形的性能是 O(N^2),但稍加努力就可以避免这种情形。像归并排序一样,快速排序也是一种分治的递归算法。

将数组S 排序的基本算法由下面的四个简单步骤组成:

    1) 如果S 的个数为0 或 1,则直接返回

    2) 取S 中任意元素 v ,称之为枢纽元(pivot)

    3) 将S 中剩余的元素分成两个不相交的集合: S1 和 S2

    4) 返回{quicksort(S1)} 继随 v ,继而{quicksort(S2)}

Why 快速排序比归并排序快?

    如同归并排序那样,快速排序递归的解决两个子问题并需要线性的附加工作(第(3)步),不过,与归并算法不同的是,两个子问题并不保证具有相等的大小,这是个潜在的隐患。快速排序更快的原因在于,第(3)步分割成两组实际上是在适当位置上进行并且非常高效,它的高效弥补了大小不等的递归调用的缺憾而且还有超出

  1. 枢纽元的选取:

    两种常用的错误方法:

    1. 将第一个元素作为枢纽元,如果输入是随机的,那么可以接受,但是如果输入是预排序的或者是反序的,那么这样的枢纽元就产生了一个劣质的分割,因为所有的元素不是都被划入 S1 中就是都被划入S2 中。更有甚者,这种情况可能发生在所有的递归调用中。实际上,如果第一个元素用做枢纽元而且输入是预先排序的,那么快速排序花费的时间将是二次的O(N^2)。

    2. 选取前两个互异的关键字中的较大者作为枢纽元,这和选取第一个元素作为枢纽元具有相同的害处。

    一种安全的做法: 随机选取枢纽元。一般来说这种策略非常安全,因为随机的枢纽元不可能总在接连不断的产生劣质的分割,另一方面,随机数的生成一般是昂贵的,根本减少不了算法其余部分的平均运行时间。

                三数中值分割法:一组N 个数的中值是第 2/N (上取整)个最大的数,枢纽元最好的选择是数组的中值。不幸的是,这很难算出,且明显减慢快速排序的速度。这样的中值估计量可以通过随机选取三个数并用他们的中值作为枢纽元而得到。事实上随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。

    2. 分割策略:

        在分割阶段要做的就是把所有小元素移到数组的左边而把所有大元素移到数组的右边。(大小是相对枢纽元素而言的)

    首先,将数组的最后一个元素与枢纽元互换,i 指向数组的第一个元素,j 指向数组的倒数第二个元素,当i 在 j 的左边时,将 i 右移,移过那些小于枢纽元的元素,并将j 左移,移过那些大于枢纽元的元素。当 i 和 j 停止时,i 指向一个大元素而 j 指向一个小元素。如果i 在 j 的左边,那么将这两个元素互换,其效果是把一个大元素移向右边而把一个小元素移向左边。当 i 大于 j 时,不再交换。分割的最后一步是将枢纽元与 i 所指向的元素交换。

考虑一个重要的细节是如何处理那些等于枢纽元的关键字:问题在于当 i 遇到一个等于枢纽元的关键字时,是否应该停止。直观地看,i 和 j 应该有同样的操作,否则分割将出现偏向一边的倾向。例如,如果 i 停, j 不停,则所有等于枢纽元的元素将被分到 S2中。考虑数组中所有元素都相等的情况。如果i 和 j 都停止,那么相等的元素间将有很多次交换,虽然这似乎没有什么意义,但是其正面的效果则是 i 和 j 将在中间交错,因此当枢纽元被代替时,这种分割建立了两个几乎相等的子数组,此时总的运行时间为

O(NlogN)。如果 i 和 j 都不停止,那么就应该有相应的程序防止 i 和 j 越出数组的界限,不进行交换的操作。虽然这样似乎不错,但是这样的做法将产生两个非常不均衡的子数组。如果所有的关键字都相同,那么运行时间为 O(N^2)。因此,如果i 和j 遇到等于枢纽元的关键字时,那么就让 i 和 j 都停止。

代码实现:

1. 随机选取枢纽元的方法:

//枢纽元的选取:产生某一范围内的随机数(包括边界)
int RandomInRange(int start, int end)
{
 srand(time(NULL));
 return start + rand() % (end - start + 1);
}

//交换两个数
void Swap(int *elem1, int *elem2)
{
 int temp;
 temp = *elem1;
 *elem1 = *elem2;
 *elem2 = temp;
}
// 选取枢纽元,将数组划分为两部分
int Partition(int data[], int length, int start, int end)
{
 if(data == NULL || length <= 0 || start < 0 || end >= length) //异常输入
  throw exception("Invalid input!");

 int index = RandomInRange(start, end);  //随机产生一个数index,将数组中第index个元素作为基准点

 Swap(&data[index], &data[end]);   //将枢纽元与最后一个元素交换,以便在遍历整个数组时,对数组进行划分

 int small = start - 1;  //small 指向已划分的小于枢纽元的那一部分的最后一个元素
 for(index = start; index < end; index ++)
 {
  if(data[index] < data[end])  //若某一元素小于枢纽元,则small就要+1
  {
   ++small;    // small != index 说明small与index中间是大于枢纽元的元素,而index位置处的元素却小于基准点
   if(small != index)   //若一划分好的小于枢纽元的那一部分的最后一个元素的下一个不等于当前所访问的元素[index],

                                    //则需要将两者进行互换
    Swap(&data[small], &data[index]);
  }
 }
 ++small;
 Swap(&data[end], &data[small]);
 return small;     //small 为最后的枢纽元
}

//快速排序

void QuickSort(int data[], int length, int start, int end)
{
 if(start == end)
  return;
 int index = Partition(data, length, start, end);  //选取枢纽元元素,并将数组进行划分
 
 if(index > start)  //有小于枢纽元的元素存在
 {
  QuickSort(data, length, start, index - 1);
 }
 if(index < end)   //有大于枢纽元的元素存在
 {
  QuickSort(data, length, index + 1, end);
 }
}

 

2. 三数中值分割法(选取枢纽元)

void InsertSort_1(int A[], int N)
{
 int i, j, tmp;
 for(i = 1; i < N; i++)
 {
  tmp = A[i];
  for(j = i; j > 0 && A[j - 1] > tmp; j--)
  {
   A[j] = A[j - 1];
  }
  A[j] = tmp;
 }
}
void swap(int *a, int *b)
{
 int tmp;
 tmp = *a;
 *a = *b;
 *b = tmp;
}
//三数中值分割法
ElemType Median3(ElemType A[], int Left, int Right)
{
 int Center = (Left + Right) / 2;
 if(A[Left] > A[Center])
  swap(&A[Left], &A[Center]);
 if(A[Left] > A[Right])
  swap(&A[Left], &A[Right]);
 if(A[Center] > A[Right])
  swap(&A[Center], &A[Right]);  //Left处的为最小值,Right为最大值,Center为中值
 swap(&A[Center], &A[Right - 1]);  //将center处的值作为枢纽元,并与倒数第二个元素互换【此时数组最后一个元素一定大

                                                        //于枢纽元,因此不必参与比较】
 return A[Right - 1];
}

// 递归快排

void Qsort(ElemType A[], int Left, int Right)
{
 int i, j;
 ElemType Pivot;
 if(Left + Cutoff <= Right)
 {
  Pivot = Median3(A, Left, Right);
  i = Left;
  j = Right - 1;
  for(;;)
  {
   while(A[++i] < Pivot){}  //i 右移
   while(A[--j] > Pivot){}  //i 左移
   if(i < j)
    swap(&A[i], &A[j]);  // A[i] >= Pivot and A[j] <= Pivot and i 在j 的左边时交换位置
   else
    break;
  }
  swap(&A[i], &A[Right - 1]);  //将枢纽元Pivot插入到数组的适当位置

  Qsort(A, Left, i - 1);   //左边数组递归调用快速排序
  Qsort(A, i + 1, Right);   //右边数组递归调用快速排序
 }
 else
  InsertSort_1(A + Left, Right - Left + 1); //元素个数小于3时,使用直接插入排序
}

void QuickSort(int A[], int N)
{
 Qsort(A, 0, N-1);
}

 

排序算法的总结:高度优化的快速排序算法即是是对很少的输入数据也能和希尔排序一样快。快速排序的改进算法仍然有 O(N^2)的最坏情形,但是这种情况出现的几率是微不足道的,以至于不能成为影响算法的因素。如果需要对一些大型的文件进行排序,则快速排序算法应该是可取的办法。但是永远不能因为省事而将第一个元素选作枢纽元,对输入数据随机的假设是不安全的。如果不想过多的考虑这个问题,则可以选择希尔排序。希尔排序有些小缺陷,但是还是可以接受的。希尔排序的最坏情况也只不过是O(N^(4/3));这种最坏情况的几率也是微不足道的。

    堆排序要比希尔排序慢,尽管他是一个带有明显紧凑的内循环O(NlogN), 但是,对该算法的考察揭示,为了移动数据, 堆排序要进行两次比较。

    插入排序只用在小的或是非常接近排好序的输入数据上。而  归并排序的性能对于主存排序并没有快速排序那么好,【合并时外部排序的中心思想】

总体代码:

#include<iostream>
using namespace std;
#define Cutoff 3  //最少的元素个数不得小于3
typedef int ElemType;
void InsertSort_1(int A[], int N)
{
 int i, j, tmp;
 for(i = 1; i < N; i++)
 {
  tmp = A[i];
  for(j = i; j > 0 && A[j - 1] > tmp; j--)
  {
   A[j] = A[j - 1];
  }
  A[j] = tmp;
 }
}
void swap(int *a, int *b)
{
 int tmp;
 tmp = *a;
 *a = *b;
 *b = tmp;
}
//三数中值分割法
ElemType Median3(ElemType A[], int Left, int Right)
{
 int Center = (Left + Right) / 2;
 if(A[Left] > A[Center])
  swap(&A[Left], &A[Center]);
 if(A[Left] > A[Right])
  swap(&A[Left], &A[Right]);
 if(A[Center] > A[Right])
  swap(&A[Center], &A[Right]);  //Left处的为最小值,Right为最大值,Center为中值
 swap(&A[Center], &A[Right - 1]);  //将center处的值作为枢纽元,并与倒数第二个元素互换【此时数组最后一个元素一定大于枢纽元,因此不必参与比较】
 return A[Right - 1];
}
void Qsort(ElemType A[], int Left, int Right)
{
 int i, j;
 ElemType Pivot;
 if(Left + Cutoff <= Right)
 {
  Pivot = Median3(A, Left, Right);
  i = Left;
  j = Right - 1;
  for(;;)
  {
   while(A[++i] < Pivot){}  //i 右移
   while(A[--j] > Pivot){}  //i 左移
   if(i < j)
    swap(&A[i], &A[j]);  // A[i] >= Pivot and A[j] <= Pivot and i 在j 的左边时交换位置
   else 
    break;
  }
  swap(&A[i], &A[Right - 1]);  //将枢纽元Pivot插入到数组的适当位置
  Qsort(A, Left, i - 1);   //左边数组递归调用快速排序
  Qsort(A, i + 1, Right);   //右边数组递归调用快速排序
 }
 else
  InsertSort_1(A + Left, Right - Left + 1); //元素个数小于3时,使用直接插入排序
}
void QuickSort(int A[], int N)
{
 Qsort(A, 0, N-1);
}
int main()
{
 int arr[10] = {5, 2, 8, 6, 3, 1, 7, 9, 4, 10};
 QuickSort(arr, 10);
 for(int i = 0; i < 10; i++)
  cout << arr[i] << " ";
 cout << endl;
 system("pause");
 return 0;
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(快速排序,和,(枢纽元的两种选择:随机法,三数分割法))