本文要介绍的快速排序是交换排序的高级算法,同时,它也是C++的STL算法库里头,排序函数所实现的算法。由此可见,快速排序是比较重要而高效的排序方法,对于元素个数比较多的序列而言,可以适时使用该算法进行高效排序。
以序列:49、38、65、97、76、13、27、49为例。开始时,设置i、j 分别指向序列的头49、尾49。先随意固定其中一者,并开始平移另一者,此处以先固定 i 为例。先让 i 固定指向49,然后用 j 所指向的49与其比较,49=49,则 i 不变,j 向前平移1个位置指向27。仍用 i 所指向的49与 j 所指向的27比较,49>27,则交换49与27所在的位置(而非交换i、j),得:27、38、65、97、76、13、49、49。显然,i 现在指向27,j 指向49。此后立刻让 i 向后平移1个位置以指向38,这回变为固定 j 所指的49,与 i 指向的38比较,38<49,则 j 不变,i 向后平移1个位置以指向65。其与 j 指向的49比较,65>49,则交换65与49的位置,变为:27、38、49、97、76、13、65、49。显然,i 现在指向49,j 指向65。立刻让 j 向前平移1个位置到13,这回又重新固定 i ,i 指向的49与 j 指向的13比,49>13,则交换49与13,变为:27、38、13、97、76、49、65、49。i 指向了13,j 指向了49。立刻让 i 向后平移1个位置到97,其与 j 指向的49比,97>49,再度交换97与49,得:27、38、13、49、76、97、65、49。现在,i 指向49,j 指向97。立刻让 j 向前平移1个位置到76,与 i 指向的49比,49<76,则 i 不变,j 向前平移1个位置到49。至此 i 与 j 同时指向49,这一轮的处理完毕。可发现,现在的序列里头,49以左的部分均比49小,49以右的部分均不比49小,49成为了序列的“中枢点”。接下来,我们将序列分割成49以左及以右的部分,对这两部分分别采用与上述完全相同的步骤进行处理。该两部分的处理又将产生新的“中枢点”,则又通过“中枢点”划分更小的部分再同样处理。显然,这是个递归的过程,直到最后新分出来的部分只有1个元素或者没有元素了,则该部分就不用处理了。最终,将能完成排序的目标:13、27、38、49、49、65、76、97。
由于快速排序过程中会出现跨越式的元素位置变动,所以其为不稳定排序(希尔排序的不稳定性也是这类原因造成的)。代码如下(注:代码中用到的栈,实际上可以是直接使用STL里头提供的栈,此处本人只是因为强迫症,而去实现了自己的栈版本而已,可忽略):
//递归实现:
void quickSortKernel(int list[],int left,int right)
{
//如果这里不进行事先判断的话,会导致即便left>=right时,都会继续进行递归,导致无限递归!
if(left<right)
{
int i=left;
int j=right;
while(i<j)
{
while((i<j)&&(list[i]<=list[j]))
--j;
if(i<j)
{
int temp=list[i];
list[i]=list[j];
list[j]=temp;
++i;
}
while((i<j)&&(list[i]<=list[j]))
++i;
if(i<j)
{
int temp=list[i];
list[i]=list[j];
list[j]=temp;
--j;
}
}
quickSortKernel(list,left,i-1);
quickSortKernel(list,i+1,right);
}
}
void quickSort(int list[],int length)
{
quickSortKernel(list,0,length-1);
}
//迭代实现:
struct ListNode
{
int left;
int right;
ListNode * next;
};
class Stack
{
private:
ListNode * list;
public:
Stack()
{
list=new ListNode;
list->next=0;
}
bool isEmpty()
{
return list->next==0;
}
void push(int left,int right)
{
ListNode * temp=new ListNode;
temp->left=left;
temp->right=right;
temp->next=list->next;
list->next=temp;
}
ListNode * get()
{
return list->next;
}
void pop()
{
ListNode * temp=list->next;
list->next=temp->next;
delete temp;
}
~Stack()
{
while(!isEmpty())
pop();
delete list;
}
};
void quickSort(int list[],int length)
{
Stack stack;
if(0<length-1)
stack.push(0,length-1);
while(!stack.isEmpty())
{
ListNode * range=stack.get();
int left=range->left;
int right=range->right;
stack.pop();
int i=left;
int j=right;
while(i<j)
{
while((i<j)&&(list[i]<=list[j]))
--j;
if(i<j)
{
int temp=list[i];
list[i]=list[j];
list[j]=temp;
++i;
}
while((i<j)&&(list[i]<=list[j]))
++i;
if(i<j)
{
int temp=list[i];
list[i]=list[j];
list[j]=temp;
--j;
}
}
if(left<i-1)
stack.push(left,i-1);
if(i+1<right)
stack.push(i+1,right);
}
}
设序列元素个数为n。与冒泡排序一样,快速排序中随元素个数改变而改变次数的操作主要由元素间的比较与交换构成,也是边比较边交换,且交换次数不大于比较次数。因此,其时间复杂度的分析主要考虑元素比较次数。由于算法采用了递归的思想,其时间复杂度的分析需要用到递归树。递归树的每个结点表示相应的函数调用,父结点通过分支形成的各个子节点便表示父结点对应的函数产生递归调用,形成对应的子结点。从上述代码看来,尽管我们不大确知快速排序的最好情况下的序列是怎样的,但最好情况下,我们将能画出如下图中的递归树:
![图片](https://i-blog.csdnimg.cn/blog_migrate/cf0c8cd04965687f6dc25d2e13e22e89.jpeg)
显然,该递归树是棵二叉树,而且接近满二叉树。树的第1层仅有1个结点,对应表示对完整序列的快速排序。该过程将要处理n个元素,进行n-1次比较。产生“中枢点”后,n个元素被“中枢点”分成两部分,对每部分递归进行快速排序,从而形成递归树的第2层中的两个结点。该两结点对应需要处理的元素个数合起来必为n-1,而每个结点对应要进行的比较次数必然为其对应元素个数减1,则两结点对应的总比较次数为n-1-2。不难发现,令第k层所有结点对应的元素个数的和为Nk,则Nk=Nk-1 - pow(2,k-2),当然,此处的k≥2,k=1 时,Nk=n。而第k层所有结点对应的元素总比较次数为Nk - pow(2,k-1),此处的pow(2,k-1)实际上就是每层的结点数。由此,最好情况下的总比较次数只需将各层所有结点对应的元素总比较次数加起来即可。该递归树有多少层呢?对于x个结点的完全二叉树(满二叉树也是棵完全二叉树),其层数有⌈log2(x+1)⌉,但上面提到,最好情况下只是构造出接近满二叉树的递归树。其实,接近满二叉树而有可能不是完全二叉树的二叉树,其层数仍然可以适用上述的公式。最终,最好情况下的总比较次数即为∑(Nk - pow(2,k-1)),其中,k从1到⌈log2(x+1)⌉,x为快速排序函数的调用次数(对应递归树中的结点数)。
最坏情况时,序列完全顺序或完全逆序。此时,构造出来的递归树将退化成如下图的1条链状:
显然,该递归树共有n-1层,每层只有1个结点,第 i 层对应有n-i+1个要处理的元素,而第 i 层需要进行的总比较次数则为n-i,则整个过程的总比较次数为1+2+……+n-1=n(n-1)/ 2。
综上所述,快速排序的时间复杂度为O(nlogn)。
由于算法采用递归实现,则快速排序所需要的辅助存储空间数量直接与递归树的层数有关,而层数是则受序列元素个数影响(当然,也涉及元素如何排列)。显然,所谓最好最坏情况与上述相仿,空间复杂度也就能有个大概的推断方向了。这里仅需要知道这些就可以了,大家记住,快速排序的空间复杂度是O(logn)。
![图片](https://i-blog.csdnimg.cn/blog_migrate/659606435bd1f07338da8622721958a9.jpeg)
显然,该递归树共有n-1层,每层只有1个结点,第 i 层对应有n-i+1个要处理的元素,而第 i 层需要进行的总比较次数则为n-i,则整个过程的总比较次数为1+2+……+n-1=n(n-1)/ 2。
综上所述,快速排序的时间复杂度为O(nlogn)。
由于算法采用递归实现,则快速排序所需要的辅助存储空间数量直接与递归树的层数有关,而层数是则受序列元素个数影响(当然,也涉及元素如何排列)。显然,所谓最好最坏情况与上述相仿,空间复杂度也就能有个大概的推断方向了。这里仅需要知道这些就可以了,大家记住,快速排序的空间复杂度是O(logn)。
由于算法采用递归实现,则快速排序所需要的辅助存储空间数量直接与递归树的层数有关,而层数是则受序列元素个数影响(当然,也涉及元素如何排列)。显然,所谓最好最坏情况与上述相仿,空间复杂度也就能有个大概的推断方向了。这里仅需要知道这些就可以了,大家记住,快速排序的空间复杂度是O(logn)。