快速排序
快速排序是一种二叉树结构交换排序算法,在日常的使用当中较为频繁的使用;
交换的思想就是 :就是将序列中的两个值值进行比较,然后根据结果交换两个值在序列中的位置,以升序为例,较大值就想序列尾部移动,较小值向序列前部移动;
快排的思想就是:在待排序的元素当中选择一个元素为基准值,按照该排序要求将序列分为两个子序列,左边子序列元素均小于基准值(左右子序列均不包含该基准值),右边子序列均大于基准值,然后左右子序列重复上述过程,直到所有元素排列到对应位置为止;
将区间按照基准值分为左右两部分的方式有:
- 左右指针法(Hoare版本)
- 前后指针法
- 挖坑法
对方法逐个实现:
左右指针法:
注意:以右边为基准值,左边先走,以左边为基准值,右边先走;
int PartSort1(int* arr,int left,int right)//左右指针法
{
//优化
int mid = GetMindIndex(arr, left, right);
Swap(&arr[mid], &arr[right]);
int key = arr[right]; // 选择基准值
int key_index = right;
//升序 left找大 , right找小
//降序 left找小 , right找大
while(left < right)
{
while(left < right && arr[left] <= key)
{
++left;
}
while(left < right && arr[right] >= key)
{
--right;
}
//各自找到时,进行交换
Swap(&arr[left],&arr[right]);
}
//左后left和right相遇后,将基准值与当前位置交换
Swap(&arr[left],&arr[key_index]);
return left;
}
前后指针法:
注意:prev要么就在cur的前一位,要么和cur相隔一段距离,prev始终指向相对当前cur位置前一个小于key的值的位置,如果相隔一段距离,则prev下一位就是大于key的,此时交换cur和prev;
int PartSort2(int* arr,int left,int right)
{
//优化
int mid = GetMindIndex(arr, left, right);
Swap(&arr[mid], &arr[right]);
int cur = left;
int prev = left -1;
int key = arr[right]//基准值
while(cur < right)
{
if(arr[cur] < key && ++prev != cur)
{
Swap(&arr[prev],&arr[cur]);
}
++cur;
}
++prev;
Swap(&arr[prev],&arr[right]);
return prev;
}
挖坑法:
注意:最后注意要填坑
int PartSort3(int* arr,int left,int right)
{
//优化
int mid = GetMindIndex(arr, left, right);
Swap(&arr[mid], &arr[right]);
int key = arr[right];//基准值
while(left < right)
{
while(left < right && arr[left] <= key)
{
++left;
}
Swap(&arr[left],&arr[right]);//填右边的坑
while(left < right && arr[right] >= key)
{
--right;
}
Swap(&arr[right],&arr[left]);//填左边的坑
}
//left = right两者相遇,左后用基准值填坑,序列划分为两个子序列
arr[left] = key;//填坑
return left;
}
快速排序实现:
注意:将待排序序列不断进行递归划分左右子序列,知道所有元素排序到位为止
void QucikSort(int* arr,int n)
{
if(left >= right)
return ;
int key_index = PartSort1(arr,left,right);
QucikSort(arr,left,key_index-1);
QucikSort(arr,key_index+1,right);
}
复杂度总结:
根据快速排序是一种二叉树型结构,可知:二叉树有lgN层,总共有那N个元素
时间复杂度 O(N*logN)
空间复杂度 O(logN)
稳定性 : 不稳定
快速排序优化
我们知道,快速排序是一种二叉树结构的交换排序算法,可知当要排序的序列是有序的,那么二叉树就会退化成单支树,那么排序的效率将会大大降低,那么快速排序就没有什么优势可言,所以我们将快速排序进行优化,
方法一:三数取中法
方法二:递归到小的子区间时,可以考虑使用插入排序
三数取中法:用于上面三种方法中
注意:该方法主要避免形成单支树这种情况
int GetMindIndex(int* arr,int left,int right)
{
int mid = left + (right - left)/2;
if(arr[left] < a[mid]) // left < mid
{
if (a[mid] < a[right]) //left < mid <right
return mid;
else if (a[left] > a[right]) // right < left < mid
return left;
else
return right;//left < right < mid
}
else //left > mid
{
if (a[right] < a[mid]) right < mid < left
return mid;
else if (a[right] < a[left]) mid < left < right
return left;
else
return right;mid < right < left
}
}
递归到小的子区间时,可以考虑使用插入排序:
当递归到小区间时,如果元素已经接近有序,但是还有快排的话,递归下去反而降低了效率,这是数据呈现接近有序和数据量小的特性,使用直接插入更加合适,就能提高效率;也是主要针对待排序序列为有序序列这种情况
优化后的快速排序:
void QucikSort(int* arr,int left,int right)
{
if(left >= right)
return ;
if(right - left + 1 > 10)
{
int key_index = PartSort1(arr,left,right);
QucikSort(arr,left,key_index-1);
QucikSort(arr,key_index+1,right);
}
else//优化方法二
{
InsertSort(arr, (right - left + 1));//自己实现
}
}
非递归的快速排序
尽管上面的快排我们已经进行了优化,但是他执行还是以递归的方式执行,但是递归有一个很重要的缺点就是栈空间溢出的问题,如果待排序的数据量很大,那么很容易造成栈溢出,反而很麻烦。因此我们可以将递归改为非递归,这样就避免了递归栈溢出的问题,这样快速排序就更加完善了!!!!
思路: 可知,递归调用不断的划分待排序序列,将问题从大化小,我们可以使用一种数据结构来模拟函数调用时栈帧这种情况-------栈,利用栈的先进后出原理就和递归相似,不断将分组后的边界位置存入栈中,然后取出进行排序知道栈为空为止;
#include<stack> //C语言下需要自己实现
void QucikSort(int* arr,int left,int right)
{
stack<int> st;
st.push(left);
st.push(right);
while(!st.empty())
{
int end = st.top();
st.pop();
int begin = st.top();
st.pop();
int mid = PartSort1(arr, begin, end);
//int mid = PartSort2(arr, begin, end);
//int mid = PartSort3(arr, begin, end);
if (begin < mid - 1)
{
st.push(begin);
st.push(mid - 1);
}
if (mid + 1 < end)
{
st.push(mid + 1);
st.push(end);
}
}
}
到此快速排序介绍完了,但是要理解:"物理上:我们操作的是一个数组,逻辑上我们操作的是一个二叉树结构"