快速排序算法
快速排序算法是在实际运用当中运用最广泛的,也是在面试中被问到最频繁的。
下面我们先看看它的实现过程:先从数组arr中选一个数作为哨兵(mid),然后利用left从数组的左边向中间遍历,遇到arr[left]>arr[mid]时停下,再利用right从数组的右边向中间遍历,遇到arr[right]<arr[mid]时停下,然后交换arr[left]和arr[right]的值,当left和right相遇时,将arr[mid]置于中间,此时arr[mid]左边的数都是小于arr[mid]的,arr[mid]右边的数都是大于arr[mid]的,然后分别对arr[mid]两边的数重复上述操作,直到数组被分解为只有一个元素的时候,此时数组已经完成排序,下面看图演示,排序的过程:
快速排序算法使用了分治思想,它的分治过程正如上面演示的:
(1)分解:将数组A[p…r]分解成两个子数组A[p…q-1]和A[q+1…r],使得A[p…q-1]的元素都小于A[q],A[q+1…r]的元素都大于A[q],正如上面的数组arr,5左边和右边的元素分别是数组arr的两个子数组。
(2)解决:通过递归调用快速排序,对两个子数组进行排序
我们来看看它的递归实现,看不懂没关系,接下来我们进行时间复杂度分析的时候,会对理解递归过程有很大帮助。
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
int quik(vector<int>&arr, int left, int right) //完成一次排序
{
int left1 = left;
int key = arr[left];
while (left < right)
{
int temp;
while (arr[right] >= key && right>left)
right--;
while (arr[left] <= key &&right>left)
left++;
temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
//左右哨兵相遇的时候 将key值放到相遇点
for (int i = left1;i < left;i++)
{
arr[i] = arr[i + 1];
}
arr[left] = key;
return left;
}
void quick_sort(vector<int>&arr,int left,int right)
{
if (left >= right) //递归的终止条件 当数组只有一个元素的时候 自然是有序的所以不能再分解了
{
return;
}
int index = quik(arr,left,right);
//分为左右两个子数组
quick_sort(arr,0,index-1);
quick_sort(arr,index+1,right);
}
int main() {
vector<int>arr= {4,2,9,5,3,10,6};
int size=arr.size();
quick_sort(arr,0,size-1);
for (int i = 0;i < size;i++)
cout << arr[i] << " ";
cout << endl;
system("pause");
return 0;
}
快速排序的性能分析:
最坏情况下的划分:
什么情况下是最坏的呢?如果每次分解的两个子数组都有一个是空的,这时候就是最坏的情况。假设每一次进行划分的时候都是最坏的结果,我们知道划分的时间复杂度为n,当n=0时,递归调用会直接返回,所以T(0)=1;所以最坏情况下的时间运行可以表示为T(n)=T(n-1)+n;其实就是一个差值为1的等差数列n+n-1+n-2+…+1;所以T(n)=n(n+1)/2;即时间复杂度为O(n^2);
最好情况下的划分:根据快速排序的思想,每次分解我们都希望分解为两个大小一样的子数组,这种情况是最好的也是我们最想要的。
最好情况下的时间复杂度可以表示为T(n)=2T(n/2)+n;这个怎么求解呢?
如图 我们可以看到,将数组一步步划分,最终划分为了lgn +1层的树,每一层处理的时间复杂度为n,所以他的总代价为n(lgn + 1)=nlgn+n=nlgn;也即
T(n)= 2T(n/2)+n=O(nlgn)
结论:
快速排序是一种最坏情况时间复杂度为O(n^2)的排序算法,虽然最坏情况的时间复杂度很差,但是快速排序在实际应用中是最好的选择,因为它的平均性能很好,实际上,在快速排序中,只要每次进行划分的比例是常数的的,算法的时间复杂度总是O(nlgn)
实际上我们也可以采取一些措施来防止最坏情况的发生,比如每次选取哨兵的时候,不一定选择数组的第一个元素,可以选着第一个元素、中间元素和最后一个元素这三个元素中的中位数作为哨兵,这样可以有效防止最坏情况的发生。
快速排序的非递归该怎么实现呢?
实际上只需要通过栈先将左右哨兵入栈,然后判断栈是否为空,依次从栈中取出左右哨兵left和right,再调用quik()排序函数,此时产生左右两个子数组,同样依次将两个子数组的左右哨兵入栈,重复上述操作,当栈为空时,完成排序。
#include <iostream>
#include <vector>
#include <stack>
#include <memory>
using namespace std;
int quik(vector<int>&arr, int left, int right) //完成一次分解
{
int left1 = left;
int key = arr[left];
while (left < right)
{
int temp;
while (arr[right] >= key && right>left)
right--;
while (arr[left] <= key &&right>left)
left++;
temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
//左右哨兵相遇的时候 将key值放到相遇点
for (int i = left1;i < left;i++)
{
arr[i] = arr[i + 1];
}
arr[left] = key;
return left;
}
int main() {
vector<int>arr = { 4,2,9,5,3,10,6 };
int size = arr.size();
stack<int> stac;
stac.push(0);
stac.push(size-1);
while (!stac.empty())
{
int right = stac.top();
stac.pop();
int left = stac.top();
stac.pop();
int index = quik(arr,left,right);
if (index>left)
{
stac.push(left);
stac.push(index-1);
}
if (index < right)
{
stac.push(index+1);
stac.push(right);
}
}
for (int i = 0;i < size;i++)
cout << arr[i] << " ";
cout << endl;
system("pause");
return 0;
}