终于轮到我们的终极算法了,快速排序顾名思义,速度肯定是很快的。它的基本思想是:把一个待排序列分隔成两个独立的序列,其中一个序列中的数比关键字小,另一个序列中的数比关键字大,然后对这两个序列再进行排序,最后使整个序列有序。
这个算法里面有两个关键点,一是怎样选择这个关键字?二是怎样把一个序列分成两个子序列,使一个子序列比关键字小,另一个子序列比关键字大?
我们首先来看第二个问题,怎样用程序实现,在这里面我们假设关键字就是待排序列的第一个数。代码如下:
private int Partition(List<int > sortList,int low,int high)
{
int pivotkey = sortList[low];
while (low < high)
{
//1.1从高位向低位循环,找出一个数,比pivotkey小的,把它与pivotkey交换,此时pivotkey的值是sortList[low]
while (low < high && sortList[high] >= pivotkey)
{
high--;
}
Swap(sortList,low,high);
//1.2从低位向高位循环,找出一个数,比pivotkey大的,把它与pivotkey交换,如果上面的while循环有交换,那么此时pivotkey的值是sortList[high]
while (low < high && sortList[low] <= pivotkey)
{
low++;
}
Swap(sortList, low, high);
}
return low;//把pivotkey数的下标返回
}
private int MyPartition(List<int> sortList, int low, int high)
{
int position;
int pivotkey = sortList[low];//默认pivotkey的值是sortList[low]
List<int> tempList = new List<int>();
//1.1 循环遍历整个序列,把比pivotkey小的值加入到tempList中去
for (int i = low; i <= high; i++)
{
if (sortList[i] < pivotkey)
{
tempList.Add(sortList[i]);
}
}
//1.2 把pivotkey的值加入到tempList中去,此时tempList中的数都是比pivotkey的值小的数
tempList.Add(pivotkey);
//1.3记录下pivotkey值的位置,这个值需要返回去
position = tempList.Count - 1 + low;
//1.4 循环遍历整个序列,把比pivotkey大的值加入到tempList中去
for (int i = low; i <= high; i++)
{
if (sortList[i] > pivotkey)
{
tempList.Add(sortList[i]);
}
}
//1.5此时tempList中的数已经分成了两个序列了,从0到position的数都比pivotkey小,后面的都比pivotkey大
//把tempList中的数重新放入到sortList中去
int k = 0;
for (int i = low; i <= high; i++)
{
sortList[i] = tempList[k++];
}
return position;
}
下面来看循环调用的方法:
private void QSort(List<int> sortList, int low, int high)
{
if (low < high)
{
int pivotkey = MyPartition(sortList, low, high);
QSort(sortList, low, pivotkey - 1);
QSort(sortList, pivotkey + 1, high);
}
}
首先是产生一个关键字,然后把把序列分成两个序列,low到pivotkey-1的数都比关键字小,pivotkey+1到high的数都比关键字大,然后递归对这两个序列又进行排序,最后整个序列就是有序的了。
真正调用的方法是:
public List<int> QuickSort(List<int> sortList)
{
QSort(sortList, 0, sortList.Count - 1);
return sortList;
}
快速排序在最优的情况下时间复杂度为O(nLogn)。关键字的选取对快速排序性能有比较重要的影响。从上面的代码中可以知道,我们把数组的第一数当作关键字,但是如果这个数是最小的一个数,那么快速排序的时间复杂度就为O(n^n)了,现在就加到第一个问题上,怎样选择关键字。
关键字的选取,有随机选取法,三数取中法,九数取中法。随机选取就是随机产生一个数,把这个数当作关键字,这种办法效果也不是很好,另外一种就是三数取中,也就是在待排序列中的前面,中间,后面各取一个数,把这三个数进行排序,把它们中间的一个数当作关键字,这种方法选择出来的关键字,比随机产生的关键字要好的多。但是如果数据量非常多,那么三数取中效果也不好,这时就又有了九数取中了,九数取中其实就是做三次取样,也就是分别做三次三数取中,把每次的中间的数拿出来进行排序,最后取一次中,把这个数当作关键字。这样的取法比三数取中要更好一些,当然对于关键字的选取,还有其他一些方法,有兴趣的可以在网上查询相关资料。
除了关键字的选择,上面的算法还有没有地方可以进行优化呢,肯定是有的。
我们在选取关键字的时候,在不断的进行交换,使一边的数比关键字小,另一边比关键字大,不管是大的一边还是小的一边,里面的数字都不要求是有序的,所以在这个过程中其实有很多交换是不必要的,我们可以对产生关键字的函数进行改进,代码如下(有注释的地方是改进的地方):
private int PartitionNew(List<int> sortList, int low, int high)
{
int pivotkey = sortList[low];
int temp = pivotkey;//用temp记录pivotkey的值
while (low < high)
{
while (low < high && sortList[high] >= pivotkey)
{
high--;
}
sortList[low] = sortList[high];//采用替换操作,而不是交换操作
while (low < high && sortList[low] <= pivotkey)
{
low++;
}
sortList[high] = sortList[low];//采用替换操作,而不是交换操作
}
sortList[low] = temp;//最后把pivotkey的值赋给sortList[low]
return low;
}
还有一种改进的就是减少递归的次数,可以把QSort方法改为:
private void QSortNew(List<int> sortList, int low, int high)
{
while (low < high)//把if改成while
{
int pivotkey = MyPartition(sortList, low, high);
QSort(sortList, low, pivotkey - 1);
low = pivotkey + 1;//这里low=pivotkey+1,再循环调用一次,相当于 QSort(sortList, pivotkey + 1, high);
}
}