目录
1、快速排序的概念:
2、快速排序的版本:
2.1、递归版本:
对于如何按照基准值将待排序列分为两子序列,即单趟排序的常见的方式有:
QuickSort(int* a, int begin,int end) 是外层递归函数,不同的递归方法QuickSort函数都是一样的,PartSort(int* a,int left,int right) 是单趟函数,也正是写法不同的地
方,所以我们这里主要探讨的是 PartSort 单趟排序函数、
递归版本的快速排序的整体思路为:
先选出基准值key,一般是给定的随机数组的第一个元素或者是最后一个元素,先使用下面三种方法进行单趟排序,若排升序,单趟排序后,则让基准值key左边部
分的所有数据均小于等于key,基准值key右边部分的所有数据均大于等于key,若排降序,单趟排序后,则让基准值key左边部分的所有数据均大于等于key,基准
值key右边部分的所有数据均小于等于key,这就完成了单趟排序,但是要注意,升序情况下, 基准值key左边部分的所有数据均小于等于key,但不一定是升序的,
基准值key右边部分的所有数据均大于等于key,但也不一定是升序的,降序情况下, 基准值key左边部分的所有数据均大于等于key,但不一定是降序的,基准值
key右边部分的所有数据均小于等于key,但也不一定是降序的,然后再使用分治的思想去递归基准值key的左右子序列即可、
2.1.1、hoare版本:
所谓单趟排序,其过程为 : 选出一个基准值Key,一般选取给定的随机数组中的第一个元素或者是最后一个元素,若选取数组中第一个元素作为基准值Key的话,则
定义变量Keyi,再把left赋值给Keyi,若选取数组中最后一个元素作为基准值Key的话,则定义变量Keyi,再把right赋值给Keyi、若排升序,则单趟排序结束后要
求,基准值左边的序列中的所有数据都小于等于基准值,基准值右边的序列中的所有数据都大于等于基准值、若排降序,则单趟排序结束后要求,基准值左边的序
列中的所有数据都大于等于基准值,基准值右边的序列中的所有数据都小于等于基准值、
思路:
以升序为例,若选取给定的随机数组中的第一个元素作为基准值Key的话,则定义变量Keyi,并把left赋值给Keyi,则right必须先走,而right在找比基准值Key小的数
据,直到找到比基准值Key小的数据后,right停止,此时,left 再开始走,而left是在找比基准值Key大的值,直到找到后停止,然后交换left和right所在位置上的数
据,然后right再找小,left再找大,找到之后再进行交换,当left和right相遇时,即相等时,再把该相遇位置上的值与Keyi位置上的数据Key进行交换,从而达到目
的,在此,即使数组元素个数为偶数个,则left和right也一定会相遇的,和数组元素个数奇数偶数没有关系,right找到比Key小的数就不动了,left找到比Key大的数
就不动了,每次移动过程中,一定是其中一个去与另外一个相遇,即,两者同时只有一个在移动,另外一个不动,所以一定会相遇,其次,最后一步需要把相遇位
置上的数据和Keyi位置上的数据Key进行交换,若相遇位置上的数小于Keyi位置上的数据Key是可以的,此时相遇位置上的数据不会等于Key,是因为right找小,left
找大,两者直接把等于Key的值跳过了,但若大于的话,就是不对的,怎么保证相遇位置上的数据一定是小于Keyi位置上的数据Key呢?,left和right两者不会在一
个数据比Key大的位置上相遇,是因为,right先走已经保证了,即每一次交换完之后都是right先走,left再走,此时的相遇只有两种情况,一,right遇到left,二,left
遇到right,若是第二种情况的话,right不动,left与right相遇,此时相遇位置上的数据一定比Key小,若是第一种情况的话,left不动,right与left相遇,因为上一次交
换后,left所在位置上的值一定比Key小,所以,right与left相遇的位置一定也比key要小,降序的话则right找大,left找小、
若选取给定的随机数组中的最后一个元素作为基准值Key的话,则定义变量Keyi,并把right赋值给Keyi,则left必须先走,其他的不需要变动,这样就保证了相遇位
置上的元素一定比Key大,分析同上、
以升序为例,若经过一次单趟排序,使得key左边的所有数据均小于等于key,key右边的所有数据均大于等于key,再将key左序列和右序列进行上述单趟排序,如
此反复进行上述操作,直到子区间中只有一个数据或者不存在数据时,即子区间中只有一个数据或者子区间不存在时,为子问题的最小规模,则递归结束,因为子
区间中只有一个数据或者不存在数据时,可以看成升序,也可看成降序、
总结:
升序并且第一个元素作为基准值,则right先走,left后走,并且,left找大,right找小,升序并且最后一个元素作为基准值,则left先走,right后走,并且,left找大,
right找小,降序并且第一个元素作为基准值,则right先走,left后走,并且,left找小,right找大,降序并且最后一个元素作为基准值,则left先走,right后走,并
且,left找小,right找大、
代码实现:
//单趟排序->hoare版本、
int PartSortHoare(int*a, int left, int right)//一级指针传参,一级指针接收、
{
assert(a);
//方法一:
//选取数组首元素作为基准值、
//三数取中法,取出数组首元素,尾元素,中间位置元素,选择这三者中,不是最大,也不是最小的那个值的下标、
int midi = GetMidIndex(a, left, right);
//若选择数组首元素作为基准值key,则应为:Swap(&a[midi], &a[left]); 这样就让数组首元素,尾元素,中间位置元素,三者中,不是最大,也不是最小的那个值作为了基准值key、
Swap(&a[midi], &a[left]);
//下面的代码不需要进行改变,在单趟排序中,不管使用何种方法,仍选取数组首元素或者尾元素作为基准值key、
int keyi = left; //用keyi记录key的下标、
while (left < right)
{
//升序、
//right先走、
while (left < right && a[right] >= a[keyi])//right找小,找不到则right--,降序则right找大,找不到则right--,变为<=、
{
right--;
}
//left再走、
while (left < right && a[left] <= a[keyi])//left找大,找不到则left++,降序则left找小,找不到则left++,变为>=、
{
left++;
}
//上面的>=和<=中的=,一是表明与基准值相等的数据在基准值的左边和右边都是就可以的,二是避免死循环,比如: 5 5 2 3 5,若两者不加等于的话,就会造成死循环、
//上面两个while中的 left < right 主要是为了防止越界访问的,若选择数组首元素作为基准值排升序和排降序时都可能会越界访问,比如1 2 3 4 5、
//交换、
Swap(&a[left], &a[right]);//找到后就交换a[left]和a[right],让小的去左边,大数去右边,降序则让小的去右边,大的去左边、
}
//交换相遇位置和keyi位置上的数据、
Swap(&a[keyi], &a[left]);
//Swap(&a[keyi], &a[right]);
return right;
//return left;
方法二:
选取数组尾元素作为基准值、
三数取中法,取出数组首元素,尾元素,中间位置元素,选择这三者中,不是最大,也不是最小的那个值的下标、
//int midi = GetMidIndex(a, left, right);
若选择数组尾元素作为基准值key,则应为:Swap(&a[midi], &a[right]); 这样就让数组首元素,尾元素,中间位置元素,三者中,不是最大,也不是最小的那个值作为了基准值key、
//Swap(&a[midi], &a[right]);
下面的代码不需要进行改变,在单趟排序中,不管使用何种方法,仍选取数组首元素或者尾元素作为基准值key、
//int keyi = right; //用keyi记录key的下标、
//while (left < right)
//{
// //升序、
// //left先走、
// while (left < right && a[left] <= a[keyi])//left找大,找不到则left++,降序则left找小,找不到则left++,变为>=、
// {
// left++;
// }
// //right再走、
// while (left < right && a[right] >= a[keyi])//right找小, 找不到则right--, 降序则right找大, 找不到则right--, 变为<= 、
// {
// right--;
// }
// //上面的>=和<=中的=,一是表明与基准值相等的数据在基准值的左边和右边都是就可以的,二是避免死循环,比如: 5 5 2 3 5,若两者不加等于的话,就会造成死循环、
// //上面两个while中的 left < right 主要是为了防止越界访问的,若选择数组尾元素作为基准值排升序和排降序时都可能会越界访问,比如1 2 3 4 5、
// //交换、
// Swap(&a[left], &a[right]);//找到后就交换a[left]和a[right],让小的去左边,大数去右边,降序则让小的去右边,大的去左边、
//}
交换相遇位置和keyi位置上的数据、
//Swap(&a[keyi], &a[left]);
Swap(&a[keyi], &a[right]);
return right;
//return left;
}
2.1.2、挖坑法:
思路:
和hoare方法类似,但有一些不同,若选择数组首元素为基准值key,则数组中第一个位置即为坑1,此时需要把数组第一个位置上的数据,即数组首元素保存到key
中,那么数组中第一个位置上的数据就已经被保存下来了,也就代表着数组第一个位置上的数据可以被覆盖了,即称之为坑1,若进行升序排列的话,则left找大,
right找小,由于坑1在左边,而升序排序需要把小于等于key的数据放在key的左边,并且right找的是比key小的值,跳过大于等于key的值,所以很自然的就先让
right先走,找到比key小的数据放在坑1里,然后该比key小的数据所在的位置变成新的坑2,left再走,找到比key大的值放在坑2中,然后该比key大的数据所在的位
置变成新的坑3,重复上述操作,直到left与right相遇,则停止,并且两者相遇的时候,所在的位置一定是一个坑位,然后再把key中的值放到该相遇位置的这个坑位
上,此时就满足了基准值key的左边所有的数据均小于等于key,基准值key的右边所有的数据均大于等于key,这就是所谓的挖坑法,与hoare方法有共同点,也有
不同点,挖坑法相比于hoare方法的优势是,挖坑法更好理解,不需要理解在升序并且选择数组首元素作为基准值的的条件下,left和right相遇的位置上的数据一定
比key小,不需要理解在升序并且选择数组尾元素作为基准值的的条件下,left和right相遇的位置上的数据一定比key大,不需要理解在降序并且选择数组首元素作为
基准值的的条件下,left和right相遇的位置上的数据一定比key大,不需要理解在降序并且选择数组尾元素作为基准值的的条件下,left和right相遇的位置上的数据一
定比key小,其次就是,不需要理解为什么选取数组首元素为key,则right必须先走,left后走,为什么选取数组尾元素为key,则left先走,right后走的原因,因为这
种情况下关于谁先走的问题就比较自然,具体解释见上述,并且,两者相遇的时候,所在的位置一定是一个坑位,因为,当right找到比key小的数据后,并把该数据
放到某一个坑里,当前right所在位置形成一个新的坑,然后left再走,若left直接与right相遇的话,则相遇位置就是一个坑位,同理,当left找到比key大的数据后,并
把该数据放到某一个坑里,当前left所在位置形成一个新的坑,然后right再走,若right直接与left相遇的话,则相遇位置就是一个坑位,即,right走的时候,left所在
的位置一定是坑位,left走的时候,right所在的位置一定是坑位,所以当left和right相遇的时候,相遇位置一定是一个坑位,此时要注意,若选择数组第一个位置为
坑,则让right先走,若选择数组最后一个位置为坑,则让left先走,挖坑法比hoare方法更容易理解,但是并没有简化代码,即本质上并没有什么区别,效率上也没
有什么区别,虽然挖坑法确实比hoare方法少了一些交换的过程,但是就是多执行了常数项次,并不影响他们的时间复杂度,虽然hoare方法比挖坑法多执行了常数
项次代码,会多使用一些时间,但是对于系统而言,这些常数项次造成的影响几乎可以忽略不计,交换则需要中间变量,代价确实会更高一些,但是对于计算机而
言,这些是可以忽略不计的,两种方法在效率上并无很大的差别,时间复杂度都是O(N),都是遍历了一遍数组、
以升序为例,若经过一次单趟排序,使得key左边的所有数据均小于等于key,key右边的所有数据均大于等于key,再将key左序列和右序列进行上述单趟排序,如
此反复进行上述操作,直到子区间中只有一个数据或者不存在数据时,即子区间中只有一个数据或者子区间不存在时,为子问题的最小规模,则递归结束,因为子
区间中只有一个数据或者不存在数据时,可以看成升序,也可看成降序、
代码实现:
注意,这里就不能再像hoare版本一样使用keyi来记录基准值key下标的形式了,因为hoare版本是进行元素的交换,而挖坑法是进行元素的覆盖,若还是按照取数组
首元素作为基准值并且排升序的条件下,right先走并找到比key小的数据,把该数据放入数组第一个元素的位置上,即放入坑中的话,就不能再找到原来的基准值
key了,所以直接使用变量key来保存基准值即可、
//单趟排序->挖坑法版本、
int PartSortPit(int*a, int left, int right)//一级指针传参,一级指针接收、
{
assert(a);
//方法一:
//选取数组首元素作为基准值key,则数组中第一个位置即为坑位、
//三数取中法,取出数组首元素,尾元素,中间位置元素,选择这三者中,不是最大,也不是最小的那个值的下标、
int midi = GetMidIndex(a, left, right);
//若选择数组首元素作为基准值key,则应为:Swap(&a[midi], &a[left]); 这样就让数组首元素,尾元素,中间位置元素,三者中,不是最大,也不是最小的那个值作为了基准值key、
Swap(&a[midi], &a[left]);
//下面的代码不需要进行改变,在单趟排序中,不管使用何种方法,仍选取数组首元素或者尾元素作为基准值key、
int key = a[left];
//记录坑位的下标、
int pit = left;
while (left < right)
{
//升序、
//right先走、
while (left < right && a[right] >= key)//right找小,找不到则right--,降序则right找大,找不到则right--,变为<=、
{
right--;
}
//找到比key小的值后放入坑pit中、
a[pit] = a[right];
//更新坑位的位置、
pit = right;
//left再走、
while (left < right && a[left] <= key)//left找大,找不到则left++,降序则left找小,找不到则left++,变为>=、
{
left++;
}
//找到比key大的值后放入坑pit中、
a[pit] = a[left];
//更新坑位的位置、
pit = left;
//上面的>=和<=中的=,一是表明与基准值相等的数据在基准值的左边和右边都是就可以的,二是避免死循环,比如: 5 5 2 3 5,若两者不加等于的话,就会造成死循环、
//上面两个while中的 left < right 主要是为了防止越界访问的,若选择数组首元素作为基准值排升序和排降序时都可能会越界访问,比如1 2 3 4 5、
}
//当left和right相遇时,该相遇位置一定是坑位,再把key中的值放到该相遇位置的这个坑位上、
a[pit] = key;
//返回坑的下标就是key最后的落脚点、
return pit;
方法二:
选取数组尾元素作为基准值key,则数组中最后一个位置即为坑位、
三数取中法,取出数组首元素,尾元素,中间位置元素,选择这三者中,不是最大,也不是最小的那个值的下标、
//int midi = GetMidIndex(a, left, right);
若选择数组尾元素作为基准值key,则应为:Swap(&a[midi], &a[right]); 这样就让数组首元素,尾元素,中间位置元素,三者中,不是最大,也不是最小的那个值作为了基准值key、
//Swap(&a[midi], &a[right]);
下面的代码不需要进行改变,在单趟排序中,不管使用何种方法,仍选取数组首元素或者尾元素作为基准值key、
//int key = a[right];
记录坑位的下标、
//int pit = right;
//while (left < right)
//{
// //升序、
// //left先走、
// while (left < right && a[left] <= key)//left找大,找不到则left++,降序则left找小,找不到则left++,变为>=、
// {
// left++;
// }
// //找到比key大的值后放入坑pit中、
// a[pit] = a[left];
// //更新坑位的位置、
// pit = left;
// //right再走、
// while (left < right && a[right] >= key)//right找小,找不到则right--,降序则right找大,找不到则right--,变为<=、
// {
// right--;
// }
// //找到比key小的值后放入坑pit中、
// a[pit] = a[right];
// //更新坑位的位置、
// pit = right;
// //上面的>=和<=中的=,一是表明与基准值相等的数据在基准值的左边和右边都是就可以的,二是避免死循环,比如: 5 5 2 3 5,若两者不加等于的话,就会造成死循环、
// //上面两个while中的 left < right 主要是为了防止越界访问的,若选择数组尾元素作为基准值排升序和排降序时都可能会越界访问,比如1 2 3 4 5、
//}
当left和right相遇时,该相遇位置一定是坑位,再把key中的值放到该相遇位置的这个坑位上、
//a[pit] = key;
返回坑的下标就是key最后的落脚点、
//return pit;
}
注意:
对于hoare方法和挖坑法和前后指针法,都是快速排序的单趟排序,若排升序,并且选择数组首元素作为基准值,对于hoare方法和挖坑法而言,则基准值key左边
所有的数据均小于等于基准值key,但是这些数据并不一定是升序的,基准值key右边所有的数据均大于等于基准值key,但是,这些数据并不一定是升序的,但是
对于前后指针法而言,以cur找小为例,则基准值key左边所有的数据均小于基准值key,但是这些数据并不一定是升序的,基准值key右边所有的数据均大于等于基
准值key,但是,这些数据并不一定是升序的,并且,不同的方法每次单趟排序后整体的结果是不一样的,比如,使用hoare方法对一个数组进行单趟排序后为:
3,1,2,5,4,6,9,7,10,8,而使用挖坑法对同一个数组进行单趟排序后为:5,1,2,4,3,6,9,7,10,8,使用前后指针方法对一个数组进行单趟排序后为:5,1,2,3,4,6,9,7,10,8,
观察三者可以发现,基准值key所在的位置是确定的,并且在hoare和挖坑法两种方法中,基准值key左边所有的数据均小于等于基准值key,但是这些数据并不一定
是升序的,基准值key右边所有的数据均大于等于基准值key,但是这些数据并不一定是升序的,在前后指针方法中,基准值key左边所有的数据均小于基准值key,
但是这些数据并不一定是升序的,基准值key右边所有的数据均大于等于基准值key,但是这些数据并不一定是升序的,从整体而言,使用不同方法的单趟排序后的
整体的结果是不一样的,即,使用不同的单趟排序的方法,基准值key的位置是确定的,对于hoare和挖坑法而言,基准值key左边所有的数据均小于等于基准值
key,但是这些数据并不一定是升序的,基准值key右边所有的数据均大于等于基准值key,但是,这些数据并不一定是升序的,对于前后指针法而言,基准值key左
边所有的数据均小于基准值key,但是这些数据并不一定是升序的,基准值key右边所有的数据均大于等于基准值key,但是这些数据并不一定是升序的,但是从整
体而言,不同的单趟排序的方法每次单趟排序后的整体的顺序是不一样的,使用不同的单趟排序的方法每次单趟排序后得到的结果大概率是不一样的,也有可能是
一样的,但是概率比较小、
2.1.3、前后指针法:
思路:
假设选取数组首元素作为基准值key,定义两个变量,分别为prev和cur,此时,prev指向数组首元素,cur指向数组第二个元素,以升序为例,则变量cur找比基准
值key小的数据,只要找到了比基准值key小的数据,则让变量prev++,然后交换prev和cur所指向的数据,若prev和cur所指向的数据是同一个数据时,在代码中可
以使用判断prev和cur所指的数据的数值是否相等来判断两个变量是否指向的是同一个数据,即,如果两个变量所指的数据的数值相等,那么他们两个变量指向的肯
定是同一个数据,不会出现,两个变量指向两个不同的数据,但这两个数据数值是相等的情况,若两个变量所指向的数据的数值不相等,则说明,这两个变量肯定
指向的不是同一个数据,若prev和cur所指向的数据是同一个数据时,则交换后数组并没有发生改变,所以当prev和cur指向同一个数据时,交不交换都是可以的,
这种情况的时候,最好不需要交换,因为做的是无用功,若prev和cur所指的数据并不是同一个数据时,此时prev和cur所指向的数据的数值一定是不相等的,则需
要进行交换,之后再让变量cur继续找小,找到比基准值key小的数据后,再让变量prev++,然后再交换prev和cur所指向的数据,这里是否需要进行交换和上面的判
断方法一样,,由上述可知,当变量cur寻找比基准值key小的数据时,在未找到比基准值key大的数据前,则变量prev和cur所指向的数据是同一个数据,在变量cur
未找到比基准值key大的数据前,变量prev和cur是追赶的状态,cur找到小,则prev++,cur再找到小,则prev再++,会发现,在变量cur未找到比基准值key大的数
据之前,变量prev++后和变量cur指向的是同一个数据,当变量cur找到了比基准值key大的数据,此时变量prev不再++,,cur也不停止,继续寻找比基准值key小的
数据,当cur再找到比基准值key小的数据时,此时再让变量prev++,然后交换prev和cur所指向的数据,由于在变量cur寻找比基准值key小的数据过程中,已经遇到
了比基准值key大的数据,所以,此时变量prev和cur所指向的数据并不是同一个数据,则这两个变量所指向的数据的数值一定不相等,所以需要进行交换,由此可
以发现,当变量cur找比基准值key小的数据时,在未找到比基准值大的数据前,此时变量prev和cur呈追赶的状态,两者最终指向同一个数据,但是,一旦cur找到
了比基准值key大的数据后,则变量prev和cur就拉开了差距,通过cur找小,找到小之后,让prev++,再交换prev和cur所指向的数据,此时按照两个变量指向的不
是同一个数据为例,这一过程就是在把比基准值key小的数据往左边抛,当变量cur找到了比基准值key大的数据,此时变量prev不再++,,cur也不停止,继续寻找
比基准值key小的数据,当cur再找到比基准值key小的数据时,在prev未进行++之前,此时,prev和cur中间的数据,左开右开,就是比基准值key大的数据,然后让
prev++,这样,prev就指向了刚才两者中间的这些比基准值key大的最左边的数据,然后再让prev和cur 所指向的数据进行交换,此时按照两个变量指向的不是同一
个数据为例,这样就把比基准值key小的数据放到了左边,把比基准值key大于或等于的数据抛到了右边,所以此时从数组第二个元素到prev的位置的所有数据都是
比基准值key小的数据,左闭右闭,从prev+1的位置到cur的位置的所有数据都是大于等于基准值key的数据,左闭右闭,从cur+1的位置到right位置的所有元素是不
确定的,还没有遍历到,左闭右闭,重复上述操作,直到cur不再小于等于right,此时循环结束,此时,prev所指向的元素就是比基准值key小的值,此时再交换
prev和keyi所指向的元素,keyi记录的是基准值key的下标,此时,基准值key左边的全部数据均是小于key的值,基准值key右边的全部数据均是大于等于key的值、
假设选取数组尾元素作为基准值key,以升序为例,定义两个变量,分别为prev和cur,此时,prev指向数组首元素的前一个位置,cur指向数组的首元素,直到cur
不再小于等于right-1,此时循环结束,此时,prev所指向的元素就是比基准值key小的值,此时不可以直接交换prev和keyi所指向的元素,keyi记录的是基准值key的
下标,由于cur找小,则prev+1到right-1的所有元素都是大于等于基准值key的数据,左闭右闭,直接再交换prev+1和keyi所指的元素即可、
总结:
使用前后指针方法,不管是选择数组首元素还是尾元素作为基准值key,则循环次数均为N-1次,即时间复杂度为:O(N),由此可以知道,对于hoare法,挖坑法,前
后指针法的单趟排序中,它们三者的效率都是差不多的,本质上没有区别,时间复杂度均是:O(N)、
1、
若选取数组首元素作为基准值key,排升序,若cur找小,cur就会跳过大于和等于,相当于把等于基准值key的数据放在了key的右边,最终基准值key左边的全部
数据均小于key,基准值key右边的所有数据均大于等于key,若cur找小等,cur就会跳过大,相当于把等于基准值key的数据放在了key的左边,最终基准值key左边
的全部数据均小于等于key,基准值key右边的所有数据均大于key,由于等于基准值key的数据放在key的左边和右边都是可以的,所以上述两种方法都行,在此默
认cur找小即可,相当于把等于基准值key的数据放在key的右边、
2、
若选取数组首元素作为基准值key,排降序,若cur找大,cur就会跳过小于和等于,相当于把等于基准值key的数据放在了key的右边,最终基准值key左边的全部
数据均大于key,基准值key右边的所有数据均小于等于key,若cur找大等,cur就会跳过小,相当于把等于基准值key的数据放在了key的左边,最终基准值key左边
的全部数据均大于等于key,基准值key右边的所有数据均小于key,由于等于基准值key的数据放在key的左边和右边都是可以的,所以上述两种方法都行,在此默
认cur找大即可,相当于把等于基准值key的数据放在key的右边、
3、
若选取数组尾元素作为基准值key,排升序,若cur找小,cur就会跳过大于和等于,相当于把等于基准值key的数据放在了key的右边,最终基准值key左边的全部数
据均小于key,基准值key右边的所有数据均大于等于key,若cur找小等,cur就会跳过大,相当于把等于基准值key的数据放在了key的左边,最终基准值key左边的
全部数据均小于等于key,基准值key右边的所有数据均大于key,由于等于基准值key的数据放在key的左边和右边都是可以的,所以上述两种方法都行,在此默认cur找小即可,相当于把等于基准值key的数据放在key的右边、
4、
若选取数组尾元素作为基准值key,排降序,若cur找大,cur就会跳过小于和等于,相当于把等于基准值key的数据放在了key的右边,最终基准值key左边的全部数
据均大于key,基准值key右边的所有数据均小于等于key,若cur找大等,cur就会跳过小,相当于把等于基准值key的数据放在了key的左边,最终基准值key左边的
全部数据均大于等于key,基准值key右边的所有数据均小于key,由于等于基准值key的数据放在key的左边和右边都是可以的,所以上述两种方法都行,在此默认
cur找大即可,相当于把等于基准值key的数据放在key的右边、
5、
hoare版本和前后指针版本是进行的元素的交换,挖坑法进行的则是元素的覆盖,基于此原因,则hoare版本和前后指针版本适合定义变量keyi来记录基准值key的下
标,而对于挖坑法则适合直接定义变量key来保存选取的基准值即可、
若在前后指针方法中定义变量key的话,则最会一步的交换还没有办法使用变量key,所以不定义变量key,直接定义keyi就可以解决这个问题,也可以既不定义key,
也不定义keyi,因为,前后指针版本在不断改变的指针是prev和cur,并不是left和right,所以,当选取数组首元素作为基准值key时可以使用left在定义keyi的情况下
代替keyi进行工作,当选取数组尾元素作为基准值key时可以使用right在定义keyi的情况下代替keyi进行工作、
若在hoar版本中定义变量key的话,则最会一步的交换还没有办法使用变量key,所以不定义变量key,直接定义keyi就可以解决这个问题,不可以key和keyi都不定
义,因为该方法和前后指针方法不一样,该方法中的left和right在过程中是在不断地发生变化的,若想要记录每一个单趟排序序列中的首元素和尾元素的话,不能使
用left和right,他们都在变化,所以还是避免不了使用额外的变量来记录每一个单趟排序序列中的首元素和尾元素作为基准值而且在该方法中不适合定义key,则直接
定义keyi即可、
若在挖坑法版本中定义变量keyi的话,由于是元素的覆盖,所以就可能导致找不到原来的基准值了,所以不定义变量keyi,直接定义变量key就可以解决问题,不可
以key和keyi都不定义,即在选取数组首元素作为基准值时,不能直接使用left在选取数组尾元素作为基准值时,不能直接使用right,因为挖坑法是进行元素的覆
盖,这样还是会导致丢失原来的基准值,所以必须定义key、
//单趟排序->前后指针版本、
int PartSortPC(int*a, int left, int right)//一级指针传参,一级指针接收、
{
assert(a);
方法一:
选取数组首元素作为基准值key,使用变量keyi来记录基准值key的下标、
在该版本内,不适合定义key,分析在下面,最好定义keyi,除此之外,可以既不定义key,也不定义keyi,因为,前后指针版本在不断改变的指针是prev和cur,并不是left和right,所以,当选取数组首元素作为基准值key时
可以使用left在定义keyi的情况下代替keyi进行工作、
三数取中法,取出数组首元素,尾元素,中间位置元素,选择这三者中,不是最大,也不是最小的那个值的下标、
//int midi = GetMidIndex(a, left, right);
若选择数组首元素作为基准值key,则应为:Swap(&a[midi], &a[left]); 这样就让数组首元素,尾元素,中间位置元素,三者中,不是最大,也不是最小的那个值作为了基准值key、
//Swap(&a[midi], &a[left]);
下面的代码不需要进行改变,在单趟排序中,不管使用何种方法,仍选取数组首元素或者尾元素作为基准值key、
//int keyi = left; //可省略、
//int prev = left;
//int cur = left + 1;
当定义上面的变量prev和cur时,是不会造成越界访问的,因为,当在递归中凡是能执行到该PartSortPC函数时,则该函数的形参部分中的left一定小于right
否则根本就执行不到该调用函数,所以,本次单趟排序中,至少存在两个数据,所以当定义两个变量prev和cur时,下面再使用这两个下标的时候,不会造成越界访问、
//while (cur <= right)
//{
// //升序,cur找小,当cur找到的数据的数值大于等于key时,则cur继续找小,即不进入下面的if语句中、
// if (a[cur] < a[keyi]) // if (a[cur] < a[left])
// {
// prev++;
// //此处可以不进行判断,直接交换也行,但是最好判断一下,避免做无用功、
// //此处直接写成 prev != cur 也是可以的,只有当prev==cur时,两者指向的数据的数值才是相等的,除此之外,不会存在两者指向不同的数据,但是两个数据的值是相等的情况
// //即不会存在当prev不等于cur时,但他们所指元素的值是相等的情况,所以当prev不等于cur时,两者指向的元素的数值一定不相等、当prev等于cur时,两者指向的元素的数值一定相等、
// if (a[prev] != a[cur]) //if (prev != cur)
// {
// Swap(&a[prev], &a[cur]);
// }
// }
// //此处会有两种情况,一是当cur找到的数据的数值大于等于key时,则cur继续找小,即不进入下面的if语句中,cur继续++找小、
// //二是当cur找到的数据的数值小于key时,进入if语句,让prev++,再判断一下是否需要进行交换,等if语句出来后,cur仍要进行++,继续往后找小、
// cur++;
// 降序,cur找大,当cur找到的数据的数值小于等于key时,则cur继续找大,即不进入下面的if语句中、
// //if (a[cur] > a[keyi]) //if (a[cur] > a[left])
// //{
// // prev++;
// // //此处可以不进行判断,直接交换也行,但是最好判断一下,避免做无用功、
// // //此处直接写成 prev != cur 也是可以的,只有当prev==cur时,两者指向的数据的数值才是相等的,除此之外,不会存在两者指向不同的数据,但是两个数据的值是相等的情况
// // //即不会存在当prev不等于cur时,但他们所指元素的值是相等的情况,所以当prev不等于cur时,两者指向的元素的数值一定不相等、当prev等于cur时,两者指向的元素的数值一定相等、
// // if (a[prev] != a[cur]) //if (prev != cur)
// // {
// // Swap(&a[prev], &a[cur]);
// // }
// //}
// 此处会有两种情况,一是当cur找到的数据的数值小于等于key时,则cur继续找大,即不进入下面的if语句中,cur继续++找大、
// 二是当cur找到的数据的数值大于key时,进入if语句,让prev++,再判断一下是否需要进行交换,等if语句出来后,cur仍要进行++,继续往后找大、
// //cur++;
//}
prev所指向的元素就是比基准值key小的值,此时再交换prev和keyi所指向的元素、
prev所指向的元素就是比基准值key大的值,此时再交换prev和keyi所指向的元素、
//Swap(&a[prev], &a[keyi]); //Swap(&a[prev], &a[left]);
//return prev;
方法二 -> 方法一的简化:
选取数组首元素作为基准值key,使用变量keyi来记录基准值key的下标、
在该版本内,不适合定义key,分析在下面,最好定义keyi,除此之外,可以既不定义key,也不定义keyi,因为,前后指针版本在不断改变的指针是prev和cur,并不是left和right,所以,当选取数组首元素作为基准值key时
可以使用left在定义keyi的情况下代替keyi进行工作、
三数取中法,取出数组首元素,尾元素,中间位置元素,选择这三者中,不是最大,也不是最小的那个值的下标、
//int midi = GetMidIndex(a, left, right);
若选择数组首元素作为基准值key,则应为:Swap(&a[midi], &a[left]); 这样就让数组首元素,尾元素,中间位置元素,三者中,不是最大,也不是最小的那个值作为了基准值key、
//Swap(&a[midi], &a[left]);
下面的代码不需要进行改变,在单趟排序中,不管使用何种方法,仍选取数组首元素或者尾元素作为基准值key、
//int keyi = left; //可省略、
//int prev = left;
//int cur = left + 1;
当定义上面的变量prev和cur时,是不会造成越界访问的,因为,当在递归中凡是能执行到该PartSortPC函数时,则该函数的形参部分中的left一定小于right
否则根本就执行不到该调用函数,所以,本次单趟排序中,至少存在两个数据,所以当定义两个变量prev和cur时,下面再使用这两个下标的时候,不会造成越界访问、
//while (cur <= right)
//{
// //默认当变量prev和cur指向同一个数据时,不需要进行交换、
// //升序,cur找小,若cur找到了小,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,若cur找到了大于等于key的值,则更不进入if语句、
// //只有当cur找到了小,并且,++prev后和cur不是指向了同一个数据时,才进入if语句进行交换、
// //此处直接写成 ++prev != cur 也是可以的,只有当prev==cur时,两者指向的数据的数值才是相等的,除此之外,不会存在两者指向不同的数据,但是两个数据的值是相等的情况
// //即不会存在当prev不等于cur时,但他们所指元素的值是相等的情况,所以当prev不等于cur时,两者指向的元素的数值一定不相等、当prev等于cur时,两者指向的元素的数值一定相等、
// if (a[cur] < a[keyi] && a[++prev] != a[cur]) //if (a[cur] < a[left] && a[++prev] != a[cur]) //if (a[cur] < a[keyi] && ++prev != cur) if (a[cur] < a[left] && ++prev != cur)
// {
// Swap(&a[prev], &a[cur]);
// }
// //此处会有三种情况,一是当cur找到的数据的数值大于等于key时,则cur继续找小,即不进入下面的if语句中,cur继续++找小、
// //二是当cur找到的数据的数值小于key时,并且++prev后和cur不是指向了同一个数据时,则进入if语句进行交换,等if语句出来后,cur仍要进行++,继续往后找小、
// //三是当cur找到的数据的数值小于key时,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,而cur仍要进行++,继续往后找小、
// cur++;
// 降序,cur找大,若cur找到了大,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,若cur找到了小于等于key的值,则更不进入if语句、
// 只有当cur找到了大,并且,++prev后和cur不是指向了同一个数据时,才进入if语句进行交换、
// 此处直接写成 ++prev != cur 也是可以的,只有当prev==cur时,两者指向的数据的数值才是相等的,除此之外,不会存在两者指向不同的数据,但是两个数据的值是相等的情况
// 即不会存在当prev不等于cur时,但他们所指元素的值是相等的情况,所以当prev不等于cur时,两者指向的元素的数值一定不相等、当prev等于cur时,两者指向的元素的数值一定相等、
// //if (a[cur] > a[keyi] && a[++prev] != a[cur]) //if (a[cur] > a[left] && a[++prev] != a[cur]) if (a[cur] > a[keyi] && ++prev != cur) if (a[cur] > a[left] && ++prev != cur)
// //{
// // Swap(&a[prev], &a[cur]);
// //}
// 此处会有三种情况,一是当cur找到的数据的数值小于等于key时,则cur继续找大,即不进入下面的if语句中,cur继续++找大、
// 二是当cur找到的数据的数值大于key时,并且++prev后和cur不是指向了同一个数据时,则进入if语句进行交换,等if语句出来后,cur仍要进行++,继续往后找大、
// 三是当cur找到的数据的数值大于key时,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,而cur仍要进行++,继续往后找大、
// //cur++;
//}
prev所指向的元素就是比基准值key小的值,此时再交换prev和keyi所指向的元素、
prev所指向的元素就是比基准值key大的值,此时再交换prev和keyi所指向的元素、
//Swap(&a[prev], &a[keyi]); //Swap(&a[prev], &a[left]);
//return prev;
//对于方法一二中,为什么不能使用变量key来直接记录基准值呢,即定义int key = a[left],在最后再进行Swap(&a[prev], &key);
//这一定是不对的,因为,最后一步的交换是为了交换数组中的元素,即,prev指向的元素与数组首元素,就是所选取的基准值进行的交换,如果按照上述方法的话,因为key是在调用函数
//内部定义的,是一个局部变量,该局部变量中存储的是数组首元素的值,如果与它进行交换的话,确实能够把基准值放到prev所指的位置上,这是因为key中存储的就是所选取的基准值,
//此时,局部变量key中存储的就是交换前prev所指的元素,但是不要忘记,此时数组首元素的值还是最初选定的基准值,原本我们是想把数组首元素的值,即选定的基准值放到变量prev
//所指的位置上,并把prev所指的元素放到数组的首位置上成为数组的首元素,若按照上述方法进行定义的话,就满足不了我们的条件了,所以不可以直接把基准值保存到变量key中,
//所以在此还是要定义变量keyi来记录基准值key的下标、
//在该前后指针版本内,不适合定义key,最好定义keyi,除此之外,可以既不定义key,也不定义keyi,因为,前后指针版本在不断改变的指针是prev和cur,并不是left和right,所以,当选取数组首元素作为基准值key时
//可以使用left在定义keyi的情况下代替keyi进行工作、
//hoare版本和前后指针版本是进行的元素的交换,挖坑法进行的则是元素的覆盖,基于此原因,则hoare版本和前后指针版本适合定义变量keyi来记录基准值key的下标,而对于挖坑法则适合直接定义变量key来保存选取的基准值即可、
//若在前后指针方法中定义变量key的话,则最会一步的交换还没有办法使用变量key,所以不定义变量key,直接定义keyi就可以解决这个问题,也可以既不定义key,也不定义keyi,因为,前后指针版本在不断改变的指针是prev和cur,
//并不是left和right,所以,当选取数组首元素作为基准值key时可以使用left在定义keyi的情况下代替keyi进行工作,当选取数组尾元素作为基准值key时可以使用right在定义keyi的情况下代替keyi进行工作、
//若在hoar版本中定义变量key的话,则最会一步的交换还没有办法使用变量key,所以不定义变量key,直接定义keyi就可以解决这个问题,不可以key和keyi都不定义,因为该方法和前后指针方法不一样,该方法中的
//left和right在过程中是在不断地发生变化的,若想要记录每一个单趟排序序列中的首元素和尾元素的话,不能使用left和right,他们都在变化,所以还是避免不了使用额外的变量来记录每一个单趟排序序列中的首元素和尾元素作为基准值
//而且在该方法中不适合定义key,则直接定义keyi即可、
//若在挖坑法版本中定义变量keyi的话,由于是元素的覆盖,所以就可能导致找不到原来的基准值了,所以不定义变量keyi,直接定义变量key就可以解决问题,不可以key和keyi都不定义,即在选取数组首元素作为基准值时,不能直接使用left
//在选取数组尾元素作为基准值时,不能直接使用right,因为挖坑法是进行元素的覆盖,这样还是会导致丢失原来的基准值,所以必须定义key、
//方法三:
//选取数组尾元素作为基准值key,使用变量keyi来记录基准值key的下标、
//在该版本内,不适合定义key,分析在上面,最好定义keyi,除此之外,可以既不定义key,也不定义keyi,因为,前后指针版本在不断改变的指针是prev和cur,并不是left和right,所以,当选取数组尾元素作为基准值key时
//可以使用right在定义keyi的情况下代替keyi进行工作、
//三数取中法,取出数组首元素,尾元素,中间位置元素,选择这三者中,不是最大,也不是最小的那个值的下标、
int midi = GetMidIndex(a, left, right);
//若选择数组尾元素作为基准值key,则应为:Swap(&a[midi], &a[right]); 这样就让数组首元素,尾元素,中间位置元素,三者中,不是最大,也不是最小的那个值作为了基准值key、
Swap(&a[midi], &a[right]);
//下面的代码不需要进行改变,在单趟排序中,不管使用何种方法,仍选取数组首元素或者尾元素作为基准值key、
int keyi = right; //可省略、
int prev = left-1;
int cur = left;
//当定义上面的变量prev和cur时,是不会造成越界访问的,因为,当在递归中凡是能执行到该PartSortPC函数时,则该函数的形参部分中的left一定小于right
//否则根本就执行不到该调用函数,所以,本次单趟排序中,至少存在两个数据,所以当定义变量cur时,下面再使用这个下标的时候,不会造成越界访问,当定义变量prev时,也不会造成越界,
//因为,是++prev,先++后使用,所以不会造成越界访问、
while (cur < right) //while (cur <= right-1)
{
//默认当变量prev和cur指向同一个数据时,不需要进行交换、
//升序,cur找小,若cur找到了小,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,若cur找到了大于等于key的值,则更不进入if语句、
//只有当cur找到了小,并且,++prev后和cur不是指向了同一个数据时,才进入if语句进行交换、
//此处直接写成 ++prev != cur 也是可以的,只有当prev==cur时,两者指向的数据的数值才是相等的,除此之外,不会存在两者指向不同的数据,但是两个数据的值是相等的情况
///即不会存在当prev不等于cur时,但他们所指元素的值是相等的情况,所以当prev不等于cur时,两者指向的元素的数值一定不相等、当prev等于cur时,两者指向的元素的数值一定相等、
if (a[cur] < a[keyi] && a[++prev] != a[cur]) //if (a[cur] < a[right] && a[++prev] != a[cur]) if (a[cur] < a[keyi] && ++prev != cur) if (a[cur] < a[right] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
//此处会有三种情况,一是当cur找到的数据的数值大于等于key时,则cur继续找小,即不进入下面的if语句中,cur继续++找小、
//二是当cur找到的数据的数值小于key时,并且++prev后和cur不是指向了同一个数据时,则进入if语句进行交换,等if语句出来后,cur仍要进行++,继续往后找小、
//三是当cur找到的数据的数值小于key时,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,而cur仍要进行++,继续往后找小、
cur++;
降序,cur找大,若cur找到了大,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,若cur找到了小于等于key的值,则更不进入if语句、
只有当cur找到了大,并且,++prev后和cur不是指向了同一个数据时,才进入if语句进行交换、
//此处直接写成 ++prev != cur 也是可以的,只有当prev==cur时,两者指向的数据的数值才是相等的,除此之外,不会存在两者指向不同的数据,但是两个数据的值是相等的情况
///即不会存在当prev不等于cur时,但他们所指元素的值是相等的情况,所以当prev不等于cur时,两者指向的元素的数值一定不相等、当prev等于cur时,两者指向的元素的数值一定相等、
//if (a[cur] > a[keyi] && a[++prev] != a[cur]) //if (a[cur] > a[right] && a[++prev] != a[cur]) if (a[cur] > a[keyi] && ++prev != cur) if (a[cur] > a[right] && ++prev != cur)
//{
// Swap(&a[prev], &a[cur]);
//}
此处会有三种情况,一是当cur找到的数据的数值小于等于key时,则cur继续找大,即不进入下面的if语句中,cur继续++找大、
二是当cur找到的数据的数值大于key时,并且++prev后和cur不是指向了同一个数据时,则进入if语句进行交换,等if语句出来后,cur仍要进行++,继续往后找大、
三是当cur找到的数据的数值大于key时,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,而cur仍要进行++,继续往后找大、
//cur++;
}
//prev所指向的元素就是比基准值key小的值,此时不可以直接交换prev和keyi所指向的元素,由于cur找小,则prev+1到right-1的所有元素都是大于等于基准值key的数据,左闭右闭,直接再交换prev+1和keyi所指的元素即可、
//prev所指向的元素就是比基准值key大的值,此时不可以直接交换prev和keyi所指向的元素,由于cur找大,则prev+1到right-1的所有元素都是小于等于基准值key的数据,左闭右闭,直接再交换prev+1和keyi所指的元素即可、
Swap(&a[++prev], &a[keyi]); //Swap(&a[++prev], &a[right]);
return prev;
//Swap(&a[prev + 1], &a[keyi]); //Swap(&a[prev + 1], &a[right]);
return prev+1;
}
2.2、快速排序递归版本的时间复杂度求解:
2.2.1、最好情况:
最好情况:O(N*logN)、
每次单趟排序选出来的基准值key,就是先对本次待排序序列进行排升序或者降序,取下标为:left+(right-left)/2 的数据作为基准值key, 即每次单趟排序所选择的
基准值key都是中位数或者非常接近于中位数的数据,即单趟排序结束后key位于序列正中间或者非常接近于正中间,所谓中位数即指,该数组排升序或者降序后,
位于中间或者非常接近与中间的值称为中位数,即最好情况发生在整个数组被分成两段长度相等或很接近相等的子数组时,这样的话,基准值key的选取会更加合
理,减少了递归的深度,此时递归深度达到了最小、
此时是要求解递归算法的时间复杂度,并且发现,在递归调用函数QuickSort内部中,存在单趟排序的调用函数PartSortXXX,对于单趟排序而言,不管使用哪种方
法,则该单趟排序的调用函数PartSortXXX中的循环次数是和递归调用函数QuickSort的参数有关的,即每一次递归函数QuickSort调用中的执行次数和该递归函数的
参数是有关系的,则递归算法的时间复杂度就不能再简单的使用递归的次数乘以每次递归中的时间复杂度来求解了,此时递归算法的时间复杂度等于所有的递归调
用中执行次数的累加,必须都列出来,最后再进行求和,所以在此可以把每一次递归函数调用中的执行次数列出来进行相加,即上图中的每一个长方形的长度进行
相加,因为此处所说的执行次数只算循环次数即可,若使用hoare和挖坑法的话,则循环次数就是数组的长度N,若使用前后指针法的话,则循环次数就是数组的长
度N再减去1,即N-1,但是1是常数次,可以忽略不计,所以就认为不管使用那种单趟排序的方法,循环次数就看做是数组的长度,也就是上图中的长方形的长度,
为了方便计算也可以从整体来计算,比如,第二层,应该是前面长方形的长度加上后面长方形的长度,而这两个长方形的和等于第一层中的长方形的长度减去1,即
整体而言就比第一层少了一个数据,所以第三层,第四层...都可以看作一个整体,观察上图发现满足满二叉树的定义,假设数组长度为N,上图中每一个长方形就固
定了一个数据的位置,而数组的长度是N,所以看做满二叉树的话,则满二叉树的节点个数为N个,则满二叉树的高度为log(N+1),第一层执行次数为N,第二层执
行次数为N-1,第三层执行次数为N-1-2,第四层执行次数为N-1-2-4.....直到第log(N+1)层,把所有的执行次数相加就得到了总的执行次数,但是直接计算的话,不
太方便进行计算,所以可以把每一层执行次数都看成N次,一共log(N+1)层,求和得到N*log(N+1),还要减去一部分,但是这部分对最后的时间复杂度并不影响,所
以,最后的时间复杂度为:O(N*logN)、
2.2.2、最坏情况:
最坏情况:O(N^2)、
1、排升序,选择数组首元素作为 基准值key,数组已经是升序,则是最坏的情况、
2、排升序,选择数组首元素作为基准值key,数组已经是降序,则是最坏的情况、
3、排升序,选择数组尾元素作为基准值key,数组已经是升序,则是最坏的情况、
4、排升序,选择数组尾元素作为基准值key,数组已经是降序,则是最坏的情况、
5、排降序,选择数组首元素作为基准值key,数组已经是升序,则是最坏的情况、
6、排降序,选择数组首元素作为基准值key,数组已经是降序,则是最坏的情况、
7、排降序,选择数组尾元素作为基准值key,数组已经是升序,则是最坏的情况、
8、排降序,选择数组尾元素作为基准值key,数组已经是降序,则是最坏的情况、
即,待排序序列已经是升序或者降序了,若每一次划分只能得到一个比上一次划分少一个数据的子序列和一个空子序列时,即为最坏的情况,即当待排序序列已经
是升序或者是降序的情况下,若每一次单趟排序再选择待排序序列中最左边或者最右边的元素作为基准值key时,那么即为最坏的情况,或者也可以理解为,因为单
趟排序时一般常选取数组首元素或尾元素作为基准值key,若每一次单趟排序中,选取的基准值key为该待排序序列中的最值,即最大值或者最小值时,则是最坏的
情况,也就是快速排序的效率达到最低的情况,由于递归深度达到最大、
以排升序,选择数组首元素作为基准值key,数组已经是升序,则是最坏的情况为例:
此时是要求解递归算法的时间复杂度,并且发现,在递归调用函数QuickSort内部中,存在单趟排序的调用函数PartSortXXX,对于单趟排序而言,不管使用哪种方
法,则该单趟排序的调用函数PartSortXXX中的循环次数是和递归调用函数QuickSort的参数有关的,即每一次递归函数QuickSort调用中的执行次数和该递归函数的
参数是有关系的,则递归算法的时间复杂度就不能再简单的使用递归的次数乘以每次递归中的时间复杂度来求解了,此时递归算法的时间复杂度等于所有的递归调
用中执行次数的累加,必须都列出来,最后再进行求和,所以,第一次递归,即第一层的循环次数为数组的长度,即为N次,若使用hoare和挖坑法的话,则循环次
数就是数组的长度N,若使用前后指针法的话,则循环次数就是数组的长度N再减去1,即N-1,但是1是常数次,可以忽略不计,所以就认为不管使用那种单趟排序
的方法,循环次数就看做是数组的长度,也就是上图中的长方形的长度,第二层理论上是有一个比原数组少一个元素的子序列,还有一个空子序列,而空子序列中
循环次数为0,可以忽略,对于这一个比原数组少一个元素的子序列而言,其循环次数为N-1次,则第二层的循环次数为N-1次,下面的过程也是类似,直到某一层
上有一个空子序列,还有一个非空子序列,该非空子序列中只有一个元素时为止,假设数组长度为N,上图中每一个长方形就固定了一个数据的位置,要想固定N个
元素的位置,则需要N个长方形,即,高度一共为N层,把所有的递归调用中的执行次数加起来,此时执行次数主要是指的循环次数,则总的执行次数即为:
T(N)=N+N-1+N-2+.....+3+2+1,然后整体再加上N-1个0,所以最终的时间复杂度为:O(N^2)、
则此时的快速排序的最好情况下的部分时间复杂度为:O(N*logN),最坏情况下的部分时间复杂度为:O(N^2),但是对于时间复杂度而言考虑的是最坏的情况,也就是
说,目前这个快速排序的时间复杂度为:O(N^2),一个快速排序的时间复杂度是:O(N^2),这怎么能称得上是快速排序呢,并且在这最坏的情况下,或者接近于最坏
的情况下,此时递归的深度较大,即数组的元素个数过多的话,还有可能会出现栈溢出的问题,所以要对这个快速排序进行优化、
C++中的STL库中的sort库函数和C语言中的qsort库函数都是使用快速排序优化版本来实现的,所以说快速排序的优化版本还是要比希尔排序更好一点的、
2.3、快速排序的两个优化:
对于未优化的快速排序而言,若数组是随机数组的话,这种未优化的快速排序的效率经测试还是比较好的,要想达到最坏的情况,概率是非常非常小的,条件比较
苛刻,除非数组已经是升序或者降序,或接近升序或者降序了,并且选取数组首元素或者尾元素作为基准值key才会出现最坏或者接近最坏的情况,能不能针对性的
进行优化一下呢,即能否针对未优化的快速排序中的最坏情况进行优化呢?
2.3.1、针对基准值key的选择进行优化:
1、
随机选择基准值key,这样的话出现最坏或者接近最坏的情况的几率就很小,虽然这种方法能够很有效的避免出现最坏或者接近最坏的情况,但是由于是随机选
key,key的值是随机选定的,并不能自己控制选取基准值key,所以该方法能够使用,但并不是最好的、
2、
三数取中法,取出数组首元素,尾元素,中间位置的元素,选择这三者中,不是最大,也不是最小的那个值做为基准值key,对于数组已经是升序或者降序的情况、
下, 若按照三数取中的方法的话,则就选到了中位数,即在本次单趟排序中是最好的情况,那么为什么不直接选择中间这个中位数,而是通过三数取中的方法来选
择到它呢,是因为,不知道数组是否已经升序或者降序,数组可能是一个随机数组,直接选中间的那个数的话,不一定是中位数,还有可能是最小值或者最大值,
这样的话,本次单趟排序就成了最坏的情况了,所以要通过三数取中来选择基准值key,若通过三数取中的话,如果本次单趟排序中的待排序序列为升序或者降序的
话,那么该次单趟排序直接从最坏的情况变成了最好的情况,如果本次单趟排序中的待排序序列是随机序列的话,通过三数取中法最少能够保证取到的基准值key不
是最大值,也不是最小值,这样就避免了本次单趟排序是最坏的情况了,当然,使用该方法,比如,5 7 2 4 3 8 9 6 1,若选取数组首元素作为基准值key的话,正
好5是中位数,这样本次单趟排序就是最好的情况,但是通过三数取中的方法,选择了3作为基准值key,反而变为了不是最好的情况,这种损失可以忽略不计,因
为,对快速排序基准值key的选择进行优化并不要求每次单趟排序都是最好,但必须要求每次单趟排序不能是最坏就行了、
对于通过两种优化后的快速排序的版本而言,经过测试,在任何情况下的效率都很高,故各个语言中官方的排序基本上使用的都是优化后的快速排序来实现的、
//三数取中法、
//取出数组首元素,尾元素,中间位置元素,得到这三者中,不是最大,也不是最小的那个值的下标、
int GetMidIndex(int*a, int left, int right)
{
assert(a);
//记录数组中间位置的下标、
//int mid = (left + right) / 2; //如果 left+right 过大可能会存在溢出,最好不采用这种方法、
int mid = left + (right - left) / 2; //避免 left+right 过大导致溢出,也可以使用移位、
//当数组首元素,尾元素,中间位置上的元素,这三个元素至少有两个相等时,则就无法选出来既不是最大,也不是最小的那个值,所以此时把这三个元素中任意一个元素的下标返回出去即可,
//也可以理解为,让这三个元素中任意一个元素作为所谓的既不是最大,也不是最小的元素,则 left mid right 均可视为既不是最大,也不是最小的那个元素的下标、
//故在代码中只需要考虑三个数据两两不相等的情况即可,因为当不是三个数据两两不相等的时候,把这三个数据中任意一个数据的下标返回出去都是可以的、
// left mid right
//方法一:
//分a[left] < a[mid]和a[left] > a[mid] 两种情况
if (a[left] < a[mid]) //a[left] < a[mid]、
{
if (a[mid] < a[right])
{
return mid; //即: a[left] a[mid] a[right] 此时mid所指的元素既不是最大,也不是最小的数据、
}
else if (a[left]>a[right])
{
return left; //即: a[right] a[left] a[mid] 此时left所指的元素既不是最大,也不是最小的数据、
}
else
{
return right; //即: a[left] a[right] a[mid] 此时right所指的元素既不是最大,也不是最小的数据、
}
}
else //a[left] > a[mid]、
{
if (a[mid] > a[right])
{
return mid; //即: a[right] a[mid] a[left] 此时mid所指的元素既不是最大,也不是最小的数据、
}
else if (a[left] > a[right])
{
return right; //即: a[mid] a[right] a[left] 此时right所指的元素既不是最大,也不是最小的数据、
}
else
{
return left; //即: a[mid] a[left] a[right] 此时left所指的元素既不是最大,也不是最小的数据、
}
}
方法二:
//int mid = left + (right - left) / 2;
//if (a[left] < a[right])
//{
// if (a[mid] < a[left])
// return left;
// else if (a[mid] > a[right])
// return right;
// else
// return mid;
//}
//else //a[left] > a[right]
//{
// if (a[mid] < a[right])
// return right;
// else if (a[mid] > a[left])
// return left;
// else
// return mid;
//}
}
2.3.2、小区间优化:
小区间优化并不是只针对于最坏的情况进行的优化,它是对所有的情况都能进行优化、
在此以最好的情况为例,当快排使用三数取中优化后要想得到每次单趟排序都是最好的情况的话,需要原数组已经升序或者降序,会发现就算是最好的情况时,也
不能避免随着递归的深入,每一层的递归次数会以2倍的形式快速增长,此时会发现,快排递归调用展开简化图就是一颗满二叉树,如果子序列中的元素个数较少
时,若再采用递归方法的话,会进行多次递归调用来排序这为数不多的数据就比较浪费,比如,假设满二叉树是10层,则第10层就有2^9次递归调用,第9层有2^8
次递归调用,随着递归深度的增加,即满二叉树深度越深,则子序列中的元素个数就越来越少,同时更深的层上的节点的个数越来越多,当子序列中的元素个数很
少的时候,此时正处在满二叉树较深的层次上,若再使用递归的话,则再深一层上的节点个数就越多,即递归调用的次数就越多,所以当子序列中的元素个数较少
时,直接采用小区间优化,所以若采用小区间优化的话,就会减少很多次递归调用,虽然能够减少很多次递归调用,但是经过测试,尤其是在release版本下,编译
器优化之后,函数调用的代价很低,所以小区间优化的效果并不是很明显,但是还是会起到一定的作用的,在Debug版本下可能会有一些效果,会更加明显一点,
所以,当子序列中元素个数很少的时候,不再使用递归的方法去排序这些为数不多的元素,直接采用插入排序对这些为数不多的元素进行排序即可,从而减少了递
归调用次数,这就是小区间优化、由于元素个数很少,使用希尔或者堆排就比较麻烦,则可以从,冒泡优化,选择优化,插入排序三者中选一个即可,虽然在逆序
或者随机时,选择优化会略胜插入一筹,但是从整体来看,这三者还是要优先选择插入排序,在除了顺序或者接近顺序的情况下,对于数据量大的时候,希尔会比
插入更有优势,但是对于数据量小的时候,希尔并不一定比插入更合适,所以此处的小区间优化,直接采用插入排序即可,这就是小区间优化、
为了减少递归树的最后几层递归,我们可以设置一个判断语句,当序列的长度小于等于某个数的时候就不再进行快速排序中的递归调用,该数一般选取13,转而使
用其他种类的排序,优先使用插入排序,小区间优化若是使用得当的话,会在一定程度上加快快速排序的效率、
//快速排序、
void QuickSort(int* a, int begin, int end)
{
assert(a);
//如果子区间中只有一个值或者子区间不存在时,即为最小规模的子问题、
//即如果子区间中只有一个数据或者不存在数据时,就是最小规模的子问题,不再进行递归,只有一个元素或者没有元素,也可看成是有序的,可以是升序,也可以是降序、
if (begin >= end)
{
return;
}
else
{
if (end - begin+1 <= 10) //此处的+1主要是为了和下面同步,一般选取13、
{
//小区间优化,直接采用插入排序来代替递归调用进行排序、
//此处要写成a+begin,否则每次都是对数组的前end-begin+1个元素进行的插入排序,不符合题意、
InsertSort(a+begin, end - begin + 1);//begin和end是左闭右闭,所以直接end-begin会导致少算一个元素,则要加上1、
//要注意,若使用了小区间优化,若排升序,不仅要在单趟排序中改为排升序,还要在直接插入排序中改为排升序,同理,排降序两边都要改、
}
else
{
int keyi = PartSortPC(a, begin, end);
//单趟排完后,key已经放在了应该在的正确的位置,如果key的左边满足升序,右边也满足升序,那么则整个数组都是升序的,如果key的左边满足降序,右边也满足降序,那么则整个数组都是降序的、
//采用分治的思想去使得key的左边和右边都满足升序或者降序即可,分治解决子问题、
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
}
}
注意:在此说明一下,对于快速排序未优化的版本而言,最好和最坏的情况不一样,不可以直接套结论求解时间复杂度,只能在两种情况下分别套结论求出两种情
况下的部分时间复杂度,具体套哪个结论,再分析,即,最好情况下的部分时间复杂度时:O(N*logN),最坏情况下的部分时间复杂度为:O(N^2),所以,未优化的快
速排序的时间复杂度为:O(N^2),现在如果对未优化的快排加上三数取中进行优化的话,则优化后的快排中每次单趟排序都是朝着最好的方向进行发展,当所有的单
趟排序都进行完毕后,则整个快速排序就很接近于整体最好的情况,理论上,优化后的快速排序整体一定也存在最好和最坏的情况,这是因为就算进行优化,那么
每次单趟排序中也不一定都达到都是最好的情况,但是不管每次单趟排序是否都能够达到最好的情况,每一次单趟排序中都是朝着最好的情况进行发展,如果优化
后,每次单趟排序都能达到最好,则优化后的快排整体就是整体最好的,若每一次单趟排序中不能都达到最好,则整体就不是最好的情况,即使每一次单趟排序中
不能都达到最好,但每一次单趟排序中都是很接近于最好的情况,所以对于整体而言,是非常接近于每次单趟排序都是最好的情况,所以优化后的快排整体就很接
近于整体最好的情况,即,优化后的快排整体最坏的情况从概率上而言也非常接近于整体最好的情况,这时,就默认优化后的快排中最好最坏的情况是一样的即
可,即最好和最坏都是接近于整体最好的情况,所以优化后的快排整体的时间复杂度就直接看做等于最好情况下的时间复杂度O(N*logN)即可、
对于快速排序未优化的版本而言,最好和最坏的情况不一样,不可以直接套结论求解空间复杂度,只能在两种情况下分别套结论求出两种情况下的部分空间复杂
度,具体套哪个结论,再分析,即,最好情况下的部分空间复杂度时:O(logN),因为,此时,递归层数为log(N+1)层,每一次递归调用中的空间复杂度为o(1),包括
代码中开辟的常数个额外的空间,再加上每一次递归调用栈帧中开辟的常数个额外的空间,两者加起来还是常数个额外的空间,所以此时空间复杂度等于
log(N+1)*O(1),就是O(log(N+1)),即O(logN),最坏情况下的部分时间复杂度为:O(N),因为,此时,递归层数为N层,每一次递归调用中的空间复杂度为o(1),包
括代码中开辟的常数个额外的空间,再加上每一次递归调用栈帧中开辟的常数个额外的空间,两者加起来还是常数个额外的空间,所以此时空间复杂度等于
N*O(1),即O(N),所以,未优化的快速排序的时间复杂度为:O(N),现在如果对未优化的快排加上三数取中进行优化的话,则优化后的快排中每次单趟排序都是朝
着最好的方向进行发展,当所有的单趟排序都进行完毕后,则整个快速排序就很接近于整体最好的情况,理论上,优化后的快速排序整体一定也存在最好和最坏的
情况,这是因为就算进行优化,那么每次单趟排序中也不一定都达到都是最好的情况,但是不管每次单趟排序是否都能够达到最好的情况,每一次单趟排序中都是
朝着最好的情况进行发展,如果优化后,每次单趟排序都能达到最好,则优化后的快排整体就是整体最好的,若每一次单趟排序中不能都达到最好,则整体就不是
最好的情况,即使每一次单趟排序中不能都达到最好,但每一次单趟排序中都是很接近于最好的情况,所以对于整体而言,是非常接近于每次单趟排序都是最好的
情况,所以优化后的快排整体就很接近于整体最好的情况,即,优化后的快排整体最坏的情况从概率上而言也非常接近于整体最好的情况,这时,就默认优化后的
快排中最好最坏的情况是一样的即可,即最好和最坏都是接近于整体最好的情况,所以优化后的快排整体的空间复杂度就直接看做等于最好情况下的空间复杂度
O(logN)即可、
上述只是针对于普通的情况而言,对于三数取中优化后的快速排序在某些特定情况下仍无法避免出现时间复杂度是:O(N^2),空间复杂度是:O(N)的情况,比如特
殊的序列中所有元素都相同的情况,此时是没有办法改善性能的、
2.4、非递归版本:
快排递归版本和非递归版本不同之处又在于外层递归调用函数的不同,我们记非递归版本为QuickSortNon_Recursive,单趟快排函数我们就随便用一个,比如就用
前后指针法的单趟快排、
大体思路:利用一个栈,拿区间的左右下标,然后单趟排序得到keyi,再分别拿子区间的左右下标,单趟排序得keyi,再分别拿子区间的左右下标,反复缩小区间,
直到区间小到只有一个数或者是空,就不再往栈中放左右下标了,并且此时已经进行完了所有的单排,所以栈为空就结束即可、
栈区相对于堆区而言比较小,递归改为非递归有两种方式,如果递归比较简单,比如,阶乘,斐波那契数列,他们可以直接使用循环来代替递归,若递归比较复杂
时,比如二叉树的前中后序遍历,已知,层序遍历借助了栈这个数据结构,是因为层序遍历不可以使用递归,而前中后序遍历可以使用递归,但是若二叉树的深度
过大时,就会造成栈溢出,所以对于前中后序遍历,若改为非递归的话,则也要借助栈这个数据结构,比较难,到后面再进行讲解,在C++中再进行讲解,同理,
快速排序若改为非递归的话,则也要借助栈这个数据结构、
对于快速排序的递归版本而言,在递归过程中建立栈帧,而栈帧中主要是存储形参,返回值,局部变量等等,比如在递归版本中,栈帧中主要存储了a,begin,
end,keyi,这四个东西,其实就是存储了排序过程中所控制的区间,所以在递归改为非递归时,栈帧中主要存储了什么,在非递归中只要模拟什么就可以了,即,
递归版本的栈帧中存储了排序过程中所控制的区间,则只需要在非递归版本中模拟排序过程中所控制的区间即可,如果原数组元素个数非常多,这样的话,栈内存
储的数据就会非常多,所以栈的底层即顺序表就会不停地扩容,而顺序表中的扩容又是在堆区上动态开辟内存空间,这样的话,堆区会不会放不开呢,答案是不会
的,因为,栈区在Linux,32位下的大小在8M,而堆区在Linux,32位下的大小大约为2G,若是64位平台下,更没有问题,所以只需要记住,堆很大就行,暂且不
考虑动态开辟因堆区空间不够从而导致不成功的问题,当然, 如果动态开辟的空间非常非常多,堆区也是有可能放不开的,这时候就会出现,动态开辟内存失败,
但这里的数组元素个数在很大的情况下,我们入栈的数据都是每个子序列的头和尾位置的下标,一个子序列就入栈两个数据,所以在这里,即使数组元素个数非常
大,也不考虑入栈每个子序列头和尾位置的两个下标时会因堆区不够大从而导致的动态开辟内存空间失败,即入栈失败的情况、
如果某个题,非递归比较简单的话,比如,非递归可以使用循环,则优先写非递归,若某些题中,非递归比较难,则此时要考虑写成递归,,但是若非递归比较
难,理论上优先选择递归方法,但是对于递归方法而言有可能会出现栈溢出的情况下,即使非递归比较难,那也只能写成非递归的方法, 所以,当某个题中递归和
非递归都简单时,不管递归是否可能出现栈溢出,则都优先选择非递归,当递归难,非递归简单,不管递归是否可能出现栈溢出,则都优先选择非递归,则更优先
选择非递归,当递归简单,非递归难,在保证不出现栈溢出时优先选择递归,若出现栈溢出,即使非递归难,也得选择非递归,而当,递归和非递归都难时,若递
归可能出现栈溢出,则选择非递归,若递归不会出现栈溢出,两者随便选即可,递归并不比循环慢,尤其是在release版本优化下,一般情况下,递归改非递归主要
的原因就是因为,递归可能会出现栈溢出的问题,各个语言中的官方排序一般都是使用的两个优化后的快排,由于三数取中的存在,所以造成栈溢出的概率不大,
所以对于这个两个优化后的快排,官方使用的都是递归的方法、
对于非递归版本的快排,不考虑两个优化,模拟的是未优化的递归版本的快排,使用队列也是可以的,是广度优先,使用栈的话,则是深度优先,层序遍历就是广
度优先,所以层序遍历和这里的队列实现非递归快排相似,和栈实现非递归快排不相似,而使用栈实现非递归的话与递归版本更加相似,
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include"Stack.h"
//交换函数,传址调用、
void Swap(int*pa, int*pb)
{
assert(pa && pb);
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
//打印、
void PrintArray(int*a, int n)
{
assert(a);
for (int i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
//单趟排序->前后指针版本、
int PartSortPC(int*a, int left, int right)//一级指针传参,一级指针接收、
{
assert(a);
//选取数组首元素作为基准值key,使用变量keyi来记录基准值key的下标、
//在该版本内,不适合定义key,分析在下面,最好定义keyi,除此之外,可以既不定义key,也不定义keyi,因为,前后指针版本在不断改变的指针是prev和cur,并不是left和right,所以,当选取数组首元素作为基准值key时
//可以使用left在定义keyi的情况下代替keyi进行工作、
int keyi = left; //可省略、
int prev = left;
int cur = left + 1;
//当定义上面的变量prev和cur时,是不会造成越界访问的,因为,当在递归中凡是能执行到该PartSortPC函数时,则该函数的形参部分中的left一定小于right
//否则根本就执行不到该调用函数,所以,本次单趟排序中,至少存在两个数据,所以当定义两个变量prev和cur时,下面再使用这两个下标的时候,不会造成越界访问、
while (cur <= right)
{
//默认当变量prev和cur指向同一个数据时,不需要进行交换、
//升序,cur找小,若cur找到了小,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,若cur找到了大于等于key的值,则更不进入if语句、
//只有当cur找到了小,并且,++prev后和cur不是指向了同一个数据时,才进入if语句进行交换、
//此处直接写成 ++prev != cur 也是可以的,只有当prev==cur时,两者指向的数据的数值才是相等的,除此之外,不会存在两者指向不同的数据,但是两个数据的值是相等的情况
//即不会存在当prev不等于cur时,但他们所指元素的值是相等的情况,所以当prev不等于cur时,两者指向的元素的数值一定不相等、当prev等于cur时,两者指向的元素的数值一定相等、
if (a[cur] < a[keyi] && a[++prev] != a[cur]) //if (a[cur] < a[left] && a[++prev] != a[cur]) //if (a[cur] < a[keyi] && ++prev != cur) if (a[cur] < a[left] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
//此处会有三种情况,一是当cur找到的数据的数值大于等于key时,则cur继续找小,即不进入下面的if语句中,cur继续++找小、
//二是当cur找到的数据的数值小于key时,并且++prev后和cur不是指向了同一个数据时,则进入if语句进行交换,等if语句出来后,cur仍要进行++,继续往后找小、
//三是当cur找到的数据的数值小于key时,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,而cur仍要进行++,继续往后找小、
cur++;
降序,cur找大,若cur找到了大,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,若cur找到了小于等于key的值,则更不进入if语句、
只有当cur找到了大,并且,++prev后和cur不是指向了同一个数据时,才进入if语句进行交换、
此处直接写成 ++prev != cur 也是可以的,只有当prev==cur时,两者指向的数据的数值才是相等的,除此之外,不会存在两者指向不同的数据,但是两个数据的值是相等的情况
即不会存在当prev不等于cur时,但他们所指元素的值是相等的情况,所以当prev不等于cur时,两者指向的元素的数值一定不相等、当prev等于cur时,两者指向的元素的数值一定相等、
//if (a[cur] > a[keyi] && a[++prev] != a[cur]) //if (a[cur] > a[left] && a[++prev] != a[cur]) if (a[cur] > a[keyi] && ++prev != cur) if (a[cur] > a[left] && ++prev != cur)
//{
// Swap(&a[prev], &a[cur]);
//}
此处会有三种情况,一是当cur找到的数据的数值小于等于key时,则cur继续找大,即不进入下面的if语句中,cur继续++找大、
二是当cur找到的数据的数值大于key时,并且++prev后和cur不是指向了同一个数据时,则进入if语句进行交换,等if语句出来后,cur仍要进行++,继续往后找大、
三是当cur找到的数据的数值大于key时,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,而cur仍要进行++,继续往后找大、
//cur++;
}
//prev所指向的元素就是比基准值key小的值,此时再交换prev和keyi所指向的元素、
//prev所指向的元素就是比基准值key大的值,此时再交换prev和keyi所指向的元素、
Swap(&a[prev], &a[keyi]); //Swap(&a[prev], &a[left]);
return prev;
}
//快速排序->非递归版本->栈实现、
//非递归若使用栈实现,可以模拟递归的未优化过程,也可以模拟递归的优化过程,所谓模拟递归的未优化过程即指while循环中的单趟排序不加三数取中,所谓模拟递归的优化过程即指while循环中的单趟排序加上三数取中,此处以非递归模拟递归版本未优化为例、
//时间复杂度:通过思路进行分析,由于是在模拟递归未优化的过程,所以和递归未优化的时间复杂度是一样的,即最好情况下的部分时间复杂度是:O(N*logN),最坏情况下的部分时间复杂度是:O(N^2),则时间复杂度就是:O(N^2)、
//空间复杂度:通过思路进行分析,由于是在模拟递归未优化的过程,所以和递归未优化的空间复杂度是一样的,即最好情况下的部分空间复杂度是:O(logN),最坏情况下的部分空间复杂度是:O(N),则空间复杂度就是:O(N)、
//若非递归模拟递归版本优化过程则有:
//时间复杂度:通过思路进行分析,由于是在模拟递归优化的过程,所以和递归优化的时间复杂度是一样的,即最好情况下的部分时间复杂度是:O(N*logN),最坏情况下的部分时间复杂度是:O(N*logN),则时间复杂度就是:O(N*logN)、
//空间复杂度:通过思路进行分析,由于是在模拟递归优化的过程,所以和递归优化的空间复杂度是一样的,即最好情况下的部分空间复杂度是:O(logN),最坏情况下的部分空间复杂度是:O(logN),则空间复杂度就是:O(logN)、
//快速排序的非递归和递归思路是一样的,无非就是外层调用函数存在差异,非递归是用栈取下标划分小区间,递归就是调用自己来划分小区间,所以由于递归和非递归的思路是一样的,则两者的时间复杂度和空间复杂度也是一样的,未优化的快排的递归和非递归版本的
//时间复杂度都是O(N^2),优化后的快排的递归和非递归版本的时间复杂度都是O(NlogN),未优化的快排的递归和非递归版本的空间复杂度都是O(N),优化后的快排的递归和非递归版本的空间复杂度都是O(logN)、
void QuickSortNon_Recursive(int* a, int begin, int end)
{
assert(a);
//定义一个栈st、
ST st;
//初始化栈、
StackInit(&st);
//入栈第一个要处理的区间,入栈是先左后右,则出栈就是先右后左,因为栈的性质是先进后出、
StackPush(&st, begin);
StackPush(&st, end);
//当栈为空时,停止、
while (!StackEmpty(&st))
{
//入栈是先左后右,则出栈就是先右后左、
int right = StackTop(&st);
StackPop(&st);
int left = StackTop(&st);
StackPop(&st);
//以前后指针法为例->单趟排序、
int keyi = PartSortPC(a, left, right);
方法一:
根->右子树->左子树、
当子序列中只有一个或者不存在数据时,则不需要再进行排序、
//if (left < keyi - 1)
//{
// //与上面保持一致,先左后右入栈、
// StackPush(&st, left);
// StackPush(&st, keyi-1);
//}
当子序列中只有一个或者不存在数据时,则不需要再进行排序、
//if (keyi+1 < right)
//{
// //与上面保持一致,先左后右入栈、
// StackPush(&st, keyi + 1);
// StackPush(&st, right);
//}
//方法二:
//根->左子树->右子树---->前序、
//当子序列中只有一个或者不存在数据时,则不需要再进行排序、
if (keyi + 1 < right)
{
//与上面保持一致,先左后右入栈、
StackPush(&st, keyi + 1);
StackPush(&st, right);
}
//当子序列中只有一个或者不存在数据时,则不需要再进行排序、
if (left < keyi - 1)
{
//与上面保持一致,先左后右入栈、
StackPush(&st, left);
StackPush(&st, keyi - 1);
}
}
//销毁栈、
StackDestory(&st);
}
//快速排序->非递归版本->栈实现、
void TestQuickSortNon_Recursive()
{
int a[] = { 6, 1, 2, 7, 9, 3, 4, 5, 10, 8 };
//快速排序->非递归版本->栈实现、
QuickSortNon_Recursive(a, 0, sizeof(a) / sizeof(int)-1);
//打印、
PrintArray(a, sizeof(a) / sizeof(int));
}
int main()
{
//快速排序->非递归版本->栈实现、
TestQuickSortNon_Recursive();
return 0;
}