您的位置:首页 > 理论基础 > 数据结构算法

排序算法之快速排序

2015-10-25 16:30 330 查看
基本思路挖坑填数 分而治之

步骤总结

完整代码实现

正确性分析
1 当选择最左边的元素Alo作为基准值时为什么必须第一次从后往前扫描

复杂度分析

另一种代码实现思路
代码实现

正确性分析
1 当选择最左边的元素Alo作为基准值时为什么必须第一次从后往前扫描

2 为什么从前往后扫描的判断条件不能是小于x而必须是小于或等于x

参考过以下博客,在此表示感谢:

1. 白话经典算法系列之六 快速排序 快速搞定

2. 坐在马桶上看算法:快速排序

1. 基本思路(挖坑填数 + 分而治之)

1.1 从数组A中取出一个数作为基准数,比如说取A[0],将A[0]保存到x中,这时可以看作已经在元素A[0]处挖了一个坑,可以将其他数据填充到这里来。初始化,i = 0(left),j = 9(right),x = A[0] = 9

0123456789
94632018151711
1.2 先从j开始从后向前开始查找,遇到小于x的数,就将其填充到已挖好的坑A[0]处。此处,j = 6时,A[j] = 8,填充到坑A[0]处,此时相当于在A[j] = A[6]处又挖了一个新坑,下个数据可以填充到这里。又因为A[0]的坑已经填充了数据,故左下标值i要加一。这样,i = 1,j = 6。

0123456789
84632018151711
1.3 接着,从i = 1开始,从前向后查找,当遇到大于等于x的数,就将其填充到坑A[6]处。当i = 4,此时A[i] = A[4] = 20 > x,故将其填充到坑A[j] = A[6]处,j减一,并在A[i] = A[4]处形成新坑。这样,i = 4,j = 5。

0123456789
846320120151711
1.4 再从j开始,向前查找,当j = 5时,A[j] = 1 < x,此时将A[5]填入坑A[4]中,i加一,而A[5]成为新坑。这时,i = 5,j = 5。

0123456789
84631120151711
1.5 接着,从i = 5开始向右开始查找,由于i = j = 5,查找结束。这时,将x填入坑A[5]中,即另A[5] = 9,第一次排序完成。这时,A[5]前面的数都小于它,A[5]后面的数都大于它。

0123456789
84631920151711
1.6 再对两个分支,A[0…4]和A[6…9]重复上述步骤就可以完成排序。

2. 步骤总结

i = L, j = R,将基准数挖出形成第一个坑A[i]

j–由后向前找出比基准数小的数,找出后挖出此数填入前一个坑A[i]中,对应的i加一

i++由前向后找出大于等于基准数的数,找到后挖出此数填到前一个坑A[j]中,对应的j加一

重复执行2,3步,直到i == j,将基准数填入A[i]中

参照此步骤,很容易写出挖坑填数的代码:

int i = L, j = R, x = A[L];     //A[L]即A[i]就是第一个坑
while(i < j)
{
//从后向前找小于x的数来填坑A[i]
while(i < j)
{
if(A[j] < x)
{
A[i] = A[j];    //将A[j]填入A[i]中,这样A[j]就成了新坑
i++;            //原来的坑A[i]已经填好,i值加一
break;
}
else
j--;
}
//从前向后找大于或等于x的数来填A[j]
while(i < j)
{
if(A[i] >= x)
{
A[j] = A[i];    //将A[i]填入A[j]中,这样A[i]就成了新坑
j--;            //原来的坑A[j]已经填好,j值减一
break;
}
else
i++;
}
}
//退出时,i = j。将x填入这个坑中
A[i] = x;


进一步改写可得:

int i = L, j = R, x = A[L];
while(i < j)
{
while(A[j] >= x && i < j)       //从后往前找小于x的数
j--;
if(i < j)
A[i++] = A[j];
while(A[i] < x && i < j)        //从前往后找大于或等于x的数
i++;
if(i < j)
A[i++] = A[j];
}
//退出时,i = j。将x填入这个坑中
A[i] = x;


3. 完整代码实现

完整的代码实现如下:

//override
void quickSortOverride(int A[], int n)
{
//利用重载,简化程序的入口
quickSortOverride(A, 0, n-1);
}

void quickSortOverride(int A[], int lo, int hi)
{
if (lo < hi)        //如果lo = hi,即只有一个元素时,已经是有序的了,不需要处理
{
int i = lo, j = hi, x = A[lo];
while (i < j)
{
while (i < j && A[j] >= x)
j--;
if (i < j)
A[i++] = A[j];

while (i < j && A[i] < x)
i++;
if (i < j)
A[j--] = A[i];
}
//退出时,i = j。将x填入这个坑中
A[i] = x;
quickSortOverride(A, lo, i-1);      //左边继续递归
quickSortOverride(A, j+1, hi);      //右边继续递归
}
}


4. 正确性分析

不变形

经过k次排序,整个数组里已有k个元素有序,对于这k个元素中的任一元素,左边的所有元素都比它小,右边的所有元素都比它大。

