快速排序
前言:快排的应用场景十分广泛,面试中也是经常被考察到的排序方式之一。快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
基本方式
1.hoare版本
2.挖坑法
3.前后指针法
我们先介绍这三种基本方式:以下均以产生一个升序序列进行描述
1.hoare版本:
hoare方法的实现即为:在待排序序列中选取一个基准元素key(以下统称为key),在left<right
的情况下:遍历待排序序列,先在序列的最右边(下标为right)选取比key小的元素,然后在最左边(下标为left)选取比key大的元素,进而进行交换。当left>=right
时,即序列一趟排序完毕,再将key与left位置处(即相遇位置)的元素进行交换,此时,key的左边即为均小于key的元素,key的右边均为大于key的元素,序列一趟排序结束后返回key的位置(即下标left)作为下一次区间划分的标准。重复上述过程,直至所有元素均有序。
具体实现:
此处的基准元素key选取待排序序列的第一个元素(可以任意选取)
int PatrSort1(int* a, int left, int right)
{
//选取基准元素
int key = a[left];
int start = left;
//寻找大于、小于基准值的元素进行交换
//先从右边找小于key的值
while (left < right)
{
while (left < right && a[right] >= key)
--right;
//再从左边找大于k的值
while (left < right && a[left] <= key)
++left;
//进行交换
Swap(&a[left], &a[right]);
}
//key的位置确定:即相遇的位置
//将key的值和相遇位置处的元素进行交换
Swap(&a[start], &a[left]);
//返回left
return left;
}
举例:
2.挖坑法
挖坑法的实现即为:在待排序序列中选取一个key,在left<right
的情况下:遍历待排序序列,先在序列的最右边(下标为right)选取比key小的元素,放在下标为left位置处(填坑),此时right位置处则空缺出来(产生一个坑),再从序列的最左边(下标为left)选取比key的大的元素,放在下标为right的位置(填坑),此时left位置处则空缺出来(产生一个坑),不断重复直至left>=right
时,即序列一趟排序完毕。再将key的值放到left位置处(填坑)。 此时,key的左边即为均小于key的元素,key的右边均为大于key的元素,序列一趟排序结束后返回key的位置(即下标left)作为下一次区间划分的标准。重复上述过程,直至所有元素均有序。
具体实现:
此处的基准元素key选取待排序序列的第一个元素(可以任意选取)
int PartSort2(int* a, int left, int right)
{
int key = a[left];
while (left < right)
{
//从右边找小于k的元素
while (left < right && a[right] >= key)
--right;
//填坑
a[left] = a[right];
//从左边找大于k的元素
while (left < right && a[left] <= key)
++left;
//填坑
a[right] = a[left];
}
//存放key
//填坑
a[left] = key;
return left;
}
示例:
3.前后指针法
前后指针法:在待排序序列中选取一个key,第一个指针prev指向当前序列最后一个小于key的值,第二个指针cur指向prev+1的位置;在cur<=right
情况下,遍历序列,判断如果下一个小于k的位置与上一个小于k的位置不连续,则说明两者之间中间有大于k的值,进行交换,将数值大的元素向后移动,数值小的元素向前移动,重复此过程,直至cur>right。然后将prev位置处的元素与key进行交换。此时,key的左边为均小于key的元素,key的右边均为大于key的元素,序列一趟排序结束后返回key的位置(即下标left)作为下一次区间划分的标准。重复上述过程,直至所有元素均有序。
具体实现:
此处的基准元素key选取待排序序列的第一个元素(可以任意选取)
int PartSort3(int* a, int left, int right)
{
//当前序列最后一个小于K的值
int prev = left;
//当前序列下一个小于K的值
int cur = prev + 1;
int key = a[left];
while (cur <= right)
{
//如果下一个小于k的位置与上一个小于k的位置不连续
//说明中间有大于k的值,进行交换,大--->向后移动,小<---向前移动
if (a[cur] < key && ++prev != cur)
Swap(&a[cur], &a[prev]);
++cur;
}
Swap(&a[left], &a[prev]);
return prev;
}
示例:
快速排序的递归实现
进行一趟排序后,递归调用函数,不断缩小划分区间,进行排序。
void QuickSortRecursion(int* a, int left, int right)
{
if (left >= right)
return;
else
{
//hearo方法:
//int key = PatrSort1(a, left, right);
//挖坑法:
//int key = PatrSort2(a, left, right);
//前后指针法:
int key = PartSort3(a, left, right);
QuickSortRecursion(a, left, key - 1);
QuickSortRecursion(a, key + 1, right);
}
}
在数据较多的情况下,使用递归进行排序容易造成栈溢出的现象,所以我们更加推荐使用非递归去实现快速排序。
快速排序的非递归方式
非递归其实相当于模拟递归的实现,在这个过程中,由于我们要根据一趟排序返回的范围来划分区间,所以我们需要借助栈这个数据结构来保存我们的区间范围,每次从栈中获取区间的起始和结束。
typedef int DataType;
typedef struct Stack
{
DataType* _a;
size_t _top;//栈顶
size_t _capacity;//容量
}Stack;
void StackInit(Stack* st)
{
assert(st);
st->_a = NULL;
st->_capacity = st->_top = 0;
}
void StackDestory(Stack* st)
{
assert(st);
free(st->_a);
st->_capacity = st->_top = 0;
}
void StackPush(Stack* st,DataType x)
{
assert(st);
//入栈:顺序表的尾插
//检查容量
if (st->_top == st->_capacity)
{
DataType* p=st->_a;
size_t newcapacity = st->_capacity == 0 ? 10 : 2 * st->_capacity;
p = (DataType*)realloc(st->_a, newcapacity * sizeof(DataType));
if (p != NULL)
st->_a = p;
st->_capacity = newcapacity;
}
//插入
st->_a[st->_top++] = x;
}
void StackPop(Stack* st)
{
assert(st);
//出栈:顺序表尾删
if (st->_top > 0)
{
--st->_top;
}
}
DataType StackTop(Stack* st)
{
assert(st);
return st->_a[st->_top - 1];
}
size_t StackSize(Stack* st)
{
assert(st);
return st->_top;
}
int StackEmpty(Stack* st)
{
assert(st);
return st->_top == 0 ? 1 : 0;
}
//利用栈模拟快速排序递归的过程
void QuickSort(int* a, int left, int right)
{
Stack st;
StackInit(&st);
if (left < right)
{
//把右区间先压栈
StackPush(&st, right);
//把左区间压栈
StackPush(&st, left);
}
while (StackEmpty(&st) == 0)
{
//区间起始位置
int start = StackTop(&st);
StackPop(&st);
//区间结束位置
int end = StackTop(&st);
StackPop(&st);
//划分当前序列区间
int mid = PartSort3(a, start, end);
//划分左区间:左边元素个数大于1时进行划分
if (start < mid - 1)
{
StackPush(&st, mid - 1);
StackPush(&st, start);
}
//划分右区间:右边元素个数大于1时进行划分
if (end > mid + 1)
{
StackPush(&st, end);
StackPush(&st, mid + 1);
}
}
}
快排的优化
1.基准优化:快排的中心思想是根据基准元素不断划分区间进行排序,所以基准元素的选取最为重要,我们可以在最开始选取待排序序列中大小偏中间的元素作为每一次排序的基准值进行排序,进而保证了划分的空间不会导致出现左右某个区间为空的现象,保证了每次划分的均衡性。
在这里我们使用三数取中法:每次在当前待排序序列中的第一个元素、最后一个元素和中间位置处的元素中选取值排中间的元素与第一个元素进行交换,以交换后的第一个元素为基准进行排序。
三数取中法具体实现:
int getMid(int* a, int left, int right)
{
int mid = left + (right - left) / 2;
if (a[mid] > a[left])
{
if (a[mid] < a[right])
return mid;
else
{
//mid>left;mid>right
if (a[left] > a[right])
return left;
else
return right;
}
}
else
{
//mid<=left
if (a[left] < a[right])
return left;
else
{
if (a[mid] > a[right])
return mid;
else
return right;
}
}
}
2.区间优化:当区间较小时,选用插入排序,不再进行递归。
快速排序的性能分析
时间复杂度:O(NlogN)~O(NN);
空间复杂度:O(logN);
稳定性:不稳定(如有相同元素,排序前后相同元素的相对位置已发生改变)。