快速排序(QuickSort):ios
快速排序是实践中最快的排序算法,它的平均运行时间是O(NlogN) ,它的最坏情形的性能是 O(N^2),但稍加努力就能够避免这种情形。像归并排序同样,快速排序也是一种分治的递归算法。算法
将数组S 排序的基本算法由下面的四个简单步骤组成:数组
1) 若是S 的个数为0 或 1,则直接返回安全
2) 取S 中任意元素 v ,称之为枢纽元(pivot)dom
3) 将S 中剩余的元素分红两个不相交的集合: S1 和 S2性能
4) 返回{quicksort(S1)} 继随 v ,继而{quicksort(S2)}优化
Why 快速排序比归并排序快?ui
如同归并排序那样,快速排序递归的解决两个子问题并须要线性的附加工做(第(3)步),不过,与归并算法不一样的是,两个子问题并不保证具备相等的大小,这是个潜在的隐患。快速排序更快的缘由在于,第(3)步分割成两组其实是在适当位置上进行而且很是高效,它的高效弥补了大小不等的递归调用的缺憾并且还有超出。spa
枢纽元的选取:code
两种经常使用的错误方法:
将第一个元素做为枢纽元,若是输入是随机的,那么能够接受,可是若是输入是预排序的或者是反序的,那么这样的枢纽元就产生了一个劣质的分割,由于全部的元素不是都被划入 S1 中就是都被划入S2 中。更有甚者,这种状况可能发生在全部的递归调用中。实际上,若是第一个元素用作枢纽元并且输入是预先排序的,那么快速排序花费的时间将是二次的O(N^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; }