单调性

经过k次排序,相对无序的元素个数缩减至n-k

正确性

经过至多n次排序后,算法必然终止,数组的元素都是有序的

4.1 当选择最左边的元素A[lo]作为基准值时,为什么必须第一次从后往前扫描?

假如是从前往后扫描,当扫描到第一个大于或等于x的元素A[i],必须将这个元素填入先前的坑A[lo]中,这样就已经出现了错误,导致最终i = j时,A[lo…i-1]的元素不是都小于A[i]的,至少A[lo]就已经不满足了。

5. 复杂度分析

为求解qSort(A, lo, hi)规模为n的问题,需要递归求解两个规模为n/2的问题qSort(A, lo, (lo+hi)/2-1)和qSort(A, (lo+hi)/2+1, hi),以及最坏情况下至多n次的比较填充操作。

而递归基为:qSort(A, lo ,hi),其中lo = hi,所需的时间为O(1)。

故可得到如下递推方程:

T(n) = 2*T(n/2) + n;

T(1) = O(1);

求解:

T(n) = 2*T(n/2) + n;

T(n/2) = 2*T(n/22) + n/2;

T(n/22) = 2*T(n/23) + n/22;

……

T(2) = 2*T(1) + 2 = 2*T(n/2log2n) + n/2log2(n-1);

T(1) = O(1);

继而可得:

T(n) = 2*T(n/2) + n;

2*T(n/2) = 22*T(n/22) + n;

22*T(n/22) = 23*T(n/23) + n;

……

2log2(n-1)T(2) = 2log2n*T(1) + n;

2log2nT(1) = 2log2nO(1) = n;

可得:

T(n) = n*(log2n - 0 + 1) = n*(log2n + 1) = O(nlogn)

即最坏情况下的时间复杂度为:

T(n) = O(nlogn)

6. 另一种代码实现思路

仍然是先选取一基准值,初始情况,可另i = lo, j = hi, x = A[lo]

也是先从后向前查找小于x的数,当找到时,记录下此时的j值。

接着从前向后查找大于x的数(注意此处是大于,不是上面一种算法实现的大于或等于),当找到时,记录下此时的i值。

然后,交换对应的A[i]和A[j],如果i仍然小于j,重复上述步骤,直至i = j。紧接着,交换A[lo]和A[i]的值,至此做完了一次排序。这是A[lo…i-1]的各元素都小于等于A[i],A[i+1…hi]的各元素都大于等于A[i]。

1. 代码实现

//override
void quickSortSwap(int A[], int n)
{
//利用重载,简化程序的接口
quickSortSwap(A, 0, n-1);
}

void mySwap(int &x1, int &x2)
{
int tmp = x1;
x1 = x2;
x2 = tmp;
}

void quickSortSwap(int A[], int lo, int hi)
{
if (lo < hi)
{
int i = lo, j = hi, x = A[lo];
while (i < j)
{
while (i < j && A[j] >= x)
j--;

while (i < j && A[i] <= x)      //注意此处必须是小于等于
i++;
if (i < j)
mySwap(A[i], A[j]);     //注意此处不能让i或j自加或者自减
}
mySwap(A[lo], A[i]);
quickSortSwap(A, lo, i-1);
quickSortSwap(A, j+1, hi);
}
}


2. 正确性分析

2.1 当选择最左边的元素A[lo]作为基准值时,为什么必须第一次从后往前扫描?

若第一次从后往前扫描。交换总是成对进行的。进行完一次交换后,必然再一次从后往前扫描,分两种情况:1. 若j一直自减,没有发现比x小的元素,直至i = j终止,此刻停在之前完成交换的元素A[i]处,由于已经是参与过交换的元素,故A[i]一定小于x,这时,要拿x与A[i]交换,不会影响整体的有序性。2. 若j一直自减,中途发现了比x小的元素,记录下此时j的值。然后从前往后扫描,没有发现大于x的值,直至i = j,此时停在之前保留的j值的地方。由于还未完成一次交换,A[j]小于x,这时,要拿x和A[j]进行交换,也不会影响整体的有序性。而要做到这些,程序里必须保证在进行一次交换后,不要让i或j的值自增或者自减,而是让程序再次扫描时处理。

反之,如果第一次从前往后扫描,同样可以按照这个方法分析,就不能保证整体的有序性。

2.2 为什么从前往后扫描的判断条件不能是小于x而必须是小于或等于x?

如果是小于x,那么第一次从前往后扫描时,就会将左边缘的基准值A[lo]记录下来,参与交换,这样如果一次排序结束,是直接交换先前保留的x的值和A[i]的值,那么会出现A[i]的值被覆盖为x的值,而之前x的值已经参与了交换,这样x的值就会重复出现。如果一次排序结束,是交换A[lo]和A[i]的值,那么会出现最终停在的i = j的位置上并不是之前选定的基准值x,而是其他元素。这样就出现了A[lo…i-1]各元素的值都小于那个其他元素,A[i+1…hi]各元素的值都大于那个其他元素,而不是基准值。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息