分治法的基本思想
分治法的基本思想是:将原问题分解为若干个规模更小但结构与原问题相似的子问题。递归地解这些子问题,然后将这些子问题的解组合为原问题的解。
快速排序介绍
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。
它的基本思想是:选择一个基准数,通过一趟排序将要排序的数据分割成独立的两部分;其中一部分的所有数据都比另外一部分的所有数据都要小。然后,再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序流程:
(1) 从数列中挑出一个基准值。
(2) 将所有比基准值小的摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边);在这个分区退出之后,该基准就处于数列的中间位置。
(3) 递归地把"基准值前面的子数列"和"基准值后面的子数列"进行排序。
快速排序说明
设当前待排序的无序区为R[low..high],利用分治法可将快速排序的基本思想描述为:
分解:
在R[low..high]中任选一个记录作为基准(Pivot),以此基准将当前无序区划分为左、右两个较小的子区间R[low..pivotpos-1)和R[pivotpos+1..high],并使左边子区间中所有记录的关键字均小于等于基准记录(不妨记为pivot)的关键字pivot.key,右边的子区间中所有记录的关键字均大于等于pivot.key,而基准记录pivot则位于正确的位置(pivotpos)上,它无须参加后续的排序。
注意:
划分的关键是要求出基准记录所在的位置pivotpos。划分的结果可以简单地表示为(注意pivot=R[pivotpos]):
R[low..pivotpos-1].keys≤R[pivotpos].key≤R[pivotpos+1..high].keys
其中low≤pivotpos≤high。
求解:
通过递归调用快速排序对左、右子区间R[low..pivotpos-1]和R[pivotpos+1..high]快速排序。
组合:
因为当"求解"步骤中的两个递归调用结束时,其左、右两个子区间已有序。对快速排序而言,"组合"步骤无须做什么,可看作是空操作。
快速排序的时间复杂度和稳定性
快速排序稳定性
快速排序是不稳定的算法,它不满足稳定算法的定义。
算法稳定性 -- 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!
快速排序时间复杂度
快速排序的时间复杂度在最坏情况下是O(N2),平均的时间复杂度是O(N*lgN)。
这句话很好理解:假设被排序的数列中有N个数。遍历一次的时间复杂度是O(N),需要遍历多少次呢?至少lg(N+1)次,最多N次。
(1) 为什么最少是lg(N+1)次?快速排序是采用的分治法进行遍历的,我们将它看作一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的定义,它的深度至少是lg(N+1)。因此,快速排序的遍历次数最少是lg(N+1)次。
(2) 为什么最多是N次?这个应该非常简单,还是将快速排序看作一棵二叉树,它的深度最大是N。因此,快读排序的遍历次数最多是N次。
1. 最坏时间复杂度
最坏情况是每次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中记录数目,仅仅比划分前的无序区中记录个数减少一个。因此,快速排序必须做n-1次划分,第i次划分开始时区间长度为n-i+1,所需的比较次数为n-i(1≤i≤n-1),故总的比较次数达到最大值:
Cmax = n(n-1)/2=O(
如果按上面给出的划分算法,每次取当前无序区的第1个记录为基准,那么当文件的记录已按递增序(或递减序)排列时,每次划分所取的基准就是当前无序区中关键字最小(或最大)的记录,则快速排序所需的比较次数反而最多。
2. 最好时间复杂度
在最好情况下,每次划分所取的基准都是当前无序区的"中值"记录,划分的结果是基准的左、右两个无序子区间的长度大致相等。总的关键字比较次数:0(nlgn)
注意:
用递归树来分析最好情况下的比较次数更简单。因为每次划分后左、右子区间长度大致相等,故递归树的高度为O(lgn),而递归树每一层上各结点所对应的划分过程中所需要的关键字比较次数总和不超过n,故整个排序过程所需要的关键字比较总次数C(n)=O(nlgn)。
因为快速排序的记录移动次数不大于比较的次数,所以快速排序的最坏时间复杂度应为0(
3. 基准关键字的选取
在当前无序区中选取划分的基准关键字是决定算法性能的关键。①"三者取中"的规则
"三者取中"规则,即在当前区间里,将该区间首、尾和中间位置上的关键字比较,取三者之中值所对应的记录作为基准,在划分开始前将该基准记录和该区伺的第1个记录进行交换,此后的划分过程与上面所给的Partition算法完全相同。
②取位于low和high之间的随机数k(low≤k≤high),用R[k]作为基准
选取基准最好的方法是用一个随机函数产生一个取位于low和high之间的随机数k(low≤k≤high),用R[k]作为基准,这相当于强迫R[low..high]中的记录是随机分布的。用此方法所得到的快速排序一般称为随机的快速排序。
注意:
随机化的快速排序与一般的快速排序算法差别很小。但随机化后,算法的性能大大地提高了,尤其是对初始有序的文件,一般不可能导致最坏情况的发生。算法的随机化不仅仅适用于快速排序,也适用于其它需要数据随机分布的算法。
4. 平均时间复杂度
尽管快速排序的最坏时间为O(n2),但就平均性能而言,它是基于关键字比较的内部排序算法中速度最快者,快速排序亦因此而得名。它的平均时间复杂度为O(nlgn)。5. 空间复杂度
快速排序在系统内部需要一个栈来实现递归。若每次划分较为均匀,则其递归树的高度为O(lgn),故递归后需栈空间为O(lgn)。最坏情况下,递归树的高度为O(n),所需的栈空间为O(n)。快速排序的实现
1、快速排序的实现(算法:C语言实现P194)
typedef int Item;
#define key(A) (A)
#define less(A, B) (key(A)<key(B))
#define exch(A, B) {Item t = A; A = B; B = t;}
#define compexch(A, B) if(less(B, A)) exch(A, B) //最后A<B
/*
快速排序:i下标元素前面是已经排过序的
*/
int partition(Item a[], int l, int r)
{
int i = l-1, j = r;
Item v = a[r]; //快速排序的排序的划分元素
for(;;)
{
while(less(a[++i], v)); //退出循环时候,i指针指向大于等于v划分元素上面
while(less(v, a[--j]))
{
if(j==l)
break; //防止当划分元素是最小元素时候,j指针越界
}
if(i>=j) break;
exch(a[i], a[j]);
}
exch(a[i], a[r]);
return i;
}
void quicksort(Item a[], int l, int r)
{
int i;
if(r<=l) return; //递归返回条件
i = partition(a, l, r);
quicksort(a, l, i-1);
quicksort(a, i+1, r);
}
2、啊哈算法中的实现方法
void quicksort1(Item a[], int l, int r)
{
if(l>r)
return;
int i = l;
int j = r;
Item v = a[l]; //用最左边的元素作为划分元素,则右边的哨兵应该先出发
Item temp;
while(i!=j) //退出循环时候,i==j
{
while(a[j]>=v && i<j) //划分元素决定哨兵扫描的先后顺序
{
j--; //j在退出循环时候,指向小于等于划分元素的位置
}
while(a[i]<=v && i<j)
{
i++;
}
//交换两个数在数组中的位置
if(i<j)
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
a[l] = a[i];
a[i] = v;
quicksort1(a, l, i-1);
quicksort1(a, i+1, r);
}
Ps:划分元素的选择,决定着左右哨兵的出发顺序!
3、快速排序的完整实现
#include <stdio.h>
#include <stdlib.h>
typedef int Item;
#define key(A) (A)
#define less(A, B) (key(A)<key(B))
#define exch(A, B) {Item t = A; A = B; B = t;}
#define compexch(A, B) if(less(B, A)) exch(A, B) //最后A<B
/*
快速排序:i下标元素前面是已经排过序的
*/
int partition(Item a[], int l, int r)
{
int i = l-1, j = r;
Item v = a[r]; //快速排序的排序的划分元素
for(;;)
{
while(less(a[++i], v)); //退出循环时候,i指针指向大于等于v划分元素上面
while(less(v, a[--j]))
{
if(j==l)
break; //防止当划分元素是最小元素时候,j指针越界
}
if(i>=j) break;
exch(a[i], a[j]);
}
exch(a[i], a[r]);
return i;
}
void quicksort(Item a[], int l, int r)
{
int i;
if(r<=l) return; //递归返回条件
i = partition(a, l, r);
quicksort(a, l, i-1);
quicksort(a, i+1, r);
}
void quicksort1(Item a[], int l, int r)
{
if(l>r)
return;
int i = l;
int j = r;
Item v = a[l]; //用最左边的元素作为划分元素,则右边的哨兵应该先出发
Item temp;
while(i!=j) //退出循环时候,i==j
{
while(a[j]>=v && i<j) //划分元素决定哨兵扫描的先后顺序
{
j--; //j在退出循环时候,指向小于等于划分元素的位置
}
while(a[i]<=v && i<j)
{
i++;
}
//交换两个数在数组中的位置
if(i<j)
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
a[l] = a[i];
a[i] = v;
quicksort1(a, l, i-1);
quicksort1(a, i+1, r);
}
int main(void)
{
int i, N, sw;
N = 50;
sw = 1;
int *a = (int *)malloc(N*sizeof(int)); //为数据a分配相应的存储空间
if (sw)
{
for(i=0; i<N; i++)
{
a[i] = 1000 * (1.0 * rand()/RAND_MAX);
}
}
else
{
for (i = 0; i < N; i++)
{
scanf("%d", &a[i]);
}
}
puts("排序前");
for(i=0; i<N; i++)
{
printf("%d ", a[i]);
}
quicksort1(a, 0, N-1);
puts("\n排序后");
for(i=0; i<N; i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
Ps:修改上面main函数中调用快速排序函数,达到测试效果。其中N是产生的测试数据数量,sw是用与选择对数组进行手动赋值还是由系统的rand()函数产生随机数。
快速排序的改进
1、快速排序的非递归实现
2、快速排序中,遇到小文件时采用插入排序
3、使用一个尽可能在文件中间划分的元素:三者取中划分
4、对于重复关键字较多的文件,采用三路划分方法